@arraypress/extension-kit 1.9.0 → 1.10.0

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.
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Making a page's elements in a line: an element with its class and text, an
3
+ * icon from the page's SVG sprite, and Chrome's 24px icon button with its
4
+ * tooltip. For extension pages and content-script overlays alike (pass the
5
+ * document the element belongs to).
6
+ *
7
+ * @module dom/make
8
+ */
9
+
10
+ /**
11
+ * An element with a class and text.
12
+ *
13
+ * @template {keyof HTMLElementTagNameMap} K
14
+ * @param {K} tag - Its tag.
15
+ * @param {string} [className] - Its class.
16
+ * @param {string} [text] - Its text.
17
+ * @param {Document} [doc] - Its document. Default `document`.
18
+ * @returns {HTMLElementTagNameMap[K]} It.
19
+ * @example
20
+ * make('span', 'meta', '12 colours');
21
+ */
22
+ export function make(tag, className = '', text = '', doc = document) {
23
+ const element = doc.createElement(tag);
24
+ if (className) element.className = className;
25
+ if (text) element.textContent = text;
26
+ return element;
27
+ }
28
+
29
+ /**
30
+ * An icon from the page's SVG sprite (`<symbol id="i-copy">`), hidden from screen readers.
31
+ *
32
+ * @param {string} id - The symbol's id, without `#`.
33
+ * @param {string} [className] - More classes beside `icon`.
34
+ * @param {Document} [doc] - Its document. Default `document`.
35
+ * @returns {SVGSVGElement} The icon.
36
+ * @example
37
+ * button.append(spriteIcon('i-copy'));
38
+ */
39
+ export function spriteIcon(id, className = '', doc = document) {
40
+ const svg = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
41
+ svg.setAttribute('class', `icon ${className}`.trim());
42
+ svg.setAttribute('aria-hidden', 'true');
43
+ const use = doc.createElementNS('http://www.w3.org/2000/svg', 'use');
44
+ use.setAttribute('href', `#${id}`);
45
+ svg.append(use);
46
+ return svg;
47
+ }
48
+
49
+ /**
50
+ * Chrome's icon button (chrome-ui.css `.icon-button`): an icon, its label as tooltip and for
51
+ * screen readers.
52
+ *
53
+ * @param {object} options
54
+ * @param {string} options.icon - The sprite symbol's id.
55
+ * @param {string} options.label - What it does.
56
+ * @param {() => void} options.onClick - Doing it.
57
+ * @param {string} [options.className] - More classes.
58
+ * @param {Document} [options.doc] - Its document. Default `document`.
59
+ * @returns {HTMLButtonElement} The button.
60
+ * @example
61
+ * iconButton({ icon: 'i-copy', label: 'Copy', onClick: () => copy(hex) });
62
+ */
63
+ export function iconButton({ icon, label, onClick, className = '', doc = document }) {
64
+ const button = make('button', `icon-button ${className}`.trim(), '', doc);
65
+ button.type = 'button';
66
+ button.title = label;
67
+ button.setAttribute('aria-label', label);
68
+ button.append(spriteIcon(icon, '', doc));
69
+ button.addEventListener('click', onClick);
70
+ return button;
71
+ }
@@ -27,6 +27,13 @@ export declare const MAC_WINDOW: Readonly<{
27
27
  }>;
28
28
  /** The window's default width (px): the store screenshot's. */
29
29
  export declare const WINDOW_W = 1152;
30
+ /**
31
+ * Text made safe for the drawn windows' HTML (and the store screenshots').
32
+ *
33
+ * @param {unknown} s - Text.
34
+ * @returns {string} It, escaped for HTML text and attributes.
35
+ */
36
+ export declare const esc: (s: unknown) => string;
30
37
  /**
31
38
  * A letter favicon for made-up sites (no real brands).
32
39
  *
@@ -26,10 +26,12 @@ export const MAC_WINDOW = Object.freeze({ titleBar: 32, radius: 16 });
26
26
  export const WINDOW_W = 1152;
27
27
 
28
28
  /**
29
+ * Text made safe for the drawn windows' HTML (and the store screenshots').
30
+ *
29
31
  * @param {unknown} s - Text.
30
32
  * @returns {string} It, escaped for HTML text and attributes.
31
33
  */
32
- const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
34
+ export const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
33
35
 
34
36
  /** Material icon paths (24×24). */
35
37
  const P = {
package/src/index.d.ts CHANGED
@@ -3,8 +3,9 @@
3
3
  * service worker and content scripts load, Chrome's page components (the ⋮
4
4
  * menu, the text dialog, the toast with Undo, a settings card's site list,
5
5
  * the built-in AI row), Chrome's window drawn around a capture, per-site lists' sites, Chrome's on-device model,
