@ralphralphai/playwright 2.4.0 → 2.5.0
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/README.md +44 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +9 -1
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/dist/reporter.cjs +1 -1
- package/dist/reporter.mjs +1 -1
- package/dist/{upload-FIxKqKb3.cjs → upload-D40MqSQa.cjs} +215 -19
- package/dist/upload-D40MqSQa.cjs.map +1 -0
- package/dist/{upload-CUtC_rnN.mjs → upload-jup_jLlm.mjs} +215 -19
- package/dist/upload-jup_jLlm.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/upload-CUtC_rnN.mjs.map +0 -1
- package/dist/upload-FIxKqKb3.cjs.map +0 -1
|
@@ -45,7 +45,7 @@ function snapshot(value) {
|
|
|
45
45
|
}
|
|
46
46
|
//#endregion
|
|
47
47
|
//#region package.json
|
|
48
|
-
var version = "2.
|
|
48
|
+
var version = "2.5.0";
|
|
49
49
|
//#endregion
|
|
50
50
|
//#region src/actions.ts
|
|
51
51
|
const BINDING = "__ralphAction";
|
|
@@ -703,6 +703,8 @@ function keepShown(node, viewportWidth) {
|
|
|
703
703
|
return kept === void 0 ? [] : [kept];
|
|
704
704
|
});
|
|
705
705
|
if (!shown && children.length === 0) return;
|
|
706
|
+
const [only] = children;
|
|
707
|
+
if (node.role === "generic" && !node.name && (node.children?.length ?? 0) > 1 && children.length === 1 && typeof only !== "string" && only?.ref !== void 0) return only;
|
|
706
708
|
return node.children === void 0 ? node : {
|
|
707
709
|
...node,
|
|
708
710
|
children
|
|
@@ -800,6 +802,179 @@ async function describe(page, child, inherited, insideDynamic) {
|
|
|
800
802
|
};
|
|
801
803
|
}
|
|
802
804
|
//#endregion
|
|
805
|
+
//#region src/animation_observer.ts
|
|
806
|
+
/** Runs in the page. Keeps native animations and DOM changes observable without app hooks. */
|
|
807
|
+
function observeAnimations() {
|
|
808
|
+
const paused = /* @__PURE__ */ new Set();
|
|
809
|
+
const roots = /* @__PURE__ */ new Set();
|
|
810
|
+
let dirty = true;
|
|
811
|
+
const observer = new MutationObserver(() => {
|
|
812
|
+
dirty = true;
|
|
813
|
+
});
|
|
814
|
+
const collect = (root, all) => {
|
|
815
|
+
if (!roots.has(root)) {
|
|
816
|
+
roots.add(root);
|
|
817
|
+
observer.observe(root, {
|
|
818
|
+
subtree: true,
|
|
819
|
+
childList: true,
|
|
820
|
+
attributes: true,
|
|
821
|
+
characterData: true
|
|
822
|
+
});
|
|
823
|
+
dirty = true;
|
|
824
|
+
}
|
|
825
|
+
root.getAnimations().forEach((animation) => all.add(animation));
|
|
826
|
+
for (const element of root.querySelectorAll("*")) if (element.shadowRoot) collect(element.shadowRoot, all);
|
|
827
|
+
};
|
|
828
|
+
return {
|
|
829
|
+
sample(finishFinite) {
|
|
830
|
+
const all = /* @__PURE__ */ new Set();
|
|
831
|
+
collect(document, all);
|
|
832
|
+
let changed = dirty;
|
|
833
|
+
dirty = false;
|
|
834
|
+
for (const animation of all) {
|
|
835
|
+
if (animation.playState !== "running") continue;
|
|
836
|
+
if (finishFinite && animation.playbackRate !== 0 && Number.isFinite(animation.effect?.getComputedTiming().endTime)) animation.finish();
|
|
837
|
+
else {
|
|
838
|
+
animation.pause();
|
|
839
|
+
paused.add(animation);
|
|
840
|
+
}
|
|
841
|
+
changed = true;
|
|
842
|
+
}
|
|
843
|
+
return changed;
|
|
844
|
+
},
|
|
845
|
+
async ready() {
|
|
846
|
+
await Promise.all([...paused].map((animation) => animation.ready));
|
|
847
|
+
},
|
|
848
|
+
resume() {
|
|
849
|
+
observer.disconnect();
|
|
850
|
+
for (const animation of paused) if (animation.playState === "paused") animation.play();
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region src/capture_freezer.ts
|
|
856
|
+
const PAUSE_LEAD_MS = 100;
|
|
857
|
+
const ANIMATION_QUIET_MS = 150;
|
|
858
|
+
const MAX_ANIMATION_SETTLE_MS = 1500;
|
|
859
|
+
/** One owner of the context-wide clock; captures in different tabs queue here. */
|
|
860
|
+
var CaptureFreezer = class {
|
|
861
|
+
context;
|
|
862
|
+
tail = Promise.resolve();
|
|
863
|
+
closed = false;
|
|
864
|
+
constructor(context) {
|
|
865
|
+
this.context = context;
|
|
866
|
+
context.once("close", () => {
|
|
867
|
+
this.closed = true;
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
install() {
|
|
871
|
+
if (this.context.pages().some((page) => page.url() !== "about:blank")) throw new Error("Ralph freezeDuringCapture requires the clock before page scripts run. Await ralph.observe(context) before creating or navigating pages in a custom context.");
|
|
872
|
+
return this.context.clock.install();
|
|
873
|
+
}
|
|
874
|
+
async capture(page, signal, capture, deadline = Date.now() + 5e3) {
|
|
875
|
+
const task = this.tail.then(async () => {
|
|
876
|
+
signal.throwIfAborted();
|
|
877
|
+
const animations = /* @__PURE__ */ new Map();
|
|
878
|
+
try {
|
|
879
|
+
await this.settleAnimations(page, animations, signal, deadline);
|
|
880
|
+
await this.pauseClock(page, signal);
|
|
881
|
+
signal.throwIfAborted();
|
|
882
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
883
|
+
await this.sampleAnimations(page, animations, signal, false);
|
|
884
|
+
await this.waitForAnimationsReady(animations, signal);
|
|
885
|
+
}
|
|
886
|
+
return await capture();
|
|
887
|
+
} finally {
|
|
888
|
+
try {
|
|
889
|
+
await this.resumeAnimations(animations);
|
|
890
|
+
} finally {
|
|
891
|
+
if (!this.closed) await this.context.clock.resume();
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
this.tail = task.catch(() => void 0);
|
|
896
|
+
return task;
|
|
897
|
+
}
|
|
898
|
+
async settleAnimations(page, animations, signal, deadline) {
|
|
899
|
+
const now = Date.now();
|
|
900
|
+
const endsAt = now + Math.min(MAX_ANIMATION_SETTLE_MS, Math.max(0, (deadline - now) / 2));
|
|
901
|
+
let quietSince = now;
|
|
902
|
+
while (Date.now() < endsAt) {
|
|
903
|
+
if (await this.sampleAnimations(page, animations, signal, true)) quietSince = Date.now();
|
|
904
|
+
if (Date.now() - quietSince >= ANIMATION_QUIET_MS) return;
|
|
905
|
+
await setTimeout$1(Math.max(0, Math.min(50, endsAt - Date.now())), void 0, { signal }).catch((error) => {
|
|
906
|
+
signal.throwIfAborted();
|
|
907
|
+
throw error;
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
async sampleAnimations(page, animations, signal, finishFinite) {
|
|
912
|
+
let changed = false;
|
|
913
|
+
for (const frame of page.frames()) {
|
|
914
|
+
signal.throwIfAborted();
|
|
915
|
+
try {
|
|
916
|
+
let handle = animations.get(frame);
|
|
917
|
+
if (!handle) {
|
|
918
|
+
handle = await frame.evaluateHandle(observeAnimations);
|
|
919
|
+
animations.set(frame, handle);
|
|
920
|
+
}
|
|
921
|
+
changed = await handle.evaluate((state, finish) => state.sample(finish), finishFinite) || changed;
|
|
922
|
+
} catch (error) {
|
|
923
|
+
if (!frame.isDetached()) throw error;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
signal.throwIfAborted();
|
|
927
|
+
return changed;
|
|
928
|
+
}
|
|
929
|
+
async waitForAnimationsReady(animations, signal) {
|
|
930
|
+
signal.throwIfAborted();
|
|
931
|
+
let onAbort;
|
|
932
|
+
const aborted = new Promise((_, reject) => {
|
|
933
|
+
onAbort = () => reject(signal.reason);
|
|
934
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
935
|
+
});
|
|
936
|
+
try {
|
|
937
|
+
await Promise.race([Promise.all([...animations].map(async ([frame, handle]) => {
|
|
938
|
+
if (frame.isDetached()) return;
|
|
939
|
+
try {
|
|
940
|
+
await handle.evaluate((state) => state.ready());
|
|
941
|
+
} catch (error) {
|
|
942
|
+
if (!frame.isDetached()) throw error;
|
|
943
|
+
}
|
|
944
|
+
})), aborted]);
|
|
945
|
+
} finally {
|
|
946
|
+
signal.removeEventListener("abort", onAbort);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
async resumeAnimations(animations) {
|
|
950
|
+
const restored = await Promise.allSettled([...animations.values()].map(async (handle) => {
|
|
951
|
+
try {
|
|
952
|
+
await handle.evaluate((state) => state.resume());
|
|
953
|
+
} catch (error) {
|
|
954
|
+
if (!destroyedContext(error)) throw error;
|
|
955
|
+
} finally {
|
|
956
|
+
await handle.dispose();
|
|
957
|
+
}
|
|
958
|
+
}));
|
|
959
|
+
for (const result of restored) if (result.status === "rejected") throw result.reason;
|
|
960
|
+
}
|
|
961
|
+
async pauseClock(page, signal) {
|
|
962
|
+
for (let attempt = 0;; attempt++) {
|
|
963
|
+
const time = await page.evaluate(() => Date.now());
|
|
964
|
+
signal.throwIfAborted();
|
|
965
|
+
try {
|
|
966
|
+
await this.context.clock.pauseAt(time + PAUSE_LEAD_MS);
|
|
967
|
+
return;
|
|
968
|
+
} catch (error) {
|
|
969
|
+
if (attempt > 0 || !(error instanceof Error) || !error.message.includes("Cannot fast-forward to the past")) throw error;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
function destroyedContext(error) {
|
|
975
|
+
return error instanceof Error && /Execution context was destroyed|Cannot find context|Target page, context or browser has been closed|Frame was detached/.test(error.message);
|
|
976
|
+
}
|
|
977
|
+
//#endregion
|
|
803
978
|
//#region src/conditions.ts
|
|
804
979
|
const declared = (declaration = {}) => {
|
|
805
980
|
const conditions = {};
|
|
@@ -861,11 +1036,11 @@ function nodeKey(conditions) {
|
|
|
861
1036
|
*/
|
|
862
1037
|
const WEBP_QUALITY = 80;
|
|
863
1038
|
const WEBP_MAX_DIMENSION = 16383;
|
|
864
|
-
async function takeScreenshot(page, deadline) {
|
|
1039
|
+
async function takeScreenshot(page, deadline, frozen = false) {
|
|
865
1040
|
const options = {
|
|
866
1041
|
fullPage: true,
|
|
867
1042
|
scale: "css",
|
|
868
|
-
animations: "disabled"
|
|
1043
|
+
animations: frozen ? "allow" : "disabled"
|
|
869
1044
|
};
|
|
870
1045
|
const webp = await page.screenshot({
|
|
871
1046
|
...options,
|
|
@@ -1094,22 +1269,30 @@ var Recorder = class {
|
|
|
1094
1269
|
/** Starts recording URL changes in every current and future tab of `context`. */
|
|
1095
1270
|
observe(context) {
|
|
1096
1271
|
if (this.closed) throw new Error("The Ralph recorder has finished.");
|
|
1097
|
-
|
|
1272
|
+
const existing = this.contexts.get(context);
|
|
1273
|
+
if (existing) return existing.ready;
|
|
1274
|
+
const freezer = this.options.freezeDuringCapture ? new CaptureFreezer(context) : void 0;
|
|
1275
|
+
const ready = freezer?.install() ?? Promise.resolve();
|
|
1098
1276
|
const onPage = (page) => this.watchPage(page);
|
|
1099
|
-
this.contexts.set(context,
|
|
1277
|
+
this.contexts.set(context, {
|
|
1278
|
+
onPage,
|
|
1279
|
+
ready,
|
|
1280
|
+
freezer
|
|
1281
|
+
});
|
|
1100
1282
|
this.actions.observe(context);
|
|
1101
1283
|
context.on("page", onPage);
|
|
1102
1284
|
for (const page of context.pages()) onPage(page);
|
|
1285
|
+
return ready;
|
|
1103
1286
|
}
|
|
1104
1287
|
async recordNode(page, options) {
|
|
1105
|
-
this.observe(page.context());
|
|
1288
|
+
await this.observe(page.context());
|
|
1106
1289
|
const state = this.pages.get(page);
|
|
1107
1290
|
if (!state) throw new Error("Cannot record a closed browser page.");
|
|
1108
1291
|
if (this.paused) return;
|
|
1109
1292
|
await this.enqueueCreateNodeRequest(state, "explicit", options);
|
|
1110
1293
|
}
|
|
1111
1294
|
async setNodeConditions(page, declaration) {
|
|
1112
|
-
this.observe(page.context());
|
|
1295
|
+
await this.observe(page.context());
|
|
1113
1296
|
const state = this.pages.get(page);
|
|
1114
1297
|
if (!state) throw new Error("Cannot tag a closed browser page.");
|
|
1115
1298
|
const key = Symbol();
|
|
@@ -1140,8 +1323,11 @@ var Recorder = class {
|
|
|
1140
1323
|
}
|
|
1141
1324
|
/** Stops watching `context` and waits for its queued recordings. */
|
|
1142
1325
|
async stopContext(context) {
|
|
1143
|
-
const
|
|
1144
|
-
if (
|
|
1326
|
+
const observed = this.contexts.get(context);
|
|
1327
|
+
if (observed) {
|
|
1328
|
+
context.off("page", observed.onPage);
|
|
1329
|
+
await observed.ready;
|
|
1330
|
+
}
|
|
1145
1331
|
const states = [...this.pages.values()].filter((state) => state.page.context() === context);
|
|
1146
1332
|
for (const state of states) state.page.off("framenavigated", state.onNavigation);
|
|
1147
1333
|
await Promise.all(states.map((state) => state.tail));
|
|
@@ -1257,6 +1443,7 @@ var Recorder = class {
|
|
|
1257
1443
|
};
|
|
1258
1444
|
try {
|
|
1259
1445
|
check();
|
|
1446
|
+
await wait(this.contexts.get(page.context()).ready);
|
|
1260
1447
|
if (request.trigger !== "explicit") await this.waitForPageToSettle(page, deadline, check);
|
|
1261
1448
|
check();
|
|
1262
1449
|
const conditions = this.declaredConditions(state);
|
|
@@ -1269,18 +1456,27 @@ var Recorder = class {
|
|
|
1269
1456
|
warnedNoTracker = true;
|
|
1270
1457
|
console.warn("Ralph: this page runs no tracker, so the node conditions the test declares cannot be checked against the tags the app reports. Production events must carry the same tags, or these nodes will match no visitors.");
|
|
1271
1458
|
}
|
|
1272
|
-
|
|
1459
|
+
const freezer = this.contexts.get(page.context()).freezer;
|
|
1460
|
+
if (this.options.reduceMotion && !freezer) {
|
|
1273
1461
|
check();
|
|
1274
1462
|
await wait(waitForAnimations(page, (deadline.endsAt - Date.now()) / 2));
|
|
1275
1463
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1464
|
+
const capture = async () => {
|
|
1465
|
+
check();
|
|
1466
|
+
const before = await wait(readLayout(page));
|
|
1467
|
+
check();
|
|
1468
|
+
const image = await wait(takeScreenshot(page, deadline.endsAt, !!freezer));
|
|
1469
|
+
check();
|
|
1470
|
+
const after = await wait(readLayout(page));
|
|
1471
|
+
check();
|
|
1472
|
+
return {
|
|
1473
|
+
before,
|
|
1474
|
+
image,
|
|
1475
|
+
after,
|
|
1476
|
+
snapshot: await Promise.race([captureAriaSnapshot(page, after).catch(() => void 0), setTimeout$1(Math.max(0, deadline.endsAt - Date.now()), void 0, { ref: false })])
|
|
1477
|
+
};
|
|
1478
|
+
};
|
|
1479
|
+
const { before, image, after, snapshot } = freezer ? await freezer.capture(page, deadline.signal, capture, deadline.endsAt) : await capture();
|
|
1284
1480
|
checkPage();
|
|
1285
1481
|
const recorded = {
|
|
1286
1482
|
...request,
|
|
@@ -1711,4 +1907,4 @@ async function responseDetail(response) {
|
|
|
1711
1907
|
//#endregion
|
|
1712
1908
|
export { resolveMode as _, prepareArtifactUpload as a, resultLink as c, REPORTER_ENV as d, Recorder as f, loadConfigSync as g, loadConfig as h, createRawGraphBundle as i, uploadRawGraph as l, version as m, bundleImagePath as n, registerDeployment as o, visitAllLinks as p, bundleSnapshotPath as r, resolveUploadOptions as s, RAW_GRAPH_BUNDLE_INDEX as t, MANIFEST_ATTACHMENT as u };
|
|
1713
1909
|
|
|
1714
|
-
//# sourceMappingURL=upload-
|
|
1910
|
+
//# sourceMappingURL=upload-jup_jLlm.mjs.map
|