@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/README.md
CHANGED
|
@@ -245,16 +245,54 @@ for (const { frameId, result } of frames) show(frameId, result);
|
|
|
245
245
|
// From the toolbar click: the tab's own side panel, panel/panel.html?tab=<id>, nothing awaited first.
|
|
246
246
|
chrome.action.onClicked.addListener((tab) => openSidePanel(chrome.sidePanel, tab.id, 'panel/panel.html'));
|
|
247
247
|
|
|
248
|
-
// In the
|
|
249
|
-
const
|
|
248
|
+
// In the panel: every frame of the tab, or why it can't be read.
|
|
249
|
+
const reading = await callInTab(chrome, tabId, 'content/main.js', 'scan');
|
|
250
|
+
if (reading.state === 'read') show(reading.frames); // top frame first
|
|
251
|
+
else showMessage(reading.state); // 'browser' (Chrome's own page) or 'no-access'
|
|
252
|
+
|
|
253
|
+
// In the content script: a layer the page's styles can't touch (a closed shadow root). The host's
|
|
254
|
+
// own styles go in hostStyle: its inline `all: initial !important` reset beats a `:host` rule.
|
|
255
|
+
const { root, remove } = pageOverlay(document, {
|
|
256
|
+
name: 'my-inspector',
|
|
257
|
+
hostStyle: { position: 'fixed', inset: '0', 'z-index': '2147483647', 'pointer-events': 'none' },
|
|
258
|
+
css: `${HIGHLIGHT_CSS} .card { position: absolute; }`,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// “Show on page”: boxes in DevTools' blue, kept on the elements as the page scrolls; Esc clears.
|
|
262
|
+
const highlight = createHighlighter(document, { root: () => root });
|
|
263
|
+
highlight.show(elements, { reveal: true });
|
|
250
264
|
```
|
|
251
265
|
|
|
252
266
|
`callInPage(scripting, target, module, name, args, {immediately})` resolves to each frame's
|
|
253
267
|
`{frameId, result}` (frames Chrome couldn't run in left out) and rejects when it can run in none;
|
|
254
268
|
`immediately` (default true) doesn't wait for the page to be idle. `openSidePanel` sets the tab's
|
|
255
269
|
panel to `path?tab=<id>` (`sidePanelPath`) and opens it in the same moment: Chrome opens a panel
|
|
256
|
-
only while the click counts. `
|
|
257
|
-
|
|
270
|
+
only while the click counts. `callInTab(edges, tabId, module, name, args)` calls it in every frame
|
|
271
|
+
and gives `{state: 'read', frames}` (frames that answered, top first), or `{state: 'browser'}` for
|
|
272
|
+
a page Chrome keeps to itself (`isBrowserPage(url)`: its own pages, the Web Store) or
|
|
273
|
+
`{state: 'no-access'}` (the tab went to another site since the click). `pageOverlay` puts a host
|
|
274
|
+
element of its own name on the root element, replacing one a run before left; `root` is its closed
|
|
275
|
+
shadow root, `css` first. The host is reset inline (`all: initial !important`) so the page can't
|
|
276
|
+
move or hide it; that also beats a `:host { … }` rule and `host.style.x = …`, so the host's own
|
|
277
|
+
styles go in `hostStyle` (set `!important` after the reset). `createHighlighter(doc, {root, onClear,
|
|
278
|
+
max})` boxes up to 500 elements (`MAX_BOXES`) in the overlay (`HIGHLIGHT_CSS` styles them), only
|
|
279
|
+
those in the window, following the page as it scrolls; `show` replaces the set, `clear` or Esc
|
|
280
|
+
ends it.
|
|
281
|
+
|
|
282
|
+
```js
|
|
283
|
+
// A page's elements in a line, and copying from a click (the copy command where the API can't).
|
|
284
|
+
row.append(make('span', 'meta', '12 colours'), iconButton({ icon: 'i-copy', label: t('copy'), onClick: () => copyFormats(document, { text: hex }) }));
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`make(tag, className, text, doc)`, `spriteIcon(id, className, doc)` (a `<use>` of the page's
|
|
288
|
+
sprite) and `iconButton({icon, label, onClick, className, doc})` (tooltip and label) take the
|
|
289
|
+
document the element belongs to, so a content script's overlay uses them too.
|
|
290
|
+
`copyFormats(doc, {text, html})` puts plain text and HTML on the clipboard at once (a titled link
|
|
291
|
+
for rich editors, its plain text for the rest) and resolves to whether it did; a content script
|
|
292
|
+
can call it from a menu click or a shortcut (Chrome lets the focused active tab write), and
|
|
293
|
+
`showPageToast(doc, 'Link copied', {name})` confirms it with Chrome's own toast in the page.
|
|
294
|
+
`noticeCard({icon, tone, title, text, buttons, actions})` is a side panel's notice (`.sp-notice`:
|
|
295
|
+
frames from other sites to allow); `counted(count, one, many, {t, lang})` a count in words.
|
|
258
296
|
|
|
259
297
|
## A content script on every site
|
|
260
298
|
|
|
@@ -350,6 +388,13 @@ changing (at most `maxWaitMs` while it keeps on); an element with a few children
|
|
|
350
388
|
default 6) whose children changed is handed over whole. Both are pure DOM, so a content script
|
|
351
389
|
imports them directly (list them in `web_accessible_resources`).
|
|
352
390
|
|
|
391
|
+
`renderedElements(doc, {skip, limit})` (`content/elements.js`) lists every element Chrome draws,
|
|
392
|
+
in document order, into open and closed shadow roots (`shadowRootOf`, through
|
|
393
|
+
`chrome.dom.openOrClosedShadowRoot`) and through `display: contents` wrappers; what's inside a
|
|
394
|
+
`visibility: hidden` element is still read, nothing inside `display: none`; at most 20,000
|
|
395
|
+
(`MAX_ELEMENTS`). `otherSiteFrames(doc)` gives the origins of frames from other sites, which a
|
|
396
|
+
reading of the tab leaves out until the person allows those sites.
|
|
397
|
+
|
|
353
398
|
## Time limits
|
|
354
399
|
|
|
355
400
|
```js
|
|
@@ -513,9 +558,10 @@ A list with an Add button needs every key but `optionsRemoveLabel`; one with an
|
|
|
513
558
|
| `vendorPackage(name, files, dest)`, `packageDir(name)` | Copy files of an installed package, stamped (`extension-kit vendor <dir> <package> <path>...`). |
|
|
514
559
|
| `checkVendored(dir)` | The stale copies under `dir` (`extension-kit vendor-check`). |
|
|
515
560
|
| `renderIcons`, `renderToolbarIcons({chromium, dir})` | See above. |
|
|
516
|
-
| `buildTestCopy({from, to, hook, manifest})` | A test build: the extension, `hook` as its service worker (`e2e-hook.js`), the manifest changed. |
|
|
561
|
+
| `buildTestCopy({from, to, hook, manifest, panelOpener})` | A test build: the extension, `hook` as its service worker (`e2e-hook.js`), the manifest changed; with `panelOpener` (`'panel/panel.html'`), `e2e/open-panel.html?tab=<id>` (`PANEL_OPENER`), whose button opens that side panel beside the tab as the toolbar click does (Playwright can't click the toolbar; its title turns `opened`). |
|
|
517
562
|
| `testBuildDir(root, name)` | `.test-builds/<name>`, or under `$EXTENSION_TEST_BUILDS` (to test another build, or run two side by side). |
|
|
518
563
|
| `storeFrame` | Store screenshots: `canvas`, `windowChrome` (and `windowChromeSvg`, from `frame/window.js`), `geometry`, `letterIcon`, `cursor`, `THEMES`, `W`×`H`. |
|
|
564
|
+
| `storeShots({browser, dir, toolbarIcon})`, `storeShotHtml(shot)`, `contextMenuHtml(menu)`, `pngUrl(png)`, `PANEL_W`, `PANEL_HEADER_H` | The family's store screenshots in one style: a caption in sentence case over Chrome's light window with the extension pinned in the toolbar, real captures (taken at 2×) inside; what headless Chrome can't capture composed around the real extension UI: the popup page hung from the pressed toolbar button, the side panel page beside the page under a header with the extension's name, the right-click menu drawn as macOS draws Chrome's (the extension's real titles). `shoot(file, shot)` writes a 1280×800 PNG. |
|
|
519
565
|
| `placeholderWarning`, `repositoryReminder`, `printReleaseChecks` | See About links. |
|
|
520
566
|
| `findDisallowedClones({runJscpd})` | Run jscpd and return clones `.jscpd-allowlist.json` doesn't cover (the `extension-kit duplication` command). |
|
|
521
567
|
|
|
@@ -546,6 +592,9 @@ except those listed, each with a reason:
|
|
|
546
592
|
| `realisticBrowser(options)`, `chromeUserAgent(browser)` | Launch options and a user agent for a browser that doesn't say it's automated, for live checks. |
|
|
547
593
|
| `closeOnSignal(close)` | On Ctrl-C or a kill, close the run's browsers, then exit. |
|
|
548
594
|
| `testBuildDir(root, name)` | As in `/build`, for the tests that load the build. |
|
|
595
|
+
| `tabIdOf(worker, page)` | A Playwright page's tab id, as the extension's worker sees it. |
|
|
596
|
+
| `callTabModule(worker, tabId, module, name, args)` | An export of the extension's page module called in a tab's frames, as `callInPage` does: `[{frameId, result}]`. |
|
|
597
|
+
| `pierceNodes(cdp)`, `nodeAttribute(node, name)`, `nodeText(node)` | A page's whole tree through the DevTools protocol, closed shadow roots and frames included (where an extension's overlay is), and a node's attribute and text. |
|
|
549
598
|
| `serve(routes, {port, tls})` | Serve test pages on 127.0.0.1 from a table, a `Map` or a function of the path; `{origin, port, server, close()}`. With `tls` (`testCertificate()`, a throwaway one from `openssl`), over HTTPS: for a site Chrome reaches only over HTTPS, mapped with `--host-resolver-rules`, Chromium started with `--ignore-certificate-errors`. |
|
|
550
599
|
| `until(fn, what, timeout?)` | Poll until truthy. |
|
|
551
600
|
| `offscreenEmulator(cdp)` | Emulate light/dark inside the offscreen document. |
|
|
@@ -554,6 +603,7 @@ except those listed, each with a reason:
|
|
|
554
603
|
| `warningsAddedBy(worker, manifest, names)` | `{warnings, without, added}`: the warnings with and without some permissions, and those only they bring (`withoutPermissions` is the pure part). |
|
|
555
604
|
| `savedTabGroups(profile)` | Chrome's saved tab groups (`{title, pinned, urls}`) read from a closed profile's `Sync Data/LevelDB`: no extension API lists them. |
|
|
556
605
|
| `parseLiveOptions(argv, env, {usage})`, `parseProxy(address)` | A live checker's command line (`--only`, `--category`, `--concurrency`, `--proxy` or `HTTPS_PROXY`, `--locale`, `--timezone`, `--out`, `--help`), and a proxy address for Playwright with its password hidden for the output. |
|
|
606
|
+
| `runLiveCheck({chromium, build, prefix, root, sites, categories, pageMs, visit, failed, onResult})`, `liveUsage(categories, n)` | A live checker's whole run: its command line (`parseLiveOptions`, usage from the categories), the sites chosen, the exit point printed, each site visited on a fresh page in a browser with the extension (one a slot, at most 4, passing for a person's), within `pageMs` (a browser whose page ran out of time replaced), Ctrl-C closing them all; `{dir, started, finished, exit, browser, results}` for the checker's report. |
|
|
557
607
|
| `exitPoint(request)` | Where the run's traffic leaves for the internet (`{ip, country, where, source}`), from a keyless lookup service through the context's proxy: sites decide what to show by it. |
|
|
558
608
|
| `filesUnder`, `zipEntries`, `missingReferences`, `manifestReferences` | For a packaging test: the zip is the folder, and every path the extension names resolves (a `#fragment` or `?query` ignored: `referencedPath`). |
|
|
559
609
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arraypress/extension-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Shared pieces for Chrome extensions: Chrome's own page styles and components, a Chrome Sync settings store, the light/dark toolbar glyph, Chrome's on-device model, content-script helpers, About links, and build and test helpers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/build/index.d.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
export { clonesFromReport, disallowedClones, findDisallowedClones } from './duplication.js';
|
|
8
8
|
export { placeholderWarning, printReleaseChecks, repositoryReminder } from './release-checks.js';
|
|
9
9
|
export * as storeFrame from './store-frame.js';
|
|
10
|
-
export {
|
|
10
|
+
export { PANEL_HEADER_H, PANEL_W, contextMenuHtml, pngUrl, storeShotHtml, storeShots } from './store-shots.js';
|
|
11
|
+
export { HOOK_FILE, PANEL_OPENER, TEST_BUILDS_ENV, buildTestCopy, testBuildDir } from './test-build.js';
|
|
11
12
|
export { ICON_SIZES, TOOLBAR_COLORS, TOOLBAR_ICON_COLORS, TOOLBAR_ICON_SIZES, renderIcons, renderToolbarIcons, toolbarIconFile, } from './toolbar-icons.js';
|
|
12
13
|
export { RUNTIME_ENTRIES, STYLESHEET, VENDOR_STAMP, checkVendored, packageDir, vendorKit, vendorPackage } from './vendor.js';
|
|
13
14
|
export { ZIP_EXCLUDE, shipsFile, zipExtension } from './zip.js';
|
package/src/build/index.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
export { clonesFromReport, disallowedClones, findDisallowedClones } from './duplication.js';
|
|
9
9
|
export { placeholderWarning, printReleaseChecks, repositoryReminder } from './release-checks.js';
|
|
10
10
|
export * as storeFrame from './store-frame.js';
|
|
11
|
-
export {
|
|
11
|
+
export { PANEL_HEADER_H, PANEL_W, contextMenuHtml, pngUrl, storeShotHtml, storeShots } from './store-shots.js';
|
|
12
|
+
export { HOOK_FILE, PANEL_OPENER, TEST_BUILDS_ENV, buildTestCopy, testBuildDir } from './test-build.js';
|
|
12
13
|
export {
|
|
13
14
|
ICON_SIZES, TOOLBAR_COLORS, TOOLBAR_ICON_COLORS, TOOLBAR_ICON_SIZES, renderIcons, renderToolbarIcons, toolbarIconFile,
|
|
14
15
|
} from './toolbar-icons.js';
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store screenshots for the whole family, in one style: a caption band in sentence case over
|
|
3
|
+
* Chrome's window (light theme) with the extension pinned in the toolbar, real captures inside.
|
|
4
|
+
* What headless Chrome can't capture is composed around the real extension UI, as Chrome draws
|
|
5
|
+
* it: the toolbar popup hanging from the extension's button (the popup page captured on its own),
|
|
6
|
+
* the side panel beside the page (the panel page captured on its own) and the right-click menu
|
|
7
|
+
* (drawn, with the extension's real menu titles).
|
|
8
|
+
*
|
|
9
|
+
* {@link storeShotHtml} lays one out (pure); {@link storeShots} renders them in a Playwright
|
|
10
|
+
* page at 2× and writes 1280×800 PNGs. Captures are taken at 2× too
|
|
11
|
+
* (`deviceScaleFactor: 2`) and shown at their CSS size, so text stays sharp.
|
|
12
|
+
*
|
|
13
|
+
* @module build/store-shots
|
|
14
|
+
*/
|
|
15
|
+
import type { Tab, Theme } from '../frame/window.js';
|
|
16
|
+
import type { Geometry } from './store-frame.js';
|
|
17
|
+
/** @import { Tab, Theme } from '../frame/window.js' */
|
|
18
|
+
/** @import { Geometry } from './store-frame.js' */
|
|
19
|
+
/** The side panel's width, as Chrome opens it (px). */
|
|
20
|
+
export declare const PANEL_W = 360;
|
|
21
|
+
/** The side panel's header: the extension's icon and name, and Chrome's close button (px). Capture a panel page `PANEL_HEADER_H` shorter than the content. */
|
|
22
|
+
export declare const PANEL_HEADER_H = 40;
|
|
23
|
+
export type MenuItem = {
|
|
24
|
+
/**
|
|
25
|
+
* Its words (none: a separator).
|
|
26
|
+
*/
|
|
27
|
+
label?: string;
|
|
28
|
+
/**
|
|
29
|
+
* A 16px icon's URL (the extension's, beside its own item).
|
|
30
|
+
*/
|
|
31
|
+
icon?: string;
|
|
32
|
+
/**
|
|
33
|
+
* It opens a submenu (an arrow).
|
|
34
|
+
*/
|
|
35
|
+
submenu?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* The pointer is over it.
|
|
38
|
+
*/
|
|
39
|
+
highlighted?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Greyed out.
|
|
42
|
+
*/
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* One row of the right-click menu.
|
|
47
|
+
*
|
|
48
|
+
* @typedef {object} MenuItem
|
|
49
|
+
* @property {string} [label] Its words (none: a separator).
|
|
50
|
+
* @property {string} [icon] A 16px icon's URL (the extension's, beside its own item).
|
|
51
|
+
* @property {boolean} [submenu] It opens a submenu (an arrow).
|
|
52
|
+
* @property {boolean} [highlighted] The pointer is over it.
|
|
53
|
+
* @property {boolean} [disabled] Greyed out.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* The right-click menu as macOS draws Chrome's (a native menu): rounded, translucent, 13px rows,
|
|
57
|
+
* the row under the pointer in the accent colour; a submenu beside its highlighted row. Its class
|
|
58
|
+
* names start `ctx` (the window's own styles have `.sep`, `.tab`…).
|
|
59
|
+
*
|
|
60
|
+
* @param {object} menu - What to draw.
|
|
61
|
+
* @param {number} menu.x - Its left edge, in the window's content area (px).
|
|
62
|
+
* @param {number} menu.y - Its top edge, in the content area.
|
|
63
|
+
* @param {readonly MenuItem[]} menu.items - Its rows.
|
|
64
|
+
* @param {readonly MenuItem[]} [menu.submenu] - The highlighted row's submenu.
|
|
65
|
+
* @returns {string} The HTML, positioned in the content area.
|
|
66
|
+
*/
|
|
67
|
+
export declare function contextMenuHtml({ x, y, items, submenu }: {
|
|
68
|
+
x: number;
|
|
69
|
+
y: number;
|
|
70
|
+
items: readonly MenuItem[];
|
|
71
|
+
submenu?: readonly MenuItem[];
|
|
72
|
+
}): string;
|
|
73
|
+
export type Shot = {
|
|
74
|
+
/**
|
|
75
|
+
* The caption, sentence case, a few words.
|
|
76
|
+
*/
|
|
77
|
+
caption: string;
|
|
78
|
+
/**
|
|
79
|
+
* The window's tabs.
|
|
80
|
+
*/
|
|
81
|
+
tabs: readonly Tab[];
|
|
82
|
+
/**
|
|
83
|
+
* The address bar's text.
|
|
84
|
+
*/
|
|
85
|
+
url: string;
|
|
86
|
+
/**
|
|
87
|
+
* The start of `url` shown in full; the rest dimmed.
|
|
88
|
+
*/
|
|
89
|
+
urlHost?: string;
|
|
90
|
+
/**
|
|
91
|
+
* The page's capture (a data: URL), at the content's size (less the side panel's when there's one).
|
|
92
|
+
*/
|
|
93
|
+
page: string;
|
|
94
|
+
/**
|
|
95
|
+
* The popup's capture and its size: hung from the extension's toolbar button.
|
|
96
|
+
*/
|
|
97
|
+
popup?: {
|
|
98
|
+
image: string;
|
|
99
|
+
width: number;
|
|
100
|
+
height: number;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* The side panel's capture (`PANEL_W` wide, the content's height less `PANEL_HEADER_H`), the extension's name and icon for its header.
|
|
104
|
+
*/
|
|
105
|
+
panel?: {
|
|
106
|
+
image: string;
|
|
107
|
+
title: string;
|
|
108
|
+
icon: string;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* A right-click menu over the page.
|
|
112
|
+
*/
|
|
113
|
+
menu?: Parameters<typeof contextMenuHtml>[0];
|
|
114
|
+
/**
|
|
115
|
+
* The mouse pointer, in the content area.
|
|
116
|
+
*/
|
|
117
|
+
pointer?: {
|
|
118
|
+
x: number;
|
|
119
|
+
y: number;
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* One store screenshot.
|
|
124
|
+
*
|
|
125
|
+
* @typedef {object} Shot
|
|
126
|
+
* @property {string} caption The caption, sentence case, a few words.
|
|
127
|
+
* @property {readonly Tab[]} tabs The window's tabs.
|
|
128
|
+
* @property {string} url The address bar's text.
|
|
129
|
+
* @property {string} [urlHost] The start of `url` shown in full; the rest dimmed.
|
|
130
|
+
* @property {string} page The page's capture (a data: URL), at the content's size (less the side panel's when there's one).
|
|
131
|
+
* @property {{image: string, width: number, height: number}} [popup] The popup's capture and its size: hung from the extension's toolbar button.
|
|
132
|
+
* @property {{image: string, title: string, icon: string}} [panel] The side panel's capture (`PANEL_W` wide, the content's height less `PANEL_HEADER_H`), the extension's name and icon for its header.
|
|
133
|
+
* @property {Parameters<typeof contextMenuHtml>[0]} [menu] A right-click menu over the page.
|
|
134
|
+
* @property {{x: number, y: number}} [pointer] The mouse pointer, in the content area.
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* The HTML of one store screenshot: the caption, Chrome's window with the extension's toolbar
|
|
138
|
+
* button, and the captures; a popup hangs from the button (shown pressed), a side panel sits at
|
|
139
|
+
* the window's right, a menu over the page.
|
|
140
|
+
*
|
|
141
|
+
* @param {Shot & {toolbarIcon: string, theme?: Theme, geo?: Geometry}} shot - What to draw; the extension's toolbar icon (a data: URL).
|
|
142
|
+
* @returns {string} A whole HTML document, {@link W}×{@link H}.
|
|
143
|
+
*/
|
|
144
|
+
export declare function storeShotHtml({ caption, tabs, url, urlHost, page, popup, panel, menu, pointer, toolbarIcon, theme, geo }: Shot & {
|
|
145
|
+
toolbarIcon: string;
|
|
146
|
+
theme?: Theme;
|
|
147
|
+
geo?: Geometry;
|
|
148
|
+
}): string;
|
|
149
|
+
/**
|
|
150
|
+
* A data: URL of a PNG.
|
|
151
|
+
*
|
|
152
|
+
* @param {Uint8Array} png - The PNG's bytes.
|
|
153
|
+
* @returns {string} The URL.
|
|
154
|
+
*/
|
|
155
|
+
export declare const pngUrl: (png: Uint8Array) => string;
|
|
156
|
+
/**
|
|
157
|
+
* A store-screenshot writer: lays each shot out with {@link storeShotHtml} in a page of the
|
|
158
|
+
* given browser at 2×, waits for its images, and saves it as a {@link W}×{@link H} PNG.
|
|
159
|
+
*
|
|
160
|
+
* @param {object} options - Where and how.
|
|
161
|
+
* @param {import('playwright').Browser | import('playwright').BrowserContext} options.browser - A browser (or a context) to draw in.
|
|
162
|
+
* @param {string} options.dir - The folder to write to (made if missing).
|
|
163
|
+
* @param {string} options.toolbarIcon - The extension's toolbar icon, a data: URL.
|
|
164
|
+
* @returns {{geo: Geometry, shoot: (file: string, shot: Shot) => Promise<string>}} The window's geometry
|
|
165
|
+
* (capture pages at `geo.content`), and `shoot`, which writes one and returns its path.
|
|
166
|
+
* @example
|
|
167
|
+
* const { geo, shoot } = storeShots({ browser, dir: 'store/screenshots', toolbarIcon });
|
|
168
|
+
* await shoot('1-menu.png', { caption: 'Copy any page as Markdown', tabs, url, page: pngUrl(await page.screenshot()) });
|
|
169
|
+
*/
|
|
170
|
+
export declare function storeShots({ browser, dir, toolbarIcon }: {
|
|
171
|
+
browser: import('playwright').Browser | import('playwright').BrowserContext;
|
|
172
|
+
dir: string;
|
|
173
|
+
toolbarIcon: string;
|
|
174
|
+
}): {
|
|
175
|
+
geo: Geometry;
|
|
176
|
+
shoot: (file: string, shot: Shot) => Promise<string>;
|
|
177
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store screenshots for the whole family, in one style: a caption band in sentence case over
|
|
3
|
+
* Chrome's window (light theme) with the extension pinned in the toolbar, real captures inside.
|
|
4
|
+
* What headless Chrome can't capture is composed around the real extension UI, as Chrome draws
|
|
5
|
+
* it: the toolbar popup hanging from the extension's button (the popup page captured on its own),
|
|
6
|
+
* the side panel beside the page (the panel page captured on its own) and the right-click menu
|
|
7
|
+
* (drawn, with the extension's real menu titles).
|
|
8
|
+
*
|
|
9
|
+
* {@link storeShotHtml} lays one out (pure); {@link storeShots} renders them in a Playwright
|
|
10
|
+
* page at 2× and writes 1280×800 PNGs. Captures are taken at 2× too
|
|
11
|
+
* (`deviceScaleFactor: 2`) and shown at their CSS size, so text stays sharp.
|
|
12
|
+
*
|
|
13
|
+
* @module build/store-shots
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { esc } from '../frame/window.js';
|
|
19
|
+
import { H, W, WIN, canvas, cursor, geometry, windowChrome } from './store-frame.js';
|
|
20
|
+
|
|
21
|
+
/** @import { Tab, Theme } from '../frame/window.js' */
|
|
22
|
+
/** @import { Geometry } from './store-frame.js' */
|
|
23
|
+
|
|
24
|
+
/** The side panel's width, as Chrome opens it (px). */
|
|
25
|
+
export const PANEL_W = 360;
|
|
26
|
+
|
|
27
|
+
/** The side panel's header: the extension's icon and name, and Chrome's close button (px). Capture a panel page `PANEL_HEADER_H` shorter than the content. */
|
|
28
|
+
export const PANEL_HEADER_H = 40;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One row of the right-click menu.
|
|
32
|
+
*
|
|
33
|
+
* @typedef {object} MenuItem
|
|
34
|
+
* @property {string} [label] Its words (none: a separator).
|
|
35
|
+
* @property {string} [icon] A 16px icon's URL (the extension's, beside its own item).
|
|
36
|
+
* @property {boolean} [submenu] It opens a submenu (an arrow).
|
|
37
|
+
* @property {boolean} [highlighted] The pointer is over it.
|
|
38
|
+
* @property {boolean} [disabled] Greyed out.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The right-click menu as macOS draws Chrome's (a native menu): rounded, translucent, 13px rows,
|
|
43
|
+
* the row under the pointer in the accent colour; a submenu beside its highlighted row. Its class
|
|
44
|
+
* names start `ctx` (the window's own styles have `.sep`, `.tab`…).
|
|
45
|
+
*
|
|
46
|
+
* @param {object} menu - What to draw.
|
|
47
|
+
* @param {number} menu.x - Its left edge, in the window's content area (px).
|
|
48
|
+
* @param {number} menu.y - Its top edge, in the content area.
|
|
49
|
+
* @param {readonly MenuItem[]} menu.items - Its rows.
|
|
50
|
+
* @param {readonly MenuItem[]} [menu.submenu] - The highlighted row's submenu.
|
|
51
|
+
* @returns {string} The HTML, positioned in the content area.
|
|
52
|
+
*/
|
|
53
|
+
export function contextMenuHtml({ x, y, items, submenu }) {
|
|
54
|
+
/** @param {readonly MenuItem[]} rows @returns {string} */
|
|
55
|
+
const list = (rows) => rows.map((row) => (row.label === undefined
|
|
56
|
+
? '<div class="ctx-sep"></div>'
|
|
57
|
+
: `<div class="ctx-row${row.highlighted ? ' on' : ''}${row.disabled ? ' off' : ''}"><span class="ctx-ic">${row.icon ? `<img src="${row.icon}" width="16" height="16" alt="">` : ''}</span><span class="ctx-label">${esc(row.label)}</span>${row.submenu ? '<span class="ctx-arrow">›</span>' : ''}</div>`)).join('');
|
|
58
|
+
const at = items.findIndex((row) => row.highlighted && row.submenu);
|
|
59
|
+
const top = (/** @type {number} */ index) => 5 + items.slice(0, index).reduce((sum, row) => sum + (row.label === undefined ? 11 : 22), 0);
|
|
60
|
+
return `<div class="ctx" style="left:${x}px;top:${y}px">${list(items)}</div>`
|
|
61
|
+
+ (submenu && at >= 0 ? `<div class="ctx" style="left:${x + 236}px;top:${y + top(at) - 5}px">${list(submenu)}</div>` : '')
|
|
62
|
+
+ `<style>
|
|
63
|
+
.ctx { position: absolute; z-index: 10; width: 240px; box-sizing: border-box; padding: 5px; border-radius: 10px; background: rgba(246,246,246,.96);
|
|
64
|
+
box-shadow: 0 0 0 .5px rgba(0,0,0,.22), 0 10px 30px rgba(0,0,0,.22); font: 13px/22px system-ui; color: #1d1d1f; }
|
|
65
|
+
.ctx-row { display: flex; align-items: center; height: 22px; padding: 0 8px 0 4px; border-radius: 5px; white-space: nowrap; }
|
|
66
|
+
.ctx-row.on { background: #0a82ff; color: #fff; }
|
|
67
|
+
.ctx-row.off { color: #b0b0b0; }
|
|
68
|
+
.ctx-ic { width: 22px; flex: none; display: flex; }
|
|
69
|
+
.ctx-label { flex: 1; overflow: hidden; text-overflow: ellipsis; }
|
|
70
|
+
.ctx-arrow { flex: none; font-size: 15px; opacity: .7; }
|
|
71
|
+
.ctx-sep { height: 1px; margin: 5px 8px; background: rgba(0,0,0,.11); }
|
|
72
|
+
</style>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* One store screenshot.
|
|
77
|
+
*
|
|
78
|
+
* @typedef {object} Shot
|
|
79
|
+
* @property {string} caption The caption, sentence case, a few words.
|
|
80
|
+
* @property {readonly Tab[]} tabs The window's tabs.
|
|
81
|
+
* @property {string} url The address bar's text.
|
|
82
|
+
* @property {string} [urlHost] The start of `url` shown in full; the rest dimmed.
|
|
83
|
+
* @property {string} page The page's capture (a data: URL), at the content's size (less the side panel's when there's one).
|
|
84
|
+
* @property {{image: string, width: number, height: number}} [popup] The popup's capture and its size: hung from the extension's toolbar button.
|
|
85
|
+
* @property {{image: string, title: string, icon: string}} [panel] The side panel's capture (`PANEL_W` wide, the content's height less `PANEL_HEADER_H`), the extension's name and icon for its header.
|
|
86
|
+
* @property {Parameters<typeof contextMenuHtml>[0]} [menu] A right-click menu over the page.
|
|
87
|
+
* @property {{x: number, y: number}} [pointer] The mouse pointer, in the content area.
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The HTML of one store screenshot: the caption, Chrome's window with the extension's toolbar
|
|
92
|
+
* button, and the captures; a popup hangs from the button (shown pressed), a side panel sits at
|
|
93
|
+
* the window's right, a menu over the page.
|
|
94
|
+
*
|
|
95
|
+
* @param {Shot & {toolbarIcon: string, theme?: Theme, geo?: Geometry}} shot - What to draw; the extension's toolbar icon (a data: URL).
|
|
96
|
+
* @returns {string} A whole HTML document, {@link W}×{@link H}.
|
|
97
|
+
*/
|
|
98
|
+
export function storeShotHtml({ caption, tabs, url, urlHost, page, popup, panel, menu, pointer, toolbarIcon, theme = 'light', geo = geometry(1) }) {
|
|
99
|
+
const { width: cw, height: ch } = geo.content;
|
|
100
|
+
const pageW = panel ? cw - PANEL_W : cw;
|
|
101
|
+
const content = `<div style="position:relative;display:flex;width:${cw}px;height:${ch}px">
|
|
102
|
+
<img src="${page}" width="${pageW}" height="${ch}" alt="" style="display:block;flex:none">
|
|
103
|
+
${panel ? `<div class="panel"><div class="panel-head"><img src="${panel.icon}" width="16" height="16" alt=""><span>${esc(panel.title)}</span><svg viewBox="0 0 24 24" width="18" height="18"><path fill="#474747" d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg></div>
|
|
104
|
+
<img src="${panel.image}" width="${PANEL_W}" height="${ch - PANEL_HEADER_H}" alt="" style="display:block"></div>` : ''}
|
|
105
|
+
${menu ? contextMenuHtml(menu) : ''}
|
|
106
|
+
${pointer ? cursor(pointer.x, pointer.y) : ''}
|
|
107
|
+
</div>
|
|
108
|
+
<style>
|
|
109
|
+
.panel { flex: none; width: ${PANEL_W}px; height: ${ch}px; background: #fff; border-left: 1px solid #d3d7dc; box-sizing: content-box; }
|
|
110
|
+
.panel-head { height: ${PANEL_HEADER_H}px; display: flex; align-items: center; gap: 10px; padding: 0 12px 0 14px; font: 500 13px system-ui; color: #1f1f1f; }
|
|
111
|
+
.panel-head span { flex: 1; }
|
|
112
|
+
</style>`;
|
|
113
|
+
const overlays = popup ? `<div class="popup" style="width:${popup.width}px;height:${popup.height}px"><img src="${popup.image}" width="${popup.width}" height="${popup.height}" alt=""></div>
|
|
114
|
+
<style>
|
|
115
|
+
.popup { position: absolute; z-index: 30; border-radius: 10px; overflow: hidden; background: #fff; box-shadow: 0 0 0 .5px rgba(0,0,0,.2), 0 4px 12px rgba(0,0,0,.14), 0 12px 32px rgba(0,0,0,.16); }
|
|
116
|
+
.popup img { display: block; }
|
|
117
|
+
.toolbar > img.pressed { border-radius: 50%; background: #e3e3e3; box-shadow: 0 0 0 9px #e3e3e3; }
|
|
118
|
+
</style>
|
|
119
|
+
<script>
|
|
120
|
+
// Hang the popup from the extension's button, as Chrome does: its right edge near the button's, just under the toolbar.
|
|
121
|
+
const button = document.querySelector('.toolbar > img');
|
|
122
|
+
button.classList.add('pressed');
|
|
123
|
+
const box = button.getBoundingClientRect();
|
|
124
|
+
const popupBox = document.querySelector('.popup');
|
|
125
|
+
popupBox.style.left = (box.right + 14 - ${popup.width}) + 'px';
|
|
126
|
+
popupBox.style.top = (${WIN.y + WIN.strip + WIN.toolbar} - 2) + 'px';
|
|
127
|
+
</script>` : '';
|
|
128
|
+
return canvas({ caption: esc(caption), theme, chrome: windowChrome({ tabs, url, urlHost, theme, w: geo.w, ext: [toolbarIcon] }), content, overlays, geo });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A data: URL of a PNG.
|
|
133
|
+
*
|
|
134
|
+
* @param {Uint8Array} png - The PNG's bytes.
|
|
135
|
+
* @returns {string} The URL.
|
|
136
|
+
*/
|
|
137
|
+
export const pngUrl = (png) => `data:image/png;base64,${Buffer.from(png).toString('base64')}`;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A store-screenshot writer: lays each shot out with {@link storeShotHtml} in a page of the
|
|
141
|
+
* given browser at 2×, waits for its images, and saves it as a {@link W}×{@link H} PNG.
|
|
142
|
+
*
|
|
143
|
+
* @param {object} options - Where and how.
|
|
144
|
+
* @param {import('playwright').Browser | import('playwright').BrowserContext} options.browser - A browser (or a context) to draw in.
|
|
145
|
+
* @param {string} options.dir - The folder to write to (made if missing).
|
|
146
|
+
* @param {string} options.toolbarIcon - The extension's toolbar icon, a data: URL.
|
|
147
|
+
* @returns {{geo: Geometry, shoot: (file: string, shot: Shot) => Promise<string>}} The window's geometry
|
|
148
|
+
* (capture pages at `geo.content`), and `shoot`, which writes one and returns its path.
|
|
149
|
+
* @example
|
|
150
|
+
* const { geo, shoot } = storeShots({ browser, dir: 'store/screenshots', toolbarIcon });
|
|
151
|
+
* await shoot('1-menu.png', { caption: 'Copy any page as Markdown', tabs, url, page: pngUrl(await page.screenshot()) });
|
|
152
|
+
*/
|
|
153
|
+
export function storeShots({ browser, dir, toolbarIcon }) {
|
|
154
|
+
const geo = geometry(1);
|
|
155
|
+
return {
|
|
156
|
+
geo,
|
|
157
|
+
shoot: async (file, shot) => {
|
|
158
|
+
const context = 'newContext' in browser ? await browser.newContext({ viewport: { width: W, height: H }, deviceScaleFactor: 2 }) : null;
|
|
159
|
+
const page = context ? await context.newPage() : await /** @type {import('playwright').BrowserContext} */ (browser).newPage();
|
|
160
|
+
try {
|
|
161
|
+
await page.setViewportSize({ width: W, height: H });
|
|
162
|
+
await page.setContent(storeShotHtml({ ...shot, toolbarIcon, geo }), { waitUntil: 'load' });
|
|
163
|
+
await page.evaluate(() => Promise.all([...document.images].map((img) => img.decode().catch(() => undefined))));
|
|
164
|
+
await mkdir(dir, { recursive: true });
|
|
165
|
+
const out = path.join(dir, file);
|
|
166
|
+
await writeFile(out, await page.screenshot({ type: 'png', scale: 'css', clip: { x: 0, y: 0, width: W, height: H } }));
|
|
167
|
+
return out;
|
|
168
|
+
} finally {
|
|
169
|
+
await (context ?? page).close();
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -54,6 +54,12 @@ export type TestBuild = {
|
|
|
54
54
|
* Changes the copy's manifest in place.
|
|
55
55
|
*/
|
|
56
56
|
manifest?: (manifest: TestManifest) => void;
|
|
57
|
+
/**
|
|
58
|
+
* A side panel page (`panel/panel.html`): adds {@link PANEL_OPENER}(`?tab=<id>`), whose button opens that panel beside the tab the way the toolbar click does
|
|
59
|
+
* (the kit's `openSidePanel`, during a real click), which Playwright can't click. Its title turns
|
|
60
|
+
* `opened` (or `error: …`). The kit must be vendored at `vendor/extension-kit/`.
|
|
61
|
+
*/
|
|
62
|
+
panelOpener?: string;
|
|
57
63
|
};
|
|
58
64
|
/**
|
|
59
65
|
* A Chrome extension manifest, as far as a test build changes it.
|
|
@@ -70,7 +76,13 @@ export type TestBuild = {
|
|
|
70
76
|
* imports the real worker and exposes what a test can't reach headlessly (a menu click, the
|
|
71
77
|
* toolbar button).
|
|
72
78
|
* @property {(manifest: TestManifest) => void} [manifest] Changes the copy's manifest in place.
|
|
79
|
+
* @property {string} [panelOpener] A side panel page (`panel/panel.html`): adds {@link PANEL_OPENER}
|
|
80
|
+
* (`?tab=<id>`), whose button opens that panel beside the tab the way the toolbar click does
|
|
81
|
+
* (the kit's `openSidePanel`, during a real click), which Playwright can't click. Its title turns
|
|
82
|
+
* `opened` (or `error: …`). The kit must be vendored at `vendor/extension-kit/`.
|
|
73
83
|
*/
|
|
84
|
+
/** The page {@link TestBuild}'s `panelOpener` adds, from the build's root. */
|
|
85
|
+
export declare const PANEL_OPENER = "e2e/open-panel.html";
|
|
74
86
|
/**
|
|
75
87
|
* Copy an extension folder into a test build, with its own hook and manifest.
|
|
76
88
|
*
|
|
@@ -82,4 +94,4 @@ export type TestBuild = {
|
|
|
82
94
|
* manifest: (manifest) => { manifest.host_permissions = ['http://127.0.0.1/*']; },
|
|
83
95
|
* });
|
|
84
96
|
*/
|
|
85
|
-
export declare function buildTestCopy({ from, to, hook, manifest: change }: TestBuild): Promise<string>;
|
|
97
|
+
export declare function buildTestCopy({ from, to, hook, manifest: change, panelOpener }: TestBuild): Promise<string>;
|
package/src/build/test-build.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @module build/test-build
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { cp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
|
|
17
17
|
/** The environment variable that moves the test builds' folder (absolute, or relative to the extension's root). */
|
|
@@ -53,8 +53,42 @@ export function testBuildDir(root, name = 'e2e', env = process.env) {
|
|
|
53
53
|
* imports the real worker and exposes what a test can't reach headlessly (a menu click, the
|
|
54
54
|
* toolbar button).
|
|
55
55
|
* @property {(manifest: TestManifest) => void} [manifest] Changes the copy's manifest in place.
|
|
56
|
+
* @property {string} [panelOpener] A side panel page (`panel/panel.html`): adds {@link PANEL_OPENER}
|
|
57
|
+
* (`?tab=<id>`), whose button opens that panel beside the tab the way the toolbar click does
|
|
58
|
+
* (the kit's `openSidePanel`, during a real click), which Playwright can't click. Its title turns
|
|
59
|
+
* `opened` (or `error: …`). The kit must be vendored at `vendor/extension-kit/`.
|
|
56
60
|
*/
|
|
57
61
|
|
|
62
|
+
/** The page {@link TestBuild}'s `panelOpener` adds, from the build's root. */
|
|
63
|
+
export const PANEL_OPENER = 'e2e/open-panel.html';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The opener page's markup (its script a file beside it: extension pages run no inline script).
|
|
67
|
+
*
|
|
68
|
+
* @returns {string} The HTML.
|
|
69
|
+
*/
|
|
70
|
+
const openerHtml = () => `<!doctype html>
|
|
71
|
+
<meta charset="utf-8">
|
|
72
|
+
<title>Open the side panel</title>
|
|
73
|
+
<!-- Test builds only: a button that does what the toolbar click does, which Playwright can't click. -->
|
|
74
|
+
<button id="open" type="button">Open the side panel</button>
|
|
75
|
+
<script type="module" src="open-panel.js"></script>
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The opener page's script.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} panel - The panel page.
|
|
82
|
+
* @returns {string} The module.
|
|
83
|
+
*/
|
|
84
|
+
const openerScript = (panel) => `import { openSidePanel } from '../vendor/extension-kit/panel/side-panel.js';
|
|
85
|
+
|
|
86
|
+
const tabId = Number(new URLSearchParams(location.search).get('tab'));
|
|
87
|
+
document.getElementById('open').addEventListener('click', () => {
|
|
88
|
+
openSidePanel(chrome.sidePanel, tabId, ${JSON.stringify(panel)}).then(() => { document.title = 'opened'; }, (error) => { document.title = \`error: \${error.message}\`; });
|
|
89
|
+
});
|
|
90
|
+
`;
|
|
91
|
+
|
|
58
92
|
/**
|
|
59
93
|
* Copy an extension folder into a test build, with its own hook and manifest.
|
|
60
94
|
*
|
|
@@ -66,7 +100,7 @@ export function testBuildDir(root, name = 'e2e', env = process.env) {
|
|
|
66
100
|
* manifest: (manifest) => { manifest.host_permissions = ['http://127.0.0.1/*']; },
|
|
67
101
|
* });
|
|
68
102
|
*/
|
|
69
|
-
export async function buildTestCopy({ from, to, hook, manifest: change }) {
|
|
103
|
+
export async function buildTestCopy({ from, to, hook, manifest: change, panelOpener }) {
|
|
70
104
|
await rm(to, { recursive: true, force: true });
|
|
71
105
|
await cp(from, to, { recursive: true });
|
|
72
106
|
const file = path.join(to, 'manifest.json');
|
|
@@ -78,5 +112,10 @@ export async function buildTestCopy({ from, to, hook, manifest: change }) {
|
|
|
78
112
|
}
|
|
79
113
|
change?.(manifest);
|
|
80
114
|
await writeFile(file, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
115
|
+
if (panelOpener) {
|
|
116
|
+
await mkdir(path.join(to, path.dirname(PANEL_OPENER)), { recursive: true });
|
|
117
|
+
await writeFile(path.join(to, PANEL_OPENER), openerHtml());
|
|
118
|
+
await writeFile(path.join(to, PANEL_OPENER.replace(/\.html$/, '.js')), openerScript(panelOpener));
|
|
119
|
+
}
|
|
81
120
|
return to;
|
|
82
121
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A page's rendered elements, for a content script that reads them all: in
|
|
3
|
+
* document order, into open shadow roots and closed ones (where Chrome lets
|
|
4
|
+
* an extension see them, `chrome.dom.openOrClosedShadowRoot`), leaving out
|
|
5
|
+
* what Chrome doesn't render and what holds no page content (scripts,
|
|
6
|
+
* styles, templates, `<head>`).
|
|
7
|
+
*
|
|
8
|
+
* @module content/elements
|
|
9
|
+
*/
|
|
10
|
+
/** The most elements read (a very long page's first ones). */
|
|
11
|
+
export declare const MAX_ELEMENTS = 20000;
|
|
12
|
+
/**
|
|
13
|
+
* An element's shadow root, closed ones too where Chrome lets the extension see them.
|
|
14
|
+
*
|
|
15
|
+
* @param {Element} element - The element.
|
|
16
|
+
* @returns {ShadowRoot | null} Its shadow root.
|
|
17
|
+
* @example
|
|
18
|
+
* const root = shadowRootOf(host); // a closed root, from a content script
|
|
19
|
+
*/
|
|
20
|
+
export declare function shadowRootOf(element: Element): ShadowRoot | null;
|
|
21
|
+
/**
|
|
22
|
+
* Every rendered element from the body down, in document order, a shadow root's elements before
|
|
23
|
+
* its host's children. An element Chrome doesn't draw (`visibility: hidden`) is left out, but
|
|
24
|
+
* what's inside it is still read (a child can be visible again), as is what's inside a
|
|
25
|
+
* `display: contents` element (a slot), which has no box of its own; nothing inside a
|
|
26
|
+
* `display: none` or `content-visibility: hidden` part is.
|
|
27
|
+
*
|
|
28
|
+
* @param {Document} doc - The page.
|
|
29
|
+
* @param {{skip?: (element: Element) => boolean, limit?: number}} [options] - `skip`: elements left
|
|
30
|
+
* out with what's inside (the extension's overlay); `limit`: the most elements ({@link MAX_ELEMENTS}).
|
|
31
|
+
* @returns {Element[]} The elements.
|
|
32
|
+
* @example
|
|
33
|
+
* for (const element of renderedElements(document, { skip: isLayer })) read(element);
|
|
34
|
+
*/
|
|
35
|
+
export declare function renderedElements(doc: Document, { skip, limit }?: {
|
|
36
|
+
skip?: (element: Element) => boolean;
|
|
37
|
+
limit?: number;
|
|
38
|
+
}): Element[];
|
|
39
|
+
/**
|
|
40
|
+
* The origins of a page's frames that are another site's: their content isn't the page's to read,
|
|
41
|
+
* and an extension reads them only with that site's access (`chrome.permissions.request`, from a
|
|
42
|
+
* click). Frames inside shadow roots are found too.
|
|
43
|
+
*
|
|
44
|
+
* @param {Document} doc - The page.
|
|
45
|
+
* @param {Element[]} [elements] - Its elements, when already listed ({@link renderedElements}).
|
|
46
|
+
* @returns {string[]} Their origins (`https://ads.example.net`), each once.
|
|
47
|
+
* @example
|
|
48
|
+
* otherSiteFrames(document); // ['https://www.youtube.com']
|
|
49
|
+
*/
|
|
50
|
+
export declare function otherSiteFrames(doc: Document, elements?: Element[]): string[];
|