@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.
- package/README.md +88 -2
- package/package.json +1 -1
- package/src/build/index.d.ts +2 -1
- package/src/build/index.js +2 -1
- package/src/build/store-shots.d.ts +177 -0
- package/src/build/store-shots.js +173 -0
- package/src/build/test-build.d.ts +13 -1
- package/src/build/test-build.js +41 -2
- package/src/build/vendor.js +1 -1
- package/src/content/elements.d.ts +50 -0
- package/src/content/elements.js +88 -0
- package/src/content/highlight.d.ts +54 -0
- package/src/content/highlight.js +92 -0
- package/src/content/overlay.d.ts +57 -0
- package/src/content/overlay.js +52 -0
- package/src/content/page-toast.d.ts +26 -0
- package/src/content/page-toast.js +72 -0
- package/src/content/slices.d.ts +16 -1
- package/src/content/slices.js +29 -10
- package/src/dom/copy.d.ts +36 -0
- package/src/dom/copy.js +84 -0
- package/src/dom/i18n.d.ts +17 -0
- package/src/dom/i18n.js +15 -0
- package/src/dom/make.d.ts +53 -0
- package/src/dom/make.js +71 -0
- package/src/frame/window.d.ts +76 -7
- package/src/frame/window.js +119 -15
- package/src/index.d.ts +33 -5
- package/src/index.js +25 -5
- package/src/panel/side-panel.d.ts +35 -0
- package/src/panel/side-panel.js +41 -0
- package/src/scripting/call.d.ts +63 -0
- package/src/scripting/call.js +57 -0
- package/src/scripting/tab.d.ts +68 -0
- package/src/scripting/tab.js +81 -0
- package/src/styles/chrome-ui.css +7 -0
- package/src/testing/index.d.ts +6 -0
- package/src/testing/index.js +4 -0
- package/src/testing/live-run.d.ts +130 -0
- package/src/testing/live-run.js +160 -0
- package/src/testing/pages.d.ts +114 -0
- package/src/testing/pages.js +109 -0
- package/src/ui/notice.d.ts +58 -0
- 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,
|
|
6
|
-
* a content script's slices
|
|
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 {
|
|
22
|
-
export {
|
|
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,
|
|
6
|
-
* a content script's slices
|
|
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 {
|
|
25
|
-
export {
|
|
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
|
+
}
|
package/src/styles/chrome-ui.css
CHANGED
|
@@ -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;
|
package/src/testing/index.d.ts
CHANGED
|
@@ -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
|
package/src/testing/index.js
CHANGED
|
@@ -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
|