6
- * a content script's slices, change watcher and overlay, a module's function called in a page,
7
- * and a tab's side panel opened from a click. Every function takes the `chrome`
6
+ * a content script's slices, change watcher, overlay and “Show on page” boxes, a module's
7
+ * function called in a page or every frame of a tab, a tab's side panel opened from a click,
8
+ * copying (plain text and HTML), and making elements and icon buttons in a line. Every function takes the `chrome`
8
9
  * APIs it uses as arguments, so tests run the real logic against fakes.
9
10
  *
10
11
  * Build-time helpers are in `@arraypress/extension-kit/build`, test helpers
@@ -17,16 +18,22 @@ export { githubAboutLinks, isPlaceholder } from './about/links.js';
17
18
  export { MODEL_MESSAGES, chromeLanguageModel, createModelSession, describeModel, downloadModel, modelAvailability, } from './ai/model.js';
18
19
  export { setShortcuts } from './commands/shortcuts.js';
19
20
  export { BLOCK_CHARS, SLICE_MS, blocksOf, eachInSlices, inSlices } from './content/slices.js';
21
+ export { MAX_ELEMENTS, otherSiteFrames, renderedElements, shadowRootOf } from './content/elements.js';
22
+ export { HIGHLIGHT_CSS, MAX_BOXES, createHighlighter } from './content/highlight.js';
20
23
  export { pageOverlay } from './content/overlay.js';
24
+ export { PAGE_TOAST_MS, showPageToast } from './content/page-toast.js';
21
25
  export { MAX_WAIT_MS, SETTLE_MS, watchChanges } from './content/watch.js';
26
+ export { copyFormats } from './dom/copy.js';
22
27
  export { focusedElement } from './dom/focus.js';
28
+ export { iconButton, make, spriteIcon } from './dom/make.js';
23
29
  export { MAC_WINDOW, THEMES, WINDOW_BARS, WINDOW_W, letterIcon, macWindowSvg, windowChrome, windowChromeSvg } from './frame/window.js';
24
- export { localize, translator } from './dom/i18n.js';
30
+ export { counted, localize, translator } from './dom/i18n.js';
25
31
  export { byId, setAboutLinks, statusFlasher, valueRow } from './dom/page.js';
26
32
  export { setUpPage } from './dom/setup.js';
27
33
  export { openSidePanel, sidePanelPath } from './panel/side-panel.js';
28
34
  export { callInPage, importAndCall } from './scripting/call.js';
29
35
  export { contentScriptSync } from './scripting/registration.js';
36
+ export { callInTab, isBrowserPage } from './scripting/tab.js';
30
37
  export { createSettingsSaver } from './settings/saver.js';
31
38
  export { MAX_SYNC_ITEM_BYTES } from './settings/storage.js';
32
39
  export { createSettingsStore } from './settings/store.js';
@@ -40,6 +47,7 @@ export { MENU_EDGE, createMenu, isInside, menuPosition, moreActionsButton } from
40
47
  export { createModelRow } from './ui/model-row.js';
41
48
  export { SITE_LIST_MESSAGES, SITE_LIST_WORDS, createSiteList } from './ui/site-list.js';
42
49
  export { askFields, askText, checkText, fieldIds } from './ui/text-dialog.js';
50
+ export { noticeCard } from './ui/notice.js';
43
51
  export { TOAST_MS, createToast } from './ui/toast.js';
44
52
  export type Availability = import('./ai/model.js').Availability;
45
53
  export type InitialPrompt = import('./ai/model.js').InitialPrompt;
@@ -50,7 +58,9 @@ export type ModelAnswer = import('./ai/model.js').ModelAnswer;
50
58
  export type ModelSession = import('./ai/model.js').ModelSession;
51
59
  export type ModelStatus = import('./ai/model.js').ModelStatus;
52
60
  export type SliceOptions = import('./content/slices.js').SliceOptions;
61
+ export type Highlighter = import('./content/highlight.js').Highlighter;
53
62
  export type PageOverlay = import('./content/overlay.js').PageOverlay;
63
+ export type ClipboardFormats = import('./dom/copy.js').ClipboardFormats;
54
64
  export type WatchHandlers = import('./content/watch.js').WatchHandlers;
55
65
  export type WindowTab = import('./frame/window.js').Tab;
56
66
  export type WindowTheme = import('./frame/window.js').Theme;
@@ -58,6 +68,8 @@ export type WindowShadow = import('./frame/window.js').WindowShadow;
58
68
  export type ModelRow = import('./ui/model-row.js').ModelRow;
59
69
  export type ContentScript = import('./scripting/registration.js').ContentScript;
60
70
  export type FrameResult = import('./scripting/call.js').FrameResult<unknown>;
