@arraypress/extension-kit 1.8.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.
Files changed (44) hide show
  1. package/README.md +88 -2
  2. package/package.json +1 -1
  3. package/src/build/index.d.ts +2 -1
  4. package/src/build/index.js +2 -1
  5. package/src/build/store-shots.d.ts +177 -0
  6. package/src/build/store-shots.js +173 -0
  7. package/src/build/test-build.d.ts +13 -1
  8. package/src/build/test-build.js +41 -2
  9. package/src/build/vendor.js +1 -1
  10. package/src/content/elements.d.ts +50 -0
  11. package/src/content/elements.js +88 -0
  12. package/src/content/highlight.d.ts +54 -0
  13. package/src/content/highlight.js +92 -0
  14. package/src/content/overlay.d.ts +57 -0
  15. package/src/content/overlay.js +52 -0
  16. package/src/content/page-toast.d.ts +26 -0
  17. package/src/content/page-toast.js +72 -0
  18. package/src/content/slices.d.ts +16 -1
  19. package/src/content/slices.js +29 -10
  20. package/src/dom/copy.d.ts +36 -0
  21. package/src/dom/copy.js +84 -0
  22. package/src/dom/i18n.d.ts +17 -0
  23. package/src/dom/i18n.js +15 -0
  24. package/src/dom/make.d.ts +53 -0
  25. package/src/dom/make.js +71 -0
  26. package/src/frame/window.d.ts +76 -7
  27. package/src/frame/window.js +119 -15
  28. package/src/index.d.ts +33 -5
  29. package/src/index.js +25 -5
  30. package/src/panel/side-panel.d.ts +35 -0
  31. package/src/panel/side-panel.js +41 -0
  32. package/src/scripting/call.d.ts +63 -0
  33. package/src/scripting/call.js +57 -0
  34. package/src/scripting/tab.d.ts +68 -0
  35. package/src/scripting/tab.js +81 -0
  36. package/src/styles/chrome-ui.css +7 -0
  37. package/src/testing/index.d.ts +6 -0
  38. package/src/testing/index.js +4 -0
  39. package/src/testing/live-run.d.ts +130 -0
  40. package/src/testing/live-run.js +160 -0
  41. package/src/testing/pages.d.ts +114 -0
  42. package/src/testing/pages.js +109 -0
  43. package/src/ui/notice.d.ts +58 -0
  44. package/src/ui/notice.js +43 -0
