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
|
@@ -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",
|
|
@@ -2786,12 +2800,21 @@ function neutralizeDocumentColors(doc, aggressive = false) {
|
|
|
2786
2800
|
|
|
2787
2801
|
// src/lib/capture.ts
|
|
2788
2802
|
var HTML2CANVAS_TIMEOUT_MS = 1e4;
|
|
2803
|
+
var CHUNK_TIMEOUT_MS = 8e3;
|
|
2789
2804
|
var FALLBACK_PAGE_BACKGROUND = "#ffffff";
|
|
2790
2805
|
var MAX_SHOT_EDGE = 1800;
|
|
2791
2806
|
var WEBP_QUALITY = 0.92;
|
|
2792
2807
|
var STUCK_ATTR = "data-qa-stuck";
|
|
2793
2808
|
var MAX_CAPTURE_EDGE = 4e3;
|
|
2794
2809
|
var MAX_RENDER_DEVICE_PIXELS = 16e6;
|
|
2810
|
+
var TOO_HEAVY_NODES = 6e3;
|
|
2811
|
+
function documentWeight() {
|
|
2812
|
+
try {
|
|
2813
|
+
return document.getElementsByTagName("*").length;
|
|
2814
|
+
} catch {
|
|
2815
|
+
return 0;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2795
2818
|
function withTimeout(promise, ms) {
|
|
2796
2819
|
let timer;
|
|
2797
2820
|
return Promise.race([
|
|
@@ -2853,8 +2876,13 @@ function fitToBudget(canvas) {
|
|
|
2853
2876
|
function canvasToBlob(canvas, type, quality) {
|
|
2854
2877
|
return new Promise((resolve) => {
|
|
2855
2878
|
if (canvas.toBlob) {
|
|
2856
|
-
|
|
2857
|
-
|
|
2879
|
+
try {
|
|
2880
|
+
canvas.toBlob((b) => resolve(b), type, quality);
|
|
2881
|
+
return;
|
|
2882
|
+
} catch {
|
|
2883
|
+
resolve(null);
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2858
2886
|
}
|
|
2859
2887
|
try {
|
|
2860
2888
|
const dataUrl = canvas.toDataURL(type, quality);
|
|
@@ -3041,7 +3069,9 @@ function planRender(rect, sx, sy, vw, vh) {
|
|
|
3041
3069
|
};
|
|
3042
3070
|
}
|
|
3043
3071
|
async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
3044
|
-
const
|
|
3072
|
+
const mod = await withTimeout(import('html2canvas'), CHUNK_TIMEOUT_MS);
|
|
3073
|
+
if (!mod) return null;
|
|
3074
|
+
const { default: html2canvas } = mod;
|
|
3045
3075
|
const { vw, vh } = viewportSize();
|
|
3046
3076
|
const plan = planRender(rect, sx, sy, vw, vh);
|
|
3047
3077
|
let scale = Math.min(window.devicePixelRatio || 1, 2);
|
|
@@ -3060,7 +3090,22 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
|
3060
3090
|
height: plan.render.height,
|
|
3061
3091
|
scale,
|
|
3062
3092
|
useCORS: true,
|
|
3063
|
-
allowTaint: true
|
|
3093
|
+
// MUST stay false. `allowTaint: true` tells html2canvas to draw
|
|
3094
|
+
// cross-origin images it could not fetch with CORS -- which succeeds,
|
|
3095
|
+
// and TAINTS the canvas. A tainted canvas throws SecurityError from
|
|
3096
|
+
// toBlob/toDataURL, so the render finishes and then the encode dies,
|
|
3097
|
+
// and the tester gets "couldn't capture" with nothing to look at.
|
|
3098
|
+
//
|
|
3099
|
+
// That is one image away on any real page: a map tile, a news
|
|
3100
|
+
// thumbnail, an avatar, an ad, a CDN logo. It made image-heavy pages
|
|
3101
|
+
// fail 100% of the time while a plain page worked, which is exactly
|
|
3102
|
+
// the "it works for you and not for me" this kept producing.
|
|
3103
|
+
//
|
|
3104
|
+
// False is strictly better: html2canvas SKIPS the images it cannot
|
|
3105
|
+
// read, so those come out blank and everything else comes out right.
|
|
3106
|
+
// A screenshot with two grey rectangles in it is worth having. A
|
|
3107
|
+
// SecurityError is not.
|
|
3108
|
+
allowTaint: false,
|
|
3064
3109
|
// The page's own background, never null — see resolvePageBackground().
|
|
3065
3110
|
// Passed straight to html2canvas's parser, so it gets the same
|
|
3066
3111
|
// CSS Color 4 treatment the document walk applies.
|
|
@@ -3109,6 +3154,10 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
3109
3154
|
}
|
|
3110
3155
|
if (prefer === "exact") return { status: "failed" };
|
|
3111
3156
|
}
|
|
3157
|
+
const weight = documentWeight();
|
|
3158
|
+
if (weight > TOO_HEAVY_NODES) {
|
|
3159
|
+
return { status: "too-heavy", nodes: weight };
|
|
3160
|
+
}
|
|
3112
3161
|
try {
|
|
3113
3162
|
const canvas = await captureViaDom(rect, sx, sy);
|
|
3114
3163
|
if (!canvas) return { status: "failed" };
|
|
@@ -3166,6 +3215,12 @@ function formatEvent(ev, t0) {
|
|
|
3166
3215
|
}
|
|
3167
3216
|
return `[${rel}] uncaught: ${oneLine(ev.message)}`;
|
|
3168
3217
|
}
|
|
3218
|
+
function noteCheckLine(note, index) {
|
|
3219
|
+
const where = oneLine(note.route) || "/";
|
|
3220
|
+
const what = oneLine(note.description) || "(no description)";
|
|
3221
|
+
const trimmed = what.length > 160 ? `${what.slice(0, 157)}...` : what;
|
|
3222
|
+
return `- [ ] **check-${index}** (\`${where}\`) \u2014 ${trimmed}`;
|
|
3223
|
+
}
|
|
3169
3224
|
function noteToMarkdown(note, opts) {
|
|
3170
3225
|
const brand = opts?.brand ?? "Qapture";
|
|
3171
3226
|
const idx = opts?.index;
|
|
@@ -3223,6 +3278,14 @@ function noteToMarkdown(note, opts) {
|
|
|
3223
3278
|
lines.push(`${i + 1}. ${formatStep(step, t0)}`);
|
|
3224
3279
|
});
|
|
3225
3280
|
}
|
|
3281
|
+
if (idx != null) {
|
|
3282
|
+
lines.push("");
|
|
3283
|
+
lines.push(`**Check ${idx} \u2014 how this will be graded**`);
|
|
3284
|
+
lines.push("");
|
|
3285
|
+
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?*");
|
|
3286
|
+
lines.push("");
|
|
3287
|
+
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\`.`);
|
|
3288
|
+
}
|
|
3226
3289
|
const ctx = note.context;
|
|
3227
3290
|
if (ctx) {
|
|
3228
3291
|
const env = ctx.env;
|
|
@@ -3395,6 +3458,17 @@ Second, **the previous attempt is evidence.** Something about the first reading
|
|
|
3395
3458
|
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
3459
|
|
|
3397
3460
|
And if a point is phrased as a question rather than a report, **answer it \u2014 do not change code on a maybe.**`
|
|
3461
|
+
);
|
|
3462
|
+
sections.push(
|
|
3463
|
+
`## You Are Being Graded On This
|
|
3464
|
+
|
|
3465
|
+
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.
|
|
3466
|
+
|
|
3467
|
+
**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.
|
|
3468
|
+
|
|
3469
|
+
**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.
|
|
3470
|
+
|
|
3471
|
+
Hand \`verify.md\` back with the work.`
|
|
3398
3472
|
);
|
|
3399
3473
|
const stack = typeof p.stack === "string" && p.stack.trim() ? p.stack.trim() : "(not provided)";
|
|
3400
3474
|
const runArr = toStrings(p.runCommands);
|
|
@@ -3542,6 +3616,39 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
|
|
|
3542
3616
|
` : "";
|
|
3543
3617
|
const notesMd = preambleMd + "\n\n---NOTES---\n\n" + notesHeader + notesBody;
|
|
3544
3618
|
zip.file("notes.md", notesMd);
|
|
3619
|
+
zip.file("verify.md", [
|
|
3620
|
+
`# ${brandLabel} \u2014 checks to satisfy`,
|
|
3621
|
+
"",
|
|
3622
|
+
`Exported: ${stamp}`,
|
|
3623
|
+
`Checks: ${notes.length}`,
|
|
3624
|
+
"",
|
|
3625
|
+
"Each line below is one thing the tester asked for. The full ask, the",
|
|
3626
|
+
"screenshot, the page and the element are in `notes.md` under the matching",
|
|
3627
|
+
'"Point" heading.',
|
|
3628
|
+
"",
|
|
3629
|
+
"**What is expected of you**",
|
|
3630
|
+
"",
|
|
3631
|
+
"1. Do the work in `notes.md`.",
|
|
3632
|
+
"2. Tick a box here only when the check is true on a fresh load of the page",
|
|
3633
|
+
" named beside it, with the tester doing nothing extra.",
|
|
3634
|
+
"3. Leave a box unticked and write one line under it saying why, if you",
|
|
3635
|
+
" could not do it or disagree with it. An unticked box with a reason is a",
|
|
3636
|
+
" good answer; a ticked box that is not true is the only bad one.",
|
|
3637
|
+
"4. Hand this file back with the work.",
|
|
3638
|
+
"",
|
|
3639
|
+
"The tester will then be walked back through these same checks on the real",
|
|
3640
|
+
"page, in this order, and asked about each one. Anything they say is still",
|
|
3641
|
+
"wrong comes back to you as round two, with their new words attached.",
|
|
3642
|
+
"",
|
|
3643
|
+
"---",
|
|
3644
|
+
"",
|
|
3645
|
+
...notes.map((n, i) => noteCheckLine(n, i + 1)),
|
|
3646
|
+
"",
|
|
3647
|
+
"---",
|
|
3648
|
+
"",
|
|
3649
|
+
"Re-open the walkthrough on the tester's machine with `?qa=walk:verify`.",
|
|
3650
|
+
""
|
|
3651
|
+
].join("\n"));
|
|
3545
3652
|
notes.forEach((n, i) => {
|
|
3546
3653
|
if (n.screenshot && shots) {
|
|
3547
3654
|
shots.file(`point-${i + 1}.${shotExtension(n.screenshot)}`, n.screenshot);
|
|
@@ -4181,6 +4288,9 @@ var GUIDE_FAILED_KEY = "guideFailed";
|
|
|
4181
4288
|
var LOGIN_KEY = "logins";
|
|
4182
4289
|
var PENDING_DELETE_KEY = "pendingDeleteIds";
|
|
4183
4290
|
var EXACT_SHOTS_KEY = "exactShots";
|
|
4291
|
+
function exactShotsWanted(store) {
|
|
4292
|
+
return store.getItem(EXACT_SHOTS_KEY) !== "0";
|
|
4293
|
+
}
|
|
4184
4294
|
var SIMPLE_MODE_KEY = "simpleMode";
|
|
4185
4295
|
var COMPACT_KEY = "compactCapture";
|
|
4186
4296
|
var LAST_CAMPAIGN_KEY = "lastCampaign";
|
|
@@ -4259,7 +4369,7 @@ function QaProvider({
|
|
|
4259
4369
|
() => new Set(storage.getJSON(LOGIN_KEY, []))
|
|
4260
4370
|
);
|
|
4261
4371
|
const [exactStatus, setExactStatus] = useState(() => {
|
|
4262
|
-
if (storage
|
|
4372
|
+
if (exactShotsWanted(storage)) armExactCapture();
|
|
4263
4373
|
return getExactCaptureStatus();
|
|
4264
4374
|
});
|
|
4265
4375
|
const exactSupported = isExactCaptureSupported();
|
|
@@ -4772,7 +4882,7 @@ function QaProvider({
|
|
|
4772
4882
|
setCapturePrefill(prefill ?? "");
|
|
4773
4883
|
setCaptureActive(true);
|
|
4774
4884
|
setFrozenAt(null);
|
|
4775
|
-
if (storage
|
|
4885
|
+
if (exactShotsWanted(storage) && isExactCaptureSupported()) {
|
|
4776
4886
|
resetExactCaptureDecline();
|
|
4777
4887
|
void freezeViewport().then((frame) => {
|
|
4778
4888
|
setFrozenAt(frame?.takenAt ?? null);
|
|
@@ -4804,19 +4914,36 @@ function QaProvider({
|
|
|
4804
4914
|
return next;
|
|
4805
4915
|
});
|
|
4806
4916
|
}, [storage]);
|
|
4917
|
+
const stampHandedOff = useCallback(async (ids, at) => {
|
|
4918
|
+
if (!ids.length) return;
|
|
4919
|
+
const idSet = new Set(ids);
|
|
4920
|
+
const touched = [];
|
|
4921
|
+
applyNotes(
|
|
4922
|
+
(prev) => prev.map((n) => {
|
|
4923
|
+
if (!idSet.has(n.id)) return n;
|
|
4924
|
+
if ((n.status ?? "open") === "verified") return n;
|
|
4925
|
+
const next = { ...n, handedOffAt: at };
|
|
4926
|
+
touched.push(next);
|
|
4927
|
+
return next;
|
|
4928
|
+
})
|
|
4929
|
+
);
|
|
4930
|
+
for (const note of touched) await idb.put(note);
|
|
4931
|
+
}, [applyNotes, idb]);
|
|
4807
4932
|
const exportZipFn = useCallback(
|
|
4808
4933
|
async (filename) => {
|
|
4809
4934
|
if (!notes.length || isExporting) return;
|
|
4810
4935
|
setIsExporting(true);
|
|
4811
4936
|
try {
|
|
4812
|
-
|
|
4937
|
+
const stamp = nowIso();
|
|
4938
|
+
await buildAndDownloadZip(notes, stamp, filename, config, guideChecked, guideSkipped);
|
|
4939
|
+
await stampHandedOff(notes.map((n) => n.id), stamp);
|
|
4813
4940
|
} catch (err) {
|
|
4814
4941
|
console.error("[QA] export failed", err);
|
|
4815
4942
|
} finally {
|
|
4816
4943
|
setIsExporting(false);
|
|
4817
4944
|
}
|
|
4818
4945
|
},
|
|
4819
|
-
[notes, isExporting, config, guideChecked, guideSkipped]
|
|
4946
|
+
[notes, isExporting, config, guideChecked, guideSkipped, stampHandedOff]
|
|
4820
4947
|
);
|
|
4821
4948
|
const exportZipRef = useRef(async () => {
|
|
4822
4949
|
});
|
|
@@ -5002,6 +5129,7 @@ function QaProvider({
|
|
|
5002
5129
|
design: 0,
|
|
5003
5130
|
enhance: 0,
|
|
5004
5131
|
open: 0,
|
|
5132
|
+
sent: 0,
|
|
5005
5133
|
fixed: 0,
|
|
5006
5134
|
verified: 0,
|
|
5007
5135
|
thisPage: 0
|
|
@@ -5015,6 +5143,7 @@ function QaProvider({
|
|
|
5015
5143
|
if (status === "verified") counts.verified++;
|
|
5016
5144
|
else if (status === "fixed") counts.fixed++;
|
|
5017
5145
|
else counts.open++;
|
|
5146
|
+
if (n.handedOffAt && status !== "verified") counts.sent++;
|
|
5018
5147
|
if (n.route.split("?")[0] === currentRoute) counts.thisPage++;
|
|
5019
5148
|
}
|
|
5020
5149
|
return counts;
|
|
@@ -5023,7 +5152,9 @@ function QaProvider({
|
|
|
5023
5152
|
const q = filter.query.trim().toLowerCase();
|
|
5024
5153
|
return notes.filter((n) => {
|
|
5025
5154
|
if (filter.severity !== "all" && (n.severity ?? "bug") !== filter.severity) return false;
|
|
5026
|
-
if (filter.status
|
|
5155
|
+
if (filter.status === "sent") {
|
|
5156
|
+
if (!n.handedOffAt || (n.status ?? "open") === "verified") return false;
|
|
5157
|
+
} else if (filter.status !== "all" && (n.status ?? "open") !== filter.status) return false;
|
|
5027
5158
|
if (filter.thisPageOnly && n.route.split("?")[0] !== currentRoute) return false;
|
|
5028
5159
|
if (!q) return true;
|
|
5029
5160
|
const haystack = `${n.description} ${n.route} ${n.target?.selector ?? ""} ${n.target?.text ?? ""}`;
|
|
@@ -5068,6 +5199,7 @@ function QaProvider({
|
|
|
5068
5199
|
const blob = await buildZipBlob(notes, stamp, config, guideChecked, guideSkipped);
|
|
5069
5200
|
if (!blob) return { status: "unsupported" };
|
|
5070
5201
|
const outcome = await shareZipFile(blob, name, config.brand.label);
|
|
5202
|
+
await stampHandedOff(notes.map((n) => n.id), stamp);
|
|
5071
5203
|
if (outcome.status === "needs-gesture") {
|
|
5072
5204
|
setPendingShare({ blob, filename: name });
|
|
5073
5205
|
} else if (outcome.status === "unsupported") {
|
|
@@ -5079,7 +5211,7 @@ function QaProvider({
|
|
|
5079
5211
|
} finally {
|
|
5080
5212
|
setIsExporting(false);
|
|
5081
5213
|
}
|
|
5082
|
-
}, [notes, config, guideChecked, guideSkipped]);
|
|
5214
|
+
}, [notes, config, guideChecked, guideSkipped, stampHandedOff]);
|
|
5083
5215
|
const sharePending = useCallback(async () => {
|
|
5084
5216
|
const pending = pendingShare;
|
|
5085
5217
|
if (!pending) return { status: "unsupported" };
|
|
@@ -5295,7 +5427,10 @@ function QaProvider({
|
|
|
5295
5427
|
window.history.replaceState({}, "", window.location.pathname + (qs ? `?${qs}` : ""));
|
|
5296
5428
|
} catch {
|
|
5297
5429
|
}
|
|
5298
|
-
if (which === "
|
|
5430
|
+
if (which === "verify") {
|
|
5431
|
+
setFilterState((prev) => ({ ...prev, status: "sent", severity: "all", thisPageOnly: false }));
|
|
5432
|
+
startWalk("notes", 0);
|
|
5433
|
+
} else if (which === "retest") {
|
|
5299
5434
|
setFilterState((prev) => ({ ...prev, status: "fixed", severity: "all", thisPageOnly: false }));
|
|
5300
5435
|
startWalk("notes", 0);
|
|
5301
5436
|
} else if (which === "notes") {
|
|
@@ -7353,7 +7488,7 @@ function FilterChip({ chip }) {
|
|
|
7353
7488
|
);
|
|
7354
7489
|
}
|
|
7355
7490
|
function NoteFilterBar() {
|
|
7356
|
-
const { filter, setFilter, noteCounts, notes, deleteNotes, t } = useQa();
|
|
7491
|
+
const { filter, setFilter, noteCounts, notes, deleteNotes, startWalk, t } = useQa();
|
|
7357
7492
|
const [confirmPrune, setConfirmPrune] = useState(false);
|
|
7358
7493
|
if (notes.length < 3) return null;
|
|
7359
7494
|
const others = notes.filter((n) => (n.status ?? "open") !== "fixed");
|
|
@@ -7395,6 +7530,18 @@ function NoteFilterBar() {
|
|
|
7395
7530
|
tone: "accent",
|
|
7396
7531
|
onClick: sev("design")
|
|
7397
7532
|
},
|
|
7533
|
+
// Only ever shown once something has actually been sent. A chip reading
|
|
7534
|
+
// "Sent 0" on a first run is noise; the same chip reading "Sent 6" the
|
|
7535
|
+
// morning after an export is the one thing the tester came back for.
|
|
7536
|
+
...noteCounts.sent > 0 ? [{
|
|
7537
|
+
key: "sent",
|
|
7538
|
+
label: t("status_sent"),
|
|
7539
|
+
count: noteCounts.sent,
|
|
7540
|
+
active: filter.status === "sent",
|
|
7541
|
+
icon: "Camera",
|
|
7542
|
+
tone: "accent",
|
|
7543
|
+
onClick: () => setFilter({ status: "sent", severity: "all", thisPageOnly: false })
|
|
7544
|
+
}] : [],
|
|
7398
7545
|
{
|
|
7399
7546
|
key: "open",
|
|
7400
7547
|
label: t("status_open"),
|
|
@@ -7463,6 +7610,24 @@ function NoteFilterBar() {
|
|
|
7463
7610
|
}
|
|
7464
7611
|
),
|
|
7465
7612
|
/* @__PURE__ */ jsx("div", { className: "qa-flex qa-flex-wrap qa-gap-1", children: chips.map((chip) => /* @__PURE__ */ jsx(FilterChip, { chip }, chip.key)) }),
|
|
7613
|
+
noteCounts.sent > 0 && /* @__PURE__ */ jsxs(
|
|
7614
|
+
"button",
|
|
7615
|
+
{
|
|
7616
|
+
type: "button",
|
|
7617
|
+
"data-qa-verify-walk": "true",
|
|
7618
|
+
onClick: () => {
|
|
7619
|
+
setFilter({ status: "sent", severity: "all", thisPageOnly: false });
|
|
7620
|
+
startWalk("notes", 0);
|
|
7621
|
+
},
|
|
7622
|
+
title: t("check_fixes_hint"),
|
|
7623
|
+
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",
|
|
7624
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
7625
|
+
children: [
|
|
7626
|
+
/* @__PURE__ */ jsx(Icon, { name: "Check", size: 13 }),
|
|
7627
|
+
t("check_fixes", { n: noteCounts.sent })
|
|
7628
|
+
]
|
|
7629
|
+
}
|
|
7630
|
+
),
|
|
7466
7631
|
showPrune && /* @__PURE__ */ jsx("div", { className: "qa-text-11", "data-qa-prune": "true", children: confirmPrune ? /* @__PURE__ */ jsxs("span", { className: "qa-text-mid", children: [
|
|
7467
7632
|
t("keep_only_retest_q", { n: others.length }),
|
|
7468
7633
|
" ",
|
|
@@ -9218,10 +9383,12 @@ function CaptureMode() {
|
|
|
9218
9383
|
const [regionMode, setRegionMode] = useState(false);
|
|
9219
9384
|
const [shot, setShot] = useState(null);
|
|
9220
9385
|
const stillRef = useRef(null);
|
|
9386
|
+
const [notesFromThisShot, setNotesFromThisShot] = useState(0);
|
|
9221
9387
|
const [shotEngine, setShotEngine] = useState(null);
|
|
9222
9388
|
const [shotUrl, setShotUrl] = useState(null);
|
|
9223
9389
|
const [capturing, setCapturing] = useState(false);
|
|
9224
9390
|
const [captureError, setCaptureError] = useState(false);
|
|
9391
|
+
const [tooHeavy, setTooHeavy] = useState(false);
|
|
9225
9392
|
const [description, setDescription] = useState(capturePrefill);
|
|
9226
9393
|
const [severity, setSeverity] = useState("bug");
|
|
9227
9394
|
const [targetForensics, setTargetForensics] = useState(void 0);
|
|
@@ -9251,6 +9418,7 @@ function CaptureMode() {
|
|
|
9251
9418
|
const runCapture = useCallback(async (rect) => {
|
|
9252
9419
|
setCapturing(true);
|
|
9253
9420
|
setCaptureError(false);
|
|
9421
|
+
setTooHeavy(false);
|
|
9254
9422
|
lockPageScroll();
|
|
9255
9423
|
try {
|
|
9256
9424
|
const outcome = await captureRegion(rect, scrollSnap.current);
|
|
@@ -9261,6 +9429,7 @@ function CaptureMode() {
|
|
|
9261
9429
|
return;
|
|
9262
9430
|
}
|
|
9263
9431
|
setCaptureError(outcome.status === "failed");
|
|
9432
|
+
setTooHeavy(outcome.status === "too-heavy");
|
|
9264
9433
|
setShotEngine(outcome.status === "ok" ? outcome.engine : null);
|
|
9265
9434
|
setShot(blob);
|
|
9266
9435
|
setShotUrl((old) => {
|
|
@@ -9522,7 +9691,17 @@ function CaptureMode() {
|
|
|
9522
9691
|
canvas.removeAttribute("style");
|
|
9523
9692
|
};
|
|
9524
9693
|
}, [exactShots.frozenAt]);
|
|
9525
|
-
const
|
|
9694
|
+
const resetForNextNote = useCallback(() => {
|
|
9695
|
+
setPhase("selecting");
|
|
9696
|
+
setSelection(null);
|
|
9697
|
+
setShot(null);
|
|
9698
|
+
setShotEngine(null);
|
|
9699
|
+
setDescription("");
|
|
9700
|
+
setSeverity("bug");
|
|
9701
|
+
setTargetForensics(void 0);
|
|
9702
|
+
setCaptureError(false);
|
|
9703
|
+
}, []);
|
|
9704
|
+
const save = async (keepGoing = false) => {
|
|
9526
9705
|
if (!selection || !description.trim()) return;
|
|
9527
9706
|
const target = {
|
|
9528
9707
|
kind: selection.kind,
|
|
@@ -9544,6 +9723,11 @@ function CaptureMode() {
|
|
|
9544
9723
|
severity,
|
|
9545
9724
|
forensics: selection.kind === "element" ? targetForensics : void 0
|
|
9546
9725
|
});
|
|
9726
|
+
if (keepGoing) {
|
|
9727
|
+
setNotesFromThisShot((n) => n + 1);
|
|
9728
|
+
resetForNextNote();
|
|
9729
|
+
return;
|
|
9730
|
+
}
|
|
9547
9731
|
endCapture();
|
|
9548
9732
|
};
|
|
9549
9733
|
const popStyleFor = useCallback((r, cardWidth = 340) => {
|
|
@@ -9611,6 +9795,21 @@ function CaptureMode() {
|
|
|
9611
9795
|
{
|
|
9612
9796
|
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
9797
|
children: [
|
|
9798
|
+
notesFromThisShot > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
9799
|
+
/* @__PURE__ */ jsxs(
|
|
9800
|
+
"span",
|
|
9801
|
+
{
|
|
9802
|
+
"data-qa-shot-reused": "true",
|
|
9803
|
+
className: "qa-flex qa-items-center qa-gap-1.5",
|
|
9804
|
+
style: { color: "var(--qa-success)" },
|
|
9805
|
+
children: [
|
|
9806
|
+
/* @__PURE__ */ jsx(Icon, { name: "Camera", size: 16 }),
|
|
9807
|
+
t("shot_reused", { n: String(notesFromThisShot) })
|
|
9808
|
+
]
|
|
9809
|
+
}
|
|
9810
|
+
),
|
|
9811
|
+
/* @__PURE__ */ jsx("span", { className: "qa-opacity-50", children: "\xB7" })
|
|
9812
|
+
] }),
|
|
9614
9813
|
/* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5", children: [
|
|
9615
9814
|
/* @__PURE__ */ jsx(Icon, { name: "MousePointerClick", size: 16 }),
|
|
9616
9815
|
t("cap_click")
|
|
@@ -10060,6 +10259,29 @@ function CaptureMode() {
|
|
|
10060
10259
|
)
|
|
10061
10260
|
]
|
|
10062
10261
|
}
|
|
10262
|
+
) : tooHeavy ? (
|
|
10263
|
+
// Not a failure to report — a refusal to explain, with the one
|
|
10264
|
+
// control that actually solves it.
|
|
10265
|
+
/* @__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: [
|
|
10266
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-xs qa-text-mid", children: t("too_heavy") }),
|
|
10267
|
+
exactShots.supported ? /* @__PURE__ */ jsxs(
|
|
10268
|
+
"button",
|
|
10269
|
+
{
|
|
10270
|
+
type: "button",
|
|
10271
|
+
onClick: () => void (async () => {
|
|
10272
|
+
const shot2 = await photographNow();
|
|
10273
|
+
if (shot2 && selection) void runCapture(selection.rect);
|
|
10274
|
+
})(),
|
|
10275
|
+
title: t("exact_hint"),
|
|
10276
|
+
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",
|
|
10277
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
10278
|
+
children: [
|
|
10279
|
+
/* @__PURE__ */ jsx(Icon, { name: "Camera", size: 13 }),
|
|
10280
|
+
t("redraw_action")
|
|
10281
|
+
]
|
|
10282
|
+
}
|
|
10283
|
+
) : /* @__PURE__ */ jsx("span", { className: "qa-text-10 qa-text-mid", children: t("redraw_no_engine") })
|
|
10284
|
+
] })
|
|
10063
10285
|
) : captureError ? (
|
|
10064
10286
|
// The render broke rather than being skipped — offer a retry
|
|
10065
10287
|
// against the same selection instead of a dead-end message.
|
|
@@ -10166,7 +10388,7 @@ function CaptureMode() {
|
|
|
10166
10388
|
value: description,
|
|
10167
10389
|
onChange: (e) => setDescription(e.target.value),
|
|
10168
10390
|
onKeyDown: (e) => {
|
|
10169
|
-
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save();
|
|
10391
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
|
|
10170
10392
|
if ((e.altKey || e.metaKey || e.ctrlKey) && SEVERITIES2[Number(e.key) - 1]) {
|
|
10171
10393
|
e.preventDefault();
|
|
10172
10394
|
setSeverity(SEVERITIES2[Number(e.key) - 1]);
|
|
@@ -10191,17 +10413,25 @@ function CaptureMode() {
|
|
|
10191
10413
|
]
|
|
10192
10414
|
}
|
|
10193
10415
|
),
|
|
10416
|
+
/* @__PURE__ */ jsxs(
|
|
10417
|
+
"button",
|
|
10418
|
+
{
|
|
10419
|
+
"data-qa-save-next": "true",
|
|
10420
|
+
onClick: () => void save(true),
|
|
10421
|
+
disabled: !description.trim(),
|
|
10422
|
+
title: t("save_next_hint"),
|
|
10423
|
+
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",
|
|
10424
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
10425
|
+
children: [
|
|
10426
|
+
/* @__PURE__ */ jsx(Icon, { name: "Plus", size: 16 }),
|
|
10427
|
+
t("save_next")
|
|
10428
|
+
]
|
|
10429
|
+
}
|
|
10430
|
+
),
|
|
10194
10431
|
/* @__PURE__ */ jsx(
|
|
10195
10432
|
"button",
|
|
10196
10433
|
{
|
|
10197
|
-
onClick:
|
|
10198
|
-
setPhase("selecting");
|
|
10199
|
-
setSelection(null);
|
|
10200
|
-
setShot(null);
|
|
10201
|
-
setDescription("");
|
|
10202
|
-
setSeverity("bug");
|
|
10203
|
-
setTargetForensics(void 0);
|
|
10204
|
-
},
|
|
10434
|
+
onClick: resetForNextNote,
|
|
10205
10435
|
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-mid",
|
|
10206
10436
|
style: { background: "transparent", cursor: "pointer" },
|
|
10207
10437
|
children: t("reselect")
|
|
@@ -10417,5 +10647,5 @@ function Qapture({ config }) {
|
|
|
10417
10647
|
}
|
|
10418
10648
|
|
|
10419
10649
|
export { Qapture, deleteQaDatabase, initQaStudio };
|
|
10420
|
-
//# sourceMappingURL=chunk-
|
|
10421
|
-
//# sourceMappingURL=chunk-
|
|
10650
|
+
//# sourceMappingURL=chunk-JGSKPW7Y.js.map
|
|
10651
|
+
//# sourceMappingURL=chunk-JGSKPW7Y.js.map
|
package/dist/index.cjs
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkIYQ5JZJX_cjs = require('./chunk-IYQ5JZJX.cjs');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
Object.defineProperty(exports, "QaStudio", {
|
|
8
8
|
enumerable: true,
|
|
9
|
-
get: function () { return
|
|
9
|
+
get: function () { return chunkIYQ5JZJX_cjs.Qapture; }
|
|
10
10
|
});
|
|
11
11
|
Object.defineProperty(exports, "Qapture", {
|
|
12
12
|
enumerable: true,
|
|
13
|
-
get: function () { return
|
|
13
|
+
get: function () { return chunkIYQ5JZJX_cjs.Qapture; }
|
|
14
14
|
});
|
|
15
15
|
Object.defineProperty(exports, "deleteQaDatabase", {
|
|
16
16
|
enumerable: true,
|
|
17
|
-
get: function () { return
|
|
17
|
+
get: function () { return chunkIYQ5JZJX_cjs.deleteQaDatabase; }
|
|
18
18
|
});
|
|
19
19
|
Object.defineProperty(exports, "initQaStudio", {
|
|
20
20
|
enumerable: true,
|
|
21
|
-
get: function () { return
|
|
21
|
+
get: function () { return chunkIYQ5JZJX_cjs.initQaStudio; }
|
|
22
22
|
});
|
|
23
23
|
//# sourceMappingURL=index.cjs.map
|
|
24
24
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.js
CHANGED
package/dist/next.cjs
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var chunkIYQ5JZJX_cjs = require('./chunk-IYQ5JZJX.cjs');
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
Object.defineProperty(exports, "QaStudio", {
|
|
9
9
|
enumerable: true,
|
|
10
|
-
get: function () { return
|
|
10
|
+
get: function () { return chunkIYQ5JZJX_cjs.Qapture; }
|
|
11
11
|
});
|
|
12
12
|
Object.defineProperty(exports, "Qapture", {
|
|
13
13
|
enumerable: true,
|
|
14
|
-
get: function () { return
|
|
14
|
+
get: function () { return chunkIYQ5JZJX_cjs.Qapture; }
|
|
15
15
|
});
|
|
16
16
|
Object.defineProperty(exports, "initQaStudio", {
|
|
17
17
|
enumerable: true,
|
|
18
|
-
get: function () { return
|
|
18
|
+
get: function () { return chunkIYQ5JZJX_cjs.initQaStudio; }
|
|
19
19
|
});
|
|
20
20
|
//# sourceMappingURL=next.cjs.map
|
|
21
21
|
//# sourceMappingURL=next.cjs.map
|
package/dist/next.js
CHANGED
package/dist/standalone.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkIYQ5JZJX_cjs = require('./chunk-IYQ5JZJX.cjs');
|
|
4
4
|
|
|
5
5
|
// src/standalone.ts
|
|
6
6
|
if (typeof window !== "undefined" && typeof customElements !== "undefined" && !customElements.get("qapture-widget")) {
|
|
@@ -38,7 +38,7 @@ if (typeof window !== "undefined" && typeof customElements !== "undefined" && !c
|
|
|
38
38
|
|
|
39
39
|
Object.defineProperty(exports, "initQaStudio", {
|
|
40
40
|
enumerable: true,
|
|
41
|
-
get: function () { return
|
|
41
|
+
get: function () { return chunkIYQ5JZJX_cjs.initQaStudio; }
|
|
42
42
|
});
|
|
43
43
|
//# sourceMappingURL=standalone.cjs.map
|
|
44
44
|
//# sourceMappingURL=standalone.cjs.map
|
package/dist/standalone.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qapture2",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Drop-in, AI-aware in-browser QA capture widget. A tester annotates the live app (element/region + auto-screenshot + note), tracks a graded testing journey, and exports a ZIP your own coding agent reads. Ships zero AI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|