71
+ export type TabReading = import('./scripting/tab.js').TabReading<unknown>;
72
+ export type TabEdges = import('./scripting/tab.js').TabEdges;
61
73
  export type ActionMenu = import('./ui/menu.js').ActionMenu;
62
74
  export type MenuEntry = import('./ui/menu.js').MenuEntry;
63
75
  export type MenuItem = import('./ui/menu.js').MenuItem;
@@ -66,6 +78,7 @@ export type SiteList = import('./ui/site-list.js').SiteList;
66
78
  export type FieldRequest = import('./ui/text-dialog.js').FieldRequest;
67
79
  export type FieldsRequest = import('./ui/text-dialog.js').FieldsRequest;
68
80
  export type TextRequest = import('./ui/text-dialog.js').TextRequest;
81
+ export type Notice = import('./ui/notice.js').Notice;
69
82
  export type Toast = import('./ui/toast.js').Toast;
70
83
  /**
71
84
  * The page helpers' types, for a page's JSDoc.
@@ -79,7 +92,9 @@ export type Toast = import('./ui/toast.js').Toast;
79
92
  * @typedef {import('./ai/model.js').ModelSession} ModelSession
80
93
  * @typedef {import('./ai/model.js').ModelStatus} ModelStatus
81
94
  * @typedef {import('./content/slices.js').SliceOptions} SliceOptions
95
+ * @typedef {import('./content/highlight.js').Highlighter} Highlighter
82
96
  * @typedef {import('./content/overlay.js').PageOverlay} PageOverlay
97
+ * @typedef {import('./dom/copy.js').ClipboardFormats} ClipboardFormats
83
98
  * @typedef {import('./content/watch.js').WatchHandlers} WatchHandlers
84
99
  * @typedef {import('./frame/window.js').Tab} WindowTab
85
100
  * @typedef {import('./frame/window.js').Theme} WindowTheme
@@ -87,6 +102,8 @@ export type Toast = import('./ui/toast.js').Toast;
87
102
  * @typedef {import('./ui/model-row.js').ModelRow} ModelRow
88
103
  * @typedef {import('./scripting/registration.js').ContentScript} ContentScript
89
104
  * @typedef {import('./scripting/call.js').FrameResult<unknown>} FrameResult
105
+ * @typedef {import('./scripting/tab.js').TabReading<unknown>} TabReading
106
+ * @typedef {import('./scripting/tab.js').TabEdges} TabEdges
90
107
  * @typedef {import('./ui/menu.js').ActionMenu} ActionMenu
91
108
  * @typedef {import('./ui/menu.js').MenuEntry} MenuEntry
92
109
  * @typedef {import('./ui/menu.js').MenuItem} MenuItem
@@ -95,5 +112,6 @@ export type Toast = import('./ui/toast.js').Toast;
95
112
  * @typedef {import('./ui/text-dialog.js').FieldRequest} FieldRequest
96
113
  * @typedef {import('./ui/text-dialog.js').FieldsRequest} FieldsRequest
97
114
  * @typedef {import('./ui/text-dialog.js').TextRequest} TextRequest
115
+ * @typedef {import('./ui/notice.js').Notice} Notice
98
116
  * @typedef {import('./ui/toast.js').Toast} Toast
99
117
  */
package/src/index.js CHANGED
@@ -3,8 +3,9 @@
3
3
  * service worker and content scripts load, Chrome's page components (the ⋮
4
4
  * menu, the text dialog, the toast with Undo, a settings card's site list,
5
5
  * the built-in AI row), Chrome's window drawn around a capture, per-site lists' sites, Chrome's on-device model,
6
- * a content script's slices, change watcher and overlay, a module's function called in a page,
7
- * and a tab's side panel opened from a click. Every function takes the `chrome`
6
+ * a content script's slices, change watcher, overlay and “Show on page” boxes, a module's
7
+ * function called in a page or every frame of a tab, a tab's side panel opened from a click,
8
+ * copying (plain text and HTML), and making elements and icon buttons in a line. Every function takes the `chrome`
8
9
  * APIs it uses as arguments, so tests run the real logic against fakes.
9
10
  *
10
11
  * Build-time helpers are in `@arraypress/extension-kit/build`, test helpers
@@ -20,16 +21,22 @@ export {
20
21
  } from './ai/model.js';
21
22
  export { setShortcuts } from './commands/shortcuts.js';
22
23
  export { BLOCK_CHARS, SLICE_MS, blocksOf, eachInSlices, inSlices } from './content/slices.js';
24
+ export { MAX_ELEMENTS, otherSiteFrames, renderedElements, shadowRootOf } from './content/elements.js';
25
+ export { HIGHLIGHT_CSS, MAX_BOXES, createHighlighter } from './content/highlight.js';
23
26
  export { pageOverlay } from './content/overlay.js';
