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
|
@@ -169,6 +169,8 @@ var DEFAULTS = {
|
|
|
169
169
|
brandLabel: "Qapture",
|
|
170
170
|
loginField: { en: "Username", ar: "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062A\u062E\u062F\u0645" },
|
|
171
171
|
rtl: false,
|
|
172
|
+
beta: false,
|
|
173
|
+
collector: null,
|
|
172
174
|
visible: void 0,
|
|
173
175
|
alwaysVisible: false,
|
|
174
176
|
hotkey: "shift+alt+q",
|
|
@@ -313,9 +315,9 @@ function warnMissingArabic(loginField, credentials, journey, warnings) {
|
|
|
313
315
|
if (c.hint !== void 0 && !hasAr(c.hint)) missing.push(`credentials role="${c.role}" (hint.ar)`);
|
|
314
316
|
}
|
|
315
317
|
if (!missing.length) return;
|
|
316
|
-
const
|
|
317
|
-
const shown = missing.slice(0,
|
|
318
|
-
const rest = missing.length >
|
|
318
|
+
const LIMIT2 = 6;
|
|
319
|
+
const shown = missing.slice(0, LIMIT2).join("; ");
|
|
320
|
+
const rest = missing.length > LIMIT2 ? ` (+${missing.length - LIMIT2} more)` : "";
|
|
319
321
|
warnings.push(
|
|
320
322
|
`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.`
|
|
321
323
|
);
|
|
@@ -340,6 +342,8 @@ function validateConfig(input) {
|
|
|
340
342
|
journey: [],
|
|
341
343
|
preamble: null,
|
|
342
344
|
rtl: DEFAULTS.rtl,
|
|
345
|
+
beta: DEFAULTS.beta,
|
|
346
|
+
collector: null,
|
|
343
347
|
visible: DEFAULTS.visible,
|
|
344
348
|
alwaysVisible: DEFAULTS.alwaysVisible,
|
|
345
349
|
hotkey: DEFAULTS.hotkey,
|
|
@@ -360,6 +364,8 @@ function validateConfig(input) {
|
|
|
360
364
|
journey: [],
|
|
361
365
|
preamble: null,
|
|
362
366
|
rtl: DEFAULTS.rtl,
|
|
367
|
+
beta: DEFAULTS.beta,
|
|
368
|
+
collector: null,
|
|
363
369
|
visible: DEFAULTS.visible,
|
|
364
370
|
alwaysVisible: DEFAULTS.alwaysVisible,
|
|
365
371
|
hotkey: DEFAULTS.hotkey,
|
|
@@ -395,6 +401,15 @@ function validateConfig(input) {
|
|
|
395
401
|
const journey = raw["journey"] !== void 0 ? coerceJourney(raw["journey"], warnings) : [];
|
|
396
402
|
const preamble = raw["preamble"] !== void 0 ? coercePreamble(raw["preamble"]) : null;
|
|
397
403
|
const rtl = typeof raw["rtl"] === "boolean" ? raw["rtl"] : DEFAULTS.rtl;
|
|
404
|
+
const beta = typeof raw["beta"] === "boolean" ? raw["beta"] : DEFAULTS.beta;
|
|
405
|
+
const rawCol = raw["collector"];
|
|
406
|
+
const collector = rawCol && typeof rawCol["url"] === "string" && typeof rawCol["token"] === "string" && typeof rawCol["project"] === "string" ? {
|
|
407
|
+
url: rawCol["url"],
|
|
408
|
+
token: rawCol["token"],
|
|
409
|
+
project: rawCol["project"],
|
|
410
|
+
campaign: typeof rawCol["campaign"] === "string" ? rawCol["campaign"] : void 0,
|
|
411
|
+
tester: typeof rawCol["tester"] === "string" ? rawCol["tester"] : void 0
|
|
412
|
+
} : DEFAULTS.collector;
|
|
398
413
|
const alwaysVisible = typeof raw["alwaysVisible"] === "boolean" ? raw["alwaysVisible"] : DEFAULTS.alwaysVisible;
|
|
399
414
|
const hotkey = isNonEmptyString(raw["hotkey"]) ? raw["hotkey"].trim() : DEFAULTS.hotkey;
|
|
400
415
|
const captureHotkey = isNonEmptyString(raw["captureHotkey"]) ? raw["captureHotkey"].trim() : DEFAULTS.captureHotkey;
|
|
@@ -417,6 +432,8 @@ function validateConfig(input) {
|
|
|
417
432
|
journey,
|
|
418
433
|
preamble,
|
|
419
434
|
rtl,
|
|
435
|
+
beta,
|
|
436
|
+
collector,
|
|
420
437
|
visible,
|
|
421
438
|
alwaysVisible,
|
|
422
439
|
hotkey,
|
|
@@ -1694,6 +1711,98 @@ function createStorage(namespace) {
|
|
|
1694
1711
|
return { getItem, setItem, getJSON, setJSON };
|
|
1695
1712
|
}
|
|
1696
1713
|
|
|
1714
|
+
// src/lib/faultLog.ts
|
|
1715
|
+
var LIMIT = 40;
|
|
1716
|
+
var faults = [];
|
|
1717
|
+
function recordFault(where, err) {
|
|
1718
|
+
const what = err instanceof Error ? `${err.name}: ${err.message}` : typeof err === "string" ? err : (() => {
|
|
1719
|
+
try {
|
|
1720
|
+
return JSON.stringify(err);
|
|
1721
|
+
} catch {
|
|
1722
|
+
return String(err);
|
|
1723
|
+
}
|
|
1724
|
+
})();
|
|
1725
|
+
faults.push({ at: Date.now(), where, what: what.slice(0, 500) });
|
|
1726
|
+
if (faults.length > LIMIT) faults.splice(0, faults.length - LIMIT);
|
|
1727
|
+
console.warn(`[QA] ${where}:`, err);
|
|
1728
|
+
}
|
|
1729
|
+
function readFaults() {
|
|
1730
|
+
return [...faults].reverse();
|
|
1731
|
+
}
|
|
1732
|
+
function clearFaults() {
|
|
1733
|
+
faults.length = 0;
|
|
1734
|
+
}
|
|
1735
|
+
function faultsAsText(version) {
|
|
1736
|
+
if (!faults.length) return "No faults recorded.";
|
|
1737
|
+
const head = [
|
|
1738
|
+
`qapture ${version}`,
|
|
1739
|
+
typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
1740
|
+
typeof location !== "undefined" ? location.href.split("?")[0] : "",
|
|
1741
|
+
""
|
|
1742
|
+
].filter(Boolean).join("\n");
|
|
1743
|
+
return head + readFaults().map((f) => `${new Date(f.at).toISOString()} [${f.where}] ${f.what}`).join("\n");
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// src/lib/collector.ts
|
|
1747
|
+
var TIMEOUT_MS = 8e3;
|
|
1748
|
+
var MAX_SHOT_BYTES = 6 * 1024 * 1024;
|
|
1749
|
+
function blobToDataUrl(blob) {
|
|
1750
|
+
return new Promise((resolve) => {
|
|
1751
|
+
try {
|
|
1752
|
+
const reader = new FileReader();
|
|
1753
|
+
reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : null);
|
|
1754
|
+
reader.onerror = () => resolve(null);
|
|
1755
|
+
reader.readAsDataURL(blob);
|
|
1756
|
+
} catch {
|
|
1757
|
+
resolve(null);
|
|
1758
|
+
}
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
async function sendToCollector(note, cfg) {
|
|
1762
|
+
if (typeof fetch === "undefined" || !cfg?.url || !cfg.token || !cfg.project) return false;
|
|
1763
|
+
let shot;
|
|
1764
|
+
if (note.screenshot && note.screenshot.size <= MAX_SHOT_BYTES) {
|
|
1765
|
+
shot = await blobToDataUrl(note.screenshot) ?? void 0;
|
|
1766
|
+
}
|
|
1767
|
+
const controller = new AbortController();
|
|
1768
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
1769
|
+
try {
|
|
1770
|
+
const res = await fetch(`${cfg.url.replace(/\/$/, "")}/notes`, {
|
|
1771
|
+
method: "POST",
|
|
1772
|
+
signal: controller.signal,
|
|
1773
|
+
headers: {
|
|
1774
|
+
"content-type": "application/json",
|
|
1775
|
+
authorization: `Bearer ${cfg.token}`
|
|
1776
|
+
},
|
|
1777
|
+
body: JSON.stringify({
|
|
1778
|
+
project: cfg.project,
|
|
1779
|
+
// A campaign per day is the shape that matches how testing actually
|
|
1780
|
+
// happens, and it means nobody has to name anything.
|
|
1781
|
+
campaign: cfg.campaign || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1782
|
+
tester: cfg.tester,
|
|
1783
|
+
id: note.id,
|
|
1784
|
+
route: note.route,
|
|
1785
|
+
description: note.description,
|
|
1786
|
+
wanted: note.wanted,
|
|
1787
|
+
why: note.why,
|
|
1788
|
+
severity: note.severity,
|
|
1789
|
+
origin: note.origin,
|
|
1790
|
+
shot
|
|
1791
|
+
})
|
|
1792
|
+
});
|
|
1793
|
+
if (!res.ok) {
|
|
1794
|
+
recordFault("collector", `server answered ${res.status}`);
|
|
1795
|
+
return false;
|
|
1796
|
+
}
|
|
1797
|
+
return true;
|
|
1798
|
+
} catch (err) {
|
|
1799
|
+
recordFault("collector", err);
|
|
1800
|
+
return false;
|
|
1801
|
+
} finally {
|
|
1802
|
+
clearTimeout(timer);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1697
1806
|
// src/lib/strings.ts
|
|
1698
1807
|
var STR = {
|
|
1699
1808
|
en: {
|
|
@@ -1728,6 +1837,32 @@ var STR = {
|
|
|
1728
1837
|
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.",
|
|
1729
1838
|
no_shot: "no screenshot (location saved)",
|
|
1730
1839
|
annotate_placeholder: "What do you want to do here? (add / remove / change\u2026)",
|
|
1840
|
+
// Asked as two plain questions, never as jargon. "Expected behaviour" is
|
|
1841
|
+
// a phrase from a bug tracker; "what should have happened" is a question
|
|
1842
|
+
// anybody can answer.
|
|
1843
|
+
q_observed: "What happened?",
|
|
1844
|
+
q_observed_hint: "What you saw. Plain words are fine.",
|
|
1845
|
+
q_wanted: "What should have happened?",
|
|
1846
|
+
q_wanted_hint: "What you expected instead. This is the important one.",
|
|
1847
|
+
q_why: "Why does it matter? (optional)",
|
|
1848
|
+
q_why_hint: "What you were trying to get done.",
|
|
1849
|
+
q_fix: "Suggested fix (optional)",
|
|
1850
|
+
q_fix_hint: "Passed on as a suggestion, not an instruction.",
|
|
1851
|
+
voice_start: "Speak",
|
|
1852
|
+
voice_stop: "Stop",
|
|
1853
|
+
voice_failed: "Dictation did not start",
|
|
1854
|
+
voice_denied: "Microphone blocked",
|
|
1855
|
+
dev_mode_label: "Developer mode",
|
|
1856
|
+
diag_title: "Diagnostics",
|
|
1857
|
+
diag_hint: "Checks the handful of things that actually stop screenshots working on a page. Run it if something looks wrong.",
|
|
1858
|
+
diag_run: "Check this page",
|
|
1859
|
+
diag_running: "Checking\u2026",
|
|
1860
|
+
diag_version: "Version {v}",
|
|
1861
|
+
diag_outdated: "Update available: {v} \u2014",
|
|
1862
|
+
diag_current: "up to date",
|
|
1863
|
+
diag_faults: "Recorded problems: {n}",
|
|
1864
|
+
dup_notice: "You already said something like this at {when}:",
|
|
1865
|
+
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.",
|
|
1731
1866
|
save_point: "Save point",
|
|
1732
1867
|
save_next: "Save + next",
|
|
1733
1868
|
save_next_hint: "Save this one and mark up another part of the same screenshot \u2014 no second permission prompt.",
|
|
@@ -1943,6 +2078,29 @@ var STR = {
|
|
|
1943
2078
|
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.",
|
|
1944
2079
|
no_shot: "\u0628\u062F\u0648\u0646 \u0635\u0648\u0631\u0629 (\u062A\u0645 \u062D\u0641\u0638 \u0627\u0644\u0645\u0648\u0642\u0639)",
|
|
1945
2080
|
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)",
|
|
2081
|
+
q_observed: "\u0645\u0627 \u0627\u0644\u0630\u064A \u062D\u062F\u062B\u061F",
|
|
2082
|
+
q_observed_hint: "\u0645\u0627 \u0631\u0623\u064A\u062A\u0647. \u0628\u0643\u0644\u0645\u0627\u062A\u0643 \u0627\u0644\u0639\u0627\u062F\u064A\u0629.",
|
|
2083
|
+
q_wanted: "\u0645\u0627 \u0627\u0644\u0630\u064A \u0643\u0627\u0646 \u064A\u062C\u0628 \u0623\u0646 \u064A\u062D\u062F\u062B\u061F",
|
|
2084
|
+
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.",
|
|
2085
|
+
q_why: "\u0644\u0645\u0627\u0630\u0627 \u064A\u0647\u0645\u0651\u0643\u061F (\u0627\u062E\u062A\u064A\u0627\u0631\u064A)",
|
|
2086
|
+
q_why_hint: "\u0645\u0627 \u0627\u0644\u0630\u064A \u0643\u0646\u062A \u062A\u062D\u0627\u0648\u0644 \u0625\u0646\u062C\u0627\u0632\u0647.",
|
|
2087
|
+
q_fix: "\u0627\u0642\u062A\u0631\u0627\u062D \u0644\u0644\u062D\u0644 (\u0627\u062E\u062A\u064A\u0627\u0631\u064A)",
|
|
2088
|
+
q_fix_hint: "\u064A\u064F\u0645\u0631\u064E\u0651\u0631 \u0643\u0627\u0642\u062A\u0631\u0627\u062D\u060C \u0644\u0627 \u0643\u0623\u0645\u0631.",
|
|
2089
|
+
voice_start: "\u062A\u062D\u062F\u0651\u062B",
|
|
2090
|
+
voice_stop: "\u0625\u064A\u0642\u0627\u0641",
|
|
2091
|
+
voice_failed: "\u0644\u0645 \u064A\u0628\u062F\u0623 \u0627\u0644\u0625\u0645\u0644\u0627\u0621 \u0627\u0644\u0635\u0648\u062A\u064A",
|
|
2092
|
+
voice_denied: "\u0627\u0644\u0645\u064A\u0643\u0631\u0648\u0641\u0648\u0646 \u0645\u062D\u062C\u0648\u0628",
|
|
2093
|
+
dev_mode_label: "\u0648\u0636\u0639 \u0627\u0644\u0645\u0637\u0648\u0651\u0631",
|
|
2094
|
+
diag_title: "\u0627\u0644\u062A\u0634\u062E\u064A\u0635",
|
|
2095
|
+
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.",
|
|
2096
|
+
diag_run: "\u0627\u0641\u062D\u0635 \u0647\u0630\u0647 \u0627\u0644\u0635\u0641\u062D\u0629",
|
|
2097
|
+
diag_running: "\u062C\u0627\u0631\u064D \u0627\u0644\u0641\u062D\u0635\u2026",
|
|
2098
|
+
diag_version: "\u0627\u0644\u0625\u0635\u062F\u0627\u0631 {v}",
|
|
2099
|
+
diag_outdated: "\u064A\u062A\u0648\u0641\u0631 \u062A\u062D\u062F\u064A\u062B: {v} \u2014",
|
|
2100
|
+
diag_current: "\u0645\u062D\u062F\u0651\u062B",
|
|
2101
|
+
diag_faults: "\u0645\u0634\u0643\u0644\u0627\u062A \u0645\u064F\u0633\u062C\u0651\u0644\u0629: {n}",
|
|
2102
|
+
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}:",
|
|
2103
|
+
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.",
|
|
1946
2104
|
save_point: "\u062D\u0641\u0638 \u0627\u0644\u0646\u0642\u0637\u0629",
|
|
1947
2105
|
save_next: "\u062D\u0641\u0638 + \u0627\u0644\u062A\u0627\u0644\u064A",
|
|
1948
2106
|
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.",
|
|
@@ -3172,12 +3330,13 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
3172
3330
|
if (blob) return { status: "ok", blob, engine: "exact" };
|
|
3173
3331
|
}
|
|
3174
3332
|
} catch (err) {
|
|
3175
|
-
|
|
3333
|
+
recordFault("screenshot/crop", err);
|
|
3176
3334
|
}
|
|
3177
3335
|
if (prefer === "exact") return { status: "failed" };
|
|
3178
3336
|
}
|
|
3179
3337
|
const weight = documentWeight();
|
|
3180
3338
|
if (weight > TOO_HEAVY_NODES) {
|
|
3339
|
+
recordFault("screenshot/too-heavy", `page has ${weight} elements; the redraw engine was refused`);
|
|
3181
3340
|
return { status: "too-heavy", nodes: weight };
|
|
3182
3341
|
}
|
|
3183
3342
|
try {
|
|
@@ -3186,14 +3345,14 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
3186
3345
|
const blob = await encodeShot(canvas);
|
|
3187
3346
|
return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
|
|
3188
3347
|
} catch (err) {
|
|
3189
|
-
|
|
3348
|
+
recordFault("screenshot/render", err);
|
|
3190
3349
|
try {
|
|
3191
3350
|
const canvas = await captureViaDom(rect, sx, sy, true);
|
|
3192
3351
|
if (!canvas) return { status: "failed" };
|
|
3193
3352
|
const blob = await encodeShot(canvas);
|
|
3194
3353
|
return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
|
|
3195
3354
|
} catch (retryErr) {
|
|
3196
|
-
|
|
3355
|
+
recordFault("screenshot/render-retry", retryErr);
|
|
3197
3356
|
return { status: "failed" };
|
|
3198
3357
|
}
|
|
3199
3358
|
}
|
|
@@ -3237,10 +3396,32 @@ function formatEvent(ev, t0) {
|
|
|
3237
3396
|
}
|
|
3238
3397
|
return `[${rel}] uncaught: ${oneLine(ev.message)}`;
|
|
3239
3398
|
}
|
|
3399
|
+
var EVIDENCE_MARK = "<!--qa:evidence-->";
|
|
3400
|
+
function noteContextMarkdown(note, index) {
|
|
3401
|
+
const body = noteToMarkdown(note, { index, keepEvidenceMark: true });
|
|
3402
|
+
const at = body.indexOf(EVIDENCE_MARK);
|
|
3403
|
+
const header = [
|
|
3404
|
+
`# Point ${index} \u2014 runtime context`,
|
|
3405
|
+
"",
|
|
3406
|
+
`Page: ${oneLine(note.route) || "/"}`,
|
|
3407
|
+
`Captured: ${oneLine(note.timestamp)}`,
|
|
3408
|
+
"",
|
|
3409
|
+
"Everything the browser recorded around this capture. Kept out of",
|
|
3410
|
+
"`notes.md` on purpose -- it is here when it is needed, and out of the way",
|
|
3411
|
+
"when it is not.",
|
|
3412
|
+
"",
|
|
3413
|
+
"---",
|
|
3414
|
+
""
|
|
3415
|
+
].join("\n");
|
|
3416
|
+
return at === -1 ? `${header}_(nothing was recorded)_
|
|
3417
|
+
` : header + body.slice(at + EVIDENCE_MARK.length).trimStart();
|
|
3418
|
+
}
|
|
3240
3419
|
function noteCheckLine(note, index) {
|
|
3241
3420
|
const where = oneLine(note.route) || "/";
|
|
3242
|
-
const
|
|
3243
|
-
const
|
|
3421
|
+
const wanted = oneLine(note.wanted);
|
|
3422
|
+
const seen = oneLine(note.description) || "(not described)";
|
|
3423
|
+
const claim = wanted || seen;
|
|
3424
|
+
const trimmed = claim.length > 180 ? `${claim.slice(0, 177)}...` : claim;
|
|
3244
3425
|
return `- [ ] **check-${index}** (\`${where}\`) \u2014 ${trimmed}`;
|
|
3245
3426
|
}
|
|
3246
3427
|
function noteToMarkdown(note, opts) {
|
|
@@ -3270,8 +3451,15 @@ function noteToMarkdown(note, opts) {
|
|
|
3270
3451
|
);
|
|
3271
3452
|
}
|
|
3272
3453
|
}
|
|
3454
|
+
if (note.origin?.component) lines.push(`- **Component:** \`${oneLine(note.origin.component)}\``);
|
|
3455
|
+
if (note.origin?.file) {
|
|
3456
|
+
lines.push(`- **Source:** \`${oneLine(note.origin.file)}${note.origin.line ? `:${note.origin.line}` : ""}\``);
|
|
3457
|
+
}
|
|
3273
3458
|
if (idx != null && note.screenshot) {
|
|
3274
3459
|
lines.push(`- **Screenshot:** screenshots/point-${idx}.${shotExtension(note.screenshot)}`);
|
|
3460
|
+
if (note.shotEngine === "dom") {
|
|
3461
|
+
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.");
|
|
3462
|
+
}
|
|
3275
3463
|
}
|
|
3276
3464
|
if (idx != null && note.afterScreenshot) {
|
|
3277
3465
|
lines.push(
|
|
@@ -3279,7 +3467,29 @@ function noteToMarkdown(note, opts) {
|
|
|
3279
3467
|
);
|
|
3280
3468
|
}
|
|
3281
3469
|
lines.push("");
|
|
3282
|
-
lines.push(
|
|
3470
|
+
lines.push("### Observed");
|
|
3471
|
+
lines.push("");
|
|
3472
|
+
lines.push(oneLine(note.description) ? note.description.trim() : "_(not described)_");
|
|
3473
|
+
if (note.wanted && oneLine(note.wanted)) {
|
|
3474
|
+
lines.push("");
|
|
3475
|
+
lines.push("### Expected");
|
|
3476
|
+
lines.push("");
|
|
3477
|
+
lines.push(note.wanted.trim());
|
|
3478
|
+
}
|
|
3479
|
+
if (note.why && oneLine(note.why)) {
|
|
3480
|
+
lines.push("");
|
|
3481
|
+
lines.push("### Why it matters");
|
|
3482
|
+
lines.push("");
|
|
3483
|
+
lines.push(note.why.trim());
|
|
3484
|
+
}
|
|
3485
|
+
if (note.fixHint && oneLine(note.fixHint)) {
|
|
3486
|
+
lines.push("");
|
|
3487
|
+
lines.push("### Suggested fix (a suggestion, not an instruction)");
|
|
3488
|
+
lines.push("");
|
|
3489
|
+
lines.push(note.fixHint.trim());
|
|
3490
|
+
lines.push("");
|
|
3491
|
+
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.");
|
|
3492
|
+
}
|
|
3283
3493
|
if (note.followUp && oneLine(note.followUp)) {
|
|
3284
3494
|
lines.push("");
|
|
3285
3495
|
lines.push(
|
|
@@ -3290,6 +3500,15 @@ function noteToMarkdown(note, opts) {
|
|
|
3290
3500
|
lines.push("");
|
|
3291
3501
|
lines.push(note.followUp.trim());
|
|
3292
3502
|
}
|
|
3503
|
+
if (idx != null) {
|
|
3504
|
+
lines.push("");
|
|
3505
|
+
lines.push(`**Check ${idx} \u2014 how this will be graded**`);
|
|
3506
|
+
lines.push("");
|
|
3507
|
+
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?*");
|
|
3508
|
+
lines.push("");
|
|
3509
|
+
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\`.`);
|
|
3510
|
+
}
|
|
3511
|
+
lines.push(EVIDENCE_MARK);
|
|
3293
3512
|
const recordedSteps = note.context?.steps ?? [];
|
|
3294
3513
|
if (recordedSteps.length) {
|
|
3295
3514
|
const t0 = Date.parse(note.timestamp) || recordedSteps[recordedSteps.length - 1].t;
|
|
@@ -3300,14 +3519,6 @@ function noteToMarkdown(note, opts) {
|
|
|
3300
3519
|
lines.push(`${i + 1}. ${formatStep(step, t0)}`);
|
|
3301
3520
|
});
|
|
3302
3521
|
}
|
|
3303
|
-
if (idx != null) {
|
|
3304
|
-
lines.push("");
|
|
3305
|
-
lines.push(`**Check ${idx} \u2014 how this will be graded**`);
|
|
3306
|
-
lines.push("");
|
|
3307
|
-
lines.push(`The tester will be taken back to \`${oneLine(note.route) || "/"}\`` + (target?.selector ? `, shown \`${oneLine(target.selector)}\`` : ", shown this region") + ", and asked: *is this now what I asked for?*");
|
|
3308
|
-
lines.push("");
|
|
3309
|
-
lines.push(`Treat it as done only when the paragraph above is true on that page, on a fresh load, without the tester doing anything extra. Record the outcome against \`check-${idx}\` in \`verify.md\`.`);
|
|
3310
|
-
}
|
|
3311
3522
|
const ctx = note.context;
|
|
3312
3523
|
if (ctx) {
|
|
3313
3524
|
const env = ctx.env;
|
|
@@ -3354,7 +3565,67 @@ function noteToMarkdown(note, opts) {
|
|
|
3354
3565
|
lines.push("");
|
|
3355
3566
|
lines.push("</details>");
|
|
3356
3567
|
}
|
|
3357
|
-
|
|
3568
|
+
const whole = lines.join("\n");
|
|
3569
|
+
if (opts?.keepEvidenceMark) return whole;
|
|
3570
|
+
if (!opts?.contextFile) return whole.replace(`${EVIDENCE_MARK}
|
|
3571
|
+
`, "").replace(EVIDENCE_MARK, "");
|
|
3572
|
+
const at = whole.indexOf(EVIDENCE_MARK);
|
|
3573
|
+
const report = at === -1 ? whole : whole.slice(0, at).trimEnd();
|
|
3574
|
+
return `${report}
|
|
3575
|
+
|
|
3576
|
+
<sub>Steps, console, network, environment and element forensics: \`${opts.contextFile}\` \u2014 open it only if the words above leave something genuinely unresolved.</sub>`;
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
// src/lib/reproSpec.ts
|
|
3580
|
+
function lit(value) {
|
|
3581
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\r?\n/g, " ");
|
|
3582
|
+
}
|
|
3583
|
+
function line(value, limit = 200) {
|
|
3584
|
+
const one = (value ?? "").replace(/\s+/g, " ").trim();
|
|
3585
|
+
if (!one) return "";
|
|
3586
|
+
return one.length > limit ? `${one.slice(0, limit - 3)}...` : one;
|
|
3587
|
+
}
|
|
3588
|
+
function reproSpec(note, index) {
|
|
3589
|
+
const selector = note.target?.selector;
|
|
3590
|
+
const route = note.route || "/";
|
|
3591
|
+
if (!selector && !note.route) return null;
|
|
3592
|
+
const observed = line(note.description);
|
|
3593
|
+
const expected = line(note.wanted);
|
|
3594
|
+
const out = [];
|
|
3595
|
+
out.push(`// check-${index} -- DRAFT. Delete this file if the change is cosmetic.`);
|
|
3596
|
+
out.push("//");
|
|
3597
|
+
out.push("// The URL and the selector below were captured from the live page, so");
|
|
3598
|
+
out.push("// they are the tedious part already done. The assertion is yours: only");
|
|
3599
|
+
out.push("// you know whether this point is worth pinning down with a test.");
|
|
3600
|
+
out.push("//");
|
|
3601
|
+
if (observed) out.push(`// Observed: ${observed}`);
|
|
3602
|
+
if (expected) out.push(`// Expected: ${expected}`);
|
|
3603
|
+
else out.push("// Expected: (the tester did not say -- ask before asserting anything)");
|
|
3604
|
+
if (note.origin?.file) {
|
|
3605
|
+
out.push(`// Rendered by: ${note.origin.component ?? "?"} (${note.origin.file}${note.origin.line ? `:${note.origin.line}` : ""})`);
|
|
3606
|
+
}
|
|
3607
|
+
out.push("");
|
|
3608
|
+
out.push("import { test, expect } from '@playwright/test';");
|
|
3609
|
+
out.push("");
|
|
3610
|
+
out.push(`test('check-${index}: ${lit(line(note.description, 60) || "reported point")}', async ({ page }) => {`);
|
|
3611
|
+
out.push(` await page.goto('${lit(route)}');`);
|
|
3612
|
+
if (selector) {
|
|
3613
|
+
out.push("");
|
|
3614
|
+
out.push(` const target = page.locator('${lit(selector)}');`);
|
|
3615
|
+
out.push(" await expect(target).toBeVisible();");
|
|
3616
|
+
out.push("");
|
|
3617
|
+
out.push(" // TODO: assert the EXPECTED behaviour quoted above.");
|
|
3618
|
+
out.push(" // Being visible only proves the element is still there -- it does not");
|
|
3619
|
+
out.push(" // prove the thing the tester asked for actually happened.");
|
|
3620
|
+
} else {
|
|
3621
|
+
out.push("");
|
|
3622
|
+
out.push(" // No element was picked for this point -- it was a region or a plain");
|
|
3623
|
+
out.push(" // note. Drive the page to the state described above, then assert.");
|
|
3624
|
+
out.push(" // TODO");
|
|
3625
|
+
}
|
|
3626
|
+
out.push("});");
|
|
3627
|
+
out.push("");
|
|
3628
|
+
return out.join("\n");
|
|
3358
3629
|
}
|
|
3359
3630
|
|
|
3360
3631
|
// src/lib/exportZip.ts
|
|
@@ -3490,7 +3761,16 @@ This archive is not a list of suggestions. Every point in \`notes.md\` is an acc
|
|
|
3490
3761
|
|
|
3491
3762
|
**What to do.** Do the work, then fill in \`verify.md\`: tick a box only when the check is true on a fresh load of the page named beside it, with the tester doing nothing extra. Where you could not do something, or you think it is the wrong thing to do, leave the box unticked and write one line saying why. An unticked box with a reason is a good answer. A ticked box that does not hold up is the only bad one \u2014 it costs the tester the trip to find out.
|
|
3492
3763
|
|
|
3493
|
-
Hand \`verify.md\` back with the work
|
|
3764
|
+
Hand \`verify.md\` back with the work.
|
|
3765
|
+
|
|
3766
|
+
### What is in this archive
|
|
3767
|
+
|
|
3768
|
+
- \`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.
|
|
3769
|
+
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.
|
|
3770
|
+
- \`verify.md\` \u2014 the checklist, one unticked box per point.
|
|
3771
|
+
- \`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.
|
|
3772
|
+
- \`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.
|
|
3773
|
+
- \`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.`
|
|
3494
3774
|
);
|
|
3495
3775
|
const stack = typeof p.stack === "string" && p.stack.trim() ? p.stack.trim() : "(not provided)";
|
|
3496
3776
|
const runArr = toStrings(p.runCommands);
|
|
@@ -3630,7 +3910,14 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
|
|
|
3630
3910
|
""
|
|
3631
3911
|
].join("\n");
|
|
3632
3912
|
const noteBlocks = notes.map(
|
|
3633
|
-
(n, i) => noteToMarkdown(n, {
|
|
3913
|
+
(n, i) => noteToMarkdown(n, {
|
|
3914
|
+
brand: brandLabel,
|
|
3915
|
+
index: i + 1,
|
|
3916
|
+
// Runtime evidence goes to its own file and is pointed at from here.
|
|
3917
|
+
// See the contextFile branch in noteMarkdown.ts for why: a long report
|
|
3918
|
+
// measurably lowers an agent's chance of fixing the thing.
|
|
3919
|
+
contextFile: n.context ? `context/point-${i + 1}.md` : void 0
|
|
3920
|
+
})
|
|
3634
3921
|
);
|
|
3635
3922
|
const notesBody = noteBlocks.length > 0 ? `${noteBlocks.join("\n\n---\n\n")}
|
|
3636
3923
|
|
|
@@ -3671,6 +3958,43 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
|
|
|
3671
3958
|
"Re-open the walkthrough on the tester's machine with `?qa=walk:verify`.",
|
|
3672
3959
|
""
|
|
3673
3960
|
].join("\n"));
|
|
3961
|
+
const contextDir = zip.folder("context");
|
|
3962
|
+
notes.forEach((n, i) => {
|
|
3963
|
+
if (n.context && contextDir) {
|
|
3964
|
+
contextDir.file(`point-${i + 1}.md`, noteContextMarkdown(n, i + 1));
|
|
3965
|
+
}
|
|
3966
|
+
});
|
|
3967
|
+
const reproDir = zip.folder("repro");
|
|
3968
|
+
let reproCount = 0;
|
|
3969
|
+
notes.forEach((n, i) => {
|
|
3970
|
+
const spec = reproSpec(n, i + 1);
|
|
3971
|
+
if (spec && reproDir) {
|
|
3972
|
+
reproDir.file(`check-${i + 1}.spec.ts`, spec);
|
|
3973
|
+
reproCount++;
|
|
3974
|
+
}
|
|
3975
|
+
});
|
|
3976
|
+
if (reproCount && reproDir) {
|
|
3977
|
+
reproDir.file("README.md", [
|
|
3978
|
+
"# Reproduction drafts",
|
|
3979
|
+
"",
|
|
3980
|
+
"One per point, and every one of them is optional.",
|
|
3981
|
+
"",
|
|
3982
|
+
"**Use one** where the point is behavioural and worth pinning down, so it",
|
|
3983
|
+
"cannot quietly come back later. An executable check is worth far more to",
|
|
3984
|
+
"you than another paragraph of steps written in English.",
|
|
3985
|
+
"",
|
|
3986
|
+
"**Delete it** where the point is cosmetic -- a colour, a spacing, a word.",
|
|
3987
|
+
"A test asserting that a heading is visible proves nothing anybody wanted",
|
|
3988
|
+
"proved, and it is one more file to maintain forever.",
|
|
3989
|
+
"",
|
|
3990
|
+
"That call is yours. The tester was not asked to make it and could not.",
|
|
3991
|
+
"",
|
|
3992
|
+
"What is already done for you in each file: the URL, a selector verified",
|
|
3993
|
+
"against the live DOM at capture time, and the observed and expected",
|
|
3994
|
+
"behaviour quoted in place. What is left is the assertion, marked TODO.",
|
|
3995
|
+
""
|
|
3996
|
+
].join("\n"));
|
|
3997
|
+
}
|
|
3674
3998
|
notes.forEach((n, i) => {
|
|
3675
3999
|
if (n.screenshot && shots) {
|
|
3676
4000
|
shots.file(`point-${i + 1}.${shotExtension(n.screenshot)}`, n.screenshot);
|
|
@@ -4140,7 +4464,7 @@ function reportMarkdownText(allNotes) {
|
|
|
4140
4464
|
"",
|
|
4141
4465
|
"---",
|
|
4142
4466
|
""
|
|
4143
|
-
].filter((
|
|
4467
|
+
].filter((line2) => line2 !== "").join("\n");
|
|
4144
4468
|
const body = ordered.map((n) => noteMarkdownForDisk(n, noteIndex[n.id])).join("\n---\n\n");
|
|
4145
4469
|
return `${header}
|
|
4146
4470
|
${body}`;
|
|
@@ -4267,18 +4591,6 @@ async function requestPersistentStorage() {
|
|
|
4267
4591
|
return false;
|
|
4268
4592
|
}
|
|
4269
4593
|
}
|
|
4270
|
-
function formatBytes(bytes) {
|
|
4271
|
-
if (!Number.isFinite(bytes) || bytes <= 0) return "0 KB";
|
|
4272
|
-
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
4273
|
-
let value = bytes;
|
|
4274
|
-
let i = 0;
|
|
4275
|
-
while (value >= 1024 && i < units.length - 1) {
|
|
4276
|
-
value /= 1024;
|
|
4277
|
-
i++;
|
|
4278
|
-
}
|
|
4279
|
-
const decimals = value < 10 && i > 1 ? 1 : 0;
|
|
4280
|
-
return `${value.toFixed(decimals)} ${units[i]}`;
|
|
4281
|
-
}
|
|
4282
4594
|
function estimateOwnBytes(notes) {
|
|
4283
4595
|
let total = 0;
|
|
4284
4596
|
for (const n of notes) {
|
|
@@ -4315,6 +4627,7 @@ function exactShotsWanted(store) {
|
|
|
4315
4627
|
}
|
|
4316
4628
|
var SIMPLE_MODE_KEY = "simpleMode";
|
|
4317
4629
|
var COMPACT_KEY = "compactCapture";
|
|
4630
|
+
var DEV_MODE_KEY = "developerMode";
|
|
4318
4631
|
var LAST_CAMPAIGN_KEY = "lastCampaign";
|
|
4319
4632
|
var AUTO_BACKUP_KEY = "autoBackup";
|
|
4320
4633
|
var AUTO_BACKUP_AT_KEY = "autoBackupAt";
|
|
@@ -4380,7 +4693,15 @@ function QaProvider({
|
|
|
4380
4693
|
const [lang, setLangState] = useState(() => {
|
|
4381
4694
|
const saved = storage.getItem(LANG_KEY);
|
|
4382
4695
|
if (saved === "ar" || saved === "en") return saved;
|
|
4383
|
-
|
|
4696
|
+
if (config.rtl) return "ar";
|
|
4697
|
+
try {
|
|
4698
|
+
const html = document.documentElement;
|
|
4699
|
+
if (html.getAttribute("dir") === "rtl") return "ar";
|
|
4700
|
+
if ((html.getAttribute("lang") || "").toLowerCase().startsWith("ar")) return "ar";
|
|
4701
|
+
if ((navigator.language || "").toLowerCase().startsWith("ar")) return "ar";
|
|
4702
|
+
} catch {
|
|
4703
|
+
}
|
|
4704
|
+
return "en";
|
|
4384
4705
|
});
|
|
4385
4706
|
const [guideChecked, setGuideChecked] = useState(
|
|
4386
4707
|
() => new Set(storage.getJSON(GUIDE_KEY, []))
|
|
@@ -4418,6 +4739,9 @@ function QaProvider({
|
|
|
4418
4739
|
const [simpleMode, setSimpleModeState] = useState(
|
|
4419
4740
|
() => storage.getItem(SIMPLE_MODE_KEY) === "1"
|
|
4420
4741
|
);
|
|
4742
|
+
const [developerMode, setDeveloperModeState] = useState(
|
|
4743
|
+
() => storage.getItem(DEV_MODE_KEY) === "1"
|
|
4744
|
+
);
|
|
4421
4745
|
const [compactCapture, setCompactCaptureState] = useState(
|
|
4422
4746
|
() => storage.getItem(COMPACT_KEY) === "1"
|
|
4423
4747
|
);
|
|
@@ -4747,6 +5071,11 @@ function QaProvider({
|
|
|
4747
5071
|
target: input.target ?? void 0,
|
|
4748
5072
|
severity: input.severity,
|
|
4749
5073
|
status: input.status,
|
|
5074
|
+
wanted: (input.wanted || "").trim() || void 0,
|
|
5075
|
+
why: (input.why || "").trim() || void 0,
|
|
5076
|
+
fixHint: (input.fixHint || "").trim() || void 0,
|
|
5077
|
+
shotEngine: input.shotEngine,
|
|
5078
|
+
origin: input.origin,
|
|
4750
5079
|
journeyRef,
|
|
4751
5080
|
context
|
|
4752
5081
|
};
|
|
@@ -4763,8 +5092,11 @@ function QaProvider({
|
|
|
4763
5092
|
});
|
|
4764
5093
|
}
|
|
4765
5094
|
await syncNoteThrough(note);
|
|
5095
|
+
if (config.collector) {
|
|
5096
|
+
void sendToCollector(note, config.collector);
|
|
5097
|
+
}
|
|
4766
5098
|
},
|
|
4767
|
-
[idb, config.journey, config.captureContext, testAlong, testAlongSteps, notify, t, syncNoteThrough, applyNotes]
|
|
5099
|
+
[idb, config.journey, config.captureContext, config.collector, testAlong, testAlongSteps, notify, t, syncNoteThrough, applyNotes]
|
|
4768
5100
|
);
|
|
4769
5101
|
const updateNote = useCallback(
|
|
4770
5102
|
async (id, patch) => {
|
|
@@ -5149,6 +5481,10 @@ function QaProvider({
|
|
|
5149
5481
|
setCompactCaptureState(on);
|
|
5150
5482
|
storage.setItem(COMPACT_KEY, on ? "1" : "0");
|
|
5151
5483
|
}, [storage]);
|
|
5484
|
+
const setDeveloperMode = useCallback((on) => {
|
|
5485
|
+
setDeveloperModeState(on);
|
|
5486
|
+
storage.setItem(DEV_MODE_KEY, on ? "1" : "0");
|
|
5487
|
+
}, [storage]);
|
|
5152
5488
|
const setFilter = useCallback((patch) => {
|
|
5153
5489
|
setFilterState((prev) => ({ ...prev, ...patch }));
|
|
5154
5490
|
}, []);
|
|
@@ -5488,6 +5824,7 @@ function QaProvider({
|
|
|
5488
5824
|
// Config passthrough
|
|
5489
5825
|
namespace: config.namespace,
|
|
5490
5826
|
brand: config.brand,
|
|
5827
|
+
beta: config.beta === true,
|
|
5491
5828
|
loginField: config.loginField,
|
|
5492
5829
|
credentials: config.credentials,
|
|
5493
5830
|
// Never the raw config value: an unconfigured project falls back to the
|
|
@@ -5596,6 +5933,8 @@ function QaProvider({
|
|
|
5596
5933
|
simpleMode,
|
|
5597
5934
|
setSimpleMode,
|
|
5598
5935
|
compactCapture,
|
|
5936
|
+
developerMode,
|
|
5937
|
+
setDeveloperMode,
|
|
5599
5938
|
setCompactCapture,
|
|
5600
5939
|
exportZip: exportZipFn
|
|
5601
5940
|
};
|
|
@@ -5663,6 +6002,16 @@ var ICONS = {
|
|
|
5663
6002
|
Square: [
|
|
5664
6003
|
["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }]
|
|
5665
6004
|
],
|
|
6005
|
+
Info: [
|
|
6006
|
+
["circle", { cx: "12", cy: "12", r: "10" }],
|
|
6007
|
+
["path", { d: "M12 16v-4" }],
|
|
6008
|
+
["path", { d: "M12 8h.01" }]
|
|
6009
|
+
],
|
|
6010
|
+
Mic: [
|
|
6011
|
+
["path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }],
|
|
6012
|
+
["path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }],
|
|
6013
|
+
["line", { x1: "12", x2: "12", y1: "19", y2: "22" }]
|
|
6014
|
+
],
|
|
5666
6015
|
ImagePlus: [
|
|
5667
6016
|
["path", { d: "M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7" }],
|
|
5668
6017
|
["line", { x1: "16", x2: "22", y1: "5", y2: "5" }],
|
|
@@ -5896,7 +6245,7 @@ function clampFabPos(p, w = FAB_SIZE_PX, h = FAB_SIZE_PX) {
|
|
|
5896
6245
|
};
|
|
5897
6246
|
}
|
|
5898
6247
|
function QaFab() {
|
|
5899
|
-
const { isOpen, setIsOpen, notes, captureActive, namespace, t } = useQa();
|
|
6248
|
+
const { isOpen, setIsOpen, notes, captureActive, namespace, beta, t } = useQa();
|
|
5900
6249
|
const [pos, setPos] = useState(() => loadFabPos(namespace));
|
|
5901
6250
|
const dragRef = useRef(null);
|
|
5902
6251
|
const didDragRef = useRef(false);
|
|
@@ -6009,6 +6358,15 @@ function QaFab() {
|
|
|
6009
6358
|
}
|
|
6010
6359
|
),
|
|
6011
6360
|
/* @__PURE__ */ jsx(Icon, { name: isOpen ? "X" : "ClipboardList", size: 24 }),
|
|
6361
|
+
beta && !isOpen && /* @__PURE__ */ jsx(
|
|
6362
|
+
"span",
|
|
6363
|
+
{
|
|
6364
|
+
className: "qa-absolute qa-rounded-full qa-text-10 qa-font-bold qa-bg-1 qa-text-mid qa-border qa-border-subtle",
|
|
6365
|
+
style: { bottom: "-6px", left: "50%", transform: "translateX(-50%)", padding: "0 5px", lineHeight: "1.3" },
|
|
6366
|
+
"aria-hidden": "true",
|
|
6367
|
+
children: "beta"
|
|
6368
|
+
}
|
|
6369
|
+
),
|
|
6012
6370
|
!isOpen && notes.length > 0 && /* @__PURE__ */ jsx(
|
|
6013
6371
|
"span",
|
|
6014
6372
|
{
|
|
@@ -7702,6 +8060,154 @@ function NoteFilterBar() {
|
|
|
7702
8060
|
) })
|
|
7703
8061
|
] });
|
|
7704
8062
|
}
|
|
8063
|
+
|
|
8064
|
+
// src/lib/doctor.ts
|
|
8065
|
+
var HEAVY_PAGE = 6e3;
|
|
8066
|
+
function canvasIsReadable() {
|
|
8067
|
+
try {
|
|
8068
|
+
const c = document.createElement("canvas");
|
|
8069
|
+
c.width = 1;
|
|
8070
|
+
c.height = 1;
|
|
8071
|
+
const ctx = c.getContext("2d");
|
|
8072
|
+
if (!ctx) return false;
|
|
8073
|
+
ctx.fillStyle = "#000";
|
|
8074
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
8075
|
+
c.toDataURL();
|
|
8076
|
+
return true;
|
|
8077
|
+
} catch {
|
|
8078
|
+
return false;
|
|
8079
|
+
}
|
|
8080
|
+
}
|
|
8081
|
+
function taintingImages() {
|
|
8082
|
+
let count = 0;
|
|
8083
|
+
const here = location.origin;
|
|
8084
|
+
for (const img of Array.from(document.images)) {
|
|
8085
|
+
const src = img.currentSrc || img.src;
|
|
8086
|
+
if (!src || src.startsWith("data:") || src.startsWith("blob:")) continue;
|
|
8087
|
+
try {
|
|
8088
|
+
if (new URL(src, here).origin !== here && !img.crossOrigin) count++;
|
|
8089
|
+
} catch {
|
|
8090
|
+
}
|
|
8091
|
+
}
|
|
8092
|
+
return count;
|
|
8093
|
+
}
|
|
8094
|
+
async function runDoctor() {
|
|
8095
|
+
const out = [];
|
|
8096
|
+
if (typeof document === "undefined") return out;
|
|
8097
|
+
const supported = isExactCaptureSupported();
|
|
8098
|
+
const status = getExactCaptureStatus();
|
|
8099
|
+
out.push(
|
|
8100
|
+
!supported ? {
|
|
8101
|
+
label: "Screenshot engine",
|
|
8102
|
+
verdict: "warn",
|
|
8103
|
+
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."
|
|
8104
|
+
} : status === "live" ? { label: "Screenshot engine", verdict: "ok", detail: "Real photographs. This is the accurate one." } : {
|
|
8105
|
+
label: "Screenshot engine",
|
|
8106
|
+
verdict: "warn",
|
|
8107
|
+
detail: "Photographs are switched off, so screenshots are re-drawn and may not match the page."
|
|
8108
|
+
}
|
|
8109
|
+
);
|
|
8110
|
+
const readable = canvasIsReadable();
|
|
8111
|
+
const risky = taintingImages();
|
|
8112
|
+
out.push(
|
|
8113
|
+
!readable ? {
|
|
8114
|
+
label: "Screenshot encoding",
|
|
8115
|
+
verdict: "bad",
|
|
8116
|
+
detail: "This page cannot turn a drawing into an image at all. Re-drawn screenshots will fail here; use photographs."
|
|
8117
|
+
} : risky > 0 ? {
|
|
8118
|
+
label: "Screenshot encoding",
|
|
8119
|
+
verdict: "info",
|
|
8120
|
+
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.`
|
|
8121
|
+
} : { label: "Screenshot encoding", verdict: "ok", detail: "Nothing on this page blocks screenshot encoding." }
|
|
8122
|
+
);
|
|
8123
|
+
const nodes = document.getElementsByTagName("*").length;
|
|
8124
|
+
out.push({
|
|
8125
|
+
label: "Page size",
|
|
8126
|
+
verdict: nodes > HEAVY_PAGE ? "warn" : "ok",
|
|
8127
|
+
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.`
|
|
8128
|
+
});
|
|
8129
|
+
try {
|
|
8130
|
+
await import('html2canvas');
|
|
8131
|
+
out.push({ label: "Screenshot library", verdict: "ok", detail: "Loads correctly." });
|
|
8132
|
+
} catch {
|
|
8133
|
+
out.push({
|
|
8134
|
+
label: "Screenshot library",
|
|
8135
|
+
verdict: "bad",
|
|
8136
|
+
detail: "Blocked or unreachable \u2014 often a strict Content-Security-Policy. Re-drawn screenshots cannot work here."
|
|
8137
|
+
});
|
|
8138
|
+
}
|
|
8139
|
+
try {
|
|
8140
|
+
const est = await navigator.storage?.estimate?.();
|
|
8141
|
+
if (est?.quota) {
|
|
8142
|
+
const usedPct = Math.round((est.usage ?? 0) / est.quota * 100);
|
|
8143
|
+
out.push({
|
|
8144
|
+
label: "Storage",
|
|
8145
|
+
verdict: usedPct > 90 ? "bad" : usedPct > 70 ? "warn" : "ok",
|
|
8146
|
+
detail: `${usedPct}% of this site's storage used. Notes are kept in this browser until you export them.`
|
|
8147
|
+
});
|
|
8148
|
+
}
|
|
8149
|
+
} catch {
|
|
8150
|
+
}
|
|
8151
|
+
const voice = typeof window !== "undefined" && !!(window.SpeechRecognition || window.webkitSpeechRecognition);
|
|
8152
|
+
out.push({
|
|
8153
|
+
label: "Voice input",
|
|
8154
|
+
verdict: voice ? "ok" : "info",
|
|
8155
|
+
detail: voice ? "Available \u2014 you can speak your notes." : "Not available in this browser. Typing still works."
|
|
8156
|
+
});
|
|
8157
|
+
out.push({
|
|
8158
|
+
label: "Secure connection",
|
|
8159
|
+
verdict: window.isSecureContext ? "ok" : "bad",
|
|
8160
|
+
detail: window.isSecureContext ? "https \u2014 photographs and voice are allowed." : "Not https. Photographs and voice are blocked by the browser on an insecure page."
|
|
8161
|
+
});
|
|
8162
|
+
return out;
|
|
8163
|
+
}
|
|
8164
|
+
|
|
8165
|
+
// src/lib/versionCheck.ts
|
|
8166
|
+
var CACHE_KEY = "qa.versionCheck";
|
|
8167
|
+
var A_DAY = 24 * 60 * 60 * 1e3;
|
|
8168
|
+
var REGISTRY = "https://registry.npmjs.org/qapture2/latest";
|
|
8169
|
+
function isNewer(latest, current) {
|
|
8170
|
+
const norm = (v) => v.replace(/^[^\d]*/, "").split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
|
|
8171
|
+
const a = norm(latest);
|
|
8172
|
+
const b = norm(current);
|
|
8173
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
8174
|
+
const x = a[i] ?? 0;
|
|
8175
|
+
const y = b[i] ?? 0;
|
|
8176
|
+
if (x !== y) return x > y;
|
|
8177
|
+
}
|
|
8178
|
+
return false;
|
|
8179
|
+
}
|
|
8180
|
+
async function latestVersion() {
|
|
8181
|
+
if (typeof window === "undefined") return null;
|
|
8182
|
+
try {
|
|
8183
|
+
const raw = window.localStorage.getItem(CACHE_KEY);
|
|
8184
|
+
if (raw) {
|
|
8185
|
+
const cached = JSON.parse(raw);
|
|
8186
|
+
if (Date.now() - cached.at < A_DAY && cached.latest) return cached.latest;
|
|
8187
|
+
}
|
|
8188
|
+
} catch {
|
|
8189
|
+
}
|
|
8190
|
+
try {
|
|
8191
|
+
const res = await fetch(REGISTRY, { headers: { accept: "application/json" } });
|
|
8192
|
+
if (!res.ok) return null;
|
|
8193
|
+
const body = await res.json();
|
|
8194
|
+
const latest = body.version;
|
|
8195
|
+
if (!latest) return null;
|
|
8196
|
+
try {
|
|
8197
|
+
window.localStorage.setItem(CACHE_KEY, JSON.stringify({ at: Date.now(), latest }));
|
|
8198
|
+
} catch {
|
|
8199
|
+
}
|
|
8200
|
+
return latest;
|
|
8201
|
+
} catch {
|
|
8202
|
+
return null;
|
|
8203
|
+
}
|
|
8204
|
+
}
|
|
8205
|
+
function upgradeHint(latest) {
|
|
8206
|
+
return `npm i qapture2@${latest}`;
|
|
8207
|
+
}
|
|
8208
|
+
|
|
8209
|
+
// src/version.ts
|
|
8210
|
+
var QA_VERSION = "0.9.1" ;
|
|
7705
8211
|
function Section({
|
|
7706
8212
|
icon,
|
|
7707
8213
|
title,
|
|
@@ -7764,8 +8270,24 @@ function SettingsSheet({ onClose }) {
|
|
|
7764
8270
|
setSimpleMode,
|
|
7765
8271
|
compactCapture,
|
|
7766
8272
|
setCompactCapture,
|
|
8273
|
+
developerMode,
|
|
8274
|
+
setDeveloperMode,
|
|
7767
8275
|
notes
|
|
7768
8276
|
} = useQa();
|
|
8277
|
+
const [checks, setChecks] = useState([]);
|
|
8278
|
+
const [running, setRunning] = useState(false);
|
|
8279
|
+
const [faults2, setFaults] = useState(() => readFaults());
|
|
8280
|
+
const [latest, setLatest] = useState(null);
|
|
8281
|
+
useEffect(() => {
|
|
8282
|
+
let alive = true;
|
|
8283
|
+
void latestVersion().then((v) => {
|
|
8284
|
+
if (alive) setLatest(v);
|
|
8285
|
+
});
|
|
8286
|
+
setFaults(readFaults());
|
|
8287
|
+
return () => {
|
|
8288
|
+
alive = false;
|
|
8289
|
+
};
|
|
8290
|
+
}, []);
|
|
7769
8291
|
const [project, setProject] = useState(lastCampaign.project);
|
|
7770
8292
|
const [campaign2, setCampaign] = useState(lastCampaign.campaign || suggestCampaignName());
|
|
7771
8293
|
const [tester, setTester] = useState(lastCampaign.tester);
|
|
@@ -7791,8 +8313,8 @@ function SettingsSheet({ onClose }) {
|
|
|
7791
8313
|
const syncing = sync.state === "syncing";
|
|
7792
8314
|
const viaZip = sync.engine === "download";
|
|
7793
8315
|
const quotaKnown = storageHealth.supported && storageHealth.quotaBytes > 0;
|
|
7794
|
-
|
|
7795
|
-
|
|
8316
|
+
quotaKnown ? Math.min(100, Math.round(storageHealth.ratio * 100)) : 0;
|
|
8317
|
+
storageHealth.level === "critical" ? "var(--qa-danger)" : storageHealth.level === "warn" ? "var(--qa-warn)" : "var(--qa-accent)";
|
|
7796
8318
|
return /* @__PURE__ */ jsxs(
|
|
7797
8319
|
"div",
|
|
7798
8320
|
{
|
|
@@ -7921,77 +8443,6 @@ function SettingsSheet({ onClose }) {
|
|
|
7921
8443
|
)
|
|
7922
8444
|
] }),
|
|
7923
8445
|
/* @__PURE__ */ jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
|
|
7924
|
-
/* @__PURE__ */ jsxs(Section, { icon: "HardDrive", title: t("storage_title"), children: [
|
|
7925
|
-
/* @__PURE__ */ jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo qa-leading-relaxed", children: t("storage_explain") }),
|
|
7926
|
-
quotaKnown && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
7927
|
-
/* @__PURE__ */ jsx(
|
|
7928
|
-
"div",
|
|
7929
|
-
{
|
|
7930
|
-
className: "qa-w-full qa-rounded-full qa-overflow-hidden qa-bg-3",
|
|
7931
|
-
role: "img",
|
|
7932
|
-
"aria-label": t("storage_used", {
|
|
7933
|
-
used: formatBytes(storageHealth.usageBytes),
|
|
7934
|
-
quota: formatBytes(storageHealth.quotaBytes)
|
|
7935
|
-
}),
|
|
7936
|
-
style: { height: 6 },
|
|
7937
|
-
children: /* @__PURE__ */ jsx("div", { style: { width: `${usedPct}%`, height: "100%", background: meterColor } })
|
|
7938
|
-
}
|
|
7939
|
-
),
|
|
7940
|
-
/* @__PURE__ */ jsxs("p", { className: "qa-m-0 qa-text-10 qa-text-mid", children: [
|
|
7941
|
-
t("storage_used", {
|
|
7942
|
-
used: formatBytes(storageHealth.usageBytes),
|
|
7943
|
-
quota: formatBytes(storageHealth.quotaBytes)
|
|
7944
|
-
}),
|
|
7945
|
-
" \xB7 ",
|
|
7946
|
-
/* @__PURE__ */ jsxs("span", { className: "qa-text-lo", children: [
|
|
7947
|
-
"Qapture ",
|
|
7948
|
-
formatBytes(storageHealth.ownBytes)
|
|
7949
|
-
] })
|
|
7950
|
-
] })
|
|
7951
|
-
] }),
|
|
7952
|
-
/* @__PURE__ */ jsxs("label", { className: "qa-flex qa-items-start qa-gap-2 qa-text-xs qa-text-hi", style: { cursor: "pointer" }, children: [
|
|
7953
|
-
/* @__PURE__ */ jsx(
|
|
7954
|
-
"input",
|
|
7955
|
-
{
|
|
7956
|
-
type: "checkbox",
|
|
7957
|
-
checked: autoBackup,
|
|
7958
|
-
onChange: (e) => setAutoBackup(e.target.checked),
|
|
7959
|
-
style: { marginTop: 2 }
|
|
7960
|
-
}
|
|
7961
|
-
),
|
|
7962
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
7963
|
-
t("autosave_label"),
|
|
7964
|
-
/* @__PURE__ */ jsx("span", { className: "qa-block qa-text-10 qa-text-lo qa-leading-relaxed", children: t("autosave_hint", { n: autoBackupEvery }) })
|
|
7965
|
-
] })
|
|
7966
|
-
] }),
|
|
7967
|
-
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-flex-wrap qa-gap-2", children: [
|
|
7968
|
-
storageHealth.persisted ? /* @__PURE__ */ 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: [
|
|
7969
|
-
/* @__PURE__ */ jsx(Icon, { name: "CheckCircle2", size: 11 }),
|
|
7970
|
-
t("persist_on")
|
|
7971
|
-
] }) : /* @__PURE__ */ jsx(
|
|
7972
|
-
"button",
|
|
7973
|
-
{
|
|
7974
|
-
type: "button",
|
|
7975
|
-
disabled: busy,
|
|
7976
|
-
onClick: () => void run2(requestPersistentStorage2),
|
|
7977
|
-
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-10 qa-text-mid",
|
|
7978
|
-
style: { background: "transparent", cursor: "pointer" },
|
|
7979
|
-
children: t("persist_keep")
|
|
7980
|
-
}
|
|
7981
|
-
),
|
|
7982
|
-
notes.some((n) => n.screenshot) && /* @__PURE__ */ jsx(
|
|
7983
|
-
"button",
|
|
7984
|
-
{
|
|
7985
|
-
type: "button",
|
|
7986
|
-
disabled: busy,
|
|
7987
|
-
onClick: () => void run2(dropAllScreenshots),
|
|
7988
|
-
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-10 qa-text-mid",
|
|
7989
|
-
style: { background: "transparent", cursor: "pointer" },
|
|
7990
|
-
children: t("drop_shots")
|
|
7991
|
-
}
|
|
7992
|
-
)
|
|
7993
|
-
] })
|
|
7994
|
-
] }),
|
|
7995
8446
|
/* @__PURE__ */ jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
|
|
7996
8447
|
/* @__PURE__ */ jsxs(Section, { icon: "Camera", title: t("exact_label"), children: [
|
|
7997
8448
|
/* @__PURE__ */ jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo qa-leading-relaxed", children: t("exact_hint") }),
|
|
@@ -8040,6 +8491,20 @@ function SettingsSheet({ onClose }) {
|
|
|
8040
8491
|
] }),
|
|
8041
8492
|
/* @__PURE__ */ jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
|
|
8042
8493
|
/* @__PURE__ */ jsxs(Section, { icon: "Settings", title: t("settings"), children: [
|
|
8494
|
+
/* @__PURE__ */ jsxs("label", { className: "qa-flex qa-items-start qa-gap-2 qa-text-xs qa-text-hi", style: { cursor: "pointer" }, children: [
|
|
8495
|
+
/* @__PURE__ */ jsx(
|
|
8496
|
+
"input",
|
|
8497
|
+
{
|
|
8498
|
+
type: "checkbox",
|
|
8499
|
+
checked: developerMode,
|
|
8500
|
+
onChange: (e) => setDeveloperMode(e.target.checked)
|
|
8501
|
+
}
|
|
8502
|
+
),
|
|
8503
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
8504
|
+
t("dev_mode_label"),
|
|
8505
|
+
/* @__PURE__ */ jsx("span", { className: "qa-block qa-text-10 qa-text-mid", children: t("dev_mode_hint") })
|
|
8506
|
+
] })
|
|
8507
|
+
] }),
|
|
8043
8508
|
/* @__PURE__ */ jsxs("label", { className: "qa-flex qa-items-center qa-gap-2 qa-text-xs qa-text-hi", style: { cursor: "pointer" }, children: [
|
|
8044
8509
|
/* @__PURE__ */ jsx(
|
|
8045
8510
|
"input",
|
|
@@ -8062,6 +8527,91 @@ function SettingsSheet({ onClose }) {
|
|
|
8062
8527
|
),
|
|
8063
8528
|
t("compact_mode")
|
|
8064
8529
|
] })
|
|
8530
|
+
] }),
|
|
8531
|
+
/* @__PURE__ */ jsxs(Section, { icon: "Info", title: t("diag_title"), children: [
|
|
8532
|
+
/* @__PURE__ */ jsx("p", { className: "qa-text-10 qa-text-mid", children: t("diag_hint") }),
|
|
8533
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-2 qa-flex-wrap", children: [
|
|
8534
|
+
/* @__PURE__ */ jsxs(
|
|
8535
|
+
"button",
|
|
8536
|
+
{
|
|
8537
|
+
type: "button",
|
|
8538
|
+
onClick: () => void (async () => {
|
|
8539
|
+
setRunning(true);
|
|
8540
|
+
try {
|
|
8541
|
+
setChecks(await runDoctor());
|
|
8542
|
+
} finally {
|
|
8543
|
+
setRunning(false);
|
|
8544
|
+
}
|
|
8545
|
+
})(),
|
|
8546
|
+
disabled: running,
|
|
8547
|
+
"data-qa-doctor": "true",
|
|
8548
|
+
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",
|
|
8549
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
8550
|
+
children: [
|
|
8551
|
+
/* @__PURE__ */ jsx(Icon, { name: "Check", size: 13 }),
|
|
8552
|
+
running ? t("diag_running") : t("diag_run")
|
|
8553
|
+
]
|
|
8554
|
+
}
|
|
8555
|
+
),
|
|
8556
|
+
/* @__PURE__ */ jsxs("span", { className: "qa-text-10 qa-text-mid", children: [
|
|
8557
|
+
t("diag_version", { v: QA_VERSION }),
|
|
8558
|
+
latest && isNewer(latest, QA_VERSION) ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
8559
|
+
" \xB7 ",
|
|
8560
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-warn", children: t("diag_outdated", { v: latest }) }),
|
|
8561
|
+
/* @__PURE__ */ jsx("code", { className: "qa-ms-1", children: upgradeHint(latest) })
|
|
8562
|
+
] }) : latest ? ` \xB7 ${t("diag_current")}` : ""
|
|
8563
|
+
] })
|
|
8564
|
+
] }),
|
|
8565
|
+
checks.length > 0 && /* @__PURE__ */ jsx("ul", { className: "qa-space-y-1", "data-qa-doctor-results": "true", children: checks.map((c) => /* @__PURE__ */ jsxs("li", { className: "qa-text-11 qa-text-hi", children: [
|
|
8566
|
+
/* @__PURE__ */ jsx(
|
|
8567
|
+
"span",
|
|
8568
|
+
{
|
|
8569
|
+
"aria-hidden": true,
|
|
8570
|
+
className: c.verdict === "ok" ? "qa-text-success" : c.verdict === "bad" ? "qa-text-danger" : c.verdict === "warn" ? "qa-text-warn" : "qa-text-mid",
|
|
8571
|
+
children: c.verdict === "ok" ? "\u25CF" : c.verdict === "bad" ? "\u25B2" : c.verdict === "warn" ? "\u25B2" : "\u25CB"
|
|
8572
|
+
}
|
|
8573
|
+
),
|
|
8574
|
+
" ",
|
|
8575
|
+
/* @__PURE__ */ jsx("strong", { children: c.label }),
|
|
8576
|
+
/* @__PURE__ */ jsx("span", { className: "qa-block qa-text-10 qa-text-mid qa-ms-3", children: c.detail })
|
|
8577
|
+
] }, c.label)) }),
|
|
8578
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-2 qa-flex-wrap", children: [
|
|
8579
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-11 qa-text-mid", children: t("diag_faults", { n: faults2.length }) }),
|
|
8580
|
+
faults2.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
8581
|
+
/* @__PURE__ */ jsx(
|
|
8582
|
+
"button",
|
|
8583
|
+
{
|
|
8584
|
+
type: "button",
|
|
8585
|
+
onClick: () => void navigator.clipboard?.writeText(faultsAsText(QA_VERSION)),
|
|
8586
|
+
className: "qa-tap qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-0.5 qa-text-10 qa-text-hi",
|
|
8587
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
8588
|
+
children: t("copy_prompt")
|
|
8589
|
+
}
|
|
8590
|
+
),
|
|
8591
|
+
/* @__PURE__ */ jsx(
|
|
8592
|
+
"button",
|
|
8593
|
+
{
|
|
8594
|
+
type: "button",
|
|
8595
|
+
onClick: () => {
|
|
8596
|
+
clearFaults();
|
|
8597
|
+
setFaults([]);
|
|
8598
|
+
},
|
|
8599
|
+
className: "qa-tap qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-0.5 qa-text-10 qa-text-mid",
|
|
8600
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
8601
|
+
children: t("clear_all")
|
|
8602
|
+
}
|
|
8603
|
+
)
|
|
8604
|
+
] })
|
|
8605
|
+
] }),
|
|
8606
|
+
faults2.length > 0 && /* @__PURE__ */ jsx("ul", { className: "qa-space-y-0.5", children: faults2.slice(0, 5).map((f, i) => /* @__PURE__ */ jsxs("li", { className: "qa-text-10 qa-text-mid", children: [
|
|
8607
|
+
/* @__PURE__ */ jsxs("code", { children: [
|
|
8608
|
+
"[",
|
|
8609
|
+
f.where,
|
|
8610
|
+
"]"
|
|
8611
|
+
] }),
|
|
8612
|
+
" ",
|
|
8613
|
+
f.what
|
|
8614
|
+
] }, `${f.at}-${i}`)) })
|
|
8065
8615
|
] })
|
|
8066
8616
|
] })
|
|
8067
8617
|
]
|
|
@@ -8396,17 +8946,17 @@ function QaPanel() {
|
|
|
8396
8946
|
if (typeof window === "undefined") return 0;
|
|
8397
8947
|
const vv = window.visualViewport;
|
|
8398
8948
|
if (!vv) return 0;
|
|
8399
|
-
const
|
|
8400
|
-
if (
|
|
8949
|
+
const overlap2 = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
|
|
8950
|
+
if (overlap2 <= KEYBOARD_OVERLAP_THRESHOLD) return 0;
|
|
8401
8951
|
const panel = panelRef.current;
|
|
8402
8952
|
if (!panel) return 0;
|
|
8403
8953
|
const root2 = panel.getRootNode();
|
|
8404
8954
|
const active = root2.activeElement;
|
|
8405
8955
|
if (!active || !panel.contains(active)) return 0;
|
|
8406
8956
|
const tag = active.tagName;
|
|
8407
|
-
if (tag === "TEXTAREA") return Math.round(
|
|
8957
|
+
if (tag === "TEXTAREA") return Math.round(overlap2) + KEYBOARD_LIFT_GAP;
|
|
8408
8958
|
if (tag === "INPUT" && !NON_TEXT_INPUT_TYPES.has(active.type)) {
|
|
8409
|
-
return Math.round(
|
|
8959
|
+
return Math.round(overlap2) + KEYBOARD_LIFT_GAP;
|
|
8410
8960
|
}
|
|
8411
8961
|
return 0;
|
|
8412
8962
|
} catch {
|
|
@@ -9346,6 +9896,117 @@ function unlockPageScroll() {
|
|
|
9346
9896
|
window.removeEventListener("touchmove", preventScroll, LISTENER_OPTIONS);
|
|
9347
9897
|
}
|
|
9348
9898
|
}
|
|
9899
|
+
|
|
9900
|
+
// src/lib/origin.ts
|
|
9901
|
+
function fiberOf(el) {
|
|
9902
|
+
for (const key in el) {
|
|
9903
|
+
if (key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")) {
|
|
9904
|
+
return el[key] ?? null;
|
|
9905
|
+
}
|
|
9906
|
+
}
|
|
9907
|
+
return null;
|
|
9908
|
+
}
|
|
9909
|
+
function nameOf(fiber) {
|
|
9910
|
+
const type = fiber.type;
|
|
9911
|
+
if (!type || typeof type === "string") return null;
|
|
9912
|
+
const direct = type.displayName || type.name;
|
|
9913
|
+
if (direct) return direct;
|
|
9914
|
+
const inner = type.render?.displayName || type.render?.name;
|
|
9915
|
+
return inner || null;
|
|
9916
|
+
}
|
|
9917
|
+
function tidyPath(file) {
|
|
9918
|
+
const marks = ["/src/", "/app/", "/components/", "/pages/", "/lib/"];
|
|
9919
|
+
for (const mark of marks) {
|
|
9920
|
+
const at = file.lastIndexOf(mark);
|
|
9921
|
+
if (at !== -1) return file.slice(at + 1);
|
|
9922
|
+
}
|
|
9923
|
+
return file.split("/").slice(-2).join("/");
|
|
9924
|
+
}
|
|
9925
|
+
var MAX_WALK = 30;
|
|
9926
|
+
function resolveOrigin(el) {
|
|
9927
|
+
if (!el || typeof window === "undefined") return void 0;
|
|
9928
|
+
let fiber;
|
|
9929
|
+
try {
|
|
9930
|
+
fiber = fiberOf(el);
|
|
9931
|
+
} catch {
|
|
9932
|
+
return void 0;
|
|
9933
|
+
}
|
|
9934
|
+
if (!fiber) return void 0;
|
|
9935
|
+
const out = {};
|
|
9936
|
+
let hops = 0;
|
|
9937
|
+
for (let f = fiber; f && hops < MAX_WALK; f = f.return ?? null, hops++) {
|
|
9938
|
+
if (!out.component) {
|
|
9939
|
+
const name = nameOf(f);
|
|
9940
|
+
if (name) out.component = name;
|
|
9941
|
+
}
|
|
9942
|
+
if (!out.file) {
|
|
9943
|
+
const src = f._debugSource;
|
|
9944
|
+
if (src?.fileName) {
|
|
9945
|
+
out.file = tidyPath(src.fileName);
|
|
9946
|
+
if (typeof src.lineNumber === "number") out.line = src.lineNumber;
|
|
9947
|
+
}
|
|
9948
|
+
}
|
|
9949
|
+
if (out.component && out.file) break;
|
|
9950
|
+
}
|
|
9951
|
+
return out.component || out.file ? out : void 0;
|
|
9952
|
+
}
|
|
9953
|
+
|
|
9954
|
+
// src/lib/duplicate.ts
|
|
9955
|
+
var NOISE = /* @__PURE__ */ new Set([
|
|
9956
|
+
"the",
|
|
9957
|
+
"this",
|
|
9958
|
+
"that",
|
|
9959
|
+
"a",
|
|
9960
|
+
"an",
|
|
9961
|
+
"is",
|
|
9962
|
+
"it",
|
|
9963
|
+
"to",
|
|
9964
|
+
"and",
|
|
9965
|
+
"of",
|
|
9966
|
+
"in",
|
|
9967
|
+
"on",
|
|
9968
|
+
"for",
|
|
9969
|
+
"be",
|
|
9970
|
+
"should",
|
|
9971
|
+
"i",
|
|
9972
|
+
"we",
|
|
9973
|
+
"please",
|
|
9974
|
+
"here",
|
|
9975
|
+
"\u0647\u0630\u0627",
|
|
9976
|
+
"\u0647\u0630\u0647",
|
|
9977
|
+
"\u0641\u064A",
|
|
9978
|
+
"\u0645\u0646",
|
|
9979
|
+
"\u0639\u0644\u0649",
|
|
9980
|
+
"\u0627\u0644\u0649",
|
|
9981
|
+
"\u0625\u0644\u0649",
|
|
9982
|
+
"\u0627\u0646",
|
|
9983
|
+
"\u0623\u0646",
|
|
9984
|
+
"\u0648",
|
|
9985
|
+
"\u0627\u0644"
|
|
9986
|
+
]);
|
|
9987
|
+
function words(text) {
|
|
9988
|
+
return new Set(
|
|
9989
|
+
(text || "").toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !NOISE.has(w))
|
|
9990
|
+
);
|
|
9991
|
+
}
|
|
9992
|
+
function overlap(a, b) {
|
|
9993
|
+
if (!a.size || !b.size) return 0;
|
|
9994
|
+
let shared = 0;
|
|
9995
|
+
for (const w of a) if (b.has(w)) shared++;
|
|
9996
|
+
return shared / (a.size + b.size - shared);
|
|
9997
|
+
}
|
|
9998
|
+
var SAME_ENOUGH = 0.7;
|
|
9999
|
+
function findDuplicate(text, selector, existing) {
|
|
10000
|
+
const typed = words(text);
|
|
10001
|
+
if (typed.size < 3) return null;
|
|
10002
|
+
for (const note of existing) {
|
|
10003
|
+
if (selector && note.target?.selector && note.target.selector !== selector) continue;
|
|
10004
|
+
if (selector && !note.target?.selector) continue;
|
|
10005
|
+
if (!selector && note.target?.selector) continue;
|
|
10006
|
+
if (overlap(typed, words(note.description)) >= SAME_ENOUGH) return note;
|
|
10007
|
+
}
|
|
10008
|
+
return null;
|
|
10009
|
+
}
|
|
9349
10010
|
var DRAG_THRESHOLD2 = 6;
|
|
9350
10011
|
var TOUCH_DRAG_THRESHOLD = 12;
|
|
9351
10012
|
var MIN_REGION_SIZE = 8;
|
|
@@ -9400,6 +10061,8 @@ function CaptureMode() {
|
|
|
9400
10061
|
setCompactCapture,
|
|
9401
10062
|
exactShots,
|
|
9402
10063
|
photographNow,
|
|
10064
|
+
developerMode,
|
|
10065
|
+
notes,
|
|
9403
10066
|
capturePrefill
|
|
9404
10067
|
} = useQa();
|
|
9405
10068
|
const coarse = useCoarsePointer();
|
|
@@ -9415,12 +10078,17 @@ function CaptureMode() {
|
|
|
9415
10078
|
const [shot, setShot] = useState(null);
|
|
9416
10079
|
const stillRef = useRef(null);
|
|
9417
10080
|
const [notesFromThisShot, setNotesFromThisShot] = useState(0);
|
|
10081
|
+
const [origin, setOrigin] = useState(void 0);
|
|
9418
10082
|
const [shotEngine, setShotEngine] = useState(null);
|
|
9419
10083
|
const [shotUrl, setShotUrl] = useState(null);
|
|
9420
10084
|
const [capturing, setCapturing] = useState(false);
|
|
9421
10085
|
const [captureError, setCaptureError] = useState(false);
|
|
9422
10086
|
const [tooHeavy, setTooHeavy] = useState(false);
|
|
9423
10087
|
const [description, setDescription] = useState(capturePrefill);
|
|
10088
|
+
const twin = useMemo(
|
|
10089
|
+
() => description.trim().length > 8 ? findDuplicate(description, selection?.selector, notes) : null,
|
|
10090
|
+
[description, selection, notes]
|
|
10091
|
+
);
|
|
9424
10092
|
const [severity, setSeverity] = useState("bug");
|
|
9425
10093
|
const [targetForensics, setTargetForensics] = useState(void 0);
|
|
9426
10094
|
const taRef = useRef(null);
|
|
@@ -9562,6 +10230,7 @@ function CaptureMode() {
|
|
|
9562
10230
|
if (regionRect) {
|
|
9563
10231
|
const sel2 = { kind: "region", rect: regionRect };
|
|
9564
10232
|
setTargetForensics(void 0);
|
|
10233
|
+
setOrigin(void 0);
|
|
9565
10234
|
if (coarse) {
|
|
9566
10235
|
setCandidate(sel2);
|
|
9567
10236
|
setPhase("confirming");
|
|
@@ -9584,6 +10253,7 @@ function CaptureMode() {
|
|
|
9584
10253
|
tagName: el.tagName.toLowerCase()
|
|
9585
10254
|
};
|
|
9586
10255
|
setTargetForensics(collectTargetForensics(el));
|
|
10256
|
+
setOrigin(resolveOrigin(el));
|
|
9587
10257
|
if (coarse) {
|
|
9588
10258
|
setCandidate(sel);
|
|
9589
10259
|
setHover({ rect: sel.rect, selector: sel.selector || "" });
|
|
@@ -9730,6 +10400,7 @@ function CaptureMode() {
|
|
|
9730
10400
|
setDescription("");
|
|
9731
10401
|
setSeverity("bug");
|
|
9732
10402
|
setTargetForensics(void 0);
|
|
10403
|
+
setOrigin(void 0);
|
|
9733
10404
|
setCaptureError(false);
|
|
9734
10405
|
}, []);
|
|
9735
10406
|
const save = async (keepGoing = false) => {
|
|
@@ -9750,8 +10421,10 @@ function CaptureMode() {
|
|
|
9750
10421
|
await addNote({
|
|
9751
10422
|
description,
|
|
9752
10423
|
screenshot: shot ?? void 0,
|
|
10424
|
+
shotEngine: shotEngine ?? void 0,
|
|
9753
10425
|
target,
|
|
9754
10426
|
severity,
|
|
10427
|
+
origin,
|
|
9755
10428
|
forensics: selection.kind === "element" ? targetForensics : void 0
|
|
9756
10429
|
});
|
|
9757
10430
|
if (keepGoing) {
|
|
@@ -10381,8 +11054,8 @@ function CaptureMode() {
|
|
|
10381
11054
|
]
|
|
10382
11055
|
}
|
|
10383
11056
|
),
|
|
10384
|
-
/* @__PURE__ */ jsx(LocationReveal, { target: selection }),
|
|
10385
|
-
/* @__PURE__ */ jsxs(
|
|
11057
|
+
developerMode && /* @__PURE__ */ jsx(LocationReveal, { target: selection }),
|
|
11058
|
+
developerMode && /* @__PURE__ */ jsxs(
|
|
10386
11059
|
"div",
|
|
10387
11060
|
{
|
|
10388
11061
|
role: "group",
|
|
@@ -10412,6 +11085,13 @@ function CaptureMode() {
|
|
|
10412
11085
|
]
|
|
10413
11086
|
}
|
|
10414
11087
|
),
|
|
11088
|
+
twin && /* @__PURE__ */ jsxs("p", { className: "qa-text-10 qa-text-warn", "data-qa-duplicate": "true", children: [
|
|
11089
|
+
t("dup_notice", { when: new Date(twin.timestamp).toLocaleTimeString() }),
|
|
11090
|
+
" \u201C",
|
|
11091
|
+
twin.description.slice(0, 70),
|
|
11092
|
+
twin.description.length > 70 ? "\u2026" : "",
|
|
11093
|
+
"\u201D"
|
|
11094
|
+
] }),
|
|
10415
11095
|
/* @__PURE__ */ jsx(
|
|
10416
11096
|
"textarea",
|
|
10417
11097
|
{
|
|
@@ -10430,6 +11110,10 @@ function CaptureMode() {
|
|
|
10430
11110
|
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"
|
|
10431
11111
|
}
|
|
10432
11112
|
),
|
|
11113
|
+
developerMode && origin && (origin.component || origin.file) && /* @__PURE__ */ jsxs("p", { className: "qa-text-10 qa-text-mid", "data-qa-origin": "true", children: [
|
|
11114
|
+
origin.component ?? "\u2014",
|
|
11115
|
+
origin.file ? ` \xB7 ${origin.file}${origin.line ? `:${origin.line}` : ""}` : ""
|
|
11116
|
+
] }),
|
|
10433
11117
|
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-2", children: [
|
|
10434
11118
|
/* @__PURE__ */ jsxs(
|
|
10435
11119
|
"button",
|
|
@@ -10471,8 +11155,7 @@ function CaptureMode() {
|
|
|
10471
11155
|
] }),
|
|
10472
11156
|
/* @__PURE__ */ jsxs("p", { className: "qa-text-center qa-text-10 qa-text-slate-400", children: [
|
|
10473
11157
|
t("save_hint"),
|
|
10474
|
-
|
|
10475
|
-
t("severity_keys")
|
|
11158
|
+
developerMode ? ` \xB7 ${t("severity_keys")}` : ""
|
|
10476
11159
|
] })
|
|
10477
11160
|
] })
|
|
10478
11161
|
]
|
|
@@ -10678,5 +11361,5 @@ function Qapture({ config }) {
|
|
|
10678
11361
|
}
|
|
10679
11362
|
|
|
10680
11363
|
export { Qapture, deleteQaDatabase, initQaStudio };
|
|
10681
|
-
//# sourceMappingURL=chunk-
|
|
10682
|
-
//# sourceMappingURL=chunk-
|
|
11364
|
+
//# sourceMappingURL=chunk-HEHVZCSV.js.map
|
|
11365
|
+
//# sourceMappingURL=chunk-HEHVZCSV.js.map
|