qapture2 0.8.0 → 0.8.3
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 +6 -0
- package/dist/bin/init.cjs +1 -1
- package/dist/{chunk-RJV6FTIB.js → chunk-32LKZ2PG.js} +291 -30
- package/dist/{chunk-E7OFH6EM.cjs → chunk-OSYYMS6R.cjs} +291 -30
- package/dist/index.cjs +5 -5
- package/dist/index.js +1 -1
- package/dist/next.cjs +4 -4
- package/dist/next.js +1 -1
- package/dist/standalone.cjs +2 -2
- package/dist/standalone.js +1 -1
- package/package.json +1 -1
|
@@ -1725,11 +1725,15 @@ var STR = {
|
|
|
1725
1725
|
sel_region: "Region selected",
|
|
1726
1726
|
sel_element: "Element selected",
|
|
1727
1727
|
capturing: "capturing screenshot\u2026",
|
|
1728
|
+
too_heavy: "This page is too big to redraw \u2014 the tab would freeze. Photograph it instead: one frame, and maps and charts come out right.",
|
|
1728
1729
|
no_shot: "no screenshot (location saved)",
|
|
1729
1730
|
annotate_placeholder: "What do you want to do here? (add / remove / change\u2026)",
|
|
1730
1731
|
save_point: "Save point",
|
|
1732
|
+
save_next: "Save + next",
|
|
1733
|
+
save_next_hint: "Save this one and mark up another part of the same screenshot \u2014 no second permission prompt.",
|
|
1734
|
+
shot_reused: "{n} saved from this screenshot",
|
|
1731
1735
|
reselect: "Reselect",
|
|
1732
|
-
save_hint: "\u2318/Ctrl + Enter to save",
|
|
1736
|
+
save_hint: "\u2318/Ctrl + Enter to save \xB7 add Shift to keep the same screenshot",
|
|
1733
1737
|
login_with: "Log in with {field} + password.",
|
|
1734
1738
|
used_count: "{n}/{m} used",
|
|
1735
1739
|
used: "used",
|
|
@@ -1765,6 +1769,9 @@ var STR = {
|
|
|
1765
1769
|
sev_design: "Design",
|
|
1766
1770
|
sev_enhance: "Enhance",
|
|
1767
1771
|
status_open: "Open",
|
|
1772
|
+
status_sent: "Sent",
|
|
1773
|
+
check_fixes: "Check the {n} fixes",
|
|
1774
|
+
check_fixes_hint: "Walk back through everything the last export sent, one spot at a time, and say whether each is now what you asked for.",
|
|
1768
1775
|
status_verified: "Verified",
|
|
1769
1776
|
context_attached: "{n} runtime events attached",
|
|
1770
1777
|
start_walkthrough: "Start walkthrough",
|
|
@@ -1933,11 +1940,15 @@ var STR = {
|
|
|
1933
1940
|
sel_region: "\u062A\u0645 \u062A\u062D\u062F\u064A\u062F \u0645\u0646\u0637\u0642\u0629",
|
|
1934
1941
|
sel_element: "\u062A\u0645 \u062A\u062D\u062F\u064A\u062F \u0639\u0646\u0635\u0631",
|
|
1935
1942
|
capturing: "\u064A\u062A\u0645 \u0627\u0644\u062A\u0642\u0627\u0637 \u0627\u0644\u0635\u0648\u0631\u0629\u2026",
|
|
1943
|
+
too_heavy: "\u0647\u0630\u0647 \u0627\u0644\u0635\u0641\u062D\u0629 \u0623\u0643\u0628\u0631 \u0645\u0646 \u0623\u0646 \u064A\u064F\u0639\u0627\u062F \u0631\u0633\u0645\u0647\u0627 \u2014 \u0633\u064A\u062A\u062C\u0645\u0651\u062F \u0627\u0644\u062A\u0628\u0648\u064A\u0628. \u0635\u0648\u0651\u0631\u0647\u0627 \u0628\u062F\u0644 \u0630\u0644\u0643: \u0625\u0637\u0627\u0631 \u0648\u0627\u062D\u062F\u060C \u0648\u062A\u0638\u0647\u0631 \u0627\u0644\u062E\u0631\u0627\u0626\u0637 \u0648\u0627\u0644\u0631\u0633\u0648\u0645 \u0643\u0645\u0627 \u0647\u064A.",
|
|
1936
1944
|
no_shot: "\u0628\u062F\u0648\u0646 \u0635\u0648\u0631\u0629 (\u062A\u0645 \u062D\u0641\u0638 \u0627\u0644\u0645\u0648\u0642\u0639)",
|
|
1937
1945
|
annotate_placeholder: "\u0645\u0627\u0630\u0627 \u062A\u0631\u064A\u062F \u0623\u0646 \u062A\u0641\u0639\u0644 \u0647\u0646\u0627\u061F (\u0625\u0636\u0627\u0641\u0629 / \u062D\u0630\u0641 / \u062A\u063A\u064A\u064A\u0631\u2026)",
|
|
1938
1946
|
save_point: "\u062D\u0641\u0638 \u0627\u0644\u0646\u0642\u0637\u0629",
|
|
1947
|
+
save_next: "\u062D\u0641\u0638 + \u0627\u0644\u062A\u0627\u0644\u064A",
|
|
1948
|
+
save_next_hint: "\u0627\u062D\u0641\u0638 \u0647\u0630\u0647 \u0648\u062D\u062F\u0651\u062F \u062C\u0632\u0621\u064B\u0627 \u0622\u062E\u0631 \u0645\u0646 \u0646\u0641\u0633 \u0627\u0644\u0644\u0642\u0637\u0629 \u2014 \u0628\u062F\u0648\u0646 \u0637\u0644\u0628 \u0625\u0630\u0646 \u062C\u062F\u064A\u062F.",
|
|
1949
|
+
shot_reused: "\u062D\u064F\u0641\u0638\u062A {n} \u0645\u0646 \u0647\u0630\u0647 \u0627\u0644\u0644\u0642\u0637\u0629",
|
|
1939
1950
|
reselect: "\u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u062A\u062D\u062F\u064A\u062F",
|
|
1940
|
-
save_hint: "\u2318/Ctrl + Enter \u0644\u0644\u062D\u0641\u0638",
|
|
1951
|
+
save_hint: "\u2318/Ctrl + Enter \u0644\u0644\u062D\u0641\u0638 \xB7 \u0623\u0636\u0641 Shift \u0644\u0644\u0628\u0642\u0627\u0621 \u0639\u0644\u0649 \u0646\u0641\u0633 \u0627\u0644\u0644\u0642\u0637\u0629",
|
|
1941
1952
|
login_with: "\u0633\u062C\u0651\u0644 \u0627\u0644\u062F\u062E\u0648\u0644 \u0628\u0640 {field} + \u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631.",
|
|
1942
1953
|
used_count: "{n}/{m} \u0645\u064F\u0633\u062A\u062E\u062F\u064E\u0645",
|
|
1943
1954
|
used: "\u0645\u064F\u0633\u062A\u062E\u062F\u064E\u0645",
|
|
@@ -1973,6 +1984,9 @@ var STR = {
|
|
|
1973
1984
|
sev_design: "Design",
|
|
1974
1985
|
sev_enhance: "Enhance",
|
|
1975
1986
|
status_open: "\u0645\u0641\u062A\u0648\u062D",
|
|
1987
|
+
status_sent: "\u0645\u064F\u0631\u0633\u064E\u0644",
|
|
1988
|
+
check_fixes: "\u062A\u062D\u0642\u0651\u0642 \u0645\u0646 {n} \u062A\u0639\u062F\u064A\u0644\u0627\u062A",
|
|
1989
|
+
check_fixes_hint: "\u0645\u0631\u0651 \u0639\u0644\u0649 \u0643\u0644 \u0645\u0627 \u0623\u0631\u0633\u0644\u0647 \u0627\u0644\u062A\u0635\u062F\u064A\u0631 \u0627\u0644\u0623\u062E\u064A\u0631\u060C \u0646\u0642\u0637\u0629 \u0646\u0642\u0637\u0629\u060C \u0648\u0642\u0644 \u0647\u0644 \u0635\u0627\u0631 \u0643\u0645\u0627 \u0637\u0644\u0628\u062A \u0623\u0645 \u0644\u0627.",
|
|
1976
1990
|
status_verified: "\u062A\u0645 \u0627\u0644\u062A\u062D\u0642\u0642",
|
|
1977
1991
|
context_attached: "{n} \u0645\u0646 \u0623\u062D\u062F\u0627\u062B \u0627\u0644\u062A\u0634\u063A\u064A\u0644 \u0645\u0631\u0641\u0642\u0629",
|
|
1978
1992
|
start_walkthrough: "\u0627\u0628\u062F\u0623 \u0627\u0644\u062C\u0648\u0644\u0629",
|
|
@@ -2369,6 +2383,25 @@ function getExactCaptureStatus() {
|
|
|
2369
2383
|
function getFrozenFrame() {
|
|
2370
2384
|
return frozen;
|
|
2371
2385
|
}
|
|
2386
|
+
var STILL_REUSE_MS = 9e4;
|
|
2387
|
+
var frozenAtScrollX = 0;
|
|
2388
|
+
var frozenAtScrollY = 0;
|
|
2389
|
+
var frozenAtPath = "";
|
|
2390
|
+
function stillIsCurrent() {
|
|
2391
|
+
if (!frozen) return false;
|
|
2392
|
+
if (typeof window === "undefined") return false;
|
|
2393
|
+
if (Date.now() - frozen.takenAt > STILL_REUSE_MS) return false;
|
|
2394
|
+
if (window.scrollX !== frozenAtScrollX || window.scrollY !== frozenAtScrollY) return false;
|
|
2395
|
+
if (window.location.pathname + window.location.search !== frozenAtPath) return false;
|
|
2396
|
+
const vw = document.documentElement.clientWidth || window.innerWidth;
|
|
2397
|
+
const vh = document.documentElement.clientHeight || window.innerHeight;
|
|
2398
|
+
if (vw !== frozen.viewportWidth || vh !== frozen.viewportHeight) return false;
|
|
2399
|
+
return true;
|
|
2400
|
+
}
|
|
2401
|
+
async function freezeOrReuse() {
|
|
2402
|
+
if (stillIsCurrent()) return frozen;
|
|
2403
|
+
return freezeViewport();
|
|
2404
|
+
}
|
|
2372
2405
|
function releaseFrozenFrame() {
|
|
2373
2406
|
if (frozen) {
|
|
2374
2407
|
frozen.canvas.width = 0;
|
|
@@ -2613,6 +2646,9 @@ async function freezeViewport() {
|
|
|
2613
2646
|
viewportHeight: vh,
|
|
2614
2647
|
takenAt: Date.now()
|
|
2615
2648
|
};
|
|
2649
|
+
frozenAtScrollX = window.scrollX;
|
|
2650
|
+
frozenAtScrollY = window.scrollY;
|
|
2651
|
+
frozenAtPath = window.location.pathname + window.location.search;
|
|
2616
2652
|
lastMode = mode;
|
|
2617
2653
|
return frozen;
|
|
2618
2654
|
} catch {
|
|
@@ -2786,12 +2822,21 @@ function neutralizeDocumentColors(doc, aggressive = false) {
|
|
|
2786
2822
|
|
|
2787
2823
|
// src/lib/capture.ts
|
|
2788
2824
|
var HTML2CANVAS_TIMEOUT_MS = 1e4;
|
|
2825
|
+
var CHUNK_TIMEOUT_MS = 8e3;
|
|
2789
2826
|
var FALLBACK_PAGE_BACKGROUND = "#ffffff";
|
|
2790
2827
|
var MAX_SHOT_EDGE = 1800;
|
|
2791
2828
|
var WEBP_QUALITY = 0.92;
|
|
2792
2829
|
var STUCK_ATTR = "data-qa-stuck";
|
|
2793
2830
|
var MAX_CAPTURE_EDGE = 4e3;
|
|
2794
2831
|
var MAX_RENDER_DEVICE_PIXELS = 16e6;
|
|
2832
|
+
var TOO_HEAVY_NODES = 6e3;
|
|
2833
|
+
function documentWeight() {
|
|
2834
|
+
try {
|
|
2835
|
+
return document.getElementsByTagName("*").length;
|
|
2836
|
+
} catch {
|
|
2837
|
+
return 0;
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2795
2840
|
function withTimeout(promise, ms) {
|
|
2796
2841
|
let timer;
|
|
2797
2842
|
return Promise.race([
|
|
@@ -2853,8 +2898,13 @@ function fitToBudget(canvas) {
|
|
|
2853
2898
|
function canvasToBlob(canvas, type, quality) {
|
|
2854
2899
|
return new Promise((resolve) => {
|
|
2855
2900
|
if (canvas.toBlob) {
|
|
2856
|
-
|
|
2857
|
-
|
|
2901
|
+
try {
|
|
2902
|
+
canvas.toBlob((b) => resolve(b), type, quality);
|
|
2903
|
+
return;
|
|
2904
|
+
} catch {
|
|
2905
|
+
resolve(null);
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2858
2908
|
}
|
|
2859
2909
|
try {
|
|
2860
2910
|
const dataUrl = canvas.toDataURL(type, quality);
|
|
@@ -3041,7 +3091,9 @@ function planRender(rect, sx, sy, vw, vh) {
|
|
|
3041
3091
|
};
|
|
3042
3092
|
}
|
|
3043
3093
|
async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
3044
|
-
const
|
|
3094
|
+
const mod = await withTimeout(import('html2canvas'), CHUNK_TIMEOUT_MS);
|
|
3095
|
+
if (!mod) return null;
|
|
3096
|
+
const { default: html2canvas } = mod;
|
|
3045
3097
|
const { vw, vh } = viewportSize();
|
|
3046
3098
|
const plan = planRender(rect, sx, sy, vw, vh);
|
|
3047
3099
|
let scale = Math.min(window.devicePixelRatio || 1, 2);
|
|
@@ -3060,7 +3112,22 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
|
3060
3112
|
height: plan.render.height,
|
|
3061
3113
|
scale,
|
|
3062
3114
|
useCORS: true,
|
|
3063
|
-
allowTaint: true
|
|
3115
|
+
// MUST stay false. `allowTaint: true` tells html2canvas to draw
|
|
3116
|
+
// cross-origin images it could not fetch with CORS -- which succeeds,
|
|
3117
|
+
// and TAINTS the canvas. A tainted canvas throws SecurityError from
|
|
3118
|
+
// toBlob/toDataURL, so the render finishes and then the encode dies,
|
|
3119
|
+
// and the tester gets "couldn't capture" with nothing to look at.
|
|
3120
|
+
//
|
|
3121
|
+
// That is one image away on any real page: a map tile, a news
|
|
3122
|
+
// thumbnail, an avatar, an ad, a CDN logo. It made image-heavy pages
|
|
3123
|
+
// fail 100% of the time while a plain page worked, which is exactly
|
|
3124
|
+
// the "it works for you and not for me" this kept producing.
|
|
3125
|
+
//
|
|
3126
|
+
// False is strictly better: html2canvas SKIPS the images it cannot
|
|
3127
|
+
// read, so those come out blank and everything else comes out right.
|
|
3128
|
+
// A screenshot with two grey rectangles in it is worth having. A
|
|
3129
|
+
// SecurityError is not.
|
|
3130
|
+
allowTaint: false,
|
|
3064
3131
|
// The page's own background, never null — see resolvePageBackground().
|
|
3065
3132
|
// Passed straight to html2canvas's parser, so it gets the same
|
|
3066
3133
|
// CSS Color 4 treatment the document walk applies.
|
|
@@ -3109,6 +3176,10 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
3109
3176
|
}
|
|
3110
3177
|
if (prefer === "exact") return { status: "failed" };
|
|
3111
3178
|
}
|
|
3179
|
+
const weight = documentWeight();
|
|
3180
|
+
if (weight > TOO_HEAVY_NODES) {
|
|
3181
|
+
return { status: "too-heavy", nodes: weight };
|
|
3182
|
+
}
|
|
3112
3183
|
try {
|
|
3113
3184
|
const canvas = await captureViaDom(rect, sx, sy);
|
|
3114
3185
|
if (!canvas) return { status: "failed" };
|
|
@@ -3166,6 +3237,12 @@ function formatEvent(ev, t0) {
|
|
|
3166
3237
|
}
|
|
3167
3238
|
return `[${rel}] uncaught: ${oneLine(ev.message)}`;
|
|
3168
3239
|
}
|
|
3240
|
+
function noteCheckLine(note, index) {
|
|
3241
|
+
const where = oneLine(note.route) || "/";
|
|
3242
|
+
const what = oneLine(note.description) || "(no description)";
|
|
3243
|
+
const trimmed = what.length > 160 ? `${what.slice(0, 157)}...` : what;
|
|
3244
|
+
return `- [ ] **check-${index}** (\`${where}\`) \u2014 ${trimmed}`;
|
|
3245
|
+
}
|
|
3169
3246
|
function noteToMarkdown(note, opts) {
|
|
3170
3247
|
const brand = opts?.brand ?? "Qapture";
|
|
3171
3248
|
const idx = opts?.index;
|
|
@@ -3223,6 +3300,14 @@ function noteToMarkdown(note, opts) {
|
|
|
3223
3300
|
lines.push(`${i + 1}. ${formatStep(step, t0)}`);
|
|
3224
3301
|
});
|
|
3225
3302
|
}
|
|
3303
|
+
if (idx != null) {
|
|
3304
|
+
lines.push("");
|
|
3305
|
+
lines.push(`**Check ${idx} \u2014 how this will be graded**`);
|
|
3306
|
+
lines.push("");
|
|
3307
|
+
lines.push(`The tester will be taken back to \`${oneLine(note.route) || "/"}\`` + (target?.selector ? `, shown \`${oneLine(target.selector)}\`` : ", shown this region") + ", and asked: *is this now what I asked for?*");
|
|
3308
|
+
lines.push("");
|
|
3309
|
+
lines.push(`Treat it as done only when the paragraph above is true on that page, on a fresh load, without the tester doing anything extra. Record the outcome against \`check-${idx}\` in \`verify.md\`.`);
|
|
3310
|
+
}
|
|
3226
3311
|
const ctx = note.context;
|
|
3227
3312
|
if (ctx) {
|
|
3228
3313
|
const env = ctx.env;
|
|
@@ -3395,6 +3480,17 @@ Second, **the previous attempt is evidence.** Something about the first reading
|
|
|
3395
3480
|
The tag sets your starting posture; **the words win.** A point tagged Design that says "this crashes" is a bug \u2014 treat it as one, and mention that you changed lane. People pick the wrong chip all the time; that is not a reason to do the wrong work.
|
|
3396
3481
|
|
|
3397
3482
|
And if a point is phrased as a question rather than a report, **answer it \u2014 do not change code on a maybe.**`
|
|
3483
|
+
);
|
|
3484
|
+
sections.push(
|
|
3485
|
+
`## You Are Being Graded On This
|
|
3486
|
+
|
|
3487
|
+
This archive is not a list of suggestions. Every point in \`notes.md\` is an acceptance test, and \`verify.md\` is the checklist those tests live in \u2014 one unticked box per point.
|
|
3488
|
+
|
|
3489
|
+
**What happens next.** The tester will be walked back through these same checks on the real page, in this order, one stop at a time, and asked about each: *is this now what I asked for?* Anything they say is still wrong comes back to you as round two with their new words attached, so nothing quietly disappears.
|
|
3490
|
+
|
|
3491
|
+
**What to do.** Do the work, then fill in \`verify.md\`: tick a box only when the check is true on a fresh load of the page named beside it, with the tester doing nothing extra. Where you could not do something, or you think it is the wrong thing to do, leave the box unticked and write one line saying why. An unticked box with a reason is a good answer. A ticked box that does not hold up is the only bad one \u2014 it costs the tester the trip to find out.
|
|
3492
|
+
|
|
3493
|
+
Hand \`verify.md\` back with the work.`
|
|
3398
3494
|
);
|
|
3399
3495
|
const stack = typeof p.stack === "string" && p.stack.trim() ? p.stack.trim() : "(not provided)";
|
|
3400
3496
|
const runArr = toStrings(p.runCommands);
|
|
@@ -3542,6 +3638,39 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
|
|
|
3542
3638
|
` : "";
|
|
3543
3639
|
const notesMd = preambleMd + "\n\n---NOTES---\n\n" + notesHeader + notesBody;
|
|
3544
3640
|
zip.file("notes.md", notesMd);
|
|
3641
|
+
zip.file("verify.md", [
|
|
3642
|
+
`# ${brandLabel} \u2014 checks to satisfy`,
|
|
3643
|
+
"",
|
|
3644
|
+
`Exported: ${stamp}`,
|
|
3645
|
+
`Checks: ${notes.length}`,
|
|
3646
|
+
"",
|
|
3647
|
+
"Each line below is one thing the tester asked for. The full ask, the",
|
|
3648
|
+
"screenshot, the page and the element are in `notes.md` under the matching",
|
|
3649
|
+
'"Point" heading.',
|
|
3650
|
+
"",
|
|
3651
|
+
"**What is expected of you**",
|
|
3652
|
+
"",
|
|
3653
|
+
"1. Do the work in `notes.md`.",
|
|
3654
|
+
"2. Tick a box here only when the check is true on a fresh load of the page",
|
|
3655
|
+
" named beside it, with the tester doing nothing extra.",
|
|
3656
|
+
"3. Leave a box unticked and write one line under it saying why, if you",
|
|
3657
|
+
" could not do it or disagree with it. An unticked box with a reason is a",
|
|
3658
|
+
" good answer; a ticked box that is not true is the only bad one.",
|
|
3659
|
+
"4. Hand this file back with the work.",
|
|
3660
|
+
"",
|
|
3661
|
+
"The tester will then be walked back through these same checks on the real",
|
|
3662
|
+
"page, in this order, and asked about each one. Anything they say is still",
|
|
3663
|
+
"wrong comes back to you as round two, with their new words attached.",
|
|
3664
|
+
"",
|
|
3665
|
+
"---",
|
|
3666
|
+
"",
|
|
3667
|
+
...notes.map((n, i) => noteCheckLine(n, i + 1)),
|
|
3668
|
+
"",
|
|
3669
|
+
"---",
|
|
3670
|
+
"",
|
|
3671
|
+
"Re-open the walkthrough on the tester's machine with `?qa=walk:verify`.",
|
|
3672
|
+
""
|
|
3673
|
+
].join("\n"));
|
|
3545
3674
|
notes.forEach((n, i) => {
|
|
3546
3675
|
if (n.screenshot && shots) {
|
|
3547
3676
|
shots.file(`point-${i + 1}.${shotExtension(n.screenshot)}`, n.screenshot);
|
|
@@ -4181,6 +4310,9 @@ var GUIDE_FAILED_KEY = "guideFailed";
|
|
|
4181
4310
|
var LOGIN_KEY = "logins";
|
|
4182
4311
|
var PENDING_DELETE_KEY = "pendingDeleteIds";
|
|
4183
4312
|
var EXACT_SHOTS_KEY = "exactShots";
|
|
4313
|
+
function exactShotsWanted(store) {
|
|
4314
|
+
return store.getItem(EXACT_SHOTS_KEY) !== "0";
|
|
4315
|
+
}
|
|
4184
4316
|
var SIMPLE_MODE_KEY = "simpleMode";
|
|
4185
4317
|
var COMPACT_KEY = "compactCapture";
|
|
4186
4318
|
var LAST_CAMPAIGN_KEY = "lastCampaign";
|
|
@@ -4221,6 +4353,7 @@ function QaProvider({
|
|
|
4221
4353
|
useEffect(() => {
|
|
4222
4354
|
notesRef.current = notes;
|
|
4223
4355
|
}, [notes]);
|
|
4356
|
+
const staleStillTimer = useRef(null);
|
|
4224
4357
|
const applyNotes = useCallback((updater) => {
|
|
4225
4358
|
const next = updater(notesRef.current);
|
|
4226
4359
|
notesRef.current = next;
|
|
@@ -4259,7 +4392,7 @@ function QaProvider({
|
|
|
4259
4392
|
() => new Set(storage.getJSON(LOGIN_KEY, []))
|
|
4260
4393
|
);
|
|
4261
4394
|
const [exactStatus, setExactStatus] = useState(() => {
|
|
4262
|
-
if (storage
|
|
4395
|
+
if (exactShotsWanted(storage)) armExactCapture();
|
|
4263
4396
|
return getExactCaptureStatus();
|
|
4264
4397
|
});
|
|
4265
4398
|
const exactSupported = isExactCaptureSupported();
|
|
@@ -4764,17 +4897,22 @@ function QaProvider({
|
|
|
4764
4897
|
flushPendingDeletes();
|
|
4765
4898
|
for (const timer of noticeTimers.current.values()) clearTimeout(timer);
|
|
4766
4899
|
noticeTimers.current.clear();
|
|
4900
|
+
if (staleStillTimer.current) {
|
|
4901
|
+
clearTimeout(staleStillTimer.current);
|
|
4902
|
+
staleStillTimer.current = null;
|
|
4903
|
+
}
|
|
4767
4904
|
releaseFrozenFrame();
|
|
4768
4905
|
};
|
|
4769
4906
|
}, [flushPendingDeletes]);
|
|
4907
|
+
const STILL_HOLD_MS = 1e5;
|
|
4770
4908
|
const startCapture = useCallback((prefill) => {
|
|
4771
4909
|
setIsOpen(false);
|
|
4772
4910
|
setCapturePrefill(prefill ?? "");
|
|
4773
4911
|
setCaptureActive(true);
|
|
4774
|
-
setFrozenAt(null);
|
|
4775
|
-
if (storage
|
|
4912
|
+
if (!stillIsCurrent()) setFrozenAt(null);
|
|
4913
|
+
if (exactShotsWanted(storage) && isExactCaptureSupported()) {
|
|
4776
4914
|
resetExactCaptureDecline();
|
|
4777
|
-
void
|
|
4915
|
+
void freezeOrReuse().then((frame) => {
|
|
4778
4916
|
setFrozenAt(frame?.takenAt ?? null);
|
|
4779
4917
|
setExactStatus(getExactCaptureStatus());
|
|
4780
4918
|
});
|
|
@@ -4782,8 +4920,11 @@ function QaProvider({
|
|
|
4782
4920
|
}, [storage]);
|
|
4783
4921
|
const endCapture = useCallback((reopen = true) => {
|
|
4784
4922
|
setCaptureActive(false);
|
|
4785
|
-
|
|
4786
|
-
|
|
4923
|
+
if (staleStillTimer.current) clearTimeout(staleStillTimer.current);
|
|
4924
|
+
staleStillTimer.current = setTimeout(() => {
|
|
4925
|
+
releaseFrozenFrame();
|
|
4926
|
+
staleStillTimer.current = null;
|
|
4927
|
+
}, STILL_HOLD_MS);
|
|
4787
4928
|
if (reopen) setIsOpen(true);
|
|
4788
4929
|
}, []);
|
|
4789
4930
|
const toggleGuide = useCallback((key) => {
|
|
@@ -4804,19 +4945,36 @@ function QaProvider({
|
|
|
4804
4945
|
return next;
|
|
4805
4946
|
});
|
|
4806
4947
|
}, [storage]);
|
|
4948
|
+
const stampHandedOff = useCallback(async (ids, at) => {
|
|
4949
|
+
if (!ids.length) return;
|
|
4950
|
+
const idSet = new Set(ids);
|
|
4951
|
+
const touched = [];
|
|
4952
|
+
applyNotes(
|
|
4953
|
+
(prev) => prev.map((n) => {
|
|
4954
|
+
if (!idSet.has(n.id)) return n;
|
|
4955
|
+
if ((n.status ?? "open") === "verified") return n;
|
|
4956
|
+
const next = { ...n, handedOffAt: at };
|
|
4957
|
+
touched.push(next);
|
|
4958
|
+
return next;
|
|
4959
|
+
})
|
|
4960
|
+
);
|
|
4961
|
+
for (const note of touched) await idb.put(note);
|
|
4962
|
+
}, [applyNotes, idb]);
|
|
4807
4963
|
const exportZipFn = useCallback(
|
|
4808
4964
|
async (filename) => {
|
|
4809
4965
|
if (!notes.length || isExporting) return;
|
|
4810
4966
|
setIsExporting(true);
|
|
4811
4967
|
try {
|
|
4812
|
-
|
|
4968
|
+
const stamp = nowIso();
|
|
4969
|
+
await buildAndDownloadZip(notes, stamp, filename, config, guideChecked, guideSkipped);
|
|
4970
|
+
await stampHandedOff(notes.map((n) => n.id), stamp);
|
|
4813
4971
|
} catch (err) {
|
|
4814
4972
|
console.error("[QA] export failed", err);
|
|
4815
4973
|
} finally {
|
|
4816
4974
|
setIsExporting(false);
|
|
4817
4975
|
}
|
|
4818
4976
|
},
|
|
4819
|
-
[notes, isExporting, config, guideChecked, guideSkipped]
|
|
4977
|
+
[notes, isExporting, config, guideChecked, guideSkipped, stampHandedOff]
|
|
4820
4978
|
);
|
|
4821
4979
|
const exportZipRef = useRef(async () => {
|
|
4822
4980
|
});
|
|
@@ -5002,6 +5160,7 @@ function QaProvider({
|
|
|
5002
5160
|
design: 0,
|
|
5003
5161
|
enhance: 0,
|
|
5004
5162
|
open: 0,
|
|
5163
|
+
sent: 0,
|
|
5005
5164
|
fixed: 0,
|
|
5006
5165
|
verified: 0,
|
|
5007
5166
|
thisPage: 0
|
|
@@ -5015,6 +5174,7 @@ function QaProvider({
|
|
|
5015
5174
|
if (status === "verified") counts.verified++;
|
|
5016
5175
|
else if (status === "fixed") counts.fixed++;
|
|
5017
5176
|
else counts.open++;
|
|
5177
|
+
if (n.handedOffAt && status !== "verified") counts.sent++;
|
|
5018
5178
|
if (n.route.split("?")[0] === currentRoute) counts.thisPage++;
|
|
5019
5179
|
}
|
|
5020
5180
|
return counts;
|
|
@@ -5023,7 +5183,9 @@ function QaProvider({
|
|
|
5023
5183
|
const q = filter.query.trim().toLowerCase();
|
|
5024
5184
|
return notes.filter((n) => {
|
|
5025
5185
|
if (filter.severity !== "all" && (n.severity ?? "bug") !== filter.severity) return false;
|
|
5026
|
-
if (filter.status
|
|
5186
|
+
if (filter.status === "sent") {
|
|
5187
|
+
if (!n.handedOffAt || (n.status ?? "open") === "verified") return false;
|
|
5188
|
+
} else if (filter.status !== "all" && (n.status ?? "open") !== filter.status) return false;
|
|
5027
5189
|
if (filter.thisPageOnly && n.route.split("?")[0] !== currentRoute) return false;
|
|
5028
5190
|
if (!q) return true;
|
|
5029
5191
|
const haystack = `${n.description} ${n.route} ${n.target?.selector ?? ""} ${n.target?.text ?? ""}`;
|
|
@@ -5068,6 +5230,7 @@ function QaProvider({
|
|
|
5068
5230
|
const blob = await buildZipBlob(notes, stamp, config, guideChecked, guideSkipped);
|
|
5069
5231
|
if (!blob) return { status: "unsupported" };
|
|
5070
5232
|
const outcome = await shareZipFile(blob, name, config.brand.label);
|
|
5233
|
+
await stampHandedOff(notes.map((n) => n.id), stamp);
|
|
5071
5234
|
if (outcome.status === "needs-gesture") {
|
|
5072
5235
|
setPendingShare({ blob, filename: name });
|
|
5073
5236
|
} else if (outcome.status === "unsupported") {
|
|
@@ -5079,7 +5242,7 @@ function QaProvider({
|
|
|
5079
5242
|
} finally {
|
|
5080
5243
|
setIsExporting(false);
|
|
5081
5244
|
}
|
|
5082
|
-
}, [notes, config, guideChecked, guideSkipped]);
|
|
5245
|
+
}, [notes, config, guideChecked, guideSkipped, stampHandedOff]);
|
|
5083
5246
|
const sharePending = useCallback(async () => {
|
|
5084
5247
|
const pending = pendingShare;
|
|
5085
5248
|
if (!pending) return { status: "unsupported" };
|
|
@@ -5295,7 +5458,10 @@ function QaProvider({
|
|
|
5295
5458
|
window.history.replaceState({}, "", window.location.pathname + (qs ? `?${qs}` : ""));
|
|
5296
5459
|
} catch {
|
|
5297
5460
|
}
|
|
5298
|
-
if (which === "
|
|
5461
|
+
if (which === "verify") {
|
|
5462
|
+
setFilterState((prev) => ({ ...prev, status: "sent", severity: "all", thisPageOnly: false }));
|
|
5463
|
+
startWalk("notes", 0);
|
|
5464
|
+
} else if (which === "retest") {
|
|
5299
5465
|
setFilterState((prev) => ({ ...prev, status: "fixed", severity: "all", thisPageOnly: false }));
|
|
5300
5466
|
startWalk("notes", 0);
|
|
5301
5467
|
} else if (which === "notes") {
|
|
@@ -7353,7 +7519,7 @@ function FilterChip({ chip }) {
|
|
|
7353
7519
|
);
|
|
7354
7520
|
}
|
|
7355
7521
|
function NoteFilterBar() {
|
|
7356
|
-
const { filter, setFilter, noteCounts, notes, deleteNotes, t } = useQa();
|
|
7522
|
+
const { filter, setFilter, noteCounts, notes, deleteNotes, startWalk, t } = useQa();
|
|
7357
7523
|
const [confirmPrune, setConfirmPrune] = useState(false);
|
|
7358
7524
|
if (notes.length < 3) return null;
|
|
7359
7525
|
const others = notes.filter((n) => (n.status ?? "open") !== "fixed");
|
|
@@ -7395,6 +7561,18 @@ function NoteFilterBar() {
|
|
|
7395
7561
|
tone: "accent",
|
|
7396
7562
|
onClick: sev("design")
|
|
7397
7563
|
},
|
|
7564
|
+
// Only ever shown once something has actually been sent. A chip reading
|
|
7565
|
+
// "Sent 0" on a first run is noise; the same chip reading "Sent 6" the
|
|
7566
|
+
// morning after an export is the one thing the tester came back for.
|
|
7567
|
+
...noteCounts.sent > 0 ? [{
|
|
7568
|
+
key: "sent",
|
|
7569
|
+
label: t("status_sent"),
|
|
7570
|
+
count: noteCounts.sent,
|
|
7571
|
+
active: filter.status === "sent",
|
|
7572
|
+
icon: "Camera",
|
|
7573
|
+
tone: "accent",
|
|
7574
|
+
onClick: () => setFilter({ status: "sent", severity: "all", thisPageOnly: false })
|
|
7575
|
+
}] : [],
|
|
7398
7576
|
{
|
|
7399
7577
|
key: "open",
|
|
7400
7578
|
label: t("status_open"),
|
|
@@ -7463,6 +7641,24 @@ function NoteFilterBar() {
|
|
|
7463
7641
|
}
|
|
7464
7642
|
),
|
|
7465
7643
|
/* @__PURE__ */ jsx("div", { className: "qa-flex qa-flex-wrap qa-gap-1", children: chips.map((chip) => /* @__PURE__ */ jsx(FilterChip, { chip }, chip.key)) }),
|
|
7644
|
+
noteCounts.sent > 0 && /* @__PURE__ */ jsxs(
|
|
7645
|
+
"button",
|
|
7646
|
+
{
|
|
7647
|
+
type: "button",
|
|
7648
|
+
"data-qa-verify-walk": "true",
|
|
7649
|
+
onClick: () => {
|
|
7650
|
+
setFilter({ status: "sent", severity: "all", thisPageOnly: false });
|
|
7651
|
+
startWalk("notes", 0);
|
|
7652
|
+
},
|
|
7653
|
+
title: t("check_fixes_hint"),
|
|
7654
|
+
className: "qa-tap qa-inline-flex qa-items-center qa-justify-center qa-gap-1.5 qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-1.5 qa-text-xs qa-text-hi qa-focus-ring",
|
|
7655
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
7656
|
+
children: [
|
|
7657
|
+
/* @__PURE__ */ jsx(Icon, { name: "Check", size: 13 }),
|
|
7658
|
+
t("check_fixes", { n: noteCounts.sent })
|
|
7659
|
+
]
|
|
7660
|
+
}
|
|
7661
|
+
),
|
|
7466
7662
|
showPrune && /* @__PURE__ */ jsx("div", { className: "qa-text-11", "data-qa-prune": "true", children: confirmPrune ? /* @__PURE__ */ jsxs("span", { className: "qa-text-mid", children: [
|
|
7467
7663
|
t("keep_only_retest_q", { n: others.length }),
|
|
7468
7664
|
" ",
|
|
@@ -9218,10 +9414,12 @@ function CaptureMode() {
|
|
|
9218
9414
|
const [regionMode, setRegionMode] = useState(false);
|
|
9219
9415
|
const [shot, setShot] = useState(null);
|
|
9220
9416
|
const stillRef = useRef(null);
|
|
9417
|
+
const [notesFromThisShot, setNotesFromThisShot] = useState(0);
|
|
9221
9418
|
const [shotEngine, setShotEngine] = useState(null);
|
|
9222
9419
|
const [shotUrl, setShotUrl] = useState(null);
|
|
9223
9420
|
const [capturing, setCapturing] = useState(false);
|
|
9224
9421
|
const [captureError, setCaptureError] = useState(false);
|
|
9422
|
+
const [tooHeavy, setTooHeavy] = useState(false);
|
|
9225
9423
|
const [description, setDescription] = useState(capturePrefill);
|
|
9226
9424
|
const [severity, setSeverity] = useState("bug");
|
|
9227
9425
|
const [targetForensics, setTargetForensics] = useState(void 0);
|
|
@@ -9251,6 +9449,7 @@ function CaptureMode() {
|
|
|
9251
9449
|
const runCapture = useCallback(async (rect) => {
|
|
9252
9450
|
setCapturing(true);
|
|
9253
9451
|
setCaptureError(false);
|
|
9452
|
+
setTooHeavy(false);
|
|
9254
9453
|
lockPageScroll();
|
|
9255
9454
|
try {
|
|
9256
9455
|
const outcome = await captureRegion(rect, scrollSnap.current);
|
|
@@ -9261,6 +9460,7 @@ function CaptureMode() {
|
|
|
9261
9460
|
return;
|
|
9262
9461
|
}
|
|
9263
9462
|
setCaptureError(outcome.status === "failed");
|
|
9463
|
+
setTooHeavy(outcome.status === "too-heavy");
|
|
9264
9464
|
setShotEngine(outcome.status === "ok" ? outcome.engine : null);
|
|
9265
9465
|
setShot(blob);
|
|
9266
9466
|
setShotUrl((old) => {
|
|
@@ -9522,7 +9722,17 @@ function CaptureMode() {
|
|
|
9522
9722
|
canvas.removeAttribute("style");
|
|
9523
9723
|
};
|
|
9524
9724
|
}, [exactShots.frozenAt]);
|
|
9525
|
-
const
|
|
9725
|
+
const resetForNextNote = useCallback(() => {
|
|
9726
|
+
setPhase("selecting");
|
|
9727
|
+
setSelection(null);
|
|
9728
|
+
setShot(null);
|
|
9729
|
+
setShotEngine(null);
|
|
9730
|
+
setDescription("");
|
|
9731
|
+
setSeverity("bug");
|
|
9732
|
+
setTargetForensics(void 0);
|
|
9733
|
+
setCaptureError(false);
|
|
9734
|
+
}, []);
|
|
9735
|
+
const save = async (keepGoing = false) => {
|
|
9526
9736
|
if (!selection || !description.trim()) return;
|
|
9527
9737
|
const target = {
|
|
9528
9738
|
kind: selection.kind,
|
|
@@ -9544,6 +9754,11 @@ function CaptureMode() {
|
|
|
9544
9754
|
severity,
|
|
9545
9755
|
forensics: selection.kind === "element" ? targetForensics : void 0
|
|
9546
9756
|
});
|
|
9757
|
+
if (keepGoing) {
|
|
9758
|
+
setNotesFromThisShot((n) => n + 1);
|
|
9759
|
+
resetForNextNote();
|
|
9760
|
+
return;
|
|
9761
|
+
}
|
|
9547
9762
|
endCapture();
|
|
9548
9763
|
};
|
|
9549
9764
|
const popStyleFor = useCallback((r, cardWidth = 340) => {
|
|
@@ -9611,6 +9826,21 @@ function CaptureMode() {
|
|
|
9611
9826
|
{
|
|
9612
9827
|
className: "qa-fixed qa-left-half qa-top-4 qa-z-10095 qa-translate-x-neg-half qa-flex qa-items-center qa-gap-3 qa-rounded-full qa-border qa-border-subtle qa-bg-2 qa-px-4 qa-py-2 qa-text-sm qa-text-hi qa-elev-2",
|
|
9613
9828
|
children: [
|
|
9829
|
+
notesFromThisShot > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
9830
|
+
/* @__PURE__ */ jsxs(
|
|
9831
|
+
"span",
|
|
9832
|
+
{
|
|
9833
|
+
"data-qa-shot-reused": "true",
|
|
9834
|
+
className: "qa-flex qa-items-center qa-gap-1.5",
|
|
9835
|
+
style: { color: "var(--qa-success)" },
|
|
9836
|
+
children: [
|
|
9837
|
+
/* @__PURE__ */ jsx(Icon, { name: "Camera", size: 16 }),
|
|
9838
|
+
t("shot_reused", { n: String(notesFromThisShot) })
|
|
9839
|
+
]
|
|
9840
|
+
}
|
|
9841
|
+
),
|
|
9842
|
+
/* @__PURE__ */ jsx("span", { className: "qa-opacity-50", children: "\xB7" })
|
|
9843
|
+
] }),
|
|
9614
9844
|
/* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5", children: [
|
|
9615
9845
|
/* @__PURE__ */ jsx(Icon, { name: "MousePointerClick", size: 16 }),
|
|
9616
9846
|
t("cap_click")
|
|
@@ -10060,6 +10290,29 @@ function CaptureMode() {
|
|
|
10060
10290
|
)
|
|
10061
10291
|
]
|
|
10062
10292
|
}
|
|
10293
|
+
) : tooHeavy ? (
|
|
10294
|
+
// Not a failure to report — a refusal to explain, with the one
|
|
10295
|
+
// control that actually solves it.
|
|
10296
|
+
/* @__PURE__ */ jsxs("span", { className: "qa-flex qa-flex-col qa-items-center qa-gap-2 qa-py-3 qa-px-3 qa-text-center", children: [
|
|
10297
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-xs qa-text-mid", children: t("too_heavy") }),
|
|
10298
|
+
exactShots.supported ? /* @__PURE__ */ jsxs(
|
|
10299
|
+
"button",
|
|
10300
|
+
{
|
|
10301
|
+
type: "button",
|
|
10302
|
+
onClick: () => void (async () => {
|
|
10303
|
+
const shot2 = await photographNow();
|
|
10304
|
+
if (shot2 && selection) void runCapture(selection.rect);
|
|
10305
|
+
})(),
|
|
10306
|
+
title: t("exact_hint"),
|
|
10307
|
+
className: "qa-tap qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-xs qa-text-hi qa-focus-ring",
|
|
10308
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
10309
|
+
children: [
|
|
10310
|
+
/* @__PURE__ */ jsx(Icon, { name: "Camera", size: 13 }),
|
|
10311
|
+
t("redraw_action")
|
|
10312
|
+
]
|
|
10313
|
+
}
|
|
10314
|
+
) : /* @__PURE__ */ jsx("span", { className: "qa-text-10 qa-text-mid", children: t("redraw_no_engine") })
|
|
10315
|
+
] })
|
|
10063
10316
|
) : captureError ? (
|
|
10064
10317
|
// The render broke rather than being skipped — offer a retry
|
|
10065
10318
|
// against the same selection instead of a dead-end message.
|
|
@@ -10166,7 +10419,7 @@ function CaptureMode() {
|
|
|
10166
10419
|
value: description,
|
|
10167
10420
|
onChange: (e) => setDescription(e.target.value),
|
|
10168
10421
|
onKeyDown: (e) => {
|
|
10169
|
-
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save();
|
|
10422
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
|
|
10170
10423
|
if ((e.altKey || e.metaKey || e.ctrlKey) && SEVERITIES2[Number(e.key) - 1]) {
|
|
10171
10424
|
e.preventDefault();
|
|
10172
10425
|
setSeverity(SEVERITIES2[Number(e.key) - 1]);
|
|
@@ -10191,17 +10444,25 @@ function CaptureMode() {
|
|
|
10191
10444
|
]
|
|
10192
10445
|
}
|
|
10193
10446
|
),
|
|
10447
|
+
/* @__PURE__ */ jsxs(
|
|
10448
|
+
"button",
|
|
10449
|
+
{
|
|
10450
|
+
"data-qa-save-next": "true",
|
|
10451
|
+
onClick: () => void save(true),
|
|
10452
|
+
disabled: !description.trim(),
|
|
10453
|
+
title: t("save_next_hint"),
|
|
10454
|
+
className: "qa-tap qa-flex qa-items-center qa-justify-center qa-gap-1.5 qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-hi",
|
|
10455
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
10456
|
+
children: [
|
|
10457
|
+
/* @__PURE__ */ jsx(Icon, { name: "Plus", size: 16 }),
|
|
10458
|
+
t("save_next")
|
|
10459
|
+
]
|
|
10460
|
+
}
|
|
10461
|
+
),
|
|
10194
10462
|
/* @__PURE__ */ jsx(
|
|
10195
10463
|
"button",
|
|
10196
10464
|
{
|
|
10197
|
-
onClick:
|
|
10198
|
-
setPhase("selecting");
|
|
10199
|
-
setSelection(null);
|
|
10200
|
-
setShot(null);
|
|
10201
|
-
setDescription("");
|
|
10202
|
-
setSeverity("bug");
|
|
10203
|
-
setTargetForensics(void 0);
|
|
10204
|
-
},
|
|
10465
|
+
onClick: resetForNextNote,
|
|
10205
10466
|
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-mid",
|
|
10206
10467
|
style: { background: "transparent", cursor: "pointer" },
|
|
10207
10468
|
children: t("reselect")
|
|
@@ -10417,5 +10678,5 @@ function Qapture({ config }) {
|
|
|
10417
10678
|
}
|
|
10418
10679
|
|
|
10419
10680
|
export { Qapture, deleteQaDatabase, initQaStudio };
|
|
10420
|
-
//# sourceMappingURL=chunk-
|
|
10421
|
-
//# sourceMappingURL=chunk-
|
|
10681
|
+
//# sourceMappingURL=chunk-32LKZ2PG.js.map
|
|
10682
|
+
//# sourceMappingURL=chunk-32LKZ2PG.js.map
|