package/src/index.d.ts CHANGED
@@ -2,8 +2,10 @@
2
2
  * The kit's browser half: what an extension's pages, offscreen document,
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
- * the built-in AI row), Chrome's window drawn around a capture, per-site lists' sites, Chrome's on-device model, and
6
- * a content script's slices and change watcher included. Every function takes the `chrome`
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, 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`
7
9
  * APIs it uses as arguments, so tests run the real logic against fakes.
8
10
  *
9
11
  * Build-time helpers are in `@arraypress/extension-kit/build`, test helpers
@@ -15,14 +17,23 @@
15
17
  export { githubAboutLinks, isPlaceholder } from './about/links.js';
16
18
  export { MODEL_MESSAGES, chromeLanguageModel, createModelSession, describeModel, downloadModel, modelAvailability, } from './ai/model.js';
17
19
  export { setShortcuts } from './commands/shortcuts.js';
18
- export { BLOCK_CHARS, SLICE_MS, blocksOf, inSlices } from './content/slices.js';
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';
23
+ export { pageOverlay } from './content/overlay.js';
24
+ export { PAGE_TOAST_MS, showPageToast } from './content/page-toast.js';
19
25
  export { MAX_WAIT_MS, SETTLE_MS, watchChanges } from './content/watch.js';
26
+ export { copyFormats } from './dom/copy.js';
20
27
  export { focusedElement } from './dom/focus.js';
21
- export { THEMES, WINDOW_BARS, WINDOW_W, letterIcon, windowChrome, windowChromeSvg } from './frame/window.js';
22
- export { localize, translator } from './dom/i18n.js';
28
+ export { iconButton, make, spriteIcon } from './dom/make.js';
29
+ export { MAC_WINDOW, THEMES, WINDOW_BARS, WINDOW_W, letterIcon, macWindowSvg, windowChrome, windowChromeSvg } from './frame/window.js';
30
+ export { counted, localize, translator } from './dom/i18n.js';
23
31
  export { byId, setAboutLinks, statusFlasher, valueRow } from './dom/page.js';
24
32
  export { setUpPage } from './dom/setup.js';
33
+ export { openSidePanel, sidePanelPath } from './panel/side-panel.js';
34
+ export { callInPage, importAndCall } from './scripting/call.js';
25
35
  export { contentScriptSync } from './scripting/registration.js';
36
+ export { callInTab, isBrowserPage } from './scripting/tab.js';
26
37
  export { createSettingsSaver } from './settings/saver.js';
27
38
  export { MAX_SYNC_ITEM_BYTES } from './settings/storage.js';
28
39
  export { createSettingsStore } from './settings/store.js';
@@ -36,6 +47,7 @@ export { MENU_EDGE, createMenu, isInside, menuPosition, moreActionsButton } from
36
47
  export { createModelRow } from './ui/model-row.js';
37
48
  export { SITE_LIST_MESSAGES, SITE_LIST_WORDS, createSiteList } from './ui/site-list.js';
38
49
  export { askFields, askText, checkText, fieldIds } from './ui/text-dialog.js';
50
+ export { noticeCard } from './ui/notice.js';
39
51
  export { TOAST_MS, createToast } from './ui/toast.js';
40
52
  export type Availability = import('./ai/model.js').Availability;
41
53
  export type InitialPrompt = import('./ai/model.js').InitialPrompt;
@@ -46,11 +58,18 @@ export type ModelAnswer = import('./ai/model.js').ModelAnswer;
46
58
  export type ModelSession = import('./ai/model.js').ModelSession;
47
59
  export type ModelStatus = import('./ai/model.js').ModelStatus;
48
60
  export type SliceOptions = import('./content/slices.js').SliceOptions;
61
+ export type Highlighter = import('./content/highlight.js').Highlighter;
62
+ export type PageOverlay = import('./content/overlay.js').PageOverlay;
63
+ export type ClipboardFormats = import('./dom/copy.js').ClipboardFormats;
49
64
  export type WatchHandlers = import('./content/watch.js').WatchHandlers;
50
65
  export type WindowTab = import('./frame/window.js').Tab;
51
66
  export type WindowTheme = import('./frame/window.js').Theme;
67
+ export type WindowShadow = import('./frame/window.js').WindowShadow;
52
68
  export type ModelRow = import('./ui/model-row.js').ModelRow;
53
69
  export type ContentScript = import('./scripting/registration.js').ContentScript;
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;
54
73
  export type ActionMenu = import('./ui/menu.js').ActionMenu;
55
74
  export type MenuEntry = import('./ui/menu.js').MenuEntry;
56
75
  export type MenuItem = import('./ui/menu.js').MenuItem;
@@ -59,6 +78,7 @@ export type SiteList = import('./ui/site-list.js').SiteList;
59
78
  export type FieldRequest = import('./ui/text-dialog.js').FieldRequest;
60
79
  export type FieldsRequest = import('./ui/text-dialog.js').FieldsRequest;
61
80
  export type TextRequest = import('./ui/text-dialog.js').TextRequest;
81
+ export type Notice = import('./ui/notice.js').Notice;
62
82
  export type Toast = import('./ui/toast.js').Toast;
63
83
  /**
64
84
  * The page helpers' types, for a page's JSDoc.
@@ -72,11 +92,18 @@ export type Toast = import('./ui/toast.js').Toast;
72
92
  * @typedef {import('./ai/model.js').ModelSession} ModelSession
73
93
  * @typedef {import('./ai/model.js').ModelStatus} ModelStatus
74
94
  * @typedef {import('./content/slices.js').SliceOptions} SliceOptions
95
+ * @typedef {import('./content/highlight.js').Highlighter} Highlighter
96
+ * @typedef {import('./content/overlay.js').PageOverlay} PageOverlay
97
+ * @typedef {import('./dom/copy.js').ClipboardFormats} ClipboardFormats
75
98
  * @typedef {import('./content/watch.js').WatchHandlers} WatchHandlers
76
99
  * @typedef {import('./frame/window.js').Tab} WindowTab
77
100
  * @typedef {import('./frame/window.js').Theme} WindowTheme
101
+ * @typedef {import('./frame/window.js').WindowShadow} WindowShadow
78
102
  * @typedef {import('./ui/model-row.js').ModelRow} ModelRow
79
103
  * @typedef {import('./scripting/registration.js').ContentScript} ContentScript
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
80
107
  * @typedef {import('./ui/menu.js').ActionMenu} ActionMenu
81
108
  * @typedef {import('./ui/menu.js').MenuEntry} MenuEntry
82
109
  * @typedef {import('./ui/menu.js').MenuItem} MenuItem
@@ -85,5 +112,6 @@ export type Toast = import('./ui/toast.js').Toast;
85
112
  * @typedef {import('./ui/text-dialog.js').FieldRequest} FieldRequest
86
113
  * @typedef {import('./ui/text-dialog.js').FieldsRequest} FieldsRequest
87
114
  * @typedef {import('./ui/text-dialog.js').TextRequest} TextRequest
115
+ * @typedef {import('./ui/notice.js').Notice} Notice
88
116
  * @typedef {import('./ui/toast.js').Toast} Toast
89
117
  */
package/src/index.js CHANGED
@@ -2,8 +2,10 @@
2
2
  * The kit's browser half: what an extension's pages, offscreen document,
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
- * the built-in AI row), Chrome's window drawn around a capture, per-site lists' sites, Chrome's on-device model, and
6
- * a content script's slices and change watcher included. Every function takes the `chrome`
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, 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`
7
9
  * APIs it uses as arguments, so tests run the real logic against fakes.
