@itmetsam/samurai-framework 0.10.0 → 0.12.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 +3 -4
- package/dist/assert/assertion-error.d.ts +1 -1
- package/dist/assert/expect.d.ts +5 -50
- package/dist/assert/expect.js +5 -182
- package/dist/assert/locator-assertions.d.ts +22 -0
- package/dist/assert/locator-assertions.js +61 -0
- package/dist/assert/pending-assertions.d.ts +7 -0
- package/dist/assert/pending-assertions.js +40 -0
- package/dist/assert/text-match.d.ts +4 -0
- package/dist/assert/text-match.js +11 -0
- package/dist/assert/value-assertions.d.ts +23 -0
- package/dist/assert/value-assertions.js +82 -0
- package/dist/browser/browser-context.d.ts +0 -1
- package/dist/browser/browser-context.js +13 -5
- package/dist/browser/browser-finder.js +5 -9
- package/dist/browser/browser.d.ts +3 -31
- package/dist/browser/browser.js +36 -219
- package/dist/browser/chrome-launch.d.ts +2 -1
- package/dist/browser/dialog-handling.d.ts +32 -0
- package/dist/browser/dialog-handling.js +85 -0
- package/dist/browser/document-watcher.d.ts +44 -0
- package/dist/browser/document-watcher.js +160 -0
- package/dist/browser/download-folders.d.ts +0 -1
- package/dist/browser/download-folders.js +7 -15
- package/dist/browser/download.js +5 -10
- package/dist/browser/downloads-dir.js +2 -2
- package/dist/browser/emit.d.ts +7 -0
- package/dist/browser/emit.js +16 -0
- package/dist/browser/emulation.d.ts +1 -1
- package/dist/browser/emulation.js +54 -18
- package/dist/browser/file-chooser.js +4 -11
- package/dist/browser/history.d.ts +6 -0
- package/dist/browser/history.js +89 -0
- package/dist/browser/index.d.ts +2 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/init-script.d.ts +2 -0
- package/dist/browser/init-script.js +16 -0
- package/dist/browser/launch-firefox.d.ts +20 -0
- package/dist/browser/launch-firefox.js +66 -0
- package/dist/browser/launch-flags.d.ts +18 -0
- package/dist/browser/launch-flags.js +100 -0
- package/dist/browser/lazy.d.ts +21 -0
- package/dist/browser/lazy.js +44 -0
- package/dist/browser/page-events.d.ts +44 -0
- package/dist/browser/page-events.js +58 -0
- package/dist/browser/page-logs.js +2 -9
- package/dist/browser/page-navigation.d.ts +58 -0
- package/dist/browser/page-navigation.js +108 -0
- package/dist/browser/page-network.d.ts +69 -0
- package/dist/browser/page-network.js +163 -0
- package/dist/browser/page-settings.d.ts +35 -0
- package/dist/browser/page-settings.js +51 -0
- package/dist/browser/page-transfers.d.ts +36 -0
- package/dist/browser/page-transfers.js +50 -0
- package/dist/browser/page.d.ts +39 -154
- package/dist/browser/page.js +134 -758
- package/dist/browser/pdf.d.ts +1 -1
- package/dist/browser/session.d.ts +16 -0
- package/dist/browser/session.js +32 -0
- package/dist/browser/viewport.d.ts +19 -0
- package/dist/browser/viewport.js +72 -0
- package/dist/browser/warm-up.d.ts +1 -1
- package/dist/browser/warm-up.js +2 -1
- package/dist/browsers/index.d.ts +1 -1
- package/dist/browsers/index.js +1 -1
- package/dist/browsers/pins.d.ts +0 -2
- package/dist/browsers/pins.js +0 -2
- package/dist/browsers/store.d.ts +0 -2
- package/dist/browsers/store.js +1 -2
- package/dist/cli/args.d.ts +13 -6
- package/dist/cli/args.js +13 -10
- package/dist/cli/browsers.d.ts +1 -2
- package/dist/cli/browsers.js +2 -2
- package/dist/cli/console-reporter.js +3 -2
- package/dist/cli/flow-reporter.js +2 -1
- package/dist/cli/flow.d.ts +1 -2
- package/dist/cli/flow.js +25 -22
- package/dist/cli/init.js +2 -7
- package/dist/cli/io.d.ts +10 -0
- package/dist/cli/io.js +1 -0
- package/dist/cli/main.d.ts +3 -10
- package/dist/cli/main.js +10 -143
- package/dist/cli/paint.d.ts +2 -0
- package/dist/cli/paint.js +4 -0
- package/dist/cli/record.d.ts +4 -0
- package/dist/cli/record.js +93 -0
- package/dist/cli/usage.d.ts +1 -0
- package/dist/cli/usage.js +58 -0
- package/dist/cli/version.d.ts +2 -0
- package/dist/cli/version.js +7 -0
- package/dist/config/browsers.d.ts +11 -0
- package/dist/config/browsers.js +32 -0
- package/dist/config/config.js +4 -1
- package/dist/config/mask.d.ts +1 -1
- package/dist/config/mask.js +7 -2
- package/dist/config/run-settings.d.ts +1 -2
- package/dist/config/run-settings.js +1 -6
- package/dist/config/timeouts.d.ts +2 -0
- package/dist/config/timeouts.js +13 -0
- package/dist/config/workers.d.ts +14 -0
- package/dist/config/workers.js +29 -0
- package/dist/flows/run/branch-context.d.ts +14 -0
- package/dist/flows/run/branch-context.js +35 -0
- package/dist/flows/run/handlers/condition.d.ts +1 -1
- package/dist/flows/run/handlers/end.d.ts +1 -1
- package/dist/flows/run/handlers/group.d.ts +1 -1
- package/dist/flows/run/handlers/index.d.ts +2 -2
- package/dist/flows/run/handlers/parallel.d.ts +1 -1
- package/dist/flows/run/handlers/start.d.ts +1 -1
- package/dist/flows/run/handlers/support.d.ts +3 -27
- package/dist/flows/run/handlers/test.d.ts +1 -1
- package/dist/flows/run/handlers/types.d.ts +27 -0
- package/dist/flows/run/handlers/types.js +1 -0
- package/dist/flows/run/index.d.ts +1 -1
- package/dist/flows/run/prepare-flow.d.ts +14 -0
- package/dist/flows/run/prepare-flow.js +44 -0
- package/dist/flows/run/project.d.ts +33 -2
- package/dist/flows/run/project.js +139 -57
- package/dist/flows/run/run.d.ts +3 -4
- package/dist/flows/run/run.js +23 -193
- package/dist/flows/run/walker.d.ts +56 -0
- package/dist/flows/run/walker.js +225 -0
- package/dist/flows/steps/catalog.d.ts +1 -1
- package/dist/flows/steps/handler.d.ts +1 -1
- package/dist/flows/steps/set.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/locator/actionability.d.ts +33 -0
- package/dist/locator/actionability.js +104 -0
- package/dist/locator/chain-resolver.d.ts +20 -0
- package/dist/locator/chain-resolver.js +102 -0
- package/dist/locator/document-swap.d.ts +1 -0
- package/dist/locator/document-swap.js +11 -0
- package/dist/locator/element-state.d.ts +3 -1
- package/dist/locator/element-state.js +11 -3
- package/dist/locator/file-actions.d.ts +35 -0
- package/dist/locator/file-actions.js +55 -0
- package/dist/{script/helpers.js → locator/helper-realm.js} +2 -2
- package/dist/locator/input-actions.d.ts +18 -0
- package/dist/locator/input-actions.js +80 -0
- package/dist/locator/label-locator.d.ts +1 -1
- package/dist/locator/label-locator.js +5 -12
- package/dist/locator/locator.d.ts +14 -37
- package/dist/locator/locator.js +68 -424
- package/dist/locator/page-scripts.d.ts +16 -0
- package/dist/locator/page-scripts.js +54 -0
- package/dist/locator/page-search.d.ts +10 -0
- package/dist/locator/page-search.js +19 -0
- package/dist/locator/selector-errors.d.ts +0 -4
- package/dist/locator/selector-errors.js +0 -7
- package/dist/locator/selector.d.ts +14 -13
- package/dist/locator/selector.js +10 -16
- package/dist/locator/text-locator.d.ts +1 -1
- package/dist/locator/text-locator.js +5 -9
- package/dist/logger/index.d.ts +6 -0
- package/dist/logger/index.js +14 -7
- package/dist/network/bidi-wire.d.ts +10 -0
- package/dist/network/bidi-wire.js +38 -0
- package/dist/network/intercept.d.ts +30 -0
- package/dist/network/intercept.js +130 -0
- package/dist/network/network-tracker.d.ts +10 -47
- package/dist/network/network-tracker.js +34 -68
- package/dist/network/network-types.d.ts +33 -0
- package/dist/network/network-types.js +1 -0
- package/dist/network/network-wait.d.ts +7 -2
- package/dist/network/network-wait.js +14 -4
- package/dist/network/response.d.ts +1 -1
- package/dist/network/response.js +1 -1
- package/dist/network/route.d.ts +92 -0
- package/dist/network/route.js +142 -0
- package/dist/network/router.d.ts +6 -91
- package/dist/network/router.js +25 -258
- package/dist/recorder/expectation.d.ts +5 -0
- package/dist/recorder/expectation.js +20 -0
- package/dist/recorder/files-recorder.d.ts +30 -0
- package/dist/recorder/files-recorder.js +88 -0
- package/dist/recorder/normalise.d.ts +17 -6
- package/dist/recorder/normalise.js +96 -91
- package/dist/recorder/options.d.ts +56 -0
- package/dist/recorder/options.js +1 -0
- package/dist/recorder/page-messages.d.ts +35 -0
- package/dist/recorder/page-messages.js +26 -0
- package/dist/recorder/ranking.js +1 -0
- package/dist/recorder/realms.d.ts +6 -0
- package/dist/recorder/realms.js +13 -0
- package/dist/recorder/recorder.d.ts +10 -74
- package/dist/recorder/recorder.js +39 -199
- package/dist/recorder/session-browser.d.ts +22 -0
- package/dist/recorder/session-browser.js +21 -0
- package/dist/recorder/session-secrets.d.ts +8 -0
- package/dist/recorder/session-secrets.js +45 -0
- package/dist/recorder/session.d.ts +5 -14
- package/dist/recorder/session.js +22 -120
- package/dist/recorder/spec-source.d.ts +39 -0
- package/dist/recorder/spec-source.js +84 -0
- package/dist/recorder/toolbar/action-guard.d.ts +21 -0
- package/dist/recorder/toolbar/action-guard.js +43 -0
- package/dist/recorder/toolbar/actions/assert.d.ts +4 -0
- package/dist/recorder/toolbar/actions/assert.js +52 -0
- package/dist/recorder/toolbar/actions/insert.d.ts +6 -0
- package/dist/recorder/toolbar/actions/insert.js +93 -0
- package/dist/recorder/toolbar/actions/pick.d.ts +8 -0
- package/dist/recorder/toolbar/actions/pick.js +26 -0
- package/dist/recorder/toolbar/actions/reply.d.ts +7 -0
- package/dist/recorder/toolbar/actions/reply.js +10 -0
- package/dist/recorder/toolbar/controller-fixture.d.ts +47 -0
- package/dist/recorder/toolbar/controller-fixture.js +129 -0
- package/dist/recorder/toolbar/controller.d.ts +12 -78
- package/dist/recorder/toolbar/controller.js +35 -247
- package/dist/recorder/toolbar/inspect.d.ts +1 -3
- package/dist/recorder/toolbar/inspect.js +2 -2
- package/dist/recorder/toolbar/page-scope.d.ts +19 -0
- package/dist/recorder/toolbar/page-scope.js +43 -0
- package/dist/recorder/toolbar/protocol.d.ts +2 -1
- package/dist/recorder/toolbar/source/attach.d.ts +2 -0
- package/dist/recorder/toolbar/source/attach.js +41 -0
- package/dist/recorder/toolbar/source/dom.d.ts +2 -0
- package/dist/recorder/toolbar/source/dom.js +47 -0
- package/dist/recorder/toolbar/source/drag.d.ts +2 -0
- package/dist/recorder/toolbar/source/drag.js +32 -0
- package/dist/recorder/toolbar/source/events-page.d.ts +2 -0
- package/dist/recorder/toolbar/source/events-page.js +67 -0
- package/dist/recorder/toolbar/source/events-toolbar.d.ts +2 -0
- package/dist/recorder/toolbar/source/events-toolbar.js +35 -0
- package/dist/recorder/toolbar/source/fields.d.ts +2 -0
- package/dist/recorder/toolbar/source/fields.js +44 -0
- package/dist/recorder/toolbar/source/handlers.d.ts +2 -0
- package/dist/recorder/toolbar/source/handlers.js +134 -0
- package/dist/recorder/toolbar/source/names.d.ts +2 -0
- package/dist/recorder/toolbar/source/names.js +12 -0
- package/dist/recorder/toolbar/source/picking.d.ts +2 -0
- package/dist/recorder/toolbar/source/picking.js +72 -0
- package/dist/recorder/toolbar/source/placement.d.ts +2 -0
- package/dist/recorder/toolbar/source/placement.js +45 -0
- package/dist/recorder/toolbar/source/recorder-side.d.ts +2 -0
- package/dist/recorder/toolbar/source/recorder-side.js +54 -0
- package/dist/recorder/toolbar/source/setup.d.ts +2 -0
- package/dist/recorder/toolbar/source/setup.js +50 -0
- package/dist/recorder/toolbar/source/views-panels.d.ts +2 -0
- package/dist/recorder/toolbar/source/views-panels.js +150 -0
- package/dist/recorder/toolbar/source/views-rail.d.ts +2 -0
- package/dist/recorder/toolbar/source/views-rail.js +40 -0
- package/dist/recorder/toolbar/source/views-render.d.ts +2 -0
- package/dist/recorder/toolbar/source/views-render.js +48 -0
- package/dist/recorder/toolbar/source.js +30 -856
- package/dist/recorder/toolbar/types.d.ts +70 -0
- package/dist/recorder/toolbar/types.js +1 -0
- package/dist/recorder/toolbar-bridge.d.ts +43 -0
- package/dist/recorder/toolbar-bridge.js +106 -0
- package/dist/runner/browsers.d.ts +0 -12
- package/dist/runner/browsers.js +1 -33
- package/dist/runner/options.d.ts +31 -0
- package/dist/runner/options.js +1 -0
- package/dist/runner/pooled-run.d.ts +43 -0
- package/dist/runner/pooled-run.js +75 -0
- package/dist/runner/ports.d.ts +2 -0
- package/dist/runner/ports.js +2 -0
- package/dist/runner/registry.d.ts +1 -1
- package/dist/runner/registry.js +1 -1
- package/dist/runner/reporter.d.ts +31 -10
- package/dist/runner/reporter.js +39 -6
- package/dist/runner/run-test.d.ts +16 -0
- package/dist/runner/run-test.js +178 -0
- package/dist/runner/run.d.ts +5 -3
- package/dist/runner/run.js +28 -29
- package/dist/runner/select.d.ts +24 -0
- package/dist/runner/select.js +77 -0
- package/dist/runner/test-dir.d.ts +13 -0
- package/dist/runner/test-dir.js +27 -0
- package/dist/runner/test-runner.d.ts +25 -56
- package/dist/runner/test-runner.js +81 -270
- package/dist/runner/worker-pool.d.ts +67 -0
- package/dist/runner/worker-pool.js +275 -0
- package/dist/runner/worker-process.d.ts +6 -0
- package/dist/runner/worker-process.js +54 -0
- package/dist/runner/worker-protocol.d.ts +67 -0
- package/dist/runner/worker-protocol.js +1 -0
- package/dist/runner/worker-session.d.ts +7 -0
- package/dist/runner/worker-session.js +60 -0
- package/dist/runner/worker.d.ts +26 -0
- package/dist/runner/worker.js +121 -0
- package/dist/runner/workers-option.d.ts +15 -0
- package/dist/runner/workers-option.js +22 -0
- package/dist/script/call-function.d.ts +1 -9
- package/dist/script/call-function.js +2 -19
- package/dist/script/element-handle.d.ts +1 -2
- package/dist/script/element-handle.js +1 -2
- package/dist/script/serialize.d.ts +3 -12
- package/dist/script/serialize.js +53 -62
- package/dist/steps/parse/call.d.ts +3 -0
- package/dist/steps/parse/call.js +81 -0
- package/dist/steps/parse/expectation.d.ts +5 -0
- package/dist/steps/parse/expectation.js +71 -0
- package/dist/steps/parse/file-entry.d.ts +4 -0
- package/dist/steps/parse/file-entry.js +74 -0
- package/dist/steps/parse/literals.d.ts +4 -0
- package/dist/steps/parse/literals.js +28 -0
- package/dist/steps/parse/step.d.ts +4 -0
- package/dist/steps/parse/step.js +81 -0
- package/dist/steps/parse.js +2 -323
- package/dist/transport/bidi-connection.d.ts +4 -15
- package/dist/transport/bidi-connection.js +9 -63
- package/dist/transport/log-redaction.d.ts +13 -0
- package/dist/transport/log-redaction.js +51 -0
- package/dist/transport/subscriptions.js +18 -13
- package/dist/transport/unsupported.d.ts +7 -0
- package/dist/transport/unsupported.js +13 -0
- package/dist/types/bidi-modules/browser.d.ts +1 -12
- package/dist/types/bidi-modules/browsing-context.d.ts +8 -18
- package/dist/types/bidi-modules/emulation.d.ts +29 -21
- package/dist/types/bidi-modules/input.d.ts +1 -9
- package/dist/types/bidi-modules/network.d.ts +29 -24
- package/dist/types/bidi-modules/script.d.ts +0 -9
- package/dist/types/bidi-modules/session.d.ts +0 -8
- package/dist/types/bidi-modules/storage.d.ts +0 -5
- package/dist/types/bidi.d.ts +7 -37
- package/dist/types/config.d.ts +12 -0
- package/dist/types/test.d.ts +8 -3
- package/dist/wait/wait-until.d.ts +1 -5
- package/dist/wait/wait-until.js +7 -16
- package/package.json +3 -2
- package/dist/config/default.d.ts +0 -2
- package/dist/config/default.js +0 -3
- package/dist/types/bidi-modules/web-extensions.d.ts +0 -9
- package/dist/types/bidi-protocols/command.d.ts +0 -7
- package/dist/types/bidi-protocols/event.d.ts +0 -17
- /package/dist/{script/helpers.d.ts → locator/helper-realm.d.ts} +0 -0
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { SETUP_JS } from "./source/setup.js";
|
|
2
|
+
import { DOM_JS } from "./source/dom.js";
|
|
3
|
+
import { NAMES_JS } from "./source/names.js";
|
|
4
|
+
import { PLACEMENT_JS } from "./source/placement.js";
|
|
5
|
+
import { VIEWS_RAIL_JS } from "./source/views-rail.js";
|
|
6
|
+
import { VIEWS_PANELS_JS } from "./source/views-panels.js";
|
|
7
|
+
import { VIEWS_RENDER_JS } from "./source/views-render.js";
|
|
8
|
+
import { PICKING_JS } from "./source/picking.js";
|
|
9
|
+
import { HANDLERS_JS } from "./source/handlers.js";
|
|
10
|
+
import { FIELDS_JS } from "./source/fields.js";
|
|
11
|
+
import { DRAG_JS } from "./source/drag.js";
|
|
12
|
+
import { EVENTS_TOOLBAR_JS } from "./source/events-toolbar.js";
|
|
13
|
+
import { EVENTS_PAGE_JS } from "./source/events-page.js";
|
|
14
|
+
import { RECORDER_SIDE_JS } from "./source/recorder-side.js";
|
|
15
|
+
import { ATTACH_JS } from "./source/attach.js";
|
|
4
16
|
/**
|
|
5
17
|
* The recorder toolbar: a function declaration `(send) => { … }` installed as a preload script in the
|
|
6
18
|
* `samurai-toolbar` sandbox realm (the caller names the sandbox), top-level documents only. `send` is the
|
|
@@ -33,857 +45,19 @@ import { TOOLBAR_CSS } from "./styles.js";
|
|
|
33
45
|
* A string, so no transpiler helper can end up in it.
|
|
34
46
|
*/
|
|
35
47
|
export const TOOLBAR_SOURCE = String.raw `(send) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const MATCHERS = [
|
|
52
|
-
["toBeVisible", "Visible"], ["toHaveText", "Has text"], ["toContainText", "Contains text"],
|
|
53
|
-
["toHaveValue", "Has value"], ["toHaveAttribute", "Has attribute"], ["toHaveCount", "Count"],
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
let channel = send;
|
|
57
|
-
let state = Object.assign({}, DEFAULTS);
|
|
58
|
-
let nextId = Date.now();
|
|
59
|
-
let railBox = { x: 0, y: 0, w: 0, h: 0 };
|
|
60
|
-
let toastTimer = 0;
|
|
61
|
-
// Local UI state; everything about steps lives in the recorder
|
|
62
|
-
const ui = {
|
|
63
|
-
panel: null, picking: false, busy: false, closeOnUpdate: false, pending: 0, focus: null,
|
|
64
|
-
field: null, assert: null, insert: null, form: null,
|
|
65
|
-
wait: { kind: "idle", seconds: "2" }, shot: { name: "", fullPage: false },
|
|
66
|
-
drag: null, dropped: null, toastId: null,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const post = (message) => {
|
|
70
|
-
try { channel(Object.assign({ type: "toolbar" }, message)); } catch (err) { /* the channel is gone */ }
|
|
71
|
-
};
|
|
72
|
-
// Listeners on the page's window and document, so dispose() can take them all off again
|
|
73
|
-
const listening = [];
|
|
74
|
-
const listen = (target, type, handler, capture) => {
|
|
75
|
-
target.addEventListener(type, handler, capture === true);
|
|
76
|
-
listening.push([target, type, handler, capture === true]);
|
|
77
|
-
};
|
|
78
|
-
const paused = () => state.status === "paused";
|
|
79
|
-
// Whole pixels, so a rail at the far edge does not overhang it by a rounding error
|
|
80
|
-
const clamp = (value, max) => Math.max(0, Math.min(value, Math.floor(max)));
|
|
81
|
-
|
|
82
|
-
// ---- DOM ----
|
|
83
|
-
const h = (tag, props, ...kids) => {
|
|
84
|
-
const node = document.createElement(tag);
|
|
85
|
-
for (const key in props || {}) {
|
|
86
|
-
const value = props[key];
|
|
87
|
-
if (value === undefined || value === null || value === false) continue;
|
|
88
|
-
if (key === "class") node.className = value;
|
|
89
|
-
else if (key === "value" || key === "checked" || key === "disabled" || key === "selected") node[key] = value;
|
|
90
|
-
else node.setAttribute(key, value === true ? "" : String(value));
|
|
91
|
-
}
|
|
92
|
-
for (const kid of kids.flat(Infinity)) {
|
|
93
|
-
if (kid === null || kid === undefined || kid === false || kid === "") continue;
|
|
94
|
-
node.append(typeof kid === "string" ? document.createTextNode(kid) : kid);
|
|
95
|
-
}
|
|
96
|
-
return node;
|
|
97
|
-
};
|
|
98
|
-
const ico = (text) => h("span", { class: "ico" }, text);
|
|
99
|
-
|
|
100
|
-
// Styles go through the CSSOM (property by property, and a constructed sheet), which a page's CSP does not
|
|
101
|
-
// block, unlike a style attribute or a <style> element without 'unsafe-inline'
|
|
102
|
-
const host = document.createElement("samurai-recorder-toolbar");
|
|
103
|
-
const HOST_STYLE = [
|
|
104
|
-
["all", "initial"], ["display", "block"], ["position", "fixed"], ["top", "0"], ["left", "0"], ["width", "0"],
|
|
105
|
-
["height", "0"], ["overflow", "visible"], ["z-index", "2147483647"],
|
|
106
|
-
];
|
|
107
|
-
for (const [name, value] of HOST_STYLE) host.style.setProperty(name, value, "important");
|
|
108
|
-
const root = host.attachShadow({ mode: "closed" });
|
|
109
|
-
const frame = h("div", { class: "frame" });
|
|
110
|
-
const container = h("div", { class: "ui" });
|
|
111
|
-
const outline = h("div", { class: "outline", hidden: true });
|
|
112
|
-
const toast = h("div", { class: "toast ui", hidden: true });
|
|
113
|
-
const notices = h("div", { class: "notices ui" });
|
|
114
|
-
let adopted = false;
|
|
115
|
-
try {
|
|
116
|
-
const sheet = new CSSStyleSheet();
|
|
117
|
-
sheet.replaceSync(CSS);
|
|
118
|
-
root.adoptedStyleSheets = [sheet];
|
|
119
|
-
adopted = root.adoptedStyleSheets.length === 1;
|
|
120
|
-
} catch (err) {
|
|
121
|
-
adopted = false;
|
|
122
|
-
}
|
|
123
|
-
// Without constructed sheets: a <style> element, which a strict CSP blocks
|
|
124
|
-
root.append(...(adopted ? [] : [h("style", null, CSS)]), frame, outline, container, toast, notices);
|
|
125
|
-
|
|
126
|
-
const viewport = () => ({ w: frame.clientWidth || innerWidth, h: frame.clientHeight || innerHeight });
|
|
127
|
-
|
|
128
|
-
// ---- Names and labels ----
|
|
129
|
-
const words = (text) => String(text).replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
130
|
-
const camel = (text) => words(text).map((w, i) => (i ? w[0].toUpperCase() + w.slice(1).toLowerCase() : w.toLowerCase())).join("");
|
|
131
|
-
const snake = (text) => words(text).map((w) => w.toUpperCase()).join("_");
|
|
132
|
-
const variableName = (text) => (/^[A-Za-z_$][\w$]*$/.test(text.trim()) ? text.trim() : camel(text));
|
|
133
|
-
const fieldName = (el) => labelTexts(el)[0] || el.getAttribute("name") || el.id || "";
|
|
134
|
-
const fieldLabel = (el) => {
|
|
135
|
-
const text = labelTexts(el)[0] || el.getAttribute("placeholder") || el.getAttribute("name") || el.id || "field";
|
|
136
|
-
return text.length > 40 ? text.slice(0, 39) + "…" : text;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
// ---- Placement ----
|
|
140
|
-
const place = (node, want) => {
|
|
141
|
-
const rect = node.getBoundingClientRect();
|
|
142
|
-
const v = viewport();
|
|
143
|
-
const x = clamp(want ? want.x : v.w - rect.width - 12, v.w - rect.width);
|
|
144
|
-
const y = clamp(want ? want.y : 12, v.h - rect.height);
|
|
145
|
-
node.style.left = x + "px";
|
|
146
|
-
node.style.top = y + "px";
|
|
147
|
-
return { x, y, w: rect.width, h: rect.height };
|
|
148
|
-
};
|
|
149
|
-
const wantedSpot = () => (ui.drag ? ui.drag.pos : ui.dropped || state.position);
|
|
150
|
-
const placePanel = (panel) => {
|
|
151
|
-
const rect = panel.getBoundingClientRect();
|
|
152
|
-
const v = viewport();
|
|
153
|
-
let x;
|
|
154
|
-
let y;
|
|
155
|
-
if (ui.panel === "assert" && ui.assert) {
|
|
156
|
-
const t = ui.assert.rect;
|
|
157
|
-
// Beside the element; below it (or above) when it is too wide for either side
|
|
158
|
-
if (t.right + 8 + rect.width <= v.w) [x, y] = [t.right + 8, t.top];
|
|
159
|
-
else if (t.left - 8 - rect.width >= 0) [x, y] = [t.left - 8 - rect.width, t.top];
|
|
160
|
-
else [x, y] = [t.left, t.bottom + 8 + rect.height <= v.h ? t.bottom + 8 : t.top - 8 - rect.height];
|
|
161
|
-
} else {
|
|
162
|
-
x = railBox.x + railBox.w / 2 > v.w / 2 ? railBox.x - 8 - rect.width : railBox.x + railBox.w + 8;
|
|
163
|
-
y = railBox.y;
|
|
164
|
-
}
|
|
165
|
-
panel.style.left = clamp(x, v.w - rect.width) + "px";
|
|
166
|
-
panel.style.top = clamp(y, v.h - rect.height) + "px";
|
|
167
|
-
};
|
|
168
|
-
const placeAll = () => {
|
|
169
|
-
const [rail, panel] = container.children;
|
|
170
|
-
if (rail) railBox = place(rail, wantedSpot());
|
|
171
|
-
if (panel) placePanel(panel);
|
|
172
|
-
};
|
|
173
|
-
const showOutline = (el) => {
|
|
174
|
-
const r = el.getBoundingClientRect();
|
|
175
|
-
outline.style.left = r.left + "px";
|
|
176
|
-
outline.style.top = r.top + "px";
|
|
177
|
-
outline.style.width = r.width + "px";
|
|
178
|
-
outline.style.height = r.height + "px";
|
|
179
|
-
outline.hidden = false;
|
|
180
|
-
};
|
|
181
|
-
const hideOutline = () => { outline.hidden = true; };
|
|
182
|
-
|
|
183
|
-
// ---- Views ----
|
|
184
|
-
const toolOn = (tool) =>
|
|
185
|
-
tool === "assert" ? ui.picking || ui.panel === "assert" : tool === "insert" ? ui.panel === "insert" || ui.panel === "new" : ui.panel === tool;
|
|
186
|
-
|
|
187
|
-
const chipView = () =>
|
|
188
|
-
h("div", { class: "chip" },
|
|
189
|
-
h("span", { "data-grip": true, title: "Drag to move" }, "⋮⋮"),
|
|
190
|
-
h("span", { class: paused() ? "dot pause" : "dot" }),
|
|
191
|
-
(paused() ? "Paused" : "Rec") + " · " + state.steps + (state.steps === 1 ? " step" : " steps"),
|
|
192
|
-
h("button", { class: "chev", "data-action": "expand", title: "Expand" }, "⌄"));
|
|
193
|
-
|
|
194
|
-
const railView = () => {
|
|
195
|
-
const tool = (action, icon, label) =>
|
|
196
|
-
h("button", { class: toolOn(action) ? "b on" : "b", "data-action": action, disabled: paused() }, ico(icon), label);
|
|
197
|
-
return h("div", { class: paused() || state.resumeQuestion ? "rail wide" : "rail" },
|
|
198
|
-
h("div", { class: "top" },
|
|
199
|
-
h("span", { "data-grip": true, title: "Drag to move" }, "⋮⋮⋮"),
|
|
200
|
-
h("button", { class: "chev", "data-action": "collapse", title: "Collapse" }, "⌃")),
|
|
201
|
-
h("div", { class: "st" }, h("span", { class: paused() ? "dot pause" : "dot" }), paused() ? "Paused" : "Recording"),
|
|
202
|
-
paused() && h("div", { class: "note" }, "Nothing you do now is recorded."),
|
|
203
|
-
state.resumeQuestion && [
|
|
204
|
-
h("div", { class: "note" }, "Add a step that opens this page?"),
|
|
205
|
-
h("div", { class: "ask" },
|
|
206
|
-
h("button", { class: "yes", "data-action": "resume-yes" }, "Yes"),
|
|
207
|
-
h("button", { "data-action": "resume-no" }, "No")),
|
|
208
|
-
],
|
|
209
|
-
h("div", { class: "sep" }),
|
|
210
|
-
paused()
|
|
211
|
-
? h("button", { class: "b on", "data-action": "resume" }, ico("▶"), "Resume")
|
|
212
|
-
: h("button", { class: "b", "data-action": "pause" }, ico("❚❚"), "Pause"),
|
|
213
|
-
tool("assert", "✓", "Assert"),
|
|
214
|
-
ui.picking && h("div", { class: "note" }, "Click an element · Esc cancels"),
|
|
215
|
-
tool("insert", "{x}", "Insert"),
|
|
216
|
-
tool("wait", "⏱", "Wait"),
|
|
217
|
-
tool("screenshot", "◉", "Screenshot"),
|
|
218
|
-
h("div", { class: "sep" }),
|
|
219
|
-
h("button", { class: "b stop", "data-action": "stop" }, ico("■"), "Stop"));
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
const unavailable = (info, matcher) =>
|
|
223
|
-
(matcher === "toHaveValue" && info.value === undefined) ||
|
|
224
|
-
(matcher === "toHaveAttribute" && info.attributes.length === 0) ||
|
|
225
|
-
(matcher === "toHaveCount" && info.counts.length === 0);
|
|
226
|
-
const prefill = (a, matcher) => {
|
|
227
|
-
const info = a.info;
|
|
228
|
-
if (matcher === "toHaveAttribute") {
|
|
229
|
-
const found = info.attributes.find((at) => at.name === a.attribute) || info.attributes[0];
|
|
230
|
-
return found ? found.value : "";
|
|
231
|
-
}
|
|
232
|
-
if (matcher === "toHaveCount") {
|
|
233
|
-
const found = info.counts[a.countIndex];
|
|
234
|
-
return found ? String(found.count) : "";
|
|
235
|
-
}
|
|
236
|
-
if (matcher === info.first.matcher && info.first.expected !== undefined) return info.first.expected;
|
|
237
|
-
if (matcher === "toHaveText" || matcher === "toContainText") return info.text;
|
|
238
|
-
if (matcher === "toHaveValue") return info.value || "";
|
|
239
|
-
return "";
|
|
240
|
-
};
|
|
241
|
-
const assertValue = (a) => {
|
|
242
|
-
const info = a.info;
|
|
243
|
-
const expected = (type) => h("input", { type, class: "in", "data-field": "expected", value: a.expected, "data-enter": "assert-add", autocomplete: "off" });
|
|
244
|
-
if (a.matcher === "toHaveText" || a.matcher === "toContainText" || a.matcher === "toHaveValue") return h("div", { class: "val" }, expected("text"));
|
|
245
|
-
if (a.matcher === "toHaveAttribute") {
|
|
246
|
-
return h("div", { class: "val" },
|
|
247
|
-
h("select", { class: "in", "data-field": "attribute" }, info.attributes.map((at) => {
|
|
248
|
-
const shown = at.name + '="' + at.value + '"';
|
|
249
|
-
return h("option", { value: at.name, selected: at.name === a.attribute }, shown.length > 44 ? shown.slice(0, 43) + "…" : shown);
|
|
250
|
-
})),
|
|
251
|
-
expected("text"));
|
|
252
|
-
}
|
|
253
|
-
if (a.matcher === "toHaveCount") {
|
|
254
|
-
return h("div", { class: "val" },
|
|
255
|
-
h("select", { class: "in", "data-field": "countIndex" }, info.counts.map((c, i) =>
|
|
256
|
-
h("option", { value: String(i), selected: i === a.countIndex }, c.label + " (" + c.count + ")"))),
|
|
257
|
-
expected("number"));
|
|
258
|
-
}
|
|
259
|
-
return null;
|
|
260
|
-
};
|
|
261
|
-
const assertPanel = () => {
|
|
262
|
-
const a = ui.assert;
|
|
263
|
-
const info = a.info;
|
|
264
|
-
const head = h("div", { class: "h" }, "Assert", info && info.ok && h("code", { title: info.label }, info.label));
|
|
265
|
-
if (!info) return [head, h("div", { class: "msg" }, "…")];
|
|
266
|
-
if (!info.ok) return [head, h("div", { class: "err" }, info.error)];
|
|
267
|
-
return [
|
|
268
|
-
head,
|
|
269
|
-
h("div", { class: "opts" }, MATCHERS.map(([matcher, label]) =>
|
|
270
|
-
h("button", { class: a.matcher === matcher ? "o sel" : "o", "data-action": "matcher", "data-matcher": matcher, disabled: unavailable(info, matcher) }, label))),
|
|
271
|
-
assertValue(a),
|
|
272
|
-
a.error && h("div", { class: "err" }, a.error),
|
|
273
|
-
h("div", { class: "row" },
|
|
274
|
-
h("label", { class: "neg" }, h("input", { type: "checkbox", "data-action": "not", checked: a.not }), "Not"),
|
|
275
|
-
h("button", { class: "add", "data-action": "assert-add", disabled: ui.busy }, "Add")),
|
|
276
|
-
];
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const matches = () => {
|
|
280
|
-
const filter = ui.insert.filter.trim().toLowerCase();
|
|
281
|
-
const hit = (item) => !filter || item.name.toLowerCase().indexOf(filter) >= 0;
|
|
282
|
-
return {
|
|
283
|
-
filter,
|
|
284
|
-
variables: state.canAddVariables ? state.variables.filter(hit) : [],
|
|
285
|
-
secrets: state.secrets.filter(hit),
|
|
286
|
-
};
|
|
287
|
-
};
|
|
288
|
-
const insertPanel = () => {
|
|
289
|
-
const ins = ui.insert;
|
|
290
|
-
if (!ins.field) return [h("div", { class: "h" }, "Insert"), h("div", { class: "msg" }, "Click into a field first")];
|
|
291
|
-
const found = matches();
|
|
292
|
-
const none = found.filter && found.variables.length === 0 && found.secrets.length === 0;
|
|
293
|
-
const item = (kind, name, value) =>
|
|
294
|
-
h("button", { class: "it", "data-action": "insert-item", "data-kind": kind, "data-name": name, disabled: ui.busy }, h("code", null, name), value);
|
|
295
|
-
return [
|
|
296
|
-
h("div", { class: "h stack" }, 'Insert into "' + fieldLabel(ins.field) + '"', h("small", null, "Environment: " + state.environment)),
|
|
297
|
-
h("input", { type: "text", class: "in search", "data-field": "filter", placeholder: "Filter…", value: ins.filter, "data-enter": "insert-first", autocomplete: "off", spellcheck: "false" }),
|
|
298
|
-
state.canAddVariables && [
|
|
299
|
-
h("div", { class: "grp" }, "Variables"),
|
|
300
|
-
found.variables.length
|
|
301
|
-
? found.variables.map((v) => item("env", v.name, h("span", { class: "v", title: v.value }, v.value)))
|
|
302
|
-
: h("div", { class: "empty" }, found.filter ? "No variable matches." : "No variables yet."),
|
|
303
|
-
],
|
|
304
|
-
h("div", { class: "grp" }, "Secrets"),
|
|
305
|
-
found.secrets.length
|
|
306
|
-
? found.secrets.map((s) => item("secret", s.name, s.set ? h("span", { class: "v" }, "••••••") : h("span", { class: "warn" }, "not set")))
|
|
307
|
-
: h("div", { class: "empty" }, found.filter ? "No secret matches." : "No secrets yet."),
|
|
308
|
-
ins.error && h("div", { class: "err" }, ins.error),
|
|
309
|
-
h("button", { class: "act", "data-action": "insert-new", "data-kind": "env" }, "+ Add variable" + (none ? ' "' + variableName(ins.filter) + '"' : "")),
|
|
310
|
-
h("button", { class: "act", "data-action": "insert-new", "data-kind": "secret" }, "+ Add secret" + (none ? ' "' + snake(ins.filter) + '"' : "")),
|
|
311
|
-
];
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
const newHint = (secret, env, name) => {
|
|
315
|
-
if (!secret) return ["Saved to ", h("code", null, "environments." + env + ".variables"), " in samurai.config.ts. Other environments don't get it — add it there before running against them."];
|
|
316
|
-
if (!state.secretsIgnored) return h("span", { class: "warn" }, "Add .env." + env + " to .gitignore before committing.");
|
|
317
|
-
return ["Saved as ", h("code", null, "SAMURAI_SECRET_" + (name.trim() || "<NAME>")), " in ", h("code", null, ".env." + env), " (git-ignored)."];
|
|
318
|
-
};
|
|
319
|
-
const newPanel = () => {
|
|
320
|
-
const f = ui.form;
|
|
321
|
-
const secret = f.kind === "secret";
|
|
322
|
-
const blocked = !secret && !state.canAddVariables;
|
|
323
|
-
const env = state.environment;
|
|
324
|
-
return [
|
|
325
|
-
h("div", { class: "h stack" }, f.setValue ? "Set value" : secret ? "New secret" : "New variable", h("small", null, 'For "' + fieldLabel(ui.insert.field) + '"')),
|
|
326
|
-
h("div", { class: "seg" },
|
|
327
|
-
h("button", { class: secret ? "" : "sel", "data-action": "new-kind", "data-kind": "env" }, "Variable"),
|
|
328
|
-
h("button", { class: secret ? "sel" : "", "data-action": "new-kind", "data-kind": "secret" }, "Secret")),
|
|
329
|
-
blocked
|
|
330
|
-
? h("div", { class: "msg" }, "This recording has no environment")
|
|
331
|
-
: [
|
|
332
|
-
h("div", { class: "lbl" }, "Name"),
|
|
333
|
-
h("div", { class: "f" }, h("input", { type: "text", class: "in code", "data-field": "name", value: f.name, "data-enter": "new-save", autocomplete: "off", spellcheck: "false" })),
|
|
334
|
-
f.nameError && h("div", { class: "field-error" }, f.nameError),
|
|
335
|
-
h("div", { class: "lbl" }, "Value in " + env),
|
|
336
|
-
h("div", { class: "f" }, h("input", { type: secret ? "password" : "text", class: "in", "data-field": "value", value: f.value, "data-enter": "new-save", autocomplete: secret ? "new-password" : "off" })),
|
|
337
|
-
h("div", { class: "hint" }, newHint(secret, env, f.name)),
|
|
338
|
-
f.error && h("div", { class: "err" }, f.error),
|
|
339
|
-
],
|
|
340
|
-
h("div", { class: "foot plain" },
|
|
341
|
-
h("button", { class: "cancel", "data-action": "new-cancel" }, "Cancel"),
|
|
342
|
-
!blocked && h("button", { class: "add", "data-action": "new-save", disabled: ui.busy || !f.name.trim() }, "Save and insert")),
|
|
343
|
-
];
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
const secondsValid = () => {
|
|
347
|
-
const seconds = Number(ui.wait.seconds);
|
|
348
|
-
return ui.wait.seconds.trim() !== "" && isFinite(seconds) && seconds >= 0.1 && seconds <= 60;
|
|
349
|
-
};
|
|
350
|
-
const radio = (field, value, checked, ...label) =>
|
|
351
|
-
h("label", { class: "radio" }, h("input", { type: "radio", name: field, "data-field": field, value, checked }), label);
|
|
352
|
-
const waitPanel = () => [
|
|
353
|
-
h("div", { class: "h" }, "Wait"),
|
|
354
|
-
radio("waitKind", "idle", ui.wait.kind === "idle", "Until the page settles"),
|
|
355
|
-
h("div", { class: "sub" }, "No requests for 500 ms"),
|
|
356
|
-
radio("waitKind", "time", ui.wait.kind === "time", "For ",
|
|
357
|
-
h("input", { type: "number", class: "num", "data-field": "seconds", min: "0.1", max: "60", step: "0.1", value: ui.wait.seconds, "data-enter": "wait-add" }),
|
|
358
|
-
" seconds"),
|
|
359
|
-
h("div", { class: "foot" }, h("button", { class: "add", "data-action": "wait-add", disabled: ui.busy || (ui.wait.kind === "time" && !secondsValid()) }, "Add")),
|
|
360
|
-
];
|
|
361
|
-
const screenshotPanel = () => [
|
|
362
|
-
h("div", { class: "h" }, "Screenshot"),
|
|
363
|
-
h("div", { class: "grp" }, "Name"),
|
|
364
|
-
h("div", { class: "f" }, h("input", { type: "text", class: "in", "data-field": "shotName", value: ui.shot.name, "data-enter": "screenshot-add", autocomplete: "off", spellcheck: "false" })),
|
|
365
|
-
radio("fullPage", "false", !ui.shot.fullPage, "Visible part"),
|
|
366
|
-
radio("fullPage", "true", ui.shot.fullPage, "Whole page"),
|
|
367
|
-
h("div", { class: "foot" }, h("button", { class: "add", "data-action": "screenshot-add", disabled: ui.busy }, "Add")),
|
|
368
|
-
];
|
|
369
|
-
const PANELS = { assert: assertPanel, insert: insertPanel, new: newPanel, wait: waitPanel, screenshot: screenshotPanel };
|
|
370
|
-
|
|
371
|
-
// Notice nodes live as long as their warning: rebuilding them would make screen readers announce each alert again
|
|
372
|
-
const noticeNodes = new Map();
|
|
373
|
-
let savingNode = null;
|
|
374
|
-
let savingText = null;
|
|
375
|
-
const renderNotices = () => {
|
|
376
|
-
const ids = new Set(state.warnings.map((w) => w.id));
|
|
377
|
-
for (const [id, node] of noticeNodes) {
|
|
378
|
-
if (ids.has(id)) continue;
|
|
379
|
-
node.remove();
|
|
380
|
-
noticeNodes.delete(id);
|
|
381
|
-
}
|
|
382
|
-
for (const w of state.warnings) {
|
|
383
|
-
if (noticeNodes.has(w.id)) continue;
|
|
384
|
-
const node = h("div", { class: "notice warning", "data-warning": String(w.id), role: "alert" },
|
|
385
|
-
h("span", null, w.text),
|
|
386
|
-
h("button", { class: "dismiss", "data-action": "dismiss", "data-id": String(w.id) }, "Dismiss"));
|
|
387
|
-
noticeNodes.set(w.id, node);
|
|
388
|
-
// Before the saving line, which stays last
|
|
389
|
-
notices.insertBefore(node, savingNode);
|
|
390
|
-
}
|
|
391
|
-
const saving = state.saving || null;
|
|
392
|
-
if (saving !== savingText) {
|
|
393
|
-
savingText = saving;
|
|
394
|
-
if (savingNode) savingNode.remove();
|
|
395
|
-
savingNode = saving ? h("div", { class: "notice saving" }, "Saving " + saving + "…") : null;
|
|
396
|
-
if (savingNode) notices.append(savingNode);
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
const render = () => {
|
|
400
|
-
const active = root.activeElement;
|
|
401
|
-
const kept = active && active.type !== "radio" ? active.getAttribute("data-field") : null;
|
|
402
|
-
let selection = null;
|
|
403
|
-
try { selection = kept ? [active.selectionStart, active.selectionEnd] : null; } catch (err) { selection = null; }
|
|
404
|
-
const panel = ui.panel && !state.collapsed ? h("div", { class: ui.panel === "new" || ui.panel === "insert" ? "panel wide" : "panel" }, PANELS[ui.panel]()) : null;
|
|
405
|
-
container.replaceChildren(state.collapsed ? chipView() : railView(), ...(panel ? [panel] : []));
|
|
406
|
-
renderNotices();
|
|
407
|
-
placeAll();
|
|
408
|
-
const focus = ui.focus || kept;
|
|
409
|
-
ui.focus = null;
|
|
410
|
-
const target = focus && root.querySelector('[data-field="' + focus + '"]');
|
|
411
|
-
if (!target) return;
|
|
412
|
-
target.focus({ preventScroll: true });
|
|
413
|
-
if (focus === kept && selection && selection[0] !== null) {
|
|
414
|
-
try { target.setSelectionRange(selection[0], selection[1]); } catch (err) { /* not a text input */ }
|
|
415
|
-
}
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
// ---- Panels and picking ----
|
|
419
|
-
const closePanel = () => {
|
|
420
|
-
ui.panel = null;
|
|
421
|
-
ui.assert = null;
|
|
422
|
-
ui.busy = false;
|
|
423
|
-
ui.closeOnUpdate = false;
|
|
424
|
-
ui.pending = 0;
|
|
425
|
-
hideOutline();
|
|
426
|
-
};
|
|
427
|
-
const stopPicking = () => {
|
|
428
|
-
ui.picking = false;
|
|
429
|
-
if (ui.panel !== "assert") hideOutline();
|
|
430
|
-
};
|
|
431
|
-
const usable = () => !paused() && !state.collapsed;
|
|
432
|
-
const openTool = (tool) => {
|
|
433
|
-
if (!usable()) return;
|
|
434
|
-
const wasOpen = toolOn(tool);
|
|
435
|
-
stopPicking();
|
|
436
|
-
closePanel();
|
|
437
|
-
if (!wasOpen) {
|
|
438
|
-
ui.panel = tool;
|
|
439
|
-
if (tool === "insert") {
|
|
440
|
-
// The field focused now, else the last one (a toolbar button may have taken the focus)
|
|
441
|
-
const active = document.activeElement;
|
|
442
|
-
const focused = active && active !== host && active.nodeType === 1 && isTextEntry(active) ? active : null;
|
|
443
|
-
const field = focused || (ui.field && ui.field.isConnected ? ui.field : null);
|
|
444
|
-
ui.field = field;
|
|
445
|
-
ui.insert = { field, filter: "", error: null };
|
|
446
|
-
if (field) ui.focus = "filter";
|
|
447
|
-
} else if (tool === "wait") {
|
|
448
|
-
ui.wait = { kind: "idle", seconds: "2" };
|
|
449
|
-
} else if (tool === "screenshot") {
|
|
450
|
-
ui.shot = { name: state.nextScreenshot, fullPage: false };
|
|
451
|
-
ui.focus = "shotName";
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
render();
|
|
455
|
-
};
|
|
456
|
-
const pick = (target) => {
|
|
457
|
-
if (!ui.picking || !target || target.nodeType !== 1) return;
|
|
458
|
-
ui.picking = false;
|
|
459
|
-
closePanel();
|
|
460
|
-
const id = nextId++;
|
|
461
|
-
const candidates = candidatesOf(target);
|
|
462
|
-
const r = target.getBoundingClientRect();
|
|
463
|
-
ui.assert = {
|
|
464
|
-
id, target, candidates, info: null, matcher: "toBeVisible", expected: "", attribute: null, countIndex: 0, not: false, error: null,
|
|
465
|
-
rect: { left: r.left, top: r.top, right: r.right, bottom: r.bottom },
|
|
466
|
-
};
|
|
467
|
-
ui.panel = "assert";
|
|
468
|
-
ui.pending = id;
|
|
469
|
-
showOutline(target);
|
|
470
|
-
post({ action: "pick", id, target, candidates });
|
|
471
|
-
render();
|
|
472
|
-
};
|
|
473
|
-
const openForm = (kind, name, setValue) => {
|
|
474
|
-
ui.form = { kind, name, value: "", nameError: null, error: null, setValue };
|
|
475
|
-
ui.panel = "new";
|
|
476
|
-
ui.focus = name ? "value" : "name";
|
|
477
|
-
render();
|
|
478
|
-
};
|
|
479
|
-
const sendInsert = (kind, name) => {
|
|
480
|
-
const field = ui.insert.field;
|
|
481
|
-
const id = nextId++;
|
|
482
|
-
ui.pending = id;
|
|
483
|
-
ui.busy = true;
|
|
484
|
-
ui.insert.error = null;
|
|
485
|
-
post({ action: "insert", id, target: field, candidates: candidatesOf(field), kind, name });
|
|
486
|
-
render();
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
// Every action is what a click on its control does; press() runs the same functions
|
|
490
|
-
const handlers = {
|
|
491
|
-
dismiss: (d) => post({ action: "dismiss", id: Number(d.id) }),
|
|
492
|
-
pause: () => { if (!paused()) post({ action: "pause" }); },
|
|
493
|
-
resume: () => { if (paused()) post({ action: "resume" }); },
|
|
494
|
-
stop: () => {
|
|
495
|
-
stopPicking();
|
|
496
|
-
closePanel();
|
|
497
|
-
post({ action: "stop" });
|
|
498
|
-
render();
|
|
499
|
-
},
|
|
500
|
-
collapse: () => {
|
|
501
|
-
stopPicking();
|
|
502
|
-
closePanel();
|
|
503
|
-
post({ action: "collapse", collapsed: true });
|
|
504
|
-
render();
|
|
505
|
-
},
|
|
506
|
-
expand: () => post({ action: "collapse", collapsed: false }),
|
|
507
|
-
"resume-yes": () => { if (state.resumeQuestion) post({ action: "resume-goto", add: true }); },
|
|
508
|
-
"resume-no": () => { if (state.resumeQuestion) post({ action: "resume-goto", add: false }); },
|
|
509
|
-
assert: () => {
|
|
510
|
-
if (!usable()) return;
|
|
511
|
-
const wasOn = toolOn("assert");
|
|
512
|
-
stopPicking();
|
|
513
|
-
closePanel();
|
|
514
|
-
ui.picking = !wasOn;
|
|
515
|
-
render();
|
|
516
|
-
},
|
|
517
|
-
insert: () => openTool("insert"),
|
|
518
|
-
wait: () => openTool("wait"),
|
|
519
|
-
screenshot: () => openTool("screenshot"),
|
|
520
|
-
pick: (detail) => pick(detail.target),
|
|
521
|
-
matcher: (detail) => {
|
|
522
|
-
const a = ui.panel === "assert" && ui.assert;
|
|
523
|
-
if (!a || !a.info || !a.info.ok || unavailable(a.info, detail.matcher)) return;
|
|
524
|
-
a.matcher = detail.matcher;
|
|
525
|
-
a.expected = prefill(a, detail.matcher);
|
|
526
|
-
render();
|
|
527
|
-
},
|
|
528
|
-
not: () => {
|
|
529
|
-
if (ui.panel !== "assert" || !ui.assert || !ui.assert.info) return;
|
|
530
|
-
ui.assert.not = !ui.assert.not;
|
|
531
|
-
render();
|
|
532
|
-
},
|
|
533
|
-
"assert-add": () => {
|
|
534
|
-
const a = ui.panel === "assert" && ui.assert;
|
|
535
|
-
if (!a || ui.busy || !a.info || !a.info.ok) return;
|
|
536
|
-
const message = { action: "assert", id: a.id, target: a.target, candidates: a.candidates, matcher: a.matcher, not: a.not };
|
|
537
|
-
if (a.matcher !== "toBeVisible") message.expected = a.expected;
|
|
538
|
-
if (a.matcher === "toHaveAttribute") message.attribute = a.attribute;
|
|
539
|
-
if (a.matcher === "toHaveCount") message.countIndex = a.countIndex;
|
|
540
|
-
ui.busy = true;
|
|
541
|
-
a.error = null;
|
|
542
|
-
post(message);
|
|
543
|
-
render();
|
|
544
|
-
},
|
|
545
|
-
"insert-item": (detail) => {
|
|
546
|
-
if (ui.panel !== "insert" || ui.busy || !ui.insert.field) return;
|
|
547
|
-
if (detail.kind === "secret") {
|
|
548
|
-
const secret = state.secrets.find((s) => s.name === detail.name);
|
|
549
|
-
if (!secret) return;
|
|
550
|
-
if (!secret.set) return openForm("secret", secret.name, true);
|
|
551
|
-
} else if (!state.canAddVariables || !state.variables.some((v) => v.name === detail.name)) {
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
sendInsert(detail.kind, detail.name);
|
|
555
|
-
},
|
|
556
|
-
"insert-first": () => {
|
|
557
|
-
if (ui.panel !== "insert" || !ui.insert.field) return;
|
|
558
|
-
const found = matches();
|
|
559
|
-
const first = found.variables[0] ? { kind: "env", name: found.variables[0].name } : found.secrets[0] && { kind: "secret", name: found.secrets[0].name };
|
|
560
|
-
if (first) handlers["insert-item"](first);
|
|
561
|
-
},
|
|
562
|
-
"insert-new": (detail) => {
|
|
563
|
-
if (ui.panel !== "insert" || !ui.insert.field) return;
|
|
564
|
-
const filter = ui.insert.filter.trim();
|
|
565
|
-
const base = filter || fieldName(ui.insert.field);
|
|
566
|
-
const name = detail.kind === "secret" ? snake(base) : filter ? variableName(filter) : camel(base);
|
|
567
|
-
openForm(detail.kind === "secret" ? "secret" : "env", name, false);
|
|
568
|
-
},
|
|
569
|
-
"new-kind": (detail) => {
|
|
570
|
-
const f = ui.panel === "new" && ui.form;
|
|
571
|
-
if (!f || ui.busy || f.kind === detail.kind) return;
|
|
572
|
-
f.kind = detail.kind === "secret" ? "secret" : "env";
|
|
573
|
-
f.name = f.kind === "secret" ? snake(f.name) : camel(f.name);
|
|
574
|
-
f.nameError = null;
|
|
575
|
-
f.error = null;
|
|
576
|
-
render();
|
|
577
|
-
},
|
|
578
|
-
"new-save": () => {
|
|
579
|
-
const f = ui.panel === "new" && ui.form;
|
|
580
|
-
if (!f || ui.busy || !f.name.trim() || (f.kind === "env" && !state.canAddVariables)) return;
|
|
581
|
-
const field = ui.insert.field;
|
|
582
|
-
const id = nextId++;
|
|
583
|
-
ui.pending = id;
|
|
584
|
-
ui.busy = true;
|
|
585
|
-
f.nameError = null;
|
|
586
|
-
f.error = null;
|
|
587
|
-
post({ action: "add-insertable", id, target: field, candidates: candidatesOf(field), kind: f.kind, name: f.name.trim(), value: f.value });
|
|
588
|
-
render();
|
|
589
|
-
},
|
|
590
|
-
"new-cancel": () => {
|
|
591
|
-
if (ui.panel !== "new") return;
|
|
592
|
-
ui.panel = "insert";
|
|
593
|
-
ui.form = null;
|
|
594
|
-
ui.busy = false;
|
|
595
|
-
ui.pending = 0;
|
|
596
|
-
ui.focus = "filter";
|
|
597
|
-
render();
|
|
598
|
-
},
|
|
599
|
-
"wait-add": () => {
|
|
600
|
-
if (ui.panel !== "wait" || ui.busy) return;
|
|
601
|
-
if (ui.wait.kind === "time" && !secondsValid()) return;
|
|
602
|
-
ui.busy = true;
|
|
603
|
-
ui.closeOnUpdate = true;
|
|
604
|
-
post(ui.wait.kind === "time" ? { action: "wait", kind: "time", ms: Math.round(Number(ui.wait.seconds) * 1000) } : { action: "wait", kind: "idle" });
|
|
605
|
-
render();
|
|
606
|
-
},
|
|
607
|
-
"screenshot-add": () => {
|
|
608
|
-
if (ui.panel !== "screenshot" || ui.busy) return;
|
|
609
|
-
ui.busy = true;
|
|
610
|
-
ui.closeOnUpdate = true;
|
|
611
|
-
post({ action: "screenshot", name: ui.shot.name.trim(), fullPage: ui.shot.fullPage });
|
|
612
|
-
render();
|
|
613
|
-
},
|
|
614
|
-
escape: () => {
|
|
615
|
-
if (!ui.picking && !ui.panel) return false;
|
|
616
|
-
stopPicking();
|
|
617
|
-
closePanel();
|
|
618
|
-
render();
|
|
619
|
-
return true;
|
|
620
|
-
},
|
|
621
|
-
};
|
|
622
|
-
const press = (action, detail) => {
|
|
623
|
-
const handler = handlers[action];
|
|
624
|
-
if (handler) handler(detail || {});
|
|
625
|
-
};
|
|
626
|
-
|
|
627
|
-
const onField = (event, target) => {
|
|
628
|
-
const t = target || event.target;
|
|
629
|
-
const field = t && t.getAttribute && t.getAttribute("data-field");
|
|
630
|
-
if (!field) return;
|
|
631
|
-
const value = t.value;
|
|
632
|
-
if (field === "expected" && ui.assert) ui.assert.expected = value;
|
|
633
|
-
else if (field === "attribute" && ui.assert) {
|
|
634
|
-
ui.assert.attribute = value;
|
|
635
|
-
ui.assert.expected = prefill(ui.assert, "toHaveAttribute");
|
|
636
|
-
render();
|
|
637
|
-
} else if (field === "countIndex" && ui.assert) {
|
|
638
|
-
ui.assert.countIndex = Number(value);
|
|
639
|
-
ui.assert.expected = prefill(ui.assert, "toHaveCount");
|
|
640
|
-
render();
|
|
641
|
-
} else if (field === "filter" && ui.insert) {
|
|
642
|
-
if (ui.insert.filter === value) return;
|
|
643
|
-
ui.insert.filter = value;
|
|
644
|
-
render();
|
|
645
|
-
} else if (field === "name" && ui.form) {
|
|
646
|
-
if (ui.form.name === value) return;
|
|
647
|
-
ui.form.name = value;
|
|
648
|
-
ui.form.nameError = null;
|
|
649
|
-
render();
|
|
650
|
-
} else if (field === "value" && ui.form) ui.form.value = value;
|
|
651
|
-
else if (field === "waitKind") {
|
|
652
|
-
ui.wait.kind = value === "time" ? "time" : "idle";
|
|
653
|
-
render();
|
|
654
|
-
} else if (field === "seconds") {
|
|
655
|
-
ui.wait.seconds = value;
|
|
656
|
-
ui.wait.kind = "time";
|
|
657
|
-
render();
|
|
658
|
-
} else if (field === "shotName") ui.shot.name = value;
|
|
659
|
-
else if (field === "fullPage") {
|
|
660
|
-
ui.shot.fullPage = value === "true";
|
|
661
|
-
render();
|
|
662
|
-
}
|
|
663
|
-
};
|
|
664
|
-
|
|
665
|
-
// ---- Drag ----
|
|
666
|
-
const onDragMove = (event) => {
|
|
667
|
-
if (!ui.drag) return;
|
|
668
|
-
ui.drag.pos = { x: event.clientX - ui.drag.dx, y: event.clientY - ui.drag.dy };
|
|
669
|
-
ui.drag.moved = true;
|
|
670
|
-
placeAll();
|
|
671
|
-
};
|
|
672
|
-
const endDrag = () => {
|
|
673
|
-
if (!ui.drag) return;
|
|
674
|
-
const { moved, pointer } = ui.drag;
|
|
675
|
-
ui.drag = null;
|
|
676
|
-
window.removeEventListener("pointermove", onDragMove, true);
|
|
677
|
-
window.removeEventListener("pointerup", endDrag, true);
|
|
678
|
-
window.removeEventListener("pointercancel", endDrag, true);
|
|
679
|
-
try { host.releasePointerCapture(pointer); } catch (err) { /* already released */ }
|
|
680
|
-
if (!moved) return;
|
|
681
|
-
ui.dropped = { x: Math.round(railBox.x), y: Math.round(railBox.y) };
|
|
682
|
-
post({ action: "move", x: ui.dropped.x, y: ui.dropped.y });
|
|
683
|
-
placeAll();
|
|
684
|
-
};
|
|
685
|
-
const startDrag = (event) => {
|
|
686
|
-
if (event.button !== 0) return;
|
|
687
|
-
event.preventDefault();
|
|
688
|
-
ui.drag = { dx: event.clientX - railBox.x, dy: event.clientY - railBox.y, pos: { x: railBox.x, y: railBox.y }, moved: false, pointer: event.pointerId };
|
|
689
|
-
// Captured on the host, which re-renders never replace
|
|
690
|
-
try { host.setPointerCapture(event.pointerId); } catch (err) { /* moves still arrive while over the window */ }
|
|
691
|
-
window.addEventListener("pointermove", onDragMove, true);
|
|
692
|
-
window.addEventListener("pointerup", endDrag, true);
|
|
693
|
-
window.addEventListener("pointercancel", endDrag, true);
|
|
694
|
-
};
|
|
695
|
-
|
|
696
|
-
// ---- Events inside the toolbar; none of them reach the page's bubbling listeners ----
|
|
697
|
-
root.addEventListener("click", (event) => {
|
|
698
|
-
event.stopPropagation();
|
|
699
|
-
const control = event.target.closest && event.target.closest("[data-action]");
|
|
700
|
-
if (!control || control.disabled) return;
|
|
701
|
-
press(control.getAttribute("data-action"), {
|
|
702
|
-
kind: control.getAttribute("data-kind"),
|
|
703
|
-
name: control.getAttribute("data-name"),
|
|
704
|
-
matcher: control.getAttribute("data-matcher"),
|
|
705
|
-
id: control.getAttribute("data-id"),
|
|
706
|
-
});
|
|
707
|
-
});
|
|
708
|
-
root.addEventListener("pointerdown", (event) => {
|
|
709
|
-
event.stopPropagation();
|
|
710
|
-
if (event.target.closest && event.target.closest("[data-grip]")) startDrag(event);
|
|
711
|
-
});
|
|
712
|
-
const onKeydown = (event, target) => {
|
|
713
|
-
const enter = event.key === "Enter" && target && target.getAttribute && target.getAttribute("data-enter");
|
|
714
|
-
if (!enter || event.isComposing) return;
|
|
715
|
-
event.preventDefault();
|
|
716
|
-
if (target.getAttribute("data-field")) onField(event, target);
|
|
717
|
-
press(enter);
|
|
718
|
-
};
|
|
719
|
-
// Typing reaches these only when it does not cross the shadow boundary (the window listeners below take the rest)
|
|
720
|
-
root.addEventListener("input", (event) => { event.stopPropagation(); onField(event); });
|
|
721
|
-
root.addEventListener("change", (event) => { event.stopPropagation(); onField(event); });
|
|
722
|
-
root.addEventListener("keydown", (event) => {
|
|
723
|
-
event.stopPropagation();
|
|
724
|
-
onKeydown(event, event.target);
|
|
725
|
-
});
|
|
726
|
-
for (const type of ["keyup", "keypress", "mousedown", "mouseup", "pointerup", "dblclick", "focusin", "focusout"]) {
|
|
727
|
-
root.addEventListener(type, (event) => event.stopPropagation());
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// ---- Events on the page (window capture runs before the page's and the capture script's listeners) ----
|
|
731
|
-
const swallow = (event) => {
|
|
732
|
-
event.preventDefault();
|
|
733
|
-
event.stopImmediatePropagation();
|
|
734
|
-
};
|
|
735
|
-
// Typing in the toolbar stays in the toolbar: these run first on the window (registered at document start,
|
|
736
|
-
// before any page script), so no page listener sees a key, typed text or the clipboard of a toolbar input
|
|
737
|
-
// (the New secret value among them). The browser's default action, the text going into the input, still
|
|
738
|
-
// happens. Outside the shadow root the event's target is the host, so the input is the focused element.
|
|
739
|
-
const TYPING = ["keydown", "keyup", "keypress", "beforeinput", "input", "compositionstart", "compositionupdate", "compositionend", "paste", "cut", "copy"];
|
|
740
|
-
for (const type of TYPING) {
|
|
741
|
-
listen(window, type, (event) => {
|
|
742
|
-
// An untrusted event was dispatched by the page itself: it neither carries the tester's typing nor acts
|
|
743
|
-
if (!event.isTrusted || !inToolbar(event)) return;
|
|
744
|
-
event.stopImmediatePropagation();
|
|
745
|
-
const target = root.activeElement;
|
|
746
|
-
if (type === "keydown") {
|
|
747
|
-
if (event.key === "Escape" && (ui.picking || ui.panel)) {
|
|
748
|
-
event.preventDefault();
|
|
749
|
-
handlers.escape();
|
|
750
|
-
} else onKeydown(event, target);
|
|
751
|
-
} else if (type === "input" && target && target.type !== "radio" && target.type !== "checkbox") {
|
|
752
|
-
// A radio or checkbox the pointer changed need not have the focus; its change event (below) updates it
|
|
753
|
-
onField(event, target);
|
|
754
|
-
}
|
|
755
|
-
}, true);
|
|
756
|
-
}
|
|
757
|
-
listen(window, "keydown", (event) => {
|
|
758
|
-
if (event.isTrusted && event.key === "Escape" && (ui.picking || ui.panel)) {
|
|
759
|
-
swallow(event);
|
|
760
|
-
handlers.escape();
|
|
761
|
-
}
|
|
762
|
-
}, true);
|
|
763
|
-
listen(window, "pointerover", (event) => {
|
|
764
|
-
if (!ui.picking) return;
|
|
765
|
-
if (inToolbar(event) || !event.target || event.target.nodeType !== 1) hideOutline();
|
|
766
|
-
else showOutline(event.target);
|
|
767
|
-
}, true);
|
|
768
|
-
for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "dblclick", "auxclick", "contextmenu"]) {
|
|
769
|
-
listen(window, type, (event) => {
|
|
770
|
-
if (inToolbar(event)) return;
|
|
771
|
-
if (ui.picking) return swallow(event);
|
|
772
|
-
if (type === "pointerdown" && ui.panel) {
|
|
773
|
-
closePanel();
|
|
774
|
-
render();
|
|
775
|
-
}
|
|
776
|
-
}, true);
|
|
777
|
-
}
|
|
778
|
-
listen(window, "click", (event) => {
|
|
779
|
-
if (!ui.picking || inToolbar(event)) return;
|
|
780
|
-
swallow(event);
|
|
781
|
-
pick(event.target);
|
|
782
|
-
}, true);
|
|
783
|
-
listen(document, "focusin", (event) => {
|
|
784
|
-
if (inToolbar(event) || !event.target || event.target.nodeType !== 1) return;
|
|
785
|
-
ui.field = isTextEntry(event.target) ? event.target : null;
|
|
786
|
-
if (ui.panel === "insert" && !ui.insert.field && ui.field) {
|
|
787
|
-
ui.insert.field = ui.field;
|
|
788
|
-
render();
|
|
789
|
-
}
|
|
790
|
-
}, true);
|
|
791
|
-
listen(window, "resize", () => render());
|
|
792
|
-
listen(window, "scroll", () => {
|
|
793
|
-
if (ui.panel === "assert" && ui.assert && !outline.hidden) showOutline(ui.assert.target);
|
|
794
|
-
}, true);
|
|
795
|
-
|
|
796
|
-
// ---- The recorder's side ----
|
|
797
|
-
const showToast = (text) => {
|
|
798
|
-
toast.textContent = text;
|
|
799
|
-
toast.hidden = false;
|
|
800
|
-
toast.classList.remove("gone");
|
|
801
|
-
clearTimeout(toastTimer);
|
|
802
|
-
toastTimer = setTimeout(() => {
|
|
803
|
-
toast.classList.add("gone");
|
|
804
|
-
toastTimer = setTimeout(() => { toast.hidden = true; }, 300);
|
|
805
|
-
}, 2500);
|
|
806
|
-
};
|
|
807
|
-
const update = (next) => {
|
|
808
|
-
state = Object.assign({}, DEFAULTS, next);
|
|
809
|
-
const dropped = ui.dropped;
|
|
810
|
-
if (dropped && state.position && Math.round(state.position.x) === dropped.x && Math.round(state.position.y) === dropped.y) ui.dropped = null;
|
|
811
|
-
if (ui.closeOnUpdate) closePanel();
|
|
812
|
-
if (!usable()) {
|
|
813
|
-
stopPicking();
|
|
814
|
-
closePanel();
|
|
815
|
-
}
|
|
816
|
-
if (state.toast && state.toast.id !== ui.toastId) {
|
|
817
|
-
ui.toastId = state.toast.id;
|
|
818
|
-
showToast(state.toast.text);
|
|
819
|
-
}
|
|
820
|
-
attach();
|
|
821
|
-
render();
|
|
822
|
-
};
|
|
823
|
-
const reply = (id, result) => {
|
|
824
|
-
if (!ui.pending || id !== ui.pending || !result) return;
|
|
825
|
-
ui.busy = false;
|
|
826
|
-
const ok = result.ok === true;
|
|
827
|
-
if (ui.panel === "assert" && ui.assert && !ui.assert.info) {
|
|
828
|
-
const a = ui.assert;
|
|
829
|
-
a.info = result;
|
|
830
|
-
if (ok) {
|
|
831
|
-
a.matcher = result.first.matcher;
|
|
832
|
-
a.attribute = result.attributes[0] ? result.attributes[0].name : null;
|
|
833
|
-
a.countIndex = 0;
|
|
834
|
-
a.expected = prefill(a, a.matcher);
|
|
835
|
-
}
|
|
836
|
-
} else if (ok) {
|
|
837
|
-
closePanel();
|
|
838
|
-
} else if (ui.panel === "assert" && ui.assert) {
|
|
839
|
-
ui.assert.error = result.error;
|
|
840
|
-
} else if (ui.panel === "insert") {
|
|
841
|
-
ui.insert.error = result.error;
|
|
842
|
-
} else if (ui.panel === "new") {
|
|
843
|
-
if (result.field === "name") ui.form.nameError = result.error;
|
|
844
|
-
else ui.form.error = result.error;
|
|
845
|
-
}
|
|
846
|
-
render();
|
|
847
|
-
};
|
|
848
|
-
|
|
849
|
-
// ---- Attach ----
|
|
850
|
-
let announced = false;
|
|
851
|
-
const attach = () => {
|
|
852
|
-
const html = document.documentElement;
|
|
853
|
-
if (!html) return false;
|
|
854
|
-
if (host.parentNode !== html) html.appendChild(host);
|
|
855
|
-
if (!announced) {
|
|
856
|
-
announced = true;
|
|
857
|
-
render();
|
|
858
|
-
post({ action: "ready", url: location.href });
|
|
859
|
-
}
|
|
860
|
-
return true;
|
|
861
|
-
};
|
|
862
|
-
const rebind = (fresh) => {
|
|
863
|
-
channel = fresh;
|
|
864
|
-
announced = false;
|
|
865
|
-
attach();
|
|
866
|
-
};
|
|
867
|
-
let watcher = null;
|
|
868
|
-
// The recording ended while this document stays open: take every listener and the host away
|
|
869
|
-
const dispose = () => {
|
|
870
|
-
endDrag();
|
|
871
|
-
for (const [target, type, handler, capture] of listening.splice(0)) target.removeEventListener(type, handler, capture);
|
|
872
|
-
if (watcher) watcher.disconnect();
|
|
873
|
-
clearTimeout(toastTimer);
|
|
874
|
-
channel = () => {};
|
|
875
|
-
host.remove();
|
|
876
|
-
if (globalThis[KEY] && globalThis[KEY].root === root) delete globalThis[KEY];
|
|
877
|
-
};
|
|
878
|
-
globalThis[KEY] = { root, update, reply, press, rebind, dispose, state: () => state, picking: () => ui.picking };
|
|
879
|
-
|
|
880
|
-
const focused = document.activeElement;
|
|
881
|
-
if (focused && focused.nodeType === 1 && isTextEntry(focused)) ui.field = focused;
|
|
882
|
-
if (!attach()) {
|
|
883
|
-
watcher = new MutationObserver(() => { if (attach()) watcher.disconnect(); });
|
|
884
|
-
watcher.observe(document, { childList: true });
|
|
885
|
-
}
|
|
886
|
-
// The page may replace the root element while it loads
|
|
887
|
-
listen(document, "DOMContentLoaded", attach);
|
|
888
|
-
listen(window, "load", attach);
|
|
48
|
+
${SETUP_JS}
|
|
49
|
+
${DOM_JS}
|
|
50
|
+
${NAMES_JS}
|
|
51
|
+
${PLACEMENT_JS}
|
|
52
|
+
${VIEWS_RAIL_JS}
|
|
53
|
+
${VIEWS_PANELS_JS}
|
|
54
|
+
${VIEWS_RENDER_JS}
|
|
55
|
+
${PICKING_JS}
|
|
56
|
+
${HANDLERS_JS}
|
|
57
|
+
${FIELDS_JS}
|
|
58
|
+
${DRAG_JS}
|
|
59
|
+
${EVENTS_TOOLBAR_JS}
|
|
60
|
+
${EVENTS_PAGE_JS}
|
|
61
|
+
${RECORDER_SIDE_JS}
|
|
62
|
+
${ATTACH_JS}
|
|
889
63
|
}`;
|