@codeyam-editor/codeyam-editor 0.1.0-staging.0f231be
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/npm/cli.js +19 -0
- package/npm/cloud.js +966 -0
- package/npm/container.js +49 -0
- package/npm/editor-dev.js +142 -0
- package/npm/editor.js +210 -0
- package/npm/open.js +15 -0
- package/npm/scenario-check.js +218 -0
- package/npm/scenario-handlers.js +82 -0
- package/npm/scenario-issues.js +35 -0
- package/npm/scenario-metrics.js +94 -0
- package/npm/scenario-mocks.js +70 -0
- package/npm/scenario-playwright.js +192 -0
- package/npm/utils.js +623 -0
- package/package.json +32 -0
- package/ui/dist/assets/ContainerInspector-CVhTYjTR.js +2 -0
- package/ui/dist/assets/EditorShell-DMepjAfe.js +38 -0
- package/ui/dist/assets/Markdown-BHBqz-Mj.js +1 -0
- package/ui/dist/assets/ScenarioDataPanel-DYP7pi_n.css +32 -0
- package/ui/dist/assets/ScenarioDataPanel-DbnhCRfn.js +5 -0
- package/ui/dist/assets/ScenarioDataPanelFullPage-CNgkwmsl.js +1 -0
- package/ui/dist/assets/ScenarioRenderer-xciw2Qja.js +1 -0
- package/ui/dist/assets/StepsSimulator-CRtmlyNZ.js +1 -0
- package/ui/dist/assets/index-CnDCJXUg.js +27 -0
- package/ui/dist/assets/index-Dnu7mncO.css +1 -0
- package/ui/dist/assets/markdown-v0F0UEt3.js +37 -0
- package/ui/dist/assets/react-CSS0HapR.js +28 -0
- package/ui/dist/assets/useEvents-DggWrtHe.js +1 -0
- package/ui/dist/assets/xterm--24IGk-x.js +9 -0
- package/ui/dist/favicon.ico +0 -0
- package/ui/dist/favicon.png +0 -0
- package/ui/dist/index.html +16 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const { createIssue } = require("./scenario-issues");
|
|
2
|
+
|
|
3
|
+
function getInitScript() {
|
|
4
|
+
return `
|
|
5
|
+
window.__codeyamUnhandledRejections = [];
|
|
6
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
7
|
+
const reason = event.reason;
|
|
8
|
+
const message =
|
|
9
|
+
reason instanceof Error ? reason.message : String(reason);
|
|
10
|
+
window.__codeyamUnhandledRejections.push(message);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Stub WebSocket during capture to prevent terminal reconnection spam.
|
|
14
|
+
window.WebSocket = class StubWebSocket {
|
|
15
|
+
static CONNECTING = 0;
|
|
16
|
+
static OPEN = 1;
|
|
17
|
+
static CLOSING = 2;
|
|
18
|
+
static CLOSED = 3;
|
|
19
|
+
readyState = 3;
|
|
20
|
+
onopen = null;
|
|
21
|
+
onclose = null;
|
|
22
|
+
onerror = null;
|
|
23
|
+
onmessage = null;
|
|
24
|
+
send() {}
|
|
25
|
+
close() {}
|
|
26
|
+
addEventListener() {}
|
|
27
|
+
removeEventListener() {}
|
|
28
|
+
dispatchEvent() { return false; }
|
|
29
|
+
constructor() {
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
if (this.onerror) this.onerror(new Event("error"));
|
|
32
|
+
if (this.onclose) this.onclose(new CloseEvent("close"));
|
|
33
|
+
}, 0);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function handleConsoleMessage(message) {
|
|
40
|
+
if (message.type() !== "error") return null;
|
|
41
|
+
const text = message.text();
|
|
42
|
+
|
|
43
|
+
// Ignore known dev-server WebSocket/HMR errors from Vite proxy
|
|
44
|
+
if (
|
|
45
|
+
text.includes("WebSocket connection to") ||
|
|
46
|
+
text.includes("Unsupported Media Type")
|
|
47
|
+
) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return createIssue("console", text);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function handlePageError(error) {
|
|
55
|
+
return createIssue("pageerror", error.message || String(error));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handleRequestFailed(request) {
|
|
59
|
+
const errorText = request.failure()?.errorText || "Request failed";
|
|
60
|
+
|
|
61
|
+
// Filter benign request cancellations. `net::ERR_ABORTED` is what Playwright
|
|
62
|
+
// emits when a request is in-flight at the moment the page is closed (or the
|
|
63
|
+
// iframe is destroyed). For scenarios whose pages fetch large payloads (the
|
|
64
|
+
// editor's own EditorShell mounts a 2.2MB `/api/tests` fetch), the
|
|
65
|
+
// browser.close() at the end of capture races the fetch and produces this
|
|
66
|
+
// event AFTER the screenshot has already been taken — there is no real
|
|
67
|
+
// failure to surface. Genuine network failures arrive under different
|
|
68
|
+
// codes (net::ERR_CONNECTION_REFUSED, net::ERR_NAME_NOT_RESOLVED, etc.)
|
|
69
|
+
// and continue to be reported.
|
|
70
|
+
if (errorText.includes("net::ERR_ABORTED")) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return createIssue("requestfailed", errorText, { url: request.url() });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
getInitScript,
|
|
79
|
+
handleConsoleMessage,
|
|
80
|
+
handlePageError,
|
|
81
|
+
handleRequestFailed,
|
|
82
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function createIssue(kind, message, extra = {}) {
|
|
2
|
+
const issue = {
|
|
3
|
+
kind,
|
|
4
|
+
message,
|
|
5
|
+
url: extra.url ?? null,
|
|
6
|
+
status: extra.status ?? null,
|
|
7
|
+
};
|
|
8
|
+
if (extra.matchedPattern != null) issue.matchedPattern = extra.matchedPattern;
|
|
9
|
+
if (extra.contextSnippet != null) issue.contextSnippet = extra.contextSnippet;
|
|
10
|
+
return issue;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function pushIssue(issues, issue) {
|
|
14
|
+
const key = JSON.stringify(issue);
|
|
15
|
+
if (!issues.some((existing) => JSON.stringify(existing) === key)) {
|
|
16
|
+
issues.push(issue);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildResult({ loaded, hasContent, issues, outputPath, url }) {
|
|
21
|
+
return {
|
|
22
|
+
ok: loaded && hasContent && issues.length === 0,
|
|
23
|
+
loaded,
|
|
24
|
+
hasContent,
|
|
25
|
+
url,
|
|
26
|
+
outputPath: outputPath ?? null,
|
|
27
|
+
issues,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
createIssue,
|
|
33
|
+
pushIssue,
|
|
34
|
+
buildResult,
|
|
35
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const LOADING_MARKERS = [
|
|
2
|
+
"Loading scenario...",
|
|
3
|
+
"Loading tests...",
|
|
4
|
+
"Loading scenarios...",
|
|
5
|
+
"disconnected",
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
function hasLoadingMarkers(text) {
|
|
9
|
+
return LOADING_MARKERS.some((marker) => text.includes(marker));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function hasRenderableContent(state) {
|
|
13
|
+
if (!state) return false;
|
|
14
|
+
if (
|
|
15
|
+
state.rootChildCount > 0 ||
|
|
16
|
+
state.rootTextLength > 0 ||
|
|
17
|
+
state.bodyTextLength > 0
|
|
18
|
+
) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if ((state.loadedImageCount || 0) > 0) return true;
|
|
22
|
+
if ((state.mediaBboxCount || 0) > 0) return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function describeBlankReason(state) {
|
|
27
|
+
if (!state) return "no content state collected";
|
|
28
|
+
const parts = [];
|
|
29
|
+
if (!(state.bodyTextLength > 0)) {
|
|
30
|
+
parts.push("no text");
|
|
31
|
+
}
|
|
32
|
+
const imageCount = state.imageCount || 0;
|
|
33
|
+
const loadedImageCount = state.loadedImageCount || 0;
|
|
34
|
+
if (imageCount > 0 && loadedImageCount === 0) {
|
|
35
|
+
parts.push(`${imageCount} unloaded image${imageCount === 1 ? "" : "s"}`);
|
|
36
|
+
} else if (imageCount === 0) {
|
|
37
|
+
parts.push("no images");
|
|
38
|
+
}
|
|
39
|
+
if (!((state.mediaBboxCount || 0) > 0)) {
|
|
40
|
+
parts.push("no svg/canvas/video");
|
|
41
|
+
}
|
|
42
|
+
return parts.join(", ");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function shouldStopWaitingForImages(images, options = {}) {
|
|
46
|
+
const { elapsedMs = 0, overallTimeoutMs = 5000 } = options;
|
|
47
|
+
if (!Array.isArray(images) || images.length === 0) return true;
|
|
48
|
+
if (elapsedMs >= overallTimeoutMs) return true;
|
|
49
|
+
return images.every((img) => img && img.complete === true);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ERROR_PATTERNS = [
|
|
53
|
+
"not found in registry",
|
|
54
|
+
"Component not found",
|
|
55
|
+
"Scenario Error",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
function hasErrorPatterns(text) {
|
|
59
|
+
return ERROR_PATTERNS.some((pattern) => text.includes(pattern));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function findErrorPattern(text) {
|
|
63
|
+
if (!text) return null;
|
|
64
|
+
for (const pattern of ERROR_PATTERNS) {
|
|
65
|
+
if (text.includes(pattern)) return pattern;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ERROR_CONTEXT_RADIUS = 60;
|
|
71
|
+
|
|
72
|
+
function buildErrorContextSnippet(text, pattern) {
|
|
73
|
+
if (!text || !pattern) return null;
|
|
74
|
+
const index = text.indexOf(pattern);
|
|
75
|
+
if (index < 0) return null;
|
|
76
|
+
const start = Math.max(0, index - ERROR_CONTEXT_RADIUS);
|
|
77
|
+
const end = Math.min(text.length, index + pattern.length + ERROR_CONTEXT_RADIUS);
|
|
78
|
+
const slice = text.slice(start, end).replace(/\s+/g, " ").trim();
|
|
79
|
+
const prefix = start > 0 ? "…" : "";
|
|
80
|
+
const suffix = end < text.length ? "…" : "";
|
|
81
|
+
return `${prefix}${slice}${suffix}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
hasLoadingMarkers,
|
|
86
|
+
hasRenderableContent,
|
|
87
|
+
describeBlankReason,
|
|
88
|
+
shouldStopWaitingForImages,
|
|
89
|
+
hasErrorPatterns,
|
|
90
|
+
findErrorPattern,
|
|
91
|
+
buildErrorContextSnippet,
|
|
92
|
+
ERROR_PATTERNS,
|
|
93
|
+
ERROR_CONTEXT_RADIUS,
|
|
94
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
function normalizeMockCandidates(url) {
|
|
2
|
+
try {
|
|
3
|
+
const parsed = new URL(url);
|
|
4
|
+
return [url, `${parsed.pathname}${parsed.search}`, parsed.pathname];
|
|
5
|
+
} catch {
|
|
6
|
+
return [url];
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function findHttpMock(httpMocks, request) {
|
|
11
|
+
const method = request.method().toUpperCase();
|
|
12
|
+
const candidates = normalizeMockCandidates(request.url());
|
|
13
|
+
for (const candidate of candidates) {
|
|
14
|
+
const key = `${method} ${candidate}`;
|
|
15
|
+
if (httpMocks[key]) {
|
|
16
|
+
return httpMocks[key];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function attachHttpMocks(page, httpMocks) {
|
|
23
|
+
if (!httpMocks || Object.keys(httpMocks).length === 0) return;
|
|
24
|
+
|
|
25
|
+
await page.route("**/*", async (route) => {
|
|
26
|
+
const mock = findHttpMock(httpMocks, route.request());
|
|
27
|
+
if (!mock) {
|
|
28
|
+
await route.continue();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const headers = { ...(mock.headers || {}) };
|
|
33
|
+
let body;
|
|
34
|
+
if (mock.body !== undefined) {
|
|
35
|
+
body =
|
|
36
|
+
typeof mock.body === "string" ? mock.body : JSON.stringify(mock.body);
|
|
37
|
+
const hasContentType = Object.keys(headers).some(
|
|
38
|
+
(key) => key.toLowerCase() === "content-type",
|
|
39
|
+
);
|
|
40
|
+
if (!hasContentType) {
|
|
41
|
+
headers["content-type"] = "application/json";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await route.fulfill({
|
|
46
|
+
status: mock.status || 200,
|
|
47
|
+
headers,
|
|
48
|
+
body,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Disable the in-page fetch mock by returning an empty active-mocks.json.
|
|
53
|
+
// The HTML injects a script that synchronously loads this file and
|
|
54
|
+
// monkey-patches window.fetch, which would bypass Playwright's route
|
|
55
|
+
// interception. This route is registered AFTER **/* so it takes priority
|
|
56
|
+
// (Playwright uses LIFO ordering for route handlers).
|
|
57
|
+
await page.route("**/active-mocks.json", async (route) => {
|
|
58
|
+
await route.fulfill({
|
|
59
|
+
status: 200,
|
|
60
|
+
headers: { "content-type": "application/json" },
|
|
61
|
+
body: "[]",
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
normalizeMockCandidates,
|
|
68
|
+
findHttpMock,
|
|
69
|
+
attachHttpMocks,
|
|
70
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const {
|
|
2
|
+
hasLoadingMarkers,
|
|
3
|
+
shouldStopWaitingForImages,
|
|
4
|
+
} = require("./scenario-metrics");
|
|
5
|
+
|
|
6
|
+
function escapeHtmlAttribute(value) {
|
|
7
|
+
return String(value).replaceAll("&", "&").replaceAll('"', """);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// The harness background defaults to transparent so the iframe's own
|
|
11
|
+
// <body> background paints through — matching what users see in the Live
|
|
12
|
+
// Preview. Callers (via scenario-check.js) pass a concrete color when the
|
|
13
|
+
// UI has detected a background it wants the capture to paint behind the
|
|
14
|
+
// iframe, e.g. `var(--bg-deep)` from the editor shell.
|
|
15
|
+
function buildIframeHarness(url, { background = "transparent" } = {}) {
|
|
16
|
+
const escapedUrl = escapeHtmlAttribute(url);
|
|
17
|
+
const bg = String(background);
|
|
18
|
+
return `<!doctype html>
|
|
19
|
+
<html>
|
|
20
|
+
<head>
|
|
21
|
+
<meta charset="utf-8">
|
|
22
|
+
<style>
|
|
23
|
+
html, body {
|
|
24
|
+
margin: 0;
|
|
25
|
+
width: 100%;
|
|
26
|
+
height: 100%;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
background: ${bg};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
iframe {
|
|
32
|
+
display: block;
|
|
33
|
+
width: 100%;
|
|
34
|
+
height: 100%;
|
|
35
|
+
border: 0;
|
|
36
|
+
background: ${bg};
|
|
37
|
+
}
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<iframe id="scenario-frame" title="Scenario Preview" src="${escapedUrl}"></iframe>
|
|
42
|
+
</body>
|
|
43
|
+
</html>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function collectContentState(target) {
|
|
47
|
+
return target.evaluate(() => {
|
|
48
|
+
const root = document.getElementById("root");
|
|
49
|
+
const imgs = Array.from(document.images || []);
|
|
50
|
+
const loadedImageCount = imgs.filter(
|
|
51
|
+
(img) => img.complete && img.naturalWidth > 0,
|
|
52
|
+
).length;
|
|
53
|
+
const mediaSelectors = ["svg", "canvas", "video"];
|
|
54
|
+
let mediaBboxCount = 0;
|
|
55
|
+
for (const selector of mediaSelectors) {
|
|
56
|
+
const nodes = document.querySelectorAll(selector);
|
|
57
|
+
for (const node of nodes) {
|
|
58
|
+
const rect = node.getBoundingClientRect();
|
|
59
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
60
|
+
mediaBboxCount += 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
bodyTextLength: document.body.innerText.trim().length,
|
|
66
|
+
rootChildCount: root ? root.childElementCount : 0,
|
|
67
|
+
rootTextLength: root ? (root.textContent || "").trim().length : 0,
|
|
68
|
+
imageCount: imgs.length,
|
|
69
|
+
loadedImageCount,
|
|
70
|
+
mediaBboxCount,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function collectImageStates(target) {
|
|
76
|
+
return target.evaluate(() =>
|
|
77
|
+
Array.from(document.images || []).map((img) => ({
|
|
78
|
+
complete: img.complete,
|
|
79
|
+
naturalWidth: img.naturalWidth,
|
|
80
|
+
src: img.currentSrc || img.src || "",
|
|
81
|
+
})),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function waitForImagesSettled(
|
|
86
|
+
target,
|
|
87
|
+
{ overallTimeoutMs = 5000, pollIntervalMs = 100 } = {},
|
|
88
|
+
) {
|
|
89
|
+
const started = Date.now();
|
|
90
|
+
let images = await collectImageStates(target);
|
|
91
|
+
while (
|
|
92
|
+
!shouldStopWaitingForImages(images, {
|
|
93
|
+
elapsedMs: Date.now() - started,
|
|
94
|
+
overallTimeoutMs,
|
|
95
|
+
})
|
|
96
|
+
) {
|
|
97
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
98
|
+
images = await collectImageStates(target);
|
|
99
|
+
}
|
|
100
|
+
const elapsedMs = Date.now() - started;
|
|
101
|
+
const allComplete = images.every((img) => img && img.complete === true);
|
|
102
|
+
return { settled: allComplete, images, elapsedMs };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function waitForAnimationsSettled(
|
|
106
|
+
target,
|
|
107
|
+
{ timeoutMs = 2000, pollIntervalMs = 100 } = {},
|
|
108
|
+
) {
|
|
109
|
+
const started = Date.now();
|
|
110
|
+
while (Date.now() - started < timeoutMs) {
|
|
111
|
+
const runningCount = await target.evaluate(() =>
|
|
112
|
+
document
|
|
113
|
+
.getAnimations()
|
|
114
|
+
.filter((a) => a.playState === "running").length,
|
|
115
|
+
);
|
|
116
|
+
if (runningCount === 0) {
|
|
117
|
+
return { settled: true, elapsedMs: Date.now() - started };
|
|
118
|
+
}
|
|
119
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
120
|
+
}
|
|
121
|
+
return { settled: false, elapsedMs: Date.now() - started };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function waitForStablePage(page, target, timeoutMs = 10000) {
|
|
125
|
+
const started = Date.now();
|
|
126
|
+
let lastHtml = "";
|
|
127
|
+
let stableCount = 0;
|
|
128
|
+
|
|
129
|
+
while (Date.now() - started < timeoutMs) {
|
|
130
|
+
await page.waitForTimeout(500);
|
|
131
|
+
|
|
132
|
+
const pageState = await target.evaluate(() => ({
|
|
133
|
+
bodyText: document.body.innerText,
|
|
134
|
+
html: document.body.innerHTML,
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
if (!hasLoadingMarkers(pageState.bodyText) && pageState.html === lastHtml) {
|
|
138
|
+
stableCount += 1;
|
|
139
|
+
if (stableCount >= 2) {
|
|
140
|
+
const remaining = () => Math.max(0, timeoutMs - (Date.now() - started));
|
|
141
|
+
await waitForAnimationsSettled(target, {
|
|
142
|
+
timeoutMs: Math.min(2000, remaining()),
|
|
143
|
+
});
|
|
144
|
+
await waitForImagesSettled(target, { overallTimeoutMs: remaining() });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
stableCount = 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
lastHtml = pageState.html;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function loadScenarioInIframe(page, url, { background } = {}) {
|
|
156
|
+
const responsePromise = page
|
|
157
|
+
.waitForResponse(
|
|
158
|
+
(response) =>
|
|
159
|
+
response.request().resourceType() === "document" &&
|
|
160
|
+
response.url() === url,
|
|
161
|
+
{ timeout: 30000 },
|
|
162
|
+
)
|
|
163
|
+
.catch(() => null);
|
|
164
|
+
|
|
165
|
+
await page.setContent(buildIframeHarness(url, { background }), {
|
|
166
|
+
waitUntil: "domcontentloaded",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const frameHandle = await page.waitForSelector("#scenario-frame", {
|
|
170
|
+
state: "attached",
|
|
171
|
+
timeout: 30000,
|
|
172
|
+
});
|
|
173
|
+
const frame = await frameHandle.contentFrame();
|
|
174
|
+
if (!frame) {
|
|
175
|
+
throw new Error("Scenario iframe did not attach");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await frame.waitForLoadState("load", { timeout: 30000 });
|
|
179
|
+
const response = await responsePromise;
|
|
180
|
+
return { frame, response };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
escapeHtmlAttribute,
|
|
185
|
+
buildIframeHarness,
|
|
186
|
+
collectContentState,
|
|
187
|
+
collectImageStates,
|
|
188
|
+
waitForImagesSettled,
|
|
189
|
+
waitForAnimationsSettled,
|
|
190
|
+
waitForStablePage,
|
|
191
|
+
loadScenarioInIframe,
|
|
192
|
+
};
|