8
10
  *
9
11
  * Build-time helpers are in `@arraypress/extension-kit/build`, test helpers
@@ -18,14 +20,23 @@ export {
18
20
  MODEL_MESSAGES, chromeLanguageModel, createModelSession, describeModel, downloadModel, modelAvailability,
19
21
  } from './ai/model.js';
20
22
  export { setShortcuts } from './commands/shortcuts.js';
21
- export { BLOCK_CHARS, SLICE_MS, blocksOf, inSlices } from './content/slices.js';
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';
26
+ export { pageOverlay } from './content/overlay.js';
27
+ export { PAGE_TOAST_MS, showPageToast } from './content/page-toast.js';
22
28
  export { MAX_WAIT_MS, SETTLE_MS, watchChanges } from './content/watch.js';
29
+ export { copyFormats } from './dom/copy.js';
23
30
  export { focusedElement } from './dom/focus.js';
24
- export { THEMES, WINDOW_BARS, WINDOW_W, letterIcon, windowChrome, windowChromeSvg } from './frame/window.js';
25
- export { localize, translator } from './dom/i18n.js';
31
+ export { iconButton, make, spriteIcon } from './dom/make.js';
32
+ export { MAC_WINDOW, THEMES, WINDOW_BARS, WINDOW_W, letterIcon, macWindowSvg, windowChrome, windowChromeSvg } from './frame/window.js';
33
+ export { counted, localize, translator } from './dom/i18n.js';
26
34
  export { byId, setAboutLinks, statusFlasher, valueRow } from './dom/page.js';
27
35
  export { setUpPage } from './dom/setup.js';
36
+ export { openSidePanel, sidePanelPath } from './panel/side-panel.js';
37
+ export { callInPage, importAndCall } from './scripting/call.js';
28
38
  export { contentScriptSync } from './scripting/registration.js';
39
+ export { callInTab, isBrowserPage } from './scripting/tab.js';
29
40
  export { createSettingsSaver } from './settings/saver.js';
30
41
  export { MAX_SYNC_ITEM_BYTES } from './settings/storage.js';
31
42
  export { createSettingsStore } from './settings/store.js';
@@ -43,6 +54,7 @@ export { MENU_EDGE, createMenu, isInside, menuPosition, moreActionsButton } from
43
54
  export { createModelRow } from './ui/model-row.js';
44
55
  export { SITE_LIST_MESSAGES, SITE_LIST_WORDS, createSiteList } from './ui/site-list.js';
45
56
  export { askFields, askText, checkText, fieldIds } from './ui/text-dialog.js';
