qapture2 0.8.3 → 0.9.1
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/dist/{chunk-32LKZ2PG.js → chunk-HEHVZCSV.js} +802 -119
- package/dist/{chunk-OSYYMS6R.cjs → chunk-J77SGE5E.cjs} +802 -119
- package/dist/index.cjs +5 -5
- package/dist/index.d.cts +38 -0
- package/dist/index.d.ts +38 -0
- 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
|
@@ -176,6 +176,8 @@ var DEFAULTS = {
|
|
|
176
176
|
brandLabel: "Qapture",
|
|
177
177
|
loginField: { en: "Username", ar: "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062A\u062E\u062F\u0645" },
|
|
178
178
|
rtl: false,
|
|
179
|
+
beta: false,
|
|
180
|
+
collector: null,
|
|
179
181
|
visible: void 0,
|
|
180
182
|
alwaysVisible: false,
|
|
181
183
|
hotkey: "shift+alt+q",
|
|
@@ -320,9 +322,9 @@ function warnMissingArabic(loginField, credentials, journey, warnings) {
|
|
|
320
322
|
if (c.hint !== void 0 && !hasAr(c.hint)) missing.push(`credentials role="${c.role}" (hint.ar)`);
|
|
321
323
|
}
|
|
322
324
|
if (!missing.length) return;
|
|
323
|
-
const
|
|
324
|
-
const shown = missing.slice(0,
|
|
325
|
-
const rest = missing.length >
|
|
325
|
+
const LIMIT2 = 6;
|
|
326
|
+
const shown = missing.slice(0, LIMIT2).join("; ");
|
|
327
|
+
const rest = missing.length > LIMIT2 ? ` (+${missing.length - LIMIT2} more)` : "";
|
|
326
328
|
warnings.push(
|
|
327
329
|
`Arabic is used elsewhere in this config, but ${missing.length} bilingual field(s) have no "ar" and will show English to an Arabic-language tester: ${shown}${rest}. Every {en, ar} pair needs BOTH filled in \u2014 a plain string or an {en}-only object reads identically to a bilingual field that was simply never translated.`
|
|
328
330
|
);
|
|
@@ -347,6 +349,8 @@ function validateConfig(input) {
|
|
|
347
349
|
journey: [],
|
|
348
350
|
preamble: null,
|
|
349
351
|
rtl: DEFAULTS.rtl,
|
|
352
|
+
beta: DEFAULTS.beta,
|
|
353
|
+
collector: null,
|
|
350
354
|
visible: DEFAULTS.visible,
|
|
351
355
|
alwaysVisible: DEFAULTS.alwaysVisible,
|
|
352
356
|
hotkey: DEFAULTS.hotkey,
|
|
@@ -367,6 +371,8 @@ function validateConfig(input) {
|
|
|
367
371
|
journey: [],
|
|
368
372
|
preamble: null,
|
|
369
373
|
rtl: DEFAULTS.rtl,
|
|
374
|
+
beta: DEFAULTS.beta,
|
|
375
|
+
collector: null,
|
|
370
376
|
visible: DEFAULTS.visible,
|
|
371
377
|
alwaysVisible: DEFAULTS.alwaysVisible,
|
|
372
378
|
hotkey: DEFAULTS.hotkey,
|
|
@@ -402,6 +408,15 @@ function validateConfig(input) {
|
|
|
402
408
|
const journey = raw["journey"] !== void 0 ? coerceJourney(raw["journey"], warnings) : [];
|
|
403
409
|
const preamble = raw["preamble"] !== void 0 ? coercePreamble(raw["preamble"]) : null;
|
|
404
410
|
const rtl = typeof raw["rtl"] === "boolean" ? raw["rtl"] : DEFAULTS.rtl;
|
|
411
|
+
const beta = typeof raw["beta"] === "boolean" ? raw["beta"] : DEFAULTS.beta;
|
|
412
|
+
const rawCol = raw["collector"];
|
|
413
|
+
const collector = rawCol && typeof rawCol["url"] === "string" && typeof rawCol["token"] === "string" && typeof rawCol["project"] === "string" ? {
|
|
414
|
+
url: rawCol["url"],
|
|
415
|
+
token: rawCol["token"],
|
|
416
|
+
project: rawCol["project"],
|
|
417
|
+
campaign: typeof rawCol["campaign"] === "string" ? rawCol["campaign"] : void 0,
|
|
418
|
+
tester: typeof rawCol["tester"] === "string" ? rawCol["tester"] : void 0
|
|
419
|
+
} : DEFAULTS.collector;
|
|
405
420
|
const alwaysVisible = typeof raw["alwaysVisible"] === "boolean" ? raw["alwaysVisible"] : DEFAULTS.alwaysVisible;
|
|
406
421
|
const hotkey = isNonEmptyString(raw["hotkey"]) ? raw["hotkey"].trim() : DEFAULTS.hotkey;
|
|
407
422
|
const captureHotkey = isNonEmptyString(raw["captureHotkey"]) ? raw["captureHotkey"].trim() : DEFAULTS.captureHotkey;
|
|
@@ -424,6 +439,8 @@ function validateConfig(input) {
|
|
|
424
439
|
journey,
|
|
425
440
|
preamble,
|
|
426
441
|
rtl,
|
|
442
|
+
beta,
|
|
443
|
+
collector,
|
|
427
444
|
visible,
|
|
428
445
|
alwaysVisible,
|
|
429
446
|
hotkey,
|
|
@@ -1701,6 +1718,98 @@ function createStorage(namespace) {
|
|
|
1701
1718
|
return { getItem, setItem, getJSON, setJSON };
|
|
1702
1719
|
}
|
|
1703
1720
|
|
|
1721
|
+
// src/lib/faultLog.ts
|
|
1722
|
+
var LIMIT = 40;
|
|
1723
|
+
var faults = [];
|
|
1724
|
+
function recordFault(where, err) {
|
|
1725
|
+
const what = err instanceof Error ? `${err.name}: ${err.message}` : typeof err === "string" ? err : (() => {
|
|
1726
|
+
try {
|
|
1727
|
+
return JSON.stringify(err);
|
|
1728
|
+
} catch {
|
|
1729
|
+
return String(err);
|
|
1730
|
+
}
|
|
1731
|
+
})();
|
|
1732
|
+
faults.push({ at: Date.now(), where, what: what.slice(0, 500) });
|
|
1733
|
+
if (faults.length > LIMIT) faults.splice(0, faults.length - LIMIT);
|
|
1734
|
+
console.warn(`[QA] ${where}:`, err);
|
|
1735
|
+
}
|
|
1736
|
+
function readFaults() {
|
|
1737
|
+
return [...faults].reverse();
|
|
1738
|
+
}
|
|
1739
|
+
function clearFaults() {
|
|
1740
|
+
faults.length = 0;
|
|
1741
|
+
}
|
|
1742
|
+
function faultsAsText(version) {
|
|
1743
|
+
if (!faults.length) return "No faults recorded.";
|
|
1744
|
+
const head = [
|
|
1745
|
+
`qapture ${version}`,
|
|
1746
|
+
typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
1747
|
+
typeof location !== "undefined" ? location.href.split("?")[0] : "",
|
|
1748
|
+
""
|
|
1749
|
+
].filter(Boolean).join("\n");
|
|
1750
|
+
return head + readFaults().map((f) => `${new Date(f.at).toISOString()} [${f.where}] ${f.what}`).join("\n");
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// src/lib/collector.ts
|
|
1754
|
+
var TIMEOUT_MS = 8e3;
|
|
1755
|
+
var MAX_SHOT_BYTES = 6 * 1024 * 1024;
|
|
1756
|
+
function blobToDataUrl(blob) {
|
|
1757
|
+
return new Promise((resolve) => {
|
|
1758
|
+
try {
|
|
1759
|
+
const reader = new FileReader();
|
|
1760
|
+
reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : null);
|
|
1761
|
+
reader.onerror = () => resolve(null);
|
|
1762
|
+
reader.readAsDataURL(blob);
|
|
1763
|
+
} catch {
|
|
1764
|
+
resolve(null);
|
|
1765
|
+
}
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
async function sendToCollector(note, cfg) {
|
|
1769
|
+
if (typeof fetch === "undefined" || !cfg?.url || !cfg.token || !cfg.project) return false;
|
|
1770
|
+
let shot;
|
|
1771
|
+
if (note.screenshot && note.screenshot.size <= MAX_SHOT_BYTES) {
|
|
1772
|
+
shot = await blobToDataUrl(note.screenshot) ?? void 0;
|
|
1773
|
+
}
|
|
1774
|
+
const controller = new AbortController();
|
|
1775
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
1776
|
+
try {
|
|
1777
|
+
const res = await fetch(`${cfg.url.replace(/\/$/, "")}/notes`, {
|
|
1778
|
+
method: "POST",
|
|
1779
|
+
signal: controller.signal,
|
|
1780
|
+
headers: {
|
|
1781
|
+
"content-type": "application/json",
|
|
1782
|
+
authorization: `Bearer ${cfg.token}`
|
|
1783
|
+
},
|
|
1784
|
+
body: JSON.stringify({
|
|
1785
|
+
project: cfg.project,
|
|
1786
|
+
// A campaign per day is the shape that matches how testing actually
|
|
1787
|
+
// happens, and it means nobody has to name anything.
|
|
1788
|
+
campaign: cfg.campaign || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1789
|
+
tester: cfg.tester,
|
|
1790
|
+
id: note.id,
|
|
1791
|
+
route: note.route,
|
|
1792
|
+
description: note.description,
|
|
1793
|
+
wanted: note.wanted,
|
|
1794
|
+
why: note.why,
|
|
1795
|
+
severity: note.severity,
|
|
1796
|
+
origin: note.origin,
|
|
1797
|
+
shot
|
|
1798
|
+
})
|
|
1799
|
+
});
|
|
1800
|
+
if (!res.ok) {
|
|
1801
|
+
recordFault("collector", `server answered ${res.status}`);
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
return true;
|
|
1805
|
+
} catch (err) {
|
|
1806
|
+
recordFault("collector", err);
|
|
1807
|
+
return false;
|
|
1808
|
+
} finally {
|
|
1809
|
+
clearTimeout(timer);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1704
1813
|
// src/lib/strings.ts
|
|
1705
1814
|
var STR = {
|
|
1706
1815
|
en: {
|
|
@@ -1735,6 +1844,32 @@ var STR = {
|
|
|
1735
1844
|
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.",
|
|
1736
1845
|
no_shot: "no screenshot (location saved)",
|
|
1737
1846
|
annotate_placeholder: "What do you want to do here? (add / remove / change\u2026)",
|
|
1847
|
+
// Asked as two plain questions, never as jargon. "Expected behaviour" is
|
|
1848
|
+
// a phrase from a bug tracker; "what should have happened" is a question
|
|
1849
|
+
// anybody can answer.
|
|
1850
|
+
q_observed: "What happened?",
|
|
1851
|
+
q_observed_hint: "What you saw. Plain words are fine.",
|
|
1852
|
+
q_wanted: "What should have happened?",
|
|
1853
|
+
q_wanted_hint: "What you expected instead. This is the important one.",
|
|
1854
|
+
q_why: "Why does it matter? (optional)",
|
|
1855
|
+
q_why_hint: "What you were trying to get done.",
|
|
1856
|
+
q_fix: "Suggested fix (optional)",
|
|
1857
|
+
q_fix_hint: "Passed on as a suggestion, not an instruction.",
|
|
1858
|
+
voice_start: "Speak",
|
|
1859
|
+
voice_stop: "Stop",
|
|
1860
|
+
voice_failed: "Dictation did not start",
|
|
1861
|
+
voice_denied: "Microphone blocked",
|
|
1862
|
+
dev_mode_label: "Developer mode",
|
|
1863
|
+
diag_title: "Diagnostics",
|
|
1864
|
+
diag_hint: "Checks the handful of things that actually stop screenshots working on a page. Run it if something looks wrong.",
|
|
1865
|
+
diag_run: "Check this page",
|
|
1866
|
+
diag_running: "Checking\u2026",
|
|
1867
|
+
diag_version: "Version {v}",
|
|
1868
|
+
diag_outdated: "Update available: {v} \u2014",
|
|
1869
|
+
diag_current: "up to date",
|
|
1870
|
+
diag_faults: "Recorded problems: {n}",
|
|
1871
|
+
dup_notice: "You already said something like this at {when}:",
|
|
1872
|
+
dev_mode_hint: "Adds severity, the element selector, a suggested-fix field and the source location. Off by default so the person reporting a problem is asked only what they can actually answer.",
|
|
1738
1873
|
save_point: "Save point",
|
|
1739
1874
|
save_next: "Save + next",
|
|
1740
1875
|
save_next_hint: "Save this one and mark up another part of the same screenshot \u2014 no second permission prompt.",
|
|
@@ -1950,6 +2085,29 @@ var STR = {
|
|
|
1950
2085
|
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.",
|
|
1951
2086
|
no_shot: "\u0628\u062F\u0648\u0646 \u0635\u0648\u0631\u0629 (\u062A\u0645 \u062D\u0641\u0638 \u0627\u0644\u0645\u0648\u0642\u0639)",
|
|
1952
2087
|
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)",
|
|
2088
|
+
q_observed: "\u0645\u0627 \u0627\u0644\u0630\u064A \u062D\u062F\u062B\u061F",
|
|
2089
|
+
q_observed_hint: "\u0645\u0627 \u0631\u0623\u064A\u062A\u0647. \u0628\u0643\u0644\u0645\u0627\u062A\u0643 \u0627\u0644\u0639\u0627\u062F\u064A\u0629.",
|
|
2090
|
+
q_wanted: "\u0645\u0627 \u0627\u0644\u0630\u064A \u0643\u0627\u0646 \u064A\u062C\u0628 \u0623\u0646 \u064A\u062D\u062F\u062B\u061F",
|
|
2091
|
+
q_wanted_hint: "\u0645\u0627 \u062A\u0648\u0642\u0651\u0639\u062A\u0647 \u0628\u062F\u0644 \u0630\u0644\u0643. \u0647\u0630\u0627 \u0647\u0648 \u0627\u0644\u0623\u0647\u0645.",
|
|
2092
|
+
q_why: "\u0644\u0645\u0627\u0630\u0627 \u064A\u0647\u0645\u0651\u0643\u061F (\u0627\u062E\u062A\u064A\u0627\u0631\u064A)",
|
|
2093
|
+
q_why_hint: "\u0645\u0627 \u0627\u0644\u0630\u064A \u0643\u0646\u062A \u062A\u062D\u0627\u0648\u0644 \u0625\u0646\u062C\u0627\u0632\u0647.",
|
|
2094
|
+
q_fix: "\u0627\u0642\u062A\u0631\u0627\u062D \u0644\u0644\u062D\u0644 (\u0627\u062E\u062A\u064A\u0627\u0631\u064A)",
|
|
2095
|
+
q_fix_hint: "\u064A\u064F\u0645\u0631\u064E\u0651\u0631 \u0643\u0627\u0642\u062A\u0631\u0627\u062D\u060C \u0644\u0627 \u0643\u0623\u0645\u0631.",
|
|
2096
|
+
voice_start: "\u062A\u062D\u062F\u0651\u062B",
|
|
2097
|
+
voice_stop: "\u0625\u064A\u0642\u0627\u0641",
|
|
2098
|
+
voice_failed: "\u0644\u0645 \u064A\u0628\u062F\u0623 \u0627\u0644\u0625\u0645\u0644\u0627\u0621 \u0627\u0644\u0635\u0648\u062A\u064A",
|
|
2099
|
+
voice_denied: "\u0627\u0644\u0645\u064A\u0643\u0631\u0648\u0641\u0648\u0646 \u0645\u062D\u062C\u0648\u0628",
|
|
2100
|
+
dev_mode_label: "\u0648\u0636\u0639 \u0627\u0644\u0645\u0637\u0648\u0651\u0631",
|
|
2101
|
+
diag_title: "\u0627\u0644\u062A\u0634\u062E\u064A\u0635",
|
|
2102
|
+
diag_hint: "\u064A\u0641\u062D\u0635 \u0627\u0644\u0623\u0634\u064A\u0627\u0621 \u0627\u0644\u0642\u0644\u064A\u0644\u0629 \u0627\u0644\u062A\u064A \u062A\u0645\u0646\u0639 \u0639\u0645\u0644 \u0627\u0644\u0644\u0642\u0637\u0627\u062A \u0641\u0639\u0644\u064A\u064B\u0627 \u0641\u064A \u0627\u0644\u0635\u0641\u062D\u0629. \u0634\u063A\u0651\u0644\u0647 \u0625\u0630\u0627 \u0628\u062F\u0627 \u0634\u064A\u0621 \u063A\u064A\u0631 \u0633\u0644\u064A\u0645.",
|
|
2103
|
+
diag_run: "\u0627\u0641\u062D\u0635 \u0647\u0630\u0647 \u0627\u0644\u0635\u0641\u062D\u0629",
|
|
2104
|
+
diag_running: "\u062C\u0627\u0631\u064D \u0627\u0644\u0641\u062D\u0635\u2026",
|
|
2105
|
+
diag_version: "\u0627\u0644\u0625\u0635\u062F\u0627\u0631 {v}",
|
|
2106
|
+
diag_outdated: "\u064A\u062A\u0648\u0641\u0631 \u062A\u062D\u062F\u064A\u062B: {v} \u2014",
|
|
2107
|
+
diag_current: "\u0645\u062D\u062F\u0651\u062B",
|
|
2108
|
+
diag_faults: "\u0645\u0634\u0643\u0644\u0627\u062A \u0645\u064F\u0633\u062C\u0651\u0644\u0629: {n}",
|
|
2109
|
+
dup_notice: "\u0633\u0628\u0642 \u0623\u0646 \u0643\u062A\u0628\u062A \u0634\u064A\u0626\u064B\u0627 \u0645\u0634\u0627\u0628\u0647\u064B\u0627 \u0627\u0644\u0633\u0627\u0639\u0629 {when}:",
|
|
2110
|
+
dev_mode_hint: "\u064A\u0636\u064A\u0641 \u0627\u0644\u0623\u0647\u0645\u064A\u0629 \u0648\u0645\u064F\u062D\u062F\u0650\u0651\u062F \u0627\u0644\u0639\u0646\u0635\u0631 \u0648\u062D\u0642\u0644 \u0627\u0642\u062A\u0631\u0627\u062D \u0627\u0644\u062D\u0644 \u0648\u0645\u0648\u0642\u0639 \u0627\u0644\u0634\u064A\u0641\u0631\u0629. \u0645\u064F\u0637\u0641\u0623 \u0627\u0641\u062A\u0631\u0627\u0636\u064A\u064B\u0627 \u062D\u062A\u0649 \u0644\u0627 \u064A\u064F\u0633\u0623\u0644 \u0645\u064E\u0646 \u064A\u064F\u0628\u0644\u0651\u063A \u0639\u0646 \u0645\u0634\u0643\u0644\u0629 \u0625\u0644\u0627 \u0639\u0645\u0651\u0627 \u064A\u0633\u062A\u0637\u064A\u0639 \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0639\u0646\u0647.",
|
|
1953
2111
|
save_point: "\u062D\u0641\u0638 \u0627\u0644\u0646\u0642\u0637\u0629",
|
|
1954
2112
|
save_next: "\u062D\u0641\u0638 + \u0627\u0644\u062A\u0627\u0644\u064A",
|
|
1955
2113
|
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.",
|
|
@@ -3179,12 +3337,13 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
3179
3337
|
if (blob) return { status: "ok", blob, engine: "exact" };
|
|
3180
3338
|
}
|
|
3181
3339
|
} catch (err) {
|
|
3182
|
-
|
|
3340
|
+
recordFault("screenshot/crop", err);
|
|
3183
3341
|
}
|
|
3184
3342
|
if (prefer === "exact") return { status: "failed" };
|
|
3185
3343
|
}
|
|
3186
3344
|
const weight = documentWeight();
|
|
3187
3345
|
if (weight > TOO_HEAVY_NODES) {
|
|
3346
|
+
recordFault("screenshot/too-heavy", `page has ${weight} elements; the redraw engine was refused`);
|
|
3188
3347
|
return { status: "too-heavy", nodes: weight };
|
|
3189
3348
|
}
|
|
3190
3349
|
try {
|
|
@@ -3193,14 +3352,14 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
3193
3352
|
const blob = await encodeShot(canvas);
|
|
3194
3353
|
return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
|
|
3195
3354
|
} catch (err) {
|
|
3196
|
-
|
|
3355
|
+
recordFault("screenshot/render", err);
|
|
3197
3356
|
try {
|
|
3198
3357
|
const canvas = await captureViaDom(rect, sx, sy, true);
|
|
3199
3358
|
if (!canvas) return { status: "failed" };
|
|
3200
3359
|
const blob = await encodeShot(canvas);
|
|
3201
3360
|
return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
|
|
3202
3361
|
} catch (retryErr) {
|
|
3203
|
-
|
|
3362
|
+
recordFault("screenshot/render-retry", retryErr);
|
|
3204
3363
|
return { status: "failed" };
|
|
3205
3364
|
}
|
|
3206
3365
|
}
|
|
@@ -3244,10 +3403,32 @@ function formatEvent(ev, t0) {
|
|
|
3244
3403
|
}
|
|
3245
3404
|
return `[${rel}] uncaught: ${oneLine(ev.message)}`;
|
|
3246
3405
|
}
|
|
3406
|
+
var EVIDENCE_MARK = "<!--qa:evidence-->";
|
|
3407
|
+
function noteContextMarkdown(note, index) {
|
|
3408
|
+
const body = noteToMarkdown(note, { index, keepEvidenceMark: true });
|
|
3409
|
+
const at = body.indexOf(EVIDENCE_MARK);
|
|
3410
|
+
const header = [
|
|
3411
|
+
`# Point ${index} \u2014 runtime context`,
|
|
3412
|
+
"",
|
|
3413
|
+
`Page: ${oneLine(note.route) || "/"}`,
|
|
3414
|
+
`Captured: ${oneLine(note.timestamp)}`,
|
|
3415
|
+
"",
|
|
3416
|
+
"Everything the browser recorded around this capture. Kept out of",
|
|
3417
|
+
"`notes.md` on purpose -- it is here when it is needed, and out of the way",
|
|
3418
|
+
"when it is not.",
|
|
3419
|
+
"",
|
|
3420
|
+
"---",
|
|
3421
|
+
""
|
|
3422
|
+
].join("\n");
|
|
3423
|
+
return at === -1 ? `${header}_(nothing was recorded)_
|
|
3424
|
+
` : header + body.slice(at + EVIDENCE_MARK.length).trimStart();
|
|
3425
|
+
}
|
|
3247
3426
|
function noteCheckLine(note, index) {
|
|
3248
3427
|
const where = oneLine(note.route) || "/";
|
|
3249
|
-
const
|
|
3250
|
-
const
|
|
3428
|
+
const wanted = oneLine(note.wanted);
|
|
3429
|
+
const seen = oneLine(note.description) || "(not described)";
|
|
3430
|
+
const claim = wanted || seen;
|
|
3431
|
+
const trimmed = claim.length > 180 ? `${claim.slice(0, 177)}...` : claim;
|
|
3251
3432
|
return `- [ ] **check-${index}** (\`${where}\`) \u2014 ${trimmed}`;
|
|
3252
3433
|
}
|
|
3253
3434
|
function noteToMarkdown(note, opts) {
|
|
@@ -3277,8 +3458,15 @@ function noteToMarkdown(note, opts) {
|
|
|
3277
3458
|
);
|
|
3278
3459
|
}
|
|
3279
3460
|
}
|
|
3461
|
+
if (note.origin?.component) lines.push(`- **Component:** \`${oneLine(note.origin.component)}\``);
|
|
3462
|
+
if (note.origin?.file) {
|
|
3463
|
+
lines.push(`- **Source:** \`${oneLine(note.origin.file)}${note.origin.line ? `:${note.origin.line}` : ""}\``);
|
|
3464
|
+
}
|
|
3280
3465
|
if (idx != null && note.screenshot) {
|
|
3281
3466
|
lines.push(`- **Screenshot:** screenshots/point-${idx}.${shotExtension(note.screenshot)}`);
|
|
3467
|
+
if (note.shotEngine === "dom") {
|
|
3468
|
+
lines.push("- **Screenshot caveat:** this is a re-drawing of the page, not a photograph. Canvas, WebGL and cross-origin images may be blank or missing in it. Trust the words over the picture where they disagree.");
|
|
3469
|
+
}
|
|
3282
3470
|
}
|
|
3283
3471
|
if (idx != null && note.afterScreenshot) {
|
|
3284
3472
|
lines.push(
|
|
@@ -3286,7 +3474,29 @@ function noteToMarkdown(note, opts) {
|
|
|
3286
3474
|
);
|
|
3287
3475
|
}
|
|
3288
3476
|
lines.push("");
|
|
3289
|
-
lines.push(
|
|
3477
|
+
lines.push("### Observed");
|
|
3478
|
+
lines.push("");
|
|
3479
|
+
lines.push(oneLine(note.description) ? note.description.trim() : "_(not described)_");
|
|
3480
|
+
if (note.wanted && oneLine(note.wanted)) {
|
|
3481
|
+
lines.push("");
|
|
3482
|
+
lines.push("### Expected");
|
|
3483
|
+
lines.push("");
|
|
3484
|
+
lines.push(note.wanted.trim());
|
|
3485
|
+
}
|
|
3486
|
+
if (note.why && oneLine(note.why)) {
|
|
3487
|
+
lines.push("");
|
|
3488
|
+
lines.push("### Why it matters");
|
|
3489
|
+
lines.push("");
|
|
3490
|
+
lines.push(note.why.trim());
|
|
3491
|
+
}
|
|
3492
|
+
if (note.fixHint && oneLine(note.fixHint)) {
|
|
3493
|
+
lines.push("");
|
|
3494
|
+
lines.push("### Suggested fix (a suggestion, not an instruction)");
|
|
3495
|
+
lines.push("");
|
|
3496
|
+
lines.push(note.fixHint.trim());
|
|
3497
|
+
lines.push("");
|
|
3498
|
+
lines.push("> Weigh this. It came from a person looking at the symptom, not at the code, and following a wrong suggestion costs more than ignoring it.");
|
|
3499
|
+
}
|
|
3290
3500
|
if (note.followUp && oneLine(note.followUp)) {
|
|
3291
3501
|
lines.push("");
|
|
3292
3502
|
lines.push(
|
|
@@ -3297,6 +3507,15 @@ function noteToMarkdown(note, opts) {
|
|
|
3297
3507
|
lines.push("");
|
|
3298
3508
|
lines.push(note.followUp.trim());
|
|
3299
3509
|
}
|
|
3510
|
+
if (idx != null) {
|
|
3511
|
+
lines.push("");
|
|
3512
|
+
lines.push(`**Check ${idx} \u2014 how this will be graded**`);
|
|
3513
|
+
lines.push("");
|
|
3514
|
+
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?*");
|
|
3515
|
+
lines.push("");
|
|
3516
|
+
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\`.`);
|
|
3517
|
+
}
|
|
3518
|
+
lines.push(EVIDENCE_MARK);
|
|
3300
3519
|
const recordedSteps = note.context?.steps ?? [];
|
|
3301
3520
|
if (recordedSteps.length) {
|
|
3302
3521
|
const t0 = Date.parse(note.timestamp) || recordedSteps[recordedSteps.length - 1].t;
|
|
@@ -3307,14 +3526,6 @@ function noteToMarkdown(note, opts) {
|
|
|
3307
3526
|
lines.push(`${i + 1}. ${formatStep(step, t0)}`);
|
|
3308
3527
|
});
|
|
3309
3528
|
}
|
|
3310
|
-
if (idx != null) {
|
|
3311
|
-
lines.push("");
|
|
3312
|
-
lines.push(`**Check ${idx} \u2014 how this will be graded**`);
|
|
3313
|
-
lines.push("");
|
|
3314
|
-
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?*");
|
|
3315
|
-
lines.push("");
|
|
3316
|
-
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\`.`);
|
|
3317
|
-
}
|
|
3318
3529
|
const ctx = note.context;
|
|
3319
3530
|
if (ctx) {
|
|
3320
3531
|
const env = ctx.env;
|
|
@@ -3361,7 +3572,67 @@ function noteToMarkdown(note, opts) {
|
|
|
3361
3572
|
lines.push("");
|
|
3362
3573
|
lines.push("</details>");
|
|
3363
3574
|
}
|
|
3364
|
-
|
|
3575
|
+
const whole = lines.join("\n");
|
|
3576
|
+
if (opts?.keepEvidenceMark) return whole;
|
|
3577
|
+
if (!opts?.contextFile) return whole.replace(`${EVIDENCE_MARK}
|
|
3578
|
+
`, "").replace(EVIDENCE_MARK, "");
|
|
3579
|
+
const at = whole.indexOf(EVIDENCE_MARK);
|
|
3580
|
+
const report = at === -1 ? whole : whole.slice(0, at).trimEnd();
|
|
3581
|
+
return `${report}
|
|
3582
|
+
|
|
3583
|
+
<sub>Steps, console, network, environment and element forensics: \`${opts.contextFile}\` \u2014 open it only if the words above leave something genuinely unresolved.</sub>`;
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
// src/lib/reproSpec.ts
|
|
3587
|
+
function lit(value) {
|
|
3588
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\r?\n/g, " ");
|
|
3589
|
+
}
|
|
3590
|
+
function line(value, limit = 200) {
|
|
3591
|
+
const one = (value ?? "").replace(/\s+/g, " ").trim();
|
|
3592
|
+
if (!one) return "";
|
|
3593
|
+
return one.length > limit ? `${one.slice(0, limit - 3)}...` : one;
|
|
3594
|
+
}
|
|
3595
|
+
function reproSpec(note, index) {
|
|
3596
|
+
const selector = note.target?.selector;
|
|
3597
|
+
const route = note.route || "/";
|
|
3598
|
+
if (!selector && !note.route) return null;
|
|
3599
|
+
const observed = line(note.description);
|
|
3600
|
+
const expected = line(note.wanted);
|
|
3601
|
+
const out = [];
|
|
3602
|
+
out.push(`// check-${index} -- DRAFT. Delete this file if the change is cosmetic.`);
|
|
3603
|
+
out.push("//");
|
|
3604
|
+
out.push("// The URL and the selector below were captured from the live page, so");
|
|
3605
|
+
out.push("// they are the tedious part already done. The assertion is yours: only");
|
|
3606
|
+
out.push("// you know whether this point is worth pinning down with a test.");
|
|
3607
|
+
out.push("//");
|
|
3608
|
+
if (observed) out.push(`// Observed: ${observed}`);
|
|
3609
|
+
if (expected) out.push(`// Expected: ${expected}`);
|
|
3610
|
+
else out.push("// Expected: (the tester did not say -- ask before asserting anything)");
|
|
3611
|
+
if (note.origin?.file) {
|
|
3612
|
+
out.push(`// Rendered by: ${note.origin.component ?? "?"} (${note.origin.file}${note.origin.line ? `:${note.origin.line}` : ""})`);
|
|
3613
|
+
}
|
|
3614
|
+
out.push("");
|
|
3615
|
+
out.push("import { test, expect } from '@playwright/test';");
|
|
3616
|
+
out.push("");
|
|
3617
|
+
out.push(`test('check-${index}: ${lit(line(note.description, 60) || "reported point")}', async ({ page }) => {`);
|
|
3618
|
+
out.push(` await page.goto('${lit(route)}');`);
|
|
3619
|
+
if (selector) {
|
|
3620
|
+
out.push("");
|
|
3621
|
+
out.push(` const target = page.locator('${lit(selector)}');`);
|
|
3622
|
+
out.push(" await expect(target).toBeVisible();");
|
|
3623
|
+
out.push("");
|
|
3624
|
+
out.push(" // TODO: assert the EXPECTED behaviour quoted above.");
|
|
3625
|
+
out.push(" // Being visible only proves the element is still there -- it does not");
|
|
3626
|
+
out.push(" // prove the thing the tester asked for actually happened.");
|
|
3627
|
+
} else {
|
|
3628
|
+
out.push("");
|
|
3629
|
+
out.push(" // No element was picked for this point -- it was a region or a plain");
|
|
3630
|
+
out.push(" // note. Drive the page to the state described above, then assert.");
|
|
3631
|
+
out.push(" // TODO");
|
|
3632
|
+
}
|
|
3633
|
+
out.push("});");
|
|
3634
|
+
out.push("");
|
|
3635
|
+
return out.join("\n");
|
|
3365
3636
|
}
|
|
3366
3637
|
|
|
3367
3638
|
// src/lib/exportZip.ts
|
|
@@ -3497,7 +3768,16 @@ This archive is not a list of suggestions. Every point in \`notes.md\` is an acc
|
|
|
3497
3768
|
|
|
3498
3769
|
**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.
|
|
3499
3770
|
|
|
3500
|
-
Hand \`verify.md\` back with the work
|
|
3771
|
+
Hand \`verify.md\` back with the work.
|
|
3772
|
+
|
|
3773
|
+
### What is in this archive
|
|
3774
|
+
|
|
3775
|
+
- \`notes.md\` \u2014 the points themselves, under an **Observed** heading. Most carry only that: the tester writes one sentence about what is wrong, which is the right amount to ask of somebody who is not an engineer. An **Expected** heading appears where somebody stated one.
|
|
3776
|
+
Where a point is ambiguous, **ask \u2014 do not pick a reading and commit to it.** That failure mode is specific to you: a person would stop and check, and an agent tends to fill the gap in and carry on. One question costs a message. A confident fix to the wrong problem costs the round.
|
|
3777
|
+
- \`verify.md\` \u2014 the checklist, one unticked box per point.
|
|
3778
|
+
- \`screenshots/\` \u2014 one per point. A point marked with a screenshot caveat was re-drawn rather than photographed, so canvases, charts and maps may be blank in it; trust the words over the picture there.
|
|
3779
|
+
- \`context/\` \u2014 console, network, environment and element forensics, one file per point. Deliberately **not** in \`notes.md\`: a longer report measurably lowers the chance of the right thing getting fixed, because the two sentences that matter get buried. Open these only when something is genuinely unresolved.
|
|
3780
|
+
- \`repro/\` \u2014 Playwright drafts, one per point, all optional. Finish the ones where the point is behavioural and worth pinning down; delete the ones where it is cosmetic. That judgement is yours.`
|
|
3501
3781
|
);
|
|
3502
3782
|
const stack = typeof p.stack === "string" && p.stack.trim() ? p.stack.trim() : "(not provided)";
|
|
3503
3783
|
const runArr = toStrings(p.runCommands);
|
|
@@ -3637,7 +3917,14 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
|
|
|
3637
3917
|
""
|
|
3638
3918
|
].join("\n");
|
|
3639
3919
|
const noteBlocks = notes.map(
|
|
3640
|
-
(n, i) => noteToMarkdown(n, {
|
|
3920
|
+
(n, i) => noteToMarkdown(n, {
|
|
3921
|
+
brand: brandLabel,
|
|
3922
|
+
index: i + 1,
|
|
3923
|
+
// Runtime evidence goes to its own file and is pointed at from here.
|
|
3924
|
+
// See the contextFile branch in noteMarkdown.ts for why: a long report
|
|
3925
|
+
// measurably lowers an agent's chance of fixing the thing.
|
|
3926
|
+
contextFile: n.context ? `context/point-${i + 1}.md` : void 0
|
|
3927
|
+
})
|
|
3641
3928
|
);
|
|
3642
3929
|
const notesBody = noteBlocks.length > 0 ? `${noteBlocks.join("\n\n---\n\n")}
|
|
3643
3930
|
|
|
@@ -3678,6 +3965,43 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
|
|
|
3678
3965
|
"Re-open the walkthrough on the tester's machine with `?qa=walk:verify`.",
|
|
3679
3966
|
""
|
|
3680
3967
|
].join("\n"));
|
|
3968
|
+
const contextDir = zip.folder("context");
|
|
3969
|
+
notes.forEach((n, i) => {
|
|
3970
|
+
if (n.context && contextDir) {
|
|
3971
|
+
contextDir.file(`point-${i + 1}.md`, noteContextMarkdown(n, i + 1));
|
|
3972
|
+
}
|
|
3973
|
+
});
|
|
3974
|
+
const reproDir = zip.folder("repro");
|
|
3975
|
+
let reproCount = 0;
|
|
3976
|
+
notes.forEach((n, i) => {
|
|
3977
|
+
const spec = reproSpec(n, i + 1);
|
|
3978
|
+
if (spec && reproDir) {
|
|
3979
|
+
reproDir.file(`check-${i + 1}.spec.ts`, spec);
|
|
3980
|
+
reproCount++;
|
|
3981
|
+
}
|
|
3982
|
+
});
|
|
3983
|
+
if (reproCount && reproDir) {
|
|
3984
|
+
reproDir.file("README.md", [
|
|
3985
|
+
"# Reproduction drafts",
|
|
3986
|
+
"",
|
|
3987
|
+
"One per point, and every one of them is optional.",
|
|
3988
|
+
"",
|
|
3989
|
+
"**Use one** where the point is behavioural and worth pinning down, so it",
|
|
3990
|
+
"cannot quietly come back later. An executable check is worth far more to",
|
|
3991
|
+
"you than another paragraph of steps written in English.",
|
|
3992
|
+
"",
|
|
3993
|
+
"**Delete it** where the point is cosmetic -- a colour, a spacing, a word.",
|
|
3994
|
+
"A test asserting that a heading is visible proves nothing anybody wanted",
|
|
3995
|
+
"proved, and it is one more file to maintain forever.",
|
|
3996
|
+
"",
|
|
3997
|
+
"That call is yours. The tester was not asked to make it and could not.",
|
|
3998
|
+
"",
|
|
3999
|
+
"What is already done for you in each file: the URL, a selector verified",
|
|
4000
|
+
"against the live DOM at capture time, and the observed and expected",
|
|
4001
|
+
"behaviour quoted in place. What is left is the assertion, marked TODO.",
|
|
4002
|
+
""
|
|
4003
|
+
].join("\n"));
|
|
4004
|
+
}
|
|
3681
4005
|
notes.forEach((n, i) => {
|
|
3682
4006
|
if (n.screenshot && shots) {
|
|
3683
4007
|
shots.file(`point-${i + 1}.${shotExtension(n.screenshot)}`, n.screenshot);
|
|
@@ -4147,7 +4471,7 @@ function reportMarkdownText(allNotes) {
|
|
|
4147
4471
|
"",
|
|
4148
4472
|
"---",
|
|
4149
4473
|
""
|
|
4150
|
-
].filter((
|
|
4474
|
+
].filter((line2) => line2 !== "").join("\n");
|
|
4151
4475
|
const body = ordered.map((n) => noteMarkdownForDisk(n, noteIndex[n.id])).join("\n---\n\n");
|
|
4152
4476
|
return `${header}
|
|
4153
4477
|
${body}`;
|
|
@@ -4274,18 +4598,6 @@ async function requestPersistentStorage() {
|
|
|
4274
4598
|
return false;
|
|
4275
4599
|
}
|
|
4276
4600
|
}
|
|
4277
|
-
function formatBytes(bytes) {
|
|
4278
|
-
if (!Number.isFinite(bytes) || bytes <= 0) return "0 KB";
|
|
4279
|
-
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
4280
|
-
let value = bytes;
|
|
4281
|
-
let i = 0;
|
|
4282
|
-
while (value >= 1024 && i < units.length - 1) {
|
|
4283
|
-
value /= 1024;
|
|
4284
|
-
i++;
|
|
4285
|
-
}
|
|
4286
|
-
const decimals = value < 10 && i > 1 ? 1 : 0;
|
|
4287
|
-
return `${value.toFixed(decimals)} ${units[i]}`;
|
|
4288
|
-
}
|
|
4289
4601
|
function estimateOwnBytes(notes) {
|
|
4290
4602
|
let total = 0;
|
|
4291
4603
|
for (const n of notes) {
|
|
@@ -4322,6 +4634,7 @@ function exactShotsWanted(store) {
|
|
|
4322
4634
|
}
|
|
4323
4635
|
var SIMPLE_MODE_KEY = "simpleMode";
|
|
4324
4636
|
var COMPACT_KEY = "compactCapture";
|
|
4637
|
+
var DEV_MODE_KEY = "developerMode";
|
|
4325
4638
|
var LAST_CAMPAIGN_KEY = "lastCampaign";
|
|
4326
4639
|
var AUTO_BACKUP_KEY = "autoBackup";
|
|
4327
4640
|
var AUTO_BACKUP_AT_KEY = "autoBackupAt";
|
|
@@ -4387,7 +4700,15 @@ function QaProvider({
|
|
|
4387
4700
|
const [lang, setLangState] = React.useState(() => {
|
|
4388
4701
|
const saved = storage.getItem(LANG_KEY);
|
|
4389
4702
|
if (saved === "ar" || saved === "en") return saved;
|
|
4390
|
-
|
|
4703
|
+
if (config.rtl) return "ar";
|
|
4704
|
+
try {
|
|
4705
|
+
const html = document.documentElement;
|
|
4706
|
+
if (html.getAttribute("dir") === "rtl") return "ar";
|
|
4707
|
+
if ((html.getAttribute("lang") || "").toLowerCase().startsWith("ar")) return "ar";
|
|
4708
|
+
if ((navigator.language || "").toLowerCase().startsWith("ar")) return "ar";
|
|
4709
|
+
} catch {
|
|
4710
|
+
}
|
|
4711
|
+
return "en";
|
|
4391
4712
|
});
|
|
4392
4713
|
const [guideChecked, setGuideChecked] = React.useState(
|
|
4393
4714
|
() => new Set(storage.getJSON(GUIDE_KEY, []))
|
|
@@ -4425,6 +4746,9 @@ function QaProvider({
|
|
|
4425
4746
|
const [simpleMode, setSimpleModeState] = React.useState(
|
|
4426
4747
|
() => storage.getItem(SIMPLE_MODE_KEY) === "1"
|
|
4427
4748
|
);
|
|
4749
|
+
const [developerMode, setDeveloperModeState] = React.useState(
|
|
4750
|
+
() => storage.getItem(DEV_MODE_KEY) === "1"
|
|
4751
|
+
);
|
|
4428
4752
|
const [compactCapture, setCompactCaptureState] = React.useState(
|
|
4429
4753
|
() => storage.getItem(COMPACT_KEY) === "1"
|
|
4430
4754
|
);
|
|
@@ -4754,6 +5078,11 @@ function QaProvider({
|
|
|
4754
5078
|
target: input.target ?? void 0,
|
|
4755
5079
|
severity: input.severity,
|
|
4756
5080
|
status: input.status,
|
|
5081
|
+
wanted: (input.wanted || "").trim() || void 0,
|
|
5082
|
+
why: (input.why || "").trim() || void 0,
|
|
5083
|
+
fixHint: (input.fixHint || "").trim() || void 0,
|
|
5084
|
+
shotEngine: input.shotEngine,
|
|
5085
|
+
origin: input.origin,
|
|
4757
5086
|
journeyRef,
|
|
4758
5087
|
context
|
|
4759
5088
|
};
|
|
@@ -4770,8 +5099,11 @@ function QaProvider({
|
|
|
4770
5099
|
});
|
|
4771
5100
|
}
|
|
4772
5101
|
await syncNoteThrough(note);
|
|
5102
|
+
if (config.collector) {
|
|
5103
|
+
void sendToCollector(note, config.collector);
|
|
5104
|
+
}
|
|
4773
5105
|
},
|
|
4774
|
-
[idb, config.journey, config.captureContext, testAlong, testAlongSteps, notify, t, syncNoteThrough, applyNotes]
|
|
5106
|
+
[idb, config.journey, config.captureContext, config.collector, testAlong, testAlongSteps, notify, t, syncNoteThrough, applyNotes]
|
|
4775
5107
|
);
|
|
4776
5108
|
const updateNote = React.useCallback(
|
|
4777
5109
|
async (id, patch) => {
|
|
@@ -5156,6 +5488,10 @@ function QaProvider({
|
|
|
5156
5488
|
setCompactCaptureState(on);
|
|
5157
5489
|
storage.setItem(COMPACT_KEY, on ? "1" : "0");
|
|
5158
5490
|
}, [storage]);
|
|
5491
|
+
const setDeveloperMode = React.useCallback((on) => {
|
|
5492
|
+
setDeveloperModeState(on);
|
|
5493
|
+
storage.setItem(DEV_MODE_KEY, on ? "1" : "0");
|
|
5494
|
+
}, [storage]);
|
|
5159
5495
|
const setFilter = React.useCallback((patch) => {
|
|
5160
5496
|
setFilterState((prev) => ({ ...prev, ...patch }));
|
|
5161
5497
|
}, []);
|
|
@@ -5495,6 +5831,7 @@ function QaProvider({
|
|
|
5495
5831
|
// Config passthrough
|
|
5496
5832
|
namespace: config.namespace,
|
|
5497
5833
|
brand: config.brand,
|
|
5834
|
+
beta: config.beta === true,
|
|
5498
5835
|
loginField: config.loginField,
|
|
5499
5836
|
credentials: config.credentials,
|
|
5500
5837
|
// Never the raw config value: an unconfigured project falls back to the
|
|
@@ -5603,6 +5940,8 @@ function QaProvider({
|
|
|
5603
5940
|
simpleMode,
|
|
5604
5941
|
setSimpleMode,
|
|
5605
5942
|
compactCapture,
|
|
5943
|
+
developerMode,
|
|
5944
|
+
setDeveloperMode,
|
|
5606
5945
|
setCompactCapture,
|
|
5607
5946
|
exportZip: exportZipFn
|
|
5608
5947
|
};
|
|
@@ -5670,6 +6009,16 @@ var ICONS = {
|
|
|
5670
6009
|
Square: [
|
|
5671
6010
|
["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }]
|
|
5672
6011
|
],
|
|
6012
|
+
Info: [
|
|
6013
|
+
["circle", { cx: "12", cy: "12", r: "10" }],
|
|
6014
|
+
["path", { d: "M12 16v-4" }],
|
|
6015
|
+
["path", { d: "M12 8h.01" }]
|
|
6016
|
+
],
|
|
6017
|
+
Mic: [
|
|
6018
|
+
["path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }],
|
|
6019
|
+
["path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }],
|
|
6020
|
+
["line", { x1: "12", x2: "12", y1: "19", y2: "22" }]
|
|
6021
|
+
],
|
|
5673
6022
|
ImagePlus: [
|
|
5674
6023
|
["path", { d: "M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7" }],
|
|
5675
6024
|
["line", { x1: "16", x2: "22", y1: "5", y2: "5" }],
|
|
@@ -5903,7 +6252,7 @@ function clampFabPos(p, w = FAB_SIZE_PX, h = FAB_SIZE_PX) {
|
|
|
5903
6252
|
};
|
|
5904
6253
|
}
|
|
5905
6254
|
function QaFab() {
|
|
5906
|
-
const { isOpen, setIsOpen, notes, captureActive, namespace, t } = useQa();
|
|
6255
|
+
const { isOpen, setIsOpen, notes, captureActive, namespace, beta, t } = useQa();
|
|
5907
6256
|
const [pos, setPos] = React.useState(() => loadFabPos(namespace));
|
|
5908
6257
|
const dragRef = React.useRef(null);
|
|
5909
6258
|
const didDragRef = React.useRef(false);
|
|
@@ -6016,6 +6365,15 @@ function QaFab() {
|
|
|
6016
6365
|
}
|
|
6017
6366
|
),
|
|
6018
6367
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: isOpen ? "X" : "ClipboardList", size: 24 }),
|
|
6368
|
+
beta && !isOpen && /* @__PURE__ */ jsxRuntime.jsx(
|
|
6369
|
+
"span",
|
|
6370
|
+
{
|
|
6371
|
+
className: "qa-absolute qa-rounded-full qa-text-10 qa-font-bold qa-bg-1 qa-text-mid qa-border qa-border-subtle",
|
|
6372
|
+
style: { bottom: "-6px", left: "50%", transform: "translateX(-50%)", padding: "0 5px", lineHeight: "1.3" },
|
|
6373
|
+
"aria-hidden": "true",
|
|
6374
|
+
children: "beta"
|
|
6375
|
+
}
|
|
6376
|
+
),
|
|
6019
6377
|
!isOpen && notes.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
6020
6378
|
"span",
|
|
6021
6379
|
{
|
|
@@ -7709,6 +8067,154 @@ function NoteFilterBar() {
|
|
|
7709
8067
|
) })
|
|
7710
8068
|
] });
|
|
7711
8069
|
}
|
|
8070
|
+
|
|
8071
|
+
// src/lib/doctor.ts
|
|
8072
|
+
var HEAVY_PAGE = 6e3;
|
|
8073
|
+
function canvasIsReadable() {
|
|
8074
|
+
try {
|
|
8075
|
+
const c = document.createElement("canvas");
|
|
8076
|
+
c.width = 1;
|
|
8077
|
+
c.height = 1;
|
|
8078
|
+
const ctx = c.getContext("2d");
|
|
8079
|
+
if (!ctx) return false;
|
|
8080
|
+
ctx.fillStyle = "#000";
|
|
8081
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
8082
|
+
c.toDataURL();
|
|
8083
|
+
return true;
|
|
8084
|
+
} catch {
|
|
8085
|
+
return false;
|
|
8086
|
+
}
|
|
8087
|
+
}
|
|
8088
|
+
function taintingImages() {
|
|
8089
|
+
let count = 0;
|
|
8090
|
+
const here = location.origin;
|
|
8091
|
+
for (const img of Array.from(document.images)) {
|
|
8092
|
+
const src = img.currentSrc || img.src;
|
|
8093
|
+
if (!src || src.startsWith("data:") || src.startsWith("blob:")) continue;
|
|
8094
|
+
try {
|
|
8095
|
+
if (new URL(src, here).origin !== here && !img.crossOrigin) count++;
|
|
8096
|
+
} catch {
|
|
8097
|
+
}
|
|
8098
|
+
}
|
|
8099
|
+
return count;
|
|
8100
|
+
}
|
|
8101
|
+
async function runDoctor() {
|
|
8102
|
+
const out = [];
|
|
8103
|
+
if (typeof document === "undefined") return out;
|
|
8104
|
+
const supported = isExactCaptureSupported();
|
|
8105
|
+
const status = getExactCaptureStatus();
|
|
8106
|
+
out.push(
|
|
8107
|
+
!supported ? {
|
|
8108
|
+
label: "Screenshot engine",
|
|
8109
|
+
verdict: "warn",
|
|
8110
|
+
detail: "This browser cannot photograph the tab, so screenshots are re-drawn. Charts, maps and anything on a canvas may come out blank. Chrome or Edge can photograph."
|
|
8111
|
+
} : status === "live" ? { label: "Screenshot engine", verdict: "ok", detail: "Real photographs. This is the accurate one." } : {
|
|
8112
|
+
label: "Screenshot engine",
|
|
8113
|
+
verdict: "warn",
|
|
8114
|
+
detail: "Photographs are switched off, so screenshots are re-drawn and may not match the page."
|
|
8115
|
+
}
|
|
8116
|
+
);
|
|
8117
|
+
const readable = canvasIsReadable();
|
|
8118
|
+
const risky = taintingImages();
|
|
8119
|
+
out.push(
|
|
8120
|
+
!readable ? {
|
|
8121
|
+
label: "Screenshot encoding",
|
|
8122
|
+
verdict: "bad",
|
|
8123
|
+
detail: "This page cannot turn a drawing into an image at all. Re-drawn screenshots will fail here; use photographs."
|
|
8124
|
+
} : risky > 0 ? {
|
|
8125
|
+
label: "Screenshot encoding",
|
|
8126
|
+
verdict: "info",
|
|
8127
|
+
detail: `${risky} image(s) come from another site without permission to be read. They are skipped and appear blank in a re-drawn screenshot; photographs are unaffected.`
|
|
8128
|
+
} : { label: "Screenshot encoding", verdict: "ok", detail: "Nothing on this page blocks screenshot encoding." }
|
|
8129
|
+
);
|
|
8130
|
+
const nodes = document.getElementsByTagName("*").length;
|
|
8131
|
+
out.push({
|
|
8132
|
+
label: "Page size",
|
|
8133
|
+
verdict: nodes > HEAVY_PAGE ? "warn" : "ok",
|
|
8134
|
+
detail: nodes > HEAVY_PAGE ? `${nodes.toLocaleString()} elements. Too many to re-draw without freezing the tab, so re-drawn screenshots are refused here. Photographs work fine.` : `${nodes.toLocaleString()} elements. Comfortable for either engine.`
|
|
8135
|
+
});
|
|
8136
|
+
try {
|
|
8137
|
+
await import('html2canvas');
|
|
8138
|
+
out.push({ label: "Screenshot library", verdict: "ok", detail: "Loads correctly." });
|
|
8139
|
+
} catch {
|
|
8140
|
+
out.push({
|
|
8141
|
+
label: "Screenshot library",
|
|
8142
|
+
verdict: "bad",
|
|
8143
|
+
detail: "Blocked or unreachable \u2014 often a strict Content-Security-Policy. Re-drawn screenshots cannot work here."
|
|
8144
|
+
});
|
|
8145
|
+
}
|
|
8146
|
+
try {
|
|
8147
|
+
const est = await navigator.storage?.estimate?.();
|
|
8148
|
+
if (est?.quota) {
|
|
8149
|
+
const usedPct = Math.round((est.usage ?? 0) / est.quota * 100);
|
|
8150
|
+
out.push({
|
|
8151
|
+
label: "Storage",
|
|
8152
|
+
verdict: usedPct > 90 ? "bad" : usedPct > 70 ? "warn" : "ok",
|
|
8153
|
+
detail: `${usedPct}% of this site's storage used. Notes are kept in this browser until you export them.`
|
|
8154
|
+
});
|
|
8155
|
+
}
|
|
8156
|
+
} catch {
|
|
8157
|
+
}
|
|
8158
|
+
const voice = typeof window !== "undefined" && !!(window.SpeechRecognition || window.webkitSpeechRecognition);
|
|
8159
|
+
out.push({
|
|
8160
|
+
label: "Voice input",
|
|
8161
|
+
verdict: voice ? "ok" : "info",
|
|
8162
|
+
detail: voice ? "Available \u2014 you can speak your notes." : "Not available in this browser. Typing still works."
|
|
8163
|
+
});
|
|
8164
|
+
out.push({
|
|
8165
|
+
label: "Secure connection",
|
|
8166
|
+
verdict: window.isSecureContext ? "ok" : "bad",
|
|
8167
|
+
detail: window.isSecureContext ? "https \u2014 photographs and voice are allowed." : "Not https. Photographs and voice are blocked by the browser on an insecure page."
|
|
8168
|
+
});
|
|
8169
|
+
return out;
|
|
8170
|
+
}
|
|
8171
|
+
|
|
8172
|
+
// src/lib/versionCheck.ts
|
|
8173
|
+
var CACHE_KEY = "qa.versionCheck";
|
|
8174
|
+
var A_DAY = 24 * 60 * 60 * 1e3;
|
|
8175
|
+
var REGISTRY = "https://registry.npmjs.org/qapture2/latest";
|
|
8176
|
+
function isNewer(latest, current) {
|
|
8177
|
+
const norm = (v) => v.replace(/^[^\d]*/, "").split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
|
|
8178
|
+
const a = norm(latest);
|
|
8179
|
+
const b = norm(current);
|
|
8180
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
8181
|
+
const x = a[i] ?? 0;
|
|
8182
|
+
const y = b[i] ?? 0;
|
|
8183
|
+
if (x !== y) return x > y;
|
|
8184
|
+
}
|
|
8185
|
+
return false;
|
|
8186
|
+
}
|
|
8187
|
+
async function latestVersion() {
|
|
8188
|
+
if (typeof window === "undefined") return null;
|
|
8189
|
+
try {
|
|
8190
|
+
const raw = window.localStorage.getItem(CACHE_KEY);
|
|
8191
|
+
if (raw) {
|
|
8192
|
+
const cached = JSON.parse(raw);
|
|
8193
|
+
if (Date.now() - cached.at < A_DAY && cached.latest) return cached.latest;
|
|
8194
|
+
}
|
|
8195
|
+
} catch {
|
|
8196
|
+
}
|
|
8197
|
+
try {
|
|
8198
|
+
const res = await fetch(REGISTRY, { headers: { accept: "application/json" } });
|
|
8199
|
+
if (!res.ok) return null;
|
|
8200
|
+
const body = await res.json();
|
|
8201
|
+
const latest = body.version;
|
|
8202
|
+
if (!latest) return null;
|
|
8203
|
+
try {
|
|
8204
|
+
window.localStorage.setItem(CACHE_KEY, JSON.stringify({ at: Date.now(), latest }));
|
|
8205
|
+
} catch {
|
|
8206
|
+
}
|
|
8207
|
+
return latest;
|
|
8208
|
+
} catch {
|
|
8209
|
+
return null;
|
|
8210
|
+
}
|
|
8211
|
+
}
|
|
8212
|
+
function upgradeHint(latest) {
|
|
8213
|
+
return `npm i qapture2@${latest}`;
|
|
8214
|
+
}
|
|
8215
|
+
|
|
8216
|
+
// src/version.ts
|
|
8217
|
+
var QA_VERSION = "0.9.1" ;
|
|
7712
8218
|
function Section({
|
|
7713
8219
|
icon,
|
|
7714
8220
|
title,
|
|
@@ -7771,8 +8277,24 @@ function SettingsSheet({ onClose }) {
|
|
|
7771
8277
|
setSimpleMode,
|
|
7772
8278
|
compactCapture,
|
|
7773
8279
|
setCompactCapture,
|
|
8280
|
+
developerMode,
|
|
8281
|
+
setDeveloperMode,
|
|
7774
8282
|
notes
|
|
7775
8283
|
} = useQa();
|
|
8284
|
+
const [checks, setChecks] = React.useState([]);
|
|
8285
|
+
const [running, setRunning] = React.useState(false);
|
|
8286
|
+
const [faults2, setFaults] = React.useState(() => readFaults());
|
|
8287
|
+
const [latest, setLatest] = React.useState(null);
|
|
8288
|
+
React.useEffect(() => {
|
|
8289
|
+
let alive = true;
|
|
8290
|
+
void latestVersion().then((v) => {
|
|
8291
|
+
if (alive) setLatest(v);
|
|
8292
|
+
});
|
|
8293
|
+
setFaults(readFaults());
|
|
8294
|
+
return () => {
|
|
8295
|
+
alive = false;
|
|
8296
|
+
};
|
|
8297
|
+
}, []);
|
|
7776
8298
|
const [project, setProject] = React.useState(lastCampaign.project);
|
|
7777
8299
|
const [campaign2, setCampaign] = React.useState(lastCampaign.campaign || suggestCampaignName());
|
|
7778
8300
|
const [tester, setTester] = React.useState(lastCampaign.tester);
|
|
@@ -7798,8 +8320,8 @@ function SettingsSheet({ onClose }) {
|
|
|
7798
8320
|
const syncing = sync.state === "syncing";
|
|
7799
8321
|
const viaZip = sync.engine === "download";
|
|
7800
8322
|
const quotaKnown = storageHealth.supported && storageHealth.quotaBytes > 0;
|
|
7801
|
-
|
|
7802
|
-
|
|
8323
|
+
quotaKnown ? Math.min(100, Math.round(storageHealth.ratio * 100)) : 0;
|
|
8324
|
+
storageHealth.level === "critical" ? "var(--qa-danger)" : storageHealth.level === "warn" ? "var(--qa-warn)" : "var(--qa-accent)";
|
|
7803
8325
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
7804
8326
|
"div",
|
|
7805
8327
|
{
|
|
@@ -7928,77 +8450,6 @@ function SettingsSheet({ onClose }) {
|
|
|
7928
8450
|
)
|
|
7929
8451
|
] }),
|
|
7930
8452
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
|
|
7931
|
-
/* @__PURE__ */ jsxRuntime.jsxs(Section, { icon: "HardDrive", title: t("storage_title"), children: [
|
|
7932
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo qa-leading-relaxed", children: t("storage_explain") }),
|
|
7933
|
-
quotaKnown && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
7934
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7935
|
-
"div",
|
|
7936
|
-
{
|
|
7937
|
-
className: "qa-w-full qa-rounded-full qa-overflow-hidden qa-bg-3",
|
|
7938
|
-
role: "img",
|
|
7939
|
-
"aria-label": t("storage_used", {
|
|
7940
|
-
used: formatBytes(storageHealth.usageBytes),
|
|
7941
|
-
quota: formatBytes(storageHealth.quotaBytes)
|
|
7942
|
-
}),
|
|
7943
|
-
style: { height: 6 },
|
|
7944
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width: `${usedPct}%`, height: "100%", background: meterColor } })
|
|
7945
|
-
}
|
|
7946
|
-
),
|
|
7947
|
-
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-m-0 qa-text-10 qa-text-mid", children: [
|
|
7948
|
-
t("storage_used", {
|
|
7949
|
-
used: formatBytes(storageHealth.usageBytes),
|
|
7950
|
-
quota: formatBytes(storageHealth.quotaBytes)
|
|
7951
|
-
}),
|
|
7952
|
-
" \xB7 ",
|
|
7953
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-text-lo", children: [
|
|
7954
|
-
"Qapture ",
|
|
7955
|
-
formatBytes(storageHealth.ownBytes)
|
|
7956
|
-
] })
|
|
7957
|
-
] })
|
|
7958
|
-
] }),
|
|
7959
|
-
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "qa-flex qa-items-start qa-gap-2 qa-text-xs qa-text-hi", style: { cursor: "pointer" }, children: [
|
|
7960
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7961
|
-
"input",
|
|
7962
|
-
{
|
|
7963
|
-
type: "checkbox",
|
|
7964
|
-
checked: autoBackup,
|
|
7965
|
-
onChange: (e) => setAutoBackup(e.target.checked),
|
|
7966
|
-
style: { marginTop: 2 }
|
|
7967
|
-
}
|
|
7968
|
-
),
|
|
7969
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
7970
|
-
t("autosave_label"),
|
|
7971
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-block qa-text-10 qa-text-lo qa-leading-relaxed", children: t("autosave_hint", { n: autoBackupEvery }) })
|
|
7972
|
-
] })
|
|
7973
|
-
] }),
|
|
7974
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-flex-wrap qa-gap-2", children: [
|
|
7975
|
-
storageHealth.persisted ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-inline-flex qa-items-center qa-gap-1 qa-rounded-full qa-bg-success-tint qa-text-success qa-px-2 qa-py-0.5 qa-text-10", children: [
|
|
7976
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "CheckCircle2", size: 11 }),
|
|
7977
|
-
t("persist_on")
|
|
7978
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
7979
|
-
"button",
|
|
7980
|
-
{
|
|
7981
|
-
type: "button",
|
|
7982
|
-
disabled: busy,
|
|
7983
|
-
onClick: () => void run2(requestPersistentStorage2),
|
|
7984
|
-
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-10 qa-text-mid",
|
|
7985
|
-
style: { background: "transparent", cursor: "pointer" },
|
|
7986
|
-
children: t("persist_keep")
|
|
7987
|
-
}
|
|
7988
|
-
),
|
|
7989
|
-
notes.some((n) => n.screenshot) && /* @__PURE__ */ jsxRuntime.jsx(
|
|
7990
|
-
"button",
|
|
7991
|
-
{
|
|
7992
|
-
type: "button",
|
|
7993
|
-
disabled: busy,
|
|
7994
|
-
onClick: () => void run2(dropAllScreenshots),
|
|
7995
|
-
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-10 qa-text-mid",
|
|
7996
|
-
style: { background: "transparent", cursor: "pointer" },
|
|
7997
|
-
children: t("drop_shots")
|
|
7998
|
-
}
|
|
7999
|
-
)
|
|
8000
|
-
] })
|
|
8001
|
-
] }),
|
|
8002
8453
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
|
|
8003
8454
|
/* @__PURE__ */ jsxRuntime.jsxs(Section, { icon: "Camera", title: t("exact_label"), children: [
|
|
8004
8455
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo qa-leading-relaxed", children: t("exact_hint") }),
|
|
@@ -8047,6 +8498,20 @@ function SettingsSheet({ onClose }) {
|
|
|
8047
8498
|
] }),
|
|
8048
8499
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
|
|
8049
8500
|
/* @__PURE__ */ jsxRuntime.jsxs(Section, { icon: "Settings", title: t("settings"), children: [
|
|
8501
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "qa-flex qa-items-start qa-gap-2 qa-text-xs qa-text-hi", style: { cursor: "pointer" }, children: [
|
|
8502
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8503
|
+
"input",
|
|
8504
|
+
{
|
|
8505
|
+
type: "checkbox",
|
|
8506
|
+
checked: developerMode,
|
|
8507
|
+
onChange: (e) => setDeveloperMode(e.target.checked)
|
|
8508
|
+
}
|
|
8509
|
+
),
|
|
8510
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
8511
|
+
t("dev_mode_label"),
|
|
8512
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-block qa-text-10 qa-text-mid", children: t("dev_mode_hint") })
|
|
8513
|
+
] })
|
|
8514
|
+
] }),
|
|
8050
8515
|
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "qa-flex qa-items-center qa-gap-2 qa-text-xs qa-text-hi", style: { cursor: "pointer" }, children: [
|
|
8051
8516
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8052
8517
|
"input",
|
|
@@ -8069,6 +8534,91 @@ function SettingsSheet({ onClose }) {
|
|
|
8069
8534
|
),
|
|
8070
8535
|
t("compact_mode")
|
|
8071
8536
|
] })
|
|
8537
|
+
] }),
|
|
8538
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Section, { icon: "Info", title: t("diag_title"), children: [
|
|
8539
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-text-10 qa-text-mid", children: t("diag_hint") }),
|
|
8540
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-2 qa-flex-wrap", children: [
|
|
8541
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
8542
|
+
"button",
|
|
8543
|
+
{
|
|
8544
|
+
type: "button",
|
|
8545
|
+
onClick: () => void (async () => {
|
|
8546
|
+
setRunning(true);
|
|
8547
|
+
try {
|
|
8548
|
+
setChecks(await runDoctor());
|
|
8549
|
+
} finally {
|
|
8550
|
+
setRunning(false);
|
|
8551
|
+
}
|
|
8552
|
+
})(),
|
|
8553
|
+
disabled: running,
|
|
8554
|
+
"data-qa-doctor": "true",
|
|
8555
|
+
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",
|
|
8556
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
8557
|
+
children: [
|
|
8558
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 13 }),
|
|
8559
|
+
running ? t("diag_running") : t("diag_run")
|
|
8560
|
+
]
|
|
8561
|
+
}
|
|
8562
|
+
),
|
|
8563
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-text-10 qa-text-mid", children: [
|
|
8564
|
+
t("diag_version", { v: QA_VERSION }),
|
|
8565
|
+
latest && isNewer(latest, QA_VERSION) ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
8566
|
+
" \xB7 ",
|
|
8567
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-warn", children: t("diag_outdated", { v: latest }) }),
|
|
8568
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { className: "qa-ms-1", children: upgradeHint(latest) })
|
|
8569
|
+
] }) : latest ? ` \xB7 ${t("diag_current")}` : ""
|
|
8570
|
+
] })
|
|
8571
|
+
] }),
|
|
8572
|
+
checks.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "qa-space-y-1", "data-qa-doctor-results": "true", children: checks.map((c) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "qa-text-11 qa-text-hi", children: [
|
|
8573
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8574
|
+
"span",
|
|
8575
|
+
{
|
|
8576
|
+
"aria-hidden": true,
|
|
8577
|
+
className: c.verdict === "ok" ? "qa-text-success" : c.verdict === "bad" ? "qa-text-danger" : c.verdict === "warn" ? "qa-text-warn" : "qa-text-mid",
|
|
8578
|
+
children: c.verdict === "ok" ? "\u25CF" : c.verdict === "bad" ? "\u25B2" : c.verdict === "warn" ? "\u25B2" : "\u25CB"
|
|
8579
|
+
}
|
|
8580
|
+
),
|
|
8581
|
+
" ",
|
|
8582
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: c.label }),
|
|
8583
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-block qa-text-10 qa-text-mid qa-ms-3", children: c.detail })
|
|
8584
|
+
] }, c.label)) }),
|
|
8585
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-2 qa-flex-wrap", children: [
|
|
8586
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-11 qa-text-mid", children: t("diag_faults", { n: faults2.length }) }),
|
|
8587
|
+
faults2.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
8588
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8589
|
+
"button",
|
|
8590
|
+
{
|
|
8591
|
+
type: "button",
|
|
8592
|
+
onClick: () => void navigator.clipboard?.writeText(faultsAsText(QA_VERSION)),
|
|
8593
|
+
className: "qa-tap qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-0.5 qa-text-10 qa-text-hi",
|
|
8594
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
8595
|
+
children: t("copy_prompt")
|
|
8596
|
+
}
|
|
8597
|
+
),
|
|
8598
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8599
|
+
"button",
|
|
8600
|
+
{
|
|
8601
|
+
type: "button",
|
|
8602
|
+
onClick: () => {
|
|
8603
|
+
clearFaults();
|
|
8604
|
+
setFaults([]);
|
|
8605
|
+
},
|
|
8606
|
+
className: "qa-tap qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-0.5 qa-text-10 qa-text-mid",
|
|
8607
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
8608
|
+
children: t("clear_all")
|
|
8609
|
+
}
|
|
8610
|
+
)
|
|
8611
|
+
] })
|
|
8612
|
+
] }),
|
|
8613
|
+
faults2.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "qa-space-y-0.5", children: faults2.slice(0, 5).map((f, i) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "qa-text-10 qa-text-mid", children: [
|
|
8614
|
+
/* @__PURE__ */ jsxRuntime.jsxs("code", { children: [
|
|
8615
|
+
"[",
|
|
8616
|
+
f.where,
|
|
8617
|
+
"]"
|
|
8618
|
+
] }),
|
|
8619
|
+
" ",
|
|
8620
|
+
f.what
|
|
8621
|
+
] }, `${f.at}-${i}`)) })
|
|
8072
8622
|
] })
|
|
8073
8623
|
] })
|
|
8074
8624
|
]
|
|
@@ -8403,17 +8953,17 @@ function QaPanel() {
|
|
|
8403
8953
|
if (typeof window === "undefined") return 0;
|
|
8404
8954
|
const vv = window.visualViewport;
|
|
8405
8955
|
if (!vv) return 0;
|
|
8406
|
-
const
|
|
8407
|
-
if (
|
|
8956
|
+
const overlap2 = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
|
|
8957
|
+
if (overlap2 <= KEYBOARD_OVERLAP_THRESHOLD) return 0;
|
|
8408
8958
|
const panel = panelRef.current;
|
|
8409
8959
|
if (!panel) return 0;
|
|
8410
8960
|
const root2 = panel.getRootNode();
|
|
8411
8961
|
const active = root2.activeElement;
|
|
8412
8962
|
if (!active || !panel.contains(active)) return 0;
|
|
8413
8963
|
const tag = active.tagName;
|
|
8414
|
-
if (tag === "TEXTAREA") return Math.round(
|
|
8964
|
+
if (tag === "TEXTAREA") return Math.round(overlap2) + KEYBOARD_LIFT_GAP;
|
|
8415
8965
|
if (tag === "INPUT" && !NON_TEXT_INPUT_TYPES.has(active.type)) {
|
|
8416
|
-
return Math.round(
|
|
8966
|
+
return Math.round(overlap2) + KEYBOARD_LIFT_GAP;
|
|
8417
8967
|
}
|
|
8418
8968
|
return 0;
|
|
8419
8969
|
} catch {
|
|
@@ -9353,6 +9903,117 @@ function unlockPageScroll() {
|
|
|
9353
9903
|
window.removeEventListener("touchmove", preventScroll, LISTENER_OPTIONS);
|
|
9354
9904
|
}
|
|
9355
9905
|
}
|
|
9906
|
+
|
|
9907
|
+
// src/lib/origin.ts
|
|
9908
|
+
function fiberOf(el) {
|
|
9909
|
+
for (const key in el) {
|
|
9910
|
+
if (key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")) {
|
|
9911
|
+
return el[key] ?? null;
|
|
9912
|
+
}
|
|
9913
|
+
}
|
|
9914
|
+
return null;
|
|
9915
|
+
}
|
|
9916
|
+
function nameOf(fiber) {
|
|
9917
|
+
const type = fiber.type;
|
|
9918
|
+
if (!type || typeof type === "string") return null;
|
|
9919
|
+
const direct = type.displayName || type.name;
|
|
9920
|
+
if (direct) return direct;
|
|
9921
|
+
const inner = type.render?.displayName || type.render?.name;
|
|
9922
|
+
return inner || null;
|
|
9923
|
+
}
|
|
9924
|
+
function tidyPath(file) {
|
|
9925
|
+
const marks = ["/src/", "/app/", "/components/", "/pages/", "/lib/"];
|
|
9926
|
+
for (const mark of marks) {
|
|
9927
|
+
const at = file.lastIndexOf(mark);
|
|
9928
|
+
if (at !== -1) return file.slice(at + 1);
|
|
9929
|
+
}
|
|
9930
|
+
return file.split("/").slice(-2).join("/");
|
|
9931
|
+
}
|
|
9932
|
+
var MAX_WALK = 30;
|
|
9933
|
+
function resolveOrigin(el) {
|
|
9934
|
+
if (!el || typeof window === "undefined") return void 0;
|
|
9935
|
+
let fiber;
|
|
9936
|
+
try {
|
|
9937
|
+
fiber = fiberOf(el);
|
|
9938
|
+
} catch {
|
|
9939
|
+
return void 0;
|
|
9940
|
+
}
|
|
9941
|
+
if (!fiber) return void 0;
|
|
9942
|
+
const out = {};
|
|
9943
|
+
let hops = 0;
|
|
9944
|
+
for (let f = fiber; f && hops < MAX_WALK; f = f.return ?? null, hops++) {
|
|
9945
|
+
if (!out.component) {
|
|
9946
|
+
const name = nameOf(f);
|
|
9947
|
+
if (name) out.component = name;
|
|
9948
|
+
}
|
|
9949
|
+
if (!out.file) {
|
|
9950
|
+
const src = f._debugSource;
|
|
9951
|
+
if (src?.fileName) {
|
|
9952
|
+
out.file = tidyPath(src.fileName);
|
|
9953
|
+
if (typeof src.lineNumber === "number") out.line = src.lineNumber;
|
|
9954
|
+
}
|
|
9955
|
+
}
|
|
9956
|
+
if (out.component && out.file) break;
|
|
9957
|
+
}
|
|
9958
|
+
return out.component || out.file ? out : void 0;
|
|
9959
|
+
}
|
|
9960
|
+
|
|
9961
|
+
// src/lib/duplicate.ts
|
|
9962
|
+
var NOISE = /* @__PURE__ */ new Set([
|
|
9963
|
+
"the",
|
|
9964
|
+
"this",
|
|
9965
|
+
"that",
|
|
9966
|
+
"a",
|
|
9967
|
+
"an",
|
|
9968
|
+
"is",
|
|
9969
|
+
"it",
|
|
9970
|
+
"to",
|
|
9971
|
+
"and",
|
|
9972
|
+
"of",
|
|
9973
|
+
"in",
|
|
9974
|
+
"on",
|
|
9975
|
+
"for",
|
|
9976
|
+
"be",
|
|
9977
|
+
"should",
|
|
9978
|
+
"i",
|
|
9979
|
+
"we",
|
|
9980
|
+
"please",
|
|
9981
|
+
"here",
|
|
9982
|
+
"\u0647\u0630\u0627",
|
|
9983
|
+
"\u0647\u0630\u0647",
|
|
9984
|
+
"\u0641\u064A",
|
|
9985
|
+
"\u0645\u0646",
|
|
9986
|
+
"\u0639\u0644\u0649",
|
|
9987
|
+
"\u0627\u0644\u0649",
|
|
9988
|
+
"\u0625\u0644\u0649",
|
|
9989
|
+
"\u0627\u0646",
|
|
9990
|
+
"\u0623\u0646",
|
|
9991
|
+
"\u0648",
|
|
9992
|
+
"\u0627\u0644"
|
|
9993
|
+
]);
|
|
9994
|
+
function words(text) {
|
|
9995
|
+
return new Set(
|
|
9996
|
+
(text || "").toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !NOISE.has(w))
|
|
9997
|
+
);
|
|
9998
|
+
}
|
|
9999
|
+
function overlap(a, b) {
|
|
10000
|
+
if (!a.size || !b.size) return 0;
|
|
10001
|
+
let shared = 0;
|
|
10002
|
+
for (const w of a) if (b.has(w)) shared++;
|
|
10003
|
+
return shared / (a.size + b.size - shared);
|
|
10004
|
+
}
|
|
10005
|
+
var SAME_ENOUGH = 0.7;
|
|
10006
|
+
function findDuplicate(text, selector, existing) {
|
|
10007
|
+
const typed = words(text);
|
|
10008
|
+
if (typed.size < 3) return null;
|
|
10009
|
+
for (const note of existing) {
|
|
10010
|
+
if (selector && note.target?.selector && note.target.selector !== selector) continue;
|
|
10011
|
+
if (selector && !note.target?.selector) continue;
|
|
10012
|
+
if (!selector && note.target?.selector) continue;
|
|
10013
|
+
if (overlap(typed, words(note.description)) >= SAME_ENOUGH) return note;
|
|
10014
|
+
}
|
|
10015
|
+
return null;
|
|
10016
|
+
}
|
|
9356
10017
|
var DRAG_THRESHOLD2 = 6;
|
|
9357
10018
|
var TOUCH_DRAG_THRESHOLD = 12;
|
|
9358
10019
|
var MIN_REGION_SIZE = 8;
|
|
@@ -9407,6 +10068,8 @@ function CaptureMode() {
|
|
|
9407
10068
|
setCompactCapture,
|
|
9408
10069
|
exactShots,
|
|
9409
10070
|
photographNow,
|
|
10071
|
+
developerMode,
|
|
10072
|
+
notes,
|
|
9410
10073
|
capturePrefill
|
|
9411
10074
|
} = useQa();
|
|
9412
10075
|
const coarse = useCoarsePointer();
|
|
@@ -9422,12 +10085,17 @@ function CaptureMode() {
|
|
|
9422
10085
|
const [shot, setShot] = React.useState(null);
|
|
9423
10086
|
const stillRef = React.useRef(null);
|
|
9424
10087
|
const [notesFromThisShot, setNotesFromThisShot] = React.useState(0);
|
|
10088
|
+
const [origin, setOrigin] = React.useState(void 0);
|
|
9425
10089
|
const [shotEngine, setShotEngine] = React.useState(null);
|
|
9426
10090
|
const [shotUrl, setShotUrl] = React.useState(null);
|
|
9427
10091
|
const [capturing, setCapturing] = React.useState(false);
|
|
9428
10092
|
const [captureError, setCaptureError] = React.useState(false);
|
|
9429
10093
|
const [tooHeavy, setTooHeavy] = React.useState(false);
|
|
9430
10094
|
const [description, setDescription] = React.useState(capturePrefill);
|
|
10095
|
+
const twin = React.useMemo(
|
|
10096
|
+
() => description.trim().length > 8 ? findDuplicate(description, selection?.selector, notes) : null,
|
|
10097
|
+
[description, selection, notes]
|
|
10098
|
+
);
|
|
9431
10099
|
const [severity, setSeverity] = React.useState("bug");
|
|
9432
10100
|
const [targetForensics, setTargetForensics] = React.useState(void 0);
|
|
9433
10101
|
const taRef = React.useRef(null);
|
|
@@ -9569,6 +10237,7 @@ function CaptureMode() {
|
|
|
9569
10237
|
if (regionRect) {
|
|
9570
10238
|
const sel2 = { kind: "region", rect: regionRect };
|
|
9571
10239
|
setTargetForensics(void 0);
|
|
10240
|
+
setOrigin(void 0);
|
|
9572
10241
|
if (coarse) {
|
|
9573
10242
|
setCandidate(sel2);
|
|
9574
10243
|
setPhase("confirming");
|
|
@@ -9591,6 +10260,7 @@ function CaptureMode() {
|
|
|
9591
10260
|
tagName: el.tagName.toLowerCase()
|
|
9592
10261
|
};
|
|
9593
10262
|
setTargetForensics(collectTargetForensics(el));
|
|
10263
|
+
setOrigin(resolveOrigin(el));
|
|
9594
10264
|
if (coarse) {
|
|
9595
10265
|
setCandidate(sel);
|
|
9596
10266
|
setHover({ rect: sel.rect, selector: sel.selector || "" });
|
|
@@ -9737,6 +10407,7 @@ function CaptureMode() {
|
|
|
9737
10407
|
setDescription("");
|
|
9738
10408
|
setSeverity("bug");
|
|
9739
10409
|
setTargetForensics(void 0);
|
|
10410
|
+
setOrigin(void 0);
|
|
9740
10411
|
setCaptureError(false);
|
|
9741
10412
|
}, []);
|
|
9742
10413
|
const save = async (keepGoing = false) => {
|
|
@@ -9757,8 +10428,10 @@ function CaptureMode() {
|
|
|
9757
10428
|
await addNote({
|
|
9758
10429
|
description,
|
|
9759
10430
|
screenshot: shot ?? void 0,
|
|
10431
|
+
shotEngine: shotEngine ?? void 0,
|
|
9760
10432
|
target,
|
|
9761
10433
|
severity,
|
|
10434
|
+
origin,
|
|
9762
10435
|
forensics: selection.kind === "element" ? targetForensics : void 0
|
|
9763
10436
|
});
|
|
9764
10437
|
if (keepGoing) {
|
|
@@ -10388,8 +11061,8 @@ function CaptureMode() {
|
|
|
10388
11061
|
]
|
|
10389
11062
|
}
|
|
10390
11063
|
),
|
|
10391
|
-
/* @__PURE__ */ jsxRuntime.jsx(LocationReveal, { target: selection }),
|
|
10392
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
11064
|
+
developerMode && /* @__PURE__ */ jsxRuntime.jsx(LocationReveal, { target: selection }),
|
|
11065
|
+
developerMode && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
10393
11066
|
"div",
|
|
10394
11067
|
{
|
|
10395
11068
|
role: "group",
|
|
@@ -10419,6 +11092,13 @@ function CaptureMode() {
|
|
|
10419
11092
|
]
|
|
10420
11093
|
}
|
|
10421
11094
|
),
|
|
11095
|
+
twin && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-text-10 qa-text-warn", "data-qa-duplicate": "true", children: [
|
|
11096
|
+
t("dup_notice", { when: new Date(twin.timestamp).toLocaleTimeString() }),
|
|
11097
|
+
" \u201C",
|
|
11098
|
+
twin.description.slice(0, 70),
|
|
11099
|
+
twin.description.length > 70 ? "\u2026" : "",
|
|
11100
|
+
"\u201D"
|
|
11101
|
+
] }),
|
|
10422
11102
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10423
11103
|
"textarea",
|
|
10424
11104
|
{
|
|
@@ -10437,6 +11117,10 @@ function CaptureMode() {
|
|
|
10437
11117
|
className: "qa-w-full qa-resize-y qa-rounded-lg qa-border qa-border-subtle qa-bg-0 qa-text-hi qa-px-2 qa-py-1.5 qa-text-sm qa-focus-ring"
|
|
10438
11118
|
}
|
|
10439
11119
|
),
|
|
11120
|
+
developerMode && origin && (origin.component || origin.file) && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-text-10 qa-text-mid", "data-qa-origin": "true", children: [
|
|
11121
|
+
origin.component ?? "\u2014",
|
|
11122
|
+
origin.file ? ` \xB7 ${origin.file}${origin.line ? `:${origin.line}` : ""}` : ""
|
|
11123
|
+
] }),
|
|
10440
11124
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-2", children: [
|
|
10441
11125
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
10442
11126
|
"button",
|
|
@@ -10478,8 +11162,7 @@ function CaptureMode() {
|
|
|
10478
11162
|
] }),
|
|
10479
11163
|
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-text-center qa-text-10 qa-text-slate-400", children: [
|
|
10480
11164
|
t("save_hint"),
|
|
10481
|
-
|
|
10482
|
-
t("severity_keys")
|
|
11165
|
+
developerMode ? ` \xB7 ${t("severity_keys")}` : ""
|
|
10483
11166
|
] })
|
|
10484
11167
|
] })
|
|
10485
11168
|
]
|
|
@@ -10687,5 +11370,5 @@ function Qapture({ config }) {
|
|
|
10687
11370
|
exports.Qapture = Qapture;
|
|
10688
11371
|
exports.deleteQaDatabase = deleteQaDatabase;
|
|
10689
11372
|
exports.initQaStudio = initQaStudio;
|
|
10690
|
-
//# sourceMappingURL=chunk-
|
|
10691
|
-
//# sourceMappingURL=chunk-
|
|
11373
|
+
//# sourceMappingURL=chunk-J77SGE5E.cjs.map
|
|
11374
|
+
//# sourceMappingURL=chunk-J77SGE5E.cjs.map
|