verifaied 0.29.0.dev76__tar.gz → 0.30.0.dev77__tar.gz
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.
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/PKG-INFO +74 -7
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/README.md +73 -6
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/pyproject.toml +1 -1
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/agent_mcp.py +110 -8
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/cli.py +12 -7
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/client.py +31 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/recorder_driver.js +11 -3
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/recorder_observer.js +92 -21
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/recorder_spec.js +5 -1
- verifaied-0.30.0.dev77/src/verifaied/replay_harvest.ts +606 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/repo_config.py +263 -24
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/stepcheck.py +28 -1
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/testcmd.py +909 -58
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/conftest.py +17 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_agent_mcp.py +293 -2
- verifaied-0.30.0.dev77/tests/test_browser_settings_parity.py +148 -0
- verifaied-0.30.0.dev77/tests/test_caption_parity.py +263 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_client.py +40 -0
- verifaied-0.30.0.dev77/tests/test_pause_parity.py +79 -0
- verifaied-0.30.0.dev77/tests/test_replay_harvest_runner.py +557 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_repo_config.py +333 -6
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_stepcheck_parity.py +66 -3
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_testcmd.py +2175 -37
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/uv.lock +1 -1
- verifaied-0.29.0.dev76/src/verifaied/replay_harvest.ts +0 -77
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/.gitignore +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/LICENSE +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/__init__.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/__main__.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/agent_steps.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/analyzer.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/audit.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/collect_browser.js +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/collect_browser.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/config.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/failure_text.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/instrument.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/instrument_snippets.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/instrumenter.js +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/instrumenter.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/prompts.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/proxy.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/record.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/recorder_picker.js +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/recorder_refs.js +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/run_reporter.js +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/runner_dir.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/skill/SKILL.md +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/skill/reference/commands.md +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/skillcmd.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/source_index.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/subprocess_util.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/suite_report.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/uploader.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/src/verifaied/varcmd.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/__init__.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_audit.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_check.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_check_done.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_clear_manual.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_cli.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_collect_browser.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_config.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_console_label_parity.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_instrument.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_instrument_detect.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_instrument_snippets.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_instrumenter.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_prompt_parity.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_proxy.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_record.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_skill_parity.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_skillcmd.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_source_index.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_suite_report.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_uploader.py +0 -0
- {verifaied-0.29.0.dev76 → verifaied-0.30.0.dev77}/tests/test_varcmd.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: verifaied
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.30.0.dev77
|
|
4
4
|
Summary: Find what's untested in your code — locally, no account required
|
|
5
5
|
Project-URL: Homepage, https://pypi.org/project/verifaied/
|
|
6
6
|
Author: Kyle Richards
|
|
@@ -470,8 +470,10 @@ verifaied test run --all
|
|
|
470
470
|
verifaied test run --all --env stage
|
|
471
471
|
|
|
472
472
|
# How much of the run's clock goes on making the video watchable.
|
|
473
|
-
# `fast` (the default)
|
|
474
|
-
#
|
|
473
|
+
# `fast` (the default) glides a cursor to what each step acts on, marks
|
|
474
|
+
# it, types at five characters a second and draws each step's caption;
|
|
475
|
+
# `watch` lingers on every action; `off` records without any of it and
|
|
476
|
+
# skips the flow's Pause steps and the holds after its checks.
|
|
475
477
|
verifaied test run "Settings/API tokens" --video watch
|
|
476
478
|
|
|
477
479
|
# What's recorded, and how each last went on this branch.
|
|
@@ -599,7 +601,67 @@ the delete is **refused and the step comes back** rather than taking a
|
|
|
599
601
|
neighbour with it.
|
|
600
602
|
|
|
601
603
|
Once the spec is saved it is a plain file in your repo, and your editor is
|
|
602
|
-
the better tool for it
|
|
604
|
+
the better tool for it — though every step's caption, and a check's hold,
|
|
605
|
+
can still be changed from the test's page without recording anything: the
|
|
606
|
+
pencil on a block stages the change, and one Save rewrites the file and
|
|
607
|
+
replays it.
|
|
608
|
+
|
|
609
|
+
### Holds and captions — what the video says
|
|
610
|
+
|
|
611
|
+
A replay's video is the record a person reviews, so two things are drawn
|
|
612
|
+
on it beyond the steps themselves. **Every check holds**: after an
|
|
613
|
+
assertion passes the replay lingers for a moment — two seconds by default
|
|
614
|
+
— so the viewer sees the state that was checked before the next step
|
|
615
|
+
moves on. **Every step is captioned**: a short line in plain English —
|
|
616
|
+
"Open the settings page", "The token appears in the list" — drawn at the
|
|
617
|
+
bottom of the video while the step runs.
|
|
618
|
+
|
|
619
|
+
Both are settings on `[browser-tests]` in `.verifaied/config.toml` — or,
|
|
620
|
+
for the whole repository at once, on its Settings page in the web under
|
|
621
|
+
**Browser tests**, where every replay-video key sits beside them. A
|
|
622
|
+
value saved on the page applies to every machine that records or replays
|
|
623
|
+
the repository and wins over the file; a key left on the machine default
|
|
624
|
+
keeps reading the file, and a `--video` flag or the Video settings menu
|
|
625
|
+
still wins over both for one run:
|
|
626
|
+
|
|
627
|
+
```toml
|
|
628
|
+
[browser-tests]
|
|
629
|
+
# How long the replay holds after each check passes. 0 for no hold.
|
|
630
|
+
assertion_pause_ms = 2000
|
|
631
|
+
# Whether steps are captioned as they land while recording.
|
|
632
|
+
captions = true
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
The studio reads them when it connects and shows them under **Settings**
|
|
636
|
+
beside Variables, where either can be changed for one recording. Any
|
|
637
|
+
check can be given its own hold from its Edit form, and any step's caption
|
|
638
|
+
can be edited, regenerated or switched off from the flow — a caption you
|
|
639
|
+
switch off stays off. While a recording is live the studio page writes
|
|
640
|
+
the captions itself, one per step as it lands, from a model given the
|
|
641
|
+
line and its place in the flow; it is a paid call at the cheapest tier,
|
|
642
|
+
and a plan or spend cap that refuses it stops the auto-captioning for that
|
|
643
|
+
recording and says so, leaving the steps to be captioned by hand. The
|
|
644
|
+
assistant captions the steps it writes, and an agent recording through
|
|
645
|
+
`verifaied test mcp` passes a `caption` with each step and `hold_seconds`
|
|
646
|
+
with each check — nothing captions a step behind your back.
|
|
647
|
+
|
|
648
|
+
Generated captions are short, concise and in simplified technical
|
|
649
|
+
English: one sentence, one idea, present tense, active voice, at most ten
|
|
650
|
+
words, naming what a person sees and never a selector, a value or a
|
|
651
|
+
password. A typed one may be anything up to 120 characters on one line,
|
|
652
|
+
except the words a caption-less step is called (`Pause 2s`, `Hold 2s`).
|
|
653
|
+
|
|
654
|
+
In the spec a captioned or held step is one named step around the line:
|
|
655
|
+
|
|
656
|
+
```ts
|
|
657
|
+
await test.step("The token appears in the list", async () => { await expect(page.getByText("tok_…")).toBeVisible(); await page.waitForTimeout(2000); });
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
A held check with no caption is titled `Hold 2s`; a step with neither is
|
|
661
|
+
written bare, exactly as before. Holds wait in every replay except one
|
|
662
|
+
run with `--video off` (or `replay_pauses = false`), which skips them
|
|
663
|
+
with the Pause steps; captions are drawn unless `replay_captions = false`
|
|
664
|
+
or the video is off. Neither counts against the replay's time budget.
|
|
603
665
|
|
|
604
666
|
### Running another test inside this one
|
|
605
667
|
|
|
@@ -976,9 +1038,14 @@ was clicked with `video.show`. 1.60 is where the last of those shipped,
|
|
|
976
1038
|
so it is a floor and not an allowlist: anything newer is allowed, and
|
|
977
1039
|
anything older is refused rather than opening a browser that would
|
|
978
1040
|
record nothing. `test run` stays ungated. Below 1.60 the annotation is
|
|
979
|
-
not available, so
|
|
980
|
-
|
|
981
|
-
|
|
1041
|
+
not available, so use `--video off` there, or set every replay key to
|
|
1042
|
+
its off value (`replay_show_actions_ms = 0`, `replay_cursor_ms = 0`,
|
|
1043
|
+
`replay_type_delay_ms = 0`, `replay_pauses = false`,
|
|
1044
|
+
`replay_captions = false` and `replay_video_step_banner = false`) — that
|
|
1045
|
+
writes exactly the config the CLI wrote before any of it existed. Every
|
|
1046
|
+
one of those keys can also be set once for the repository on its
|
|
1047
|
+
Settings page, which the CLI reads at the start of each recording and
|
|
1048
|
+
replay and lays over the file.
|
|
982
1049
|
|
|
983
1050
|
### Exit codes
|
|
984
1051
|
|
|
@@ -424,8 +424,10 @@ verifaied test run --all
|
|
|
424
424
|
verifaied test run --all --env stage
|
|
425
425
|
|
|
426
426
|
# How much of the run's clock goes on making the video watchable.
|
|
427
|
-
# `fast` (the default)
|
|
428
|
-
#
|
|
427
|
+
# `fast` (the default) glides a cursor to what each step acts on, marks
|
|
428
|
+
# it, types at five characters a second and draws each step's caption;
|
|
429
|
+
# `watch` lingers on every action; `off` records without any of it and
|
|
430
|
+
# skips the flow's Pause steps and the holds after its checks.
|
|
429
431
|
verifaied test run "Settings/API tokens" --video watch
|
|
430
432
|
|
|
431
433
|
# What's recorded, and how each last went on this branch.
|
|
@@ -553,7 +555,67 @@ the delete is **refused and the step comes back** rather than taking a
|
|
|
553
555
|
neighbour with it.
|
|
554
556
|
|
|
555
557
|
Once the spec is saved it is a plain file in your repo, and your editor is
|
|
556
|
-
the better tool for it
|
|
558
|
+
the better tool for it — though every step's caption, and a check's hold,
|
|
559
|
+
can still be changed from the test's page without recording anything: the
|
|
560
|
+
pencil on a block stages the change, and one Save rewrites the file and
|
|
561
|
+
replays it.
|
|
562
|
+
|
|
563
|
+
### Holds and captions — what the video says
|
|
564
|
+
|
|
565
|
+
A replay's video is the record a person reviews, so two things are drawn
|
|
566
|
+
on it beyond the steps themselves. **Every check holds**: after an
|
|
567
|
+
assertion passes the replay lingers for a moment — two seconds by default
|
|
568
|
+
— so the viewer sees the state that was checked before the next step
|
|
569
|
+
moves on. **Every step is captioned**: a short line in plain English —
|
|
570
|
+
"Open the settings page", "The token appears in the list" — drawn at the
|
|
571
|
+
bottom of the video while the step runs.
|
|
572
|
+
|
|
573
|
+
Both are settings on `[browser-tests]` in `.verifaied/config.toml` — or,
|
|
574
|
+
for the whole repository at once, on its Settings page in the web under
|
|
575
|
+
**Browser tests**, where every replay-video key sits beside them. A
|
|
576
|
+
value saved on the page applies to every machine that records or replays
|
|
577
|
+
the repository and wins over the file; a key left on the machine default
|
|
578
|
+
keeps reading the file, and a `--video` flag or the Video settings menu
|
|
579
|
+
still wins over both for one run:
|
|
580
|
+
|
|
581
|
+
```toml
|
|
582
|
+
[browser-tests]
|
|
583
|
+
# How long the replay holds after each check passes. 0 for no hold.
|
|
584
|
+
assertion_pause_ms = 2000
|
|
585
|
+
# Whether steps are captioned as they land while recording.
|
|
586
|
+
captions = true
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
The studio reads them when it connects and shows them under **Settings**
|
|
590
|
+
beside Variables, where either can be changed for one recording. Any
|
|
591
|
+
check can be given its own hold from its Edit form, and any step's caption
|
|
592
|
+
can be edited, regenerated or switched off from the flow — a caption you
|
|
593
|
+
switch off stays off. While a recording is live the studio page writes
|
|
594
|
+
the captions itself, one per step as it lands, from a model given the
|
|
595
|
+
line and its place in the flow; it is a paid call at the cheapest tier,
|
|
596
|
+
and a plan or spend cap that refuses it stops the auto-captioning for that
|
|
597
|
+
recording and says so, leaving the steps to be captioned by hand. The
|
|
598
|
+
assistant captions the steps it writes, and an agent recording through
|
|
599
|
+
`verifaied test mcp` passes a `caption` with each step and `hold_seconds`
|
|
600
|
+
with each check — nothing captions a step behind your back.
|
|
601
|
+
|
|
602
|
+
Generated captions are short, concise and in simplified technical
|
|
603
|
+
English: one sentence, one idea, present tense, active voice, at most ten
|
|
604
|
+
words, naming what a person sees and never a selector, a value or a
|
|
605
|
+
password. A typed one may be anything up to 120 characters on one line,
|
|
606
|
+
except the words a caption-less step is called (`Pause 2s`, `Hold 2s`).
|
|
607
|
+
|
|
608
|
+
In the spec a captioned or held step is one named step around the line:
|
|
609
|
+
|
|
610
|
+
```ts
|
|
611
|
+
await test.step("The token appears in the list", async () => { await expect(page.getByText("tok_…")).toBeVisible(); await page.waitForTimeout(2000); });
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
A held check with no caption is titled `Hold 2s`; a step with neither is
|
|
615
|
+
written bare, exactly as before. Holds wait in every replay except one
|
|
616
|
+
run with `--video off` (or `replay_pauses = false`), which skips them
|
|
617
|
+
with the Pause steps; captions are drawn unless `replay_captions = false`
|
|
618
|
+
or the video is off. Neither counts against the replay's time budget.
|
|
557
619
|
|
|
558
620
|
### Running another test inside this one
|
|
559
621
|
|
|
@@ -930,9 +992,14 @@ was clicked with `video.show`. 1.60 is where the last of those shipped,
|
|
|
930
992
|
so it is a floor and not an allowlist: anything newer is allowed, and
|
|
931
993
|
anything older is refused rather than opening a browser that would
|
|
932
994
|
record nothing. `test run` stays ungated. Below 1.60 the annotation is
|
|
933
|
-
not available, so
|
|
934
|
-
|
|
935
|
-
|
|
995
|
+
not available, so use `--video off` there, or set every replay key to
|
|
996
|
+
its off value (`replay_show_actions_ms = 0`, `replay_cursor_ms = 0`,
|
|
997
|
+
`replay_type_delay_ms = 0`, `replay_pauses = false`,
|
|
998
|
+
`replay_captions = false` and `replay_video_step_banner = false`) — that
|
|
999
|
+
writes exactly the config the CLI wrote before any of it existed. Every
|
|
1000
|
+
one of those keys can also be set once for the repository on its
|
|
1001
|
+
Settings page, which the CLI reads at the start of each recording and
|
|
1002
|
+
replay and lays over the file.
|
|
936
1003
|
|
|
937
1004
|
### Exit codes
|
|
938
1005
|
|
|
@@ -44,6 +44,7 @@ from __future__ import annotations
|
|
|
44
44
|
|
|
45
45
|
import asyncio
|
|
46
46
|
import atexit
|
|
47
|
+
import math
|
|
47
48
|
import queue as queue_module
|
|
48
49
|
import signal
|
|
49
50
|
import subprocess
|
|
@@ -66,10 +67,12 @@ from verifaied.agent_steps import (
|
|
|
66
67
|
report,
|
|
67
68
|
)
|
|
68
69
|
from verifaied.config import web_url_for
|
|
70
|
+
from verifaied.repo_config import RepoConfigError, web_authoring_keys
|
|
69
71
|
from verifaied.stepcheck import (
|
|
70
72
|
MAX_FLOW_STEPS,
|
|
71
73
|
MAX_SCRIPT_CHARS,
|
|
72
74
|
compose_script,
|
|
75
|
+
validate_caption_text,
|
|
73
76
|
validate_step_line,
|
|
74
77
|
)
|
|
75
78
|
from verifaied.testcmd import (
|
|
@@ -80,6 +83,8 @@ from verifaied.testcmd import (
|
|
|
80
83
|
SessionHooks,
|
|
81
84
|
ShellRunner,
|
|
82
85
|
StreamRunner,
|
|
86
|
+
authoring_settings,
|
|
87
|
+
compose_pause,
|
|
83
88
|
detect_playwright_version,
|
|
84
89
|
discard_test_row,
|
|
85
90
|
ensure_supported_playwright,
|
|
@@ -87,6 +92,10 @@ from verifaied.testcmd import (
|
|
|
87
92
|
list_recorded_tests,
|
|
88
93
|
normalize_start_url,
|
|
89
94
|
parse_test_ref,
|
|
95
|
+
pause_label,
|
|
96
|
+
pause_refusal,
|
|
97
|
+
read_repo_config,
|
|
98
|
+
read_service_settings,
|
|
90
99
|
record_one,
|
|
91
100
|
ref_of,
|
|
92
101
|
resolve_or_create_test,
|
|
@@ -192,7 +201,7 @@ Guessing what a page will look like after a click is how flows break.
|
|
|
192
201
|
|
|
193
202
|
{_LOCATOR_DOCTRINE}
|
|
194
203
|
|
|
195
|
-
Each step is ONE of
|
|
204
|
+
Each step is ONE of four shapes and nothing else:
|
|
196
205
|
|
|
197
206
|
- an ACTION (kind "action"): `await page.<locator>.<verb>(...);` with
|
|
198
207
|
<verb> one of click, dblclick, fill, press, check, uncheck,
|
|
@@ -208,6 +217,22 @@ Each step is ONE of three shapes and nothing else:
|
|
|
208
217
|
is wrapped in `await page.evaluate(async () => {{ … }});` for you, so
|
|
209
218
|
`await` works inside it. It lands in the flow as an action labelled
|
|
210
219
|
"Run a script in the page".
|
|
220
|
+
- a PAUSE (kind "pause"): `{{"kind": "pause", "seconds": 2}}`, no code.
|
|
221
|
+
It holds the replay's video still for that long so a reader can see
|
|
222
|
+
the result of the step before it — a toast, a filled form, a page
|
|
223
|
+
that just arrived. Between 0.1 and 30 seconds. It is skipped when the
|
|
224
|
+
video is off, so it must never be what a flow *depends* on: a pause
|
|
225
|
+
never waits for the app — an assertion does that, and waits until
|
|
226
|
+
the page agrees.
|
|
227
|
+
|
|
228
|
+
Any step may carry a `"caption"`: one short sentence shown under the
|
|
229
|
+
video while the step runs, for a reviewer who does not read code — one
|
|
230
|
+
idea, present tense, active voice, at most 8 words; an action is an
|
|
231
|
+
imperative ("Open the settings page"), a check says what is true ("The
|
|
232
|
+
token appears in the list"), a pause says what the viewer watches ("The
|
|
233
|
+
chart loads"); name what a person sees, never a selector or a value.
|
|
234
|
+
An `assert` may carry `"hold_seconds"`: how long the video holds on what
|
|
235
|
+
it checked (0 for none); left out, the repo's own default applies.
|
|
211
236
|
|
|
212
237
|
Rules for the code:
|
|
213
238
|
|
|
@@ -303,9 +328,9 @@ today and fails next week. Replace it: `delete_step` the line and queue a
|
|
|
303
328
|
corrected one naming the stable part yourself (`getByText(/Matches/)`,
|
|
304
329
|
`toContainText('Matches')`).
|
|
305
330
|
|
|
306
|
-
Arguments: `steps` is a list of `{{"kind": "action"|"assert"|"script"
|
|
307
|
-
"<one statement, or a script's body>", "label": "<a few words saying
|
|
308
|
-
does>"}}
|
|
331
|
+
Arguments: `steps` is a list of `{{"kind": "action"|"assert"|"script"|"pause",
|
|
332
|
+
"code": "<one statement, or a script's body>", "label": "<a few words saying
|
|
333
|
+
what it does>"}}` — a pause carries `"seconds"` instead of code."""
|
|
309
334
|
|
|
310
335
|
|
|
311
336
|
# What each ending actually does, so a refusal says why waiting for the
|
|
@@ -445,6 +470,18 @@ class AgentRecorderServer:
|
|
|
445
470
|
self.token = token
|
|
446
471
|
self.repo_id = repo_id
|
|
447
472
|
self.root = root
|
|
473
|
+
# The repo's authoring defaults: what a check holds the video for
|
|
474
|
+
# when the agent says nothing, and whether captions are expected.
|
|
475
|
+
# Read once; the MCP process is one recording long.
|
|
476
|
+
try:
|
|
477
|
+
service = read_service_settings(
|
|
478
|
+
api_url=api_url, token=token, repo_id=repo_id
|
|
479
|
+
)
|
|
480
|
+
self._settings = authoring_settings(
|
|
481
|
+
read_repo_config(root, service).authoring, web_authoring_keys(service)
|
|
482
|
+
)
|
|
483
|
+
except (RecordedTestError, RepoConfigError):
|
|
484
|
+
self._settings = {}
|
|
448
485
|
self.branch = branch
|
|
449
486
|
self.commit_sha = commit_sha
|
|
450
487
|
self.env_name = env_name
|
|
@@ -778,17 +815,78 @@ class AgentRecorderServer:
|
|
|
778
815
|
raw = str(fields.get("code") or "")
|
|
779
816
|
code = raw.strip()
|
|
780
817
|
label = str(fields.get("label") or "").strip() or code
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
818
|
+
# A caption and, on a check, a hold ride beside the line and
|
|
819
|
+
# are composed into the step by the session — the line itself
|
|
820
|
+
# stays what every validator reads. The hold defaults to the
|
|
821
|
+
# repo's own, so an agent that says nothing gets the same
|
|
822
|
+
# video a person does.
|
|
823
|
+
caption = fields.get("caption")
|
|
824
|
+
if caption is not None:
|
|
825
|
+
reason = validate_caption_text(caption)
|
|
826
|
+
if reason:
|
|
827
|
+
entries.append(f"REFUSED — {reason}")
|
|
828
|
+
continue
|
|
829
|
+
caption = str(caption).strip()
|
|
830
|
+
hold_ms: int | None = None
|
|
831
|
+
if kind == "assert":
|
|
832
|
+
hold_seconds = fields.get("hold_seconds")
|
|
833
|
+
if hold_seconds is None:
|
|
834
|
+
hold_ms = self._settings.get("assertion_pause_ms") or None
|
|
835
|
+
elif isinstance(hold_seconds, bool) or not isinstance(
|
|
836
|
+
hold_seconds, (int, float)
|
|
837
|
+
):
|
|
838
|
+
entries.append(
|
|
839
|
+
"REFUSED — `hold_seconds` is a number of seconds, or 0"
|
|
840
|
+
)
|
|
841
|
+
continue
|
|
842
|
+
elif not math.isfinite(float(hold_seconds)):
|
|
843
|
+
entries.append(
|
|
844
|
+
"REFUSED — `hold_seconds` is a number of seconds, or 0"
|
|
845
|
+
)
|
|
846
|
+
continue
|
|
847
|
+
else:
|
|
848
|
+
ms = round(float(hold_seconds) * 1000)
|
|
849
|
+
if ms != 0 and pause_refusal(hold_seconds):
|
|
850
|
+
entries.append(
|
|
851
|
+
f"REFUSED — a hold {pause_refusal(hold_seconds)[8:]}"
|
|
852
|
+
)
|
|
853
|
+
continue
|
|
854
|
+
hold_ms = ms or None
|
|
855
|
+
elif fields.get("hold_seconds") is not None:
|
|
856
|
+
entries.append(
|
|
857
|
+
"REFUSED — a hold follows a check — only an `assert` holds"
|
|
858
|
+
)
|
|
784
859
|
continue
|
|
860
|
+
if kind == "pause":
|
|
861
|
+
# No code to validate: the seconds are the whole of it,
|
|
862
|
+
# and the line is composed here exactly as the studio's
|
|
863
|
+
# Pause tab composes it, so the block reopens there.
|
|
864
|
+
reason = pause_refusal(fields.get("seconds"))
|
|
865
|
+
if reason:
|
|
866
|
+
entries.append(f"REFUSED — {reason}")
|
|
867
|
+
continue
|
|
868
|
+
ms = round(float(fields.get("seconds")) * 1000)
|
|
869
|
+
kind, code, label = "action", compose_pause(ms), pause_label(ms)
|
|
870
|
+
else:
|
|
871
|
+
reason = validate_step_line(kind, raw if kind == "script" else code)
|
|
872
|
+
if reason:
|
|
873
|
+
entries.append(f"REFUSED — {reason}")
|
|
874
|
+
continue
|
|
785
875
|
if kind == "script":
|
|
786
876
|
# The body is the agent's; the wrapper and the label are
|
|
787
877
|
# not. Composed exactly as the studio's Run Code tab
|
|
788
878
|
# composes, so the block reopens there and keeps its name
|
|
789
879
|
# once the spec is saved.
|
|
790
880
|
kind, code, label = "action", compose_script(raw), CONSOLE_LABEL
|
|
791
|
-
accepted.append(
|
|
881
|
+
accepted.append(
|
|
882
|
+
{
|
|
883
|
+
"kind": kind,
|
|
884
|
+
"code": code,
|
|
885
|
+
"label": label,
|
|
886
|
+
"caption": caption,
|
|
887
|
+
"hold_ms": hold_ms,
|
|
888
|
+
}
|
|
889
|
+
)
|
|
792
890
|
entries.append(
|
|
793
891
|
AgentStep(
|
|
794
892
|
handle=recording.next_handle(),
|
|
@@ -817,6 +915,10 @@ class AgentRecorderServer:
|
|
|
817
915
|
"flow_index": position - 1,
|
|
818
916
|
"flow_size": len(accepted),
|
|
819
917
|
}
|
|
918
|
+
if step.get("caption") is not None:
|
|
919
|
+
payload["caption"] = step["caption"]
|
|
920
|
+
if step.get("hold_ms"):
|
|
921
|
+
payload["hold_ms"] = step["hold_ms"]
|
|
820
922
|
if recording.page.snapshot_id is not None:
|
|
821
923
|
# Which look at the page this line's refs were read from.
|
|
822
924
|
# The driver compares it against the tree it holds and
|
|
@@ -1818,9 +1818,12 @@ def test_record_command(
|
|
|
1818
1818
|
"--video",
|
|
1819
1819
|
help=(
|
|
1820
1820
|
"How the replay that follows this recording should be paced. "
|
|
1821
|
-
"`fast`
|
|
1822
|
-
"every action; `off` records
|
|
1823
|
-
"
|
|
1821
|
+
"`fast` moves a cursor to each target, marks it and types at a "
|
|
1822
|
+
"readable pace; `watch` lingers on every action; `off` records "
|
|
1823
|
+
"without any of it and skips the pauses the flow asks for. "
|
|
1824
|
+
"Overrides replay_show_actions_ms / replay_slow_mo_ms / "
|
|
1825
|
+
"replay_cursor_ms / replay_type_delay_ms / replay_pauses for "
|
|
1826
|
+
"this run."
|
|
1824
1827
|
),
|
|
1825
1828
|
),
|
|
1826
1829
|
) -> None:
|
|
@@ -2042,10 +2045,12 @@ def test_run_command(
|
|
|
2042
2045
|
None,
|
|
2043
2046
|
"--video",
|
|
2044
2047
|
help=(
|
|
2045
|
-
"How the video should be paced. `fast` (the default)
|
|
2046
|
-
"each
|
|
2047
|
-
"`off` records without
|
|
2048
|
-
"
|
|
2048
|
+
"How the video should be paced. `fast` (the default) moves a "
|
|
2049
|
+
"cursor to each target, marks it and types at a readable pace; "
|
|
2050
|
+
"`watch` lingers on every action; `off` records without any of "
|
|
2051
|
+
"it and skips the pauses the flow asks for. Overrides "
|
|
2052
|
+
"replay_show_actions_ms / replay_slow_mo_ms / replay_cursor_ms / "
|
|
2053
|
+
"replay_type_delay_ms / replay_pauses for this run."
|
|
2049
2054
|
),
|
|
2050
2055
|
),
|
|
2051
2056
|
) -> None:
|
|
@@ -386,6 +386,37 @@ def _media_type(path: Path) -> str:
|
|
|
386
386
|
return _MEDIA_TYPES.get(path.suffix.lower(), "application/octet-stream")
|
|
387
387
|
|
|
388
388
|
|
|
389
|
+
def get_browser_test_settings(
|
|
390
|
+
*,
|
|
391
|
+
api_url: str,
|
|
392
|
+
token: str,
|
|
393
|
+
repo_id: UUID,
|
|
394
|
+
timeout: float = 10.0,
|
|
395
|
+
) -> dict[str, Any]:
|
|
396
|
+
"""The repository's browser-test settings as saved on the web.
|
|
397
|
+
|
|
398
|
+
The keys are the ``[browser-tests]`` keys, each null when the web
|
|
399
|
+
never set it — the CLI lays what is set over the file
|
|
400
|
+
(``apply_service_settings``). Raises :class:`ApiError` for anything
|
|
401
|
+
but a settings object, and the caller turns that into "the file
|
|
402
|
+
decides".
|
|
403
|
+
"""
|
|
404
|
+
url = f"{api_url.rstrip('/')}/repositories/{repo_id}/browser-test-settings"
|
|
405
|
+
try:
|
|
406
|
+
response = httpx.get(
|
|
407
|
+
url, headers={"Authorization": f"Bearer {token}"}, timeout=timeout
|
|
408
|
+
)
|
|
409
|
+
except httpx.HTTPError as e:
|
|
410
|
+
raise ApiError(0, f"could not reach {url}: {e}") from e
|
|
411
|
+
if response.status_code >= 400:
|
|
412
|
+
raise ApiError(response.status_code, _detail(response))
|
|
413
|
+
data = response.json()
|
|
414
|
+
settings = data.get("settings") if isinstance(data, dict) else None
|
|
415
|
+
if not isinstance(settings, dict):
|
|
416
|
+
raise ApiError(0, f"unexpected response from {url}: no settings object")
|
|
417
|
+
return settings
|
|
418
|
+
|
|
419
|
+
|
|
389
420
|
def list_recorded_tests(
|
|
390
421
|
*,
|
|
391
422
|
api_url: str,
|
|
@@ -232,12 +232,18 @@ function loadPlaywright() {
|
|
|
232
232
|
// locator expression, for the same reason.
|
|
233
233
|
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
234
234
|
|
|
235
|
+
// What `test` means inside a body run this way. A saved spec wraps an
|
|
236
|
+
// inserted test and a pause in `test.step(...)`, which only exists under
|
|
237
|
+
// the test runner; here the step is simply its body, run — so a pause in
|
|
238
|
+
// a chain waits during the recording's warm-up, and a nested insert runs.
|
|
239
|
+
const TEST_SHIM = { step: async (_title, body) => body() };
|
|
240
|
+
|
|
235
241
|
async function runPreSteps(preSteps, page, expect) {
|
|
236
242
|
for (let index = 0; index < preSteps.length; index += 1) {
|
|
237
243
|
const step = preSteps[index];
|
|
238
244
|
emit({ type: "pre_step", index, label: step.label, status: "running" });
|
|
239
|
-
const body = new AsyncFunction("page", "expect", step.body);
|
|
240
|
-
await body(page, expect);
|
|
245
|
+
const body = new AsyncFunction("page", "expect", "test", step.body);
|
|
246
|
+
await body(page, expect, TEST_SHIM);
|
|
241
247
|
emit({ type: "pre_step", index, label: step.label, status: "done" });
|
|
242
248
|
}
|
|
243
249
|
}
|
|
@@ -831,6 +837,7 @@ async function main() {
|
|
|
831
837
|
"new-tab": "a new tab opened — actions in it are not recorded yet",
|
|
832
838
|
dialog: "a browser dialog is not recorded yet",
|
|
833
839
|
download: "a download is not recorded yet",
|
|
840
|
+
key: "a key this keyboard layout types is not recorded yet",
|
|
834
841
|
};
|
|
835
842
|
async function note(reason) {
|
|
836
843
|
let docId = "";
|
|
@@ -1324,9 +1331,10 @@ async function main() {
|
|
|
1324
1331
|
let timer = null;
|
|
1325
1332
|
await setState({ selfActing: true });
|
|
1326
1333
|
try {
|
|
1327
|
-
const run = new AsyncFunction("page", "expect", body)(
|
|
1334
|
+
const run = new AsyncFunction("page", "expect", "test", body)(
|
|
1328
1335
|
page,
|
|
1329
1336
|
playwright.expect,
|
|
1337
|
+
TEST_SHIM,
|
|
1330
1338
|
);
|
|
1331
1339
|
// Attached before the race: a rejection arriving after the deadline
|
|
1332
1340
|
// would otherwise be an unhandled rejection, and Node kills the
|