@ralphralphai/playwright 2.3.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-CRXt3vt1.cjs → upload-D40MqSQa.cjs} +267 -33
- package/dist/upload-D40MqSQa.cjs.map +1 -0
- package/dist/{upload-CEc_qo01.mjs → upload-jup_jLlm.mjs} +267 -33
- package/dist/upload-jup_jLlm.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/upload-CEc_qo01.mjs.map +0 -1
- package/dist/upload-CRXt3vt1.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";
|
|
@@ -522,6 +522,54 @@ async function waitForAnimations(page, timeoutMs) {
|
|
|
522
522
|
if (!(error instanceof errors.TimeoutError)) throw error;
|
|
523
523
|
}
|
|
524
524
|
}
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/image_responses.ts
|
|
527
|
+
const IMAGE_HASH_TIMEOUT_MS = 3e4;
|
|
528
|
+
const hashesByPage = /* @__PURE__ */ new WeakMap();
|
|
529
|
+
function sha256(data) {
|
|
530
|
+
return createHash("sha256").update(data).digest("hex");
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Hashes every image the page loads, as it loads, so a snapshot hashes the
|
|
534
|
+
* bytes the page showed instead of fetching them a second time. A second fetch
|
|
535
|
+
* of a page's images comes as a burst, which hosts such as
|
|
536
|
+
* upload.wikimedia.org answer with 429 for a share of them, and the image then
|
|
537
|
+
* reads as replaced. Only the hash is kept, not the body. Entries persist
|
|
538
|
+
* across navigations until another request for the same image replaces them
|
|
539
|
+
* or the page closes.
|
|
540
|
+
*/
|
|
541
|
+
function watchImageResponses(page) {
|
|
542
|
+
if (hashesByPage.has(page)) return;
|
|
543
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
544
|
+
hashesByPage.set(page, hashes);
|
|
545
|
+
page.on("request", (request) => {
|
|
546
|
+
if (request.resourceType() !== "image") return;
|
|
547
|
+
const hash = request.response().then(async (response) => {
|
|
548
|
+
return response?.ok() ? sha256(await response.body()) : void 0;
|
|
549
|
+
}).catch(() => void 0);
|
|
550
|
+
for (let redirected = request; redirected !== null; redirected = redirected.redirectedFrom()) hashes.set(redirected.url(), hash);
|
|
551
|
+
});
|
|
552
|
+
page.once("close", () => hashesByPage.delete(page));
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* The hash from the latest observed request for this URL, across navigations.
|
|
556
|
+
* Undefined when no request was observed, the latest load failed, or it has
|
|
557
|
+
* not finished within the timeout, so an image that never finishes loading
|
|
558
|
+
* cannot stall a snapshot.
|
|
559
|
+
*/
|
|
560
|
+
async function loadedImageHash(page, url, timeoutMs = IMAGE_HASH_TIMEOUT_MS) {
|
|
561
|
+
const hash = hashesByPage.get(page)?.get(url.split("#")[0]);
|
|
562
|
+
if (hash === void 0) return;
|
|
563
|
+
let timer;
|
|
564
|
+
const timeout = new Promise((resolve) => {
|
|
565
|
+
timer = setTimeout(resolve, timeoutMs, void 0);
|
|
566
|
+
});
|
|
567
|
+
try {
|
|
568
|
+
return await Promise.race([hash, timeout]);
|
|
569
|
+
} finally {
|
|
570
|
+
clearTimeout(timer);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
525
573
|
const MAX_ARIA_DEPTH = Math.floor(128 / 2) - 2;
|
|
526
574
|
const STYLE_PROPERTIES = [
|
|
527
575
|
"font-family",
|
|
@@ -655,6 +703,8 @@ function keepShown(node, viewportWidth) {
|
|
|
655
703
|
return kept === void 0 ? [] : [kept];
|
|
656
704
|
});
|
|
657
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;
|
|
658
708
|
return node.children === void 0 ? node : {
|
|
659
709
|
...node,
|
|
660
710
|
children
|
|
@@ -681,7 +731,8 @@ async function readElement(page, ref, isImage) {
|
|
|
681
731
|
try {
|
|
682
732
|
return await page.locator(`aria-ref=${ref}`).evaluate((element, { properties, isImage, dynamicAttribute }) => {
|
|
683
733
|
const computed = getComputedStyle(element);
|
|
684
|
-
const
|
|
734
|
+
const typed = "computedStyleMap" in element ? element.computedStyleMap() : void 0;
|
|
735
|
+
const style = Object.fromEntries(properties.map((property) => [property, property.startsWith("margin-") && typed?.get(property)?.toString() === "auto" ? "auto" : computed.getPropertyValue(property)]));
|
|
685
736
|
const dynamic = element.closest(`[${dynamicAttribute}]`) !== null;
|
|
686
737
|
if (!isImage) return {
|
|
687
738
|
style,
|
|
@@ -719,23 +770,11 @@ async function readElement(page, ref, isImage) {
|
|
|
719
770
|
return;
|
|
720
771
|
}
|
|
721
772
|
}
|
|
722
|
-
function sha256(data) {
|
|
723
|
-
return createHash("sha256").update(data).digest("hex");
|
|
724
|
-
}
|
|
725
773
|
async function hashImage(page, image) {
|
|
726
774
|
if ("markup" in image) return sha256(image.markup);
|
|
727
775
|
if (image.src === "") return;
|
|
728
776
|
if (image.src.startsWith("data:")) return sha256(image.src);
|
|
729
|
-
|
|
730
|
-
const response = await page.request.get(image.src);
|
|
731
|
-
try {
|
|
732
|
-
return response.ok() ? sha256(await response.body()) : void 0;
|
|
733
|
-
} finally {
|
|
734
|
-
await response.dispose();
|
|
735
|
-
}
|
|
736
|
-
} catch {
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
777
|
+
return loadedImageHash(page, image.src);
|
|
739
778
|
}
|
|
740
779
|
function distinctStyle(style, inherited) {
|
|
741
780
|
const distinct = Object.entries(style).filter(([property, value]) => {
|
|
@@ -763,6 +802,179 @@ async function describe(page, child, inherited, insideDynamic) {
|
|
|
763
802
|
};
|
|
764
803
|
}
|
|
765
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
|
|
766
978
|
//#region src/conditions.ts
|
|
767
979
|
const declared = (declaration = {}) => {
|
|
768
980
|
const conditions = {};
|
|
@@ -824,11 +1036,11 @@ function nodeKey(conditions) {
|
|
|
824
1036
|
*/
|
|
825
1037
|
const WEBP_QUALITY = 80;
|
|
826
1038
|
const WEBP_MAX_DIMENSION = 16383;
|
|
827
|
-
async function takeScreenshot(page, deadline) {
|
|
1039
|
+
async function takeScreenshot(page, deadline, frozen = false) {
|
|
828
1040
|
const options = {
|
|
829
1041
|
fullPage: true,
|
|
830
1042
|
scale: "css",
|
|
831
|
-
animations: "disabled"
|
|
1043
|
+
animations: frozen ? "allow" : "disabled"
|
|
832
1044
|
};
|
|
833
1045
|
const webp = await page.screenshot({
|
|
834
1046
|
...options,
|
|
@@ -1057,22 +1269,30 @@ var Recorder = class {
|
|
|
1057
1269
|
/** Starts recording URL changes in every current and future tab of `context`. */
|
|
1058
1270
|
observe(context) {
|
|
1059
1271
|
if (this.closed) throw new Error("The Ralph recorder has finished.");
|
|
1060
|
-
|
|
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();
|
|
1061
1276
|
const onPage = (page) => this.watchPage(page);
|
|
1062
|
-
this.contexts.set(context,
|
|
1277
|
+
this.contexts.set(context, {
|
|
1278
|
+
onPage,
|
|
1279
|
+
ready,
|
|
1280
|
+
freezer
|
|
1281
|
+
});
|
|
1063
1282
|
this.actions.observe(context);
|
|
1064
1283
|
context.on("page", onPage);
|
|
1065
1284
|
for (const page of context.pages()) onPage(page);
|
|
1285
|
+
return ready;
|
|
1066
1286
|
}
|
|
1067
1287
|
async recordNode(page, options) {
|
|
1068
|
-
this.observe(page.context());
|
|
1288
|
+
await this.observe(page.context());
|
|
1069
1289
|
const state = this.pages.get(page);
|
|
1070
1290
|
if (!state) throw new Error("Cannot record a closed browser page.");
|
|
1071
1291
|
if (this.paused) return;
|
|
1072
1292
|
await this.enqueueCreateNodeRequest(state, "explicit", options);
|
|
1073
1293
|
}
|
|
1074
1294
|
async setNodeConditions(page, declaration) {
|
|
1075
|
-
this.observe(page.context());
|
|
1295
|
+
await this.observe(page.context());
|
|
1076
1296
|
const state = this.pages.get(page);
|
|
1077
1297
|
if (!state) throw new Error("Cannot tag a closed browser page.");
|
|
1078
1298
|
const key = Symbol();
|
|
@@ -1103,8 +1323,11 @@ var Recorder = class {
|
|
|
1103
1323
|
}
|
|
1104
1324
|
/** Stops watching `context` and waits for its queued recordings. */
|
|
1105
1325
|
async stopContext(context) {
|
|
1106
|
-
const
|
|
1107
|
-
if (
|
|
1326
|
+
const observed = this.contexts.get(context);
|
|
1327
|
+
if (observed) {
|
|
1328
|
+
context.off("page", observed.onPage);
|
|
1329
|
+
await observed.ready;
|
|
1330
|
+
}
|
|
1108
1331
|
const states = [...this.pages.values()].filter((state) => state.page.context() === context);
|
|
1109
1332
|
for (const state of states) state.page.off("framenavigated", state.onNavigation);
|
|
1110
1333
|
await Promise.all(states.map((state) => state.tail));
|
|
@@ -1157,6 +1380,7 @@ var Recorder = class {
|
|
|
1157
1380
|
}
|
|
1158
1381
|
}).catch(() => void 0);
|
|
1159
1382
|
page.on("framenavigated", state.onNavigation);
|
|
1383
|
+
watchImageResponses(page);
|
|
1160
1384
|
this.urlChanged(state);
|
|
1161
1385
|
}
|
|
1162
1386
|
urlChanged(state) {
|
|
@@ -1219,6 +1443,7 @@ var Recorder = class {
|
|
|
1219
1443
|
};
|
|
1220
1444
|
try {
|
|
1221
1445
|
check();
|
|
1446
|
+
await wait(this.contexts.get(page.context()).ready);
|
|
1222
1447
|
if (request.trigger !== "explicit") await this.waitForPageToSettle(page, deadline, check);
|
|
1223
1448
|
check();
|
|
1224
1449
|
const conditions = this.declaredConditions(state);
|
|
@@ -1231,18 +1456,27 @@ var Recorder = class {
|
|
|
1231
1456
|
warnedNoTracker = true;
|
|
1232
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.");
|
|
1233
1458
|
}
|
|
1234
|
-
|
|
1459
|
+
const freezer = this.contexts.get(page.context()).freezer;
|
|
1460
|
+
if (this.options.reduceMotion && !freezer) {
|
|
1235
1461
|
check();
|
|
1236
1462
|
await wait(waitForAnimations(page, (deadline.endsAt - Date.now()) / 2));
|
|
1237
1463
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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();
|
|
1246
1480
|
checkPage();
|
|
1247
1481
|
const recorded = {
|
|
1248
1482
|
...request,
|
|
@@ -1673,4 +1907,4 @@ async function responseDetail(response) {
|
|
|
1673
1907
|
//#endregion
|
|
1674
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 };
|
|
1675
1909
|
|
|
1676
|
-
//# sourceMappingURL=upload-
|
|
1910
|
+
//# sourceMappingURL=upload-jup_jLlm.mjs.map
|