pi-chrome 0.15.47 → 0.15.49
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/CHANGELOG.md +18 -0
- package/README.md +15 -1
- package/SECURITY.md +1 -1
- package/docs/ARCHITECTURE.md +16 -4
- package/docs/COMPARISON.md +1 -1
- package/extensions/chrome-profile-bridge/browser-extension/manifest.json +1 -1
- package/extensions/chrome-profile-bridge/browser-extension/service_worker.js +161 -91
- package/extensions/chrome-profile-bridge/index.ts +97 -106
- package/package.json +2 -2
- package/test-suite/README.md +18 -0
- package/test-suite/challenges/21-keyboard-modifiers.html +5 -2
- package/test-suite/challenges/43-hard-background.html +42 -0
- package/test-suite/challenges/44-input-reliability.html +105 -0
- package/test-suite/manifest.json +119 -2
- package/test-suite/unit/automation-target.test.mjs +1 -1
- package/test-suite/unit/background-policy.test.mjs +489 -0
- package/test-suite/unit/input-reliability.test.mjs +401 -0
package/test-suite/README.md
CHANGED
|
@@ -176,6 +176,24 @@ The dashboard renders this from `manifest.json`. In brief:
|
|
|
176
176
|
40. dynamic wait/readiness
|
|
177
177
|
41. explicit tab lifecycle
|
|
178
178
|
42. strict CSP eval/snapshot via CDP (regression guard for the CSP bypass)
|
|
179
|
+
43. hard background: inactive-tab visibility and trusted input
|
|
180
|
+
44. input reliability: native rich-editor insertion, full replacement, per-character override, Shift chords, single Enter, and file upload
|
|
181
|
+
|
|
182
|
+
### Input regressions (16, 21, 31, 44)
|
|
183
|
+
|
|
184
|
+
Challenge 44 exercises `chrome_type`/`chrome_fill` bulk insertion into contenteditables, including long Unicode text and multiple paragraphs. It also checks `perCharacter:true`, Shift-only printable keys, one Enter per `pressEnter`, and real file contents. Read `window.__inputFixture.caption` and follow its manifest recipe. Use `domFallback:false` to ensure failures are not hidden by synthetic events.
|
|
185
|
+
|
|
186
|
+
Challenge 16 intentionally requests `perCharacter:true`: its grader requires a caret update for each keystroke. Challenge 21 focuses its field before sending Shift+a and grades after Shift release, not during the earlier input event. Challenge 31 retains standalone upload coverage.
|
|
187
|
+
|
|
188
|
+
`npm test` also fault-injects zero/missing/rejected upload node IDs, attachment/cleanup failures, stale UIDs, insertion failures, shortcut suppression, and authorization/option forwarding. These mocked tests cannot prove `isTrusted` or real browser selection behavior; run the browser challenges too.
|
|
189
|
+
|
|
190
|
+
### Hard-background regression (43)
|
|
191
|
+
|
|
192
|
+
Keep `/chrome background on`. Open challenge 43, then manually select a different tab in the **same window** and return to Pi. Target the challenge URL explicitly, arm its probe, and run its manifest recipe without selecting the target again. Inspect the saved screenshot for the colored target. The page detects visibility changes and synthetic input; it cannot establish OS focus or macOS Spaces behavior.
|
|
193
|
+
|
|
194
|
+
Separately live-test with Chrome behind another app, a minimized window, multiple windows, and macOS Spaces. Confirm the foreground app/window and each active tab stay unchanged during background new-tab creation, viewport/full-page screenshots, and trusted input. Confirm `/chrome background off` permits explicit activation. Native prompts/page popups are outside this focus policy, not a zero-focus guarantee.
|
|
195
|
+
|
|
196
|
+
`npm test` runs mocked regressions for every registered tool's policy, old-companion rejection, worker focus writes, CDP capture success/failure, trusted input, and full-page scroll restoration. No live Chrome is required; Node.js 22.13+ is required for TypeScript stripping.
|
|
179
197
|
|
|
180
198
|
## Design notes
|
|
181
199
|
|
|
@@ -30,9 +30,12 @@ for (const name of ["keydown","keypress","keyup","input"]) {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// input fires before keyup. Grade only when Shift is released so a valid chord
|
|
34
|
+
// cannot fail merely because its release events have not happened yet.
|
|
35
|
+
t.addEventListener("keyup", (event) => {
|
|
36
|
+
if (event.key !== "Shift") return;
|
|
35
37
|
const bad = [];
|
|
38
|
+
if (t.value !== "A") bad.push(`value=${JSON.stringify(t.value)} (need 'A')`);
|
|
36
39
|
const downShift = log.find(e => e.name === "keydown" && e.key === "Shift");
|
|
37
40
|
const downA = log.find(e => e.name === "keydown" && e.code === "KeyA");
|
|
38
41
|
const upShift = log.find(e => e.name === "keyup" && e.key === "Shift");
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<meta charset="utf-8">
|
|
3
|
+
<title>43 hard background</title>
|
|
4
|
+
<link rel="stylesheet" href="../_style.css">
|
|
5
|
+
<script src="../_lib.js"></script>
|
|
6
|
+
<body>
|
|
7
|
+
<main>
|
|
8
|
+
<h1>Hard-background target</h1>
|
|
9
|
+
<p>Setup: keep <code>/chrome background on</code>. Select another tab in this same Chrome window so this target is inactive, then return to Pi. Do not select this tab again until verification finishes.</p>
|
|
10
|
+
<p>Runner: target this URL explicitly. Evaluate <code>armBackgroundProbe()</code>, snapshot and screenshot with <code>background:false</code>, then click Verify with <code>domFallback:false</code> and <code>background:false</code>. These foreground requests must not activate this tab. Read <code>window.__verdict</code> without activating it.</p>
|
|
11
|
+
<p>This page grades tab visibility and trusted input, not OS focus or macOS Spaces. Inspect the screenshot for the colored target below; test OS focus separately. Reload to reset.</p>
|
|
12
|
+
<div style="background:#2463aa;color:white;padding:32px;border:16px solid #e5a323">SCREENSHOT TARGET: hard background</div>
|
|
13
|
+
<button id="verify">Verify hidden trusted click</button>
|
|
14
|
+
</main>
|
|
15
|
+
<script>
|
|
16
|
+
Challenge.init({ id: "hard-background", instructions: "inactive tab → arm → snapshot/screenshot → trusted click; no activation" });
|
|
17
|
+
let armed = false;
|
|
18
|
+
window.armBackgroundProbe = () => {
|
|
19
|
+
if (armed) throw new Error("Already armed; reload to reset");
|
|
20
|
+
if (!document.hidden) throw new Error("Select another tab in this window before arming");
|
|
21
|
+
armed = true;
|
|
22
|
+
Challenge.log("armed", { visibility: document.visibilityState, focused: document.hasFocus() });
|
|
23
|
+
return { armed, visibility: document.visibilityState };
|
|
24
|
+
};
|
|
25
|
+
document.addEventListener("visibilitychange", () => {
|
|
26
|
+
Challenge.log("visibilitychange", { visibility: document.visibilityState, armed });
|
|
27
|
+
if (armed && !document.hidden) Challenge.fail("Target became visible after arming: background activation leaked");
|
|
28
|
+
});
|
|
29
|
+
for (const name of ["focus", "blur"]) {
|
|
30
|
+
window.addEventListener(name, () => Challenge.log(name, { visibility: document.visibilityState, focused: document.hasFocus() }));
|
|
31
|
+
}
|
|
32
|
+
document.getElementById("verify").addEventListener("click", (event) => {
|
|
33
|
+
Challenge.log("verify", { trusted: event.isTrusted, visibility: document.visibilityState, armed });
|
|
34
|
+
const failures = [];
|
|
35
|
+
if (!armed) failures.push("Probe was not armed while inactive");
|
|
36
|
+
if (!event.isTrusted) failures.push("Verify click isTrusted=false: synthetic input substituted");
|
|
37
|
+
if (!document.hidden) failures.push("Verify target is not hidden");
|
|
38
|
+
if (failures.length) Challenge.fail(...failures);
|
|
39
|
+
else Challenge.pass("Trusted click received while target remained hidden");
|
|
40
|
+
});
|
|
41
|
+
</script>
|
|
42
|
+
</body>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<meta charset="utf-8">
|
|
3
|
+
<title>44 input reliability</title>
|
|
4
|
+
<link rel="stylesheet" href="../_style.css">
|
|
5
|
+
<script src="../_lib.js"></script>
|
|
6
|
+
<style>
|
|
7
|
+
[contenteditable] { min-height: 48px; max-height: 180px; overflow: auto; padding: 10px; border: 1px solid #777; white-space: pre-wrap; }
|
|
8
|
+
label { display: block; margin-top: 20px; }
|
|
9
|
+
pre { white-space: pre-wrap; }
|
|
10
|
+
</style>
|
|
11
|
+
<body>
|
|
12
|
+
<main>
|
|
13
|
+
<h1>Upload and text-input reliability</h1>
|
|
14
|
+
<p>Read <code>window.__inputFixture.caption</code>. Type it into the empty editor,
|
|
15
|
+
then fill the old multi-paragraph editor with the same text. Leave
|
|
16
|
+
<code>perCharacter</code> off for both. Native text insertion must preserve
|
|
17
|
+
Unicode, newlines, and literal markup without hundreds of key events.</p>
|
|
18
|
+
<details><summary>Exact caption (manual baseline: copy and paste into both editors)</summary><pre id="expected"></pre></details>
|
|
19
|
+
<label for="typed">Empty editor — chrome_type</label>
|
|
20
|
+
<div id="typed" role="textbox" aria-label="Empty editor" contenteditable="true"></div>
|
|
21
|
+
<label for="filled">Old paragraphs — chrome_fill must replace all of them</label>
|
|
22
|
+
<div id="filled" role="textbox" aria-label="Old paragraphs" contenteditable="true"><div>OLD first paragraph</div><div>OLD second paragraph</div><div>OLD third paragraph</div></div>
|
|
23
|
+
<label for="keys">Key-event editor — type Ab! with perCharacter:true</label>
|
|
24
|
+
<div id="keys" role="textbox" aria-label="Key-event editor" contenteditable="true"></div>
|
|
25
|
+
<label for="shift">Shift-only chords — Shift+a, Shift+1, Shift+/ must produce A!?</label>
|
|
26
|
+
<input id="shift" aria-label="Shift-only chords" autocomplete="off">
|
|
27
|
+
<form id="onceForm">
|
|
28
|
+
<label for="once">Type done with pressEnter:true — submit exactly once</label>
|
|
29
|
+
<input id="once" aria-label="Single Enter" autocomplete="off">
|
|
30
|
+
<button type="submit">Submit once</button>
|
|
31
|
+
</form>
|
|
32
|
+
<label for="file">Attach test-suite/fixtures/pi-chrome-upload.txt</label>
|
|
33
|
+
<input id="file" type="file">
|
|
34
|
+
<p>Node-ID conversion failure is fault-injected by the Node unit tests; this page
|
|
35
|
+
checks the real file contents and native upload event.</p>
|
|
36
|
+
<button id="verify" type="button">Verify all input</button>
|
|
37
|
+
</main>
|
|
38
|
+
<script>
|
|
39
|
+
Challenge.init({ id: "input-reliability", instructions: "Complete six fields, then Verify all input" });
|
|
40
|
+
window.__inputFixture = {
|
|
41
|
+
caption: Array.from({ length: 8 }, (_, i) => `${i + 1}. Caption: café 中文 👩🏽💻 — <b>plain text</b>. Keep spaces.\nSecond paragraph.`).join("\n\n"),
|
|
42
|
+
};
|
|
43
|
+
document.getElementById("expected").textContent = window.__inputFixture.caption;
|
|
44
|
+
const metrics = {};
|
|
45
|
+
const chords = [];
|
|
46
|
+
for (const id of ["typed", "filled", "keys", "shift", "once", "file"]) {
|
|
47
|
+
const el = document.getElementById(id);
|
|
48
|
+
metrics[id] = { beforeinput: 0, input: 0, printableKeys: 0, nativeChange: 0 };
|
|
49
|
+
for (const name of ["keydown", "keyup", "beforeinput", "input", "change"]) {
|
|
50
|
+
el.addEventListener(name, (event) => {
|
|
51
|
+
Challenge.log(name, { id, key: event.key, code: event.code, trusted: event.isTrusted, inputType: event.inputType, dataLength: event.data?.length });
|
|
52
|
+
if (event.isTrusted && name === "beforeinput") metrics[id].beforeinput++;
|
|
53
|
+
if (event.isTrusted && name === "input") metrics[id].input++;
|
|
54
|
+
if (event.isTrusted && name === "change") metrics[id].nativeChange++;
|
|
55
|
+
if (name === "keydown" && event.key.length === 1 && !(event.metaKey || event.ctrlKey || event.altKey)) metrics[id].printableKeys++;
|
|
56
|
+
if (id === "shift" && (name === "keydown" || name === "keyup")) {
|
|
57
|
+
chords.push({ name, key: event.key, code: event.code, shiftKey: event.shiftKey, trusted: event.isTrusted });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let submits = 0;
|
|
63
|
+
document.getElementById("onceForm").addEventListener("submit", (event) => {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
submits++;
|
|
66
|
+
Challenge.log("submit", { submits, trusted: event.isTrusted });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// innerText reflects contenteditable line breaks; Chrome may preserve repeated
|
|
70
|
+
// spaces using NBSP nodes, which are equivalent here to their rendered spaces.
|
|
71
|
+
function textOf(id) {
|
|
72
|
+
return document.getElementById(id).innerText.replace(/\r\n/g, "\n").replace(/\u00a0/g, " ");
|
|
73
|
+
}
|
|
74
|
+
document.getElementById("verify").addEventListener("click", async (event) => {
|
|
75
|
+
const bad = [];
|
|
76
|
+
if (!event.isTrusted) bad.push("verification click was synthetic");
|
|
77
|
+
for (const id of ["typed", "filled"]) {
|
|
78
|
+
if (textOf(id) !== window.__inputFixture.caption) bad.push(`${id}: text/paragraphs do not match`);
|
|
79
|
+
if (!metrics[id].beforeinput || !metrics[id].input) bad.push(`${id}: missing native beforeinput/input`);
|
|
80
|
+
if (metrics[id].printableKeys) bad.push(`${id}: ${metrics[id].printableKeys} per-character keydowns (expected native bulk insertion)`);
|
|
81
|
+
if (document.getElementById(id).querySelector("b")) bad.push(`${id}: literal markup became HTML`);
|
|
82
|
+
}
|
|
83
|
+
if (textOf("keys") !== "Ab!" || metrics.keys.printableKeys !== 3 || metrics.keys.input < 3) bad.push("perCharacter editor needs Ab! via three native keystrokes");
|
|
84
|
+
if (document.getElementById("shift").value !== "A!?") bad.push("Shift-only chords did not type A!?");
|
|
85
|
+
const expected = [["A", "KeyA"], ["!", "Digit1"], ["?", "Slash"]];
|
|
86
|
+
for (let i = 0; i < expected.length; i++) {
|
|
87
|
+
const [key, code] = expected[i];
|
|
88
|
+
const group = chords.slice(i * 4, i * 4 + 4);
|
|
89
|
+
if (group.length !== 4 || group.some((e) => !e.trusted) ||
|
|
90
|
+
group[0].name !== "keydown" || group[0].key !== "Shift" ||
|
|
91
|
+
group[1].name !== "keydown" || group[1].key !== key || group[1].code !== code || !group[1].shiftKey ||
|
|
92
|
+
group[2].name !== "keyup" || group[2].key !== key ||
|
|
93
|
+
group[3].name !== "keyup" || group[3].key !== "Shift") bad.push(`bad Shift chord for ${key}`);
|
|
94
|
+
}
|
|
95
|
+
if (chords.length !== 12) bad.push(`Shift chord event count=${chords.length}, expected 12`);
|
|
96
|
+
if (document.getElementById("once").value !== "done" || submits !== 1) bad.push(`single Enter: value or submission count wrong (${submits})`);
|
|
97
|
+
const file = document.getElementById("file").files[0];
|
|
98
|
+
if (!file || file.name !== "pi-chrome-upload.txt" || await file.text() !== "pi-chrome upload fixture\n") bad.push("uploaded file contents do not match fixture");
|
|
99
|
+
if (!metrics.file.nativeChange) bad.push("upload lacks native change event");
|
|
100
|
+
Challenge.log("metrics", { metrics, submits, chords });
|
|
101
|
+
if (bad.length) Challenge.fail(...bad);
|
|
102
|
+
else Challenge.pass("native bulk text, full replacement, key-event override, Shift chords, single Enter, and file upload");
|
|
103
|
+
});
|
|
104
|
+
</script>
|
|
105
|
+
</body>
|
package/test-suite/manifest.json
CHANGED
|
@@ -491,7 +491,7 @@
|
|
|
491
491
|
"id": "contenteditable-selection",
|
|
492
492
|
"file": "challenges/16-contenteditable-selection.html",
|
|
493
493
|
"category": "editing",
|
|
494
|
-
"goal": "Contenteditable typing advances selection/caret
|
|
494
|
+
"goal": "Contenteditable perCharacter typing advances selection/caret for each key (bulk insertion is covered by challenge 44).",
|
|
495
495
|
"expected": {
|
|
496
496
|
"synthetic": "CONDITIONAL",
|
|
497
497
|
"trusted": "PASS",
|
|
@@ -502,7 +502,8 @@
|
|
|
502
502
|
"tool": "chrome_type",
|
|
503
503
|
"params": {
|
|
504
504
|
"selector": "#ed",
|
|
505
|
-
"text": "hello"
|
|
505
|
+
"text": "hello",
|
|
506
|
+
"perCharacter": true
|
|
506
507
|
}
|
|
507
508
|
}
|
|
508
509
|
],
|
|
@@ -687,6 +688,10 @@
|
|
|
687
688
|
"manual": "PASS"
|
|
688
689
|
},
|
|
689
690
|
"recipe": [
|
|
691
|
+
{
|
|
692
|
+
"tool": "chrome_click",
|
|
693
|
+
"params": { "selector": "#t", "domFallback": false }
|
|
694
|
+
},
|
|
690
695
|
{
|
|
691
696
|
"tool": "chrome_key",
|
|
692
697
|
"params": {
|
|
@@ -1565,6 +1570,9 @@
|
|
|
1565
1570
|
"difficulty": "L2",
|
|
1566
1571
|
"gate": "core",
|
|
1567
1572
|
"goal": "Create a new tab, inspect it, close it, return to the parent tab, and verify tab lifecycle state.",
|
|
1573
|
+
"prerequisites": [
|
|
1574
|
+
"Ask the user to run /chrome background off for this explicit activation test. Background on intentionally blocks chrome_tab activate."
|
|
1575
|
+
],
|
|
1568
1576
|
"expected": {
|
|
1569
1577
|
"synthetic": "CONDITIONAL",
|
|
1570
1578
|
"trusted": "PASS",
|
|
@@ -1677,5 +1685,114 @@
|
|
|
1677
1685
|
],
|
|
1678
1686
|
"manualBaseline": "unverified",
|
|
1679
1687
|
"gradeSource": "page"
|
|
1688
|
+
},
|
|
1689
|
+
{
|
|
1690
|
+
"id": "hard-background",
|
|
1691
|
+
"file": "challenges/43-hard-background.html",
|
|
1692
|
+
"category": "focus-keyboard",
|
|
1693
|
+
"difficulty": "L2",
|
|
1694
|
+
"gate": "conditional",
|
|
1695
|
+
"goal": "Snapshot and screenshot an inactive target with background:false, then send a trusted click without activating it. Inspect the screenshot for the colored target.",
|
|
1696
|
+
"expected": {
|
|
1697
|
+
"synthetic": "FAIL",
|
|
1698
|
+
"trusted": "PASS",
|
|
1699
|
+
"manual": "FAIL"
|
|
1700
|
+
},
|
|
1701
|
+
"prerequisites": [
|
|
1702
|
+
"Keep /chrome background on. Reload both Pi and the updated Chrome companion.",
|
|
1703
|
+
"Open the target, manually select a different tab in the same window, then return to Pi before arming. Do not select the target until grading finishes."
|
|
1704
|
+
],
|
|
1705
|
+
"recipe": [
|
|
1706
|
+
{
|
|
1707
|
+
"tool": "chrome_evaluate",
|
|
1708
|
+
"params": {
|
|
1709
|
+
"urlIncludes": "/43-hard-background.html",
|
|
1710
|
+
"expression": "armBackgroundProbe()",
|
|
1711
|
+
"background": true
|
|
1712
|
+
}
|
|
1713
|
+
},
|
|
1714
|
+
{
|
|
1715
|
+
"tool": "chrome_snapshot",
|
|
1716
|
+
"params": {
|
|
1717
|
+
"urlIncludes": "/43-hard-background.html",
|
|
1718
|
+
"background": false
|
|
1719
|
+
}
|
|
1720
|
+
},
|
|
1721
|
+
{
|
|
1722
|
+
"tool": "chrome_screenshot",
|
|
1723
|
+
"params": {
|
|
1724
|
+
"urlIncludes": "/43-hard-background.html",
|
|
1725
|
+
"path": ".pi/chrome-screenshots/hard-background.png",
|
|
1726
|
+
"background": false
|
|
1727
|
+
}
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
"tool": "chrome_click",
|
|
1731
|
+
"params": {
|
|
1732
|
+
"urlIncludes": "/43-hard-background.html",
|
|
1733
|
+
"selector": "#verify",
|
|
1734
|
+
"background": false,
|
|
1735
|
+
"domFallback": false,
|
|
1736
|
+
"includeSnapshot": true
|
|
1737
|
+
}
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
"tool": "chrome_evaluate",
|
|
1741
|
+
"params": {
|
|
1742
|
+
"urlIncludes": "/43-hard-background.html",
|
|
1743
|
+
"expression": "JSON.stringify({v:window.__verdict,r:window.__reason,e:window.__events})",
|
|
1744
|
+
"background": true
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
],
|
|
1748
|
+
"requires": {
|
|
1749
|
+
"cdp": true
|
|
1750
|
+
},
|
|
1751
|
+
"tags": ["background", "visibility", "screenshot", "trusted-input"],
|
|
1752
|
+
"notes": [
|
|
1753
|
+
"The page grades visibility and isTrusted, not OS focus or macOS Spaces. The runner must also inspect the screenshot for target fidelity.",
|
|
1754
|
+
"Manual FAIL is intentional: a normal human click requires selecting the hidden tab. Synthetic FAIL detects replacing Chrome input with DOM events.",
|
|
1755
|
+
"Selecting the target while armed causes sticky FAIL, including after PASS. Read results through background tools before returning to this tab."
|
|
1756
|
+
],
|
|
1757
|
+
"flakeRisk": "medium",
|
|
1758
|
+
"manualBaseline": "unverified",
|
|
1759
|
+
"gradeSource": "both"
|
|
1760
|
+
},
|
|
1761
|
+
{
|
|
1762
|
+
"id": "input-reliability",
|
|
1763
|
+
"file": "challenges/44-input-reliability.html",
|
|
1764
|
+
"category": "editing",
|
|
1765
|
+
"difficulty": "L2",
|
|
1766
|
+
"gate": "core",
|
|
1767
|
+
"goal": "Upload a fixture, enter long Unicode rich-editor text, replace all old paragraphs, retain per-character typing, type Shift-only chords, and submit exactly once.",
|
|
1768
|
+
"expected": { "synthetic": "FAIL", "trusted": "PASS", "manual": "PASS" },
|
|
1769
|
+
"prerequisites": [
|
|
1770
|
+
"Reload Pi and the updated Chrome companion. Snapshot before input; replace descriptive selectors with snapshot UIDs where available.",
|
|
1771
|
+
"Substitute the chrome_evaluate caption result for $CAPTION and expand $PWD to the repository root."
|
|
1772
|
+
],
|
|
1773
|
+
"recipe": [
|
|
1774
|
+
{ "tool": "chrome_evaluate", "params": { "expression": "window.__inputFixture.caption" } },
|
|
1775
|
+
{ "tool": "chrome_type", "params": { "selector": "#typed", "text": "$CAPTION", "includeSnapshot": true } },
|
|
1776
|
+
{ "tool": "chrome_fill", "params": { "selector": "#filled", "text": "$CAPTION", "domFallback": false, "includeSnapshot": true } },
|
|
1777
|
+
{ "tool": "chrome_type", "params": { "selector": "#keys", "text": "Ab!", "perCharacter": true, "includeSnapshot": true } },
|
|
1778
|
+
{ "tool": "chrome_click", "params": { "selector": "#shift", "domFallback": false } },
|
|
1779
|
+
{ "tool": "chrome_key", "params": { "key": "a", "modifiers": { "shiftKey": true } } },
|
|
1780
|
+
{ "tool": "chrome_key", "params": { "key": "1", "modifiers": { "shiftKey": true } } },
|
|
1781
|
+
{ "tool": "chrome_key", "params": { "key": "/", "modifiers": { "shiftKey": true }, "includeSnapshot": true } },
|
|
1782
|
+
{ "tool": "chrome_type", "params": { "selector": "#once", "text": "done", "pressEnter": true, "includeSnapshot": true } },
|
|
1783
|
+
{ "tool": "chrome_upload_file", "params": { "selector": "#file", "paths": ["$PWD/test-suite/fixtures/pi-chrome-upload.txt"] } },
|
|
1784
|
+
{ "tool": "chrome_click", "params": { "selector": "#verify", "domFallback": false, "includeSnapshot": true } },
|
|
1785
|
+
{ "tool": "chrome_evaluate", "params": { "expression": "JSON.stringify({v:window.__verdict,r:window.__reason,e:window.__events.slice(-20)})" } }
|
|
1786
|
+
],
|
|
1787
|
+
"requires": { "cdp": true },
|
|
1788
|
+
"tags": ["upload", "contenteditable", "insertText", "keyboard", "unicode", "modifiers"],
|
|
1789
|
+
"notes": [
|
|
1790
|
+
"Bulk insertion emits native editing events, not a keydown per character or a clipboard paste event. Per-character behavior remains covered by challenge 16 and the explicit keys editor here.",
|
|
1791
|
+
"Manual baseline: paste the exact caption into both editors (select all old paragraphs first), type Ab!, release Shift between each Shift chord, type done then Enter once, and choose the fixture file.",
|
|
1792
|
+
"The page checks actual file contents/native change. Node-ID conversion failure and cleanup are covered by fault-injected worker unit tests, not forced by the page."
|
|
1793
|
+
],
|
|
1794
|
+
"flakeRisk": "low",
|
|
1795
|
+
"manualBaseline": "unverified",
|
|
1796
|
+
"gradeSource": "page"
|
|
1680
1797
|
}
|
|
1681
1798
|
]
|
|
@@ -345,7 +345,7 @@ async function run() {
|
|
|
345
345
|
ok(state.tabs.has(state.userArticle.id), "tab.close: user's active tab was NOT closed");
|
|
346
346
|
ok(state.windows.size === windowsBefore && state.tabs.size === tabsBefore, "tab.close: did not spawn a throwaway tab/window");
|
|
347
347
|
|
|
348
|
-
await throwsWith(() => w.dispatch("tab.activate", { sessionKey: SK }), /no automation tab yet|Pass targetId/, "tab.activate: errors with no target/owned target");
|
|
348
|
+
await throwsWith(() => w.dispatch("tab.activate", { sessionKey: SK, foreground: true }), /no automation tab yet|Pass targetId/, "tab.activate: errors with no target/owned target");
|
|
349
349
|
|
|
350
350
|
// Once an automation target exists, management actions operate on it (not on the user tab).
|
|
351
351
|
const nav = await w.dispatch("page.navigate", { url: "https://pi.test/manage", waitUntilLoad: false, sessionKey: SK });
|