@reticlehq/browser 2.13.1 → 3.1.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/dist/actions/actions-dom.d.ts +8 -20
- package/dist/actions/actions-dom.js +70 -69
- package/dist/actions/actions-press.d.ts +14 -0
- package/dist/actions/actions-press.js +123 -0
- package/dist/actions/actions.d.ts +1 -89
- package/dist/actions/actions.js +64 -365
- package/dist/actions/appeared-text.d.ts +0 -20
- package/dist/actions/appeared-text.js +6 -36
- package/dist/actions/danger-context.d.ts +3 -0
- package/dist/actions/danger-context.js +39 -0
- package/dist/actions/scroll.d.ts +8 -10
- package/dist/actions/scroll.js +28 -17
- package/dist/actions/synthetic/synthetic-input.d.ts +2 -0
- package/dist/actions/synthetic/synthetic-input.js +13 -0
- package/dist/actions/value-input.d.ts +0 -30
- package/dist/actions/value-input.js +0 -33
- package/dist/commands/commands.d.ts +0 -2
- package/dist/commands/commands.js +10 -70
- package/dist/connect-options.d.ts +2 -96
- package/dist/dom/a11y.d.ts +0 -29
- package/dist/dom/a11y.js +3 -90
- package/dist/dom/addressing/auto-anchor.d.ts +25 -0
- package/dist/dom/addressing/auto-anchor.js +41 -0
- package/dist/dom/addressing/refs.d.ts +14 -0
- package/dist/dom/addressing/refs.js +114 -0
- package/dist/dom/addressing/source.d.ts +9 -0
- package/dist/dom/addressing/source.js +29 -0
- package/dist/dom/chart.d.ts +0 -61
- package/dist/dom/chart.js +0 -60
- package/dist/dom/desktop-capture.d.ts +0 -30
- package/dist/dom/desktop-capture.js +1 -73
- package/dist/dom/dom-ignore.d.ts +0 -30
- package/dist/dom/dom-ignore.js +1 -48
- package/dist/dom/occlusion.d.ts +0 -7
- package/dist/dom/occlusion.js +31 -12
- package/dist/dom/paint-context.d.ts +6 -0
- package/dist/dom/paint-context.js +38 -0
- package/dist/dom/query.d.ts +0 -14
- package/dist/dom/query.js +50 -216
- package/dist/dom/realm.d.ts +0 -20
- package/dist/dom/realm.js +0 -28
- package/dist/dom/shadow-registry.d.ts +0 -33
- package/dist/dom/shadow-registry.js +2 -72
- package/dist/dom/snapshot.d.ts +2 -43
- package/dist/dom/snapshot.js +27 -152
- package/dist/dom/theme.d.ts +0 -37
- package/dist/dom/theme.js +2 -52
- package/dist/dom/virtualized.d.ts +0 -17
- package/dist/dom/virtualized.js +0 -27
- package/dist/edit-epoch.d.ts +1 -23
- package/dist/edit-epoch.js +1 -53
- package/dist/index.d.ts +4 -5
- package/dist/index.js +4 -28
- package/dist/observers/animation.d.ts +0 -1
- package/dist/observers/animation.js +1 -4
- package/dist/observers/blind-spots.d.ts +0 -26
- package/dist/observers/blind-spots.js +3 -62
- package/dist/observers/console.d.ts +1 -2
- package/dist/observers/console.js +41 -30
- package/dist/observers/context-open.d.ts +1 -11
- package/dist/observers/context-open.js +10 -14
- package/dist/observers/departure.d.ts +3 -0
- package/dist/observers/departure.js +52 -0
- package/dist/observers/dialogs.d.ts +2 -0
- package/dist/observers/dialogs.js +39 -0
- package/dist/observers/dom.js +28 -52
- package/dist/observers/download.d.ts +1 -2
- package/dist/observers/download.js +39 -56
- package/dist/observers/focus.d.ts +0 -6
- package/dist/observers/focus.js +0 -7
- package/dist/observers/frames.d.ts +0 -9
- package/dist/observers/frames.js +1 -28
- package/dist/observers/health.d.ts +0 -5
- package/dist/observers/health.js +2 -34
- package/dist/observers/install-all.d.ts +2 -0
- package/dist/observers/install-all.js +26 -27
- package/dist/observers/ipc.d.ts +0 -25
- package/dist/observers/ipc.js +0 -65
- package/dist/observers/navigation.d.ts +2 -0
- package/dist/observers/navigation.js +33 -0
- package/dist/observers/net-detail/network-body.d.ts +12 -0
- package/dist/observers/net-detail/network-body.js +71 -0
- package/dist/observers/net-detail/network-redact.d.ts +1 -0
- package/dist/observers/net-detail/network-redact.js +1 -0
- package/dist/observers/network-stream.d.ts +0 -21
- package/dist/observers/network-stream.js +3 -46
- package/dist/observers/network.d.ts +3 -27
- package/dist/observers/network.js +178 -295
- package/dist/observers/perf.d.ts +0 -10
- package/dist/observers/perf.js +0 -16
- package/dist/observers/route.d.ts +1 -2
- package/dist/observers/route.js +13 -19
- package/dist/observers/scroll.d.ts +0 -1
- package/dist/observers/scroll.js +2 -11
- package/dist/observers/sdk-failure.d.ts +1 -9
- package/dist/observers/sdk-failure.js +1 -30
- package/dist/observers/state.d.ts +1 -11
- package/dist/observers/state.js +13 -41
- package/dist/observers/storage.d.ts +0 -14
- package/dist/observers/storage.js +10 -34
- package/dist/observers/types.d.ts +0 -15
- package/dist/observers/types.js +0 -14
- package/dist/patching/capture-method.d.ts +0 -24
- package/dist/patching/capture-method.js +0 -26
- package/dist/presenter/chrome/layers.d.ts +2 -0
- package/dist/presenter/chrome/layers.js +2 -0
- package/dist/presenter/chrome/log-kinds.d.ts +12 -0
- package/dist/presenter/chrome/log-kinds.js +7 -0
- package/dist/presenter/{overlay.d.ts → chrome/overlay.d.ts} +0 -1
- package/dist/presenter/{overlay.js → chrome/overlay.js} +0 -1
- package/dist/presenter/chrome/presenter-brand.d.ts +3 -0
- package/dist/presenter/{presenter-brand.js → chrome/presenter-brand.js} +0 -21
- package/dist/presenter/chrome/presenter-effects.d.ts +4 -0
- package/dist/presenter/{presenter-effects.js → chrome/presenter-effects.js} +1 -10
- package/dist/presenter/chrome/presenter-hud-chrome.d.ts +7 -0
- package/dist/presenter/{presenter-hud-chrome.js → chrome/presenter-hud-chrome.js} +0 -10
- package/dist/presenter/chrome/presenter-log.d.ts +17 -0
- package/dist/presenter/{presenter-log.js → chrome/presenter-log.js} +5 -54
- package/dist/presenter/chrome/presenter-report-copy.d.ts +51 -0
- package/dist/presenter/chrome/presenter-report-copy.js +95 -0
- package/dist/presenter/chrome/presenter-safe-html.d.ts +2 -0
- package/dist/presenter/chrome/presenter-safe-html.js +16 -0
- package/dist/presenter/{presenter-tally.d.ts → chrome/presenter-tally.d.ts} +0 -5
- package/dist/presenter/{presenter-tally.js → chrome/presenter-tally.js} +1 -5
- package/dist/presenter/{presenter-verbs.js → chrome/presenter-verbs.js} +0 -7
- package/dist/presenter/{presenter-heroicons-data.d.ts → icons/presenter-heroicons-data.d.ts} +1 -8
- package/dist/presenter/{presenter-heroicons-data.js → icons/presenter-heroicons-data.js} +1 -8
- package/dist/presenter/{presenter-icons.d.ts → icons/presenter-icons.d.ts} +1 -6
- package/dist/presenter/{presenter-icons.js → icons/presenter-icons.js} +1 -6
- package/dist/presenter/presenter-account-styles.d.ts +1 -0
- package/dist/presenter/presenter-account-styles.js +99 -0
- package/dist/presenter/presenter-account.d.ts +40 -0
- package/dist/presenter/presenter-account.js +178 -0
- package/dist/presenter/presenter-config.d.ts +10 -107
- package/dist/presenter/presenter-config.js +5 -92
- package/dist/presenter/presenter-controls.d.ts +1 -35
- package/dist/presenter/presenter-controls.js +10 -133
- package/dist/presenter/presenter-dock-layout.d.ts +0 -3
- package/dist/presenter/presenter-dock-layout.js +1 -5
- package/dist/presenter/presenter-drag.d.ts +2 -42
- package/dist/presenter/presenter-drag.js +3 -76
- package/dist/presenter/presenter-glow.d.ts +0 -21
- package/dist/presenter/presenter-glow.js +2 -28
- package/dist/presenter/presenter-hud-position.d.ts +18 -0
- package/dist/presenter/presenter-hud-position.js +50 -0
- package/dist/presenter/presenter-report-styles.d.ts +0 -6
- package/dist/presenter/presenter-report-styles.js +22 -7
- package/dist/presenter/presenter-report.d.ts +5 -7
- package/dist/presenter/presenter-report.js +44 -95
- package/dist/presenter/presenter-run-state.d.ts +1 -8
- package/dist/presenter/presenter-run-state.js +1 -6
- package/dist/presenter/presenter-settings-styles.d.ts +0 -1
- package/dist/presenter/presenter-settings-styles.js +2 -2
- package/dist/presenter/presenter-settings.d.ts +5 -23
- package/dist/presenter/presenter-settings.js +21 -33
- package/dist/presenter/presenter-shell-styles.js +23 -20
- package/dist/presenter/presenter-shell.d.ts +6 -18
- package/dist/presenter/presenter-shell.js +26 -77
- package/dist/presenter/presenter-styles.d.ts +2 -10
- package/dist/presenter/presenter-styles.js +6 -12
- package/dist/presenter/presenter-workspace.d.ts +4 -4
- package/dist/presenter/presenter-workspace.js +30 -10
- package/dist/presenter/presenter.d.ts +3 -63
- package/dist/presenter/presenter.js +29 -132
- package/dist/presenter/tour/tour-view.d.ts +33 -0
- package/dist/presenter/tour/tour-view.js +188 -0
- package/dist/presenter/tour/tour.d.ts +25 -0
- package/dist/presenter/tour/tour.js +245 -0
- package/dist/recorder/recorder-styles.d.ts +0 -6
- package/dist/recorder/recorder-styles.js +0 -6
- package/dist/recorder/recorder.d.ts +0 -34
- package/dist/recorder/recorder.js +6 -49
- package/dist/registry/auto-testids.d.ts +0 -19
- package/dist/registry/auto-testids.js +1 -21
- package/dist/registry/capabilities.d.ts +0 -31
- package/dist/registry/capabilities.js +1 -46
- package/dist/registry/commit-and-signal.d.ts +0 -6
- package/dist/registry/commit-and-signal.js +1 -7
- package/dist/registry/declared-channels.d.ts +2 -0
- package/dist/registry/declared-channels.js +14 -0
- package/dist/registry/declared-platform.d.ts +4 -0
- package/dist/registry/declared-platform.js +34 -0
- package/dist/registry/domains.d.ts +0 -8
- package/dist/registry/domains.js +0 -7
- package/dist/registry/emitter.d.ts +0 -14
- package/dist/registry/emitter.js +1 -11
- package/dist/registry/store-adapters.d.ts +0 -186
- package/dist/registry/store-adapters.js +1 -164
- package/dist/registry/{adapters.d.ts → stores/adapters.d.ts} +0 -10
- package/dist/registry/{adapters.js → stores/adapters.js} +0 -10
- package/dist/registry/stores.d.ts +0 -33
- package/dist/registry/stores.js +1 -60
- package/dist/reticle-presenter-helpers.d.ts +0 -13
- package/dist/reticle-presenter-helpers.js +10 -20
- package/dist/reticle-singleton.d.ts +2 -0
- package/dist/reticle-singleton.js +3 -0
- package/dist/reticle.d.ts +1 -52
- package/dist/reticle.js +115 -180
- package/dist/review/annotator-styles.js +1 -8
- package/dist/review/annotator.d.ts +2 -13
- package/dist/review/annotator.js +2 -42
- package/dist/review/mark-anchor.d.ts +0 -11
- package/dist/review/mark-anchor.js +3 -11
- package/dist/security/serialization.d.ts +0 -20
- package/dist/security/serialization.js +1 -66
- package/dist/session-continuity.d.ts +0 -16
- package/dist/session-continuity.js +1 -20
- package/dist/timers/clock.d.ts +1 -14
- package/dist/timers/clock.js +59 -60
- package/dist/timers/console/native-console.js +5 -0
- package/dist/timers/native/native-timers.d.ts +11 -0
- package/dist/timers/native/native-timers.js +48 -0
- package/dist/transport/transport.d.ts +0 -51
- package/dist/transport/transport.js +5 -131
- package/dist/transport/unreachable-message.d.ts +0 -17
- package/dist/transport/unreachable-message.js +0 -17
- package/dist/util/captured-method.d.ts +0 -35
- package/dist/util/captured-method.js +0 -35
- package/package.json +5 -4
- package/dist/actions/synthetic-input.d.ts +0 -9
- package/dist/actions/synthetic-input.js +0 -40
- package/dist/dom/auto-anchor.d.ts +0 -65
- package/dist/dom/auto-anchor.js +0 -74
- package/dist/dom/refs.d.ts +0 -90
- package/dist/dom/refs.js +0 -245
- package/dist/dom/source.d.ts +0 -53
- package/dist/dom/source.js +0 -74
- package/dist/observers/network-body.d.ts +0 -35
- package/dist/observers/network-body.js +0 -168
- package/dist/observers/network-redact.d.ts +0 -20
- package/dist/observers/network-redact.js +0 -80
- package/dist/presenter/presenter-brand.d.ts +0 -22
- package/dist/presenter/presenter-effects.d.ts +0 -13
- package/dist/presenter/presenter-hud-chrome.d.ts +0 -17
- package/dist/presenter/presenter-log.d.ts +0 -64
- package/dist/presenter/presenter-report-copy.d.ts +0 -112
- package/dist/presenter/presenter-report-copy.js +0 -156
- package/dist/presenter/presenter-test-helpers.d.ts +0 -16
- package/dist/presenter/presenter-test-helpers.js +0 -41
- package/dist/timers/native-console.js +0 -9
- package/dist/timers/native-timers.d.ts +0 -36
- package/dist/timers/native-timers.js +0 -78
- /package/dist/presenter/{presenter-verbs.d.ts → chrome/presenter-verbs.d.ts} +0 -0
- /package/dist/timers/{native-console.d.ts → console/native-console.d.ts} +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isLastSlide } from './tour-view.js';
|
|
2
|
+
export declare const TOUR_SEEN_KEY_PREFIX = "reticle.tour.seen.";
|
|
3
|
+
export declare const tourSeenKey: (projectId: string) => string;
|
|
4
|
+
export interface TourStorage {
|
|
5
|
+
getItem: (key: string) => string | null;
|
|
6
|
+
setItem: (key: string, value: string) => void;
|
|
7
|
+
}
|
|
8
|
+
export interface TourDeps {
|
|
9
|
+
readonly document: Document;
|
|
10
|
+
readonly storage: TourStorage | undefined;
|
|
11
|
+
readonly projectId: string;
|
|
12
|
+
readonly isDriving: () => boolean;
|
|
13
|
+
readonly search?: string;
|
|
14
|
+
readonly copy?: (text: string) => Promise<boolean> | boolean;
|
|
15
|
+
readonly select?: (element: Element) => void;
|
|
16
|
+
}
|
|
17
|
+
export interface TourHandle {
|
|
18
|
+
readonly destroy: () => void;
|
|
19
|
+
readonly isOpen: () => boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function openedByReticle(search: string | undefined): boolean;
|
|
22
|
+
export declare function tourAlreadySeen(storage: TourStorage | undefined, projectId: string): boolean;
|
|
23
|
+
export declare function mountTour(deps: TourDeps): TourHandle | undefined;
|
|
24
|
+
export { isLastSlide };
|
|
25
|
+
export declare function safeLocalStorage(): TourStorage | undefined;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { COPIED_LABEL, COPY_FLASH_MS, COPY_LABEL, COPY_MANUAL_LABEL, TOUR_ATTR, TOUR_CSS, TOUR_PROMPT_ATTR, TOUR_TARGET_ATTR, holeRects, isInteractive, isLastSlide, nextIndex, slideHtml, tourSlides, } from './tour-view.js';
|
|
2
|
+
import { TourAnchor } from '@reticlehq/core/tour';
|
|
3
|
+
import { RETICLE_URL_PARAM } from '@reticlehq/core';
|
|
4
|
+
export const TOUR_SEEN_KEY_PREFIX = 'reticle.tour.seen.';
|
|
5
|
+
const APP_SELECTORS = ['main', '#root', '#app'];
|
|
6
|
+
const HUD_PARTS = ['[data-reticle-hud]', '[data-reticle-chat-panel]'];
|
|
7
|
+
function unionOfVisible(doc, selectors) {
|
|
8
|
+
let left = Number.POSITIVE_INFINITY;
|
|
9
|
+
let top = Number.POSITIVE_INFINITY;
|
|
10
|
+
let right = Number.NEGATIVE_INFINITY;
|
|
11
|
+
let bottom = Number.NEGATIVE_INFINITY;
|
|
12
|
+
for (const selector of selectors) {
|
|
13
|
+
const box = doc.querySelector(selector)?.getBoundingClientRect();
|
|
14
|
+
if (undefined === box || box.width <= 0 || box.height <= 0)
|
|
15
|
+
continue;
|
|
16
|
+
left = Math.min(left, box.left);
|
|
17
|
+
top = Math.min(top, box.top);
|
|
18
|
+
right = Math.max(right, box.left + box.width);
|
|
19
|
+
bottom = Math.max(bottom, box.top + box.height);
|
|
20
|
+
}
|
|
21
|
+
if (right === Number.NEGATIVE_INFINITY)
|
|
22
|
+
return undefined;
|
|
23
|
+
return { left, top, width: right - left, height: bottom - top };
|
|
24
|
+
}
|
|
25
|
+
const HUD_CONTROL_SELECTORS = {
|
|
26
|
+
[TourAnchor.HUD_CHAT]: '[data-reticle-chat-toggle]',
|
|
27
|
+
[TourAnchor.HUD_ANNOTATE]: '[data-reticle-annotate-btn]',
|
|
28
|
+
[TourAnchor.HUD_IMPACT]: '[data-reticle-report-btn]',
|
|
29
|
+
[TourAnchor.HUD_SETTINGS]: '[data-reticle-settings-btn]',
|
|
30
|
+
};
|
|
31
|
+
function anchorBox(doc, anchor) {
|
|
32
|
+
if (TourAnchor.HUD === anchor)
|
|
33
|
+
return unionOfVisible(doc, HUD_PARTS);
|
|
34
|
+
const control = HUD_CONTROL_SELECTORS[anchor];
|
|
35
|
+
if (undefined !== control) {
|
|
36
|
+
const box = doc.querySelector(control)?.getBoundingClientRect();
|
|
37
|
+
return undefined === box ? undefined : box;
|
|
38
|
+
}
|
|
39
|
+
if (TourAnchor.APP !== anchor)
|
|
40
|
+
return undefined;
|
|
41
|
+
for (const selector of APP_SELECTORS) {
|
|
42
|
+
const found = doc.querySelector(selector);
|
|
43
|
+
if (null === found)
|
|
44
|
+
continue;
|
|
45
|
+
const box = found.getBoundingClientRect();
|
|
46
|
+
if (box.width > 0)
|
|
47
|
+
return box;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
export const tourSeenKey = (projectId) => `${TOUR_SEEN_KEY_PREFIX}${projectId}`;
|
|
52
|
+
export function openedByReticle(search) {
|
|
53
|
+
if (search === undefined || '' === search)
|
|
54
|
+
return false;
|
|
55
|
+
const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search);
|
|
56
|
+
return (params.has(RETICLE_URL_PARAM.OPENED) ||
|
|
57
|
+
params.has(RETICLE_URL_PARAM.SESSION) ||
|
|
58
|
+
params.has(RETICLE_URL_PARAM.PROJECT));
|
|
59
|
+
}
|
|
60
|
+
export function tourAlreadySeen(storage, projectId) {
|
|
61
|
+
if (undefined === storage)
|
|
62
|
+
return true;
|
|
63
|
+
try {
|
|
64
|
+
return null !== storage.getItem(tourSeenKey(projectId));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function markSeen(storage, projectId) {
|
|
71
|
+
try {
|
|
72
|
+
storage?.setItem(tourSeenKey(projectId), '1');
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function mountTour(deps) {
|
|
78
|
+
if (deps.isDriving())
|
|
79
|
+
return undefined;
|
|
80
|
+
if (openedByReticle(deps.search))
|
|
81
|
+
return undefined;
|
|
82
|
+
if (tourAlreadySeen(deps.storage, deps.projectId))
|
|
83
|
+
return undefined;
|
|
84
|
+
const doc = deps.document;
|
|
85
|
+
const style = doc.createElement('style');
|
|
86
|
+
style.setAttribute(TOUR_ATTR, '');
|
|
87
|
+
style.textContent = TOUR_CSS;
|
|
88
|
+
doc.head.appendChild(style);
|
|
89
|
+
const root = doc.createElement('div');
|
|
90
|
+
root.setAttribute(TOUR_ATTR, '');
|
|
91
|
+
const slides = tourSlides();
|
|
92
|
+
let index = 0;
|
|
93
|
+
let open = true;
|
|
94
|
+
let detachKeys = () => undefined;
|
|
95
|
+
const close = () => {
|
|
96
|
+
if (!open)
|
|
97
|
+
return;
|
|
98
|
+
open = false;
|
|
99
|
+
detachKeys();
|
|
100
|
+
markSeen(deps.storage, deps.projectId);
|
|
101
|
+
root.remove();
|
|
102
|
+
style.remove();
|
|
103
|
+
};
|
|
104
|
+
const draw = () => {
|
|
105
|
+
const slide = slides[index];
|
|
106
|
+
if (undefined === slide)
|
|
107
|
+
return;
|
|
108
|
+
root.innerHTML = `<div class="reticle-tour-scrim"></div>${slideHtml(slide)}`;
|
|
109
|
+
if (TourAnchor.NONE !== slide.anchor) {
|
|
110
|
+
const box = anchorBox(doc, slide.anchor);
|
|
111
|
+
if (undefined !== box && box.width > 0) {
|
|
112
|
+
const region = TourAnchor.APP === slide.anchor;
|
|
113
|
+
const ring = doc.createElement('div');
|
|
114
|
+
ring.className = region ? 'reticle-tour-ring is-region' : 'reticle-tour-ring';
|
|
115
|
+
const pad = region ? -2 : 6;
|
|
116
|
+
ring.style.left = `${String(Math.round(box.left - pad))}px`;
|
|
117
|
+
ring.style.top = `${String(Math.round(box.top - pad))}px`;
|
|
118
|
+
ring.style.width = `${String(Math.round(box.width + pad * 2))}px`;
|
|
119
|
+
ring.style.height = `${String(Math.round(box.height + pad * 2))}px`;
|
|
120
|
+
root.appendChild(ring);
|
|
121
|
+
if (!region)
|
|
122
|
+
root.querySelector('.reticle-tour-scrim')?.classList.add('is-clear');
|
|
123
|
+
if (isInteractive(slide)) {
|
|
124
|
+
ring.classList.add('is-live');
|
|
125
|
+
root.querySelector('.reticle-tour-scrim')?.remove();
|
|
126
|
+
const view = doc.defaultView;
|
|
127
|
+
for (const rect of holeRects({
|
|
128
|
+
left: box.left - pad,
|
|
129
|
+
top: box.top - pad,
|
|
130
|
+
width: box.width + pad * 2,
|
|
131
|
+
height: box.height + pad * 2,
|
|
132
|
+
}, view?.innerWidth ?? doc.documentElement.clientWidth, view?.innerHeight ?? doc.documentElement.clientHeight)) {
|
|
133
|
+
const blocker = doc.createElement('div');
|
|
134
|
+
blocker.className = 'reticle-tour-blocker';
|
|
135
|
+
blocker.style.left = `${String(Math.round(rect.left))}px`;
|
|
136
|
+
blocker.style.top = `${String(Math.round(rect.top))}px`;
|
|
137
|
+
blocker.style.width = `${String(Math.round(rect.width))}px`;
|
|
138
|
+
blocker.style.height = `${String(Math.round(rect.height))}px`;
|
|
139
|
+
root.appendChild(blocker);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const copyPrompt = (button) => {
|
|
146
|
+
const slide = slides[index];
|
|
147
|
+
const which = Number(button.getAttribute(TOUR_PROMPT_ATTR));
|
|
148
|
+
const prompt = slide?.prompts?.[which];
|
|
149
|
+
if (undefined === prompt)
|
|
150
|
+
return;
|
|
151
|
+
const flash = (label, mark) => {
|
|
152
|
+
button.textContent = label;
|
|
153
|
+
button.classList.add(mark);
|
|
154
|
+
doc.defaultView?.setTimeout(() => {
|
|
155
|
+
button.textContent = COPY_LABEL;
|
|
156
|
+
button.classList.remove(mark);
|
|
157
|
+
}, COPY_FLASH_MS);
|
|
158
|
+
};
|
|
159
|
+
const settle = (copied) => {
|
|
160
|
+
if (copied) {
|
|
161
|
+
flash(COPIED_LABEL, 'is-done');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const text = root.querySelector(`.reticle-tour-prompt[${TOUR_PROMPT_ATTR}="${String(which)}"] .reticle-tour-prompt-text`);
|
|
165
|
+
if (null !== text)
|
|
166
|
+
deps.select?.(text);
|
|
167
|
+
flash(COPY_MANUAL_LABEL, 'is-manual');
|
|
168
|
+
};
|
|
169
|
+
const result = deps.copy?.(prompt.text);
|
|
170
|
+
if (undefined === result) {
|
|
171
|
+
settle(false);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if ('boolean' === typeof result) {
|
|
175
|
+
settle(result);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
void result.then(settle).catch(() => {
|
|
179
|
+
settle(false);
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
root.addEventListener('click', (event) => {
|
|
183
|
+
const target = event.target;
|
|
184
|
+
if (!(target instanceof Element))
|
|
185
|
+
return;
|
|
186
|
+
const hit = target.closest(`[${TOUR_TARGET_ATTR}]`);
|
|
187
|
+
if (null === hit)
|
|
188
|
+
return;
|
|
189
|
+
const what = hit.getAttribute(TOUR_TARGET_ATTR);
|
|
190
|
+
if (null === what)
|
|
191
|
+
return;
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
event.stopPropagation();
|
|
194
|
+
if ('skip' === what || 'done' === what) {
|
|
195
|
+
close();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if ('copy' === what) {
|
|
199
|
+
copyPrompt(hit);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const moved = nextIndex(index, 'back' === what ? -1 : 1);
|
|
203
|
+
if (moved === index)
|
|
204
|
+
return;
|
|
205
|
+
index = moved;
|
|
206
|
+
draw();
|
|
207
|
+
});
|
|
208
|
+
const onKey = (event) => {
|
|
209
|
+
if (!open)
|
|
210
|
+
return;
|
|
211
|
+
const step = 'ArrowRight' === event.key ? 1 : 'ArrowLeft' === event.key ? -1 : 0;
|
|
212
|
+
if ('Escape' === event.key) {
|
|
213
|
+
event.preventDefault();
|
|
214
|
+
close();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (0 === step)
|
|
218
|
+
return;
|
|
219
|
+
const moved = nextIndex(index, step);
|
|
220
|
+
if (moved === index)
|
|
221
|
+
return;
|
|
222
|
+
event.preventDefault();
|
|
223
|
+
index = moved;
|
|
224
|
+
draw();
|
|
225
|
+
};
|
|
226
|
+
doc.addEventListener('keydown', onKey);
|
|
227
|
+
detachKeys = () => {
|
|
228
|
+
doc.removeEventListener('keydown', onKey);
|
|
229
|
+
};
|
|
230
|
+
draw();
|
|
231
|
+
doc.body.appendChild(root);
|
|
232
|
+
return {
|
|
233
|
+
destroy: close,
|
|
234
|
+
isOpen: () => open,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
export { isLastSlide };
|
|
238
|
+
export function safeLocalStorage() {
|
|
239
|
+
try {
|
|
240
|
+
return window.localStorage;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Inline style strings for the floating recorder toolbar. Browser-local UI
|
|
3
|
-
* text (never crosses the wire), split out of recorder.ts to keep that file under the 500-line cap
|
|
4
|
-
* (mirrors how presenter.ts extracts presenter-log.ts). All toolbar nodes also carry
|
|
5
|
-
* data-reticle-overlay, so they are snapshot-excluded via dom-ignore.ts regardless of these styles.
|
|
6
|
-
*/
|
|
7
1
|
export declare const TOOLBAR_CSS: string;
|
|
8
2
|
export declare const BTN_CSS: string;
|
|
9
3
|
export declare const NAME_CSS: string;
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Inline style strings for the floating recorder toolbar. Browser-local UI
|
|
3
|
-
* text (never crosses the wire), split out of recorder.ts to keep that file under the 500-line cap
|
|
4
|
-
* (mirrors how presenter.ts extracts presenter-log.ts). All toolbar nodes also carry
|
|
5
|
-
* data-reticle-overlay, so they are snapshot-excluded via dom-ignore.ts regardless of these styles.
|
|
6
|
-
*/
|
|
7
1
|
export const TOOLBAR_CSS = [
|
|
8
2
|
'position:fixed',
|
|
9
3
|
'top:8px',
|
|
@@ -1,59 +1,25 @@
|
|
|
1
1
|
import { AnnotationKind, EventType, RecorderPhase, type FlowAnchor, type FlowFile, type FlowStep } from '@reticlehq/core';
|
|
2
|
-
/**
|
|
3
|
-
* The human recorder. A floating toolbar (Record / Stop / Annotate) lets a
|
|
4
|
-
* human click the golden path; capture-phase listeners map each real interaction to a SEMANTIC
|
|
5
|
-
* anchor (testid → role+name → degraded placeholder, NEVER a volatile ref — invariant #1) and
|
|
6
|
-
* append a FlowStep. Stop compiles a FlowFile in-page and emits ONE EventType.FLOW_RECORDED event
|
|
7
|
-
* the server persists. Four structured annotation kinds compile into expect/dynamic/success.
|
|
8
|
-
*
|
|
9
|
-
* FIRST CUT scope: structured annotations only (the 4 AnnotationKind values via a menu + a signal
|
|
10
|
-
* <select> drawn from registered capabilities). Explicitly FUTURE, not faked here:
|
|
11
|
-
* - NL annotation → predicate compiler (CO-OWNED-FLOWS invariant #2 best-UX path).
|
|
12
|
-
* - fixtures/preconditions (FlowFile.fixture stays a schema slot, never written).
|
|
13
|
-
* - wait-for / ignore-region annotation kinds (only the 4 requested ship).
|
|
14
|
-
*/
|
|
15
|
-
/** Browser-local UI text — never crosses the wire, mirrors presenter.ts CHIP_LABEL precedent. */
|
|
16
2
|
export declare const RECORDER_EMPTY_MSG = "recorded 0 steps";
|
|
17
3
|
interface RecorderDeps {
|
|
18
|
-
/** Emit a wire event (Reticle.#emit bound). */
|
|
19
4
|
emit: (type: EventType, data: Record<string, unknown>) => void;
|
|
20
|
-
/** Injected clock for FlowFile.createdAt — never Date.now in pure logic (rule 7). */
|
|
21
5
|
now: () => number;
|
|
22
|
-
/** Default flow name when the toolbar field is blank. */
|
|
23
6
|
defaultName?: string;
|
|
24
7
|
}
|
|
25
8
|
export interface RecorderHandle {
|
|
26
9
|
mount(): void;
|
|
27
10
|
destroy(): void;
|
|
28
|
-
/** Test seam: current phase. */
|
|
29
11
|
phase(): RecorderPhase;
|
|
30
|
-
/** Test seam: steps captured so far in the active span. */
|
|
31
12
|
steps(): FlowStep[];
|
|
32
13
|
}
|
|
33
|
-
/** A structured annotation captured while recording (compiled into flow fields at Stop). */
|
|
34
14
|
export interface Annotation {
|
|
35
15
|
kind: AnnotationKind;
|
|
36
16
|
anchor: FlowAnchor;
|
|
37
|
-
/** Required for ASSERT_SIGNAL / SUCCESS_STATE; the chosen capability signal name. */
|
|
38
17
|
signal?: string;
|
|
39
18
|
}
|
|
40
|
-
/**
|
|
41
|
-
* Pure: map a clicked/typed element → semantic FlowAnchor. testid wins; else role+accessible-name;
|
|
42
|
-
* else the DEGRADED_ANCHOR_ROLE placeholder + degraded:true (never a ref — invariant #1).
|
|
43
|
-
*/
|
|
44
19
|
export declare function anchorFor(el: Element): {
|
|
45
20
|
anchor: FlowAnchor;
|
|
46
21
|
degraded: boolean;
|
|
47
22
|
};
|
|
48
|
-
/**
|
|
49
|
-
* Pure: assemble the in-page FlowFile from captured steps + annotations.
|
|
50
|
-
* - ASSERT_SIGNAL → the most-recent step's expect.signal.
|
|
51
|
-
* - ASSERT_VISIBLE → the most-recent step's expect.element (from the annotation anchor).
|
|
52
|
-
* - MARK_DYNAMIC → push the anchor into flow.dynamic[].
|
|
53
|
-
* - SUCCESS_STATE → flow.success = { signal }.
|
|
54
|
-
* (FUTURE: per-step annotation targeting beyond the most-recent step.)
|
|
55
|
-
*/
|
|
56
23
|
export declare function compileRecording(name: string, steps: FlowStep[], annotations: Annotation[], createdAt: number, startPath?: string): FlowFile;
|
|
57
|
-
/** Install the floating recorder toolbar. Default off; gated by the host (reticle.ts recorder flag). */
|
|
58
24
|
export declare function installRecorder(deps: RecorderDeps): RecorderHandle;
|
|
59
25
|
export {};
|
|
@@ -1,23 +1,10 @@
|
|
|
1
1
|
import { ActionType, AnchorKind, AnnotationKind, DEGRADED_ANCHOR_ROLE, EventType, FLOW_FILE_VERSION, FlowStepTool, RecorderPhase, } from '@reticlehq/core';
|
|
2
2
|
import { REDACTED_VALUE } from '@reticlehq/core';
|
|
3
|
+
import { sourceFromDom } from '../dom/addressing/source.js';
|
|
3
4
|
import { getAccessibleName, getRole, isSensitiveField } from '../dom/a11y.js';
|
|
4
5
|
import { isReticleOverlay } from '../dom/dom-ignore.js';
|
|
5
6
|
import { getCapabilities } from '../registry/capabilities.js';
|
|
6
7
|
import { TOOLBAR_CSS, BTN_CSS, NAME_CSS, STATUS_CSS, MENU_CSS } from './recorder-styles.js';
|
|
7
|
-
/**
|
|
8
|
-
* The human recorder. A floating toolbar (Record / Stop / Annotate) lets a
|
|
9
|
-
* human click the golden path; capture-phase listeners map each real interaction to a SEMANTIC
|
|
10
|
-
* anchor (testid → role+name → degraded placeholder, NEVER a volatile ref — invariant #1) and
|
|
11
|
-
* append a FlowStep. Stop compiles a FlowFile in-page and emits ONE EventType.FLOW_RECORDED event
|
|
12
|
-
* the server persists. Four structured annotation kinds compile into expect/dynamic/success.
|
|
13
|
-
*
|
|
14
|
-
* FIRST CUT scope: structured annotations only (the 4 AnnotationKind values via a menu + a signal
|
|
15
|
-
* <select> drawn from registered capabilities). Explicitly FUTURE, not faked here:
|
|
16
|
-
* - NL annotation → predicate compiler (CO-OWNED-FLOWS invariant #2 best-UX path).
|
|
17
|
-
* - fixtures/preconditions (FlowFile.fixture stays a schema slot, never written).
|
|
18
|
-
* - wait-for / ignore-region annotation kinds (only the 4 requested ship).
|
|
19
|
-
*/
|
|
20
|
-
/** Browser-local UI text — never crosses the wire, mirrors presenter.ts CHIP_LABEL precedent. */
|
|
21
8
|
export const RECORDER_EMPTY_MSG = 'recorded 0 steps';
|
|
22
9
|
const STATUS_RECORDING = 'recording…';
|
|
23
10
|
const STATUS_IDLE = 'ready';
|
|
@@ -38,16 +25,11 @@ const ANNOTATION_LABEL = {
|
|
|
38
25
|
[AnnotationKind.SUCCESS_STATE]: 'success state',
|
|
39
26
|
[AnnotationKind.INTENT]: 'intent',
|
|
40
27
|
};
|
|
41
|
-
/** Annotation kinds whose compilation needs a signal name (from the capabilities <select>). */
|
|
42
28
|
const NEEDS_SIGNAL = new Set([
|
|
43
29
|
AnnotationKind.ASSERT_SIGNAL,
|
|
44
30
|
AnnotationKind.SUCCESS_STATE,
|
|
45
31
|
]);
|
|
46
32
|
const DEFAULT_NAME = 'recorded-flow';
|
|
47
|
-
/**
|
|
48
|
-
* Pure: map a clicked/typed element → semantic FlowAnchor. testid wins; else role+accessible-name;
|
|
49
|
-
* else the DEGRADED_ANCHOR_ROLE placeholder + degraded:true (never a ref — invariant #1).
|
|
50
|
-
*/
|
|
51
33
|
export function anchorFor(el) {
|
|
52
34
|
const testid = el.getAttribute(TESTID_ATTR);
|
|
53
35
|
if (testid !== null && testid.length > 0) {
|
|
@@ -59,7 +41,6 @@ export function anchorFor(el) {
|
|
|
59
41
|
const anchor = name.length > 0 ? { kind: AnchorKind.ROLE, role, name } : { kind: AnchorKind.ROLE, role };
|
|
60
42
|
return { anchor, degraded: false };
|
|
61
43
|
}
|
|
62
|
-
// No resolvable testid/role/name — keep the step with a legible placeholder, never a ref.
|
|
63
44
|
return { anchor: { kind: AnchorKind.ROLE, role: DEGRADED_ANCHOR_ROLE }, degraded: true };
|
|
64
45
|
}
|
|
65
46
|
function isTextbox(el) {
|
|
@@ -84,16 +65,13 @@ function buildStep(el, action, args) {
|
|
|
84
65
|
step.args = args;
|
|
85
66
|
if (degraded)
|
|
86
67
|
step.degraded = true;
|
|
68
|
+
const source = sourceFromDom(el);
|
|
69
|
+
if (source !== undefined) {
|
|
70
|
+
step.source =
|
|
71
|
+
source.line === undefined ? { file: source.file } : { file: source.file, line: source.line };
|
|
72
|
+
}
|
|
87
73
|
return step;
|
|
88
74
|
}
|
|
89
|
-
/**
|
|
90
|
-
* Pure: assemble the in-page FlowFile from captured steps + annotations.
|
|
91
|
-
* - ASSERT_SIGNAL → the most-recent step's expect.signal.
|
|
92
|
-
* - ASSERT_VISIBLE → the most-recent step's expect.element (from the annotation anchor).
|
|
93
|
-
* - MARK_DYNAMIC → push the anchor into flow.dynamic[].
|
|
94
|
-
* - SUCCESS_STATE → flow.success = { signal }.
|
|
95
|
-
* (FUTURE: per-step annotation targeting beyond the most-recent step.)
|
|
96
|
-
*/
|
|
97
75
|
export function compileRecording(name, steps, annotations, createdAt, startPath) {
|
|
98
76
|
const out = steps.map((s) => ({ ...s }));
|
|
99
77
|
const dynamic = [];
|
|
@@ -137,12 +115,10 @@ function anchorToElement(anchor) {
|
|
|
137
115
|
}
|
|
138
116
|
return {};
|
|
139
117
|
}
|
|
140
|
-
/** Live recorder instance: holds the capture buffer, toolbar DOM, and the phase machine. */
|
|
141
118
|
class Recorder {
|
|
142
119
|
#deps;
|
|
143
120
|
#phase = RecorderPhase.IDLE;
|
|
144
121
|
#steps = [];
|
|
145
|
-
/** The pathname the recording began on, so replay can navigate here before step 1. */
|
|
146
122
|
#startPath;
|
|
147
123
|
#annotations = [];
|
|
148
124
|
#pendingFill;
|
|
@@ -150,7 +126,6 @@ class Recorder {
|
|
|
150
126
|
#root;
|
|
151
127
|
#statusEl;
|
|
152
128
|
#menuEl;
|
|
153
|
-
/** The annotation being assembled in ANNOTATING phase. */
|
|
154
129
|
#draft;
|
|
155
130
|
constructor(deps) {
|
|
156
131
|
this.#deps = deps;
|
|
@@ -183,7 +158,6 @@ class Recorder {
|
|
|
183
158
|
this.#draft = undefined;
|
|
184
159
|
this.#pendingFill = undefined;
|
|
185
160
|
}
|
|
186
|
-
// ---- capture ----
|
|
187
161
|
#installCapture() {
|
|
188
162
|
const onClick = (ev) => this.#onClick(ev);
|
|
189
163
|
const onInput = (ev) => this.#onInput(ev);
|
|
@@ -200,7 +174,6 @@ class Recorder {
|
|
|
200
174
|
document.removeEventListener('submit', onSubmit, true);
|
|
201
175
|
});
|
|
202
176
|
}
|
|
203
|
-
/** True if the event should be ignored (toolbar self-click or non-element target). */
|
|
204
177
|
#ignore(ev) {
|
|
205
178
|
const target = ev.target;
|
|
206
179
|
if (!(target instanceof Element))
|
|
@@ -219,9 +192,6 @@ class Recorder {
|
|
|
219
192
|
}
|
|
220
193
|
if (this.#phase !== RecorderPhase.RECORDING)
|
|
221
194
|
return;
|
|
222
|
-
// Controls that emit `change` (checkbox, radio, select) are recorded by #onChange as their
|
|
223
|
-
// semantic step (CHECK/UNCHECK/SELECT). Recording a CLICK here too would replay any onChange
|
|
224
|
-
// side effect twice (analytics, dependent-field resets), so skip the duplicate.
|
|
225
195
|
if ((target instanceof HTMLInputElement &&
|
|
226
196
|
('checkbox' === inputRole(target) || 'radio' === inputRole(target))) ||
|
|
227
197
|
target instanceof HTMLSelectElement) {
|
|
@@ -236,7 +206,6 @@ class Recorder {
|
|
|
236
206
|
const target = this.#ignore(ev);
|
|
237
207
|
if (target === undefined || !isTextbox(target))
|
|
238
208
|
return;
|
|
239
|
-
// Debounce per-keystroke input: keep the latest value, flush on change/blur/next-act.
|
|
240
209
|
this.#pendingFill = { el: target, value: target.value };
|
|
241
210
|
}
|
|
242
211
|
#onChange(ev) {
|
|
@@ -279,12 +248,9 @@ class Recorder {
|
|
|
279
248
|
if (pending === undefined)
|
|
280
249
|
return;
|
|
281
250
|
this.#pendingFill = undefined;
|
|
282
|
-
// Never persist a typed secret into a git-checked flow file: password/OTP/cc/sensitive-keyed
|
|
283
|
-
// fields record REDACTED_VALUE, the same redaction live snapshots apply (dom/a11y.ts getValue).
|
|
284
251
|
const value = isSensitiveField(pending.el) ? REDACTED_VALUE : pending.value;
|
|
285
252
|
this.#steps.push(buildStep(pending.el, ActionType.FILL, { value }));
|
|
286
253
|
}
|
|
287
|
-
// ---- annotation ----
|
|
288
254
|
#captureAnnotationTarget(target) {
|
|
289
255
|
const draft = this.#draft;
|
|
290
256
|
if (draft === undefined)
|
|
@@ -299,7 +265,6 @@ class Recorder {
|
|
|
299
265
|
this.#setPhase(RecorderPhase.RECORDING);
|
|
300
266
|
this.#closeMenu();
|
|
301
267
|
}
|
|
302
|
-
/** Annotate-on-prior: confirm the most-recent step as the target (no extra click needed). */
|
|
303
268
|
#confirmAnnotationOnPrior() {
|
|
304
269
|
const draft = this.#draft;
|
|
305
270
|
if (draft === undefined)
|
|
@@ -325,9 +290,7 @@ class Recorder {
|
|
|
325
290
|
const value = select?.value ?? '';
|
|
326
291
|
return value.length > 0 ? value : undefined;
|
|
327
292
|
}
|
|
328
|
-
// ---- toolbar lifecycle actions ----
|
|
329
293
|
#start() {
|
|
330
|
-
// A fresh span — no leakage from a previous Record→Stop cycle.
|
|
331
294
|
this.#steps = [];
|
|
332
295
|
this.#startPath = 'undefined' === typeof location ? undefined : location.pathname;
|
|
333
296
|
this.#annotations = [];
|
|
@@ -355,7 +318,6 @@ class Recorder {
|
|
|
355
318
|
#setPhase(phase) {
|
|
356
319
|
this.#phase = phase;
|
|
357
320
|
}
|
|
358
|
-
// ---- toolbar DOM (all nodes data-reticle-overlay → snapshot-excluded via dom-ignore.ts) ----
|
|
359
321
|
#buildToolbar() {
|
|
360
322
|
const root = document.createElement('div');
|
|
361
323
|
root.setAttribute('data-reticle-overlay', '');
|
|
@@ -396,9 +358,6 @@ class Recorder {
|
|
|
396
358
|
return;
|
|
397
359
|
menu.textContent = '';
|
|
398
360
|
for (const kind of Object.values(AnnotationKind)) {
|
|
399
|
-
// INTENT is free-text (the business goal), not a signal/testid pick — authored via
|
|
400
|
-
// reticle_annotate { kind:'intent', text }. The human toolbar's name field is signal/testid only,
|
|
401
|
-
// so intent is omitted from this menu rather than shipped as a control that can't capture text.
|
|
402
361
|
if (kind === AnnotationKind.INTENT)
|
|
403
362
|
continue;
|
|
404
363
|
const item = document.createElement('button');
|
|
@@ -426,7 +385,6 @@ class Recorder {
|
|
|
426
385
|
}
|
|
427
386
|
menu.appendChild(select);
|
|
428
387
|
}
|
|
429
|
-
// "Confirm on prior step" path (annotate-on-prior); or the next click selects the target.
|
|
430
388
|
const confirm = document.createElement('button');
|
|
431
389
|
confirm.setAttribute('data-reticle-action', 'annotate-confirm');
|
|
432
390
|
confirm.textContent = 'on prior step';
|
|
@@ -448,7 +406,6 @@ class Recorder {
|
|
|
448
406
|
return value.length > 0 ? value : undefined;
|
|
449
407
|
}
|
|
450
408
|
}
|
|
451
|
-
/** Install the floating recorder toolbar. Default off; gated by the host (reticle.ts recorder flag). */
|
|
452
409
|
export function installRecorder(deps) {
|
|
453
410
|
return new Recorder(deps);
|
|
454
411
|
}
|
|
@@ -1,21 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The testids the page is actually showing, read from the live DOM.
|
|
3
|
-
*
|
|
4
|
-
* `registerCapabilities({ testids: [...] })` used to be the only source, and `init` filled it by
|
|
5
|
-
* grepping the source at install time. That list is a snapshot of the codebase on the day `init` ran:
|
|
6
|
-
* it goes stale on the next commit that renames an element, it is empty for every app whose testids
|
|
7
|
-
* arrive with a lazy route, and finishing it was homework handed to whoever ran the install.
|
|
8
|
-
*
|
|
9
|
-
* The attribute in the DOM is the same declaration, read fresh, so nothing has to be copied into a
|
|
10
|
-
* config file to be true. Reticle's own presenter markup is excluded — advertising our own controls
|
|
11
|
-
* as the app's testable surface would be the tool describing itself.
|
|
12
|
-
*/
|
|
13
|
-
/**
|
|
14
|
-
* Ceiling on the reported list.
|
|
15
|
-
*
|
|
16
|
-
* A virtualized 500-row grid stamps a testid per cell, and the capabilities payload rides in every
|
|
17
|
-
* announce — an uncapped read turns one long table into a permanently expensive message. The cap is
|
|
18
|
-
* on the REPORT, not on what can be driven: a testid past it is still findable by query.
|
|
19
|
-
*/
|
|
20
1
|
export declare const MAX_AUTO_TESTIDS = 200;
|
|
21
2
|
export declare function domTestids(doc: Document): string[];
|
|
@@ -1,26 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The testids the page is actually showing, read from the live DOM.
|
|
3
|
-
*
|
|
4
|
-
* `registerCapabilities({ testids: [...] })` used to be the only source, and `init` filled it by
|
|
5
|
-
* grepping the source at install time. That list is a snapshot of the codebase on the day `init` ran:
|
|
6
|
-
* it goes stale on the next commit that renames an element, it is empty for every app whose testids
|
|
7
|
-
* arrive with a lazy route, and finishing it was homework handed to whoever ran the install.
|
|
8
|
-
*
|
|
9
|
-
* The attribute in the DOM is the same declaration, read fresh, so nothing has to be copied into a
|
|
10
|
-
* config file to be true. Reticle's own presenter markup is excluded — advertising our own controls
|
|
11
|
-
* as the app's testable surface would be the tool describing itself.
|
|
12
|
-
*/
|
|
13
1
|
const TESTID_ATTR = 'data-testid';
|
|
14
2
|
const TESTID_SELECTOR = `[${TESTID_ATTR}]`;
|
|
15
|
-
/**
|
|
16
|
-
* Ceiling on the reported list.
|
|
17
|
-
*
|
|
18
|
-
* A virtualized 500-row grid stamps a testid per cell, and the capabilities payload rides in every
|
|
19
|
-
* announce — an uncapped read turns one long table into a permanently expensive message. The cap is
|
|
20
|
-
* on the REPORT, not on what can be driven: a testid past it is still findable by query.
|
|
21
|
-
*/
|
|
22
3
|
export const MAX_AUTO_TESTIDS = 200;
|
|
23
|
-
/** Reticle's own UI carries testids; they are not the host app's surface. */
|
|
24
4
|
const RETICLE_OWN_PREFIX = 'reticle-';
|
|
25
5
|
export function domTestids(doc) {
|
|
26
6
|
const out = [];
|
|
@@ -30,7 +10,7 @@ export function domTestids(doc) {
|
|
|
30
10
|
nodes = doc.querySelectorAll(TESTID_SELECTOR);
|
|
31
11
|
}
|
|
32
12
|
catch {
|
|
33
|
-
return out;
|
|
13
|
+
return out;
|
|
34
14
|
}
|
|
35
15
|
for (let i = 0; i < nodes.length && out.length < MAX_AUTO_TESTIDS; i += 1) {
|
|
36
16
|
const value = nodes[i]?.getAttribute(TESTID_ATTR);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Self-describing capability registry — the testable surface the app advertises. */
|
|
2
1
|
export interface CapabilityFlow {
|
|
3
2
|
name: string;
|
|
4
3
|
steps: string[];
|
|
@@ -8,46 +7,16 @@ export interface Capabilities {
|
|
|
8
7
|
signals: string[];
|
|
9
8
|
stores: string[];
|
|
10
9
|
flows: CapabilityFlow[];
|
|
11
|
-
/**
|
|
12
|
-
* True when Reticle's OWN presenter is visible to snapshots and queries.
|
|
13
|
-
*
|
|
14
|
-
* Present only when the hatch is open, and reported here rather than left implicit, because a
|
|
15
|
-
* verdict drawn against Reticle's own interface is not an ordinary verdict: an agent that can see
|
|
16
|
-
* the impact panel can also assert against it. Anybody reading a result should be able to tell
|
|
17
|
-
* which kind they are holding without going to look at a build config.
|
|
18
|
-
*/
|
|
19
10
|
presenterExposed?: boolean;
|
|
20
11
|
}
|
|
21
|
-
/** What the host app passes to reticle.describe; all fields optional. */
|
|
22
12
|
export interface CapabilitiesInput {
|
|
23
13
|
testids?: string[];
|
|
24
14
|
signals?: string[];
|
|
25
15
|
stores?: string[];
|
|
26
16
|
flows?: CapabilityFlow[];
|
|
27
17
|
}
|
|
28
|
-
/** Set by the SDK at connect. Idempotent; the last connect wins. */
|
|
29
18
|
export declare function setCapabilitiesListener(cb: (() => void) | undefined): void;
|
|
30
|
-
/** Called by the host app via reticle.describe. Merges (idempotent), never replaces wholesale. */
|
|
31
19
|
export declare function registerCapabilities(input: CapabilitiesInput): void;
|
|
32
|
-
/** Snapshot copy of the registered capabilities (defensive — never hand out the live arrays). */
|
|
33
20
|
export declare function getCapabilities(): Capabilities;
|
|
34
|
-
/**
|
|
35
|
-
* Testids the app DECLARED, without the ones merely observed in the DOM.
|
|
36
|
-
*
|
|
37
|
-
* The difference carries meaning the merged list cannot. `knownEmptyState` asks "did the app NAME
|
|
38
|
-
* this element as part of its testable surface?", which is how a zero-match query against
|
|
39
|
-
* `cart-empty-region` is told apart from a broken selector. Every testid on the page is observed, so
|
|
40
|
-
* answering that over the merged list makes it true whenever the page has any testid at all — the
|
|
41
|
-
* same as not asking.
|
|
42
|
-
*/
|
|
43
21
|
export declare function declaredTestids(): string[];
|
|
44
|
-
/**
|
|
45
|
-
* Whether the app has a testable surface at all (used in the HELLO flag).
|
|
46
|
-
*
|
|
47
|
-
* Answered over the OBSERVED surface as well as the declared one. It used to be answerable only by
|
|
48
|
-
* declaration, so an app with two hundred testids in its DOM and a live Redux store reported
|
|
49
|
-
* `hasCapabilities: false` until somebody typed those facts into a config file — and the agent
|
|
50
|
-
* reading that flag concluded, correctly by the flag and wrongly in fact, that there was nothing to
|
|
51
|
-
* drive.
|
|
52
|
-
*/
|
|
53
22
|
export declare function hasCapabilities(): boolean;
|