57
+ export { noticeCard } from './ui/notice.js';
46
58
  export { TOAST_MS, createToast } from './ui/toast.js';
47
59
 
48
60
  /**
@@ -57,11 +69,18 @@ export { TOAST_MS, createToast } from './ui/toast.js';
57
69
  * @typedef {import('./ai/model.js').ModelSession} ModelSession
58
70
  * @typedef {import('./ai/model.js').ModelStatus} ModelStatus
59
71
  * @typedef {import('./content/slices.js').SliceOptions} SliceOptions
72
+ * @typedef {import('./content/highlight.js').Highlighter} Highlighter
73
+ * @typedef {import('./content/overlay.js').PageOverlay} PageOverlay
74
+ * @typedef {import('./dom/copy.js').ClipboardFormats} ClipboardFormats
60
75
  * @typedef {import('./content/watch.js').WatchHandlers} WatchHandlers
61
76
  * @typedef {import('./frame/window.js').Tab} WindowTab
62
77
  * @typedef {import('./frame/window.js').Theme} WindowTheme
78
+ * @typedef {import('./frame/window.js').WindowShadow} WindowShadow
63
79
  * @typedef {import('./ui/model-row.js').ModelRow} ModelRow
64
80
  * @typedef {import('./scripting/registration.js').ContentScript} ContentScript
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
65
84
  * @typedef {import('./ui/menu.js').ActionMenu} ActionMenu
66
85
  * @typedef {import('./ui/menu.js').MenuEntry} MenuEntry
67
86
  * @typedef {import('./ui/menu.js').MenuItem} MenuItem
@@ -70,5 +89,6 @@ export { TOAST_MS, createToast } from './ui/toast.js';
70
89
  * @typedef {import('./ui/text-dialog.js').FieldRequest} FieldRequest
71
90
  * @typedef {import('./ui/text-dialog.js').FieldsRequest} FieldsRequest
72
91
  * @typedef {import('./ui/text-dialog.js').TextRequest} TextRequest
92
+ * @typedef {import('./ui/notice.js').Notice} Notice
73
93
  * @typedef {import('./ui/toast.js').Toast} Toast
74
94
  */
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Opening Chrome's side panel for one tab, from a click: the panel is the
3
+ * tab's own (no `side_panel` in the manifest, no global panel), so it shows
4
+ * only beside the page it's about and goes when the person switches tabs, as
5
+ * Chrome does for a tab's panel. Chrome opens a panel only while the click
6
+ * still counts, so nothing is awaited before `open`.
7
+ *
8
+ * @module panel/side-panel
9
+ */
10
+ export type SidePanelApi = Pick<typeof chrome.sidePanel, 'setOptions' | 'open'>;
11
+ /**
12
+ * The parts of `chrome.sidePanel` opening one uses.
13
+ *
14
+ * @typedef {Pick<typeof chrome.sidePanel, 'setOptions' | 'open'>} SidePanelApi
15
+ */
16
+ /**
17
+ * The panel page's address for a tab: `path?tab=<id>`, so the page knows its tab.
18
+ *
19
+ * @param {string} path - The panel page, `panel/panel.html`.
20
+ * @param {number} tabId - The tab.
21
+ * @returns {string} `panel/panel.html?tab=12`.
22
+ */
23
+ export declare const sidePanelPath: (path: string, tabId: number) => string;
24
+ /**
25
+ * Open the side panel beside a tab, from a click (the toolbar button's, a
26
+ * shortcut's, or a button on an extension page).
27
+ *
28
+ * @param {SidePanelApi} sidePanel - `chrome.sidePanel` (or a fake in tests).
29
+ * @param {number} tabId - The tab.
30
+ * @param {string} path - The panel page; it gets `?tab=<id>`.
31
+ * @returns {Promise<void>} Once it's open; rejects when Chrome refuses (no click behind it).
32
+ * @example
33
+ * chrome.action.onClicked.addListener((tab) => openSidePanel(chrome.sidePanel, tab.id, 'panel/panel.html'));
34
+ */
35
+ export declare function openSidePanel(sidePanel: SidePanelApi, tabId: number, path: string): Promise<void>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Opening Chrome's side panel for one tab, from a click: the panel is the
3
+ * tab's own (no `side_panel` in the manifest, no global panel), so it shows
4
+ * only beside the page it's about and goes when the person switches tabs, as
5
+ * Chrome does for a tab's panel. Chrome opens a panel only while the click
6
+ * still counts, so nothing is awaited before `open`.
7
+ *
8
+ * @module panel/side-panel
9
+ */
10
+
11
+ /**
12
+ * The parts of `chrome.sidePanel` opening one uses.
13
+ *
14
+ * @typedef {Pick<typeof chrome.sidePanel, 'setOptions' | 'open'>} SidePanelApi
15
+ */
16
+
17
+ /**
18
+ * The panel page's address for a tab: `path?tab=<id>`, so the page knows its tab.
19
+ *
20
+ * @param {string} path - The panel page, `panel/panel.html`.
21
+ * @param {number} tabId - The tab.
22
+ * @returns {string} `panel/panel.html?tab=12`.
23
+ */
24
+ export const sidePanelPath = (path, tabId) => `${path}?tab=${tabId}`;
25
+
26
+ /**
27
+ * Open the side panel beside a tab, from a click (the toolbar button's, a
28
+ * shortcut's, or a button on an extension page).
29
+ *
30
+ * @param {SidePanelApi} sidePanel - `chrome.sidePanel` (or a fake in tests).
31
+ * @param {number} tabId - The tab.
32
+ * @param {string} path - The panel page; it gets `?tab=<id>`.
33
+ * @returns {Promise<void>} Once it's open; rejects when Chrome refuses (no click behind it).
34
+ * @example
35
+ * chrome.action.onClicked.addListener((tab) => openSidePanel(chrome.sidePanel, tab.id, 'panel/panel.html'));
36
+ */
37
+ export function openSidePanel(sidePanel, tabId, path) {
38
+ // Not awaited: open() must be called while the click still counts.
39
+ sidePanel.setOptions({ tabId, path: sidePanelPath(path, tabId), enabled: true }).catch((error) => console.error('Setting the side panel up:', error));
40
+ return sidePanel.open({ tabId });
41
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Calling one of an extension module's functions in a page: `executeScript`
3
+ * runs a small function in the page's isolated world that imports the module
4
+ * (by its extension address, so it must be listed in `web_accessible_resources`)
5
+ * and calls the export named, with arguments that survive structured cloning.
6
+ * The module, and anything it keeps, stays loaded in that world between calls,
7
+ * so a content script can hold a session (a picker, an inspector) across them.
8
+ *
9
+ * @module scripting/call
10
+ */
11
+ export type CallScripting = Pick<typeof chrome.scripting, 'executeScript'>;
12
+ export type FrameResult<T> = {
13
+ /**
14
+ * The frame (0 is the top one).
15
+ */
16
+ frameId: number;
17
+ /**
18
+ * The function's result, once its promise settled.
19
+ */
20
+ result: T;
21
+ };
22
+ /**
23
+ * The parts of `chrome.scripting` a call uses.
24
+ *
25
+ * @typedef {Pick<typeof chrome.scripting, 'executeScript'>} CallScripting
26
+ */
27
+ /**
28
+ * What one frame's call gave back.
29
+ *
30
+ * @template T
31
+ * @typedef {object} FrameResult
32
+ * @property {number} frameId The frame (0 is the top one).
33
+ * @property {T} result The function's result, once its promise settled.
34
+ */
35
+ /**
36
+ * The function `executeScript` runs in the page: alone, without closures.
37
+ *
38
+ * @param {string} module - The module's path in the extension, `content/main.js`.
39
+ * @param {string} name - The export to call.
40
+ * @param {unknown[]} args - Its arguments.
41
+ * @returns {Promise<unknown>} What it returns.
42
+ */
43
+ export declare const importAndCall: (module: string, name: string, args: unknown[]) => Promise<unknown>;
44
+ /**
45
+ * Call `name` from the extension module `module` in the target's frames.
46
+ *
47
+ * @template [T=unknown]
48
+ * @param {CallScripting} scripting - `chrome.scripting` (or a fake in tests).
49
+ * @param {chrome.scripting.InjectionTarget} target - The tab, and which frames (`allFrames`, `frameIds`).
50
+ * @param {string} module - The module's path in the extension (listed in `web_accessible_resources`).
51
+ * @param {string} name - The exported function.
52
+ * @param {unknown[]} [args] - Its arguments (structured-cloneable).
53
+ * @param {{immediately?: boolean}} [options] - `immediately` (default true): don't wait for the
54
+ * page to be idle, which a page still loading ads or a player may not be for a long time.
55
+ * @returns {Promise<FrameResult<T>[]>} Each frame's result, frames Chrome couldn't run in left out;
56
+ * rejects when it can run in none (a page Chrome keeps to itself, no access).
57
+ * @example
58
+ * const [{ result }] = await callInPage(chrome.scripting, { tabId }, 'content/main.js', 'scan', [options]);
59
+ * await callInPage(chrome.scripting, { tabId, allFrames: true }, 'content/main.js', 'highlight', [key]);
60
+ */
61
+ export declare function callInPage<T = unknown>(scripting: CallScripting, target: chrome.scripting.InjectionTarget, module: string, name: string, args?: unknown[], { immediately }?: {
62
+ immediately?: boolean;
63
+ }): Promise<FrameResult<T>[]>;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Calling one of an extension module's functions in a page: `executeScript`
3
+ * runs a small function in the page's isolated world that imports the module
4
+ * (by its extension address, so it must be listed in `web_accessible_resources`)
5
+ * and calls the export named, with arguments that survive structured cloning.
6
+ * The module, and anything it keeps, stays loaded in that world between calls,
7
+ * so a content script can hold a session (a picker, an inspector) across them.
8
+ *
9
+ * @module scripting/call
10
+ */
11
+
12
+ /**
13
+ * The parts of `chrome.scripting` a call uses.
14
+ *
15
+ * @typedef {Pick<typeof chrome.scripting, 'executeScript'>} CallScripting
16
+ */
17
+
18
+ /**
19
+ * What one frame's call gave back.
20
+ *
21
+ * @template T
22
+ * @typedef {object} FrameResult
23
+ * @property {number} frameId The frame (0 is the top one).
24
+ * @property {T} result The function's result, once its promise settled.
25
+ */
26
+
27
+ /**
28
+ * The function `executeScript` runs in the page: alone, without closures.
29
+ *
30
+ * @param {string} module - The module's path in the extension, `content/main.js`.
31
+ * @param {string} name - The export to call.
32
+ * @param {unknown[]} args - Its arguments.
33
+ * @returns {Promise<unknown>} What it returns.
34
+ */
35
+ export const importAndCall = (module, name, args) => import(chrome.runtime.getURL(module)).then((loaded) => loaded[name](...args));
36
+
37
+ /**
38
+ * Call `name` from the extension module `module` in the target's frames.
39
+ *
40
+ * @template [T=unknown]
41
+ * @param {CallScripting} scripting - `chrome.scripting` (or a fake in tests).
42
+ * @param {chrome.scripting.InjectionTarget} target - The tab, and which frames (`allFrames`, `frameIds`).
43
+ * @param {string} module - The module's path in the extension (listed in `web_accessible_resources`).
44
+ * @param {string} name - The exported function.
45
+ * @param {unknown[]} [args] - Its arguments (structured-cloneable).
46
+ * @param {{immediately?: boolean}} [options] - `immediately` (default true): don't wait for the
47
+ * page to be idle, which a page still loading ads or a player may not be for a long time.
48
+ * @returns {Promise<FrameResult<T>[]>} Each frame's result, frames Chrome couldn't run in left out;
49
+ * rejects when it can run in none (a page Chrome keeps to itself, no access).
50
+ * @example
51
+ * const [{ result }] = await callInPage(chrome.scripting, { tabId }, 'content/main.js', 'scan', [options]);
52
+ * await callInPage(chrome.scripting, { tabId, allFrames: true }, 'content/main.js', 'highlight', [key]);
53
+ */
54
+ export async function callInPage(scripting, target, module, name, args = [], { immediately = true } = {}) {
55
+ const frames = await scripting.executeScript({ target, injectImmediately: immediately, func: importAndCall, args: [module, name, args] });
56
+ return frames.map((frame) => ({ frameId: frame.frameId, result: /** @type {T} */ (frame.result) }));
57
+ }
@@ -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