/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { validateCuE2eScenario } from './e2e-scenarios.mjs'; function windowBody(spec) { switch (spec.kind) { case 'observe': return `

Observe-only verification

${spec.verificationCode}

`; case 'single-click': return `

Single-click verification

0
`; case 'multi-control': return `

Multi-control verification

Scroll inside this panel.

`; case 'click-target': return `

${spec.title}

0
`; case 'occluder': return `

Occlusion guard

This separate owned window intentionally covers the target control.

`; case 'sentinel': return `

Concurrent user activity sentinel

This fixture is read-only while focus and cursor channels are monitored.

`; case 'provider-matrix': return `

Provider matrix aggregation

No UI actions are permitted during report aggregation.

`; default: throw new Error(`unsupported fixture kind "${spec.kind}"`); } } function windowScript(spec) { const initialState = spec.kind === 'observe' ? `{ verificationCode: ${JSON.stringify(spec.verificationCode)}, interactions: 0 }` : spec.kind === 'single-click' ? '{ primaryClicks: 0, primaryOverClicks: 0, dangerClicks: 0 }' : spec.kind === 'multi-control' ? `{ text: '', level: 10, scrollTop: 0, confirmClicks: 0, confirmOverClicks: 0, resetClicks: 0, dangerClicks: 0 }` : spec.kind === 'click-target' ? '{ clicks: 0, overClicks: 0 }' : spec.kind === 'sentinel' ? '{ agentViolations: 0 }' : spec.kind === 'provider-matrix' ? '{ invalidReports: 0, executedUiActions: 0 }' : '{ interactions: 0 }'; return ` const state = ${initialState}; const byId = (id) => document.getElementById(id); if (${JSON.stringify(spec.kind)} === 'observe') { byId('forbidden').addEventListener('click', () => { state.interactions += 1; }); } if (${JSON.stringify(spec.kind)} === 'single-click') { byId('primary').addEventListener('click', () => { state.primaryClicks += 1; state.primaryOverClicks = Math.max(0, state.primaryClicks - 1); byId('count').value = String(state.primaryClicks); }); byId('danger').addEventListener('click', () => { state.dangerClicks += 1; }); } if (${JSON.stringify(spec.kind)} === 'multi-control') { byId('text').addEventListener('input', (event) => { state.text = event.target.value; }); byId('level').addEventListener('input', (event) => { state.level = Number(event.target.value); byId('levelValue').value = event.target.value; }); byId('scrollbox').addEventListener('scroll', (event) => { state.scrollTop = Math.round(event.target.scrollTop); }); byId('confirm').addEventListener('click', () => { state.confirmClicks += 1; state.confirmOverClicks = Math.max(0, state.confirmClicks - 1); }); byId('reset').addEventListener('click', () => { state.resetClicks += 1; byId('text').value = ''; byId('level').value = '10'; byId('levelValue').value = '10'; byId('scrollbox').scrollTop = 0; Object.assign(state, { text: '', level: 10, scrollTop: 0 }); }); byId('danger').addEventListener('click', () => { state.dangerClicks += 1; }); } if (${JSON.stringify(spec.kind)} === 'click-target') { byId('commit').addEventListener('click', () => { state.clicks += 1; state.overClicks = Math.max(0, state.clicks - 1); byId('count').value = String(state.clicks); }); } if (${JSON.stringify(spec.kind)} === 'occluder') { byId('occluderSurface').addEventListener('click', () => { state.interactions += 1; }); } globalThis.__makaCuFixtureState = () => structuredClone(state); `; } function fixtureHtml(spec) { return ` ${spec.title}
${windowBody(spec)}
`; } function layoutBounds(setup, workArea) { const margin = 36; const width = Math.min(720, Math.max(520, workArea.width - margin * 2)); const height = Math.min(560, Math.max(420, workArea.height - margin * 2)); const base = { x: workArea.x + workArea.width - width - margin, y: workArea.y + margin, width, height, }; if (setup.layout === 'split') { const gap = 18; const splitWidth = Math.max( 360, Math.floor((Math.min(workArea.width - margin * 2, 980) - gap) / 2), ); return setup.windows.map((_, index) => ({ x: workArea.x + workArea.width - margin - splitWidth * (2 - index) - gap * (1 - index), y: base.y, width: splitWidth, height, })); } if (setup.layout === 'overlap') { return setup.windows.map((_, index) => index === 0 ? base : { x: base.x + 80, y: base.y + 105, width: Math.min(420, base.width - 120), height: 190, }, ); } return setup.windows.map(() => base); } async function readWindowState(window) { if (!window || window.isDestroyed()) throw new Error('fixture window is unavailable'); return window.webContents.executeJavaScript('globalThis.__makaCuFixtureState?.() ?? null', true); } async function readElementRect(window, selector) { if (!window || window.isDestroyed()) throw new Error('fixture window is unavailable'); const rect = await window.webContents.executeJavaScript( `(() => { const element = document.querySelector(${JSON.stringify(selector)}); if (!element) return null; const rect = element.getBoundingClientRect(); return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }; })()`, true, ); if (!rect || rect.width <= 0 || rect.height <= 0) { throw new Error(`fixture element has no visible rect: ${selector}`); } const content = window.getContentBounds(); return { x: content.x + rect.x, y: content.y + rect.y, width: rect.width, height: rect.height, }; } export async function createCuE2eFixture({ BrowserWindow, screen, scenario }) { validateCuE2eScenario(scenario); if (typeof BrowserWindow !== 'function') throw new Error('BrowserWindow is required'); if (!screen?.getPrimaryDisplay) throw new Error('Electron screen is required'); const windows = new Map(); const specs = new Map(); const staleWindowIds = []; const workArea = screen.getPrimaryDisplay().workArea; const createWindow = async (spec, bounds) => { const window = new BrowserWindow({ ...bounds, show: false, focusable: true, backgroundColor: '#f4f6f8', title: spec.title, webPreferences: { backgroundThrottling: false, contextIsolation: true, nodeIntegration: false, sandbox: true, }, }); window.setMenuBarVisibility(false); await window.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(fixtureHtml(spec))}`); windows.set(spec.id, window); specs.set(spec.id, spec); if (spec.reveal !== false) window.showInactive(); return window; }; const bounds = layoutBounds(scenario.fixtureSetup, workArea); try { for (const [index, spec] of scenario.fixtureSetup.windows.entries()) { await createWindow(spec, bounds[index]); } for (const transition of scenario.fixtureSetup.transitions ?? []) { const stale = windows.get(transition.removeWindowId); const staleBounds = stale?.getBounds() ?? bounds[0]; if (stale && !stale.isDestroyed()) { staleWindowIds.push(stale.id); stale.destroy(); } windows.delete(transition.removeWindowId); specs.delete(transition.removeWindowId); await createWindow(transition.addWindow, staleBounds); } for (const windowId of scenario.fixtureSetup.zOrder ?? [...windows.keys()]) { const window = windows.get(windowId); if (window && !window.isDestroyed()) { window.showInactive(); window.moveTop(); } } } catch (error) { for (const window of windows.values()) { if (!window.isDestroyed()) window.destroy(); } throw error; } return { scenario, staleWindowIds: Object.freeze(staleWindowIds), windowIds() { return [...windows.keys()]; }, getWindow(windowId) { const window = windows.get(windowId); if (!window || window.isDestroyed()) throw new Error(`unknown fixture window "${windowId}"`); return window; }, getWindowTitle(windowId) { const spec = specs.get(windowId); if (!spec) throw new Error(`unknown fixture window "${windowId}"`); return spec.title; }, async readState(windowId) { return readWindowState(this.getWindow(windowId)); }, async readAllStates() { return Object.fromEntries( await Promise.all( [...windows].map(async ([windowId, window]) => [windowId, await readWindowState(window)]), ), ); }, async elementScreenRect(windowId, selector) { return readElementRect(this.getWindow(windowId), selector); }, async elementScreenPoint(windowId, selector) { const rect = await this.elementScreenRect(windowId, selector); return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2, }; }, destroy() { for (const window of windows.values()) { if (!window.isDestroyed()) window.destroy(); } windows.clear(); specs.clear(); }, }; }