27
+ export { PAGE_TOAST_MS, showPageToast } from './content/page-toast.js';
24
28
  export { MAX_WAIT_MS, SETTLE_MS, watchChanges } from './content/watch.js';
29
+ export { copyFormats } from './dom/copy.js';
25
30
  export { focusedElement } from './dom/focus.js';
31
+ export { iconButton, make, spriteIcon } from './dom/make.js';
26
32
  export { MAC_WINDOW, THEMES, WINDOW_BARS, WINDOW_W, letterIcon, macWindowSvg, windowChrome, windowChromeSvg } from './frame/window.js';
27
- export { localize, translator } from './dom/i18n.js';
33
+ export { counted, localize, translator } from './dom/i18n.js';
28
34
  export { byId, setAboutLinks, statusFlasher, valueRow } from './dom/page.js';
29
35
  export { setUpPage } from './dom/setup.js';
30
36
  export { openSidePanel, sidePanelPath } from './panel/side-panel.js';
31
37
  export { callInPage, importAndCall } from './scripting/call.js';
32
38
  export { contentScriptSync } from './scripting/registration.js';
39
+ export { callInTab, isBrowserPage } from './scripting/tab.js';
33
40
  export { createSettingsSaver } from './settings/saver.js';
34
41
  export { MAX_SYNC_ITEM_BYTES } from './settings/storage.js';
35
42
  export { createSettingsStore } from './settings/store.js';
@@ -47,6 +54,7 @@ export { MENU_EDGE, createMenu, isInside, menuPosition, moreActionsButton } from
47
54
  export { createModelRow } from './ui/model-row.js';
48
55
  export { SITE_LIST_MESSAGES, SITE_LIST_WORDS, createSiteList } from './ui/site-list.js';
49
56
  export { askFields, askText, checkText, fieldIds } from './ui/text-dialog.js';
57
+ export { noticeCard } from './ui/notice.js';
50
58
  export { TOAST_MS, createToast } from './ui/toast.js';
51
59
 
