zerocheck 0.1.7 → 0.1.8
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 -3
- package/dist/index.js +234 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Paste a checklist, review one drafted check and browser verification result per
|
|
|
9
9
|
Requires Node.js 20.19 or newer and a Zerocheck project/account. Create a project in the web app’s Settings, then authorize a device or create a project token.
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
npm install --save-dev --save-exact zerocheck@0.1.
|
|
12
|
+
npm install --save-dev --save-exact zerocheck@0.1.8
|
|
13
13
|
export ZEROCHECK_API=https://your-instance.example.com
|
|
14
14
|
npx zerocheck login
|
|
15
15
|
npx zerocheck init --project your-project-id --url http://localhost:3000
|
|
@@ -155,7 +155,7 @@ Use the installed CLI as a stdio MCP server, with an explicit project directory:
|
|
|
155
155
|
"mcpServers": {
|
|
156
156
|
"zerocheck": {
|
|
157
157
|
"command": "npx",
|
|
158
|
-
"args": ["--yes", "zerocheck@0.1.
|
|
158
|
+
"args": ["--yes", "zerocheck@0.1.8", "mcp", "--project-dir", "/absolute/path/to/project"]
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
}
|
|
@@ -173,7 +173,7 @@ Local execution sends selected check YAML, the page context needed for AI (inclu
|
|
|
173
173
|
|
|
174
174
|
## Repository development
|
|
175
175
|
|
|
176
|
-
The CLI bundles the shared engine source into `dist/index.js`; it has no unpublished engine-package dependency. From this repository, run `npm run build`, `npm run check`, and `npm test` in `packages/cli`. Tests include a real MCP client communicating with the bundled CLI over stdio. The generated workflow and documentation use version `0.1.
|
|
176
|
+
The CLI bundles the shared engine source into `dist/index.js`; it has no unpublished engine-package dependency. From this repository, run `npm run build`, `npm run check`, and `npm test` in `packages/cli`. Tests include a real MCP client communicating with the bundled CLI over stdio. The generated workflow and documentation use version `0.1.8`; update that pin together with the package version for a release.
|
|
177
177
|
|
|
178
178
|
Hosted runs and imports return a queued handle when browsers are occupied; CLI and MCP keep polling until they finish. The queue holds 20 waiting jobs for up to five minutes and never automatically resumes interrupted jobs after restart. Recent drafts is available in the web app’s Checks page.
|
|
179
179
|
|
package/dist/index.js
CHANGED
|
@@ -7006,7 +7006,7 @@ async function loginCommand() {
|
|
|
7006
7006
|
let device;
|
|
7007
7007
|
try {
|
|
7008
7008
|
device = await apiRequest("/v1/cli/auth/device", {
|
|
7009
|
-
body: { device_name: deviceName, client_version: "0.1.
|
|
7009
|
+
body: { device_name: deviceName, client_version: "0.1.8" },
|
|
7010
7010
|
authed: false
|
|
7011
7011
|
});
|
|
7012
7012
|
} catch (err) {
|
|
@@ -7761,6 +7761,32 @@ import { selectors } from "playwright";
|
|
|
7761
7761
|
|
|
7762
7762
|
// ../../src/agent/dom-operations.ts
|
|
7763
7763
|
var facts = {
|
|
7764
|
+
/** Bounded observation preparation, not an authored action. Never scroll a
|
|
7765
|
+
* hidden/clip axis: scrollIntoView would expose intentionally hidden content. */
|
|
7766
|
+
revealForObservation: (target) => {
|
|
7767
|
+
if (!target.isConnected) throw new Error("Text target detached during observation.");
|
|
7768
|
+
const win = target.ownerDocument.defaultView;
|
|
7769
|
+
const ancestors = [];
|
|
7770
|
+
for (let node = target; node; node = node.parentElement ?? node.getRootNode()?.host) {
|
|
7771
|
+
const style = win.getComputedStyle(node);
|
|
7772
|
+
if (style.display === "none" || style.visibility !== "visible" || Number(style.opacity) === 0 || style.contentVisibility === "hidden") return false;
|
|
7773
|
+
ancestors.push(node);
|
|
7774
|
+
}
|
|
7775
|
+
let moved = false;
|
|
7776
|
+
for (const node of ancestors.slice(1)) {
|
|
7777
|
+
if (["HTML", "BODY"].includes(node.tagName)) continue;
|
|
7778
|
+
const style = win.getComputedStyle(node), box = node.getBoundingClientRect(), rect = target.getBoundingClientRect();
|
|
7779
|
+
const left = box.left + node.clientLeft, top = box.top + node.clientTop;
|
|
7780
|
+
const x = rect.left < left ? rect.left - left : Math.max(0, rect.right - left - node.clientWidth);
|
|
7781
|
+
const y = rect.top < top ? rect.top - top : Math.max(0, rect.bottom - top - node.clientHeight);
|
|
7782
|
+
const beforeX = node.scrollLeft, beforeY = node.scrollTop;
|
|
7783
|
+
const scrollX = /^(auto|scroll)$/.test(style.overflowX) && node.scrollWidth > node.clientWidth;
|
|
7784
|
+
const scrollY = /^(auto|scroll)$/.test(style.overflowY) && node.scrollHeight > node.clientHeight;
|
|
7785
|
+
if (scrollX && x || scrollY && y) node.scrollTo({ left: beforeX + (scrollX ? x : 0), top: beforeY + (scrollY ? y : 0), behavior: "instant" });
|
|
7786
|
+
moved ||= node.scrollLeft !== beforeX || node.scrollTop !== beforeY;
|
|
7787
|
+
}
|
|
7788
|
+
return moved;
|
|
7789
|
+
},
|
|
7764
7790
|
/** Native geometry shared by text and embedded-frame observations. Partial
|
|
7765
7791
|
* clipping cannot establish a complete text node or a frame's contents. */
|
|
7766
7792
|
perceptibleBounds: (node, suppliedRect) => {
|
|
@@ -7820,7 +7846,20 @@ var facts = {
|
|
|
7820
7846
|
const range = doc.createRange();
|
|
7821
7847
|
range.selectNodeContents(node);
|
|
7822
7848
|
const rects = Array.from(range.getClientRects());
|
|
7823
|
-
const
|
|
7849
|
+
const parent = node.parentElement, style = win.getComputedStyle(parent);
|
|
7850
|
+
const lineHeight = parseFloat(style.lineHeight), fontSize = parseFloat(style.fontSize);
|
|
7851
|
+
let plain = rects.length === 1 && parent.childElementCount === 0 && style.writingMode === "horizontal-tb" && style.clip === "auto" && style.clipPath === "none" && lineHeight >= fontSize;
|
|
7852
|
+
for (let owner = parent; plain && owner; owner = owner.parentElement ?? owner.getRootNode()?.host) {
|
|
7853
|
+
const outer = win.getComputedStyle(owner);
|
|
7854
|
+
if (outer.transform !== "none" || outer.zoom !== "1") plain = false;
|
|
7855
|
+
}
|
|
7856
|
+
const box = parent.getBoundingClientRect();
|
|
7857
|
+
const contentTop = box.top + parent.clientTop + parseFloat(style.paddingTop);
|
|
7858
|
+
const contentHeight = parent.clientHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom);
|
|
7859
|
+
const states = rects.map((rect) => nativeFacts.perceptibleBounds(
|
|
7860
|
+
parent,
|
|
7861
|
+
plain && contentHeight >= lineHeight && rect.height >= lineHeight ? { left: rect.left, right: rect.right, top: Math.max(rect.top, contentTop), bottom: Math.min(rect.bottom, contentTop + contentHeight) } : rect
|
|
7862
|
+
));
|
|
7824
7863
|
if (states.includes("partial") || states.includes("visible") && states.includes("hidden")) throw new Error("Partly clipped text is inconclusive.");
|
|
7825
7864
|
if (states.length && states.every((state) => state === "visible")) parts.push(node.textContent);
|
|
7826
7865
|
return;
|
|
@@ -8070,6 +8109,7 @@ var facts = {
|
|
|
8070
8109
|
}
|
|
8071
8110
|
return "";
|
|
8072
8111
|
},
|
|
8112
|
+
focusWrapper: (element) => element.hasAttribute("tabindex") && !element.isContentEditable && !element.matches('button,a[href],input,select,textarea,summary,[role="button"],[role="link"],[role="checkbox"],[role="radio"],[role="switch"],[role="tab"],[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"],[role="option"],[role="combobox"],[role="textbox"],[role="searchbox"],[role="spinbutton"]') && !element.hasAttribute("onclick"),
|
|
8073
8113
|
decorations: (element) => {
|
|
8074
8114
|
const el = element;
|
|
8075
8115
|
const values = [];
|
|
@@ -8135,6 +8175,13 @@ var facts = {
|
|
|
8135
8175
|
mutationPossible: false
|
|
8136
8176
|
};
|
|
8137
8177
|
},
|
|
8178
|
+
ordinalCollectionLabel: (element) => {
|
|
8179
|
+
const item = element.closest('li,[role="listitem"],tr,[role="row"],[role="option"],[role="treeitem"]');
|
|
8180
|
+
const collection = item?.parentElement?.closest('ol,ul,table,[role="list"],[role="grid"],[role="table"],[role="listbox"],[role="tree"]');
|
|
8181
|
+
if (!collection) return "";
|
|
8182
|
+
const ids = String(collection.getAttribute("aria-labelledby") || "").trim().split(/\s+/).filter(Boolean);
|
|
8183
|
+
return (ids.length ? ids.map((id) => collection.ownerDocument.getElementById(id)?.textContent || "").join(" ") : collection.getAttribute("aria-label") || "").replace(/\s+/g, " ").trim();
|
|
8184
|
+
},
|
|
8138
8185
|
progressState: (element) => {
|
|
8139
8186
|
const el = element;
|
|
8140
8187
|
return {
|
|
@@ -8148,30 +8195,70 @@ var facts = {
|
|
|
8148
8195
|
progressBoundary: (element) => element.matches('form,article,aside,nav,header,footer,tr,li,[role="form"],[role="dialog"],[role="region"],[role="row"],[role="listitem"],[role="complementary"],[role="navigation"],[role="banner"],[role="contentinfo"],[data-record-id],[data-row-id],[data-entity-id],[data-item-id],[aria-label],[aria-labelledby]'),
|
|
8149
8196
|
progressView: (scope, _input, nativeFacts) => {
|
|
8150
8197
|
if (!scope.isConnected) throw new Error("The active view detached during progress observation.");
|
|
8151
|
-
const
|
|
8152
|
-
|
|
8153
|
-
|
|
8198
|
+
const unknown2 = { fields: [], fieldless: false, ready: false };
|
|
8199
|
+
const owner = nativeFacts.readiness(scope, { ownSelectAX: false });
|
|
8200
|
+
if (!owner.active || owner.moving) return unknown2;
|
|
8201
|
+
const ownership = { owns(node) {
|
|
8154
8202
|
for (let parent = node.parentElement; parent && parent !== scope; parent = parent.parentElement) {
|
|
8155
|
-
if (nativeFacts.progressBoundary(parent))
|
|
8156
|
-
foreignOwner = true;
|
|
8157
|
-
break;
|
|
8158
|
-
}
|
|
8203
|
+
if (nativeFacts.progressBoundary(parent)) return false;
|
|
8159
8204
|
}
|
|
8160
|
-
|
|
8205
|
+
return scope.contains(node);
|
|
8206
|
+
} };
|
|
8207
|
+
const fields = [];
|
|
8208
|
+
let fieldCount = 0;
|
|
8209
|
+
for (const node of scope.querySelectorAll('input:not([type="hidden"],[type="submit"],[type="button"],[type="reset"]),select,textarea,[contenteditable=""],[contenteditable="true" i],[contenteditable="plaintext-only" i],[role="textbox"],[role="searchbox"],[role="spinbutton"],[role="combobox"],[role="checkbox"],[role="radio"],[role="switch"]')) {
|
|
8210
|
+
if (!ownership.owns(node)) continue;
|
|
8161
8211
|
const readiness = nativeFacts.readiness(node, { ownSelectAX: false });
|
|
8162
|
-
if (readiness.moving) return
|
|
8212
|
+
if (readiness.moving) return unknown2;
|
|
8163
8213
|
if (!readiness.active) continue;
|
|
8164
8214
|
const visibility = nativeFacts.perceptibleBounds(node);
|
|
8165
|
-
if (visibility === "partial") return
|
|
8215
|
+
if (visibility === "partial") return unknown2;
|
|
8166
8216
|
if (visibility !== "visible") continue;
|
|
8217
|
+
fieldCount++;
|
|
8167
8218
|
const identity = nativeFacts.identity(node);
|
|
8168
|
-
if (
|
|
8169
|
-
|
|
8219
|
+
if (identity?.name) fields.push(JSON.stringify([identity.role, identity.name.replace(/\s+/g, " ").trim(), identity.inputType]));
|
|
8220
|
+
}
|
|
8221
|
+
const headings = [];
|
|
8222
|
+
const status = '[role="alert"],[role="status"],[role="log"],[role="timer"],[role="marquee"],[aria-live]:not([aria-live="off"])';
|
|
8223
|
+
for (const node of scope.querySelectorAll('h1,h2,h3,h4,h5,h6,[role="heading"]')) {
|
|
8224
|
+
if (!ownership.owns(node) || node.parentElement?.closest(status)) continue;
|
|
8225
|
+
const readiness = nativeFacts.readiness(node, { ownSelectAX: false });
|
|
8226
|
+
if (!readiness.active && !readiness.moving) continue;
|
|
8227
|
+
const visibility = nativeFacts.perceptibleBounds(node);
|
|
8228
|
+
if (visibility === "hidden" && !readiness.moving) continue;
|
|
8229
|
+
const eligible = !node.matches(status) && !node.querySelector(status);
|
|
8230
|
+
let text = "", ready = !readiness.moving && visibility === "visible";
|
|
8231
|
+
if (eligible && ready) {
|
|
8232
|
+
try {
|
|
8233
|
+
text = nativeFacts.perceptibleText(node, void 0, nativeFacts).replace(/\s+/g, " ").trim();
|
|
8234
|
+
} catch (error2) {
|
|
8235
|
+
if (error2 instanceof Error && /clipp/.test(error2.message)) ready = false;
|
|
8236
|
+
else throw error2;
|
|
8237
|
+
}
|
|
8238
|
+
}
|
|
8239
|
+
headings.push({ node, level: Number(node.getAttribute("aria-level") || node.localName.match(/^h([1-6])$/)?.[1] || 2), text, eligible, ready });
|
|
8170
8240
|
}
|
|
8171
|
-
|
|
8241
|
+
const labelled = String(scope.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean);
|
|
8242
|
+
const explicit = headings.filter((heading) => labelled.includes(heading.node.id));
|
|
8243
|
+
const principal = labelled.length ? explicit : headings.filter((heading) => heading.level === Math.min(...headings.map((candidate) => candidate.level)));
|
|
8244
|
+
const chosen = principal.length === 1 && principal[0].eligible ? principal[0] : void 0;
|
|
8245
|
+
return { fields, fieldless: fieldCount === 0, ready: chosen?.ready ?? true, ...chosen?.ready && chosen.text ? { heading: chosen.text, headingLevel: chosen.level } : {} };
|
|
8172
8246
|
}
|
|
8173
8247
|
};
|
|
8174
8248
|
var elements = {
|
|
8249
|
+
actionDescendants: (element) => Array.from(element.querySelectorAll('button,a[href],input,select,textarea,summary,[contenteditable],[role="button"],[role="link"],[role="checkbox"],[role="radio"],[role="switch"],[role="tab"],[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"],[role="option"],[role="combobox"],[role="textbox"],[role="searchbox"],[role="spinbutton"],[tabindex],[onclick]')),
|
|
8250
|
+
ordinalItems: (element) => {
|
|
8251
|
+
const itemSelector = 'li,[role="listitem"],tr,[role="row"],[role="option"],[role="treeitem"]';
|
|
8252
|
+
const collectionSelector = 'ol,ul,table,[role="list"],[role="grid"],[role="table"],[role="listbox"],[role="tree"]';
|
|
8253
|
+
const item = element.closest(itemSelector);
|
|
8254
|
+
const collection = item?.parentElement?.closest(collectionSelector);
|
|
8255
|
+
if (!collection) return [];
|
|
8256
|
+
return Array.from(collection.querySelectorAll(itemSelector)).filter((node) => {
|
|
8257
|
+
if (node.parentElement?.closest(collectionSelector) !== collection) return false;
|
|
8258
|
+
for (let parent = node.parentElement; parent && parent !== collection; parent = parent.parentElement) if (parent.matches(itemSelector)) return false;
|
|
8259
|
+
return true;
|
|
8260
|
+
});
|
|
8261
|
+
},
|
|
8175
8262
|
containingLabel: (control) => control.isConnected ? control.closest("label") : null,
|
|
8176
8263
|
selects: (root) => {
|
|
8177
8264
|
if (root.localName === "form") return Array.from(root.elements).filter((node) => node.localName === "select");
|
|
@@ -8245,7 +8332,7 @@ var elements = {
|
|
|
8245
8332
|
const root = el.getRootNode();
|
|
8246
8333
|
return form ? Array.from((root.documentElement ?? root).querySelectorAll("button,input")).find((control) => control.form === form && ["submit", "image"].includes(control.type)) ?? null : null;
|
|
8247
8334
|
},
|
|
8248
|
-
progressScope: (element) => {
|
|
8335
|
+
progressScope: (element, _input, nativeFacts) => {
|
|
8249
8336
|
if (element.form && !element.form.contains(element)) return element.form;
|
|
8250
8337
|
let fieldScope = null;
|
|
8251
8338
|
for (let node = element.parentElement; node && !["BODY", "HTML"].includes(node.tagName); node = node.parentElement) {
|
|
@@ -8253,6 +8340,10 @@ var elements = {
|
|
|
8253
8340
|
if (node.hasAttribute("aria-label") || node.hasAttribute("aria-labelledby")) return fieldScope ?? node;
|
|
8254
8341
|
if (node.querySelector(':scope > h1,:scope > h2,:scope > h3,:scope > h4,:scope > [role="heading"],:scope > header > h1,:scope > header > h2,:scope > header > h3')) return fieldScope ?? node;
|
|
8255
8342
|
if (!fieldScope && node.querySelector('input,select,textarea,[role="textbox"],[role="combobox"],[role="checkbox"],[role="radio"]')) fieldScope = node;
|
|
8343
|
+
if (Array.from(node.querySelectorAll('h1,h2,h3,h4,h5,h6,[role="heading"]')).some((heading) => {
|
|
8344
|
+
for (let parent = heading.parentElement; parent && parent !== node; parent = parent.parentElement) if (nativeFacts.progressBoundary(parent)) return false;
|
|
8345
|
+
return true;
|
|
8346
|
+
})) return fieldScope ?? node;
|
|
8256
8347
|
}
|
|
8257
8348
|
return fieldScope;
|
|
8258
8349
|
},
|
|
@@ -8600,7 +8691,11 @@ var TargetProgress = class {
|
|
|
8600
8691
|
if (await read(() => signature(handle)) !== prior.signature) return;
|
|
8601
8692
|
if (prior.scope && await read(() => readDOM(prior.scope, "connected"))) {
|
|
8602
8693
|
const current = await read(() => readDOM(prior.scope, "progressView"));
|
|
8603
|
-
if (prior.view.
|
|
8694
|
+
if (prior.view.ready && current.ready) {
|
|
8695
|
+
const fieldsChanged = prior.view.fields.some((control) => !current.fields.includes(control)) && current.fields.some((control) => !prior.view.fields.includes(control));
|
|
8696
|
+
const fieldlessTransition = (prior.view.fieldless || current.fieldless) && prior.view.heading && current.heading && prior.view.headingLevel === current.headingLevel && prior.view.heading !== current.heading;
|
|
8697
|
+
if (fieldsChanged || fieldlessTransition) return;
|
|
8698
|
+
}
|
|
8604
8699
|
}
|
|
8605
8700
|
await delay(Math.min(50, Math.max(1, deadline - Date.now())), void 0, { signal });
|
|
8606
8701
|
}
|
|
@@ -8611,7 +8706,7 @@ var TargetProgress = class {
|
|
|
8611
8706
|
try {
|
|
8612
8707
|
const scopeValue = await selectDOM(handle, "progressScope");
|
|
8613
8708
|
scope = scopeValue ?? void 0;
|
|
8614
|
-
return { handle: duplicate, ...scope ? { scope } : {}, signature: await signature(handle), view: scope ? await readDOM(scope, "progressView") : [], lineNumber, entity };
|
|
8709
|
+
return { handle: duplicate, ...scope ? { scope } : {}, signature: await signature(handle), view: scope ? await readDOM(scope, "progressView") : { fields: [], fieldless: false, ready: false }, lineNumber, entity };
|
|
8615
8710
|
} catch (error2) {
|
|
8616
8711
|
await duplicate.dispose();
|
|
8617
8712
|
await scope?.dispose();
|
|
@@ -16993,6 +17088,7 @@ async function waitForVisibleText(page, assertion, timeoutMs, signal, targetPoli
|
|
|
16993
17088
|
for (let current = frame; current.parentFrame(); current = current.parentFrame()) {
|
|
16994
17089
|
const owner = await read(() => current.frameElement());
|
|
16995
17090
|
try {
|
|
17091
|
+
await read(() => readDOM(owner, "revealForObservation"));
|
|
16996
17092
|
const state = await read(() => readDOM(owner, "perceptibleBounds"));
|
|
16997
17093
|
if (state === "hidden") {
|
|
16998
17094
|
hiddenOwner = true;
|
|
@@ -17015,6 +17111,7 @@ async function waitForVisibleText(page, assertion, timeoutMs, signal, targetPoli
|
|
|
17015
17111
|
const handle = await read(() => locator.nth(index).elementHandle());
|
|
17016
17112
|
if (!handle) throw new Error("Text target detached during observation.");
|
|
17017
17113
|
try {
|
|
17114
|
+
await read(() => readDOM(handle, "revealForObservation"));
|
|
17018
17115
|
const text = await read(() => readDOM(handle, "perceptibleText"));
|
|
17019
17116
|
if (assertion.match === "phrase" ? phrasePattern(assertion.text).test(text) : text === assertion.text.replace(/\s+/g, " ").trim()) {
|
|
17020
17117
|
found = true;
|
|
@@ -17927,7 +18024,13 @@ async function bindIntendedTarget(handle, step, identity, policy) {
|
|
|
17927
18024
|
if (!frame) throw new TargetChangedError("Target detached before authored binding.");
|
|
17928
18025
|
const authored = targetInstruction(step);
|
|
17929
18026
|
const explicitSelector = explicitRoleTarget(authored.replace(/^(["'])(.*)\1$/, "$2"));
|
|
17930
|
-
|
|
18027
|
+
let instruction = explicitSelector ? JSON.stringify(explicitSelector.name) : authored;
|
|
18028
|
+
let tiers;
|
|
18029
|
+
if (hasAuthoredOrdinal(instruction) && literalTargetName(instruction) === void 0) {
|
|
18030
|
+
tiers = await bindingFrames(frame, policy);
|
|
18031
|
+
const whole = normalizeTargetName(instruction);
|
|
18032
|
+
if (await observedLiteralName(tiers, whole, step)) instruction = JSON.stringify(whole);
|
|
18033
|
+
}
|
|
17931
18034
|
const fullName = normalizeTargetName(instruction);
|
|
17932
18035
|
const localName = !identity.name && identity.textEntry ? await readDOM(handle, "localLabel") : "";
|
|
17933
18036
|
const name = (identity.name || localName).trim().replace(/\s+/g, " ").toLowerCase();
|
|
@@ -17946,12 +18049,20 @@ async function bindIntendedTarget(handle, step, identity, policy) {
|
|
|
17946
18049
|
const role = ["field", "input", "box"].includes(authoredRole ?? "") ? "textbox" : authoredRole === "dropdown" ? "combobox" : authoredRole ?? (step.keyword === "enter" ? "textbox" : step.keyword === "select" ? "combobox" : void 0);
|
|
17947
18050
|
const clickFamily = !explicitSelector && step.keyword === "click" && ["button", "link"].includes(role ?? "");
|
|
17948
18051
|
if (!targetRolesMatch(role, identity, clickFamily)) throw new SelectedTargetMismatchError("Ambiguous target: the authored control role does not match.");
|
|
17949
|
-
|
|
18052
|
+
let authoredName = anchored?.name ?? literalTargetName(qualifier?.rawControl ?? instruction);
|
|
18053
|
+
if (ordinal && authoredName === void 0) {
|
|
18054
|
+
tiers ??= await bindingFrames(frame, policy);
|
|
18055
|
+
if (await observedLiteralName(tiers, control, step, role)) authoredName = control;
|
|
18056
|
+
}
|
|
17950
18057
|
const literal3 = authoredName !== void 0 || control === name;
|
|
17951
|
-
if (ordinal && !literal3)
|
|
18058
|
+
if (ordinal && !literal3) await assertDescriptiveOrder(handle, ordinal, identity.role, control);
|
|
17952
18059
|
const controlName = roleOnly ? void 0 : authoredName;
|
|
17953
18060
|
if (!literal3 && !roleOnly && step.keyword !== "act" && scanQuotedText(instruction).spans.length > 0) throw new SelectedTargetMismatchError("Ambiguous target: the authored control name does not match.");
|
|
17954
|
-
if (!literal3 && !roleOnly && !qualifier)
|
|
18061
|
+
if (!literal3 && !roleOnly && !qualifier) {
|
|
18062
|
+
const unbound = { ...identity };
|
|
18063
|
+
delete unbound.binding;
|
|
18064
|
+
return unbound;
|
|
18065
|
+
}
|
|
17955
18066
|
const observedName = authoredName ?? (identity.name.includes("[redacted]") ? (await readTargetIdentity(handle, [], Number.MAX_SAFE_INTEGER)).name : identity.name || localName);
|
|
17956
18067
|
const escapedName = observedName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17957
18068
|
const queryName = new RegExp(`^[\\s\uE000-\uF8FF\u2190-\u21FF\u27F0-\u27FF\u2900-\u297F]*${escapedName}[\\s\uE000-\uF8FF\u2190-\u21FF\u27F0-\u27FF\u2900-\u297F]*$`, "i");
|
|
@@ -17961,28 +18072,7 @@ async function bindIntendedTarget(handle, step, identity, policy) {
|
|
|
17961
18072
|
let selected = false;
|
|
17962
18073
|
let rejected = false;
|
|
17963
18074
|
let ordered = 0;
|
|
17964
|
-
const primary =
|
|
17965
|
-
const requiredFrames = [];
|
|
17966
|
-
for (let current = frame; current; current = current.parentFrame()) requiredFrames.push(current);
|
|
17967
|
-
const topOrigin = policy ? await frameOrigin(frame.page().mainFrame()) : void 0;
|
|
17968
|
-
for (const candidateFrame of policy || anchored ? frame.page().frames() : [frame]) {
|
|
17969
|
-
try {
|
|
17970
|
-
if (!await isFrameVisible(candidateFrame)) continue;
|
|
17971
|
-
} catch (error2) {
|
|
17972
|
-
if (!(error2 instanceof NativeFrameDetachedError)) throw error2;
|
|
17973
|
-
if (requiredFrames.some((required) => required.isDetached())) throw new TargetChangedError("The selected target frame detached before authored binding.");
|
|
17974
|
-
continue;
|
|
17975
|
-
}
|
|
17976
|
-
if (policy && (await blockedFrameOrigins(candidateFrame, policy)).length) continue;
|
|
17977
|
-
let sameChain = true;
|
|
17978
|
-
if (policy) {
|
|
17979
|
-
for (let ancestor = candidateFrame; ancestor; ancestor = ancestor.parentFrame()) if (await frameOrigin(ancestor) !== topOrigin) {
|
|
17980
|
-
sameChain = false;
|
|
17981
|
-
break;
|
|
17982
|
-
}
|
|
17983
|
-
}
|
|
17984
|
-
(sameChain ? primary : embedded).push(candidateFrame);
|
|
17985
|
-
}
|
|
18075
|
+
const [primary, embedded] = tiers ?? await bindingFrames(frame, policy, Boolean(anchored));
|
|
17986
18076
|
for (const tier of [primary, embedded]) {
|
|
17987
18077
|
for (const candidateFrame of tier) {
|
|
17988
18078
|
const nativeRole = ["button", "link", "textbox", "combobox", "checkbox", "radio", "switch", "tab", "menuitem", "menuitemcheckbox", "menuitemradio", "option", "searchbox", "spinbutton", "img"].includes(identity.role);
|
|
@@ -18018,6 +18108,7 @@ async function bindIntendedTarget(handle, step, identity, policy) {
|
|
|
18018
18108
|
const isSelected = candidateFrame === frame && await candidate.evaluate((element, original) => element === original, handle);
|
|
18019
18109
|
if (match === "conflict" && !isSelected) continue;
|
|
18020
18110
|
if (literal3 && !roleOnly && !await nativeNameMatches(candidate, observedName, identity.role, Boolean(identity.textEntry))) continue;
|
|
18111
|
+
if (literal3 && step.keyword === "click" && await isDuplicateFocusWrapper(candidate, observedName)) continue;
|
|
18021
18112
|
if (match === "conflict") {
|
|
18022
18113
|
rejected = true;
|
|
18023
18114
|
continue;
|
|
@@ -18076,6 +18167,96 @@ async function nativeNameMatches(handle, expected, role, textEntry) {
|
|
|
18076
18167
|
}
|
|
18077
18168
|
return normalize(name) === normalize(expected);
|
|
18078
18169
|
}
|
|
18170
|
+
async function observedLiteralName(tiers, name, step, role) {
|
|
18171
|
+
if (!name) return false;
|
|
18172
|
+
for (const tier of tiers) for (const frame of tier) {
|
|
18173
|
+
const candidates = await frame.locator('button,a[href],input,select,textarea,summary,[contenteditable],[role="button"],[role="link"],[role="checkbox"],[role="radio"],[role="switch"],[role="tab"],[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"],[role="option"],[role="combobox"],[role="textbox"],[role="searchbox"],[role="spinbutton"],[tabindex],[onclick]').elementHandles();
|
|
18174
|
+
try {
|
|
18175
|
+
for (const candidate of candidates) {
|
|
18176
|
+
if (!(await targetReadiness(candidate)).active) continue;
|
|
18177
|
+
const identity = await readTargetIdentity(candidate);
|
|
18178
|
+
if (step.keyword === "click" && !identity.interactive || step.keyword === "enter" && !identity.textEntry || step.keyword === "select" && identity.role !== "combobox") continue;
|
|
18179
|
+
if (!targetRolesMatch(role, identity, step.keyword === "click" && ["button", "link"].includes(role ?? ""))) continue;
|
|
18180
|
+
if (await nativeNameMatches(candidate, name, identity.role, Boolean(identity.textEntry)) && !await isDuplicateFocusWrapper(candidate, name)) return true;
|
|
18181
|
+
}
|
|
18182
|
+
} finally {
|
|
18183
|
+
await Promise.all(candidates.map((candidate) => candidate.dispose()));
|
|
18184
|
+
}
|
|
18185
|
+
}
|
|
18186
|
+
return false;
|
|
18187
|
+
}
|
|
18188
|
+
async function focusWrapperWithoutDeclaredActivation(handle) {
|
|
18189
|
+
return await readDOM(handle, "focusWrapper") && !await handle.evaluate((element) => typeof element.onclick === "function");
|
|
18190
|
+
}
|
|
18191
|
+
async function isDuplicateFocusWrapper(handle, name) {
|
|
18192
|
+
if (!await focusWrapperWithoutDeclaredActivation(handle)) return false;
|
|
18193
|
+
const descendants = await selectAllDOM(handle, "actionDescendants");
|
|
18194
|
+
let matches = 0;
|
|
18195
|
+
try {
|
|
18196
|
+
for (const child of descendants) {
|
|
18197
|
+
if (!(await targetReadiness(child)).active || await focusWrapperWithoutDeclaredActivation(child)) continue;
|
|
18198
|
+
const identity = await readTargetIdentity(child);
|
|
18199
|
+
if (identity.interactive && await nativeNameMatches(child, name, identity.role, Boolean(identity.textEntry))) matches++;
|
|
18200
|
+
}
|
|
18201
|
+
return matches === 1;
|
|
18202
|
+
} finally {
|
|
18203
|
+
await Promise.all(descendants.map((child) => child.dispose()));
|
|
18204
|
+
}
|
|
18205
|
+
}
|
|
18206
|
+
async function assertDescriptiveOrder(handle, ordinal, role, description) {
|
|
18207
|
+
const label = await readDOM(handle, "ordinalCollectionLabel");
|
|
18208
|
+
if (!label || label.trim().replace(/\s+/g, " ").toLowerCase() !== description) return;
|
|
18209
|
+
const items = await selectAllDOM(handle, "ordinalItems");
|
|
18210
|
+
let count = 0;
|
|
18211
|
+
let selected = 0;
|
|
18212
|
+
try {
|
|
18213
|
+
for (const item of items) {
|
|
18214
|
+
if (!(await targetReadiness(item)).active) continue;
|
|
18215
|
+
const descendants = await selectAllDOM(item, "actionDescendants");
|
|
18216
|
+
const matches = [];
|
|
18217
|
+
try {
|
|
18218
|
+
for (const candidate of [item, ...descendants]) {
|
|
18219
|
+
if (!(await targetReadiness(candidate)).active || await focusWrapperWithoutDeclaredActivation(candidate)) continue;
|
|
18220
|
+
const identity = await readTargetIdentity(candidate);
|
|
18221
|
+
if (identity.role === role && identity.interactive) matches.push(candidate);
|
|
18222
|
+
}
|
|
18223
|
+
if (matches.length !== 1) return;
|
|
18224
|
+
count++;
|
|
18225
|
+
if (await sameDOMNode(matches[0], handle)) selected = count;
|
|
18226
|
+
} finally {
|
|
18227
|
+
await Promise.all(descendants.map((candidate) => candidate.dispose()));
|
|
18228
|
+
}
|
|
18229
|
+
}
|
|
18230
|
+
if (selected && selected !== ordinal) throw new SelectedTargetMismatchError("Ambiguous target: the selected control contradicts the authored ordinal in its collection.");
|
|
18231
|
+
} finally {
|
|
18232
|
+
await Promise.all(items.map((item) => item.dispose()));
|
|
18233
|
+
}
|
|
18234
|
+
}
|
|
18235
|
+
async function bindingFrames(frame, policy, acrossFrames = false) {
|
|
18236
|
+
const primary = [], embedded = [];
|
|
18237
|
+
const requiredFrames = [];
|
|
18238
|
+
for (let current = frame; current; current = current.parentFrame()) requiredFrames.push(current);
|
|
18239
|
+
const topOrigin = policy ? await frameOrigin(frame.page().mainFrame()) : void 0;
|
|
18240
|
+
for (const candidateFrame of policy || acrossFrames ? frame.page().frames() : [frame]) {
|
|
18241
|
+
try {
|
|
18242
|
+
if (!await isFrameVisible(candidateFrame)) continue;
|
|
18243
|
+
} catch (error2) {
|
|
18244
|
+
if (!(error2 instanceof NativeFrameDetachedError)) throw error2;
|
|
18245
|
+
if (requiredFrames.some((required) => required.isDetached())) throw new TargetChangedError("The selected target frame detached before authored binding.");
|
|
18246
|
+
continue;
|
|
18247
|
+
}
|
|
18248
|
+
if (policy && (await blockedFrameOrigins(candidateFrame, policy)).length) continue;
|
|
18249
|
+
let sameChain = true;
|
|
18250
|
+
if (policy) {
|
|
18251
|
+
for (let ancestor = candidateFrame; ancestor; ancestor = ancestor.parentFrame()) if (await frameOrigin(ancestor) !== topOrigin) {
|
|
18252
|
+
sameChain = false;
|
|
18253
|
+
break;
|
|
18254
|
+
}
|
|
18255
|
+
}
|
|
18256
|
+
(sameChain ? primary : embedded).push(candidateFrame);
|
|
18257
|
+
}
|
|
18258
|
+
return [primary, embedded];
|
|
18259
|
+
}
|
|
18079
18260
|
|
|
18080
18261
|
// ../../src/policy/action-safety.ts
|
|
18081
18262
|
function evaluateActionSafety(input) {
|
|
@@ -20924,7 +21105,12 @@ async function draftItem(item, request, options, draftId, earlier = []) {
|
|
|
20924
21105
|
const aligned = assertionIndices.length === explicit.length && explicit.every((outcome, index) => {
|
|
20925
21106
|
const at = assertionIndices[index];
|
|
20926
21107
|
const proposed = parseStep(steps[at], at + 1);
|
|
20927
|
-
|
|
21108
|
+
try {
|
|
21109
|
+
validateOutcomeStep(outcome);
|
|
21110
|
+
} catch {
|
|
21111
|
+
return false;
|
|
21112
|
+
}
|
|
21113
|
+
return proposed.keyword === outcome.keyword && (outcome.source.final ? at === steps.length - 1 : checkpointAligned(outcome, steps, at));
|
|
20928
21114
|
});
|
|
20929
21115
|
if (aligned) {
|
|
20930
21116
|
explicit.forEach((outcome, index) => {
|
|
@@ -21017,15 +21203,6 @@ function sourceClauses(text) {
|
|
|
21017
21203
|
return { start, boundary, end, raw };
|
|
21018
21204
|
});
|
|
21019
21205
|
}
|
|
21020
|
-
function equivalentOutcome(expected, proposed, bareText = false) {
|
|
21021
|
-
try {
|
|
21022
|
-
const a = parseVisibleTextAssertion(expected, bareText), b = parseVisibleTextAssertion(proposed, bareText);
|
|
21023
|
-
if (a && b) return JSON.stringify(a) === JSON.stringify(b);
|
|
21024
|
-
} catch {
|
|
21025
|
-
return false;
|
|
21026
|
-
}
|
|
21027
|
-
return expected.replace(/\s+/g, " ").replace(/[.!]$/, "").trim() === proposed.replace(/\s+/g, " ").replace(/[.!]$/, "").trim();
|
|
21028
|
-
}
|
|
21029
21206
|
function actionIdentity(raw) {
|
|
21030
21207
|
const step = parseStep(raw.replace(/[.!]$/, ""), 0);
|
|
21031
21208
|
if (step.keyword === "act" || isOutcomeStep(step)) return void 0;
|
|
@@ -22243,7 +22420,7 @@ jobs:
|
|
|
22243
22420
|
node-version: '22'
|
|
22244
22421
|
- name: Install Zerocheck
|
|
22245
22422
|
run: |
|
|
22246
|
-
npm install --prefix "$RUNNER_TEMP/zerocheck-cli" --no-save --package-lock=false zerocheck@0.1.
|
|
22423
|
+
npm install --prefix "$RUNNER_TEMP/zerocheck-cli" --no-save --package-lock=false zerocheck@0.1.8
|
|
22247
22424
|
echo "$RUNNER_TEMP/zerocheck-cli/node_modules/.bin" >> "$GITHUB_PATH"
|
|
22248
22425
|
- name: Restore learned targets
|
|
22249
22426
|
uses: actions/cache@v4
|
|
@@ -28263,7 +28440,7 @@ var runner = z2.enum(["local", "hosted"]).default("local").describe("Local brows
|
|
|
28263
28440
|
var output = (value) => ({ content: [{ type: "text", text: JSON.stringify(value) }], structuredContent: { result: value } });
|
|
28264
28441
|
var error = (value) => ({ isError: true, content: [{ type: "text", text: JSON.stringify({ error: "operation_failed", message: value instanceof Error ? value.message : String(value) }) }] });
|
|
28265
28442
|
function createMcpServer(services) {
|
|
28266
|
-
const server = new McpServer({ name: "zerocheck", version: "0.1.
|
|
28443
|
+
const server = new McpServer({ name: "zerocheck", version: "0.1.8" });
|
|
28267
28444
|
server.registerTool("list_checks", {
|
|
28268
28445
|
description: "List the repository YAML checks, IDs, exact contents and revisions. Does not execute them.",
|
|
28269
28446
|
inputSchema: { paths: z2.array(z2.string()).optional(), environment },
|
|
@@ -28403,7 +28580,7 @@ async function mcpCommand(options) {
|
|
|
28403
28580
|
}
|
|
28404
28581
|
|
|
28405
28582
|
// src/index.ts
|
|
28406
|
-
var program = new Command().name("zerocheck").version("0.1.
|
|
28583
|
+
var program = new Command().name("zerocheck").version("0.1.8").description("Turn your team\u2019s manual release checklist into repeatable browser tests.");
|
|
28407
28584
|
var directory = (command) => command.option("--project-dir <path>", "Repository project directory", process.cwd());
|
|
28408
28585
|
var environment2 = (command) => directory(command).addOption(new Option("--env <environment>", "Named project environment").choices(["dev", "staging", "production"]));
|
|
28409
28586
|
var execution = (command) => environment2(command).addOption(new Option("--runner <runner>", "Browser execution location").choices(["local", "hosted"]).default("local"));
|