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
|
@@ -1518,6 +1518,128 @@ function matchRouteToSteps(journey, route) {
|
|
|
1518
1518
|
return [...exact, ...param];
|
|
1519
1519
|
}
|
|
1520
1520
|
|
|
1521
|
+
// src/lib/fallbackJourney.ts
|
|
1522
|
+
var ANY_PAGE = "*";
|
|
1523
|
+
var FALLBACK_JOURNEY = [
|
|
1524
|
+
{
|
|
1525
|
+
id: "first-run",
|
|
1526
|
+
role: { en: "First look", ar: "\u0627\u0644\u0646\u0638\u0631\u0629 \u0627\u0644\u0623\u0648\u0644\u0649" },
|
|
1527
|
+
steps: [
|
|
1528
|
+
{
|
|
1529
|
+
path: ANY_PAGE,
|
|
1530
|
+
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" },
|
|
1531
|
+
expect: {
|
|
1532
|
+
en: "Content appears, nothing flashes or jumps, no error in the corner",
|
|
1533
|
+
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"
|
|
1534
|
+
},
|
|
1535
|
+
risk: "red"
|
|
1536
|
+
},
|
|
1537
|
+
{
|
|
1538
|
+
path: ANY_PAGE,
|
|
1539
|
+
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" },
|
|
1540
|
+
expect: {
|
|
1541
|
+
en: "It is obvious what this is and what to do next",
|
|
1542
|
+
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"
|
|
1543
|
+
},
|
|
1544
|
+
risk: "amber"
|
|
1545
|
+
}
|
|
1546
|
+
]
|
|
1547
|
+
},
|
|
1548
|
+
{
|
|
1549
|
+
id: "moving-around",
|
|
1550
|
+
role: { en: "Moving around", ar: "\u0627\u0644\u062A\u0646\u0642\u0644" },
|
|
1551
|
+
steps: [
|
|
1552
|
+
{
|
|
1553
|
+
path: ANY_PAGE,
|
|
1554
|
+
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" },
|
|
1555
|
+
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" },
|
|
1556
|
+
risk: "red"
|
|
1557
|
+
},
|
|
1558
|
+
{
|
|
1559
|
+
path: ANY_PAGE,
|
|
1560
|
+
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" },
|
|
1561
|
+
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" },
|
|
1562
|
+
risk: "amber"
|
|
1563
|
+
},
|
|
1564
|
+
{
|
|
1565
|
+
path: ANY_PAGE,
|
|
1566
|
+
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" },
|
|
1567
|
+
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" },
|
|
1568
|
+
risk: "red"
|
|
1569
|
+
}
|
|
1570
|
+
]
|
|
1571
|
+
},
|
|
1572
|
+
{
|
|
1573
|
+
id: "the-main-thing",
|
|
1574
|
+
role: { en: "The main thing", ar: "\u0627\u0644\u0645\u0647\u0645\u0629 \u0627\u0644\u0623\u0633\u0627\u0633\u064A\u0629" },
|
|
1575
|
+
steps: [
|
|
1576
|
+
{
|
|
1577
|
+
path: ANY_PAGE,
|
|
1578
|
+
what: {
|
|
1579
|
+
en: "Do what this app is for, start to finish, without shortcuts",
|
|
1580
|
+
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"
|
|
1581
|
+
},
|
|
1582
|
+
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" },
|
|
1583
|
+
risk: "red"
|
|
1584
|
+
},
|
|
1585
|
+
{
|
|
1586
|
+
path: ANY_PAGE,
|
|
1587
|
+
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" },
|
|
1588
|
+
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" },
|
|
1589
|
+
risk: "amber"
|
|
1590
|
+
}
|
|
1591
|
+
]
|
|
1592
|
+
},
|
|
1593
|
+
{
|
|
1594
|
+
id: "when-it-goes-wrong",
|
|
1595
|
+
role: { en: "When it goes wrong", ar: "\u0639\u0646\u062F \u0627\u0644\u062E\u0637\u0623" },
|
|
1596
|
+
steps: [
|
|
1597
|
+
{
|
|
1598
|
+
path: ANY_PAGE,
|
|
1599
|
+
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" },
|
|
1600
|
+
expect: {
|
|
1601
|
+
en: "It refuses politely and says which field and why \u2014 it does not crash or save",
|
|
1602
|
+
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"
|
|
1603
|
+
},
|
|
1604
|
+
risk: "red"
|
|
1605
|
+
},
|
|
1606
|
+
{
|
|
1607
|
+
path: ANY_PAGE,
|
|
1608
|
+
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" },
|
|
1609
|
+
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" },
|
|
1610
|
+
risk: "amber"
|
|
1611
|
+
},
|
|
1612
|
+
{
|
|
1613
|
+
path: ANY_PAGE,
|
|
1614
|
+
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" },
|
|
1615
|
+
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" },
|
|
1616
|
+
risk: "red"
|
|
1617
|
+
}
|
|
1618
|
+
]
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
id: "on-a-phone",
|
|
1622
|
+
role: { en: "On a phone", ar: "\u0639\u0644\u0649 \u0627\u0644\u0647\u0627\u062A\u0641" },
|
|
1623
|
+
steps: [
|
|
1624
|
+
{
|
|
1625
|
+
path: ANY_PAGE,
|
|
1626
|
+
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" },
|
|
1627
|
+
expect: {
|
|
1628
|
+
en: "Nothing is cut off, nothing overlaps, no sideways scrolling",
|
|
1629
|
+
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"
|
|
1630
|
+
},
|
|
1631
|
+
risk: "amber"
|
|
1632
|
+
},
|
|
1633
|
+
{
|
|
1634
|
+
path: ANY_PAGE,
|
|
1635
|
+
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" },
|
|
1636
|
+
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" },
|
|
1637
|
+
risk: "green"
|
|
1638
|
+
}
|
|
1639
|
+
]
|
|
1640
|
+
}
|
|
1641
|
+
];
|
|
1642
|
+
|
|
1521
1643
|
// src/lib/storage.ts
|
|
1522
1644
|
function isStorageAvailable() {
|
|
1523
1645
|
if (typeof window === "undefined") return false;
|
|
@@ -1620,6 +1742,7 @@ var STR = {
|
|
|
1620
1742
|
loc_hide: "Hide location",
|
|
1621
1743
|
loc_locate: "Locate on page",
|
|
1622
1744
|
journey_title: "Testing journey",
|
|
1745
|
+
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.",
|
|
1623
1746
|
export_name_title: "Name your export",
|
|
1624
1747
|
export_name_placeholder: "file name",
|
|
1625
1748
|
tap_element: "Tap an element",
|
|
@@ -1683,6 +1806,12 @@ var STR = {
|
|
|
1683
1806
|
sync_lost_permission: "Folder access expired \u2014 click Reconnect to keep saving",
|
|
1684
1807
|
sync_write_failed: "Couldn't write to the folder \u2014 notes are still saved in this browser",
|
|
1685
1808
|
sync_unsupported: "Folder saving needs Chrome, Edge or Brave on desktop. Use Export here.",
|
|
1809
|
+
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.",
|
|
1810
|
+
sync_zip_start: "Start saving",
|
|
1811
|
+
sync_zip_save_now: "Save folder now",
|
|
1812
|
+
sync_zip_saved: "Saved {name} to your downloads",
|
|
1813
|
+
sync_zip_failed: "Couldn't build the folder \u2014 your notes are still safe in this browser",
|
|
1814
|
+
sync_zip_where: "Unzip into your QA folder:",
|
|
1686
1815
|
sync_writing_to: "Writing to",
|
|
1687
1816
|
storage_title: "Storage",
|
|
1688
1817
|
storage_used: "{used} of {quota} used",
|
|
@@ -1807,6 +1936,7 @@ var STR = {
|
|
|
1807
1936
|
loc_hide: "\u0625\u062E\u0641\u0627\u0621 \u0627\u0644\u0645\u0648\u0642\u0639",
|
|
1808
1937
|
loc_locate: "\u0625\u0638\u0647\u0627\u0631 \u0639\u0644\u0649 \u0627\u0644\u0635\u0641\u062D\u0629",
|
|
1809
1938
|
journey_title: "\u0631\u062D\u0644\u0629 \u0627\u0644\u0627\u062E\u062A\u0628\u0627\u0631",
|
|
1939
|
+
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.",
|
|
1810
1940
|
export_name_title: "\u0633\u0645\u0650\u0651 \u0645\u0644\u0641 \u0627\u0644\u062A\u0635\u062F\u064A\u0631",
|
|
1811
1941
|
export_name_placeholder: "\u0627\u0633\u0645 \u0627\u0644\u0645\u0644\u0641",
|
|
1812
1942
|
tap_element: "\u0627\u0646\u0642\u0631 \u0639\u0644\u0649 \u0639\u0646\u0635\u0631",
|
|
@@ -1870,6 +2000,12 @@ var STR = {
|
|
|
1870
2000
|
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",
|
|
1871
2001
|
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",
|
|
1872
2002
|
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.",
|
|
2003
|
+
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.",
|
|
2004
|
+
sync_zip_start: "\u0627\u0628\u062F\u0623 \u0627\u0644\u062D\u0641\u0638",
|
|
2005
|
+
sync_zip_save_now: "\u0627\u062D\u0641\u0638 \u0627\u0644\u0645\u062C\u0644\u062F \u0627\u0644\u0622\u0646",
|
|
2006
|
+
sync_zip_saved: "\u062A\u0645 \u062D\u0641\u0638 {name} \u0641\u064A \u0627\u0644\u062A\u0646\u0632\u064A\u0644\u0627\u062A",
|
|
2007
|
+
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",
|
|
2008
|
+
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:",
|
|
1873
2009
|
sync_writing_to: "\u0627\u0644\u0643\u062A\u0627\u0628\u0629 \u0625\u0644\u0649",
|
|
1874
2010
|
storage_title: "\u0627\u0644\u062A\u062E\u0632\u064A\u0646",
|
|
1875
2011
|
storage_used: "{used} \u0645\u0646 {quota} \u0645\u0633\u062A\u062E\u062F\u064E\u0645\u0629",
|
|
@@ -2256,6 +2392,145 @@ async function grabExactRegion(rect) {
|
|
|
2256
2392
|
}
|
|
2257
2393
|
}
|
|
2258
2394
|
|
|
2395
|
+
// src/lib/cssColors.ts
|
|
2396
|
+
var UNSUPPORTED_FN = /\b(?:oklch|oklab|lch|lab|color-mix|color)\(/i;
|
|
2397
|
+
function unsupportedFnScanner() {
|
|
2398
|
+
return /\b(?:oklch|oklab|lch|lab|color-mix|color)\(/gi;
|
|
2399
|
+
}
|
|
2400
|
+
var COLOR_PROPS = [
|
|
2401
|
+
"color",
|
|
2402
|
+
"backgroundColor",
|
|
2403
|
+
"borderTopColor",
|
|
2404
|
+
"borderRightColor",
|
|
2405
|
+
"borderBottomColor",
|
|
2406
|
+
"borderLeftColor",
|
|
2407
|
+
"outlineColor",
|
|
2408
|
+
"textDecorationColor",
|
|
2409
|
+
"columnRuleColor",
|
|
2410
|
+
"caretColor",
|
|
2411
|
+
"fill",
|
|
2412
|
+
"stroke"
|
|
2413
|
+
];
|
|
2414
|
+
var COMPOUND_PROPS = ["backgroundImage", "boxShadow", "textShadow"];
|
|
2415
|
+
var probeCtx;
|
|
2416
|
+
function getProbe() {
|
|
2417
|
+
if (probeCtx !== void 0) return probeCtx;
|
|
2418
|
+
try {
|
|
2419
|
+
const canvas = document.createElement("canvas");
|
|
2420
|
+
canvas.width = canvas.height = 1;
|
|
2421
|
+
probeCtx = canvas.getContext("2d", { willReadFrequently: true });
|
|
2422
|
+
} catch {
|
|
2423
|
+
probeCtx = null;
|
|
2424
|
+
}
|
|
2425
|
+
return probeCtx;
|
|
2426
|
+
}
|
|
2427
|
+
var conversionCache = /* @__PURE__ */ new Map();
|
|
2428
|
+
function toRenderableColor(value) {
|
|
2429
|
+
const ctx = getProbe();
|
|
2430
|
+
if (!ctx || !value) return null;
|
|
2431
|
+
const cached = conversionCache.get(value);
|
|
2432
|
+
if (cached !== void 0) return cached;
|
|
2433
|
+
const result = convertUncached(ctx, value);
|
|
2434
|
+
conversionCache.set(value, result);
|
|
2435
|
+
return result;
|
|
2436
|
+
}
|
|
2437
|
+
function convertUncached(ctx, value) {
|
|
2438
|
+
try {
|
|
2439
|
+
ctx.fillStyle = "#000000";
|
|
2440
|
+
ctx.fillStyle = value;
|
|
2441
|
+
const fromBlack = String(ctx.fillStyle);
|
|
2442
|
+
ctx.fillStyle = "#ffffff";
|
|
2443
|
+
ctx.fillStyle = value;
|
|
2444
|
+
if (fromBlack !== String(ctx.fillStyle)) return null;
|
|
2445
|
+
if (!UNSUPPORTED_FN.test(fromBlack)) return fromBlack;
|
|
2446
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
2447
|
+
ctx.fillStyle = value;
|
|
2448
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
2449
|
+
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
|
|
2450
|
+
return a === 255 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(3)})`;
|
|
2451
|
+
} catch {
|
|
2452
|
+
return null;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
function neutralizeColorFunctions(value) {
|
|
2456
|
+
if (!value || !UNSUPPORTED_FN.test(value)) return value;
|
|
2457
|
+
const scanner = unsupportedFnScanner();
|
|
2458
|
+
let out = "";
|
|
2459
|
+
let last = 0;
|
|
2460
|
+
let match;
|
|
2461
|
+
while ((match = scanner.exec(value)) !== null) {
|
|
2462
|
+
const start = match.index;
|
|
2463
|
+
if (start < last) continue;
|
|
2464
|
+
let depth = 0;
|
|
2465
|
+
let i = start + match[0].length - 1;
|
|
2466
|
+
for (; i < value.length; i++) {
|
|
2467
|
+
if (value[i] === "(") depth++;
|
|
2468
|
+
else if (value[i] === ")") {
|
|
2469
|
+
depth--;
|
|
2470
|
+
if (depth === 0) break;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
if (i >= value.length) break;
|
|
2474
|
+
const converted = toRenderableColor(value.slice(start, i + 1));
|
|
2475
|
+
out += value.slice(last, start) + (converted ?? "rgba(0, 0, 0, 0)");
|
|
2476
|
+
last = i + 1;
|
|
2477
|
+
scanner.lastIndex = last;
|
|
2478
|
+
}
|
|
2479
|
+
return out + value.slice(last);
|
|
2480
|
+
}
|
|
2481
|
+
function hasUnsupportedColor(value) {
|
|
2482
|
+
return !!value && UNSUPPORTED_FN.test(value);
|
|
2483
|
+
}
|
|
2484
|
+
function safeBackgroundColor(value, fallback = "#ffffff") {
|
|
2485
|
+
if (!hasUnsupportedColor(value)) return value;
|
|
2486
|
+
return toRenderableColor(value) ?? fallback;
|
|
2487
|
+
}
|
|
2488
|
+
function neutralizeDocumentColors(doc, aggressive = false) {
|
|
2489
|
+
const view = doc.defaultView;
|
|
2490
|
+
if (!view || typeof view.getComputedStyle !== "function") return 0;
|
|
2491
|
+
let touched = 0;
|
|
2492
|
+
const all = doc.querySelectorAll("*");
|
|
2493
|
+
const elements = [doc.documentElement, ...Array.from(all)];
|
|
2494
|
+
for (const el of elements) {
|
|
2495
|
+
if (!el || !el.style) continue;
|
|
2496
|
+
let changed = false;
|
|
2497
|
+
let computed;
|
|
2498
|
+
try {
|
|
2499
|
+
computed = view.getComputedStyle(el);
|
|
2500
|
+
} catch {
|
|
2501
|
+
continue;
|
|
2502
|
+
}
|
|
2503
|
+
for (const prop of COLOR_PROPS) {
|
|
2504
|
+
const value = computed[prop] ?? computed[prop];
|
|
2505
|
+
if (!hasUnsupportedColor(value)) continue;
|
|
2506
|
+
const converted = toRenderableColor(value);
|
|
2507
|
+
if (converted) {
|
|
2508
|
+
el.style[prop] = converted;
|
|
2509
|
+
changed = true;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
for (const prop of COMPOUND_PROPS) {
|
|
2513
|
+
const value = computed[prop];
|
|
2514
|
+
if (!value || value === "none") continue;
|
|
2515
|
+
if (aggressive && prop !== "backgroundImage") {
|
|
2516
|
+
el.style[prop] = "none";
|
|
2517
|
+
changed = true;
|
|
2518
|
+
continue;
|
|
2519
|
+
}
|
|
2520
|
+
if (!hasUnsupportedColor(value)) continue;
|
|
2521
|
+
if (aggressive) {
|
|
2522
|
+
el.style[prop] = "none";
|
|
2523
|
+
changed = true;
|
|
2524
|
+
continue;
|
|
2525
|
+
}
|
|
2526
|
+
el.style[prop] = neutralizeColorFunctions(value);
|
|
2527
|
+
changed = true;
|
|
2528
|
+
}
|
|
2529
|
+
if (changed) touched++;
|
|
2530
|
+
}
|
|
2531
|
+
return touched;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2259
2534
|
// src/lib/capture.ts
|
|
2260
2535
|
var HTML2CANVAS_TIMEOUT_MS = 1e4;
|
|
2261
2536
|
var FALLBACK_PAGE_BACKGROUND = "#ffffff";
|
|
@@ -2427,7 +2702,7 @@ function cropCanvas(full, rect, scale) {
|
|
|
2427
2702
|
ctx.drawImage(full, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
2428
2703
|
return out;
|
|
2429
2704
|
}
|
|
2430
|
-
async function captureViaDom(rect, sx, sy) {
|
|
2705
|
+
async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
2431
2706
|
const { default: html2canvas } = await import('html2canvas');
|
|
2432
2707
|
const scale = Math.min(window.devicePixelRatio || 1, 2);
|
|
2433
2708
|
const vw = document.documentElement.clientWidth || window.innerWidth;
|
|
@@ -2445,14 +2720,19 @@ async function captureViaDom(rect, sx, sy) {
|
|
|
2445
2720
|
useCORS: true,
|
|
2446
2721
|
allowTaint: true,
|
|
2447
2722
|
// The page's own background, never null — see resolvePageBackground().
|
|
2448
|
-
|
|
2723
|
+
// Passed straight to html2canvas's parser, so it gets the same
|
|
2724
|
+
// CSS Color 4 treatment the document walk applies.
|
|
2725
|
+
backgroundColor: safeBackgroundColor(resolvePageBackground(), FALLBACK_PAGE_BACKGROUND),
|
|
2449
2726
|
logging: false,
|
|
2450
2727
|
scrollX: sx,
|
|
2451
2728
|
scrollY: sy,
|
|
2452
2729
|
windowWidth: vw,
|
|
2453
2730
|
windowHeight: vh,
|
|
2454
2731
|
ignoreElements: (el) => el.nodeType === 1 && typeof el.hasAttribute === "function" && el.hasAttribute("data-qa-overlay"),
|
|
2455
|
-
onclone: (doc) =>
|
|
2732
|
+
onclone: (doc) => {
|
|
2733
|
+
neutralizeDocumentColors(doc, aggressiveColors);
|
|
2734
|
+
pinStuckClones(doc);
|
|
2735
|
+
}
|
|
2456
2736
|
}),
|
|
2457
2737
|
HTML2CANVAS_TIMEOUT_MS
|
|
2458
2738
|
);
|
|
@@ -2487,8 +2767,16 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
2487
2767
|
const blob = await encodeShot(canvas);
|
|
2488
2768
|
return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
|
|
2489
2769
|
} catch (err) {
|
|
2490
|
-
console.warn("[QA] region capture failed:", err);
|
|
2491
|
-
|
|
2770
|
+
console.warn("[QA] region capture failed, retrying without gradients/shadows:", err);
|
|
2771
|
+
try {
|
|
2772
|
+
const canvas = await captureViaDom(rect, sx, sy, true);
|
|
2773
|
+
if (!canvas) return { status: "failed" };
|
|
2774
|
+
const blob = await encodeShot(canvas);
|
|
2775
|
+
return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
|
|
2776
|
+
} catch (retryErr) {
|
|
2777
|
+
console.warn("[QA] region capture failed after retry:", retryErr);
|
|
2778
|
+
return { status: "failed" };
|
|
2779
|
+
}
|
|
2492
2780
|
}
|
|
2493
2781
|
}
|
|
2494
2782
|
|
|
@@ -2911,6 +3199,7 @@ async function shareZipFile(blob, filename, title) {
|
|
|
2911
3199
|
// src/lib/fsSync.ts
|
|
2912
3200
|
var MAX_SLUG_LENGTH = 48;
|
|
2913
3201
|
var FS_ROOT_META_KEY = "fsSyncRoot";
|
|
3202
|
+
var FS_CAMPAIGN_META_KEY = "fsSyncCampaign";
|
|
2914
3203
|
var root = null;
|
|
2915
3204
|
var campaignDir = null;
|
|
2916
3205
|
var notesDir = null;
|
|
@@ -2919,6 +3208,7 @@ var campaign = null;
|
|
|
2919
3208
|
var noteIndex = {};
|
|
2920
3209
|
var state = "unsupported";
|
|
2921
3210
|
var lastError = "";
|
|
3211
|
+
var metaStore = null;
|
|
2922
3212
|
var bulkWriting = false;
|
|
2923
3213
|
var listeners = /* @__PURE__ */ new Set();
|
|
2924
3214
|
function setState(next, error = "") {
|
|
@@ -2941,8 +3231,20 @@ function onFsSyncChange(fn) {
|
|
|
2941
3231
|
function isFsSyncSupported() {
|
|
2942
3232
|
return typeof window !== "undefined" && typeof window.showDirectoryPicker === "function";
|
|
2943
3233
|
}
|
|
3234
|
+
function isFsSyncAvailable() {
|
|
3235
|
+
return typeof window !== "undefined";
|
|
3236
|
+
}
|
|
3237
|
+
function getFsSyncEngine() {
|
|
3238
|
+
if (isFsSyncSupported() || typeof document === "undefined") return "disk";
|
|
3239
|
+
return "download";
|
|
3240
|
+
}
|
|
2944
3241
|
function getFsSyncState() {
|
|
2945
|
-
if (!
|
|
3242
|
+
if (!isFsSyncAvailable()) return "unsupported";
|
|
3243
|
+
if (!isFsSyncSupported()) {
|
|
3244
|
+
if (state === "syncing") return "syncing";
|
|
3245
|
+
if (state === "error") return "error";
|
|
3246
|
+
return "connected";
|
|
3247
|
+
}
|
|
2946
3248
|
return state === "unsupported" ? "off" : state;
|
|
2947
3249
|
}
|
|
2948
3250
|
function getFsSyncError() {
|
|
@@ -2952,9 +3254,18 @@ function getFsSyncCampaign() {
|
|
|
2952
3254
|
return campaign;
|
|
2953
3255
|
}
|
|
2954
3256
|
function getFsSyncPath() {
|
|
3257
|
+
if (getFsSyncEngine() === "download") return campaign ? campaignRelativePath() : "";
|
|
2955
3258
|
if (!root) return "";
|
|
2956
3259
|
if (!campaign) return root.name;
|
|
2957
|
-
return `${root.name}/${
|
|
3260
|
+
return `${root.name}/${campaignRelativePath()}`;
|
|
3261
|
+
}
|
|
3262
|
+
function campaignRelativePath() {
|
|
3263
|
+
if (!campaign) return "";
|
|
3264
|
+
return `${safeSegment(campaign.project)}/${safeSegment(campaign.campaign)}`;
|
|
3265
|
+
}
|
|
3266
|
+
function campaignZipFileName() {
|
|
3267
|
+
if (!campaign) return "qa-campaign.zip";
|
|
3268
|
+
return `${safeSegment(campaign.project)} - ${safeSegment(campaign.campaign)}.zip`;
|
|
2958
3269
|
}
|
|
2959
3270
|
function safeSegment(input, fallback = "untitled") {
|
|
2960
3271
|
let s = (input ?? "").normalize("NFC").replace(/[\\/:*?"<>|\u0000-\u001F]/g, "-");
|
|
@@ -2996,10 +3307,25 @@ async function permissionFor(handle, request) {
|
|
|
2996
3307
|
return "granted";
|
|
2997
3308
|
}
|
|
2998
3309
|
async function restoreFsSync(store) {
|
|
2999
|
-
|
|
3310
|
+
metaStore = store;
|
|
3311
|
+
if (!isFsSyncAvailable()) {
|
|
3000
3312
|
setState("unsupported");
|
|
3001
3313
|
return "unsupported";
|
|
3002
3314
|
}
|
|
3315
|
+
if (getFsSyncEngine() === "download") {
|
|
3316
|
+
try {
|
|
3317
|
+
const saved = await store.getMeta(FS_CAMPAIGN_META_KEY);
|
|
3318
|
+
if (saved?.campaign?.project && saved.active !== false) {
|
|
3319
|
+
campaign = saved.campaign;
|
|
3320
|
+
noteIndex = saved.notes ?? {};
|
|
3321
|
+
setState("syncing");
|
|
3322
|
+
return "syncing";
|
|
3323
|
+
}
|
|
3324
|
+
} catch {
|
|
3325
|
+
}
|
|
3326
|
+
setState("connected");
|
|
3327
|
+
return "connected";
|
|
3328
|
+
}
|
|
3003
3329
|
try {
|
|
3004
3330
|
const saved = await store.getMeta(FS_ROOT_META_KEY);
|
|
3005
3331
|
if (!saved || typeof saved.getDirectoryHandle !== "function") {
|
|
@@ -3016,7 +3342,11 @@ async function restoreFsSync(store) {
|
|
|
3016
3342
|
}
|
|
3017
3343
|
}
|
|
3018
3344
|
async function chooseFsSyncRoot(store) {
|
|
3019
|
-
|
|
3345
|
+
metaStore = store;
|
|
3346
|
+
if (!isFsSyncSupported()) {
|
|
3347
|
+
setState("connected");
|
|
3348
|
+
return true;
|
|
3349
|
+
}
|
|
3020
3350
|
const picker = window.showDirectoryPicker;
|
|
3021
3351
|
try {
|
|
3022
3352
|
const handle = await picker({ id: "qapture-qa", mode: "readwrite", startIn: "documents" });
|
|
@@ -3064,6 +3394,30 @@ function describeError(err) {
|
|
|
3064
3394
|
return e?.message || "write-failed";
|
|
3065
3395
|
}
|
|
3066
3396
|
async function openFsCampaign(input) {
|
|
3397
|
+
if (getFsSyncEngine() === "download") {
|
|
3398
|
+
const sameAsBefore = campaign?.project === input.project && campaign?.campaign === input.campaign;
|
|
3399
|
+
let startedAt = sameAsBefore && campaign ? campaign.startedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
3400
|
+
if (!sameAsBefore) {
|
|
3401
|
+
noteIndex = {};
|
|
3402
|
+
try {
|
|
3403
|
+
const saved = await metaStore?.getMeta(FS_CAMPAIGN_META_KEY);
|
|
3404
|
+
if (saved?.campaign?.project === input.project && saved.campaign.campaign === input.campaign) {
|
|
3405
|
+
noteIndex = saved.notes ?? {};
|
|
3406
|
+
startedAt = saved.campaign.startedAt || startedAt;
|
|
3407
|
+
}
|
|
3408
|
+
} catch {
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
campaign = {
|
|
3412
|
+
project: input.project,
|
|
3413
|
+
campaign: input.campaign,
|
|
3414
|
+
tester: input.tester,
|
|
3415
|
+
startedAt
|
|
3416
|
+
};
|
|
3417
|
+
await persistDownloadCampaign();
|
|
3418
|
+
setState("syncing");
|
|
3419
|
+
return true;
|
|
3420
|
+
}
|
|
3067
3421
|
if (!root) return false;
|
|
3068
3422
|
try {
|
|
3069
3423
|
const perm = await permissionFor(root, false);
|
|
@@ -3102,11 +3456,23 @@ async function openFsCampaign(input) {
|
|
|
3102
3456
|
}
|
|
3103
3457
|
}
|
|
3104
3458
|
function closeFsCampaign() {
|
|
3459
|
+
if (getFsSyncEngine() === "download") void persistDownloadCampaign(false);
|
|
3105
3460
|
campaignDir = notesDir = shotsDir = null;
|
|
3106
3461
|
campaign = null;
|
|
3107
3462
|
noteIndex = {};
|
|
3463
|
+
if (getFsSyncEngine() === "download") {
|
|
3464
|
+
setState("connected");
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3108
3467
|
if (root) setState("connected");
|
|
3109
3468
|
}
|
|
3469
|
+
async function persistDownloadCampaign(active = true) {
|
|
3470
|
+
if (!metaStore || !campaign) return;
|
|
3471
|
+
try {
|
|
3472
|
+
await metaStore.setMeta(FS_CAMPAIGN_META_KEY, { campaign, notes: noteIndex, active });
|
|
3473
|
+
} catch {
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3110
3476
|
async function writeFile(dir, name, data) {
|
|
3111
3477
|
const handle = await dir.getFileHandle(name, { create: true });
|
|
3112
3478
|
const writable = await handle.createWritable();
|
|
@@ -3137,6 +3503,13 @@ function entryFor(note) {
|
|
|
3137
3503
|
};
|
|
3138
3504
|
}
|
|
3139
3505
|
async function syncNoteToDisk(note, allNotes) {
|
|
3506
|
+
if (getFsSyncEngine() === "download") {
|
|
3507
|
+
if (!campaign) return false;
|
|
3508
|
+
noteIndex[note.id] = entryFor(note);
|
|
3509
|
+
await persistDownloadCampaign();
|
|
3510
|
+
setState("syncing");
|
|
3511
|
+
return true;
|
|
3512
|
+
}
|
|
3140
3513
|
if (!campaignDir || !notesDir || !shotsDir || !campaign) return false;
|
|
3141
3514
|
try {
|
|
3142
3515
|
const previous = noteIndex[note.id];
|
|
@@ -3164,6 +3537,12 @@ async function syncNoteToDisk(note, allNotes) {
|
|
|
3164
3537
|
}
|
|
3165
3538
|
}
|
|
3166
3539
|
async function removeNoteFromDisk(noteId, allNotes) {
|
|
3540
|
+
if (getFsSyncEngine() === "download") {
|
|
3541
|
+
if (!noteIndex[noteId]) return true;
|
|
3542
|
+
delete noteIndex[noteId];
|
|
3543
|
+
await persistDownloadCampaign();
|
|
3544
|
+
return true;
|
|
3545
|
+
}
|
|
3167
3546
|
if (!campaignDir || !notesDir || !shotsDir) return false;
|
|
3168
3547
|
const entry = noteIndex[noteId];
|
|
3169
3548
|
if (!entry) return true;
|
|
@@ -3189,8 +3568,15 @@ function noteMarkdownForDisk(note, entry) {
|
|
|
3189
3568
|
}
|
|
3190
3569
|
async function flushCampaignFiles(allNotes) {
|
|
3191
3570
|
if (!campaignDir || !campaign) return;
|
|
3192
|
-
|
|
3193
|
-
|
|
3571
|
+
await writeFile(campaignDir, "campaign.json", campaignJsonText(allNotes));
|
|
3572
|
+
await writeFile(campaignDir, "REPORT.md", reportMarkdownText(allNotes));
|
|
3573
|
+
}
|
|
3574
|
+
function orderedKnownNotes(allNotes) {
|
|
3575
|
+
return allNotes.filter((n) => noteIndex[n.id]).slice().sort((a, b) => (noteIndex[a.id].seq ?? 0) - (noteIndex[b.id].seq ?? 0));
|
|
3576
|
+
}
|
|
3577
|
+
function campaignJsonText(allNotes) {
|
|
3578
|
+
if (!campaign) return "{}";
|
|
3579
|
+
const ordered = orderedKnownNotes(allNotes);
|
|
3194
3580
|
const meta = {
|
|
3195
3581
|
project: campaign.project,
|
|
3196
3582
|
campaign: campaign.campaign,
|
|
@@ -3200,7 +3586,11 @@ async function flushCampaignFiles(allNotes) {
|
|
|
3200
3586
|
noteCount: ordered.length,
|
|
3201
3587
|
notes: noteIndex
|
|
3202
3588
|
};
|
|
3203
|
-
|
|
3589
|
+
return JSON.stringify(meta, null, 2);
|
|
3590
|
+
}
|
|
3591
|
+
function reportMarkdownText(allNotes) {
|
|
3592
|
+
if (!campaign) return "";
|
|
3593
|
+
const ordered = orderedKnownNotes(allNotes);
|
|
3204
3594
|
const counts = { bug: 0, question: 0, polish: 0, verified: 0 };
|
|
3205
3595
|
for (const n of ordered) {
|
|
3206
3596
|
const sev = n.severity ?? "bug";
|
|
@@ -3214,20 +3604,66 @@ async function flushCampaignFiles(allNotes) {
|
|
|
3214
3604
|
"",
|
|
3215
3605
|
campaign.tester ? `Tester: ${campaign.tester} ` : "",
|
|
3216
3606
|
`Started: ${campaign.startedAt} `,
|
|
3217
|
-
`Updated: ${
|
|
3607
|
+
`Updated: ${(/* @__PURE__ */ new Date()).toISOString()} `,
|
|
3218
3608
|
`Points: ${ordered.length} (${counts.bug} bug, ${counts.question} question, ${counts.polish} polish \xB7 ${counts.verified} verified)`,
|
|
3219
3609
|
"",
|
|
3220
|
-
"Written live by Qapture as each point was saved. Screenshots are in",
|
|
3610
|
+
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",
|
|
3221
3611
|
"`screenshots/`, one Markdown file per point in `notes/`.",
|
|
3222
3612
|
"",
|
|
3223
3613
|
"---",
|
|
3224
3614
|
""
|
|
3225
3615
|
].filter((line) => line !== "").join("\n");
|
|
3226
3616
|
const body = ordered.map((n) => noteMarkdownForDisk(n, noteIndex[n.id])).join("\n---\n\n");
|
|
3227
|
-
|
|
3228
|
-
${body}
|
|
3617
|
+
return `${header}
|
|
3618
|
+
${body}`;
|
|
3619
|
+
}
|
|
3620
|
+
async function buildCampaignZipBlob(allNotes) {
|
|
3621
|
+
if (!campaign || typeof document === "undefined") return null;
|
|
3622
|
+
try {
|
|
3623
|
+
const { default: JSZip } = await import('jszip');
|
|
3624
|
+
const zip = new JSZip();
|
|
3625
|
+
const base = zip.folder(safeSegment(campaign.project, "Project"))?.folder(safeSegment(campaign.campaign, defaultCampaignName()));
|
|
3626
|
+
if (!base) return null;
|
|
3627
|
+
base.file("campaign.json", campaignJsonText(allNotes));
|
|
3628
|
+
base.file("REPORT.md", reportMarkdownText(allNotes));
|
|
3629
|
+
const notesFolder = base.folder("notes");
|
|
3630
|
+
const shotsFolder = base.folder("screenshots");
|
|
3631
|
+
for (const note of orderedKnownNotes(allNotes)) {
|
|
3632
|
+
const entry = noteIndex[note.id];
|
|
3633
|
+
notesFolder?.file(entry.file, noteMarkdownForDisk(note, entry));
|
|
3634
|
+
if (note.screenshot && entry.shot) shotsFolder?.file(entry.shot, note.screenshot);
|
|
3635
|
+
if (note.afterScreenshot && entry.after) shotsFolder?.file(entry.after, note.afterScreenshot);
|
|
3636
|
+
}
|
|
3637
|
+
return await zip.generateAsync({ type: "blob" });
|
|
3638
|
+
} catch (err) {
|
|
3639
|
+
setState("error", describeError(err));
|
|
3640
|
+
return null;
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
async function downloadCampaignZip(allNotes) {
|
|
3644
|
+
const blob = await buildCampaignZipBlob(allNotes);
|
|
3645
|
+
if (!blob) return false;
|
|
3646
|
+
const url = URL.createObjectURL(blob);
|
|
3647
|
+
const a = document.createElement("a");
|
|
3648
|
+
a.href = url;
|
|
3649
|
+
a.download = campaignZipFileName();
|
|
3650
|
+
document.body.appendChild(a);
|
|
3651
|
+
a.click();
|
|
3652
|
+
a.remove();
|
|
3653
|
+
setTimeout(() => URL.revokeObjectURL(url), 2e3);
|
|
3654
|
+
setState("syncing");
|
|
3655
|
+
return true;
|
|
3229
3656
|
}
|
|
3230
3657
|
async function syncAllToDisk(allNotes) {
|
|
3658
|
+
if (getFsSyncEngine() === "download") {
|
|
3659
|
+
if (!campaign) return false;
|
|
3660
|
+
for (const note of allNotes.slice().reverse()) {
|
|
3661
|
+
if (!noteIndex[note.id]) noteIndex[note.id] = entryFor(note);
|
|
3662
|
+
}
|
|
3663
|
+
await persistDownloadCampaign();
|
|
3664
|
+
setState("syncing");
|
|
3665
|
+
return true;
|
|
3666
|
+
}
|
|
3231
3667
|
if (!campaignDir) return false;
|
|
3232
3668
|
const ordered = allNotes.slice().reverse();
|
|
3233
3669
|
bulkWriting = true;
|
|
@@ -4023,6 +4459,15 @@ function QaProvider({
|
|
|
4023
4459
|
closeFsCampaign();
|
|
4024
4460
|
notify(t("sync_paused"), { id: "sync-state" });
|
|
4025
4461
|
}, [notify, t]);
|
|
4462
|
+
const saveSyncFolderNow = React.useCallback(async () => {
|
|
4463
|
+
const name = campaignZipFileName();
|
|
4464
|
+
const ok = await downloadCampaignZip(notesRef.current);
|
|
4465
|
+
notify(ok ? t("sync_zip_saved", { name }) : t("sync_zip_failed"), {
|
|
4466
|
+
tone: ok ? "success" : "error",
|
|
4467
|
+
id: "sync-zip"
|
|
4468
|
+
});
|
|
4469
|
+
return ok;
|
|
4470
|
+
}, [notify, t]);
|
|
4026
4471
|
const forgetSyncFolder = React.useCallback(async () => {
|
|
4027
4472
|
await disconnectFsSync(idb);
|
|
4028
4473
|
storage.setJSON(LAST_CAMPAIGN_KEY, {});
|
|
@@ -4071,10 +4516,23 @@ function QaProvider({
|
|
|
4071
4516
|
React.useEffect(() => {
|
|
4072
4517
|
if (!autoBackup) return;
|
|
4073
4518
|
if (notes.length === 0 || isExporting) return;
|
|
4074
|
-
|
|
4519
|
+
const syncing = getFsSyncState() === "syncing";
|
|
4520
|
+
const viaDownload = getFsSyncEngine() === "download";
|
|
4521
|
+
if (syncing && !viaDownload) return;
|
|
4075
4522
|
const lastAt = Number(storage.getItem(AUTO_BACKUP_AT_KEY) ?? "0") || 0;
|
|
4076
4523
|
if (notes.length < lastAt + AUTO_BACKUP_EVERY) return;
|
|
4077
4524
|
storage.setItem(AUTO_BACKUP_AT_KEY, String(notes.length));
|
|
4525
|
+
if (syncing && viaDownload) {
|
|
4526
|
+
const name = campaignZipFileName();
|
|
4527
|
+
void downloadCampaignZip(notesRef.current).then((ok) => {
|
|
4528
|
+
notify(ok ? t("sync_zip_saved", { name }) : t("sync_zip_failed"), {
|
|
4529
|
+
tone: ok ? "success" : "error",
|
|
4530
|
+
id: "sync-zip",
|
|
4531
|
+
duration: 3e3
|
|
4532
|
+
});
|
|
4533
|
+
});
|
|
4534
|
+
return;
|
|
4535
|
+
}
|
|
4078
4536
|
const stamp = nowIso().slice(0, 16).replace(/[:T]/g, "-");
|
|
4079
4537
|
void exportZipRef.current(`qa-autosave-${stamp}-${notes.length}`).then(() => {
|
|
4080
4538
|
notify(t("autosave_done", { n: notes.length }), { id: "autosave", duration: 3e3 });
|
|
@@ -4375,6 +4833,7 @@ function QaProvider({
|
|
|
4375
4833
|
}, []);
|
|
4376
4834
|
React.useEffect(() => {
|
|
4377
4835
|
if (typeof window === "undefined") return;
|
|
4836
|
+
if (notesLoading) return;
|
|
4378
4837
|
const params = new URLSearchParams(window.location.search);
|
|
4379
4838
|
const raw = params.get("qa");
|
|
4380
4839
|
if (!raw || !raw.startsWith("walk")) return;
|
|
@@ -4393,7 +4852,7 @@ function QaProvider({
|
|
|
4393
4852
|
} else {
|
|
4394
4853
|
startWalk("plan", 0);
|
|
4395
4854
|
}
|
|
4396
|
-
}, [startWalk]);
|
|
4855
|
+
}, [startWalk, notesLoading]);
|
|
4397
4856
|
const value = {
|
|
4398
4857
|
// Data
|
|
4399
4858
|
notes,
|
|
@@ -4414,7 +4873,10 @@ function QaProvider({
|
|
|
4414
4873
|
brand: config.brand,
|
|
4415
4874
|
loginField: config.loginField,
|
|
4416
4875
|
credentials: config.credentials,
|
|
4417
|
-
|
|
4876
|
+
// Never the raw config value: an unconfigured project falls back to the
|
|
4877
|
+
// generic plan so the Guide is never a blank page.
|
|
4878
|
+
journey: journeyOrEmpty(config.journey),
|
|
4879
|
+
journeyIsGeneric: !config.journey?.some((lane) => lane.steps.length > 0),
|
|
4418
4880
|
preamble: config.preamble,
|
|
4419
4881
|
// i18n helpers
|
|
4420
4882
|
t,
|
|
@@ -4462,7 +4924,8 @@ function QaProvider({
|
|
|
4462
4924
|
// rebuilt when fsSync's module state changes (path/campaign live outside
|
|
4463
4925
|
// React).
|
|
4464
4926
|
sync: {
|
|
4465
|
-
supported:
|
|
4927
|
+
supported: isFsSyncAvailable(),
|
|
4928
|
+
engine: getFsSyncEngine(),
|
|
4466
4929
|
state: syncState,
|
|
4467
4930
|
path: syncTick >= 0 ? getFsSyncPath() : "",
|
|
4468
4931
|
campaign: getFsSyncCampaign(),
|
|
@@ -4472,6 +4935,7 @@ function QaProvider({
|
|
|
4472
4935
|
reconnectSyncFolder,
|
|
4473
4936
|
startSyncCampaign,
|
|
4474
4937
|
stopSyncCampaign,
|
|
4938
|
+
saveSyncFolderNow,
|
|
4475
4939
|
forgetSyncFolder,
|
|
4476
4940
|
suggestCampaignName,
|
|
4477
4941
|
lastCampaign,
|
|
@@ -4516,7 +4980,9 @@ function QaProvider({
|
|
|
4516
4980
|
return /* @__PURE__ */ jsxRuntime.jsx(QaContext.Provider, { value, children });
|
|
4517
4981
|
}
|
|
4518
4982
|
function journeyOrEmpty(journey) {
|
|
4519
|
-
|
|
4983
|
+
const configured = Array.isArray(journey) ? journey : [];
|
|
4984
|
+
const hasSteps = configured.some((lane) => lane.steps.length > 0);
|
|
4985
|
+
return hasSteps ? configured : FALLBACK_JOURNEY;
|
|
4520
4986
|
}
|
|
4521
4987
|
function useQa() {
|
|
4522
4988
|
const ctx = React.useContext(QaContext);
|
|
@@ -6497,6 +6963,7 @@ function SettingsSheet({ onClose }) {
|
|
|
6497
6963
|
forgetSyncFolder,
|
|
6498
6964
|
suggestCampaignName,
|
|
6499
6965
|
lastCampaign,
|
|
6966
|
+
saveSyncFolderNow,
|
|
6500
6967
|
storageHealth,
|
|
6501
6968
|
refreshStorageHealth,
|
|
6502
6969
|
requestPersistentStorage: requestPersistentStorage2,
|
|
@@ -6538,6 +7005,7 @@ function SettingsSheet({ onClose }) {
|
|
|
6538
7005
|
}
|
|
6539
7006
|
};
|
|
6540
7007
|
const syncing = sync.state === "syncing";
|
|
7008
|
+
const viaZip = sync.engine === "download";
|
|
6541
7009
|
const quotaKnown = storageHealth.supported && storageHealth.quotaBytes > 0;
|
|
6542
7010
|
const usedPct = quotaKnown ? Math.min(100, Math.round(storageHealth.ratio * 100)) : 0;
|
|
6543
7011
|
const meterColor = storageHealth.level === "critical" ? "var(--qa-danger)" : storageHealth.level === "warn" ? "var(--qa-warn)" : "var(--qa-accent)";
|
|
@@ -6565,6 +7033,7 @@ function SettingsSheet({ onClose }) {
|
|
|
6565
7033
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex-1 qa-space-y-4 qa-overflow-y-auto qa-p-3", children: [
|
|
6566
7034
|
/* @__PURE__ */ jsxRuntime.jsxs(Section, { icon: "Folder", title: t("sync_title"), children: [
|
|
6567
7035
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo qa-leading-relaxed", children: t("sync_hint") }),
|
|
7036
|
+
viaZip ? /* @__PURE__ */ jsxRuntime.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,
|
|
6568
7037
|
!sync.supported ? /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs(
|
|
6569
7038
|
"button",
|
|
6570
7039
|
{
|
|
@@ -6592,24 +7061,41 @@ function SettingsSheet({ onClose }) {
|
|
|
6592
7061
|
]
|
|
6593
7062
|
}
|
|
6594
7063
|
) : syncing ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-2", children: [
|
|
7064
|
+
viaZip && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo", children: t("sync_zip_where") }),
|
|
6595
7065
|
/* @__PURE__ */ jsxRuntime.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: [
|
|
6596
7066
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "FolderCheck", size: 13, className: "qa-shrink-0" }),
|
|
6597
7067
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-truncate qa-dir-ltr", dir: "ltr", title: sync.path, children: sync.path })
|
|
6598
7068
|
] }),
|
|
6599
|
-
/* @__PURE__ */ jsxRuntime.
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
7069
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-flex-wrap qa-gap-2", children: [
|
|
7070
|
+
viaZip && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
7071
|
+
"button",
|
|
7072
|
+
{
|
|
7073
|
+
type: "button",
|
|
7074
|
+
disabled: busy,
|
|
7075
|
+
onClick: () => void run2(saveSyncFolderNow),
|
|
7076
|
+
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",
|
|
7077
|
+
style: { border: "none", cursor: "pointer" },
|
|
7078
|
+
children: [
|
|
7079
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Download", size: 13 }),
|
|
7080
|
+
t("sync_zip_save_now")
|
|
7081
|
+
]
|
|
7082
|
+
}
|
|
7083
|
+
),
|
|
7084
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7085
|
+
"button",
|
|
7086
|
+
{
|
|
7087
|
+
type: "button",
|
|
7088
|
+
onClick: stopSyncCampaign,
|
|
7089
|
+
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-10 qa-text-mid",
|
|
7090
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
7091
|
+
children: t("sync_stop")
|
|
7092
|
+
}
|
|
7093
|
+
)
|
|
7094
|
+
] })
|
|
6609
7095
|
] }) : (
|
|
6610
7096
|
/* connected: folder chosen, campaign not open yet */
|
|
6611
7097
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-2", children: [
|
|
6612
|
-
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-m-0 qa-flex qa-items-center qa-gap-1.5 qa-text-10 qa-text-mid", children: [
|
|
7098
|
+
!viaZip && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-m-0 qa-flex qa-items-center qa-gap-1.5 qa-text-10 qa-text-mid", children: [
|
|
6613
7099
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Folder", size: 12, className: "qa-shrink-0" }),
|
|
6614
7100
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-truncate qa-dir-ltr", dir: "ltr", children: sync.path })
|
|
6615
7101
|
] }),
|
|
@@ -6631,11 +7117,11 @@ function SettingsSheet({ onClose }) {
|
|
|
6631
7117
|
style: { border: "none", cursor: "pointer" },
|
|
6632
7118
|
children: [
|
|
6633
7119
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 13 }),
|
|
6634
|
-
t("sync_start")
|
|
7120
|
+
viaZip ? t("sync_zip_start") : t("sync_start")
|
|
6635
7121
|
]
|
|
6636
7122
|
}
|
|
6637
7123
|
),
|
|
6638
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7124
|
+
!viaZip && /* @__PURE__ */ jsxRuntime.jsx(
|
|
6639
7125
|
"button",
|
|
6640
7126
|
{
|
|
6641
7127
|
type: "button",
|
|
@@ -7145,12 +7631,31 @@ function Lane({
|
|
|
7145
7631
|
] });
|
|
7146
7632
|
}
|
|
7147
7633
|
function GuideSection() {
|
|
7148
|
-
const {
|
|
7634
|
+
const {
|
|
7635
|
+
guideChecked,
|
|
7636
|
+
toggleGuide,
|
|
7637
|
+
t,
|
|
7638
|
+
journey,
|
|
7639
|
+
journeyIsGeneric,
|
|
7640
|
+
pick: pick2,
|
|
7641
|
+
lang,
|
|
7642
|
+
startTestAlong,
|
|
7643
|
+
startWalk,
|
|
7644
|
+
testAlongSteps
|
|
7645
|
+
} = useQa();
|
|
7149
7646
|
const all = journey.flatMap((g) => g.steps.map((s) => keyOf(g.id, s.path)));
|
|
7150
7647
|
const done = all.filter((k) => guideChecked.has(k)).length;
|
|
7151
7648
|
const pct = all.length > 0 ? Math.round(done / all.length * 100) : 0;
|
|
7152
7649
|
const coverage = computeCoverage(journey, guideChecked);
|
|
7153
7650
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-3", children: [
|
|
7651
|
+
journeyIsGeneric && /* @__PURE__ */ jsxRuntime.jsx(
|
|
7652
|
+
"p",
|
|
7653
|
+
{
|
|
7654
|
+
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",
|
|
7655
|
+
"data-qa-generic-plan": "true",
|
|
7656
|
+
children: t("journey_generic")
|
|
7657
|
+
}
|
|
7658
|
+
),
|
|
7154
7659
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-rounded-xl qa-border qa-border-accent qa-bg-accent-tint qa-p-3 qa-elev-1", children: [
|
|
7155
7660
|
coverage.red.total > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mb-1 qa-flex qa-items-center qa-gap-1.5 qa-text-11", children: [
|
|
7156
7661
|
/* @__PURE__ */ jsxRuntime.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" }),
|
|
@@ -7207,8 +7712,7 @@ function GuideSection() {
|
|
|
7207
7712
|
pick: pick2
|
|
7208
7713
|
},
|
|
7209
7714
|
g.id
|
|
7210
|
-
))
|
|
7211
|
-
journey.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-py-8 qa-text-center qa-text-sm qa-text-slate-400", children: t("tab_guide") })
|
|
7715
|
+
))
|
|
7212
7716
|
] });
|
|
7213
7717
|
}
|
|
7214
7718
|
var TABS = [
|
|
@@ -8053,7 +8557,7 @@ function WalkHud() {
|
|
|
8053
8557
|
)
|
|
8054
8558
|
] });
|
|
8055
8559
|
}
|
|
8056
|
-
const onThisPage = typeof window !== "undefined" && stop.path === window.location.pathname;
|
|
8560
|
+
const onThisPage = stop.path === ANY_PAGE || typeof window !== "undefined" && stop.path === window.location.pathname;
|
|
8057
8561
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
8058
8562
|
"div",
|
|
8059
8563
|
{
|
|
@@ -9151,6 +9655,7 @@ function CaptureMode() {
|
|
|
9151
9655
|
type: "button",
|
|
9152
9656
|
onClick: () => selection && void runCapture(selection.rect),
|
|
9153
9657
|
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",
|
|
9658
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
9154
9659
|
children: [
|
|
9155
9660
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "RotateCcw", size: 13 }),
|
|
9156
9661
|
t("retry")
|
|
@@ -9452,5 +9957,5 @@ function Qapture({ config }) {
|
|
|
9452
9957
|
exports.Qapture = Qapture;
|
|
9453
9958
|
exports.deleteQaDatabase = deleteQaDatabase;
|
|
9454
9959
|
exports.initQaStudio = initQaStudio;
|
|
9455
|
-
//# sourceMappingURL=chunk-
|
|
9456
|
-
//# sourceMappingURL=chunk-
|
|
9960
|
+
//# sourceMappingURL=chunk-WAOYPLZA.cjs.map
|
|
9961
|
+
//# sourceMappingURL=chunk-WAOYPLZA.cjs.map
|