52
60
  /**
@@ -61,7 +69,9 @@ export { TOAST_MS, createToast } from './ui/toast.js';
61
69
  * @typedef {import('./ai/model.js').ModelSession} ModelSession
62
70
  * @typedef {import('./ai/model.js').ModelStatus} ModelStatus
63
71
  * @typedef {import('./content/slices.js').SliceOptions} SliceOptions
72
+ * @typedef {import('./content/highlight.js').Highlighter} Highlighter
64
73
  * @typedef {import('./content/overlay.js').PageOverlay} PageOverlay
74
+ * @typedef {import('./dom/copy.js').ClipboardFormats} ClipboardFormats
65
75
  * @typedef {import('./content/watch.js').WatchHandlers} WatchHandlers
66
76
  * @typedef {import('./frame/window.js').Tab} WindowTab
67
77
  * @typedef {import('./frame/window.js').Theme} WindowTheme
@@ -69,6 +79,8 @@ export { TOAST_MS, createToast } from './ui/toast.js';
69
79
  * @typedef {import('./ui/model-row.js').ModelRow} ModelRow
70
80
  * @typedef {import('./scripting/registration.js').ContentScript} ContentScript
71
81
  * @typedef {import('./scripting/call.js').FrameResult<unknown>} FrameResult
82
+ * @typedef {import('./scripting/tab.js').TabReading<unknown>} TabReading
83
+ * @typedef {import('./scripting/tab.js').TabEdges} TabEdges
72
84
  * @typedef {import('./ui/menu.js').ActionMenu} ActionMenu
73
85
  * @typedef {import('./ui/menu.js').MenuEntry} MenuEntry
74
86
  * @typedef {import('./ui/menu.js').MenuItem} MenuItem
@@ -77,5 +89,6 @@ export { TOAST_MS, createToast } from './ui/toast.js';
77
89
  * @typedef {import('./ui/text-dialog.js').FieldRequest} FieldRequest
78
90
  * @typedef {import('./ui/text-dialog.js').FieldsRequest} FieldsRequest
79
91
  * @typedef {import('./ui/text-dialog.js').TextRequest} TextRequest
92
+ * @typedef {import('./ui/notice.js').Notice} Notice
80
93
  * @typedef {import('./ui/toast.js').Toast} Toast
81
94
  */
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Reading a tab: one of the extension's page modules called in every frame
3
+ * Chrome lets it run in, the frames' answers top first; or why it can't be
4
+ * read: a page Chrome keeps to itself (its own pages, the Web Store, other
5
+ * extensions'), or no access (the tab went to another site since the click
6
+ * that gave activeTab).
7
+ *
8
+ * @module scripting/tab
9
+ */
10
+ import type { FrameResult } from './call.js';
11
+ /**
12
+ * Whether Chrome keeps a page from extensions.
13
+ *
14
+ * @param {string} url - The page's address.
15
+ * @returns {boolean} It does.
16
+ * @example
17
+ * isBrowserPage('chrome://settings'); // true
18
+ * isBrowserPage('https://chromewebstore.google.com/detail/x'); // true
19
+ */
20
+ export declare const isBrowserPage: (url: string) => boolean;
21
+ export type TabReading<T> = {
22
+ state: 'read';
23
+ frames: FrameResult<T>[];
24
+ } | {
25
+ state: 'browser';
26
+ } | {
27
+ state: 'no-access';
28
+ };
29
+ export type TabEdges = {
30
+ /**
31
+ * `chrome.tabs`.
32
+ */
33
+ tabs: Pick<typeof chrome.tabs, 'get'>;
34
+ /**
35
+ * `chrome.scripting`.
36
+ */
37
+ scripting: Pick<typeof chrome.scripting, 'executeScript'>;
38
+ };
39
+ /**
40
+ * What reading a tab gave.
41
+ *
42
+ * @template T
43
+ * @typedef {{state: 'read', frames: FrameResult<T>[]} | {state: 'browser'} | {state: 'no-access'}} TabReading
44
+ * `frames`: each frame that answered (not null), the top one first, then in Chrome's frame order.
45
+ */
46
+ /**
47
+ * The Chrome APIs a reading uses (fakes in unit tests).
48
+ *
49
+ * @typedef {object} TabEdges
50
+ * @property {Pick<typeof chrome.tabs, 'get'>} tabs `chrome.tabs`.
51
+ * @property {Pick<typeof chrome.scripting, 'executeScript'>} scripting `chrome.scripting`.
52
+ */
53
+ /**
54
+ * Call a page module's export in every frame of a tab, or say why it can't be.
55
+ *
56
+ * @template [T=unknown]
57
+ * @param {TabEdges} edges - Chrome's tabs and scripting.
58
+ * @param {number} tabId - The tab.
59
+ * @param {string} module - The module (in `web_accessible_resources`).
60
+ * @param {string} name - The export.
61
+ * @param {unknown[]} [args] - Its arguments.
62
+ * @returns {Promise<TabReading<T>>} The frames' answers, or why there are none.
63
+ * @example
64
+ * const reading = await callInTab(chrome, tabId, 'content/main.js', 'scan');
65
+ * if (reading.state === 'read') show(reading.frames);
66
+ * else showMessage(reading.state); // 'browser' or 'no-access'
67
+ */
68
+ export declare function callInTab<T = unknown>(edges: TabEdges, tabId: number, module: string, name: string, args?: unknown[]): Promise<TabReading<T>>;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Reading a tab: one of the extension's page modules called in every frame
3
+ * Chrome lets it run in, the frames' answers top first; or why it can't be
4
+ * read: a page Chrome keeps to itself (its own pages, the Web Store, other
5
+ * extensions'), or no access (the tab went to another site since the click
6
+ * that gave activeTab).
7
+ *
8
+ * @module scripting/tab
9
+ */
10
+
11
+ import { callInPage } from './call.js';
12
+
13
+ /** @import { FrameResult } from './call.js' */
14
+
15
+ /** Addresses Chrome keeps from every extension. */
16
+ const BROWSER_PAGE = /^(?:chrome|chrome-extension|chrome-untrusted|devtools|edge|about|view-source):|^https:\/\/(?:chromewebstore\.google\.com|chrome\.google\.com\/webstore)\//;
17
+
18
+ /** Chrome's refusals for its own pages, when the page's address isn't known (no access to the tab). */
19
+ const BROWSER_REFUSAL = /Cannot access a (?:chrome|chrome-extension|chrome-untrusted|devtools|edge):\/\/ URL|extensions gallery cannot be scripted/i;
20
+
21
+ /**
22
+ * Whether Chrome keeps a page from extensions.
23
+ *
24
+ * @param {string} url - The page's address.
25
+ * @returns {boolean} It does.
26
+ * @example
27
+ * isBrowserPage('chrome://settings'); // true
28
+ * isBrowserPage('https://chromewebstore.google.com/detail/x'); // true
29
+ */
30
+ export const isBrowserPage = (url) => BROWSER_PAGE.test(url);
31
+
32
+ /**
33
+ * What reading a tab gave.
34
+ *
35
+ * @template T
36
+ * @typedef {{state: 'read', frames: FrameResult<T>[]} | {state: 'browser'} | {state: 'no-access'}} TabReading
37
+ * `frames`: each frame that answered (not null), the top one first, then in Chrome's frame order.
38
+ */
39
+
40
+ /**
41
+ * The Chrome APIs a reading uses (fakes in unit tests).
42
+ *
43
+ * @typedef {object} TabEdges
44
+ * @property {Pick<typeof chrome.tabs, 'get'>} tabs `chrome.tabs`.
45
+ * @property {Pick<typeof chrome.scripting, 'executeScript'>} scripting `chrome.scripting`.
46
+ */
47
+
48
+ /**
49
+ * Call a page module's export in every frame of a tab, or say why it can't be.
50
+ *
51
+ * @template [T=unknown]
52
+ * @param {TabEdges} edges - Chrome's tabs and scripting.
53
+ * @param {number} tabId - The tab.
54
+ * @param {string} module - The module (in `web_accessible_resources`).
55
+ * @param {string} name - The export.
56
+ * @param {unknown[]} [args] - Its arguments.
57
+ * @returns {Promise<TabReading<T>>} The frames' answers, or why there are none.
58
+ * @example
59
+ * const reading = await callInTab(chrome, tabId, 'content/main.js', 'scan');
60
+ * if (reading.state === 'read') show(reading.frames);
61
+ * else showMessage(reading.state); // 'browser' or 'no-access'
62
+ */
63
+ export async function callInTab(edges, tabId, module, name, args = []) {
64
+ let url;
65
+ try {
66
+ url = (await edges.tabs.get(tabId)).url;
67
+ } catch {
68
+ return { state: 'no-access' };
69
+ }
70
+ if (url && isBrowserPage(url)) return { state: 'browser' };
71
+ /** @type {FrameResult<T>[]} */
72
+ let frames;
73
+ try {
74
+ frames = await callInPage(edges.scripting, { tabId, allFrames: true }, module, name, args);
75
+ } catch (error) {
76
+ // Chrome's own page (said in its refusal), or no access: the tab went to another site since the click.
77
+ return { state: BROWSER_REFUSAL.test(String(/** @type {Error} */ (error)?.message)) ? 'browser' : 'no-access' };
78
+ }
79
+ const read = frames.filter((frame) => frame.result !== null && frame.result !== undefined).sort((a, b) => a.frameId - b.frameId);
80
+ return read.length ? { state: 'read', frames: read } : { state: 'no-access' };
81
+ }
@@ -1063,6 +1063,13 @@ label.row, .row[actionable] { cursor: pointer; }
1063
1063
  .sp-item:is(:hover, :focus-within) .sp-suffix,
