@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
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
|
|
13
|
+
import { mkdir, rm } from 'node:fs/promises';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { within } from '../time/time.js';
|
|
16
|
+
import { eachLimited } from '../time/pool.js';
|
|
17
|
+
import { chromeUserAgent, closeOnSignal, realisticBrowser } from './browser.js';
|
|
18
|
+
import { launchExtension } from './e2e.js';
|
|
19
|
+
import { exitPoint, parseLiveOptions } from './live.js';
|
|
20
|
+
|
|
21
|
+
/** @import { BrowserType, Page } from 'playwright' */
|
|
22
|
+
/** @import { LaunchedExtension } from './e2e.js' */
|
|
23
|
+
/** @import { Exit } from './live.js' */
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A site a checker visits.
|
|
27
|
+
*
|
|
28
|
+
* @typedef {{name: string, url: string, category: string}} LiveSite
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A finished run, for the checker's report.
|
|
33
|
+
*
|
|
34
|
+
* @template {LiveSite} S
|
|
35
|
+
* @template R
|
|
36
|
+
* @typedef {object} LiveRun
|
|
37
|
+
* @property {string} dir The report's folder (made empty).
|
|
38
|
+
* @property {string} started When it started (ISO).
|
|
39
|
+
* @property {string} finished When it finished (ISO).
|
|
40
|
+
* @property {Exit | null} exit Where the traffic left.
|
|
41
|
+
* @property {{locale: string, timezone: string, proxy: string}} browser How the browsers looked.
|
|
42
|
+
* @property {{site: S, visit: R}[]} results Each site's visit, in the sites' order.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A live checker's usage text.
|
|
47
|
+
*
|
|
48
|
+
* @param {string[]} categories - The categories' ids.
|
|
49
|
+
* @param {number} concurrency - The default and most sites at once.
|
|
50
|
+
* @returns {string} The text.
|
|
51
|
+
*/
|
|
52
|
+
export const liveUsage = (categories, concurrency) => `Usage: npm run live -- [options] [word …]
|
|
53
|
+
|
|
54
|
+
--only <word> only pages whose address or name contains it (repeatable; bare words too)
|
|
55
|
+
--category <id> only this category (repeatable): ${categories.join(', ')}
|
|
56
|
+
--concurrency <n> pages checked at once (default ${concurrency}, at most ${concurrency})
|
|
57
|
+
--proxy <url> send everything through this proxy (default: HTTPS_PROXY)
|
|
58
|
+
--locale <tag> browser language (default en-GB)
|
|
59
|
+
--timezone <zone> browser time zone (default Europe/London)
|
|
60
|
+
--out <dir> write the report there (default test/live/report)
|
|
61
|
+
--help`;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Run a live check: read the command line, visit the chosen sites, give back every visit.
|
|
65
|
+
* Prints the usage and exits for `--help` (or an option it doesn't know), and exits when no
|
|
66
|
+
* site matches.
|
|
67
|
+
*
|
|
68
|
+
* @template {LiveSite} S
|
|
69
|
+
* @template R
|
|
70
|
+
* @param {object} run
|
|
71
|
+
* @param {BrowserType} run.chromium - Playwright's chromium.
|
|
72
|
+
* @param {string} run.build - The extension's live test build (every site granted).
|
|
73
|
+
* @param {string} run.prefix - The browsers' profile prefix.
|
|
74
|
+
* @param {string} run.root - The extension's repository (the report goes in `test/live/report`).
|
|
75
|
+
* @param {S[]} run.sites - Every site.
|
|
76
|
+
* @param {string[]} run.categories - The categories' ids, for the usage.
|
|
77
|
+
* @param {number} run.pageMs - The most a visit may take.
|
|
78
|
+
* @param {(browser: LaunchedExtension, page: Page, site: S) => Promise<R>} run.visit - Check one site, on a fresh page.
|
|
79
|
+
* @param {(site: S, error: string) => R} run.failed - A visit that couldn't finish (`'timed out'`).
|
|
80
|
+
* @param {(result: {site: S, visit: R}) => void} [run.onResult] - Each visit as it finishes (a line of output).
|
|
81
|
+
* @param {{argv?: string[], env?: Record<string, string | undefined>, concurrency?: number, findExit?: typeof exitPoint}} [run.command] - The
|
|
82
|
+
* arguments and environment (default the process's), the most sites at once (default 4), and how the exit point
|
|
83
|
+
* is found (default `exitPoint`, which asks a lookup service).
|
|
84
|
+
* @returns {Promise<LiveRun<S, R>>} The run.
|
|
85
|
+
* @example
|
|
86
|
+
* const run = await runLiveCheck({ chromium, build: BUILDS.live, prefix: 'better-colors-live-', root, sites: allSites(),
|
|
87
|
+
* categories: CATEGORIES.map((c) => c.id), pageMs: 120_000, visit: (browser, page, site) => visit(browser, page, site.url),
|
|
88
|
+
* failed: (site, error) => ({ url: site.url, error }), onResult: (result) => console.log(pageLine(result)) });
|
|
89
|
+
*/
|
|
90
|
+
export async function runLiveCheck({ chromium, build, prefix, root, sites, categories, pageMs, visit, failed, onResult = () => {}, command = {} }) {
|
|
91
|
+
const { argv = process.argv.slice(2), env = process.env, concurrency = 4, findExit = exitPoint } = command;
|
|
92
|
+
const usage = liveUsage(categories, concurrency);
|
|
93
|
+
/** @type {import('./live.js').LiveOptions} */
|
|
94
|
+
let options;
|
|
95
|
+
try {
|
|
96
|
+
options = parseLiveOptions(argv, env, { usage, concurrency, maxConcurrency: concurrency });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(error instanceof Error ? error.message : error);
|
|
99
|
+
process.exit(2);
|
|
100
|
+
}
|
|
101
|
+
if (options.help) {
|
|
102
|
+
console.log(usage);
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
const words = options.only.map((word) => word.toLowerCase());
|
|
106
|
+
const chosen = sites.filter((site) => (!options.categories.length || options.categories.includes(site.category))
|
|
107
|
+
&& (!words.length || words.some((word) => site.url.toLowerCase().includes(word) || site.name.toLowerCase().includes(word))));
|
|
108
|
+
if (!chosen.length) {
|
|
109
|
+
console.error('No pages match.');
|
|
110
|
+
process.exit(2);
|
|
111
|
+
}
|
|
112
|
+
const dir = options.out ? path.resolve(options.out) : path.join(root, 'test', 'live', 'report');
|
|
113
|
+
await rm(dir, { recursive: true, force: true });
|
|
114
|
+
await mkdir(dir, { recursive: true });
|
|
115
|
+
|
|
116
|
+
const browserOptions = realisticBrowser(options.proxy ? { proxy: options.proxy } : {});
|
|
117
|
+
const plain = await chromium.launch({ channel: 'chromium', ...browserOptions });
|
|
118
|
+
/** @type {(LaunchedExtension | null)[]} */
|
|
119
|
+
const browsers = [];
|
|
120
|
+
const closeAll = async () => {
|
|
121
|
+
await Promise.allSettled([plain.close(), ...browsers.map((launched) => launched?.close())]);
|
|
122
|
+
};
|
|
123
|
+
const forget = closeOnSignal(closeAll);
|
|
124
|
+
try {
|
|
125
|
+
const userAgent = await chromeUserAgent(plain);
|
|
126
|
+
const probe = await plain.newContext();
|
|
127
|
+
const exit = await findExit(probe.request);
|
|
128
|
+
await probe.close();
|
|
129
|
+
console.log(exit ? `Exit: ${exit.country} (${exit.ip}${exit.where ? `, ${exit.where}` : ''})${options.proxyLabel ? ` through ${options.proxyLabel}` : ''}` : 'Exit: unknown');
|
|
130
|
+
console.log(`Pages: ${chosen.length}, ${options.concurrency} at a time.\n`);
|
|
131
|
+
const launchOptions = { ...browserOptions, userAgent, locale: options.locale, timezoneId: options.timezone, viewport: { width: 1280, height: 900 } };
|
|
132
|
+
const started = new Date().toISOString();
|
|
133
|
+
/** @type {{site: S, visit: R}[]} */
|
|
134
|
+
const results = [];
|
|
135
|
+
await eachLimited(chosen, options.concurrency, async (site, slot, index) => {
|
|
136
|
+
browsers[slot] ??= await launchExtension(chromium, build, { prefix: `${prefix}${slot}-`, ...launchOptions });
|
|
137
|
+
const launched = /** @type {LaunchedExtension} */ (browsers[slot]);
|
|
138
|
+
await within(launched.context.clearCookies(), 10_000, undefined);
|
|
139
|
+
const page = await launched.context.newPage();
|
|
140
|
+
const result = await within(visit(launched, page, site), pageMs, undefined);
|
|
141
|
+
const timedOut = result === undefined;
|
|
142
|
+
await within(page.close(), 15_000, undefined);
|
|
143
|
+
if (timedOut) {
|
|
144
|
+
// A page that ran out of time may have left its browser stuck: the slot gets a new one.
|
|
145
|
+
await within(launched.close(), 15_000, undefined);
|
|
146
|
+
browsers[slot] = null;
|
|
147
|
+
}
|
|
148
|
+
results[index] = { site, visit: timedOut ? failed(site, 'timed out') : /** @type {R} */ (result) };
|
|
149
|
+
onResult(results[index]);
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
dir, started, finished: new Date().toISOString(), exit,
|
|
153
|
+
browser: { locale: options.locale, timezone: options.timezone, proxy: options.proxyLabel },
|
|
154
|
+
results: results.filter(Boolean),
|
|
155
|
+
};
|
|
156
|
+
} finally {
|
|
157
|
+
forget();
|
|
158
|
+
await closeAll();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reaching a tested extension's pages from an e2e test: a Playwright page's
|
|
3
|
+
* tab id (as the extension's worker sees it), a page module called in a
|
|
4
|
+
* tab's frames as the extension calls it, and a page's whole tree through
|
|
5
|
+
* the DevTools protocol, closed shadow roots and frames included (where an
|
|
6
|
+
* extension's overlay lives). Tests only: an extension never needs CDP.
|
|
7
|
+
*
|
|
8
|
+
* @module testing/pages
|
|
9
|
+
*/
|
|
10
|
+
import type { CDPSession, Page, Worker } from 'playwright';
|
|
11
|
+
/** @import { CDPSession, Page, Worker } from 'playwright' */
|
|
12
|
+
/**
|
|
13
|
+
* The tab id of a page: the newest tab at its address (any tab for a `chrome:` page).
|
|
14
|
+
*
|
|
15
|
+
* @param {Worker} worker - The extension's service worker.
|
|
16
|
+
* @param {Page} page - The page.
|
|
17
|
+
* @returns {Promise<number>} Its tab's id.
|
|
18
|
+
* @example
|
|
19
|
+
* const tabId = await tabIdOf(worker, page);
|
|
20
|
+
*/
|
|
21
|
+
export declare function tabIdOf(worker: Worker, page: Page): Promise<number>;
|
|
22
|
+
/**
|
|
23
|
+
* Call an export of the extension's page module in a tab's frames, as `callInPage` does from the
|
|
24
|
+
* worker.
|
|
25
|
+
*
|
|
26
|
+
* @param {Worker} worker - The extension's service worker.
|
|
27
|
+
* @param {number} tabId - The tab.
|
|
28
|
+
* @param {string} module - The module's path in the extension (`content/main.js`).
|
|
29
|
+
* @param {string} name - The export.
|
|
30
|
+
* @param {unknown[]} [args] - Its arguments.
|
|
31
|
+
* @returns {Promise<{frameId: number, result: any}[]>} Each frame's answer.
|
|
32
|
+
* @example
|
|
33
|
+
* const [{ result }] = await callTabModule(worker, tabId, 'content/main.js', 'scan');
|
|
34
|
+
*/
|
|
35
|
+
export declare function callTabModule(worker: Worker, tabId: number, module: string, name: string, args?: unknown[]): Promise<{
|
|
36
|
+
frameId: number;
|
|
37
|
+
result: any;
|
|
38
|
+
}[]>;
|
|
39
|
+
export type CdpNode = {
|
|
40
|
+
/**
|
|
41
|
+
* Its id in the session.
|
|
42
|
+
*/
|
|
43
|
+
nodeId: number;
|
|
44
|
+
/**
|
|
45
|
+
* Its lasting id.
|
|
46
|
+
*/
|
|
47
|
+
backendNodeId: number;
|
|
48
|
+
/**
|
|
49
|
+
* Its DOM node type.
|
|
50
|
+
*/
|
|
51
|
+
nodeType: number;
|
|
52
|
+
/**
|
|
53
|
+
* Its name.
|
|
54
|
+
*/
|
|
55
|
+
nodeName: string;
|
|
56
|
+
/**
|
|
57
|
+
* A text node's text.
|
|
58
|
+
*/
|
|
59
|
+
nodeValue?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Its attributes, name and value in turn.
|
|
62
|
+
*/
|
|
63
|
+
attributes?: string[];
|
|
64
|
+
/**
|
|
65
|
+
* Its children.
|
|
66
|
+
*/
|
|
67
|
+
children?: CdpNode[];
|
|
68
|
+
/**
|
|
69
|
+
* Its shadow roots, closed ones too.
|
|
70
|
+
*/
|
|
71
|
+
shadowRoots?: CdpNode[];
|
|
72
|
+
/**
|
|
73
|
+
* A frame's document.
|
|
74
|
+
*/
|
|
75
|
+
contentDocument?: CdpNode;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* A node as the DevTools protocol's `DOM.getDocument` gives it (the parts read here).
|
|
79
|
+
*
|
|
80
|
+
* @typedef {object} CdpNode
|
|
81
|
+
* @property {number} nodeId Its id in the session.
|
|
82
|
+
* @property {number} backendNodeId Its lasting id.
|
|
83
|
+
* @property {number} nodeType Its DOM node type.
|
|
84
|
+
* @property {string} nodeName Its name.
|
|
85
|
+
* @property {string} [nodeValue] A text node's text.
|
|
86
|
+
* @property {string[]} [attributes] Its attributes, name and value in turn.
|
|
87
|
+
* @property {CdpNode[]} [children] Its children.
|
|
88
|
+
* @property {CdpNode[]} [shadowRoots] Its shadow roots, closed ones too.
|
|
89
|
+
* @property {CdpNode} [contentDocument] A frame's document.
|
|
90
|
+
*/
|
|
91
|
+
/**
|
|
92
|
+
* Every node of a page's tree in tree order, closed shadow roots and same-process frames included.
|
|
93
|
+
*
|
|
94
|
+
* @param {CDPSession} cdp - A session on the page.
|
|
95
|
+
* @returns {Promise<CdpNode[]>} The nodes; a node's shadow roots come before its children.
|
|
96
|
+
* @example
|
|
97
|
+
* const boxes = (await pierceNodes(cdp)).filter((node) => nodeAttribute(node, 'class') === 'box');
|
|
98
|
+
*/
|
|
99
|
+
export declare function pierceNodes(cdp: CDPSession): Promise<CdpNode[]>;
|
|
100
|
+
/**
|
|
101
|
+
* A node's attribute.
|
|
102
|
+
*
|
|
103
|
+
* @param {CdpNode} node - The node.
|
|
104
|
+
* @param {string} name - The attribute.
|
|
105
|
+
* @returns {string} Its value, or ''.
|
|
106
|
+
*/
|
|
107
|
+
export declare function nodeAttribute(node: CdpNode, name: string): string;
|
|
108
|
+
/**
|
|
109
|
+
* A node's text: a text node's own, an element's children's joined.
|
|
110
|
+
*
|
|
111
|
+
* @param {CdpNode} node - The node.
|
|
112
|
+
* @returns {string} Its text.
|
|
113
|
+
*/
|
|
114
|
+
export declare const nodeText: (node: CdpNode) => string;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reaching a tested extension's pages from an e2e test: a Playwright page's
|
|
3
|
+
* tab id (as the extension's worker sees it), a page module called in a
|
|
4
|
+
* tab's frames as the extension calls it, and a page's whole tree through
|
|
5
|
+
* the DevTools protocol, closed shadow roots and frames included (where an
|
|
6
|
+
* extension's overlay lives). Tests only: an extension never needs CDP.
|
|
7
|
+
*
|
|
8
|
+
* @module testing/pages
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** @import { CDPSession, Page, Worker } from 'playwright' */
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The tab id of a page: the newest tab at its address (any tab for a `chrome:` page).
|
|
15
|
+
*
|
|
16
|
+
* @param {Worker} worker - The extension's service worker.
|
|
17
|
+
* @param {Page} page - The page.
|
|
18
|
+
* @returns {Promise<number>} Its tab's id.
|
|
19
|
+
* @example
|
|
20
|
+
* const tabId = await tabIdOf(worker, page);
|
|
21
|
+
*/
|
|
22
|
+
export async function tabIdOf(worker, page) {
|
|
23
|
+
return worker.evaluate(async (url) => {
|
|
24
|
+
const query = url.startsWith('chrome:') ? {} : { url: url.replace(/#.*$/, '') };
|
|
25
|
+
const ids = (await chrome.tabs.query(query)).map((tab) => tab.id ?? -1);
|
|
26
|
+
if (!ids.length) throw new Error(`No tab for ${url}`);
|
|
27
|
+
return Math.max(...ids);
|
|
28
|
+
}, page.url());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Call an export of the extension's page module in a tab's frames, as `callInPage` does from the
|
|
33
|
+
* worker.
|
|
34
|
+
*
|
|
35
|
+
* @param {Worker} worker - The extension's service worker.
|
|
36
|
+
* @param {number} tabId - The tab.
|
|
37
|
+
* @param {string} module - The module's path in the extension (`content/main.js`).
|
|
38
|
+
* @param {string} name - The export.
|
|
39
|
+
* @param {unknown[]} [args] - Its arguments.
|
|
40
|
+
* @returns {Promise<{frameId: number, result: any}[]>} Each frame's answer.
|
|
41
|
+
* @example
|
|
42
|
+
* const [{ result }] = await callTabModule(worker, tabId, 'content/main.js', 'scan');
|
|
43
|
+
*/
|
|
44
|
+
export function callTabModule(worker, tabId, module, name, args = []) {
|
|
45
|
+
return worker.evaluate(({ tabId, module, name, args }) => chrome.scripting.executeScript({
|
|
46
|
+
target: { tabId, allFrames: true },
|
|
47
|
+
injectImmediately: true,
|
|
48
|
+
// As the kit's importAndCall (a worker's evaluate takes no function from outside, and its CSP no eval).
|
|
49
|
+
func: (/** @type {string} */ module, /** @type {string} */ name, /** @type {unknown[]} */ args) => import(chrome.runtime.getURL(module)).then((loaded) => loaded[name](...args)),
|
|
50
|
+
args: [module, name, args],
|
|
51
|
+
}).then((frames) => frames.map(({ frameId, result }) => ({ frameId, result }))), { tabId, module, name, args });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A node as the DevTools protocol's `DOM.getDocument` gives it (the parts read here).
|
|
56
|
+
*
|
|
57
|
+
* @typedef {object} CdpNode
|
|
58
|
+
* @property {number} nodeId Its id in the session.
|
|
59
|
+
* @property {number} backendNodeId Its lasting id.
|
|
60
|
+
* @property {number} nodeType Its DOM node type.
|
|
61
|
+
* @property {string} nodeName Its name.
|
|
62
|
+
* @property {string} [nodeValue] A text node's text.
|
|
63
|
+
* @property {string[]} [attributes] Its attributes, name and value in turn.
|
|
64
|
+
* @property {CdpNode[]} [children] Its children.
|
|
65
|
+
* @property {CdpNode[]} [shadowRoots] Its shadow roots, closed ones too.
|
|
66
|
+
* @property {CdpNode} [contentDocument] A frame's document.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Every node of a page's tree in tree order, closed shadow roots and same-process frames included.
|
|
71
|
+
*
|
|
72
|
+
* @param {CDPSession} cdp - A session on the page.
|
|
73
|
+
* @returns {Promise<CdpNode[]>} The nodes; a node's shadow roots come before its children.
|
|
74
|
+
* @example
|
|
75
|
+
* const boxes = (await pierceNodes(cdp)).filter((node) => nodeAttribute(node, 'class') === 'box');
|
|
76
|
+
*/
|
|
77
|
+
export async function pierceNodes(cdp) {
|
|
78
|
+
const { root } = /** @type {{root: CdpNode}} */ (await cdp.send('DOM.getDocument', { depth: -1, pierce: true }));
|
|
79
|
+
/** @type {CdpNode[]} */
|
|
80
|
+
const nodes = [];
|
|
81
|
+
/** @param {CdpNode} node */
|
|
82
|
+
const walk = (node) => {
|
|
83
|
+
nodes.push(node);
|
|
84
|
+
for (const child of [...(node.shadowRoots ?? []), ...(node.children ?? []), ...(node.contentDocument ? [node.contentDocument] : [])]) walk(child);
|
|
85
|
+
};
|
|
86
|
+
walk(root);
|
|
87
|
+
return nodes;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A node's attribute.
|
|
92
|
+
*
|
|
93
|
+
* @param {CdpNode} node - The node.
|
|
94
|
+
* @param {string} name - The attribute.
|
|
95
|
+
* @returns {string} Its value, or ''.
|
|
96
|
+
*/
|
|
97
|
+
export function nodeAttribute(node, name) {
|
|
98
|
+
const attributes = node.attributes ?? [];
|
|
99
|
+
const at = attributes.findIndex((value, index) => index % 2 === 0 && value === name);
|
|
100
|
+
return at >= 0 ? attributes[at + 1] : '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A node's text: a text node's own, an element's children's joined.
|
|
105
|
+
*
|
|
106
|
+
* @param {CdpNode} node - The node.
|
|
107
|
+
* @returns {string} Its text.
|
|
108
|
+
*/
|
|
109
|
+
export const nodeText = (node) => (node.nodeType === 3 ? node.nodeValue ?? '' : (node.children ?? []).map(nodeText).join(''));
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A notice in a side panel's list (chrome-ui.css `.sp-notice`): a card with an icon, a heading,
|
|
3
|
+
* a line, buttons beside the heading and actions under the line. For what a list leaves out or
|
|
4
|
+
* can't do: frames from other sites to allow, a web font that didn't load.
|
|
5
|
+
*
|
|
6
|
+
* @module ui/notice
|
|
7
|
+
*/
|
|
8
|
+
export type Notice = {
|
|
9
|
+
/**
|
|
10
|
+
* Its sprite symbol's id.
|
|
11
|
+
*/
|
|
12
|
+
icon: string;
|
|
13
|
+
/**
|
|
14
|
+
* An information's icon is grey, a warning's red. Default `warning`.
|
|
15
|
+
*/
|
|
16
|
+
tone?: 'info' | 'warning';
|
|
17
|
+
/**
|
|
18
|
+
* The heading.
|
|
19
|
+
*/
|
|
20
|
+
title: string;
|
|
21
|
+
/**
|
|
22
|
+
* The line under it.
|
|
23
|
+
*/
|
|
24
|
+
text: string;
|
|
25
|
+
/**
|
|
26
|
+
* Icon buttons at the heading's end.
|
|
27
|
+
*/
|
|
28
|
+
buttons?: HTMLElement[];
|
|
29
|
+
/**
|
|
30
|
+
* Buttons under the line, at the end (“Allow”).
|
|
31
|
+
*/
|
|
32
|
+
actions?: HTMLElement[];
|
|
33
|
+
/**
|
|
34
|
+
* The page. Default `document`.
|
|
35
|
+
*/
|
|
36
|
+
doc?: Document;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* A notice's parts.
|
|
40
|
+
*
|
|
41
|
+
* @typedef {object} Notice
|
|
42
|
+
* @property {string} icon Its sprite symbol's id.
|
|
43
|
+
* @property {'info' | 'warning'} [tone] An information's icon is grey, a warning's red. Default `warning`.
|
|
44
|
+
* @property {string} title The heading.
|
|
45
|
+
* @property {string} text The line under it.
|
|
46
|
+
* @property {HTMLElement[]} [buttons] Icon buttons at the heading's end.
|
|
47
|
+
* @property {HTMLElement[]} [actions] Buttons under the line, at the end (“Allow”).
|
|
48
|
+
* @property {Document} [doc] The page. Default `document`.
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* A notice card.
|
|
52
|
+
*
|
|
53
|
+
* @param {Notice} notice - Its parts.
|
|
54
|
+
* @returns {HTMLElement} The card.
|
|
55
|
+
* @example
|
|
56
|
+
* notices.append(noticeCard({ icon: 'i-frame', tone: 'info', title: t('panelFrames'), text, actions: [allow] }));
|
|
57
|
+
*/
|
|
58
|
+
export declare function noticeCard({ icon, tone, title, text, buttons, actions, doc }: Notice): HTMLElement;
|
package/src/ui/notice.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A notice in a side panel's list (chrome-ui.css `.sp-notice`): a card with an icon, a heading,
|
|
3
|
+
* a line, buttons beside the heading and actions under the line. For what a list leaves out or
|
|
4
|
+
* can't do: frames from other sites to allow, a web font that didn't load.
|
|
5
|
+
*
|
|
6
|
+
* @module ui/notice
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { make, spriteIcon } from '../dom/make.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A notice's parts.
|
|
13
|
+
*
|
|
14
|
+
* @typedef {object} Notice
|
|
15
|
+
* @property {string} icon Its sprite symbol's id.
|
|
16
|
+
* @property {'info' | 'warning'} [tone] An information's icon is grey, a warning's red. Default `warning`.
|
|
17
|
+
* @property {string} title The heading.
|
|
18
|
+
* @property {string} text The line under it.
|
|
19
|
+
* @property {HTMLElement[]} [buttons] Icon buttons at the heading's end.
|
|
20
|
+
* @property {HTMLElement[]} [actions] Buttons under the line, at the end (“Allow”).
|
|
21
|
+
* @property {Document} [doc] The page. Default `document`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A notice card.
|
|
26
|
+
*
|
|
27
|
+
* @param {Notice} notice - Its parts.
|
|
28
|
+
* @returns {HTMLElement} The card.
|
|
29
|
+
* @example
|
|
30
|
+
* notices.append(noticeCard({ icon: 'i-frame', tone: 'info', title: t('panelFrames'), text, actions: [allow] }));
|
|
31
|
+
*/
|
|
32
|
+
export function noticeCard({ icon, tone = 'warning', title, text, buttons = [], actions = [], doc = document }) {
|
|
33
|
+
const card = make('section', `sp-card sp-notice ${tone}`, '', doc);
|
|
34
|
+
const heading = make('div', 'sp-heading', '', doc);
|
|
35
|
+
heading.append(spriteIcon(icon, '', doc), make('h2', '', title, doc), ...buttons);
|
|
36
|
+
card.append(heading, make('p', 'sp-notice-text', text, doc));
|
|
37
|
+
if (actions.length) {
|
|
38
|
+
const row = make('div', 'sp-notice-actions', '', doc);
|
|
39
|
+
row.append(...actions);
|
|
40
|
+
card.append(row);
|
|
41
|
+
}
|
|
42
|
+
return card;
|
|
43
|
+
}
|