qapture2 0.8.0 → 0.8.2
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-E7OFH6EM.cjs → chunk-IYQ5JZJX.cjs} +256 -26
- package/dist/{chunk-RJV6FTIB.js → chunk-JGSKPW7Y.js} +256 -26
- 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
|
@@ -1732,11 +1732,15 @@ var STR = {
|
|
|
1732
1732
|
sel_region: "Region selected",
|
|
1733
1733
|
sel_element: "Element selected",
|
|
1734
1734
|
capturing: "capturing screenshot\u2026",
|
|
1735
|
+
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.",
|
|
1735
1736
|
no_shot: "no screenshot (location saved)",
|
|
1736
1737
|
annotate_placeholder: "What do you want to do here? (add / remove / change\u2026)",
|
|
1737
1738
|
save_point: "Save point",
|
|
1739
|
+
save_next: "Save + next",
|
|
1740
|
+
save_next_hint: "Save this one and mark up another part of the same screenshot \u2014 no second permission prompt.",
|
|
1741
|
+
shot_reused: "{n} saved from this screenshot",
|
|
1738
1742
|
reselect: "Reselect",
|
|
1739
|
-
save_hint: "\u2318/Ctrl + Enter to save",
|
|
1743
|
+
save_hint: "\u2318/Ctrl + Enter to save \xB7 add Shift to keep the same screenshot",
|
|
1740
1744
|
login_with: "Log in with {field} + password.",
|
|
1741
1745
|
used_count: "{n}/{m} used",
|
|
1742
1746
|
used: "used",
|
|
@@ -1772,6 +1776,9 @@ var STR = {
|
|
|
1772
1776
|
sev_design: "Design",
|
|
1773
1777
|
sev_enhance: "Enhance",
|
|
1774
1778
|
status_open: "Open",
|
|
1779
|
+
status_sent: "Sent",
|
|
1780
|
+
check_fixes: "Check the {n} fixes",
|
|
1781
|
+
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.",
|
|
1775
1782
|
status_verified: "Verified",
|
|
1776
1783
|
context_attached: "{n} runtime events attached",
|
|
1777
1784
|
start_walkthrough: "Start walkthrough",
|
|
@@ -1940,11 +1947,15 @@ var STR = {
|
|
|
1940
1947
|
sel_region: "\u062A\u0645 \u062A\u062D\u062F\u064A\u062F \u0645\u0646\u0637\u0642\u0629",
|
|
1941
1948
|
sel_element: "\u062A\u0645 \u062A\u062D\u062F\u064A\u062F \u0639\u0646\u0635\u0631",
|
|
1942
1949
|
capturing: "\u064A\u062A\u0645 \u0627\u0644\u062A\u0642\u0627\u0637 \u0627\u0644\u0635\u0648\u0631\u0629\u2026",
|
|
1950
|
+
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.",
|
|
1943
1951
|
no_shot: "\u0628\u062F\u0648\u0646 \u0635\u0648\u0631\u0629 (\u062A\u0645 \u062D\u0641\u0638 \u0627\u0644\u0645\u0648\u0642\u0639)",
|
|
1944
1952
|
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)",
|
|
1945
1953
|
save_point: "\u062D\u0641\u0638 \u0627\u0644\u0646\u0642\u0637\u0629",
|
|
1954
|
+
save_next: "\u062D\u0641\u0638 + \u0627\u0644\u062A\u0627\u0644\u064A",
|
|
1955
|
+
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.",
|
|
1956
|
+
shot_reused: "\u062D\u064F\u0641\u0638\u062A {n} \u0645\u0646 \u0647\u0630\u0647 \u0627\u0644\u0644\u0642\u0637\u0629",
|
|
1946
1957
|
reselect: "\u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u062A\u062D\u062F\u064A\u062F",
|
|
1947
|
-
save_hint: "\u2318/Ctrl + Enter \u0644\u0644\u062D\u0641\u0638",
|
|
1958
|
+
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",
|
|
1948
1959
|
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.",
|
|
1949
1960
|
used_count: "{n}/{m} \u0645\u064F\u0633\u062A\u062E\u062F\u064E\u0645",
|
|
1950
1961
|
used: "\u0645\u064F\u0633\u062A\u062E\u062F\u064E\u0645",
|
|
@@ -1980,6 +1991,9 @@ var STR = {
|
|
|
1980
1991
|
sev_design: "Design",
|
|
1981
1992
|
sev_enhance: "Enhance",
|
|
1982
1993
|
status_open: "\u0645\u0641\u062A\u0648\u062D",
|
|
1994
|
+
status_sent: "\u0645\u064F\u0631\u0633\u064E\u0644",
|
|
1995
|
+
check_fixes: "\u062A\u062D\u0642\u0651\u0642 \u0645\u0646 {n} \u062A\u0639\u062F\u064A\u0644\u0627\u062A",
|
|
1996
|
+
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.",
|
|
1983
1997
|
status_verified: "\u062A\u0645 \u0627\u0644\u062A\u062D\u0642\u0642",
|
|
1984
1998
|
context_attached: "{n} \u0645\u0646 \u0623\u062D\u062F\u0627\u062B \u0627\u0644\u062A\u0634\u063A\u064A\u0644 \u0645\u0631\u0641\u0642\u0629",
|
|
1985
1999
|
start_walkthrough: "\u0627\u0628\u062F\u0623 \u0627\u0644\u062C\u0648\u0644\u0629",
|
|
@@ -2793,12 +2807,21 @@ function neutralizeDocumentColors(doc, aggressive = false) {
|
|
|
2793
2807
|
|
|
2794
2808
|
// src/lib/capture.ts
|
|
2795
2809
|
var HTML2CANVAS_TIMEOUT_MS = 1e4;
|
|
2810
|
+
var CHUNK_TIMEOUT_MS = 8e3;
|
|
2796
2811
|
var FALLBACK_PAGE_BACKGROUND = "#ffffff";
|
|
2797
2812
|
var MAX_SHOT_EDGE = 1800;
|
|
2798
2813
|
var WEBP_QUALITY = 0.92;
|
|
2799
2814
|
var STUCK_ATTR = "data-qa-stuck";
|
|
2800
2815
|
var MAX_CAPTURE_EDGE = 4e3;
|
|
2801
2816
|
var MAX_RENDER_DEVICE_PIXELS = 16e6;
|
|
2817
|
+
var TOO_HEAVY_NODES = 6e3;
|
|
2818
|
+
function documentWeight() {
|
|
2819
|
+
try {
|
|
2820
|
+
return document.getElementsByTagName("*").length;
|
|
2821
|
+
} catch {
|
|
2822
|
+
return 0;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2802
2825
|
function withTimeout(promise, ms) {
|
|
2803
2826
|
let timer;
|
|
2804
2827
|
return Promise.race([
|
|
@@ -2860,8 +2883,13 @@ function fitToBudget(canvas) {
|
|
|
2860
2883
|
function canvasToBlob(canvas, type, quality) {
|
|
2861
2884
|
return new Promise((resolve) => {
|
|
2862
2885
|
if (canvas.toBlob) {
|
|
2863
|
-
|
|
2864
|
-
|
|
2886
|
+
try {
|
|
2887
|
+
canvas.toBlob((b) => resolve(b), type, quality);
|
|
2888
|
+
return;
|
|
2889
|
+
} catch {
|
|
2890
|
+
resolve(null);
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2865
2893
|
}
|
|
2866
2894
|
try {
|
|
2867
2895
|
const dataUrl = canvas.toDataURL(type, quality);
|
|
@@ -3048,7 +3076,9 @@ function planRender(rect, sx, sy, vw, vh) {
|
|
|
3048
3076
|
};
|
|
3049
3077
|
}
|
|
3050
3078
|
async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
3051
|
-
const
|
|
3079
|
+
const mod = await withTimeout(import('html2canvas'), CHUNK_TIMEOUT_MS);
|
|
3080
|
+
if (!mod) return null;
|
|
3081
|
+
const { default: html2canvas } = mod;
|
|
3052
3082
|
const { vw, vh } = viewportSize();
|
|
3053
3083
|
const plan = planRender(rect, sx, sy, vw, vh);
|
|
3054
3084
|
let scale = Math.min(window.devicePixelRatio || 1, 2);
|
|
@@ -3067,7 +3097,22 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
|
3067
3097
|
height: plan.render.height,
|
|
3068
3098
|
scale,
|
|
3069
3099
|
useCORS: true,
|
|
3070
|
-
allowTaint: true
|
|
3100
|
+
// MUST stay false. `allowTaint: true` tells html2canvas to draw
|
|
3101
|
+
// cross-origin images it could not fetch with CORS -- which succeeds,
|
|
3102
|
+
// and TAINTS the canvas. A tainted canvas throws SecurityError from
|
|
3103
|
+
// toBlob/toDataURL, so the render finishes and then the encode dies,
|
|
3104
|
+
// and the tester gets "couldn't capture" with nothing to look at.
|
|
3105
|
+
//
|
|
3106
|
+
// That is one image away on any real page: a map tile, a news
|
|
3107
|
+
// thumbnail, an avatar, an ad, a CDN logo. It made image-heavy pages
|
|
3108
|
+
// fail 100% of the time while a plain page worked, which is exactly
|
|
3109
|
+
// the "it works for you and not for me" this kept producing.
|
|
3110
|
+
//
|
|
3111
|
+
// False is strictly better: html2canvas SKIPS the images it cannot
|
|
3112
|
+
// read, so those come out blank and everything else comes out right.
|
|
3113
|
+
// A screenshot with two grey rectangles in it is worth having. A
|
|
3114
|
+
// SecurityError is not.
|
|
3115
|
+
allowTaint: false,
|
|
3071
3116
|
// The page's own background, never null — see resolvePageBackground().
|
|
3072
3117
|
// Passed straight to html2canvas's parser, so it gets the same
|
|
3073
3118
|
// CSS Color 4 treatment the document walk applies.
|
|
@@ -3116,6 +3161,10 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
3116
3161
|
}
|
|
3117
3162
|
if (prefer === "exact") return { status: "failed" };
|
|
3118
3163
|
}
|
|
3164
|
+
const weight = documentWeight();
|
|
3165
|
+
if (weight > TOO_HEAVY_NODES) {
|
|
3166
|
+
return { status: "too-heavy", nodes: weight };
|
|
3167
|
+
}
|
|
3119
3168
|
try {
|
|
3120
3169
|
const canvas = await captureViaDom(rect, sx, sy);
|
|
3121
3170
|
if (!canvas) return { status: "failed" };
|
|
@@ -3173,6 +3222,12 @@ function formatEvent(ev, t0) {
|
|
|
3173
3222
|
}
|
|
3174
3223
|
return `[${rel}] uncaught: ${oneLine(ev.message)}`;
|
|
3175
3224
|
}
|
|
3225
|
+
function noteCheckLine(note, index) {
|
|
3226
|
+
const where = oneLine(note.route) || "/";
|
|
3227
|
+
const what = oneLine(note.description) || "(no description)";
|
|
3228
|
+
const trimmed = what.length > 160 ? `${what.slice(0, 157)}...` : what;
|
|
3229
|
+
return `- [ ] **check-${index}** (\`${where}\`) \u2014 ${trimmed}`;
|
|
3230
|
+
}
|
|
3176
3231
|
function noteToMarkdown(note, opts) {
|
|
3177
3232
|
const brand = opts?.brand ?? "Qapture";
|
|
3178
3233
|
const idx = opts?.index;
|
|
@@ -3230,6 +3285,14 @@ function noteToMarkdown(note, opts) {
|
|
|
3230
3285
|
lines.push(`${i + 1}. ${formatStep(step, t0)}`);
|
|
3231
3286
|
});
|
|
3232
3287
|
}
|
|
3288
|
+
if (idx != null) {
|
|
3289
|
+
lines.push("");
|
|
3290
|
+
lines.push(`**Check ${idx} \u2014 how this will be graded**`);
|
|
3291
|
+
lines.push("");
|
|
3292
|
+
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?*");
|
|
3293
|
+
lines.push("");
|
|
3294
|
+
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\`.`);
|
|
3295
|
+
}
|
|
3233
3296
|
const ctx = note.context;
|
|
3234
3297
|
if (ctx) {
|
|
3235
3298
|
const env = ctx.env;
|
|
@@ -3402,6 +3465,17 @@ Second, **the previous attempt is evidence.** Something about the first reading
|
|
|
3402
3465
|
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.
|
|
3403
3466
|
|
|
3404
3467
|
And if a point is phrased as a question rather than a report, **answer it \u2014 do not change code on a maybe.**`
|
|
3468
|
+
);
|
|
3469
|
+
sections.push(
|
|
3470
|
+
`## You Are Being Graded On This
|
|
3471
|
+
|
|
3472
|
+
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.
|
|
3473
|
+
|
|
3474
|
+
**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.
|
|
3475
|
+
|
|
3476
|
+
**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.
|
|
3477
|
+
|
|
3478
|
+
Hand \`verify.md\` back with the work.`
|
|
3405
3479
|
);
|
|
3406
3480
|
const stack = typeof p.stack === "string" && p.stack.trim() ? p.stack.trim() : "(not provided)";
|
|
3407
3481
|
const runArr = toStrings(p.runCommands);
|
|
@@ -3549,6 +3623,39 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
|
|
|
3549
3623
|
` : "";
|
|
3550
3624
|
const notesMd = preambleMd + "\n\n---NOTES---\n\n" + notesHeader + notesBody;
|
|
3551
3625
|
zip.file("notes.md", notesMd);
|
|
3626
|
+
zip.file("verify.md", [
|
|
3627
|
+
`# ${brandLabel} \u2014 checks to satisfy`,
|
|
3628
|
+
"",
|
|
3629
|
+
`Exported: ${stamp}`,
|
|
3630
|
+
`Checks: ${notes.length}`,
|
|
3631
|
+
"",
|
|
3632
|
+
"Each line below is one thing the tester asked for. The full ask, the",
|
|
3633
|
+
"screenshot, the page and the element are in `notes.md` under the matching",
|
|
3634
|
+
'"Point" heading.',
|
|
3635
|
+
"",
|
|
3636
|
+
"**What is expected of you**",
|
|
3637
|
+
"",
|
|
3638
|
+
"1. Do the work in `notes.md`.",
|
|
3639
|
+
"2. Tick a box here only when the check is true on a fresh load of the page",
|
|
3640
|
+
" named beside it, with the tester doing nothing extra.",
|
|
3641
|
+
"3. Leave a box unticked and write one line under it saying why, if you",
|
|
3642
|
+
" could not do it or disagree with it. An unticked box with a reason is a",
|
|
3643
|
+
" good answer; a ticked box that is not true is the only bad one.",
|
|
3644
|
+
"4. Hand this file back with the work.",
|
|
3645
|
+
"",
|
|
3646
|
+
"The tester will then be walked back through these same checks on the real",
|
|
3647
|
+
"page, in this order, and asked about each one. Anything they say is still",
|
|
3648
|
+
"wrong comes back to you as round two, with their new words attached.",
|
|
3649
|
+
"",
|
|
3650
|
+
"---",
|
|
3651
|
+
"",
|
|
3652
|
+
...notes.map((n, i) => noteCheckLine(n, i + 1)),
|
|
3653
|
+
"",
|
|
3654
|
+
"---",
|
|
3655
|
+
"",
|
|
3656
|
+
"Re-open the walkthrough on the tester's machine with `?qa=walk:verify`.",
|
|
3657
|
+
""
|
|
3658
|
+
].join("\n"));
|
|
3552
3659
|
notes.forEach((n, i) => {
|
|
3553
3660
|
if (n.screenshot && shots) {
|
|
3554
3661
|
shots.file(`point-${i + 1}.${shotExtension(n.screenshot)}`, n.screenshot);
|
|
@@ -4188,6 +4295,9 @@ var GUIDE_FAILED_KEY = "guideFailed";
|
|
|
4188
4295
|
var LOGIN_KEY = "logins";
|
|
4189
4296
|
var PENDING_DELETE_KEY = "pendingDeleteIds";
|
|
4190
4297
|
var EXACT_SHOTS_KEY = "exactShots";
|
|
4298
|
+
function exactShotsWanted(store) {
|
|
4299
|
+
return store.getItem(EXACT_SHOTS_KEY) !== "0";
|
|
4300
|
+
}
|
|
4191
4301
|
var SIMPLE_MODE_KEY = "simpleMode";
|
|
4192
4302
|
var COMPACT_KEY = "compactCapture";
|
|
4193
4303
|
var LAST_CAMPAIGN_KEY = "lastCampaign";
|
|
@@ -4266,7 +4376,7 @@ function QaProvider({
|
|
|
4266
4376
|
() => new Set(storage.getJSON(LOGIN_KEY, []))
|
|
4267
4377
|
);
|
|
4268
4378
|
const [exactStatus, setExactStatus] = React.useState(() => {
|
|
4269
|
-
if (storage
|
|
4379
|
+
if (exactShotsWanted(storage)) armExactCapture();
|
|
4270
4380
|
return getExactCaptureStatus();
|
|
4271
4381
|
});
|
|
4272
4382
|
const exactSupported = isExactCaptureSupported();
|
|
@@ -4779,7 +4889,7 @@ function QaProvider({
|
|
|
4779
4889
|
setCapturePrefill(prefill ?? "");
|
|
4780
4890
|
setCaptureActive(true);
|
|
4781
4891
|
setFrozenAt(null);
|
|
4782
|
-
if (storage
|
|
4892
|
+
if (exactShotsWanted(storage) && isExactCaptureSupported()) {
|
|
4783
4893
|
resetExactCaptureDecline();
|
|
4784
4894
|
void freezeViewport().then((frame) => {
|
|
4785
4895
|
setFrozenAt(frame?.takenAt ?? null);
|
|
@@ -4811,19 +4921,36 @@ function QaProvider({
|
|
|
4811
4921
|
return next;
|
|
4812
4922
|
});
|
|
4813
4923
|
}, [storage]);
|
|
4924
|
+
const stampHandedOff = React.useCallback(async (ids, at) => {
|
|
4925
|
+
if (!ids.length) return;
|
|
4926
|
+
const idSet = new Set(ids);
|
|
4927
|
+
const touched = [];
|
|
4928
|
+
applyNotes(
|
|
4929
|
+
(prev) => prev.map((n) => {
|
|
4930
|
+
if (!idSet.has(n.id)) return n;
|
|
4931
|
+
if ((n.status ?? "open") === "verified") return n;
|
|
4932
|
+
const next = { ...n, handedOffAt: at };
|
|
4933
|
+
touched.push(next);
|
|
4934
|
+
return next;
|
|
4935
|
+
})
|
|
4936
|
+
);
|
|
4937
|
+
for (const note of touched) await idb.put(note);
|
|
4938
|
+
}, [applyNotes, idb]);
|
|
4814
4939
|
const exportZipFn = React.useCallback(
|
|
4815
4940
|
async (filename) => {
|
|
4816
4941
|
if (!notes.length || isExporting) return;
|
|
4817
4942
|
setIsExporting(true);
|
|
4818
4943
|
try {
|
|
4819
|
-
|
|
4944
|
+
const stamp = nowIso();
|
|
4945
|
+
await buildAndDownloadZip(notes, stamp, filename, config, guideChecked, guideSkipped);
|
|
4946
|
+
await stampHandedOff(notes.map((n) => n.id), stamp);
|
|
4820
4947
|
} catch (err) {
|
|
4821
4948
|
console.error("[QA] export failed", err);
|
|
4822
4949
|
} finally {
|
|
4823
4950
|
setIsExporting(false);
|
|
4824
4951
|
}
|
|
4825
4952
|
},
|
|
4826
|
-
[notes, isExporting, config, guideChecked, guideSkipped]
|
|
4953
|
+
[notes, isExporting, config, guideChecked, guideSkipped, stampHandedOff]
|
|
4827
4954
|
);
|
|
4828
4955
|
const exportZipRef = React.useRef(async () => {
|
|
4829
4956
|
});
|
|
@@ -5009,6 +5136,7 @@ function QaProvider({
|
|
|
5009
5136
|
design: 0,
|
|
5010
5137
|
enhance: 0,
|
|
5011
5138
|
open: 0,
|
|
5139
|
+
sent: 0,
|
|
5012
5140
|
fixed: 0,
|
|
5013
5141
|
verified: 0,
|
|
5014
5142
|
thisPage: 0
|
|
@@ -5022,6 +5150,7 @@ function QaProvider({
|
|
|
5022
5150
|
if (status === "verified") counts.verified++;
|
|
5023
5151
|
else if (status === "fixed") counts.fixed++;
|
|
5024
5152
|
else counts.open++;
|
|
5153
|
+
if (n.handedOffAt && status !== "verified") counts.sent++;
|
|
5025
5154
|
if (n.route.split("?")[0] === currentRoute) counts.thisPage++;
|
|
5026
5155
|
}
|
|
5027
5156
|
return counts;
|
|
@@ -5030,7 +5159,9 @@ function QaProvider({
|
|
|
5030
5159
|
const q = filter.query.trim().toLowerCase();
|
|
5031
5160
|
return notes.filter((n) => {
|
|
5032
5161
|
if (filter.severity !== "all" && (n.severity ?? "bug") !== filter.severity) return false;
|
|
5033
|
-
if (filter.status
|
|
5162
|
+
if (filter.status === "sent") {
|
|
5163
|
+
if (!n.handedOffAt || (n.status ?? "open") === "verified") return false;
|
|
5164
|
+
} else if (filter.status !== "all" && (n.status ?? "open") !== filter.status) return false;
|
|
5034
5165
|
if (filter.thisPageOnly && n.route.split("?")[0] !== currentRoute) return false;
|
|
5035
5166
|
if (!q) return true;
|
|
5036
5167
|
const haystack = `${n.description} ${n.route} ${n.target?.selector ?? ""} ${n.target?.text ?? ""}`;
|
|
@@ -5075,6 +5206,7 @@ function QaProvider({
|
|
|
5075
5206
|
const blob = await buildZipBlob(notes, stamp, config, guideChecked, guideSkipped);
|
|
5076
5207
|
if (!blob) return { status: "unsupported" };
|
|
5077
5208
|
const outcome = await shareZipFile(blob, name, config.brand.label);
|
|
5209
|
+
await stampHandedOff(notes.map((n) => n.id), stamp);
|
|
5078
5210
|
if (outcome.status === "needs-gesture") {
|
|
5079
5211
|
setPendingShare({ blob, filename: name });
|
|
5080
5212
|
} else if (outcome.status === "unsupported") {
|
|
@@ -5086,7 +5218,7 @@ function QaProvider({
|
|
|
5086
5218
|
} finally {
|
|
5087
5219
|
setIsExporting(false);
|
|
5088
5220
|
}
|
|
5089
|
-
}, [notes, config, guideChecked, guideSkipped]);
|
|
5221
|
+
}, [notes, config, guideChecked, guideSkipped, stampHandedOff]);
|
|
5090
5222
|
const sharePending = React.useCallback(async () => {
|
|
5091
5223
|
const pending = pendingShare;
|
|
5092
5224
|
if (!pending) return { status: "unsupported" };
|
|
@@ -5302,7 +5434,10 @@ function QaProvider({
|
|
|
5302
5434
|
window.history.replaceState({}, "", window.location.pathname + (qs ? `?${qs}` : ""));
|
|
5303
5435
|
} catch {
|
|
5304
5436
|
}
|
|
5305
|
-
if (which === "
|
|
5437
|
+
if (which === "verify") {
|
|
5438
|
+
setFilterState((prev) => ({ ...prev, status: "sent", severity: "all", thisPageOnly: false }));
|
|
5439
|
+
startWalk("notes", 0);
|
|
5440
|
+
} else if (which === "retest") {
|
|
5306
5441
|
setFilterState((prev) => ({ ...prev, status: "fixed", severity: "all", thisPageOnly: false }));
|
|
5307
5442
|
startWalk("notes", 0);
|
|
5308
5443
|
} else if (which === "notes") {
|
|
@@ -7360,7 +7495,7 @@ function FilterChip({ chip }) {
|
|
|
7360
7495
|
);
|
|
7361
7496
|
}
|
|
7362
7497
|
function NoteFilterBar() {
|
|
7363
|
-
const { filter, setFilter, noteCounts, notes, deleteNotes, t } = useQa();
|
|
7498
|
+
const { filter, setFilter, noteCounts, notes, deleteNotes, startWalk, t } = useQa();
|
|
7364
7499
|
const [confirmPrune, setConfirmPrune] = React.useState(false);
|
|
7365
7500
|
if (notes.length < 3) return null;
|
|
7366
7501
|
const others = notes.filter((n) => (n.status ?? "open") !== "fixed");
|
|
@@ -7402,6 +7537,18 @@ function NoteFilterBar() {
|
|
|
7402
7537
|
tone: "accent",
|
|
7403
7538
|
onClick: sev("design")
|
|
7404
7539
|
},
|
|
7540
|
+
// Only ever shown once something has actually been sent. A chip reading
|
|
7541
|
+
// "Sent 0" on a first run is noise; the same chip reading "Sent 6" the
|
|
7542
|
+
// morning after an export is the one thing the tester came back for.
|
|
7543
|
+
...noteCounts.sent > 0 ? [{
|
|
7544
|
+
key: "sent",
|
|
7545
|
+
label: t("status_sent"),
|
|
7546
|
+
count: noteCounts.sent,
|
|
7547
|
+
active: filter.status === "sent",
|
|
7548
|
+
icon: "Camera",
|
|
7549
|
+
tone: "accent",
|
|
7550
|
+
onClick: () => setFilter({ status: "sent", severity: "all", thisPageOnly: false })
|
|
7551
|
+
}] : [],
|
|
7405
7552
|
{
|
|
7406
7553
|
key: "open",
|
|
7407
7554
|
label: t("status_open"),
|
|
@@ -7470,6 +7617,24 @@ function NoteFilterBar() {
|
|
|
7470
7617
|
}
|
|
7471
7618
|
),
|
|
7472
7619
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-flex qa-flex-wrap qa-gap-1", children: chips.map((chip) => /* @__PURE__ */ jsxRuntime.jsx(FilterChip, { chip }, chip.key)) }),
|
|
7620
|
+
noteCounts.sent > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
7621
|
+
"button",
|
|
7622
|
+
{
|
|
7623
|
+
type: "button",
|
|
7624
|
+
"data-qa-verify-walk": "true",
|
|
7625
|
+
onClick: () => {
|
|
7626
|
+
setFilter({ status: "sent", severity: "all", thisPageOnly: false });
|
|
7627
|
+
startWalk("notes", 0);
|
|
7628
|
+
},
|
|
7629
|
+
title: t("check_fixes_hint"),
|
|
7630
|
+
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",
|
|
7631
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
7632
|
+
children: [
|
|
7633
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 13 }),
|
|
7634
|
+
t("check_fixes", { n: noteCounts.sent })
|
|
7635
|
+
]
|
|
7636
|
+
}
|
|
7637
|
+
),
|
|
7473
7638
|
showPrune && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-text-11", "data-qa-prune": "true", children: confirmPrune ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-text-mid", children: [
|
|
7474
7639
|
t("keep_only_retest_q", { n: others.length }),
|
|
7475
7640
|
" ",
|
|
@@ -9225,10 +9390,12 @@ function CaptureMode() {
|
|
|
9225
9390
|
const [regionMode, setRegionMode] = React.useState(false);
|
|
9226
9391
|
const [shot, setShot] = React.useState(null);
|
|
9227
9392
|
const stillRef = React.useRef(null);
|
|
9393
|
+
const [notesFromThisShot, setNotesFromThisShot] = React.useState(0);
|
|
9228
9394
|
const [shotEngine, setShotEngine] = React.useState(null);
|
|
9229
9395
|
const [shotUrl, setShotUrl] = React.useState(null);
|
|
9230
9396
|
const [capturing, setCapturing] = React.useState(false);
|
|
9231
9397
|
const [captureError, setCaptureError] = React.useState(false);
|
|
9398
|
+
const [tooHeavy, setTooHeavy] = React.useState(false);
|
|
9232
9399
|
const [description, setDescription] = React.useState(capturePrefill);
|
|
9233
9400
|
const [severity, setSeverity] = React.useState("bug");
|
|
9234
9401
|
const [targetForensics, setTargetForensics] = React.useState(void 0);
|
|
@@ -9258,6 +9425,7 @@ function CaptureMode() {
|
|
|
9258
9425
|
const runCapture = React.useCallback(async (rect) => {
|
|
9259
9426
|
setCapturing(true);
|
|
9260
9427
|
setCaptureError(false);
|
|
9428
|
+
setTooHeavy(false);
|
|
9261
9429
|
lockPageScroll();
|
|
9262
9430
|
try {
|
|
9263
9431
|
const outcome = await captureRegion(rect, scrollSnap.current);
|
|
@@ -9268,6 +9436,7 @@ function CaptureMode() {
|
|
|
9268
9436
|
return;
|
|
9269
9437
|
}
|
|
9270
9438
|
setCaptureError(outcome.status === "failed");
|
|
9439
|
+
setTooHeavy(outcome.status === "too-heavy");
|
|
9271
9440
|
setShotEngine(outcome.status === "ok" ? outcome.engine : null);
|
|
9272
9441
|
setShot(blob);
|
|
9273
9442
|
setShotUrl((old) => {
|
|
@@ -9529,7 +9698,17 @@ function CaptureMode() {
|
|
|
9529
9698
|
canvas.removeAttribute("style");
|
|
9530
9699
|
};
|
|
9531
9700
|
}, [exactShots.frozenAt]);
|
|
9532
|
-
const
|
|
9701
|
+
const resetForNextNote = React.useCallback(() => {
|
|
9702
|
+
setPhase("selecting");
|
|
9703
|
+
setSelection(null);
|
|
9704
|
+
setShot(null);
|
|
9705
|
+
setShotEngine(null);
|
|
9706
|
+
setDescription("");
|
|
9707
|
+
setSeverity("bug");
|
|
9708
|
+
setTargetForensics(void 0);
|
|
9709
|
+
setCaptureError(false);
|
|
9710
|
+
}, []);
|
|
9711
|
+
const save = async (keepGoing = false) => {
|
|
9533
9712
|
if (!selection || !description.trim()) return;
|
|
9534
9713
|
const target = {
|
|
9535
9714
|
kind: selection.kind,
|
|
@@ -9551,6 +9730,11 @@ function CaptureMode() {
|
|
|
9551
9730
|
severity,
|
|
9552
9731
|
forensics: selection.kind === "element" ? targetForensics : void 0
|
|
9553
9732
|
});
|
|
9733
|
+
if (keepGoing) {
|
|
9734
|
+
setNotesFromThisShot((n) => n + 1);
|
|
9735
|
+
resetForNextNote();
|
|
9736
|
+
return;
|
|
9737
|
+
}
|
|
9554
9738
|
endCapture();
|
|
9555
9739
|
};
|
|
9556
9740
|
const popStyleFor = React.useCallback((r, cardWidth = 340) => {
|
|
@@ -9618,6 +9802,21 @@ function CaptureMode() {
|
|
|
9618
9802
|
{
|
|
9619
9803
|
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",
|
|
9620
9804
|
children: [
|
|
9805
|
+
notesFromThisShot > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
9806
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
9807
|
+
"span",
|
|
9808
|
+
{
|
|
9809
|
+
"data-qa-shot-reused": "true",
|
|
9810
|
+
className: "qa-flex qa-items-center qa-gap-1.5",
|
|
9811
|
+
style: { color: "var(--qa-success)" },
|
|
9812
|
+
children: [
|
|
9813
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Camera", size: 16 }),
|
|
9814
|
+
t("shot_reused", { n: String(notesFromThisShot) })
|
|
9815
|
+
]
|
|
9816
|
+
}
|
|
9817
|
+
),
|
|
9818
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-opacity-50", children: "\xB7" })
|
|
9819
|
+
] }),
|
|
9621
9820
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5", children: [
|
|
9622
9821
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MousePointerClick", size: 16 }),
|
|
9623
9822
|
t("cap_click")
|
|
@@ -10067,6 +10266,29 @@ function CaptureMode() {
|
|
|
10067
10266
|
)
|
|
10068
10267
|
]
|
|
10069
10268
|
}
|
|
10269
|
+
) : tooHeavy ? (
|
|
10270
|
+
// Not a failure to report — a refusal to explain, with the one
|
|
10271
|
+
// control that actually solves it.
|
|
10272
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-flex-col qa-items-center qa-gap-2 qa-py-3 qa-px-3 qa-text-center", children: [
|
|
10273
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-xs qa-text-mid", children: t("too_heavy") }),
|
|
10274
|
+
exactShots.supported ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
10275
|
+
"button",
|
|
10276
|
+
{
|
|
10277
|
+
type: "button",
|
|
10278
|
+
onClick: () => void (async () => {
|
|
10279
|
+
const shot2 = await photographNow();
|
|
10280
|
+
if (shot2 && selection) void runCapture(selection.rect);
|
|
10281
|
+
})(),
|
|
10282
|
+
title: t("exact_hint"),
|
|
10283
|
+
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",
|
|
10284
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
10285
|
+
children: [
|
|
10286
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Camera", size: 13 }),
|
|
10287
|
+
t("redraw_action")
|
|
10288
|
+
]
|
|
10289
|
+
}
|
|
10290
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-10 qa-text-mid", children: t("redraw_no_engine") })
|
|
10291
|
+
] })
|
|
10070
10292
|
) : captureError ? (
|
|
10071
10293
|
// The render broke rather than being skipped — offer a retry
|
|
10072
10294
|
// against the same selection instead of a dead-end message.
|
|
@@ -10173,7 +10395,7 @@ function CaptureMode() {
|
|
|
10173
10395
|
value: description,
|
|
10174
10396
|
onChange: (e) => setDescription(e.target.value),
|
|
10175
10397
|
onKeyDown: (e) => {
|
|
10176
|
-
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save();
|
|
10398
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
|
|
10177
10399
|
if ((e.altKey || e.metaKey || e.ctrlKey) && SEVERITIES2[Number(e.key) - 1]) {
|
|
10178
10400
|
e.preventDefault();
|
|
10179
10401
|
setSeverity(SEVERITIES2[Number(e.key) - 1]);
|
|
@@ -10198,17 +10420,25 @@ function CaptureMode() {
|
|
|
10198
10420
|
]
|
|
10199
10421
|
}
|
|
10200
10422
|
),
|
|
10423
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
10424
|
+
"button",
|
|
10425
|
+
{
|
|
10426
|
+
"data-qa-save-next": "true",
|
|
10427
|
+
onClick: () => void save(true),
|
|
10428
|
+
disabled: !description.trim(),
|
|
10429
|
+
title: t("save_next_hint"),
|
|
10430
|
+
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",
|
|
10431
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
10432
|
+
children: [
|
|
10433
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Plus", size: 16 }),
|
|
10434
|
+
t("save_next")
|
|
10435
|
+
]
|
|
10436
|
+
}
|
|
10437
|
+
),
|
|
10201
10438
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10202
10439
|
"button",
|
|
10203
10440
|
{
|
|
10204
|
-
onClick:
|
|
10205
|
-
setPhase("selecting");
|
|
10206
|
-
setSelection(null);
|
|
10207
|
-
setShot(null);
|
|
10208
|
-
setDescription("");
|
|
10209
|
-
setSeverity("bug");
|
|
10210
|
-
setTargetForensics(void 0);
|
|
10211
|
-
},
|
|
10441
|
+
onClick: resetForNextNote,
|
|
10212
10442
|
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-mid",
|
|
10213
10443
|
style: { background: "transparent", cursor: "pointer" },
|
|
10214
10444
|
children: t("reselect")
|
|
@@ -10426,5 +10656,5 @@ function Qapture({ config }) {
|
|
|
10426
10656
|
exports.Qapture = Qapture;
|
|
10427
10657
|
exports.deleteQaDatabase = deleteQaDatabase;
|
|
10428
10658
|
exports.initQaStudio = initQaStudio;
|
|
10429
|
-
//# sourceMappingURL=chunk-
|
|
10430
|
-
//# sourceMappingURL=chunk-
|
|
10659
|
+
//# sourceMappingURL=chunk-IYQ5JZJX.cjs.map
|
|
10660
|
+
//# sourceMappingURL=chunk-IYQ5JZJX.cjs.map
|