Extend browser capture parking harness
This commit is contained in:
@@ -19,6 +19,15 @@ Run it with the repo Electron:
|
|||||||
npm run capture-harness --workspace=@getpaseo/desktop
|
npm run capture-harness --workspace=@getpaseo/desktop
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On macOS the harness process must set `app.setActivationPolicy("accessory")` and
|
||||||
|
hide the Dock icon before creating any window. `showInactive()` only prevents window
|
||||||
|
focus; a normal Electron app launch can still activate the app and steal focus.
|
||||||
|
Harness windows are then created hidden, positioned in a screen corner, skipped from
|
||||||
|
the taskbar where Electron supports it, and revealed with `showInactive()` from
|
||||||
|
`ready-to-show`. Do not replace this with `show()`, `focus()`, or `app.focus()`:
|
||||||
|
the compositor only needs visible inactive windows, and harness runs must not steal
|
||||||
|
focus from the person using the machine.
|
||||||
|
|
||||||
The harness writes PNG evidence and `results.json` to:
|
The harness writes PNG evidence and `results.json` to:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -70,5 +70,20 @@
|
|||||||
<div class="sub">LOCAL MAGENTA TARGET 1280x800 VIEWPORT</div>
|
<div class="sub">LOCAL MAGENTA TARGET 1280x800 VIEWPORT</div>
|
||||||
<div class="block"></div>
|
<div class="block"></div>
|
||||||
<div class="bottom">FULL PAGE MARKER AT Y=1370</div>
|
<div class="bottom">FULL PAGE MARKER AT Y=1370</div>
|
||||||
|
<script>
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const label = params.get("label");
|
||||||
|
const sub = params.get("sub");
|
||||||
|
const bottom = params.get("bottom");
|
||||||
|
if (label) {
|
||||||
|
document.querySelector(".label").textContent = label;
|
||||||
|
}
|
||||||
|
if (sub) {
|
||||||
|
document.querySelector(".sub").textContent = sub;
|
||||||
|
}
|
||||||
|
if (bottom) {
|
||||||
|
document.querySelector(".bottom").textContent = bottom;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -43,10 +43,16 @@
|
|||||||
const RESIDENT_VIEWPORT_HEIGHT = 800;
|
const RESIDENT_VIEWPORT_HEIGHT = 800;
|
||||||
const host = document.getElementById("paseo-browser-resident-webviews");
|
const host = document.getElementById("paseo-browser-resident-webviews");
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const webviewCount = Math.max(2, Number(params.get("webviewCount")) || 2);
|
const requestedWebviewCount = params.has("webviewCount")
|
||||||
|
? Number(params.get("webviewCount"))
|
||||||
|
: 2;
|
||||||
|
const webviewCount = Number.isFinite(requestedWebviewCount)
|
||||||
|
? Math.max(0, requestedWebviewCount)
|
||||||
|
: 2;
|
||||||
const webviews = [];
|
const webviews = [];
|
||||||
const activeTokens = new Set();
|
const activeTokens = new Set();
|
||||||
let nextTokenId = 0;
|
let nextTokenId = 0;
|
||||||
|
let permanentParkingState = params.get("permanentParkingState") || "";
|
||||||
|
|
||||||
function appendHarnessWebview(sourceUrl) {
|
function appendHarnessWebview(sourceUrl) {
|
||||||
const index = webviews.length;
|
const index = webviews.length;
|
||||||
@@ -112,6 +118,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyParking() {
|
function applyParking() {
|
||||||
|
permanentParkingState = "";
|
||||||
applyHostParking();
|
applyHostParking();
|
||||||
webviews.forEach((webview) => {
|
webviews.forEach((webview) => {
|
||||||
applyStackedWebviewStyle(webview);
|
applyStackedWebviewStyle(webview);
|
||||||
@@ -126,6 +133,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyCapturePrep() {
|
function applyCapturePrep() {
|
||||||
|
permanentParkingState = "";
|
||||||
host.style.position = "fixed";
|
host.style.position = "fixed";
|
||||||
host.style.left = "0";
|
host.style.left = "0";
|
||||||
host.style.top = "0";
|
host.style.top = "0";
|
||||||
@@ -140,6 +148,99 @@
|
|||||||
host.style.transform = "";
|
host.style.transform = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyHostPermanentBase() {
|
||||||
|
host.style.position = "fixed";
|
||||||
|
host.style.left = "0";
|
||||||
|
host.style.top = "0";
|
||||||
|
host.style.width = `${RESIDENT_VIEWPORT_WIDTH}px`;
|
||||||
|
host.style.height = `${RESIDENT_VIEWPORT_HEIGHT}px`;
|
||||||
|
host.style.overflow = "hidden";
|
||||||
|
host.style.opacity = "1";
|
||||||
|
host.style.pointerEvents = "none";
|
||||||
|
host.style.zIndex = "1";
|
||||||
|
host.style.clipPath = "";
|
||||||
|
host.style.visibility = "";
|
||||||
|
host.style.transform = "";
|
||||||
|
host.style.transformOrigin = "0 0";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPermanentWebviewBase(webview, index) {
|
||||||
|
applyStackedWebviewStyle(webview);
|
||||||
|
webview.style.opacity = "1";
|
||||||
|
webview.style.transform = "";
|
||||||
|
webview.style.pointerEvents = "none";
|
||||||
|
webview.style.zIndex = "0";
|
||||||
|
webview.style.width = `${RESIDENT_VIEWPORT_WIDTH}px`;
|
||||||
|
webview.style.height = `${RESIDENT_VIEWPORT_HEIGHT}px`;
|
||||||
|
webview.style.left = "0";
|
||||||
|
webview.style.top = "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPermanentParkingState(stateName) {
|
||||||
|
permanentParkingState = stateName;
|
||||||
|
activeTokens.clear();
|
||||||
|
applyHostPermanentBase();
|
||||||
|
webviews.forEach(applyPermanentWebviewBase);
|
||||||
|
|
||||||
|
if (stateName === "p1-overflow-1x1") {
|
||||||
|
host.style.width = "1px";
|
||||||
|
host.style.height = "1px";
|
||||||
|
host.style.overflow = "hidden";
|
||||||
|
return state();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateName === "p2-clip-path-1x1") {
|
||||||
|
host.style.width = "1px";
|
||||||
|
host.style.height = "1px";
|
||||||
|
host.style.overflow = "visible";
|
||||||
|
host.style.clipPath = "inset(1px)";
|
||||||
|
return state();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateName === "p3-opacity-0") {
|
||||||
|
host.style.opacity = "0";
|
||||||
|
return state();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateName === "p4-transform-scale-0") {
|
||||||
|
host.style.transform = "scale(0)";
|
||||||
|
return state();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateName === "p4-transform-scale-0001") {
|
||||||
|
host.style.transform = "scale(0.001)";
|
||||||
|
return state();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateName === "p5-webview-0x0") {
|
||||||
|
webviews.forEach((webview) => {
|
||||||
|
webview.style.width = "0";
|
||||||
|
webview.style.height = "0";
|
||||||
|
});
|
||||||
|
return state();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateName === "p5-webview-1x1") {
|
||||||
|
webviews.forEach((webview) => {
|
||||||
|
webview.style.width = "1px";
|
||||||
|
webview.style.height = "1px";
|
||||||
|
});
|
||||||
|
return state();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateName === "p6-z-index-negative") {
|
||||||
|
host.style.zIndex = "-1";
|
||||||
|
return state();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateName === "p7-opacity-001") {
|
||||||
|
host.style.opacity = "0.01";
|
||||||
|
return state();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown permanent parking state: ${stateName}`);
|
||||||
|
}
|
||||||
|
|
||||||
function applyTargetStacking(targetIndex) {
|
function applyTargetStacking(targetIndex) {
|
||||||
webviews.forEach((webview, index) => {
|
webviews.forEach((webview, index) => {
|
||||||
applyStackedWebviewStyle(webview, index === targetIndex ? "2" : "0");
|
applyStackedWebviewStyle(webview, index === targetIndex ? "2" : "0");
|
||||||
@@ -162,7 +263,7 @@
|
|||||||
function state(targetIndex = 0) {
|
function state(targetIndex = 0) {
|
||||||
const hostRect = host.getBoundingClientRect();
|
const hostRect = host.getBoundingClientRect();
|
||||||
const targetWebview = webviews[targetIndex] || webviews[0];
|
const targetWebview = webviews[targetIndex] || webviews[0];
|
||||||
const webviewRect = targetWebview.getBoundingClientRect();
|
const webviewRect = targetWebview?.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
hostStyle: {
|
hostStyle: {
|
||||||
left: host.style.left,
|
left: host.style.left,
|
||||||
@@ -172,6 +273,9 @@
|
|||||||
overflow: host.style.overflow,
|
overflow: host.style.overflow,
|
||||||
opacity: host.style.opacity,
|
opacity: host.style.opacity,
|
||||||
pointerEvents: host.style.pointerEvents,
|
pointerEvents: host.style.pointerEvents,
|
||||||
|
zIndex: host.style.zIndex,
|
||||||
|
clipPath: host.style.clipPath,
|
||||||
|
transform: host.style.transform,
|
||||||
},
|
},
|
||||||
hostRect: {
|
hostRect: {
|
||||||
x: hostRect.x,
|
x: hostRect.x,
|
||||||
@@ -180,10 +284,10 @@
|
|||||||
height: hostRect.height,
|
height: hostRect.height,
|
||||||
},
|
},
|
||||||
webviewRect: {
|
webviewRect: {
|
||||||
x: webviewRect.x,
|
x: webviewRect?.x ?? 0,
|
||||||
y: webviewRect.y,
|
y: webviewRect?.y ?? 0,
|
||||||
width: webviewRect.width,
|
width: webviewRect?.width ?? 0,
|
||||||
height: webviewRect.height,
|
height: webviewRect?.height ?? 0,
|
||||||
},
|
},
|
||||||
webviews: webviews.map((webview) => {
|
webviews: webviews.map((webview) => {
|
||||||
const rect = webview.getBoundingClientRect();
|
const rect = webview.getBoundingClientRect();
|
||||||
@@ -191,6 +295,7 @@
|
|||||||
id: webview.id,
|
id: webview.id,
|
||||||
zIndex: webview.style.zIndex,
|
zIndex: webview.style.zIndex,
|
||||||
position: webview.style.position,
|
position: webview.style.position,
|
||||||
|
opacity: webview.style.opacity,
|
||||||
x: rect.x,
|
x: rect.x,
|
||||||
y: rect.y,
|
y: rect.y,
|
||||||
width: rect.width,
|
width: rect.width,
|
||||||
@@ -213,6 +318,16 @@
|
|||||||
addWebview(sourceUrl) {
|
addWebview(sourceUrl) {
|
||||||
return appendHarnessWebview(sourceUrl || params.get("targetUrl") || "bright.html");
|
return appendHarnessWebview(sourceUrl || params.get("targetUrl") || "bright.html");
|
||||||
},
|
},
|
||||||
|
addPermanentWebview(sourceUrl, stateName) {
|
||||||
|
const index = appendHarnessWebview(sourceUrl || params.get("targetUrl") || "bright.html");
|
||||||
|
applyPermanentParkingState(stateName || permanentParkingState);
|
||||||
|
return index;
|
||||||
|
},
|
||||||
|
async applyPermanentParkingState(stateName) {
|
||||||
|
const nextState = applyPermanentParkingState(stateName);
|
||||||
|
await waitFrames(2);
|
||||||
|
return nextState;
|
||||||
|
},
|
||||||
async prepareForPixelCapture(targetIndex = 0) {
|
async prepareForPixelCapture(targetIndex = 0) {
|
||||||
const token = `capture-${++nextTokenId}`;
|
const token = `capture-${++nextTokenId}`;
|
||||||
activeTokens.add(token);
|
activeTokens.add(token);
|
||||||
@@ -252,6 +367,10 @@
|
|||||||
return state();
|
return state();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (permanentParkingState) {
|
||||||
|
applyPermanentParkingState(permanentParkingState);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const fs = require("node:fs");
|
const fs = require("node:fs");
|
||||||
const fsp = require("node:fs/promises");
|
const fsp = require("node:fs/promises");
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const { app, BrowserWindow, nativeImage } = require("electron");
|
const { app, BrowserWindow, nativeImage, screen } = require("electron");
|
||||||
|
|
||||||
const ROOT = __dirname;
|
const ROOT = __dirname;
|
||||||
const OUT_DIR = process.env.PASEO_CAPTURE_HARNESS_OUT_DIR || path.join(ROOT, "out");
|
const OUT_DIR = process.env.PASEO_CAPTURE_HARNESS_OUT_DIR || path.join(ROOT, "out");
|
||||||
@@ -11,15 +11,155 @@ const FULL_PAGE_HEIGHT = 1600;
|
|||||||
const CAPTURE_TIMEOUT_MS = 5000;
|
const CAPTURE_TIMEOUT_MS = 5000;
|
||||||
const CAPTURE_RETRY_INTERVAL_MS = 200;
|
const CAPTURE_RETRY_INTERVAL_MS = 200;
|
||||||
const REPEAT_COUNT = 5;
|
const REPEAT_COUNT = 5;
|
||||||
|
const FRESH_REPEAT_COUNT = 3;
|
||||||
|
const SOAK_MS = Number(process.env.PASEO_CAPTURE_HARNESS_SOAK_MS || 75000);
|
||||||
|
const HARNESS_GROUP = process.env.PASEO_CAPTURE_HARNESS_GROUP || "all";
|
||||||
|
const PERMANENT_STATE_FILTER = new Set(
|
||||||
|
(process.env.PASEO_CAPTURE_HARNESS_STATES || "")
|
||||||
|
.split(",")
|
||||||
|
.map((state) => state.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
const PERMANENT_VARIANT_FILTER = new Set(
|
||||||
|
(process.env.PASEO_CAPTURE_HARNESS_VARIANTS || "")
|
||||||
|
.split(",")
|
||||||
|
.map((variant) => variant.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
const PERMANENT_CAPTURE_MODES = ["viewport", "full-page"];
|
||||||
|
const PERMANENT_THROTTLING_VARIANTS = [
|
||||||
|
{
|
||||||
|
id: "capture-only-throttling",
|
||||||
|
code: "capture-only",
|
||||||
|
label: "backgroundThrottling disabled only during each capture",
|
||||||
|
disableGuestBackgroundThrottlingAtAttach: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "attach-off-throttling",
|
||||||
|
code: "attach-off",
|
||||||
|
label: "backgroundThrottling disabled once at guest attach",
|
||||||
|
disableGuestBackgroundThrottlingAtAttach: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const ATTACH_OFF_VARIANT_STATE_CODES = new Set(["P1", "P3", "P7"]);
|
||||||
|
const PERMANENT_PARKING_STATES = [
|
||||||
|
{
|
||||||
|
id: "p1-overflow-1x1",
|
||||||
|
code: "P1",
|
||||||
|
label: "host 1x1 overflow hidden",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "p2-clip-path-1x1",
|
||||||
|
code: "P2",
|
||||||
|
label: "host 1x1 clip-path inset 1px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "p3-opacity-0",
|
||||||
|
code: "P3",
|
||||||
|
label: "host full-size opacity 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "p4-transform-scale-0",
|
||||||
|
code: "P4a",
|
||||||
|
label: "host full-size transform scale(0)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "p4-transform-scale-0001",
|
||||||
|
code: "P4b",
|
||||||
|
label: "host full-size transform scale(0.001)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "p5-webview-0x0",
|
||||||
|
code: "P5a",
|
||||||
|
label: "webview element 0x0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "p5-webview-1x1",
|
||||||
|
code: "P5b",
|
||||||
|
label: "webview element 1x1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "p6-z-index-negative",
|
||||||
|
code: "P6",
|
||||||
|
label: "host full-size z-index -1 behind page",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "p7-opacity-001",
|
||||||
|
code: "P7",
|
||||||
|
label: "host full-size opacity 0.01",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
function fileUrl(filePath) {
|
function applyEarlyMacHarnessActivationPolicy() {
|
||||||
return new URL(`file://${filePath}`).toString();
|
if (process.platform !== "darwin") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
app.setActivationPolicy("accessory");
|
||||||
|
} catch {
|
||||||
|
// App readiness varies by Electron/macOS version; enforce again before windows.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMacHarnessActivationPolicyBeforeWindows() {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
app.setActivationPolicy("accessory");
|
||||||
|
app.dock?.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyEarlyMacHarnessActivationPolicy();
|
||||||
|
|
||||||
|
function fileUrl(filePath, params = {}) {
|
||||||
|
const url = new URL(`file://${filePath}`);
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
url.searchParams.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureDirSync(dir) {
|
function ensureDirSync(dir) {
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cornerWindowBounds(width, height) {
|
||||||
|
const { workArea } = screen.getPrimaryDisplay();
|
||||||
|
const inset = 12;
|
||||||
|
return {
|
||||||
|
x: Math.round(workArea.x + workArea.width - width - inset),
|
||||||
|
y: Math.round(workArea.y + workArea.height - height - inset),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInactiveHarnessWindow(input) {
|
||||||
|
const { width, height, ...options } = input;
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
...options,
|
||||||
|
...cornerWindowBounds(width, height),
|
||||||
|
show: false,
|
||||||
|
skipTaskbar: true,
|
||||||
|
});
|
||||||
|
const readyToShow = new Promise((resolve) => {
|
||||||
|
win.once("ready-to-show", () => {
|
||||||
|
if (!win.isDestroyed()) {
|
||||||
|
win.showInactive();
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return { win, readyToShow };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForInactiveReveal(handle, label) {
|
||||||
|
await withTimeout(handle.readyToShow, `${label} ready-to-show`);
|
||||||
|
await delay(250);
|
||||||
|
}
|
||||||
|
|
||||||
function withTimeout(promise, label) {
|
function withTimeout(promise, label) {
|
||||||
let timeoutId;
|
let timeoutId;
|
||||||
const timeout = new Promise((_, reject) => {
|
const timeout = new Promise((_, reject) => {
|
||||||
@@ -48,8 +188,11 @@ function analyzeImage(image, expected, guestMetrics) {
|
|||||||
return {
|
return {
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
|
logicalWidthAtDpr: 0,
|
||||||
|
logicalHeightAtDpr: 0,
|
||||||
brightRatio: 0,
|
brightRatio: 0,
|
||||||
textNonUniform: false,
|
textNonUniform: false,
|
||||||
|
matchedSize: false,
|
||||||
pass: false,
|
pass: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -131,6 +274,51 @@ function analyzeImage(image, expected, guestMetrics) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expectedForMode(mode) {
|
||||||
|
return mode === "viewport"
|
||||||
|
? { width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT, minBrightRatio: 0.65 }
|
||||||
|
: { width: VIEWPORT_WIDTH, height: FULL_PAGE_HEIGHT, minBrightRatio: 0.55 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeAnalysis(analysis) {
|
||||||
|
if (!analysis) {
|
||||||
|
return {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
logicalWidthAtDpr: 0,
|
||||||
|
logicalHeightAtDpr: 0,
|
||||||
|
brightRatio: 0,
|
||||||
|
textNonUniform: false,
|
||||||
|
matchedSize: false,
|
||||||
|
pass: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: analysis.width,
|
||||||
|
height: analysis.height,
|
||||||
|
logicalWidthAtDpr: analysis.logicalWidthAtDpr,
|
||||||
|
logicalHeightAtDpr: analysis.logicalHeightAtDpr,
|
||||||
|
brightRatio: analysis.brightRatio,
|
||||||
|
textNonUniform: analysis.textNonUniform,
|
||||||
|
matchedSize: analysis.matchedSize,
|
||||||
|
pass: analysis.pass,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function analysisSize(analysis) {
|
||||||
|
if (!analysis) {
|
||||||
|
return "0x0";
|
||||||
|
}
|
||||||
|
return `${analysis.width}x${analysis.height}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function analysisLogicalSize(analysis) {
|
||||||
|
if (!analysis) {
|
||||||
|
return "0x0";
|
||||||
|
}
|
||||||
|
return `${analysis.logicalWidthAtDpr}x${analysis.logicalHeightAtDpr}`;
|
||||||
|
}
|
||||||
|
|
||||||
function pass(message) {
|
function pass(message) {
|
||||||
console.log(`PASS ${message}`);
|
console.log(`PASS ${message}`);
|
||||||
}
|
}
|
||||||
@@ -141,6 +329,7 @@ function fail(message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveImage(image, outputPath) {
|
async function saveImage(image, outputPath) {
|
||||||
|
ensureDirSync(path.dirname(outputPath));
|
||||||
await fsp.writeFile(outputPath, image.toPNG());
|
await fsp.writeFile(outputPath, image.toPNG());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +425,267 @@ async function captureFullPage(contents) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function captureFullPageSequence(contents) {
|
||||||
|
const previousBackgroundThrottling = contents.getBackgroundThrottling();
|
||||||
|
contents.setBackgroundThrottling(false);
|
||||||
|
try {
|
||||||
|
contents.invalidate();
|
||||||
|
return await captureFullPage(contents);
|
||||||
|
} finally {
|
||||||
|
contents.setBackgroundThrottling(previousBackgroundThrottling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installHarnessWebviewGuards(win) {
|
||||||
|
win.webContents.on("will-attach-webview", (_event, webPreferences) => {
|
||||||
|
webPreferences.nodeIntegration = false;
|
||||||
|
webPreferences.contextIsolation = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackAttachedGuests(win, input = {}) {
|
||||||
|
const attachedGuests = [];
|
||||||
|
const waiters = [];
|
||||||
|
win.webContents.on("did-attach-webview", (_event, contents) => {
|
||||||
|
if (input.disableGuestBackgroundThrottlingAtAttach) {
|
||||||
|
contents.setBackgroundThrottling(false);
|
||||||
|
}
|
||||||
|
attachedGuests.push(contents);
|
||||||
|
const waiter = waiters.shift();
|
||||||
|
if (waiter) {
|
||||||
|
waiter(contents);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
attachedGuests,
|
||||||
|
waitForNextAttachedGuest() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
waiters.push(resolve);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPermanentHarnessWindow(state, variant) {
|
||||||
|
const handle = createInactiveHarnessWindow({
|
||||||
|
width: 1400,
|
||||||
|
height: 900,
|
||||||
|
backgroundColor: "#202020",
|
||||||
|
webPreferences: {
|
||||||
|
webviewTag: true,
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
sandbox: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { win } = handle;
|
||||||
|
installHarnessWebviewGuards(win);
|
||||||
|
const tracker = trackAttachedGuests(win, {
|
||||||
|
disableGuestBackgroundThrottlingAtAttach: variant.disableGuestBackgroundThrottlingAtAttach,
|
||||||
|
});
|
||||||
|
await withTimeout(
|
||||||
|
win.loadFile(path.join(ROOT, "index.html"), {
|
||||||
|
query: {
|
||||||
|
webviewCount: "0",
|
||||||
|
permanentParkingState: state.id,
|
||||||
|
targetUrl: fileUrl(path.join(ROOT, "bright.html")),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
"permanent harness window loadFile",
|
||||||
|
);
|
||||||
|
await waitForInactiveReveal(handle, "permanent harness window");
|
||||||
|
return { win, tracker };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPermanentKeeperWindow() {
|
||||||
|
const handle = createInactiveHarnessWindow({
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
webPreferences: {
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
sandbox: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { win } = handle;
|
||||||
|
await withTimeout(win.loadURL("about:blank"), "permanent keeper loadURL");
|
||||||
|
await waitForInactiveReveal(handle, "permanent keeper");
|
||||||
|
return win;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeHarnessWindow(win) {
|
||||||
|
if (!win.isDestroyed()) {
|
||||||
|
win.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetUrlForVariant(state, variant, phase, mode, index) {
|
||||||
|
const webviewNumber = index + 1;
|
||||||
|
return fileUrl(path.join(ROOT, "bright.html"), {
|
||||||
|
label: `${state.code} ${variant.code} ${phase.toUpperCase()} W${webviewNumber}`,
|
||||||
|
sub: `${mode.toUpperCase()} ${state.id} ${variant.code}`,
|
||||||
|
bottom: `${state.code} ${variant.code} FULL PAGE MARKER`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function variantsForPermanentState(state) {
|
||||||
|
const defaultVariants = ATTACH_OFF_VARIANT_STATE_CODES.has(state.code)
|
||||||
|
? PERMANENT_THROTTLING_VARIANTS
|
||||||
|
: [PERMANENT_THROTTLING_VARIANTS[0]];
|
||||||
|
if (PERMANENT_VARIANT_FILTER.size === 0) {
|
||||||
|
return defaultVariants;
|
||||||
|
}
|
||||||
|
return defaultVariants.filter(
|
||||||
|
(variant) =>
|
||||||
|
PERMANENT_VARIANT_FILTER.has(variant.id) || PERMANENT_VARIANT_FILTER.has(variant.code),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function appendPermanentWebview({ win, tracker, state, sourceUrl }) {
|
||||||
|
const guestPromise = tracker.waitForNextAttachedGuest();
|
||||||
|
const targetIndex = await withTimeout(
|
||||||
|
renderer(
|
||||||
|
win,
|
||||||
|
`window.captureHarness.addPermanentWebview(${JSON.stringify(sourceUrl)}, ${JSON.stringify(state.id)})`,
|
||||||
|
),
|
||||||
|
"permanent add webview",
|
||||||
|
);
|
||||||
|
const guest = await withTimeout(guestPromise, "permanent did-attach-webview");
|
||||||
|
return { guest, targetIndex };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function measurePermanentCapture({
|
||||||
|
contents,
|
||||||
|
mode,
|
||||||
|
state,
|
||||||
|
variant,
|
||||||
|
phase,
|
||||||
|
repeatIndex,
|
||||||
|
repeatTotal,
|
||||||
|
targetIndex,
|
||||||
|
guestMetrics,
|
||||||
|
retryUntilPass = false,
|
||||||
|
}) {
|
||||||
|
const outputPath = path.join(
|
||||||
|
OUT_DIR,
|
||||||
|
"permanent-parking",
|
||||||
|
variant.id,
|
||||||
|
state.id,
|
||||||
|
`${phase}-${mode}-webview-${targetIndex + 1}-${repeatIndex}.png`,
|
||||||
|
);
|
||||||
|
await fsp.rm(outputPath, { force: true });
|
||||||
|
const expected = expectedForMode(mode);
|
||||||
|
const start = Date.now();
|
||||||
|
const deadline = start + CAPTURE_TIMEOUT_MS;
|
||||||
|
let attempt = 0;
|
||||||
|
let lastAnalysis = null;
|
||||||
|
let lastError = "";
|
||||||
|
let lastImage = null;
|
||||||
|
|
||||||
|
while (Date.now() < deadline || attempt === 0) {
|
||||||
|
attempt += 1;
|
||||||
|
try {
|
||||||
|
const image =
|
||||||
|
mode === "viewport"
|
||||||
|
? await capturePageSequence(contents)
|
||||||
|
: await captureFullPageSequence(contents);
|
||||||
|
lastImage = image;
|
||||||
|
lastAnalysis = analyzeImage(image, expected, guestMetrics);
|
||||||
|
if (lastAnalysis.pass) {
|
||||||
|
await saveImage(image, outputPath);
|
||||||
|
const latencyMs = Date.now() - start;
|
||||||
|
const result = {
|
||||||
|
group: "permanent-parking",
|
||||||
|
stateId: state.id,
|
||||||
|
stateCode: state.code,
|
||||||
|
stateLabel: state.label,
|
||||||
|
variantId: variant.id,
|
||||||
|
variantCode: variant.code,
|
||||||
|
variantLabel: variant.label,
|
||||||
|
attachTimeBackgroundThrottlingDisabled: variant.disableGuestBackgroundThrottlingAtAttach,
|
||||||
|
phase,
|
||||||
|
mode,
|
||||||
|
repeatIndex,
|
||||||
|
repeatTotal,
|
||||||
|
targetIndex,
|
||||||
|
attempts: attempt,
|
||||||
|
latencyMs,
|
||||||
|
outputPath,
|
||||||
|
error: null,
|
||||||
|
analysis: summarizeAnalysis(lastAnalysis),
|
||||||
|
pass: true,
|
||||||
|
};
|
||||||
|
pass(
|
||||||
|
`permanent ${state.code} ${variant.code} ${phase} ${mode} webview ${targetIndex + 1} ${repeatIndex}/${repeatTotal} attempts=${attempt} size=${analysisSize(lastAnalysis)} logical=${analysisLogicalSize(lastAnalysis)} bright=${lastAnalysis.brightRatio.toFixed(4)} text=${lastAnalysis.textNonUniform} file=${outputPath}`,
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error instanceof Error ? error.message : String(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!retryUntilPass) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const remainingMs = deadline - Date.now();
|
||||||
|
if (remainingMs <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await delay(Math.min(CAPTURE_RETRY_INTERVAL_MS, remainingMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastImage && !lastImage.isEmpty()) {
|
||||||
|
await saveImage(lastImage, outputPath);
|
||||||
|
}
|
||||||
|
const latencyMs = Date.now() - start;
|
||||||
|
const bright = lastAnalysis ? lastAnalysis.brightRatio.toFixed(4) : "0.0000";
|
||||||
|
const textNonUniform = lastAnalysis ? lastAnalysis.textNonUniform : false;
|
||||||
|
const size = analysisSize(lastAnalysis);
|
||||||
|
const logicalSize = analysisLogicalSize(lastAnalysis);
|
||||||
|
const result = {
|
||||||
|
group: "permanent-parking",
|
||||||
|
stateId: state.id,
|
||||||
|
stateCode: state.code,
|
||||||
|
stateLabel: state.label,
|
||||||
|
variantId: variant.id,
|
||||||
|
variantCode: variant.code,
|
||||||
|
variantLabel: variant.label,
|
||||||
|
attachTimeBackgroundThrottlingDisabled: variant.disableGuestBackgroundThrottlingAtAttach,
|
||||||
|
phase,
|
||||||
|
mode,
|
||||||
|
repeatIndex,
|
||||||
|
repeatTotal,
|
||||||
|
targetIndex,
|
||||||
|
attempts: attempt,
|
||||||
|
latencyMs,
|
||||||
|
outputPath: lastImage && !lastImage.isEmpty() ? outputPath : null,
|
||||||
|
error: lastError || null,
|
||||||
|
analysis: summarizeAnalysis(lastAnalysis),
|
||||||
|
pass: false,
|
||||||
|
};
|
||||||
|
console.log(
|
||||||
|
`FAIL permanent ${state.code} ${variant.code} ${phase} ${mode} webview ${targetIndex + 1} ${repeatIndex}/${repeatTotal} attempts=${attempt} size=${size} logical=${logicalSize} bright=${bright} text=${textNonUniform} error=${lastError || "pixel verdict failed"} file=${result.outputPath || "none"}`,
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writePermanentParkingResults(results) {
|
||||||
|
await fsp.writeFile(
|
||||||
|
path.join(OUT_DIR, "permanent-parking-results.json"),
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
soakMs: SOAK_MS,
|
||||||
|
states: PERMANENT_PARKING_STATES,
|
||||||
|
variants: PERMANENT_THROTTLING_VARIANTS,
|
||||||
|
results,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function captureWithPrep({
|
async function captureWithPrep({
|
||||||
win,
|
win,
|
||||||
contents,
|
contents,
|
||||||
@@ -269,7 +719,7 @@ async function captureWithPrep({
|
|||||||
const image =
|
const image =
|
||||||
mode === "viewport"
|
mode === "viewport"
|
||||||
? await capturePageSequence(contents)
|
? await capturePageSequence(contents)
|
||||||
: await captureFullPage(contents);
|
: await captureFullPageSequence(contents);
|
||||||
const analysis = analyzeImage(image, expected, guestMetrics);
|
const analysis = analyzeImage(image, expected, guestMetrics);
|
||||||
const size = `${analysis.width}x${analysis.height}`;
|
const size = `${analysis.width}x${analysis.height}`;
|
||||||
const logicalSize = `${analysis.logicalWidthAtDpr}x${analysis.logicalHeightAtDpr}`;
|
const logicalSize = `${analysis.logicalWidthAtDpr}x${analysis.logicalHeightAtDpr}`;
|
||||||
@@ -314,7 +764,9 @@ async function expectLegacySecondWebviewFailure({ win, contents, mode, guestMetr
|
|||||||
const outputPath = path.join(OUT_DIR, `${mode}-legacy-webview-${targetIndex + 1}.png`);
|
const outputPath = path.join(OUT_DIR, `${mode}-legacy-webview-${targetIndex + 1}.png`);
|
||||||
try {
|
try {
|
||||||
const image =
|
const image =
|
||||||
mode === "viewport" ? await capturePageSequence(contents) : await captureFullPage(contents);
|
mode === "viewport"
|
||||||
|
? await capturePageSequence(contents)
|
||||||
|
: await captureFullPageSequence(contents);
|
||||||
await saveImage(image, outputPath);
|
await saveImage(image, outputPath);
|
||||||
const expected =
|
const expected =
|
||||||
mode === "viewport"
|
mode === "viewport"
|
||||||
@@ -372,8 +824,307 @@ async function captureFreshDelayedWebview({ win, waitForNextAttachedGuest, mode
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runPermanentFreshPhase(state, variant, results) {
|
||||||
|
for (let repeatIndex = 1; repeatIndex <= FRESH_REPEAT_COUNT; repeatIndex += 1) {
|
||||||
|
for (const mode of PERMANENT_CAPTURE_MODES) {
|
||||||
|
const { win, tracker } = await createPermanentHarnessWindow(state, variant);
|
||||||
|
try {
|
||||||
|
const sourceUrl = targetUrlForVariant(state, variant, "fresh", mode, 0);
|
||||||
|
const { guest, targetIndex } = await appendPermanentWebview({
|
||||||
|
win,
|
||||||
|
tracker,
|
||||||
|
state,
|
||||||
|
sourceUrl,
|
||||||
|
});
|
||||||
|
const guestMetrics = await readGuestMetrics(guest);
|
||||||
|
results.push(
|
||||||
|
await measurePermanentCapture({
|
||||||
|
contents: guest,
|
||||||
|
mode,
|
||||||
|
state,
|
||||||
|
variant,
|
||||||
|
phase: "fresh",
|
||||||
|
repeatIndex,
|
||||||
|
repeatTotal: FRESH_REPEAT_COUNT,
|
||||||
|
targetIndex,
|
||||||
|
guestMetrics,
|
||||||
|
retryUntilPass: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await closeHarnessWindow(win);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPermanentSettledAndSoakPhases(state, variant, results) {
|
||||||
|
const { win, tracker } = await createPermanentHarnessWindow(state, variant);
|
||||||
|
try {
|
||||||
|
const targets = [];
|
||||||
|
for (const mode of PERMANENT_CAPTURE_MODES) {
|
||||||
|
const sourceUrl = targetUrlForVariant(state, variant, "settled", mode, targets.length);
|
||||||
|
const target = await appendPermanentWebview({ win, tracker, state, sourceUrl });
|
||||||
|
targets.push({ mode, ...target });
|
||||||
|
}
|
||||||
|
await Promise.all(targets.map((target) => waitForGuestLoad(target.guest)));
|
||||||
|
await delay(250);
|
||||||
|
const guestMetrics = new Map();
|
||||||
|
for (const target of targets) {
|
||||||
|
guestMetrics.set(target.targetIndex, await readGuestMetrics(target.guest));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
for (let repeatIndex = 1; repeatIndex <= REPEAT_COUNT; repeatIndex += 1) {
|
||||||
|
results.push(
|
||||||
|
await measurePermanentCapture({
|
||||||
|
contents: target.guest,
|
||||||
|
mode: target.mode,
|
||||||
|
state,
|
||||||
|
variant,
|
||||||
|
phase: "settled",
|
||||||
|
repeatIndex,
|
||||||
|
repeatTotal: REPEAT_COUNT,
|
||||||
|
targetIndex: target.targetIndex,
|
||||||
|
guestMetrics: guestMetrics.get(target.targetIndex),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`SOAK permanent ${state.code} idle ${SOAK_MS}ms`);
|
||||||
|
await delay(SOAK_MS);
|
||||||
|
|
||||||
|
for (let repeatIndex = 1; repeatIndex <= REPEAT_COUNT; repeatIndex += 1) {
|
||||||
|
for (const target of targets) {
|
||||||
|
results.push(
|
||||||
|
await measurePermanentCapture({
|
||||||
|
contents: target.guest,
|
||||||
|
mode: target.mode,
|
||||||
|
state,
|
||||||
|
variant,
|
||||||
|
phase: "soak",
|
||||||
|
repeatIndex,
|
||||||
|
repeatTotal: REPEAT_COUNT,
|
||||||
|
targetIndex: target.targetIndex,
|
||||||
|
guestMetrics: guestMetrics.get(target.targetIndex),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await closeHarnessWindow(win);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPermanentMultiTabPhase(state, variant, results) {
|
||||||
|
const { win, tracker } = await createPermanentHarnessWindow(state, variant);
|
||||||
|
try {
|
||||||
|
const targets = [];
|
||||||
|
for (let index = 0; index < 3; index += 1) {
|
||||||
|
const sourceUrl = targetUrlForVariant(state, variant, "multi", "multi", index);
|
||||||
|
const target = await appendPermanentWebview({ win, tracker, state, sourceUrl });
|
||||||
|
targets.push(target);
|
||||||
|
}
|
||||||
|
await Promise.all(targets.map((target) => waitForGuestLoad(target.guest)));
|
||||||
|
await delay(250);
|
||||||
|
const guestMetrics = new Map();
|
||||||
|
for (const target of targets) {
|
||||||
|
guestMetrics.set(target.targetIndex, await readGuestMetrics(target.guest));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const target of targets.slice(1, 3)) {
|
||||||
|
for (const mode of PERMANENT_CAPTURE_MODES) {
|
||||||
|
results.push(
|
||||||
|
await measurePermanentCapture({
|
||||||
|
contents: target.guest,
|
||||||
|
mode,
|
||||||
|
state,
|
||||||
|
variant,
|
||||||
|
phase: "multi-tab",
|
||||||
|
repeatIndex: 1,
|
||||||
|
repeatTotal: 1,
|
||||||
|
targetIndex: target.targetIndex,
|
||||||
|
guestMetrics: guestMetrics.get(target.targetIndex),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await closeHarnessWindow(win);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createOccludingWindow(targetWindow) {
|
||||||
|
const bounds = targetWindow.getBounds();
|
||||||
|
const handle = createInactiveHarnessWindow({
|
||||||
|
width: bounds.width,
|
||||||
|
height: bounds.height,
|
||||||
|
alwaysOnTop: true,
|
||||||
|
backgroundColor: "#101010",
|
||||||
|
webPreferences: {
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
sandbox: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { win: blocker } = handle;
|
||||||
|
await blocker.loadURL(
|
||||||
|
"data:text/html;charset=utf-8,<html><body style='margin:0;background:#101010'></body></html>",
|
||||||
|
);
|
||||||
|
await waitForInactiveReveal(handle, "occluding window");
|
||||||
|
blocker.setAlwaysOnTop(true);
|
||||||
|
await delay(500);
|
||||||
|
return blocker;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPermanentWindowHostilityPhase(state, variant, results) {
|
||||||
|
const { win, tracker } = await createPermanentHarnessWindow(state, variant);
|
||||||
|
let blocker = null;
|
||||||
|
try {
|
||||||
|
const targets = [];
|
||||||
|
for (const mode of PERMANENT_CAPTURE_MODES) {
|
||||||
|
const sourceUrl = targetUrlForVariant(state, variant, "hostile", mode, targets.length);
|
||||||
|
const target = await appendPermanentWebview({ win, tracker, state, sourceUrl });
|
||||||
|
targets.push({ mode, ...target });
|
||||||
|
}
|
||||||
|
await Promise.all(targets.map((target) => waitForGuestLoad(target.guest)));
|
||||||
|
await delay(250);
|
||||||
|
const guestMetrics = new Map();
|
||||||
|
for (const target of targets) {
|
||||||
|
guestMetrics.set(target.targetIndex, await readGuestMetrics(target.guest));
|
||||||
|
}
|
||||||
|
|
||||||
|
blocker = await createOccludingWindow(win);
|
||||||
|
for (const target of targets) {
|
||||||
|
results.push(
|
||||||
|
await measurePermanentCapture({
|
||||||
|
contents: target.guest,
|
||||||
|
mode: target.mode,
|
||||||
|
state,
|
||||||
|
variant,
|
||||||
|
phase: "window-occluded",
|
||||||
|
repeatIndex: 1,
|
||||||
|
repeatTotal: 1,
|
||||||
|
targetIndex: target.targetIndex,
|
||||||
|
guestMetrics: guestMetrics.get(target.targetIndex),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await closeHarnessWindow(blocker);
|
||||||
|
blocker = null;
|
||||||
|
|
||||||
|
win.minimize();
|
||||||
|
await delay(800);
|
||||||
|
for (const target of targets) {
|
||||||
|
results.push(
|
||||||
|
await measurePermanentCapture({
|
||||||
|
contents: target.guest,
|
||||||
|
mode: target.mode,
|
||||||
|
state,
|
||||||
|
variant,
|
||||||
|
phase: "window-minimized",
|
||||||
|
repeatIndex: 1,
|
||||||
|
repeatTotal: 1,
|
||||||
|
targetIndex: target.targetIndex,
|
||||||
|
guestMetrics: guestMetrics.get(target.targetIndex),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (blocker) {
|
||||||
|
await closeHarnessWindow(blocker);
|
||||||
|
}
|
||||||
|
await closeHarnessWindow(win);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPermanentParkingState(state, variant, results) {
|
||||||
|
console.log(`STATE permanent ${state.code} ${variant.code} ${state.label}`);
|
||||||
|
await runPermanentFreshPhase(state, variant, results);
|
||||||
|
await writePermanentParkingResults(results);
|
||||||
|
await runPermanentSettledAndSoakPhases(state, variant, results);
|
||||||
|
await writePermanentParkingResults(results);
|
||||||
|
await runPermanentMultiTabPhase(state, variant, results);
|
||||||
|
await writePermanentParkingResults(results);
|
||||||
|
await runPermanentWindowHostilityPhase(state, variant, results);
|
||||||
|
await writePermanentParkingResults(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPermanentParkingGroup() {
|
||||||
|
const results = [];
|
||||||
|
await fsp.rm(path.join(OUT_DIR, "permanent-parking"), { recursive: true, force: true });
|
||||||
|
await fsp.rm(path.join(OUT_DIR, "permanent-parking-results.json"), { force: true });
|
||||||
|
const keeper = await createPermanentKeeperWindow();
|
||||||
|
const states =
|
||||||
|
PERMANENT_STATE_FILTER.size === 0
|
||||||
|
? PERMANENT_PARKING_STATES
|
||||||
|
: PERMANENT_PARKING_STATES.filter(
|
||||||
|
(state) => PERMANENT_STATE_FILTER.has(state.id) || PERMANENT_STATE_FILTER.has(state.code),
|
||||||
|
);
|
||||||
|
console.log(`RUN permanent-parking states=${states.length} soakMs=${SOAK_MS}`);
|
||||||
|
try {
|
||||||
|
for (const state of states) {
|
||||||
|
for (const variant of variantsForPermanentState(state)) {
|
||||||
|
try {
|
||||||
|
await runPermanentParkingState(state, variant, results);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
console.log(`FAIL permanent ${state.code} ${variant.code} fatal error=${message}`);
|
||||||
|
results.push({
|
||||||
|
group: "permanent-parking",
|
||||||
|
stateId: state.id,
|
||||||
|
stateCode: state.code,
|
||||||
|
stateLabel: state.label,
|
||||||
|
variantId: variant.id,
|
||||||
|
variantCode: variant.code,
|
||||||
|
variantLabel: variant.label,
|
||||||
|
attachTimeBackgroundThrottlingDisabled:
|
||||||
|
variant.disableGuestBackgroundThrottlingAtAttach,
|
||||||
|
phase: "fatal",
|
||||||
|
mode: "setup",
|
||||||
|
repeatIndex: 1,
|
||||||
|
repeatTotal: 1,
|
||||||
|
targetIndex: 0,
|
||||||
|
attempts: 0,
|
||||||
|
latencyMs: 0,
|
||||||
|
outputPath: null,
|
||||||
|
error: message,
|
||||||
|
analysis: summarizeAnalysis(null),
|
||||||
|
pass: false,
|
||||||
|
});
|
||||||
|
await writePermanentParkingResults(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await closeHarnessWindow(keeper);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
ensureDirSync(OUT_DIR);
|
ensureDirSync(OUT_DIR);
|
||||||
|
if (!["all", "existing", "permanent-parking"].includes(HARNESS_GROUP)) {
|
||||||
|
fail(`unknown harness group ${HARNESS_GROUP}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HARNESS_GROUP === "permanent-parking") {
|
||||||
|
const permanentParkingResults = await runPermanentParkingGroup();
|
||||||
|
await fsp.writeFile(
|
||||||
|
path.join(OUT_DIR, "results.json"),
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
permanentParkingResults,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`,
|
||||||
|
);
|
||||||
|
pass(`capture harness permanent-parking complete output=${OUT_DIR}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const attachedGuests = [];
|
const attachedGuests = [];
|
||||||
const freshGuestWaiters = [];
|
const freshGuestWaiters = [];
|
||||||
@@ -385,10 +1136,9 @@ async function main() {
|
|||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
freshGuestWaiters.push(resolve);
|
freshGuestWaiters.push(resolve);
|
||||||
});
|
});
|
||||||
const win = new BrowserWindow({
|
const handle = createInactiveHarnessWindow({
|
||||||
width: 1000,
|
width: 1000,
|
||||||
height: 700,
|
height: 700,
|
||||||
show: true,
|
|
||||||
backgroundColor: "#202020",
|
backgroundColor: "#202020",
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
webviewTag: true,
|
webviewTag: true,
|
||||||
@@ -397,6 +1147,7 @@ async function main() {
|
|||||||
sandbox: false,
|
sandbox: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const { win } = handle;
|
||||||
|
|
||||||
win.webContents.on("will-attach-webview", (_event, webPreferences) => {
|
win.webContents.on("will-attach-webview", (_event, webPreferences) => {
|
||||||
webPreferences.nodeIntegration = false;
|
webPreferences.nodeIntegration = false;
|
||||||
@@ -416,6 +1167,7 @@ async function main() {
|
|||||||
await win.loadFile(path.join(ROOT, "index.html"), {
|
await win.loadFile(path.join(ROOT, "index.html"), {
|
||||||
query: { targetUrl: fileUrl(path.join(ROOT, "bright.html")), webviewCount: "2" },
|
query: { targetUrl: fileUrl(path.join(ROOT, "bright.html")), webviewCount: "2" },
|
||||||
});
|
});
|
||||||
|
await waitForInactiveReveal(handle, "capture harness window");
|
||||||
await withTimeout(guestsPromise, "did-attach-webview");
|
await withTimeout(guestsPromise, "did-attach-webview");
|
||||||
await Promise.all(attachedGuests.map((guest) => waitForGuestLoad(guest)));
|
await Promise.all(attachedGuests.map((guest) => waitForGuestLoad(guest)));
|
||||||
await renderer(win, "window.captureHarness.waitForFrames(2)");
|
await renderer(win, "window.captureHarness.waitForFrames(2)");
|
||||||
@@ -504,9 +1256,20 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const permanentParkingResults = HARNESS_GROUP === "all" ? await runPermanentParkingGroup() : [];
|
||||||
|
|
||||||
await fsp.writeFile(
|
await fsp.writeFile(
|
||||||
path.join(OUT_DIR, "results.json"),
|
path.join(OUT_DIR, "results.json"),
|
||||||
`${JSON.stringify({ generatedAt: new Date().toISOString(), guestMetrics, results }, null, 2)}\n`,
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
guestMetrics,
|
||||||
|
results,
|
||||||
|
permanentParkingResults,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`,
|
||||||
);
|
);
|
||||||
pass(`capture harness complete output=${OUT_DIR}`);
|
pass(`capture harness complete output=${OUT_DIR}`);
|
||||||
|
|
||||||
@@ -516,8 +1279,14 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app
|
app
|
||||||
|
.on("window-all-closed", () => {
|
||||||
|
// The permanent parking sweep intentionally opens and closes many phase windows.
|
||||||
|
})
|
||||||
.whenReady()
|
.whenReady()
|
||||||
.then(main)
|
.then(() => {
|
||||||
|
applyMacHarnessActivationPolicyBeforeWindows();
|
||||||
|
return main();
|
||||||
|
})
|
||||||
.then(() => app.quit())
|
.then(() => app.quit())
|
||||||
.catch(async (error) => {
|
.catch(async (error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
Reference in New Issue
Block a user