@arraypress/extension-kit 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -2
- package/package.json +1 -1
- package/src/build/index.d.ts +2 -1
- package/src/build/index.js +2 -1
- package/src/build/store-shots.d.ts +177 -0
- package/src/build/store-shots.js +173 -0
- package/src/build/test-build.d.ts +13 -1
- package/src/build/test-build.js +41 -2
- package/src/build/vendor.js +1 -1
- package/src/content/elements.d.ts +50 -0
- package/src/content/elements.js +88 -0
- package/src/content/highlight.d.ts +54 -0
- package/src/content/highlight.js +92 -0
- package/src/content/overlay.d.ts +57 -0
- package/src/content/overlay.js +52 -0
- package/src/content/page-toast.d.ts +26 -0
- package/src/content/page-toast.js +72 -0
- package/src/content/slices.d.ts +16 -1
- package/src/content/slices.js +29 -10
- package/src/dom/copy.d.ts +36 -0
- package/src/dom/copy.js +84 -0
- package/src/dom/i18n.d.ts +17 -0
- package/src/dom/i18n.js +15 -0
- package/src/dom/make.d.ts +53 -0
- package/src/dom/make.js +71 -0
- package/src/frame/window.d.ts +76 -7
- package/src/frame/window.js +119 -15
- package/src/index.d.ts +33 -5
- package/src/index.js +25 -5
- package/src/panel/side-panel.d.ts +35 -0
- package/src/panel/side-panel.js +41 -0
- package/src/scripting/call.d.ts +63 -0
- package/src/scripting/call.js +57 -0
- package/src/scripting/tab.d.ts +68 -0
- package/src/scripting/tab.js +81 -0
- package/src/styles/chrome-ui.css +7 -0
- package/src/testing/index.d.ts +6 -0
- package/src/testing/index.js +4 -0
- package/src/testing/live-run.d.ts +130 -0
- package/src/testing/live-run.js +160 -0
- package/src/testing/pages.d.ts +114 -0
- package/src/testing/pages.js +109 -0
- package/src/ui/notice.d.ts +58 -0
- package/src/ui/notice.js +43 -0
package/README.md
CHANGED
|
@@ -206,6 +206,19 @@ scale. Chrome draws an SVG image with HTML inside (`foreignObject`) without tain
|
|
|
206
206
|
(checked in Chromium, `tests/frame-chromium.test.js`). Images in it must be `data:` URLs
|
|
207
207
|
(`letterIcon` makes one). `THEMES` holds the light and dark colours.
|
|
208
208
|
|
|
209
|
+
A plain macOS window (1.9.0) draws the same way: `macWindowSvg({ title, theme, w })` is its title
|
|
210
|
+
bar as macOS 26 and later draw it (measured from AppKit on macOS 27.2), `w` wide and
|
|
211
|
+
`MAC_WINDOW.titleBar` (32px) high: 14px traffic lights (the same markup as Chrome's strip), the
|
|
212
|
+
title after them, a hairline below. The canvas draws the rest: corners of `MAC_WINDOW.radius`
|
|
213
|
+
(16px, `NSThemeFrame`'s) and the theme's shadow, `THEMES[theme].windowShadow`
|
|
214
|
+
(`{ y, blur, color }` for a canvas's `shadowOffsetY`, `shadowBlur`, `shadowColor`; `THEMES[theme].shadow`
|
|
215
|
+
is the same shadow as CSS, the store frame's).
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
context.shadowColor = THEMES.light.windowShadow.color; // and its blur and y, at the scale
|
|
219
|
+
context.roundRect(x, y, w * scale, (MAC_WINDOW.titleBar + contentHeight) * scale, MAC_WINDOW.radius * scale);
|
|
220
|
+
```
|
|
221
|
+
|
|
209
222
|
## Sites
|
|
210
223
|
|
|
211
224
|
```js
|
|
@@ -221,6 +234,66 @@ withSite(['a.com', 'b.com'], 'a.com'); // ['b.com', 'a.com'], what a list
|
|
|
221
234
|
A site is http or https with a dot in its host (or `localhost`); text without a scheme is https;
|
|
222
235
|
`localhost:8080` is a host and port. Pure, so a content script can load `sites/sites.js` alone.
|
|
223
236
|
|
|
237
|
+
## A page's side
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
// In the worker or a page: call content/main.js's `scan` in the tab's frames (main.js and what it
|
|
241
|
+
// imports listed in web_accessible_resources). The module stays loaded between calls.
|
|
242
|
+
const frames = await callInPage(chrome.scripting, { tabId, allFrames: true }, 'content/main.js', 'scan', [options]);
|
|
243
|
+
for (const { frameId, result } of frames) show(frameId, result);
|
|
244
|
+
|
|
245
|
+
// From the toolbar click: the tab's own side panel, panel/panel.html?tab=<id>, nothing awaited first.
|
|
246
|
+
chrome.action.onClicked.addListener((tab) => openSidePanel(chrome.sidePanel, tab.id, 'panel/panel.html'));
|
|
247
|
+
|
|
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 });
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`callInPage(scripting, target, module, name, args, {immediately})` resolves to each frame's
|
|
267
|
+
`{frameId, result}` (frames Chrome couldn't run in left out) and rejects when it can run in none;
|
|
268
|
+
`immediately` (default true) doesn't wait for the page to be idle. `openSidePanel` sets the tab's
|
|
269
|
+
panel to `path?tab=<id>` (`sidePanelPath`) and opens it in the same moment: Chrome opens a panel
|
|
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.
|
|
296
|
+
|
|
224
297
|
## A content script on every site
|
|
225
298
|
|
|
226
299
|
```js
|
|
@@ -309,11 +382,19 @@ const stop = watchChanges(document.body, {
|
|
|
309
382
|
|
|
310
383
|
`inSlices(root, work, {blockChars, sliceMs, pause})` cuts a page into blocks (`blocksOf`) and
|
|
311
384
|
hands the main thread back whenever a slice has run its time, so a 2,000-item listing never
|
|
312
|
-
freezes. `
|
|
385
|
+
freezes. `eachInSlices(items, work, {sliceMs, pause})` does the same for any list (each element's
|
|
386
|
+
style read, each text measured): `work(item, index)`. `watchChanges` hands over what was added or changed a little after the page stops
|
|
313
387
|
changing (at most `maxWaitMs` while it keeps on); an element with a few children (`regroupChildren`,
|
|
314
388
|
default 6) whose children changed is handed over whole. Both are pure DOM, so a content script
|
|
315
389
|
imports them directly (list them in `web_accessible_resources`).
|
|
316
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
|
+
|
|
317
398
|
## Time limits
|
|
318
399
|
|
|
319
400
|
```js
|
|
@@ -477,9 +558,10 @@ A list with an Add button needs every key but `optionsRemoveLabel`; one with an
|
|
|
477
558
|
| `vendorPackage(name, files, dest)`, `packageDir(name)` | Copy files of an installed package, stamped (`extension-kit vendor <dir> <package> <path>...`). |
|
|
478
559
|
| `checkVendored(dir)` | The stale copies under `dir` (`extension-kit vendor-check`). |
|
|
479
560
|
| `renderIcons`, `renderToolbarIcons({chromium, dir})` | See above. |
|
|
480
|
-
| `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`). |
|
|
481
562
|
| `testBuildDir(root, name)` | `.test-builds/<name>`, or under `$EXTENSION_TEST_BUILDS` (to test another build, or run two side by side). |
|
|
482
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. |
|
|
483
565
|
| `placeholderWarning`, `repositoryReminder`, `printReleaseChecks` | See About links. |
|
|
484
566
|
| `findDisallowedClones({runJscpd})` | Run jscpd and return clones `.jscpd-allowlist.json` doesn't cover (the `extension-kit duplication` command). |
|
|
485
567
|
|
|
@@ -510,6 +592,9 @@ except those listed, each with a reason:
|
|
|
510
592
|
| `realisticBrowser(options)`, `chromeUserAgent(browser)` | Launch options and a user agent for a browser that doesn't say it's automated, for live checks. |
|
|
511
593
|
| `closeOnSignal(close)` | On Ctrl-C or a kill, close the run's browsers, then exit. |
|
|
512
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. |
|
|
513
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`. |
|
|
514
599
|
| `until(fn, what, timeout?)` | Poll until truthy. |
|
|
515
600
|
| `offscreenEmulator(cdp)` | Emulate light/dark inside the offscreen document. |
|
|
@@ -518,6 +603,7 @@ except those listed, each with a reason:
|
|
|
518
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). |
|
|
519
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. |
|
|
520
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. |
|
|
521
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. |
|
|
522
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`). |
|
|
523
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
|
}
|
package/src/build/vendor.js
CHANGED
|
@@ -28,7 +28,7 @@ const SOURCE = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
|
28
28
|
const KIT = '@arraypress/extension-kit';
|
|
29
29
|
|
|
30
30
|
/** What a vendored copy of the kit holds, relative to `src/`: everything an extension page or worker may load. */
|
|
31
|
-
export const RUNTIME_ENTRIES = Object.freeze(['index.js', 'index.d.ts', 'about', 'ai', 'commands', 'content', 'dom', 'frame', 'scripting', 'settings', 'sites', 'styles', 'time', 'toolbar', 'ui']);
|
|
31
|
+
export const RUNTIME_ENTRIES = Object.freeze(['index.js', 'index.d.ts', 'about', 'ai', 'commands', 'content', 'dom', 'frame', 'panel', 'scripting', 'settings', 'sites', 'styles', 'time', 'toolbar', 'ui']);
|
|
32
32
|
|
|
33
33
|
/** The kit's stylesheet, an absolute path in the installed package. */
|
|
34
34
|
export const STYLESHEET = path.join(SOURCE, 'styles', 'chrome-ui.css');
|