zerocheck 0.1.7 → 0.1.9
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 +72 -128
- package/dist/index.js +234 -57
- package/package.json +22 -2
package/README.md
CHANGED
|
@@ -1,180 +1,124 @@
|
|
|
1
1
|
# Zerocheck
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Repeatable browser tests from your release checklist**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Zerocheck turns your team's manual release checklist into saved end-to-end browser tests. Start with the important user journeys, review the test definitions and browser results, and rerun the checks as your app changes.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
This is the Zerocheck CLI. Use it for repository tests on your laptop or in CI, or connect a coding agent through MCP.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**New to Zerocheck? [Request pilot access](https://tryzerocheck.com/demo/).** We provision a customer instance and help you configure your first handful of tests during hands-on onboarding.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
npm install --save-dev --save-exact zerocheck@0.1.7
|
|
13
|
-
export ZEROCHECK_API=https://your-instance.example.com
|
|
14
|
-
npx zerocheck login
|
|
15
|
-
npx zerocheck init --project your-project-id --url http://localhost:3000
|
|
16
|
-
npx zerocheck install
|
|
17
|
-
```
|
|
11
|
+
**Already onboarded?** [Run your saved tests](#run-your-saved-tests), or open the instance URL supplied during onboarding to use the web app.
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
## Before you start
|
|
20
14
|
|
|
21
|
-
|
|
15
|
+
Test execution requires a Zerocheck account, a provisioned customer instance and a project in that instance. **There is no default public service.**
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```sh
|
|
26
|
-
npx zerocheck import release-checklist.md --env dev
|
|
27
|
-
cat release-checklist.md | npx zerocheck import - --env staging
|
|
28
|
-
npx zerocheck import --env dev
|
|
29
|
-
```
|
|
17
|
+
For the CLI, have these ready:
|
|
30
18
|
|
|
31
|
-
|
|
19
|
+
- **Node.js 20.19 or newer.**
|
|
20
|
+
- **Your instance URL** and account access, supplied during onboarding.
|
|
21
|
+
- **Your project files:** `zerocheck.yaml` with the correct project, environment and app URL, plus at least one saved YAML test in its configured test directory. We set up these files with you during onboarding.
|
|
22
|
+
- **A running app** reachable from your machine, with the test accounts, fixtures and credential variables your checks need.
|
|
32
23
|
|
|
33
|
-
|
|
34
|
-
- **Check failed:** the requested outcome failed. Review the check and captured result; this alone does not diagnose a product bug.
|
|
35
|
-
- **Needs input:** an essential outcome, account, route or fixture is missing, or the item still needs a browser-executable interpretation.
|
|
36
|
-
- **Could not run:** execution could not complete; inspect the recorded reason.
|
|
24
|
+
Local execution uses Zerocheck's AI service and uploads selected test definitions, page context and run evidence. It requires network access and authentication even though the browser runs on your machine.
|
|
37
25
|
|
|
38
|
-
|
|
26
|
+
## Run your saved tests
|
|
39
27
|
|
|
40
|
-
|
|
28
|
+
Run from the app repository containing the project files above. Replace the instance URL with the one you received during onboarding.
|
|
41
29
|
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
|
|
30
|
+
```bash
|
|
31
|
+
npm install --save-dev --save-exact zerocheck@0.1.9
|
|
32
|
+
export ZEROCHECK_API="https://your-instance.example.com"
|
|
33
|
+
npx zerocheck login
|
|
34
|
+
npx zerocheck install
|
|
35
|
+
npx zerocheck validate
|
|
36
|
+
npx zerocheck run --json .zerocheck/latest-run.json
|
|
45
37
|
```
|
|
46
38
|
|
|
47
|
-
|
|
39
|
+
`login` prints a URL and code for you to approve in your browser. `install` downloads Chromium for local runs. `validate` checks the saved YAML without running a browser.
|
|
48
40
|
|
|
49
|
-
|
|
41
|
+
`run` executes the saved tests in the configured test directory, using `default_environment` from `zerocheck.yaml`. Confirm that environment points to the app you intend to test. When a results link is printed, open it to inspect outcomes, failed steps and screenshots after signing in. The JSON file also retains the run details. If publication fails, follow the upload-retry command printed by the CLI.
|
|
50
42
|
|
|
51
|
-
|
|
52
|
-
{"item-3":"Use the existing demo product with SKU DEMO-1; expect a total of $12.00."}
|
|
53
|
-
```
|
|
43
|
+
To run just one test, use its actual repository path:
|
|
54
44
|
|
|
55
|
-
```
|
|
56
|
-
npx zerocheck
|
|
45
|
+
```bash
|
|
46
|
+
npx zerocheck run zerocheck/tests/your-test.yaml
|
|
57
47
|
```
|
|
58
48
|
|
|
59
|
-
|
|
49
|
+
### Starting from the web app
|
|
60
50
|
|
|
61
|
-
|
|
51
|
+
Open your provisioned instance, choose a saved test and run it against the intended environment. You can use the web app without installing this package.
|
|
62
52
|
|
|
63
|
-
|
|
53
|
+
To bring an individual web test into your repository, use **Download YAML** and put that file in the configured test directory. This creates an independent copy. Web tests are edited in the web app; repository tests are edited in their files. The CLI does not automatically download your saved web tests.
|
|
64
54
|
|
|
65
|
-
|
|
66
|
-
project: your-project-id
|
|
67
|
-
default_environment: dev
|
|
68
|
-
test_directory: zerocheck/tests
|
|
69
|
-
environments:
|
|
70
|
-
dev:
|
|
71
|
-
url: http://localhost:3000
|
|
72
|
-
staging:
|
|
73
|
-
url: https://staging.example.com
|
|
74
|
-
secrets:
|
|
75
|
-
EMAIL: ${ZEROCHECK_EMAIL}
|
|
76
|
-
PASSWORD: ${ZEROCHECK_PASSWORD}
|
|
77
|
-
login_steps:
|
|
78
|
-
- Navigate to /login
|
|
79
|
-
- Enter "${EMAIL}" into the Email field
|
|
80
|
-
- Enter "${PASSWORD}" into the Password field
|
|
81
|
-
- Click the Sign in button
|
|
82
|
-
- Verify the dashboard is visible
|
|
83
|
-
production:
|
|
84
|
-
url: https://example.com
|
|
85
|
-
execution:
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Only explicitly referenced secret variables are read for execution. Missing variables fail clearly. Listing, reading, saving and validating checks do not require execution secrets.
|
|
55
|
+
For a reviewed checklist import, the CLI can save its drafted tests with `import --draft IMPORT_ID`, as shown below. An import ID is different from an individual saved-test ID or run ID.
|
|
89
56
|
|
|
90
|
-
|
|
57
|
+
<details>
|
|
58
|
+
<summary><strong>Need to create the project files first?</strong></summary>
|
|
91
59
|
|
|
92
|
-
|
|
60
|
+
After installing and logging in, replace the project ID and app URL, then initialize a new configuration:
|
|
93
61
|
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
name: Release notes are available
|
|
97
|
-
blocks_merge: true
|
|
98
|
-
steps:
|
|
99
|
-
- Navigate to /releases
|
|
100
|
-
- Verify the Release notes heading is visible
|
|
62
|
+
```bash
|
|
63
|
+
npx zerocheck init --project "YOUR_PROJECT_ID" --env dev --url "http://localhost:3000"
|
|
101
64
|
```
|
|
102
65
|
|
|
103
|
-
|
|
66
|
+
`init` creates `zerocheck.yaml` and an empty test directory. If the configuration already exists, edit that file to change its project or environments.
|
|
104
67
|
|
|
105
|
-
|
|
68
|
+
Add your onboarding-provided test YAML, download an agreed web test, or draft tests from a real release checklist:
|
|
106
69
|
|
|
107
|
-
|
|
70
|
+
```bash
|
|
71
|
+
npx zerocheck install
|
|
72
|
+
npx zerocheck import release-checklist.md --env dev
|
|
73
|
+
```
|
|
108
74
|
|
|
109
|
-
|
|
75
|
+
Import drafts and runs checks immediately. Review the displayed YAML and browser results before saving. Resolve missing inputs and inspect failed or incomplete checks. Replace `IMPORT_ID` with the import identifier printed by the command:
|
|
110
76
|
|
|
111
|
-
```
|
|
77
|
+
```bash
|
|
78
|
+
npx zerocheck import --draft IMPORT_ID
|
|
112
79
|
npx zerocheck validate
|
|
113
80
|
npx zerocheck run --env dev
|
|
114
|
-
npx zerocheck run zerocheck/tests/release.yaml --env staging --runner hosted
|
|
115
|
-
npx zerocheck run --env production --runner local --fail-on-flaky
|
|
116
|
-
npx zerocheck run --env staging --json .zerocheck/latest-run.json --junit .zerocheck/junit.xml
|
|
117
|
-
npx zerocheck results RUN_ID
|
|
118
|
-
npx zerocheck doctor --env staging
|
|
119
81
|
```
|
|
120
82
|
|
|
121
|
-
|
|
83
|
+
Saving a draft does not change a failed result into a pass. Keep the expected outcome when investigating a failure.
|
|
122
84
|
|
|
123
|
-
|
|
85
|
+
</details>
|
|
124
86
|
|
|
125
|
-
|
|
87
|
+
## What a test looks like
|
|
126
88
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
## GitHub Actions and PR comments
|
|
130
|
-
|
|
131
|
-
```sh
|
|
132
|
-
npx zerocheck init --github-actions
|
|
133
|
-
```
|
|
89
|
+
Each repository test is a readable YAML file. Adapt this example to a route and outcome that exist in your app:
|
|
134
90
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
npx zerocheck report --file .zerocheck/latest-run.json
|
|
91
|
+
```yaml
|
|
92
|
+
version: zerocheck/v1
|
|
93
|
+
name: Release notes are available
|
|
94
|
+
blocks_merge: true
|
|
95
|
+
steps:
|
|
96
|
+
- Navigate to /releases
|
|
97
|
+
- Verify the Release notes heading is visible
|
|
143
98
|
```
|
|
144
99
|
|
|
145
|
-
|
|
100
|
+
## Coding agents and other runners
|
|
146
101
|
|
|
147
|
-
|
|
102
|
+
After completing project setup, a compatible coding agent can use the same CLI through its local stdio MCP server:
|
|
148
103
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
Use the installed CLI as a stdio MCP server, with an explicit project directory:
|
|
152
|
-
|
|
153
|
-
```json
|
|
154
|
-
{
|
|
155
|
-
"mcpServers": {
|
|
156
|
-
"zerocheck": {
|
|
157
|
-
"command": "npx",
|
|
158
|
-
"args": ["--yes", "zerocheck@0.1.7", "mcp", "--project-dir", "/absolute/path/to/project"]
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
104
|
+
```bash
|
|
105
|
+
npx zerocheck mcp --project-dir "/absolute/path/to/your/app"
|
|
162
106
|
```
|
|
163
107
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
Tools: `list_checks`, `read_check`, `save_check`, `import_checklist`, `answer_import`, `adopt_import`, `verify_check`, `run_checks`, `get_results`, and `cancel`. Existing check replacements require their current `expectedRevision`. File operations stay within the configured project, including symbolic-link checks.
|
|
167
|
-
|
|
168
|
-
Browser operations return job handles. Poll `get_results` with `kind: "job"` and `waitMs` up to 30000; inspect the returned run/draft ID for durable retrieval. `kind: "run"` reads a saved run after process restart; `kind: "import"` reads an uploaded import. Cancel with the job ID. Each MCP process permits one active browser job. Closing it cancels that job; cancellation cannot undo actions already performed.
|
|
169
|
-
|
|
170
|
-
## Data sent to Zerocheck
|
|
108
|
+
Configure the process in your MCP client using the [MCP guide](https://tryzerocheck.com/docs/mcp/). Device sign-in needs your approval. The MCP tools can inspect, draft, save and run tests, then read their results.
|
|
171
109
|
|
|
172
|
-
|
|
110
|
+
For hosted execution, add `--runner hosted` to `run`; Chromium installation on your machine is unnecessary for that runner. The hosted browser must be able to reach the configured app URL. It cannot automatically reach your laptop's localhost. Hosted CLI runs still use repository configuration and test files.
|
|
173
111
|
|
|
174
|
-
##
|
|
112
|
+
## When a run needs attention
|
|
175
113
|
|
|
176
|
-
|
|
114
|
+
| Situation | Next step |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| No instance configured | Set `ZEROCHECK_API` to your customer instance before login. |
|
|
117
|
+
| No configuration or tests found | Add the project configuration and saved YAML tests. `init` creates an empty test directory. |
|
|
118
|
+
| Missing test credentials | Supply the variables referenced by your configured environment. |
|
|
119
|
+
| A test fails or cannot complete | Open the result and inspect the failed step and evidence. |
|
|
120
|
+
| Result publication fails | Follow the upload-retry command printed by the CLI. |
|
|
177
121
|
|
|
178
|
-
|
|
122
|
+
Exit codes: **0** means the run completed within the configured merge policy; **1** means a blocking failure; **2** means a setup, incomplete-execution, infrastructure or result-publication error.
|
|
179
123
|
|
|
180
|
-
|
|
124
|
+
[Request pilot access](https://tryzerocheck.com/demo/) · [Developer resources](https://tryzerocheck.com/developers/) · [CLI guide](https://tryzerocheck.com/cli/) · [Web app guide](https://tryzerocheck.com/docs/web-app/) · [Results](https://tryzerocheck.com/docs/results/)
|
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.9" },
|
|
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.9
|
|
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.9" });
|
|
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.9").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"));
|
package/package.json
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zerocheck",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Turn
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "Turn release checklists into repeatable browser tests. Run with the Zerocheck CLI, CI or web app and inspect the evidence.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"qa-agent",
|
|
7
|
+
"ai-testing",
|
|
8
|
+
"agentic-testing",
|
|
9
|
+
"zerocheck",
|
|
10
|
+
"testing",
|
|
11
|
+
"qa",
|
|
12
|
+
"e2e",
|
|
13
|
+
"end-to-end",
|
|
14
|
+
"browser-testing",
|
|
15
|
+
"test-automation",
|
|
16
|
+
"release-checklist",
|
|
17
|
+
"plain-english",
|
|
18
|
+
"playwright",
|
|
19
|
+
"cli",
|
|
20
|
+
"mcp",
|
|
21
|
+
"ci"
|
|
22
|
+
],
|
|
23
|
+
"homepage": "https://tryzerocheck.com/",
|
|
24
|
+
"license": "MIT",
|
|
5
25
|
"type": "module",
|
|
6
26
|
"repository": {
|
|
7
27
|
"type": "git",
|