qapture2 0.7.0 → 0.7.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/README.md +43 -5
- package/dist/{chunk-QBBWY3BF.js → chunk-JSEFC2VP.js} +545 -40
- package/dist/{chunk-UOUFJCPF.cjs → chunk-WAOYPLZA.cjs} +545 -40
- package/dist/index.cjs +5 -5
- package/dist/index.js +1 -1
- package/dist/next.cjs +4 -4
- package/dist/next.js +1 -1
- package/dist/standalone.cjs +2 -2
- package/dist/standalone.js +1 -1
- package/package.json +2 -1
|
@@ -1511,6 +1511,128 @@ function matchRouteToSteps(journey, route) {
|
|
|
1511
1511
|
return [...exact, ...param];
|
|
1512
1512
|
}
|
|
1513
1513
|
|
|
1514
|
+
// src/lib/fallbackJourney.ts
|
|
1515
|
+
var ANY_PAGE = "*";
|
|
1516
|
+
var FALLBACK_JOURNEY = [
|
|
1517
|
+
{
|
|
1518
|
+
id: "first-run",
|
|
1519
|
+
role: { en: "First look", ar: "\u0627\u0644\u0646\u0638\u0631\u0629 \u0627\u0644\u0623\u0648\u0644\u0649" },
|
|
1520
|
+
steps: [
|
|
1521
|
+
{
|
|
1522
|
+
path: ANY_PAGE,
|
|
1523
|
+
what: { en: "Open the app fresh and let it finish loading", ar: "\u0627\u0641\u062A\u062D \u0627\u0644\u062A\u0637\u0628\u064A\u0642 \u0645\u0646 \u062C\u062F\u064A\u062F \u0648\u0627\u062A\u0631\u0643\u0647 \u064A\u0643\u0645\u0644 \u0627\u0644\u062A\u062D\u0645\u064A\u0644" },
|
|
1524
|
+
expect: {
|
|
1525
|
+
en: "Content appears, nothing flashes or jumps, no error in the corner",
|
|
1526
|
+
ar: "\u064A\u0638\u0647\u0631 \u0627\u0644\u0645\u062D\u062A\u0648\u0649 \u062F\u0648\u0646 \u0648\u0645\u064A\u0636 \u0623\u0648 \u0642\u0641\u0632\u060C \u0648\u0628\u062F\u0648\u0646 \u0631\u0633\u0627\u0644\u0629 \u062E\u0637\u0623"
|
|
1527
|
+
},
|
|
1528
|
+
risk: "red"
|
|
1529
|
+
},
|
|
1530
|
+
{
|
|
1531
|
+
path: ANY_PAGE,
|
|
1532
|
+
what: { en: "Read the first screen as a new user would", ar: "\u0627\u0642\u0631\u0623 \u0627\u0644\u0634\u0627\u0634\u0629 \u0627\u0644\u0623\u0648\u0644\u0649 \u0628\u0639\u064A\u0646 \u0645\u0633\u062A\u062E\u062F\u0645 \u062C\u062F\u064A\u062F" },
|
|
1533
|
+
expect: {
|
|
1534
|
+
en: "It is obvious what this is and what to do next",
|
|
1535
|
+
ar: "\u0648\u0627\u0636\u062D \u0645\u0627 \u0647\u0630\u0627 \u0648\u0645\u0627 \u0627\u0644\u062E\u0637\u0648\u0629 \u0627\u0644\u062A\u0627\u0644\u064A\u0629"
|
|
1536
|
+
},
|
|
1537
|
+
risk: "amber"
|
|
1538
|
+
}
|
|
1539
|
+
]
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
id: "moving-around",
|
|
1543
|
+
role: { en: "Moving around", ar: "\u0627\u0644\u062A\u0646\u0642\u0644" },
|
|
1544
|
+
steps: [
|
|
1545
|
+
{
|
|
1546
|
+
path: ANY_PAGE,
|
|
1547
|
+
what: { en: "Visit every item in the main menu", ar: "\u0627\u0641\u062A\u062D \u0643\u0644 \u0639\u0646\u0635\u0631 \u0641\u064A \u0627\u0644\u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0631\u0626\u064A\u0633\u064A\u0629" },
|
|
1548
|
+
expect: { en: "Each one opens the page it names \u2014 no blank screens", ar: "\u0643\u0644 \u0639\u0646\u0635\u0631 \u064A\u0641\u062A\u062D \u0635\u0641\u062D\u062A\u0647 \u2014 \u0628\u062F\u0648\u0646 \u0634\u0627\u0634\u0627\u062A \u0641\u0627\u0631\u063A\u0629" },
|
|
1549
|
+
risk: "red"
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
path: ANY_PAGE,
|
|
1553
|
+
what: { en: "Press the browser Back button after moving", ar: "\u0627\u0636\u063A\u0637 \u0632\u0631 \u0627\u0644\u0631\u062C\u0648\u0639 \u0641\u064A \u0627\u0644\u0645\u062A\u0635\u0641\u062D \u0628\u0639\u062F \u0627\u0644\u062A\u0646\u0642\u0644" },
|
|
1554
|
+
expect: { en: "You land where you were, with your place kept", ar: "\u062A\u0639\u0648\u062F \u0625\u0644\u0649 \u062D\u064A\u062B \u0643\u0646\u062A \u0645\u0639 \u0628\u0642\u0627\u0621 \u0645\u0648\u0636\u0639\u0643" },
|
|
1555
|
+
risk: "amber"
|
|
1556
|
+
},
|
|
1557
|
+
{
|
|
1558
|
+
path: ANY_PAGE,
|
|
1559
|
+
what: { en: "Reload the page you are on", ar: "\u0623\u0639\u062F \u062A\u062D\u0645\u064A\u0644 \u0627\u0644\u0635\u0641\u062D\u0629 \u0627\u0644\u062D\u0627\u0644\u064A\u0629" },
|
|
1560
|
+
expect: { en: "The same page comes back, still signed in", ar: "\u062A\u0639\u0648\u062F \u0627\u0644\u0635\u0641\u062D\u0629 \u0646\u0641\u0633\u0647\u0627 \u0648\u064A\u0628\u0642\u0649 \u062A\u0633\u062C\u064A\u0644 \u0627\u0644\u062F\u062E\u0648\u0644" },
|
|
1561
|
+
risk: "red"
|
|
1562
|
+
}
|
|
1563
|
+
]
|
|
1564
|
+
},
|
|
1565
|
+
{
|
|
1566
|
+
id: "the-main-thing",
|
|
1567
|
+
role: { en: "The main thing", ar: "\u0627\u0644\u0645\u0647\u0645\u0629 \u0627\u0644\u0623\u0633\u0627\u0633\u064A\u0629" },
|
|
1568
|
+
steps: [
|
|
1569
|
+
{
|
|
1570
|
+
path: ANY_PAGE,
|
|
1571
|
+
what: {
|
|
1572
|
+
en: "Do what this app is for, start to finish, without shortcuts",
|
|
1573
|
+
ar: "\u0646\u0641\u0651\u0630 \u0627\u0644\u063A\u0631\u0636 \u0627\u0644\u0623\u0633\u0627\u0633\u064A \u0645\u0646 \u0627\u0644\u062A\u0637\u0628\u064A\u0642 \u0645\u0646 \u0627\u0644\u0628\u062F\u0627\u064A\u0629 \u0644\u0644\u0646\u0647\u0627\u064A\u0629 \u062F\u0648\u0646 \u0627\u062E\u062A\u0635\u0627\u0631\u0627\u062A"
|
|
1574
|
+
},
|
|
1575
|
+
expect: { en: "It completes, and you are told clearly that it did", ar: "\u062A\u0643\u062A\u0645\u0644 \u0627\u0644\u0645\u0647\u0645\u0629 \u0648\u064A\u0635\u0644\u0643 \u062A\u0623\u0643\u064A\u062F \u0648\u0627\u0636\u062D" },
|
|
1576
|
+
risk: "red"
|
|
1577
|
+
},
|
|
1578
|
+
{
|
|
1579
|
+
path: ANY_PAGE,
|
|
1580
|
+
what: { en: "Do it a second time with different choices", ar: "\u0643\u0631\u0631\u0647\u0627 \u0645\u0631\u0629 \u062B\u0627\u0646\u064A\u0629 \u0628\u062E\u064A\u0627\u0631\u0627\u062A \u0645\u062E\u062A\u0644\u0641\u0629" },
|
|
1581
|
+
expect: { en: "Same result, nothing left over from the first run", ar: "\u0627\u0644\u0646\u062A\u064A\u062C\u0629 \u0646\u0641\u0633\u0647\u0627\u060C \u062F\u0648\u0646 \u0628\u0642\u0627\u064A\u0627 \u0645\u0646 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629 \u0627\u0644\u0623\u0648\u0644\u0649" },
|
|
1582
|
+
risk: "amber"
|
|
1583
|
+
}
|
|
1584
|
+
]
|
|
1585
|
+
},
|
|
1586
|
+
{
|
|
1587
|
+
id: "when-it-goes-wrong",
|
|
1588
|
+
role: { en: "When it goes wrong", ar: "\u0639\u0646\u062F \u0627\u0644\u062E\u0637\u0623" },
|
|
1589
|
+
steps: [
|
|
1590
|
+
{
|
|
1591
|
+
path: ANY_PAGE,
|
|
1592
|
+
what: { en: "Submit a form empty, then with obviously wrong values", ar: "\u0623\u0631\u0633\u0644 \u0627\u0644\u0646\u0645\u0648\u0630\u062C \u0641\u0627\u0631\u063A\u064B\u0627 \u062B\u0645 \u0628\u0642\u064A\u0645 \u062E\u0627\u0637\u0626\u0629 \u0628\u0648\u0636\u0648\u062D" },
|
|
1593
|
+
expect: {
|
|
1594
|
+
en: "It refuses politely and says which field and why \u2014 it does not crash or save",
|
|
1595
|
+
ar: "\u064A\u0631\u0641\u0636 \u0628\u0644\u0637\u0641 \u0648\u064A\u0648\u0636\u062D \u0627\u0644\u062D\u0642\u0644 \u0648\u0627\u0644\u0633\u0628\u0628 \u2014 \u062F\u0648\u0646 \u062A\u0639\u0637\u0644 \u0623\u0648 \u062D\u0641\u0638"
|
|
1596
|
+
},
|
|
1597
|
+
risk: "red"
|
|
1598
|
+
},
|
|
1599
|
+
{
|
|
1600
|
+
path: ANY_PAGE,
|
|
1601
|
+
what: { en: "Press the main button twice, quickly", ar: "\u0627\u0636\u063A\u0637 \u0627\u0644\u0632\u0631 \u0627\u0644\u0631\u0626\u064A\u0633\u064A \u0645\u0631\u062A\u064A\u0646 \u0628\u0633\u0631\u0639\u0629" },
|
|
1602
|
+
expect: { en: "One result, not two", ar: "\u0646\u062A\u064A\u062C\u0629 \u0648\u0627\u062D\u062F\u0629 \u0644\u0627 \u0627\u062B\u0646\u062A\u0627\u0646" },
|
|
1603
|
+
risk: "amber"
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
path: ANY_PAGE,
|
|
1607
|
+
what: { en: "Open a page you should not be allowed to see", ar: "\u0627\u0641\u062A\u062D \u0635\u0641\u062D\u0629 \u0644\u0627 \u064A\u064F\u0641\u062A\u0631\u0636 \u0623\u0646 \u062A\u0635\u0644 \u0625\u0644\u064A\u0647\u0627" },
|
|
1608
|
+
expect: { en: "You are turned away, not shown someone else\u2019s data", ar: "\u064A\u062A\u0645 \u0645\u0646\u0639\u0643\u060C \u0648\u0644\u0627 \u062A\u0638\u0647\u0631 \u0628\u064A\u0627\u0646\u0627\u062A \u0634\u062E\u0635 \u0622\u062E\u0631" },
|
|
1609
|
+
risk: "red"
|
|
1610
|
+
}
|
|
1611
|
+
]
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
id: "on-a-phone",
|
|
1615
|
+
role: { en: "On a phone", ar: "\u0639\u0644\u0649 \u0627\u0644\u0647\u0627\u062A\u0641" },
|
|
1616
|
+
steps: [
|
|
1617
|
+
{
|
|
1618
|
+
path: ANY_PAGE,
|
|
1619
|
+
what: { en: "Make the window narrow, or open it on a phone", ar: "\u0635\u063A\u0651\u0631 \u0627\u0644\u0646\u0627\u0641\u0630\u0629 \u0623\u0648 \u0627\u0641\u062A\u062D\u0647 \u0639\u0644\u0649 \u0627\u0644\u0647\u0627\u062A\u0641" },
|
|
1620
|
+
expect: {
|
|
1621
|
+
en: "Nothing is cut off, nothing overlaps, no sideways scrolling",
|
|
1622
|
+
ar: "\u0644\u0627 \u0634\u064A\u0621 \u0645\u0642\u0635\u0648\u0635 \u0623\u0648 \u0645\u062A\u062F\u0627\u062E\u0644\u060C \u0648\u0628\u062F\u0648\u0646 \u062A\u0645\u0631\u064A\u0631 \u062C\u0627\u0646\u0628\u064A"
|
|
1623
|
+
},
|
|
1624
|
+
risk: "amber"
|
|
1625
|
+
},
|
|
1626
|
+
{
|
|
1627
|
+
path: ANY_PAGE,
|
|
1628
|
+
what: { en: "Tap the small controls with a thumb", ar: "\u0627\u0636\u063A\u0637 \u0627\u0644\u0639\u0646\u0627\u0635\u0631 \u0627\u0644\u0635\u063A\u064A\u0631\u0629 \u0628\u0625\u0628\u0647\u0627\u0645\u0643" },
|
|
1629
|
+
expect: { en: "They are big enough to hit first time", ar: "\u0643\u0628\u064A\u0631\u0629 \u0628\u0645\u0627 \u064A\u0643\u0641\u064A \u0644\u0644\u0636\u063A\u0637 \u0645\u0646 \u0623\u0648\u0644 \u0645\u0631\u0629" },
|
|
1630
|
+
risk: "green"
|
|
1631
|
+
}
|
|
1632
|
+
]
|
|
1633
|
+
}
|
|
1634
|
+
];
|
|
1635
|
+
|
|
1514
1636
|
// src/lib/storage.ts
|
|
1515
1637
|
function isStorageAvailable() {
|
|
1516
1638
|
if (typeof window === "undefined") return false;
|
|
@@ -1613,6 +1735,7 @@ var STR = {
|
|
|
1613
1735
|
loc_hide: "Hide location",
|
|
1614
1736
|
loc_locate: "Locate on page",
|
|
1615
1737
|
journey_title: "Testing journey",
|
|
1738
|
+
journey_generic: "No test plan was set up for this project, so this is a general one that fits any app. Work through it, or ask whoever set up Qapture to add the real journey to qa.config.",
|
|
1616
1739
|
export_name_title: "Name your export",
|
|
1617
1740
|
export_name_placeholder: "file name",
|
|
1618
1741
|
tap_element: "Tap an element",
|
|
@@ -1676,6 +1799,12 @@ var STR = {
|
|
|
1676
1799
|
sync_lost_permission: "Folder access expired \u2014 click Reconnect to keep saving",
|
|
1677
1800
|
sync_write_failed: "Couldn't write to the folder \u2014 notes are still saved in this browser",
|
|
1678
1801
|
sync_unsupported: "Folder saving needs Chrome, Edge or Brave on desktop. Use Export here.",
|
|
1802
|
+
sync_zip_mode: "This browser cannot write into a folder directly, so Qapture builds the folder for you: name the project and campaign, and you get a zip that unzips into exactly that structure. It refreshes itself every few points.",
|
|
1803
|
+
sync_zip_start: "Start saving",
|
|
1804
|
+
sync_zip_save_now: "Save folder now",
|
|
1805
|
+
sync_zip_saved: "Saved {name} to your downloads",
|
|
1806
|
+
sync_zip_failed: "Couldn't build the folder \u2014 your notes are still safe in this browser",
|
|
1807
|
+
sync_zip_where: "Unzip into your QA folder:",
|
|
1679
1808
|
sync_writing_to: "Writing to",
|
|
1680
1809
|
storage_title: "Storage",
|
|
1681
1810
|
storage_used: "{used} of {quota} used",
|
|
@@ -1800,6 +1929,7 @@ var STR = {
|
|
|
1800
1929
|
loc_hide: "\u0625\u062E\u0641\u0627\u0621 \u0627\u0644\u0645\u0648\u0642\u0639",
|
|
1801
1930
|
loc_locate: "\u0625\u0638\u0647\u0627\u0631 \u0639\u0644\u0649 \u0627\u0644\u0635\u0641\u062D\u0629",
|
|
1802
1931
|
journey_title: "\u0631\u062D\u0644\u0629 \u0627\u0644\u0627\u062E\u062A\u0628\u0627\u0631",
|
|
1932
|
+
journey_generic: "\u0644\u0645 \u064A\u062A\u0645 \u0625\u0639\u062F\u0627\u062F \u062E\u0637\u0629 \u0627\u062E\u062A\u0628\u0627\u0631 \u0644\u0647\u0630\u0627 \u0627\u0644\u0645\u0634\u0631\u0648\u0639\u060C \u0644\u0630\u0644\u0643 \u0647\u0630\u0647 \u062E\u0637\u0629 \u0639\u0627\u0645\u0629 \u062A\u0635\u0644\u062D \u0644\u0623\u064A \u062A\u0637\u0628\u064A\u0642. \u0627\u0639\u0645\u0644 \u0628\u0647\u0627\u060C \u0623\u0648 \u0627\u0637\u0644\u0628 \u0645\u0645\u0646 \u062B\u0628\u0651\u062A Qapture \u0623\u0646 \u064A\u0636\u064A\u0641 \u0631\u062D\u0644\u0629 \u0627\u0644\u0645\u0634\u0631\u0648\u0639 \u0641\u064A qa.config.",
|
|
1803
1933
|
export_name_title: "\u0633\u0645\u0650\u0651 \u0645\u0644\u0641 \u0627\u0644\u062A\u0635\u062F\u064A\u0631",
|
|
1804
1934
|
export_name_placeholder: "\u0627\u0633\u0645 \u0627\u0644\u0645\u0644\u0641",
|
|
1805
1935
|
tap_element: "\u0627\u0646\u0642\u0631 \u0639\u0644\u0649 \u0639\u0646\u0635\u0631",
|
|
@@ -1863,6 +1993,12 @@ var STR = {
|
|
|
1863
1993
|
sync_lost_permission: "\u0627\u0646\u062A\u0647\u062A \u0635\u0644\u0627\u062D\u064A\u0629 \u0627\u0644\u0648\u0635\u0648\u0644 \u0644\u0644\u0645\u062C\u0644\u062F \u2014 \u0627\u0636\u063A\u0637 \u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0631\u0628\u0637 \u0644\u0644\u0645\u062A\u0627\u0628\u0639\u0629",
|
|
1864
1994
|
sync_write_failed: "\u062A\u0639\u0630\u0651\u0631\u062A \u0627\u0644\u0643\u062A\u0627\u0628\u0629 \u0641\u064A \u0627\u0644\u0645\u062C\u0644\u062F \u2014 \u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0627\u062A \u0645\u062D\u0641\u0648\u0638\u0629 \u0641\u064A \u0627\u0644\u0645\u062A\u0635\u0641\u062D",
|
|
1865
1995
|
sync_unsupported: "\u0627\u0644\u062D\u0641\u0638 \u0641\u064A \u0645\u062C\u0644\u062F \u064A\u062A\u0637\u0644\u0628 Chrome \u0623\u0648 Edge \u0623\u0648 Brave \u0639\u0644\u0649 \u0633\u0637\u062D \u0627\u0644\u0645\u0643\u062A\u0628. \u0627\u0633\u062A\u062E\u062F\u0645 \u0627\u0644\u062A\u0635\u062F\u064A\u0631 \u0647\u0646\u0627.",
|
|
1996
|
+
sync_zip_mode: "\u0647\u0630\u0627 \u0627\u0644\u0645\u062A\u0635\u0641\u062D \u0644\u0627 \u064A\u0633\u062A\u0637\u064A\u0639 \u0627\u0644\u0643\u062A\u0627\u0628\u0629 \u062F\u0627\u062E\u0644 \u0645\u062C\u0644\u062F \u0645\u0628\u0627\u0634\u0631\u0629\u060C \u0644\u0630\u0644\u0643 \u064A\u0628\u0646\u064A Qapture \u0627\u0644\u0645\u062C\u0644\u062F \u0646\u064A\u0627\u0628\u0629\u064B \u0639\u0646\u0643: \u0627\u0643\u062A\u0628 \u0627\u0633\u0645 \u0627\u0644\u0645\u0634\u0631\u0648\u0639 \u0648\u0627\u0644\u062D\u0645\u0644\u0629\u060C \u0648\u0633\u062A\u062D\u0635\u0644 \u0639\u0644\u0649 \u0645\u0644\u0641 \u0645\u0636\u063A\u0648\u0637 \u064A\u064F\u0641\u064E\u0643 \u0625\u0644\u0649 \u0627\u0644\u0628\u0646\u064A\u0629 \u0646\u0641\u0633\u0647\u0627 \u062A\u0645\u0627\u0645\u064B\u0627. \u0648\u064A\u062A\u062D\u062F\u0651\u062B \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627 \u0643\u0644 \u0628\u0636\u0639 \u0646\u0642\u0627\u0637.",
|
|
1997
|
+
sync_zip_start: "\u0627\u0628\u062F\u0623 \u0627\u0644\u062D\u0641\u0638",
|
|
1998
|
+
sync_zip_save_now: "\u0627\u062D\u0641\u0638 \u0627\u0644\u0645\u062C\u0644\u062F \u0627\u0644\u0622\u0646",
|
|
1999
|
+
sync_zip_saved: "\u062A\u0645 \u062D\u0641\u0638 {name} \u0641\u064A \u0627\u0644\u062A\u0646\u0632\u064A\u0644\u0627\u062A",
|
|
2000
|
+
sync_zip_failed: "\u062A\u0639\u0630\u0651\u0631 \u0628\u0646\u0627\u0621 \u0627\u0644\u0645\u062C\u0644\u062F \u2014 \u0645\u0644\u0627\u062D\u0638\u0627\u062A\u0643 \u0645\u0627 \u0632\u0627\u0644\u062A \u0645\u062D\u0641\u0648\u0638\u0629 \u0641\u064A \u0647\u0630\u0627 \u0627\u0644\u0645\u062A\u0635\u0641\u062D",
|
|
2001
|
+
sync_zip_where: "\u0641\u064F\u0643\u0651 \u0627\u0644\u0636\u063A\u0637 \u062F\u0627\u062E\u0644 \u0645\u062C\u0644\u062F QA \u0644\u062F\u064A\u0643:",
|
|
1866
2002
|
sync_writing_to: "\u0627\u0644\u0643\u062A\u0627\u0628\u0629 \u0625\u0644\u0649",
|
|
1867
2003
|
storage_title: "\u0627\u0644\u062A\u062E\u0632\u064A\u0646",
|
|
1868
2004
|
storage_used: "{used} \u0645\u0646 {quota} \u0645\u0633\u062A\u062E\u062F\u064E\u0645\u0629",
|
|
@@ -2249,6 +2385,145 @@ async function grabExactRegion(rect) {
|
|
|
2249
2385
|
}
|
|
2250
2386
|
}
|
|
2251
2387
|
|
|
2388
|
+
// src/lib/cssColors.ts
|
|
2389
|
+
var UNSUPPORTED_FN = /\b(?:oklch|oklab|lch|lab|color-mix|color)\(/i;
|
|
2390
|
+
function unsupportedFnScanner() {
|
|
2391
|
+
return /\b(?:oklch|oklab|lch|lab|color-mix|color)\(/gi;
|
|
2392
|
+
}
|
|
2393
|
+
var COLOR_PROPS = [
|
|
2394
|
+
"color",
|
|
2395
|
+
"backgroundColor",
|
|
2396
|
+
"borderTopColor",
|
|
2397
|
+
"borderRightColor",
|
|
2398
|
+
"borderBottomColor",
|
|
2399
|
+
"borderLeftColor",
|
|
2400
|
+
"outlineColor",
|
|
2401
|
+
"textDecorationColor",
|
|
2402
|
+
"columnRuleColor",
|
|
2403
|
+
"caretColor",
|
|
2404
|
+
"fill",
|
|
2405
|
+
"stroke"
|
|
2406
|
+
];
|
|
2407
|
+
var COMPOUND_PROPS = ["backgroundImage", "boxShadow", "textShadow"];
|
|
2408
|
+
var probeCtx;
|
|
2409
|
+
function getProbe() {
|
|
2410
|
+
if (probeCtx !== void 0) return probeCtx;
|
|
2411
|
+
try {
|
|
2412
|
+
const canvas = document.createElement("canvas");
|
|
2413
|
+
canvas.width = canvas.height = 1;
|
|
2414
|
+
probeCtx = canvas.getContext("2d", { willReadFrequently: true });
|
|
2415
|
+
} catch {
|
|
2416
|
+
probeCtx = null;
|
|
2417
|
+
}
|
|
2418
|
+
return probeCtx;
|
|
2419
|
+
}
|
|
2420
|
+
var conversionCache = /* @__PURE__ */ new Map();
|
|
2421
|
+
function toRenderableColor(value) {
|
|
2422
|
+
const ctx = getProbe();
|
|
2423
|
+
if (!ctx || !value) return null;
|
|
2424
|
+
const cached = conversionCache.get(value);
|
|
2425
|
+
if (cached !== void 0) return cached;
|
|
2426
|
+
const result = convertUncached(ctx, value);
|
|
2427
|
+
conversionCache.set(value, result);
|
|
2428
|
+
return result;
|
|
2429
|
+
}
|
|
2430
|
+
function convertUncached(ctx, value) {
|
|
2431
|
+
try {
|
|
2432
|
+
ctx.fillStyle = "#000000";
|
|
2433
|
+
ctx.fillStyle = value;
|
|
2434
|
+
const fromBlack = String(ctx.fillStyle);
|
|
2435
|
+
ctx.fillStyle = "#ffffff";
|
|
2436
|
+
ctx.fillStyle = value;
|
|
2437
|
+
if (fromBlack !== String(ctx.fillStyle)) return null;
|
|
2438
|
+
if (!UNSUPPORTED_FN.test(fromBlack)) return fromBlack;
|
|
2439
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
2440
|
+
ctx.fillStyle = value;
|
|
2441
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
2442
|
+
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
|
|
2443
|
+
return a === 255 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(3)})`;
|
|
2444
|
+
} catch {
|
|
2445
|
+
return null;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
function neutralizeColorFunctions(value) {
|
|
2449
|
+
if (!value || !UNSUPPORTED_FN.test(value)) return value;
|
|
2450
|
+
const scanner = unsupportedFnScanner();
|
|
2451
|
+
let out = "";
|
|
2452
|
+
let last = 0;
|
|
2453
|
+
let match;
|
|
2454
|
+
while ((match = scanner.exec(value)) !== null) {
|
|
2455
|
+
const start = match.index;
|
|
2456
|
+
if (start < last) continue;
|
|
2457
|
+
let depth = 0;
|
|
2458
|
+
let i = start + match[0].length - 1;
|
|
2459
|
+
for (; i < value.length; i++) {
|
|
2460
|
+
if (value[i] === "(") depth++;
|
|
2461
|
+
else if (value[i] === ")") {
|
|
2462
|
+
depth--;
|
|
2463
|
+
if (depth === 0) break;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
if (i >= value.length) break;
|
|
2467
|
+
const converted = toRenderableColor(value.slice(start, i + 1));
|
|
2468
|
+
out += value.slice(last, start) + (converted ?? "rgba(0, 0, 0, 0)");
|
|
2469
|
+
last = i + 1;
|
|
2470
|
+
scanner.lastIndex = last;
|
|
2471
|
+
}
|
|
2472
|
+
return out + value.slice(last);
|
|
2473
|
+
}
|
|
2474
|
+
function hasUnsupportedColor(value) {
|
|
2475
|
+
return !!value && UNSUPPORTED_FN.test(value);
|
|
2476
|
+
}
|
|
2477
|
+
function safeBackgroundColor(value, fallback = "#ffffff") {
|
|
2478
|
+
if (!hasUnsupportedColor(value)) return value;
|
|
2479
|
+
return toRenderableColor(value) ?? fallback;
|
|
2480
|
+
}
|
|
2481
|
+
function neutralizeDocumentColors(doc, aggressive = false) {
|
|
2482
|
+
const view = doc.defaultView;
|
|
2483
|
+
if (!view || typeof view.getComputedStyle !== "function") return 0;
|
|
2484
|
+
let touched = 0;
|
|
2485
|
+
const all = doc.querySelectorAll("*");
|
|
2486
|
+
const elements = [doc.documentElement, ...Array.from(all)];
|
|
2487
|
+
for (const el of elements) {
|
|
2488
|
+
if (!el || !el.style) continue;
|
|
2489
|
+
let changed = false;
|
|
2490
|
+
let computed;
|
|
2491
|
+
try {
|
|
2492
|
+
computed = view.getComputedStyle(el);
|
|
2493
|
+
} catch {
|
|
2494
|
+
continue;
|
|
2495
|
+
}
|
|
2496
|
+
for (const prop of COLOR_PROPS) {
|
|
2497
|
+
const value = computed[prop] ?? computed[prop];
|
|
2498
|
+
if (!hasUnsupportedColor(value)) continue;
|
|
2499
|
+
const converted = toRenderableColor(value);
|
|
2500
|
+
if (converted) {
|
|
2501
|
+
el.style[prop] = converted;
|
|
2502
|
+
changed = true;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
for (const prop of COMPOUND_PROPS) {
|
|
2506
|
+
const value = computed[prop];
|
|
2507
|
+
if (!value || value === "none") continue;
|
|
2508
|
+
if (aggressive && prop !== "backgroundImage") {
|
|
2509
|
+
el.style[prop] = "none";
|
|
2510
|
+
changed = true;
|
|
2511
|
+
continue;
|
|
2512
|
+
}
|
|
2513
|
+
if (!hasUnsupportedColor(value)) continue;
|
|
2514
|
+
if (aggressive) {
|
|
2515
|
+
el.style[prop] = "none";
|
|
2516
|
+
changed = true;
|
|
2517
|
+
continue;
|
|
2518
|
+
}
|
|
2519
|
+
el.style[prop] = neutralizeColorFunctions(value);
|
|
2520
|
+
changed = true;
|
|
2521
|
+
}
|
|
2522
|
+
if (changed) touched++;
|
|
2523
|
+
}
|
|
2524
|
+
return touched;
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2252
2527
|
// src/lib/capture.ts
|
|
2253
2528
|
var HTML2CANVAS_TIMEOUT_MS = 1e4;
|
|
2254
2529
|
var FALLBACK_PAGE_BACKGROUND = "#ffffff";
|
|
@@ -2420,7 +2695,7 @@ function cropCanvas(full, rect, scale) {
|
|
|
2420
2695
|
ctx.drawImage(full, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
2421
2696
|
return out;
|
|
2422
2697
|
}
|
|
2423
|
-
async function captureViaDom(rect, sx, sy) {
|
|
2698
|
+
async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
2424
2699
|
const { default: html2canvas } = await import('html2canvas');
|
|
2425
2700
|
const scale = Math.min(window.devicePixelRatio || 1, 2);
|
|
2426
2701
|
const vw = document.documentElement.clientWidth || window.innerWidth;
|
|
@@ -2438,14 +2713,19 @@ async function captureViaDom(rect, sx, sy) {
|
|
|
2438
2713
|
useCORS: true,
|
|
2439
2714
|
allowTaint: true,
|
|
2440
2715
|
// The page's own background, never null — see resolvePageBackground().
|
|
2441
|
-
|
|
2716
|
+
// Passed straight to html2canvas's parser, so it gets the same
|
|
2717
|
+
// CSS Color 4 treatment the document walk applies.
|
|
2718
|
+
backgroundColor: safeBackgroundColor(resolvePageBackground(), FALLBACK_PAGE_BACKGROUND),
|
|
2442
2719
|
logging: false,
|
|
2443
2720
|
scrollX: sx,
|
|
2444
2721
|
scrollY: sy,
|
|
2445
2722
|
windowWidth: vw,
|
|
2446
2723
|
windowHeight: vh,
|
|
2447
2724
|
ignoreElements: (el) => el.nodeType === 1 && typeof el.hasAttribute === "function" && el.hasAttribute("data-qa-overlay"),
|
|
2448
|
-
onclone: (doc) =>
|
|
2725
|
+
onclone: (doc) => {
|
|
2726
|
+
neutralizeDocumentColors(doc, aggressiveColors);
|
|
2727
|
+
pinStuckClones(doc);
|
|
2728
|
+
}
|
|
2449
2729
|
}),
|
|
2450
2730
|
HTML2CANVAS_TIMEOUT_MS
|
|
2451
2731
|
);
|
|
@@ -2480,8 +2760,16 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
2480
2760
|
const blob = await encodeShot(canvas);
|
|
2481
2761
|
return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
|
|
2482
2762
|
} catch (err) {
|
|
2483
|
-
console.warn("[QA] region capture failed:", err);
|
|
2484
|
-
|
|
2763
|
+
console.warn("[QA] region capture failed, retrying without gradients/shadows:", err);
|
|
2764
|
+
try {
|
|
2765
|
+
const canvas = await captureViaDom(rect, sx, sy, true);
|
|
2766
|
+
if (!canvas) return { status: "failed" };
|
|
2767
|
+
const blob = await encodeShot(canvas);
|
|
2768
|
+
return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
|
|
2769
|
+
} catch (retryErr) {
|
|
2770
|
+
console.warn("[QA] region capture failed after retry:", retryErr);
|
|
2771
|
+
return { status: "failed" };
|
|
2772
|
+
}
|
|
2485
2773
|
}
|
|
2486
2774
|
}
|
|
2487
2775
|
|
|
@@ -2904,6 +3192,7 @@ async function shareZipFile(blob, filename, title) {
|
|
|
2904
3192
|
// src/lib/fsSync.ts
|
|
2905
3193
|
var MAX_SLUG_LENGTH = 48;
|
|
2906
3194
|
var FS_ROOT_META_KEY = "fsSyncRoot";
|
|
3195
|
+
var FS_CAMPAIGN_META_KEY = "fsSyncCampaign";
|
|
2907
3196
|
var root = null;
|
|
2908
3197
|
var campaignDir = null;
|
|
2909
3198
|
var notesDir = null;
|
|
@@ -2912,6 +3201,7 @@ var campaign = null;
|
|
|
2912
3201
|
var noteIndex = {};
|
|
2913
3202
|
var state = "unsupported";
|
|
2914
3203
|
var lastError = "";
|
|
3204
|
+
var metaStore = null;
|
|
2915
3205
|
var bulkWriting = false;
|
|
2916
3206
|
var listeners = /* @__PURE__ */ new Set();
|
|
2917
3207
|
function setState(next, error = "") {
|
|
@@ -2934,8 +3224,20 @@ function onFsSyncChange(fn) {
|
|
|
2934
3224
|
function isFsSyncSupported() {
|
|
2935
3225
|
return typeof window !== "undefined" && typeof window.showDirectoryPicker === "function";
|
|
2936
3226
|
}
|
|
3227
|
+
function isFsSyncAvailable() {
|
|
3228
|
+
return typeof window !== "undefined";
|
|
3229
|
+
}
|
|
3230
|
+
function getFsSyncEngine() {
|
|
3231
|
+
if (isFsSyncSupported() || typeof document === "undefined") return "disk";
|
|
3232
|
+
return "download";
|
|
3233
|
+
}
|
|
2937
3234
|
function getFsSyncState() {
|
|
2938
|
-
if (!
|
|
3235
|
+
if (!isFsSyncAvailable()) return "unsupported";
|
|
3236
|
+
if (!isFsSyncSupported()) {
|
|
3237
|
+
if (state === "syncing") return "syncing";
|
|
3238
|
+
if (state === "error") return "error";
|
|
3239
|
+
return "connected";
|
|
3240
|
+
}
|
|
2939
3241
|
return state === "unsupported" ? "off" : state;
|
|
2940
3242
|
}
|
|
2941
3243
|
function getFsSyncError() {
|
|
@@ -2945,9 +3247,18 @@ function getFsSyncCampaign() {
|
|
|
2945
3247
|
return campaign;
|
|
2946
3248
|
}
|
|
2947
3249
|
function getFsSyncPath() {
|
|
3250
|
+
if (getFsSyncEngine() === "download") return campaign ? campaignRelativePath() : "";
|
|
2948
3251
|
if (!root) return "";
|
|
2949
3252
|
if (!campaign) return root.name;
|
|
2950
|
-
return `${root.name}/${
|
|
3253
|
+
return `${root.name}/${campaignRelativePath()}`;
|
|
3254
|
+
}
|
|
3255
|
+
function campaignRelativePath() {
|
|
3256
|
+
if (!campaign) return "";
|
|
3257
|
+
return `${safeSegment(campaign.project)}/${safeSegment(campaign.campaign)}`;
|
|
3258
|
+
}
|
|
3259
|
+
function campaignZipFileName() {
|
|
3260
|
+
if (!campaign) return "qa-campaign.zip";
|
|
3261
|
+
return `${safeSegment(campaign.project)} - ${safeSegment(campaign.campaign)}.zip`;
|
|
2951
3262
|
}
|
|
2952
3263
|
function safeSegment(input, fallback = "untitled") {
|
|
2953
3264
|
let s = (input ?? "").normalize("NFC").replace(/[\\/:*?"<>|\u0000-\u001F]/g, "-");
|
|
@@ -2989,10 +3300,25 @@ async function permissionFor(handle, request) {
|
|
|
2989
3300
|
return "granted";
|
|
2990
3301
|
}
|
|
2991
3302
|
async function restoreFsSync(store) {
|
|
2992
|
-
|
|
3303
|
+
metaStore = store;
|
|
3304
|
+
if (!isFsSyncAvailable()) {
|
|
2993
3305
|
setState("unsupported");
|
|
2994
3306
|
return "unsupported";
|
|
2995
3307
|
}
|
|
3308
|
+
if (getFsSyncEngine() === "download") {
|
|
3309
|
+
try {
|
|
3310
|
+
const saved = await store.getMeta(FS_CAMPAIGN_META_KEY);
|
|
3311
|
+
if (saved?.campaign?.project && saved.active !== false) {
|
|
3312
|
+
campaign = saved.campaign;
|
|
3313
|
+
noteIndex = saved.notes ?? {};
|
|
3314
|
+
setState("syncing");
|
|
3315
|
+
return "syncing";
|
|
3316
|
+
}
|
|
3317
|
+
} catch {
|
|
3318
|
+
}
|
|
3319
|
+
setState("connected");
|
|
3320
|
+
return "connected";
|
|
3321
|
+
}
|
|
2996
3322
|
try {
|
|
2997
3323
|
const saved = await store.getMeta(FS_ROOT_META_KEY);
|
|
2998
3324
|
if (!saved || typeof saved.getDirectoryHandle !== "function") {
|
|
@@ -3009,7 +3335,11 @@ async function restoreFsSync(store) {
|
|
|
3009
3335
|
}
|
|
3010
3336
|
}
|
|
3011
3337
|
async function chooseFsSyncRoot(store) {
|
|
3012
|
-
|
|
3338
|
+
metaStore = store;
|
|
3339
|
+
if (!isFsSyncSupported()) {
|
|
3340
|
+
setState("connected");
|
|
3341
|
+
return true;
|
|
3342
|
+
}
|
|
3013
3343
|
const picker = window.showDirectoryPicker;
|
|
3014
3344
|
try {
|
|
3015
3345
|
const handle = await picker({ id: "qapture-qa", mode: "readwrite", startIn: "documents" });
|
|
@@ -3057,6 +3387,30 @@ function describeError(err) {
|
|
|
3057
3387
|
return e?.message || "write-failed";
|
|
3058
3388
|
}
|
|
3059
3389
|
async function openFsCampaign(input) {
|
|
3390
|
+
if (getFsSyncEngine() === "download") {
|
|
3391
|
+
const sameAsBefore = campaign?.project === input.project && campaign?.campaign === input.campaign;
|
|
3392
|
+
let startedAt = sameAsBefore && campaign ? campaign.startedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
3393
|
+
if (!sameAsBefore) {
|
|
3394
|
+
noteIndex = {};
|
|
3395
|
+
try {
|
|
3396
|
+
const saved = await metaStore?.getMeta(FS_CAMPAIGN_META_KEY);
|
|
3397
|
+
if (saved?.campaign?.project === input.project && saved.campaign.campaign === input.campaign) {
|
|
3398
|
+
noteIndex = saved.notes ?? {};
|
|
3399
|
+
startedAt = saved.campaign.startedAt || startedAt;
|
|
3400
|
+
}
|
|
3401
|
+
} catch {
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
campaign = {
|
|
3405
|
+
project: input.project,
|
|
3406
|
+
campaign: input.campaign,
|
|
3407
|
+
tester: input.tester,
|
|
3408
|
+
startedAt
|
|
3409
|
+
};
|
|
3410
|
+
await persistDownloadCampaign();
|
|
3411
|
+
setState("syncing");
|
|
3412
|
+
return true;
|
|
3413
|
+
}
|
|
3060
3414
|
if (!root) return false;
|
|
3061
3415
|
try {
|
|
3062
3416
|
const perm = await permissionFor(root, false);
|
|
@@ -3095,11 +3449,23 @@ async function openFsCampaign(input) {
|
|
|
3095
3449
|
}
|
|
3096
3450
|
}
|
|
3097
3451
|
function closeFsCampaign() {
|
|
3452
|
+
if (getFsSyncEngine() === "download") void persistDownloadCampaign(false);
|
|
3098
3453
|
campaignDir = notesDir = shotsDir = null;
|
|
3099
3454
|
campaign = null;
|
|
3100
3455
|
noteIndex = {};
|
|
3456
|
+
if (getFsSyncEngine() === "download") {
|
|
3457
|
+
setState("connected");
|
|
3458
|
+
return;
|
|
3459
|
+
}
|
|
3101
3460
|
if (root) setState("connected");
|
|
3102
3461
|
}
|
|
3462
|
+
async function persistDownloadCampaign(active = true) {
|
|
3463
|
+
if (!metaStore || !campaign) return;
|
|
3464
|
+
try {
|
|
3465
|
+
await metaStore.setMeta(FS_CAMPAIGN_META_KEY, { campaign, notes: noteIndex, active });
|
|
3466
|
+
} catch {
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3103
3469
|
async function writeFile(dir, name, data) {
|
|
3104
3470
|
const handle = await dir.getFileHandle(name, { create: true });
|
|
3105
3471
|
const writable = await handle.createWritable();
|
|
@@ -3130,6 +3496,13 @@ function entryFor(note) {
|
|
|
3130
3496
|
};
|
|
3131
3497
|
}
|
|
3132
3498
|
async function syncNoteToDisk(note, allNotes) {
|
|
3499
|
+
if (getFsSyncEngine() === "download") {
|
|
3500
|
+
if (!campaign) return false;
|
|
3501
|
+
noteIndex[note.id] = entryFor(note);
|
|
3502
|
+
await persistDownloadCampaign();
|
|
3503
|
+
setState("syncing");
|
|
3504
|
+
return true;
|
|
3505
|
+
}
|
|
3133
3506
|
if (!campaignDir || !notesDir || !shotsDir || !campaign) return false;
|
|
3134
3507
|
try {
|
|
3135
3508
|
const previous = noteIndex[note.id];
|
|
@@ -3157,6 +3530,12 @@ async function syncNoteToDisk(note, allNotes) {
|
|
|
3157
3530
|
}
|
|
3158
3531
|
}
|
|
3159
3532
|
async function removeNoteFromDisk(noteId, allNotes) {
|
|
3533
|
+
if (getFsSyncEngine() === "download") {
|
|
3534
|
+
if (!noteIndex[noteId]) return true;
|
|
3535
|
+
delete noteIndex[noteId];
|
|
3536
|
+
await persistDownloadCampaign();
|
|
3537
|
+
return true;
|
|
3538
|
+
}
|
|
3160
3539
|
if (!campaignDir || !notesDir || !shotsDir) return false;
|
|
3161
3540
|
const entry = noteIndex[noteId];
|
|
3162
3541
|
if (!entry) return true;
|
|
@@ -3182,8 +3561,15 @@ function noteMarkdownForDisk(note, entry) {
|
|
|
3182
3561
|
}
|
|
3183
3562
|
async function flushCampaignFiles(allNotes) {
|
|
3184
3563
|
if (!campaignDir || !campaign) return;
|
|
3185
|
-
|
|
3186
|
-
|
|
3564
|
+
await writeFile(campaignDir, "campaign.json", campaignJsonText(allNotes));
|
|
3565
|
+
await writeFile(campaignDir, "REPORT.md", reportMarkdownText(allNotes));
|
|
3566
|
+
}
|
|
3567
|
+
function orderedKnownNotes(allNotes) {
|
|
3568
|
+
return allNotes.filter((n) => noteIndex[n.id]).slice().sort((a, b) => (noteIndex[a.id].seq ?? 0) - (noteIndex[b.id].seq ?? 0));
|
|
3569
|
+
}
|
|
3570
|
+
function campaignJsonText(allNotes) {
|
|
3571
|
+
if (!campaign) return "{}";
|
|
3572
|
+
const ordered = orderedKnownNotes(allNotes);
|
|
3187
3573
|
const meta = {
|
|
3188
3574
|
project: campaign.project,
|
|
3189
3575
|
campaign: campaign.campaign,
|
|
@@ -3193,7 +3579,11 @@ async function flushCampaignFiles(allNotes) {
|
|
|
3193
3579
|
noteCount: ordered.length,
|
|
3194
3580
|
notes: noteIndex
|
|
3195
3581
|
};
|
|
3196
|
-
|
|
3582
|
+
return JSON.stringify(meta, null, 2);
|
|
3583
|
+
}
|
|
3584
|
+
function reportMarkdownText(allNotes) {
|
|
3585
|
+
if (!campaign) return "";
|
|
3586
|
+
const ordered = orderedKnownNotes(allNotes);
|
|
3197
3587
|
const counts = { bug: 0, question: 0, polish: 0, verified: 0 };
|
|
3198
3588
|
for (const n of ordered) {
|
|
3199
3589
|
const sev = n.severity ?? "bug";
|
|
@@ -3207,20 +3597,66 @@ async function flushCampaignFiles(allNotes) {
|
|
|
3207
3597
|
"",
|
|
3208
3598
|
campaign.tester ? `Tester: ${campaign.tester} ` : "",
|
|
3209
3599
|
`Started: ${campaign.startedAt} `,
|
|
3210
|
-
`Updated: ${
|
|
3600
|
+
`Updated: ${(/* @__PURE__ */ new Date()).toISOString()} `,
|
|
3211
3601
|
`Points: ${ordered.length} (${counts.bug} bug, ${counts.question} question, ${counts.polish} polish \xB7 ${counts.verified} verified)`,
|
|
3212
3602
|
"",
|
|
3213
|
-
"Written live by Qapture as each point was saved. Screenshots are in",
|
|
3603
|
+
getFsSyncEngine() === "download" ? "Saved by Qapture as the campaign was tested. Screenshots are in" : "Written live by Qapture as each point was saved. Screenshots are in",
|
|
3214
3604
|
"`screenshots/`, one Markdown file per point in `notes/`.",
|
|
3215
3605
|
"",
|
|
3216
3606
|
"---",
|
|
3217
3607
|
""
|
|
3218
3608
|
].filter((line) => line !== "").join("\n");
|
|
3219
3609
|
const body = ordered.map((n) => noteMarkdownForDisk(n, noteIndex[n.id])).join("\n---\n\n");
|
|
3220
|
-
|
|
3221
|
-
${body}
|
|
3610
|
+
return `${header}
|
|
3611
|
+
${body}`;
|
|
3612
|
+
}
|
|
3613
|
+
async function buildCampaignZipBlob(allNotes) {
|
|
3614
|
+
if (!campaign || typeof document === "undefined") return null;
|
|
3615
|
+
try {
|
|
3616
|
+
const { default: JSZip } = await import('jszip');
|
|
3617
|
+
const zip = new JSZip();
|
|
3618
|
+
const base = zip.folder(safeSegment(campaign.project, "Project"))?.folder(safeSegment(campaign.campaign, defaultCampaignName()));
|
|
3619
|
+
if (!base) return null;
|
|
3620
|
+
base.file("campaign.json", campaignJsonText(allNotes));
|
|
3621
|
+
base.file("REPORT.md", reportMarkdownText(allNotes));
|
|
3622
|
+
const notesFolder = base.folder("notes");
|
|
3623
|
+
const shotsFolder = base.folder("screenshots");
|
|
3624
|
+
for (const note of orderedKnownNotes(allNotes)) {
|
|
3625
|
+
const entry = noteIndex[note.id];
|
|
3626
|
+
notesFolder?.file(entry.file, noteMarkdownForDisk(note, entry));
|
|
3627
|
+
if (note.screenshot && entry.shot) shotsFolder?.file(entry.shot, note.screenshot);
|
|
3628
|
+
if (note.afterScreenshot && entry.after) shotsFolder?.file(entry.after, note.afterScreenshot);
|
|
3629
|
+
}
|
|
3630
|
+
return await zip.generateAsync({ type: "blob" });
|
|
3631
|
+
} catch (err) {
|
|
3632
|
+
setState("error", describeError(err));
|
|
3633
|
+
return null;
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
async function downloadCampaignZip(allNotes) {
|
|
3637
|
+
const blob = await buildCampaignZipBlob(allNotes);
|
|
3638
|
+
if (!blob) return false;
|
|
3639
|
+
const url = URL.createObjectURL(blob);
|
|
3640
|
+
const a = document.createElement("a");
|
|
3641
|
+
a.href = url;
|
|
3642
|
+
a.download = campaignZipFileName();
|
|
3643
|
+
document.body.appendChild(a);
|
|
3644
|
+
a.click();
|
|
3645
|
+
a.remove();
|
|
3646
|
+
setTimeout(() => URL.revokeObjectURL(url), 2e3);
|
|
3647
|
+
setState("syncing");
|
|
3648
|
+
return true;
|
|
3222
3649
|
}
|
|
3223
3650
|
async function syncAllToDisk(allNotes) {
|
|
3651
|
+
if (getFsSyncEngine() === "download") {
|
|
3652
|
+
if (!campaign) return false;
|
|
3653
|
+
for (const note of allNotes.slice().reverse()) {
|
|
3654
|
+
if (!noteIndex[note.id]) noteIndex[note.id] = entryFor(note);
|
|
3655
|
+
}
|
|
3656
|
+
await persistDownloadCampaign();
|
|
3657
|
+
setState("syncing");
|
|
3658
|
+
return true;
|
|
3659
|
+
}
|
|
3224
3660
|
if (!campaignDir) return false;
|
|
3225
3661
|
const ordered = allNotes.slice().reverse();
|
|
3226
3662
|
bulkWriting = true;
|
|
@@ -4016,6 +4452,15 @@ function QaProvider({
|
|
|
4016
4452
|
closeFsCampaign();
|
|
4017
4453
|
notify(t("sync_paused"), { id: "sync-state" });
|
|
4018
4454
|
}, [notify, t]);
|
|
4455
|
+
const saveSyncFolderNow = useCallback(async () => {
|
|
4456
|
+
const name = campaignZipFileName();
|
|
4457
|
+
const ok = await downloadCampaignZip(notesRef.current);
|
|
4458
|
+
notify(ok ? t("sync_zip_saved", { name }) : t("sync_zip_failed"), {
|
|
4459
|
+
tone: ok ? "success" : "error",
|
|
4460
|
+
id: "sync-zip"
|
|
4461
|
+
});
|
|
4462
|
+
return ok;
|
|
4463
|
+
}, [notify, t]);
|
|
4019
4464
|
const forgetSyncFolder = useCallback(async () => {
|
|
4020
4465
|
await disconnectFsSync(idb);
|
|
4021
4466
|
storage.setJSON(LAST_CAMPAIGN_KEY, {});
|
|
@@ -4064,10 +4509,23 @@ function QaProvider({
|
|
|
4064
4509
|
useEffect(() => {
|
|
4065
4510
|
if (!autoBackup) return;
|
|
4066
4511
|
if (notes.length === 0 || isExporting) return;
|
|
4067
|
-
|
|
4512
|
+
const syncing = getFsSyncState() === "syncing";
|
|
4513
|
+
const viaDownload = getFsSyncEngine() === "download";
|
|
4514
|
+
if (syncing && !viaDownload) return;
|
|
4068
4515
|
const lastAt = Number(storage.getItem(AUTO_BACKUP_AT_KEY) ?? "0") || 0;
|
|
4069
4516
|
if (notes.length < lastAt + AUTO_BACKUP_EVERY) return;
|
|
4070
4517
|
storage.setItem(AUTO_BACKUP_AT_KEY, String(notes.length));
|
|
4518
|
+
if (syncing && viaDownload) {
|
|
4519
|
+
const name = campaignZipFileName();
|
|
4520
|
+
void downloadCampaignZip(notesRef.current).then((ok) => {
|
|
4521
|
+
notify(ok ? t("sync_zip_saved", { name }) : t("sync_zip_failed"), {
|
|
4522
|
+
tone: ok ? "success" : "error",
|
|
4523
|
+
id: "sync-zip",
|
|
4524
|
+
duration: 3e3
|
|
4525
|
+
});
|
|
4526
|
+
});
|
|
4527
|
+
return;
|
|
4528
|
+
}
|
|
4071
4529
|
const stamp = nowIso().slice(0, 16).replace(/[:T]/g, "-");
|
|
4072
4530
|
void exportZipRef.current(`qa-autosave-${stamp}-${notes.length}`).then(() => {
|
|
4073
4531
|
notify(t("autosave_done", { n: notes.length }), { id: "autosave", duration: 3e3 });
|
|
@@ -4368,6 +4826,7 @@ function QaProvider({
|
|
|
4368
4826
|
}, []);
|
|
4369
4827
|
useEffect(() => {
|
|
4370
4828
|
if (typeof window === "undefined") return;
|
|
4829
|
+
if (notesLoading) return;
|
|
4371
4830
|
const params = new URLSearchParams(window.location.search);
|
|
4372
4831
|
const raw = params.get("qa");
|
|
4373
4832
|
if (!raw || !raw.startsWith("walk")) return;
|
|
@@ -4386,7 +4845,7 @@ function QaProvider({
|
|
|
4386
4845
|
} else {
|
|
4387
4846
|
startWalk("plan", 0);
|
|
4388
4847
|
}
|
|
4389
|
-
}, [startWalk]);
|
|
4848
|
+
}, [startWalk, notesLoading]);
|
|
4390
4849
|
const value = {
|
|
4391
4850
|
// Data
|
|
4392
4851
|
notes,
|
|
@@ -4407,7 +4866,10 @@ function QaProvider({
|
|
|
4407
4866
|
brand: config.brand,
|
|
4408
4867
|
loginField: config.loginField,
|
|
4409
4868
|
credentials: config.credentials,
|
|
4410
|
-
|
|
4869
|
+
// Never the raw config value: an unconfigured project falls back to the
|
|
4870
|
+
// generic plan so the Guide is never a blank page.
|
|
4871
|
+
journey: journeyOrEmpty(config.journey),
|
|
4872
|
+
journeyIsGeneric: !config.journey?.some((lane) => lane.steps.length > 0),
|
|
4411
4873
|
preamble: config.preamble,
|
|
4412
4874
|
// i18n helpers
|
|
4413
4875
|
t,
|
|
@@ -4455,7 +4917,8 @@ function QaProvider({
|
|
|
4455
4917
|
// rebuilt when fsSync's module state changes (path/campaign live outside
|
|
4456
4918
|
// React).
|
|
4457
4919
|
sync: {
|
|
4458
|
-
supported:
|
|
4920
|
+
supported: isFsSyncAvailable(),
|
|
4921
|
+
engine: getFsSyncEngine(),
|
|
4459
4922
|
state: syncState,
|
|
4460
4923
|
path: syncTick >= 0 ? getFsSyncPath() : "",
|
|
4461
4924
|
campaign: getFsSyncCampaign(),
|
|
@@ -4465,6 +4928,7 @@ function QaProvider({
|
|
|
4465
4928
|
reconnectSyncFolder,
|
|
4466
4929
|
startSyncCampaign,
|
|
4467
4930
|
stopSyncCampaign,
|
|
4931
|
+
saveSyncFolderNow,
|
|
4468
4932
|
forgetSyncFolder,
|
|
4469
4933
|
suggestCampaignName,
|
|
4470
4934
|
lastCampaign,
|
|
@@ -4509,7 +4973,9 @@ function QaProvider({
|
|
|
4509
4973
|
return /* @__PURE__ */ jsx(QaContext.Provider, { value, children });
|
|
4510
4974
|
}
|
|
4511
4975
|
function journeyOrEmpty(journey) {
|
|
4512
|
-
|
|
4976
|
+
const configured = Array.isArray(journey) ? journey : [];
|
|
4977
|
+
const hasSteps = configured.some((lane) => lane.steps.length > 0);
|
|
4978
|
+
return hasSteps ? configured : FALLBACK_JOURNEY;
|
|
4513
4979
|
}
|
|
4514
4980
|
function useQa() {
|
|
4515
4981
|
const ctx = useContext(QaContext);
|
|
@@ -6490,6 +6956,7 @@ function SettingsSheet({ onClose }) {
|
|
|
6490
6956
|
forgetSyncFolder,
|
|
6491
6957
|
suggestCampaignName,
|
|
6492
6958
|
lastCampaign,
|
|
6959
|
+
saveSyncFolderNow,
|
|
6493
6960
|
storageHealth,
|
|
6494
6961
|
refreshStorageHealth,
|
|
6495
6962
|
requestPersistentStorage: requestPersistentStorage2,
|
|
@@ -6531,6 +6998,7 @@ function SettingsSheet({ onClose }) {
|
|
|
6531
6998
|
}
|
|
6532
6999
|
};
|
|
6533
7000
|
const syncing = sync.state === "syncing";
|
|
7001
|
+
const viaZip = sync.engine === "download";
|
|
6534
7002
|
const quotaKnown = storageHealth.supported && storageHealth.quotaBytes > 0;
|
|
6535
7003
|
const usedPct = quotaKnown ? Math.min(100, Math.round(storageHealth.ratio * 100)) : 0;
|
|
6536
7004
|
const meterColor = storageHealth.level === "critical" ? "var(--qa-danger)" : storageHealth.level === "warn" ? "var(--qa-warn)" : "var(--qa-accent)";
|
|
@@ -6558,6 +7026,7 @@ function SettingsSheet({ onClose }) {
|
|
|
6558
7026
|
/* @__PURE__ */ jsxs("div", { className: "qa-flex-1 qa-space-y-4 qa-overflow-y-auto qa-p-3", children: [
|
|
6559
7027
|
/* @__PURE__ */ jsxs(Section, { icon: "Folder", title: t("sync_title"), children: [
|
|
6560
7028
|
/* @__PURE__ */ jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo qa-leading-relaxed", children: t("sync_hint") }),
|
|
7029
|
+
viaZip ? /* @__PURE__ */ jsx("p", { className: "qa-m-0 qa-rounded-lg qa-border qa-border-dashed qa-border-subtle qa-p-2 qa-text-10 qa-text-mid qa-leading-relaxed", children: t("sync_zip_mode") }) : null,
|
|
6561
7030
|
!sync.supported ? /* @__PURE__ */ jsx("p", { className: "qa-m-0 qa-rounded-lg qa-border qa-border-dashed qa-border-subtle qa-p-2 qa-text-10 qa-text-mid", children: t("sync_unsupported") }) : sync.state === "off" ? /* @__PURE__ */ jsxs(
|
|
6562
7031
|
"button",
|
|
6563
7032
|
{
|
|
@@ -6585,24 +7054,41 @@ function SettingsSheet({ onClose }) {
|
|
|
6585
7054
|
]
|
|
6586
7055
|
}
|
|
6587
7056
|
) : syncing ? /* @__PURE__ */ jsxs("div", { className: "qa-space-y-2", children: [
|
|
7057
|
+
viaZip && /* @__PURE__ */ jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo", children: t("sync_zip_where") }),
|
|
6588
7058
|
/* @__PURE__ */ jsxs("p", { className: "qa-m-0 qa-flex qa-items-center qa-gap-1.5 qa-rounded-lg qa-bg-success-tint qa-text-success qa-px-2 qa-py-1.5 qa-text-10", children: [
|
|
6589
7059
|
/* @__PURE__ */ jsx(Icon, { name: "FolderCheck", size: 13, className: "qa-shrink-0" }),
|
|
6590
7060
|
/* @__PURE__ */ jsx("span", { className: "qa-truncate qa-dir-ltr", dir: "ltr", title: sync.path, children: sync.path })
|
|
6591
7061
|
] }),
|
|
6592
|
-
/* @__PURE__ */
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
7062
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-flex-wrap qa-gap-2", children: [
|
|
7063
|
+
viaZip && /* @__PURE__ */ jsxs(
|
|
7064
|
+
"button",
|
|
7065
|
+
{
|
|
7066
|
+
type: "button",
|
|
7067
|
+
disabled: busy,
|
|
7068
|
+
onClick: () => void run2(saveSyncFolderNow),
|
|
7069
|
+
className: "qa-tap qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-lg qa-bg-accent qa-px-3 qa-py-1.5 qa-text-xs qa-font-semibold",
|
|
7070
|
+
style: { border: "none", cursor: "pointer" },
|
|
7071
|
+
children: [
|
|
7072
|
+
/* @__PURE__ */ jsx(Icon, { name: "Download", size: 13 }),
|
|
7073
|
+
t("sync_zip_save_now")
|
|
7074
|
+
]
|
|
7075
|
+
}
|
|
7076
|
+
),
|
|
7077
|
+
/* @__PURE__ */ jsx(
|
|
7078
|
+
"button",
|
|
7079
|
+
{
|
|
7080
|
+
type: "button",
|
|
7081
|
+
onClick: stopSyncCampaign,
|
|
7082
|
+
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-10 qa-text-mid",
|
|
7083
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
7084
|
+
children: t("sync_stop")
|
|
7085
|
+
}
|
|
7086
|
+
)
|
|
7087
|
+
] })
|
|
6602
7088
|
] }) : (
|
|
6603
7089
|
/* connected: folder chosen, campaign not open yet */
|
|
6604
7090
|
/* @__PURE__ */ jsxs("div", { className: "qa-space-y-2", children: [
|
|
6605
|
-
/* @__PURE__ */ jsxs("p", { className: "qa-m-0 qa-flex qa-items-center qa-gap-1.5 qa-text-10 qa-text-mid", children: [
|
|
7091
|
+
!viaZip && /* @__PURE__ */ jsxs("p", { className: "qa-m-0 qa-flex qa-items-center qa-gap-1.5 qa-text-10 qa-text-mid", children: [
|
|
6606
7092
|
/* @__PURE__ */ jsx(Icon, { name: "Folder", size: 12, className: "qa-shrink-0" }),
|
|
6607
7093
|
/* @__PURE__ */ jsx("span", { className: "qa-truncate qa-dir-ltr", dir: "ltr", children: sync.path })
|
|
6608
7094
|
] }),
|
|
@@ -6624,11 +7110,11 @@ function SettingsSheet({ onClose }) {
|
|
|
6624
7110
|
style: { border: "none", cursor: "pointer" },
|
|
6625
7111
|
children: [
|
|
6626
7112
|
/* @__PURE__ */ jsx(Icon, { name: "Check", size: 13 }),
|
|
6627
|
-
t("sync_start")
|
|
7113
|
+
viaZip ? t("sync_zip_start") : t("sync_start")
|
|
6628
7114
|
]
|
|
6629
7115
|
}
|
|
6630
7116
|
),
|
|
6631
|
-
/* @__PURE__ */ jsx(
|
|
7117
|
+
!viaZip && /* @__PURE__ */ jsx(
|
|
6632
7118
|
"button",
|
|
6633
7119
|
{
|
|
6634
7120
|
type: "button",
|
|
@@ -7138,12 +7624,31 @@ function Lane({
|
|
|
7138
7624
|
] });
|
|
7139
7625
|
}
|
|
7140
7626
|
function GuideSection() {
|
|
7141
|
-
const {
|
|
7627
|
+
const {
|
|
7628
|
+
guideChecked,
|
|
7629
|
+
toggleGuide,
|
|
7630
|
+
t,
|
|
7631
|
+
journey,
|
|
7632
|
+
journeyIsGeneric,
|
|
7633
|
+
pick: pick2,
|
|
7634
|
+
lang,
|
|
7635
|
+
startTestAlong,
|
|
7636
|
+
startWalk,
|
|
7637
|
+
testAlongSteps
|
|
7638
|
+
} = useQa();
|
|
7142
7639
|
const all = journey.flatMap((g) => g.steps.map((s) => keyOf(g.id, s.path)));
|
|
7143
7640
|
const done = all.filter((k) => guideChecked.has(k)).length;
|
|
7144
7641
|
const pct = all.length > 0 ? Math.round(done / all.length * 100) : 0;
|
|
7145
7642
|
const coverage = computeCoverage(journey, guideChecked);
|
|
7146
7643
|
return /* @__PURE__ */ jsxs("div", { className: "qa-space-y-3", children: [
|
|
7644
|
+
journeyIsGeneric && /* @__PURE__ */ jsx(
|
|
7645
|
+
"p",
|
|
7646
|
+
{
|
|
7647
|
+
className: "qa-m-0 qa-rounded-xl qa-border qa-border-dashed qa-border-subtle qa-p-2 qa-text-10 qa-text-mid qa-leading-relaxed",
|
|
7648
|
+
"data-qa-generic-plan": "true",
|
|
7649
|
+
children: t("journey_generic")
|
|
7650
|
+
}
|
|
7651
|
+
),
|
|
7147
7652
|
/* @__PURE__ */ jsxs("div", { className: "qa-rounded-xl qa-border qa-border-accent qa-bg-accent-tint qa-p-3 qa-elev-1", children: [
|
|
7148
7653
|
coverage.red.total > 0 && /* @__PURE__ */ jsxs("div", { className: "qa-mb-1 qa-flex qa-items-center qa-gap-1.5 qa-text-11", children: [
|
|
7149
7654
|
/* @__PURE__ */ jsx("span", { className: "qa-bg-danger-tint qa-text-danger qa-rounded qa-px-1 qa-font-bold", children: lang === "ar" ? "\u0623\u062D\u0645\u0631" : "RED" }),
|
|
@@ -7200,8 +7705,7 @@ function GuideSection() {
|
|
|
7200
7705
|
pick: pick2
|
|
7201
7706
|
},
|
|
7202
7707
|
g.id
|
|
7203
|
-
))
|
|
7204
|
-
journey.length === 0 && /* @__PURE__ */ jsx("p", { className: "qa-py-8 qa-text-center qa-text-sm qa-text-slate-400", children: t("tab_guide") })
|
|
7708
|
+
))
|
|
7205
7709
|
] });
|
|
7206
7710
|
}
|
|
7207
7711
|
var TABS = [
|
|
@@ -8046,7 +8550,7 @@ function WalkHud() {
|
|
|
8046
8550
|
)
|
|
8047
8551
|
] });
|
|
8048
8552
|
}
|
|
8049
|
-
const onThisPage = typeof window !== "undefined" && stop.path === window.location.pathname;
|
|
8553
|
+
const onThisPage = stop.path === ANY_PAGE || typeof window !== "undefined" && stop.path === window.location.pathname;
|
|
8050
8554
|
return /* @__PURE__ */ jsxs(
|
|
8051
8555
|
"div",
|
|
8052
8556
|
{
|
|
@@ -9144,6 +9648,7 @@ function CaptureMode() {
|
|
|
9144
9648
|
type: "button",
|
|
9145
9649
|
onClick: () => selection && void runCapture(selection.rect),
|
|
9146
9650
|
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-mid qa-focus-ring",
|
|
9651
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
9147
9652
|
children: [
|
|
9148
9653
|
/* @__PURE__ */ jsx(Icon, { name: "RotateCcw", size: 13 }),
|
|
9149
9654
|
t("retry")
|
|
@@ -9443,5 +9948,5 @@ function Qapture({ config }) {
|
|
|
9443
9948
|
}
|
|
9444
9949
|
|
|
9445
9950
|
export { Qapture, deleteQaDatabase, initQaStudio };
|
|
9446
|
-
//# sourceMappingURL=chunk-
|
|
9447
|
-
//# sourceMappingURL=chunk-
|
|
9951
|
+
//# sourceMappingURL=chunk-JSEFC2VP.js.map
|
|
9952
|
+
//# sourceMappingURL=chunk-JSEFC2VP.js.map
|