@tonys-software/mimic 0.0.0-stage → 0.1.0-stg.3.1

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/inject.js ADDED
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ (() => {
3
+ // ../extension/src/content/locate.ts
4
+ function normalizeText(text) {
5
+ return (text ?? "").replace(/\s+/g, " ").trim();
6
+ }
7
+ function elementText(el) {
8
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
9
+ return normalizeText(el.value);
10
+ }
11
+ return normalizeText(el.textContent);
12
+ }
13
+ function querySelectorAllSafe(root, selector) {
14
+ try {
15
+ return Array.from(root.querySelectorAll(selector));
16
+ } catch {
17
+ return [];
18
+ }
19
+ }
20
+ function resolveText(locator, root) {
21
+ const needle = normalizeText(locator.text);
22
+ if (!needle) return [];
23
+ const matches = querySelectorAllSafe(root, locator.selector || "body *").filter((el) => {
24
+ if (el instanceof HTMLScriptElement || el instanceof HTMLStyleElement) return false;
25
+ const text = elementText(el);
26
+ return locator.mode === "Contains" ? text.includes(needle) : text === needle;
27
+ });
28
+ return matches.filter((el) => !matches.some((other) => other !== el && el.contains(other)));
29
+ }
30
+ function resolveLocator(locator, root = document) {
31
+ switch (locator.type) {
32
+ case "Text":
33
+ return resolveText(locator, root);
34
+ case "Css":
35
+ return querySelectorAllSafe(root, locator.selector);
36
+ case "Anchored": {
37
+ const containers = /* @__PURE__ */ new Set();
38
+ for (const anchor of resolveLocator(locator.anchor, root)) {
39
+ const container = anchor.closest(locator.container);
40
+ if (container && (root === document || root.contains(container))) containers.add(container);
41
+ }
42
+ if (!locator.target) return Array.from(containers);
43
+ const results = /* @__PURE__ */ new Set();
44
+ for (const container of containers) {
45
+ for (const el of resolveLocator(locator.target, container)) results.add(el);
46
+ }
47
+ return Array.from(results);
48
+ }
49
+ }
50
+ }
51
+ function locate(locator) {
52
+ if (!locator) return { element: null, error: "step has no locator" };
53
+ const matches = resolveLocator(locator);
54
+ if (matches.length === 1) return { element: matches[0] };
55
+ return { element: null, error: matches.length > 1 ? `target is ambiguous (${matches.length} matches)` : "element not found" };
56
+ }
57
+
58
+ // ../extension/src/content/actionability.ts
59
+ function waitForContentSettle(quietMs = 300, timeoutMs = 1500) {
60
+ return new Promise((resolve) => {
61
+ if (!document.documentElement) {
62
+ resolve();
63
+ return;
64
+ }
65
+ let quietTimer;
66
+ function finish() {
67
+ clearTimeout(quietTimer);
68
+ clearTimeout(overallTimer);
69
+ observer.disconnect();
70
+ resolve();
71
+ }
72
+ const overallTimer = setTimeout(finish, timeoutMs);
73
+ const observer = new MutationObserver(() => {
74
+ clearTimeout(quietTimer);
75
+ quietTimer = setTimeout(finish, quietMs);
76
+ });
77
+ observer.observe(document.documentElement, {
78
+ childList: true,
79
+ subtree: true,
80
+ attributes: true,
81
+ characterData: true
82
+ });
83
+ quietTimer = setTimeout(finish, quietMs);
84
+ });
85
+ }
86
+ function clickPoint(el) {
87
+ const rect = Array.from(el.getClientRects()).find((r) => r.width > 0 && r.height > 0) ?? el.getBoundingClientRect();
88
+ if (rect.width === 0 || rect.height === 0) return void 0;
89
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
90
+ }
91
+ function describe(el) {
92
+ const id = el.id ? `#${el.id}` : "";
93
+ const classes = typeof el.className === "string" ? el.className.trim().split(/\s+/).filter(Boolean) : [];
94
+ return `<${el.tagName.toLowerCase()}${id}${classes.map((c) => `.${c}`).join("")}>`;
95
+ }
96
+ function isVisible(el) {
97
+ return el.checkVisibility({ visibilityProperty: true }) && clickPoint(el) !== void 0;
98
+ }
99
+ function visibilityProblem(el) {
100
+ if (!isVisible(el)) return "element is not visible";
101
+ if (el.matches(":disabled")) return "element is disabled";
102
+ return void 0;
103
+ }
104
+ function clickProblem(el) {
105
+ const problem = visibilityProblem(el);
106
+ if (problem) return problem;
107
+ el.scrollIntoView({ block: "center", inline: "center" });
108
+ const point = clickPoint(el);
109
+ const hit = point && document.elementFromPoint(point.x, point.y);
110
+ if (!hit) return "element is outside the page";
111
+ if (hit === el || el.contains(hit) || hit.closest("label")?.control === el) return void 0;
112
+ return `element is covered by ${describe(hit)}`;
113
+ }
114
+ function inputProblem(el) {
115
+ return visibilityProblem(el);
116
+ }
117
+ function fillProblem(el) {
118
+ return el instanceof HTMLSelectElement ? clickProblem(el) : inputProblem(el);
119
+ }
120
+ function isSelectable(option) {
121
+ return !option.matches(":disabled") && getComputedStyle(option).display !== "none";
122
+ }
123
+ function pickOption(el, label) {
124
+ if (!(el instanceof HTMLSelectElement)) return "target is not a <select>";
125
+ if (el.multiple || el.size > 1) return "target is a list box, not a dropdown";
126
+ const wanted = normalizeText(label);
127
+ const matches = Array.from(el.options).filter((o) => normalizeText(o.label) === wanted);
128
+ if (matches.length === 0) return `no option "${label}"`;
129
+ if (matches.length > 1) return `option "${label}" is ambiguous (${matches.length} matches)`;
130
+ const [option] = matches;
131
+ if (option.matches(":disabled")) return `option "${label}" is disabled`;
132
+ if (!isSelectable(option)) return `option "${label}" is hidden`;
133
+ return option;
134
+ }
135
+ function optionMoves(el, label) {
136
+ const option = pickOption(el, label);
137
+ if (typeof option === "string") return option;
138
+ const select = el;
139
+ const from = select.selectedIndex;
140
+ const to = option.index;
141
+ const passed = Array.from(select.options).filter((o) => isSelectable(o) && (to > from ? o.index > from && o.index <= to : o.index < from && o.index >= to));
142
+ return to > from ? passed.length : -passed.length;
143
+ }
144
+ function editProblem(el) {
145
+ if ((el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) && el.readOnly) return "element is read-only";
146
+ return void 0;
147
+ }
148
+
149
+ // ../extension/src/content/assertions.ts
150
+ function expectedUrl(value) {
151
+ try {
152
+ return new URL(value, location.href).href;
153
+ } catch {
154
+ return value;
155
+ }
156
+ }
157
+ function assertionCheck(step) {
158
+ const expected = step.value ?? "";
159
+ if (step.assertionType === "UrlEquals") {
160
+ const url = expectedUrl(expected);
161
+ return location.href === url ? void 0 : `expected URL "${url}", but got "${location.href}"`;
162
+ }
163
+ const { element, error } = locate(step.locator);
164
+ if (!element) return error ?? "element not found";
165
+ switch (step.assertionType) {
166
+ case "ElementExists":
167
+ return void 0;
168
+ case "ElementVisible":
169
+ return isVisible(element) ? void 0 : "element is not visible";
170
+ case "TextContains": {
171
+ const actual = elementText(element);
172
+ return actual.includes(normalizeText(expected)) ? void 0 : `expected text containing "${expected}", but got "${actual}"`;
173
+ }
174
+ case "TextEquals": {
175
+ const actual = elementText(element);
176
+ return actual === normalizeText(expected) ? void 0 : `expected text "${expected}", but got "${actual}"`;
177
+ }
178
+ case "InputValueEquals":
179
+ if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement)) {
180
+ return `<${element.tagName.toLowerCase()}> is not an input element`;
181
+ }
182
+ return element.value === expected ? void 0 : `expected value "${expected}", but got "${element.value}"`;
183
+ default: {
184
+ const unhandled = step.assertionType;
185
+ return `unknown assertion type: ${unhandled}`;
186
+ }
187
+ }
188
+ }
189
+ function assertionProblem(step) {
190
+ const problem = assertionCheck(step);
191
+ return problem && `${step.assertionType} assertion failed: ${problem}`;
192
+ }
193
+
194
+ // src/shared/inject.ts
195
+ var runner = {
196
+ find(locator, kind) {
197
+ const { element, error } = locate(locator);
198
+ if (!element) return error ?? "element not found";
199
+ return (kind === "click" ? clickProblem : fillProblem)(element) ?? element;
200
+ },
201
+ isConnected: (el) => el.isConnected,
202
+ editProblem,
203
+ optionMoves,
204
+ assertionProblem,
205
+ assertionCheck,
206
+ settle: () => waitForContentSettle()
207
+ };
208
+ Object.defineProperty(window, "__scenarioRunner", { value: runner, configurable: true });
209
+ })();
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@tonys-software/mimic",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "type": "module",
4
+ "version": "0.1.0-stg.3.1",
5
+ "description": "Run Mimic scenarios from the command line",
6
+ "license": "UNLICENSED",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/tonys-software/service.git",
10
+ "directory": "cli"
11
+ },
12
+ "bin": {
13
+ "mimic": "dist/cli.js"
14
+ },
15
+ "files": [
16
+ "dist/cli.js",
17
+ "dist/inject.js"
18
+ ],
19
+ "engines": {
20
+ "node": ">=22"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "postinstall": "playwright install chromium",
27
+ "build": "tsc --noEmit && node build.mjs",
28
+ "run-all": "node dist/cli.js run"
29
+ },
30
+ "dependencies": {
31
+ "playwright": "^1.64.0"
32
+ },
33
+ "devDependencies": {
34
+ "@service/jsclient": "file:../jsclient",
35
+ "@types/node": "^22.18.0",
36
+ "esbuild": "^0.25.10",
37
+ "typescript": "~5.9.3"
38
+ }
39
+ }