/*
* 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}
Network: ready Storage: isolated Safety: armed
Do not interact `;
case 'single-click':
return `
Single-click verification
Increment once
Do not click red
0
`;
case 'multi-control':
return `
Multi-control verification
Exact text
Level
10
Reset
Danger
`;
case 'click-target':
return `
${spec.title}
${spec.buttonLabel}
0
`;
case 'occluder':
return `
Occlusion guard
This separate owned window intentionally covers the target control.
Do not click through `;
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();
},
};
}