1064
1064
  .sp-suffix.always { display: flex; }
1065
1065
 
1066
+ /* .sp-notice (ui/notice.js): a card saying what a list leaves out or can't do; a warning's icon in
1067
+ the error colour, an information's in the secondary text colour. */
1068
+ .sp-notice .sp-heading > .icon { flex: none; width: 18px; height: 18px; color: var(--error); }
1069
+ .sp-notice.info .sp-heading > .icon { color: var(--text-secondary); }
1070
+ .sp-notice-text { margin: 0; padding: 2px 16px 8px; color: var(--text-secondary); font-size: 12px; line-height: 16px; }
1071
+ .sp-notice-actions { display: flex; justify-content: flex-end; padding: 0 16px 8px; }
1072
+
1066
1073
  /* sp-empty-state: a centred heading and a line of help. */
1067
1074
  .sp-empty {
1068
1075
  display: flex;
@@ -10,12 +10,14 @@ export { TEST_BUILDS_ENV, testBuildDir } from '../build/test-build.js';
10
10
  export { chromeUserAgent, closeOnSignal, realisticBrowser } from './browser.js';
11
11
  export { CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, runChecks, until, } from './e2e.js';
12
12
  export { exitPoint, parseLiveOptions, parseProxy } from './live.js';
13
+ export { liveUsage, runLiveCheck } from './live-run.js';
13
14
  export { eachLimited } from '../time/pool.js';
14
15
  export { filesIn, launchDownloading } from './downloads.js';
15
16
  export { fakeLanguageModel } from './fake-model.js';
16
17
  export { messageLookup, readMessages } from './messages.js';
17
18
  export { heartbeat } from './heartbeat.js';
18
19
  export { playPermissionPrompt } from './prompt.js';
20
+ export { callTabModule, nodeAttribute, nodeText, pierceNodes, tabIdOf } from './pages.js';
19
21
  export { serve, testCertificate } from './server.js';
20
22
  export { SYNC_QUOTA_BYTES, SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
21
23
  export { filesUnder, manifestReferences, missingReferences, referencedPath, zipEntries } from './package.js';
@@ -28,8 +30,10 @@ export type ExtensionProfile = import('./e2e.js').ExtensionProfile;
28
30
  export type LaunchedExtension = import('./e2e.js').LaunchedExtension;
29
31
  export type LaunchOptions = import('./e2e.js').LaunchOptions;
30
32
  export type Messages = import('./messages.js').Messages;
33
+ export type CdpNode = import('./pages.js').CdpNode;
31
34
  export type Exit = import('./live.js').Exit;
32
35
  export type LiveOptions = import('./live.js').LiveOptions;
36
+ export type LiveSite = import('./live-run.js').LiveSite;
33
37
  export type SavedTabGroup = import('./saved-groups.js').SavedTabGroup;
34
38
  export type Manifest = import('./warnings.js').Manifest;
35
39
  export type Reply = import('./server.js').Reply;
@@ -45,8 +49,10 @@ export type TestServer = import('./server.js').TestServer;
45
49
  * @typedef {import('./e2e.js').LaunchedExtension} LaunchedExtension
46
50
  * @typedef {import('./e2e.js').LaunchOptions} LaunchOptions
47
51
  * @typedef {import('./messages.js').Messages} Messages
52
+ * @typedef {import('./pages.js').CdpNode} CdpNode
48
53
  * @typedef {import('./live.js').Exit} Exit
49
54
  * @typedef {import('./live.js').LiveOptions} LiveOptions
55
+ * @typedef {import('./live-run.js').LiveSite} LiveSite
50
56
  * @typedef {import('./saved-groups.js').SavedTabGroup} SavedTabGroup
51
57
  * @typedef {import('./warnings.js').Manifest} Manifest
52
58
  * @typedef {import('./server.js').Reply} Reply
@@ -13,12 +13,14 @@ export {
13
13
  CHECK_MS, checkRunner, checkToolbarTheme, extensionProfile, launchExtension, offscreenEmulator, recordIconCalls, runChecks, until,
14
14
  } from './e2e.js';
15
15
  export { exitPoint, parseLiveOptions, parseProxy } from './live.js';
16
+ export { liveUsage, runLiveCheck } from './live-run.js';
16
17
  export { eachLimited } from '../time/pool.js';
17
18
  export { filesIn, launchDownloading } from './downloads.js';
18
19
  export { fakeLanguageModel } from './fake-model.js';
19
20
  export { messageLookup, readMessages } from './messages.js';
20
21
  export { heartbeat } from './heartbeat.js';
21
22
  export { playPermissionPrompt } from './prompt.js';
23
+ export { callTabModule, nodeAttribute, nodeText, pierceNodes, tabIdOf } from './pages.js';
22
24
  export { serve, testCertificate } from './server.js';
23
25
  export { SYNC_QUOTA_BYTES, SYNC_WRITES_PER_MINUTE, fakeChromeStorage } from './fake-storage.js';
24
26
  export { filesUnder, manifestReferences, missingReferences, referencedPath, zipEntries } from './package.js';
@@ -35,8 +37,10 @@ export { installWarnings, warningsAddedBy, withoutPermissions } from './warnings
35
37
  * @typedef {import('./e2e.js').LaunchedExtension} LaunchedExtension
36
38
  * @typedef {import('./e2e.js').LaunchOptions} LaunchOptions
37
39
  * @typedef {import('./messages.js').Messages} Messages
40
+ * @typedef {import('./pages.js').CdpNode} CdpNode
38
41
  * @typedef {import('./live.js').Exit} Exit
39
42
  * @typedef {import('./live.js').LiveOptions} LiveOptions
43
+ * @typedef {import('./live-run.js').LiveSite} LiveSite
40
44
  * @typedef {import('./saved-groups.js').SavedTabGroup} SavedTabGroup
41
45
  * @typedef {import('./warnings.js').Manifest} Manifest
42
46
  * @typedef {import('./server.js').Reply} Reply
@@ -0,0 +1,130 @@
1
+ /**
2
+ * A live checker's run: the command line read (`parseLiveOptions`), the
3
+ * sites chosen, where the traffic leaves for the internet printed, then each
4
+ * site visited in a browser with the extension loaded, a few at once (one
5
+ * browser a slot, launched on first use, passing for a person's), each within
6
+ * a time limit (a browser whose page ran out of time is replaced), Ctrl-C
7
+ * closing every browser. What a visit checks, and the report, are the
8
+ * checker's own.
9
+ *
10
+ * @module testing/live-run
11
+ */
12
+ import { exitPoint } from './live.js';
13
+ import type { BrowserType, Page } from 'playwright';
14
+ import type { LaunchedExtension } from './e2e.js';
15
+ import type { Exit } from './live.js';
16
+ export type LiveSite = {
17
+ name: string;
18
+ url: string;
19
+ category: string;
20
+ };
21
+ export type LiveRun<S extends LiveSite, R> = {
22
+ /**
23
+ * The report's folder (made empty).
24
+ */
25
+ dir: string;
26
+ /**
27
+ * When it started (ISO).
28
+ */
29
+ started: string;
30
+ /**
31
+ * When it finished (ISO).
32
+ */
33
+ finished: string;
34
+ /**
35
+ * Where the traffic left.
36
+ */
37
+ exit: Exit | null;
38
+ /**
39
+ * How the browsers looked.
40
+ */
41
+ browser: {
42
+ locale: string;
43
+ timezone: string;
44
+ proxy: string;
45
+ };
46
+ /**
47
+ * Each site's visit, in the sites' order.
48
+ */
49
+ results: {
50
+ site: S;
51
+ visit: R;
52
+ }[];
53
+ };
54
+ /** @import { BrowserType, Page } from 'playwright' */
55
+ /** @import { LaunchedExtension } from './e2e.js' */
56
+ /** @import { Exit } from './live.js' */
57
+ /**
58
+ * A site a checker visits.
59
+ *
60
+ * @typedef {{name: string, url: string, category: string}} LiveSite
61
+ */
62
+ /**
63
+ * A finished run, for the checker's report.
64
+ *
65
+ * @template {LiveSite} S
66
+ * @template R
67
+ * @typedef {object} LiveRun
68
+ * @property {string} dir The report's folder (made empty).
69
+ * @property {string} started When it started (ISO).
70
+ * @property {string} finished When it finished (ISO).
71
+ * @property {Exit | null} exit Where the traffic left.
72
+ * @property {{locale: string, timezone: string, proxy: string}} browser How the browsers looked.
73
+ * @property {{site: S, visit: R}[]} results Each site's visit, in the sites' order.
74
+ */
75
+ /**
76
+ * A live checker's usage text.
77
+ *
78
+ * @param {string[]} categories - The categories' ids.
79
+ * @param {number} concurrency - The default and most sites at once.
80
+ * @returns {string} The text.
81
+ */
82
+ export declare const liveUsage: (categories: string[], concurrency: number) => string;
83
+ /**
84
+ * Run a live check: read the command line, visit the chosen sites, give back every visit.
85
+ * Prints the usage and exits for `--help` (or an option it doesn't know), and exits when no
86
+ * site matches.
87
+ *
88
+ * @template {LiveSite} S
89
+ * @template R
90
+ * @param {object} run
91
+ * @param {BrowserType} run.chromium - Playwright's chromium.
92
+ * @param {string} run.build - The extension's live test build (every site granted).
93
+ * @param {string} run.prefix - The browsers' profile prefix.
94
+ * @param {string} run.root - The extension's repository (the report goes in `test/live/report`).
95
+ * @param {S[]} run.sites - Every site.
96
+ * @param {string[]} run.categories - The categories' ids, for the usage.
97
+ * @param {number} run.pageMs - The most a visit may take.
98
+ * @param {(browser: LaunchedExtension, page: Page, site: S) => Promise<R>} run.visit - Check one site, on a fresh page.
99
+ * @param {(site: S, error: string) => R} run.failed - A visit that couldn't finish (`'timed out'`).
100
+ * @param {(result: {site: S, visit: R}) => void} [run.onResult] - Each visit as it finishes (a line of output).
101
+ * @param {{argv?: string[], env?: Record<string, string | undefined>, concurrency?: number, findExit?: typeof exitPoint}} [run.command] - The
102
+ * arguments and environment (default the process's), the most sites at once (default 4), and how the exit point
103
+ * is found (default `exitPoint`, which asks a lookup service).
104
+ * @returns {Promise<LiveRun<S, R>>} The run.
105
+ * @example
106
+ * const run = await runLiveCheck({ chromium, build: BUILDS.live, prefix: 'better-colors-live-', root, sites: allSites(),
107
+ * categories: CATEGORIES.map((c) => c.id), pageMs: 120_000, visit: (browser, page, site) => visit(browser, page, site.url),
108
+ * failed: (site, error) => ({ url: site.url, error }), onResult: (result) => console.log(pageLine(result)) });
109
+ */
110
+ export declare function runLiveCheck<S extends LiveSite, R>({ chromium, build, prefix, root, sites, categories, pageMs, visit, failed, onResult, command }: {
111
+ chromium: BrowserType;
112
+ build: string;
113
+ prefix: string;
114
+ root: string;
115
+ sites: S[];
116
+ categories: string[];
117
+ pageMs: number;
118
+ visit: (browser: LaunchedExtension, page: Page, site: S) => Promise<R>;
119
+ failed: (site: S, error: string) => R;
120
+ onResult?: (result: {
121
+ site: S;
122
+ visit: R;
123
+ }) => void;
124
+ command?: {
125
+ argv?: string[];
126
+ env?: Record<string, string | undefined>;
127
+ concurrency?: number;
128
+ findExit?: typeof exitPoint;
129
+ };
130
+ }): Promise<LiveRun<S, R>>;