@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.
- package/README.md +55 -5
- 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/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 +16 -6
- package/src/content/overlay.js +16 -6
- package/src/content/page-toast.d.ts +26 -0
- package/src/content/page-toast.js +72 -0
- 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 +7 -0
- package/src/frame/window.js +3 -1
- package/src/index.d.ts +21 -3
- package/src/index.js +16 -3
- 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/dom/make.js
ADDED
|
@@ -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
|
+
}
|
package/src/frame/window.d.ts
CHANGED
|
@@ -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
|
*
|
package/src/frame/window.js
CHANGED
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
34
|
+
export const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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
|
|
7
|
-
*
|
|
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
|
|
7
|
-
*
|
|
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
|
+
}
|
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
|
|
@@ -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>>;
|