|
| 1 | +# Verification methods |
| 2 | + |
| 3 | +[`../verification.md`](../verification.md) chooses the form; this file holds the patterns that reach behaviour the UI does not expose directly. Each is written twice where the two forms differ: driving a session ([`../../e2e/README.md`](../../e2e/README.md#8-verification-sessions)) and authoring a spec. Failures and gotchas are [`verification-debugging.md`](verification-debugging.md)'s. |
| 4 | + |
| 5 | +## Script execution: GM APIs and injection |
| 6 | + |
| 7 | +Making a userscript actually inject and run needs two things: the `userScripts` permission granted, and the permission prompt answered. |
| 8 | + |
| 9 | +A session grants `userScripts` at `start`, so injection works out of the box. It does **not** auto-approve prompts — a GM API that needs a grant opens `confirm.html`, which you answer like any other page: |
| 10 | + |
| 11 | +```bash |
| 12 | +node e2e/drive.mjs pages # 找到 confirm.html |
| 13 | +node e2e/drive.mjs use <i> |
| 14 | +node e2e/drive.mjs click "[data-testid=confirm-duration-permanent]" |
| 15 | +node e2e/drive.mjs click "[data-testid=confirm-allow]" |
| 16 | +``` |
| 17 | + |
| 18 | +In a spec, `testWithUserScripts` and `autoApprovePermissions` solve both ([`../../e2e/README.md`](../../e2e/README.md#3-harness-chain)) — import them rather than re-deriving the launch dance. |
| 19 | + |
| 20 | +### The in-page self-test pattern |
| 21 | + |
| 22 | +A userscript runs assertions in the page and prints a summary line the harness parses from the console. The bundled scripts in [`../../example/tests/`](../../example/tests/) do this; the line varies by script, and each emits a `通过`/`Passed` and a `失败`/`Failed` count: |
| 23 | + |
| 24 | +``` |
| 25 | +总计: 12 | 通过: 12 | 失败: 0 # inject_content_test.js / sandbox_test.js (combined line) |
| 26 | +总测试数: 12 / 通过: 12 / 失败: 0 # gm_api_sync_test.js / gm_api_async_test.js (counts on separate lines) |
| 27 | +Total: 12 | Passed: 12 | Failed: 0 # window_message_test.js (English) |
| 28 | +``` |
| 29 | + |
| 30 | +In a session there is nothing to wire up — the collector already recorded the line, whichever context printed it (a `@background` script prints from `src/sandbox.html`, not from a page): |
| 31 | + |
| 32 | +```bash |
| 33 | +node e2e/drive.mjs console 200 | grep -E "(通过|Passed)[::] *[0-9]+" |
| 34 | +``` |
| 35 | + |
| 36 | +In a spec, collect and assert on it — this regex matches all three layouts: |
| 37 | + |
| 38 | +```ts |
| 39 | +const logs: string[] = []; |
| 40 | +let passed = -1; |
| 41 | +let failed = -1; |
| 42 | +page.on("console", (msg) => { |
| 43 | + const text = msg.text(); |
| 44 | + logs.push(text); |
| 45 | + const pass = text.match(/(通过|Passed)[::]\s*(\d+)/); |
| 46 | + const fail = text.match(/(失败|Failed)[::]\s*(\d+)/); |
| 47 | + if (pass) passed = parseInt(pass[2], 10); |
| 48 | + if (fail) failed = parseInt(fail[2], 10); |
| 49 | +}); |
| 50 | +// ...navigate to the target page, then: |
| 51 | +expect(failed, logs.join("\n")).toBe(0); |
| 52 | +expect(passed).toBeGreaterThan(0); |
| 53 | +``` |
| 54 | + |
| 55 | +For a new GM API, write a small self-test userscript in the same style. In a session, `node e2e/drive.mjs install <file.user.js>` installs it through the Service Worker and `node e2e/drive.mjs console` shows the summary line the script printed; in a spec, use `installScriptByCode`. Keep the script inside the scenario directory — it is verification scaffolding, not a committed example. |
| 56 | + |
| 57 | +## Behaviour fired from extension UI |
| 58 | + |
| 59 | +The self-test pattern covers only what a userscript observes in the page. Some behaviour is fired from extension UI — a `GM_registerMenuCommand` menu is triggered from the popup. Clicking that button is not drivable ([`verification-debugging.md`](verification-debugging.md#common-gotchas)); sending the message it sends is. |
| 60 | + |
| 61 | +Clients talk to the Service Worker via `chrome.runtime.sendMessage({ action, data })`, where `action` is `<client-prefix>/<method>` and the reply is wrapped as `{ code, data }` — payload is `res.data`, a truthy `code` means error ([`../../packages/message/client.ts`](../../packages/message/client.ts)). Read the tab coordinates you need (`tabId`/`frameId`/`documentId`) from a prior `getPopupData` call. |
| 62 | + |
| 63 | +```ts |
| 64 | +// from a chrome-extension:// page (e.g. options.html); poll until the async registration shows up |
| 65 | +const res = await chrome.runtime.sendMessage({ |
| 66 | + action: "serviceWorker/popup/getPopupData", |
| 67 | + data: { tabId, url }, |
| 68 | +}); |
| 69 | +const script = res.data.scriptList.find((s) => s.menus.some((m) => m.name === "your-menu")); |
| 70 | +await chrome.runtime.sendMessage({ |
| 71 | + action: "serviceWorker/popup/menuClick", |
| 72 | + data: { uuid: script.uuid, menus: script.menus }, // menus carry the target tabId/frameId/documentId |
| 73 | +}); |
| 74 | +``` |
| 75 | + |
| 76 | +From a session the same call is one command, since `eval` already runs on an extension page: |
| 77 | + |
| 78 | +```bash |
| 79 | +node e2e/drive.mjs open options |
| 80 | +node e2e/drive.mjs eval "const [tab] = await chrome.tabs.query({active:true,lastFocusedWindow:true}); if (!tab?.id || !tab.url) throw new Error('no active tab'); const r = await chrome.runtime.sendMessage({action:'serviceWorker/popup/getPopupData', data:{tabId:tab.id, url:tab.url}}); return r.data.scriptList" |
| 81 | +``` |
| 82 | + |
| 83 | +This drives the real SW → content → sandbox → callback path, behaviourally identical to the popup button, which discards the DOM event and calls the same message. It is a substitution: the verdict row names it and says the popup's own click path was not covered. |
| 84 | + |
| 85 | +## A UI change across light and dark theme |
| 86 | + |
| 87 | +The theme is stored in `localStorage` under `lightMode` with value `"light"` / `"dark"` / `"auto"` ([`../../src/pages/components/theme-provider.tsx`](../../src/pages/components/theme-provider.tsx), and [`../../src/pages/common.ts`](../../src/pages/common.ts), which reads the same key during pre-render to avoid a theme flash). Setting it before the page's own scripts run — `context.addInitScript` — is what applies the theme on first paint instead of flashing the default. |
| 88 | + |
| 89 | +Confirm that timing for a `chrome-extension://` page in your own setup before relying on it: `addInitScript` timing relative to an extension page's bootstrap can differ from a normal web page. Capture one screenshot per theme as separate evidence; one theme's screenshot does not show the other renders correctly. |
| 90 | + |
| 91 | +A session has no `addInitScript` hook of its own, so set the key and reload — the pre-render read in `common.ts` then picks it up before first paint: |
| 92 | + |
| 93 | +```bash |
| 94 | +node e2e/drive.mjs eval "localStorage.setItem('lightMode','dark'); return location.reload()" |
| 95 | +node e2e/drive.mjs shot settings-dark |
| 96 | +``` |
0 commit comments