@gfdelarue/testkit 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +455 -56
- package/bin/testkit-windows-owner-arm64.exe +0 -0
- package/bin/testkit-windows-owner-x64.exe +0 -0
- package/dist/app/bugs/batch.js +1 -0
- package/dist/app/bugs/ci-report.js +2 -2
- package/dist/app/bugs/ci-summary.js +3 -3
- package/dist/app/bugs/draft.js +2 -2
- package/dist/app/bugs/file.js +1 -1
- package/dist/app/bugs/guide.js +1 -1
- package/dist/app/bugs/record.js +1 -1
- package/dist/app/bugs/run-reading.js +1 -1
- package/dist/app/bugs/store.js +1 -1
- package/dist/app/campaigns/summary.js +1 -1
- package/dist/app/cleanup/index.js +1 -1
- package/dist/app/db-schema/prepare.js +1 -1
- package/dist/app/db-schema/refresh.js +1 -1
- package/dist/app/db-schema/verify.js +1 -1
- package/dist/app/destroy/index.js +1 -1
- package/dist/app/environments/config-driven.js +1 -1
- package/dist/app/environments/file-setup.js +1 -1
- package/dist/app/environments/routing.js +1 -1
- package/dist/app/environments/static-values.js +1 -1
- package/dist/app/environments/supplied.js +1 -1
- package/dist/app/lint/shared.js +1 -1
- package/dist/app/run/live-run.js +1 -1
- package/dist/app/run/orchestrator.js +1 -1
- package/dist/app/run/planning.js +1 -1
- package/dist/app/run/results.js +1 -1
- package/dist/app/run/run-finalization.js +1 -1
- package/dist/app/run/run-finisher.js +1 -0
- package/dist/app/run/setup/runtime-node-setup-protocol.js +1 -1
- package/dist/app/run/setup/runtime-node-setup-worker.js +1 -1
- package/dist/app/run/setup/runtime-node-setup.js +2 -2
- package/dist/app/status/runs.js +1 -1
- package/dist/app/stop/index.js +1 -1
- package/dist/app/tracker/index.js +1 -1
- package/dist/app/tracker/token.js +2 -2
- package/dist/app/typecheck/index.js +4 -4
- package/dist/cli/command-catalog.js +1 -1
- package/dist/cli/command-flags.js +1 -1
- package/dist/cli/commands/bug/file.js +3 -2
- package/dist/cli/framework/help.js +5 -5
- package/dist/cli/framework/parser.js +1 -1
- package/dist/cli/json-errors.js +1 -1
- package/dist/cli/renderers/bug/text.js +1 -1
- package/dist/cli/renderers/run/formatting.js +1 -1
- package/dist/config/loader/config-loader.js +1 -1
- package/dist/config/loader/discover-tests.js +1 -1
- package/dist/config/loader/discovery-result.js +1 -0
- package/dist/config/loader/index.js +1 -1
- package/dist/config/loader/runtime-isolation.js +1 -1
- package/dist/config/loader/skip-config.js +1 -1
- package/dist/config/loader/test-selection.js +1 -1
- package/dist/database/admin.js +5 -2
- package/dist/database/call-caps.js +1 -0
- package/dist/database/cleanup.js +1 -1
- package/dist/database/configured-steps.js +1 -1
- package/dist/database/constants.js +1 -1
- package/dist/database/docker-endpoint.js +1 -0
- package/dist/database/docker-metadata.js +2 -2
- package/dist/database/docker.js +5 -5
- package/dist/database/index.js +2 -1
- package/dist/database/lifecycle-cleanup.js +1 -1
- package/dist/database/local-postgres.js +4 -1
- package/dist/database/naming.js +1 -1
- package/dist/database/operation-deadline.js +1 -0
- package/dist/database/postgres/connection-check.js +1 -1
- package/dist/database/postgres/index.js +2 -2
- package/dist/database/postgres/owned-exec.js +27 -0
- package/dist/database/postgres/protocol.js +2 -2
- package/dist/database/provider.js +1 -0
- package/dist/database/roles.js +2 -2
- package/dist/database/schema-operations.js +5 -1
- package/dist/database/schema-source.js +3 -3
- package/dist/database/steps.js +1 -1
- package/dist/database/template-steps.js +4 -4
- package/dist/domain/suite-selection.js +1 -1
- package/dist/engines/adapters/evidence-files.js +1 -1
- package/dist/engines/adapters/execution.js +1 -1
- package/dist/engines/node-vitest/adapter/adapter.js +1 -1
- package/dist/engines/node-vitest/adapter/driver.js +2 -2
- package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
- package/dist/engines/playwright/adapter/adapter.js +1 -1
- package/dist/engines/playwright/adapter/browser-network-evidence.js +4 -0
- package/dist/engines/playwright/adapter/config.js +16 -6
- package/dist/engines/playwright/adapter/evidence.js +2 -1
- package/dist/engines/playwright/adapter/failure-details.js +2 -2
- package/dist/engines/playwright/adapter/reporter.js +1 -1
- package/dist/engines/playwright/runtime/browser-network.js +1 -0
- package/dist/engines/playwright/runtime/browser-traffic.js +1 -0
- package/dist/engines/playwright/runtime/chromium-environment.js +1 -0
- package/dist/engines/playwright/runtime/emulation.js +1 -0
- package/dist/engines/playwright/runtime/index.js +1 -1
- package/dist/engines/playwright/runtime/managed-test.js +1 -1
- package/dist/engines/playwright/runtime/runtime-input.js +2 -2
- package/dist/environment/lifecycle-controller.js +1 -1
- package/dist/environment/lifecycle.js +1 -1
- package/dist/environment/maintenance.js +1 -1
- package/dist/environment/run-environment.js +1 -1
- package/dist/environment/run-stopping.js +1 -1
- package/dist/environment/runtime-manager.js +1 -1
- package/dist/evidence/artifacts.js +5 -2
- package/dist/evidence/bundles.js +2 -1
- package/dist/evidence/compressed-artifacts.js +1 -0
- package/dist/evidence/environment-record.js +1 -1
- package/dist/evidence/index.js +1 -1
- package/dist/evidence/live-run-summary.js +1 -0
- package/dist/evidence/local-paths.js +1 -0
- package/dist/evidence/maintenance.js +1 -1
- package/dist/evidence/operation-timings.js +1 -1
- package/dist/evidence/redaction.js +4 -4
- package/dist/evidence/run-evidence.js +1 -0
- package/dist/evidence/setup-progress.js +1 -1
- package/dist/evidence/storage-inventory.js +1 -1
- package/dist/evidence/storage.js +2 -2
- package/dist/kernel/redaction-targets.js +1 -1
- package/dist/kernel/redaction.js +7 -4
- package/dist/process/engine-environment-keys.js +1 -0
- package/dist/process/environment-overlay.js +1 -1
- package/dist/process/environment.js +1 -1
- package/dist/process/execa-bundle.json +3 -3
- package/dist/process/files/evidence-writes.js +1 -0
- package/dist/process/files/product-dir.js +1 -1
- package/dist/process/host-process.js +1 -1
- package/dist/process/index.js +1 -1
- package/dist/process/lifecycle-fence.js +1 -1
- package/dist/process/lock-progress.js +2 -0
- package/dist/process/locks.js +4 -4
- package/dist/process/process-identity.js +1 -1
- package/dist/process/process.js +35 -35
- package/dist/process/recovery.js +1 -1
- package/dist/process/runs/manifest.js +1 -1
- package/dist/process/runs/ownership.js +1 -1
- package/dist/process/runs/stop.js +1 -1
- package/dist/process/shutdown.js +1 -1
- package/dist/process/windows-native.js +1 -1
- package/dist/repo/history.js +7 -4
- package/dist/repo/managed-git.js +1 -1
- package/dist/repo/state.js +4 -4
- package/dist/steps/environment.js +1 -1
- package/dist/toolchains/node-provisioning.js +2 -2
- package/dist/tracker/gitea.js +1 -1
- package/dist/tracker/github.js +1 -1
- package/dist/tracker/http.js +1 -1
- package/dist/tracker/ownership.js +1 -0
- package/dist/tracker/redaction.js +1 -1
- package/dist/tracker/responses.js +1 -1
- package/dist/types/playwright.d.ts +21 -0
- package/dist/types/workspace.d.ts +4 -4
- package/docs/bug-records.md +16 -10
- package/docs/engines.md +52 -4
- package/docs/release-notes.md +730 -0
- package/package.json +14 -2
- package/testkit-config-schema.md +36 -42
- package/dist/config/loader/task-policy.js +0 -1
- package/dist/environment/lifecycle-artifacts.js +0 -1
package/README.md
CHANGED
|
@@ -50,13 +50,18 @@ whose empty selection is intentional; it should not be the default CI command.
|
|
|
50
50
|
|
|
51
51
|
Testkit writes the immutable, verifiable run bundle under
|
|
52
52
|
`.testkit/results/runs/<run-id>/`. Upload `.testkit/results` with `if: always()`
|
|
53
|
-
when CI evidence should survive a failed job.
|
|
53
|
+
when CI evidence should survive a failed job. If the job can kill Testkit before
|
|
54
|
+
it seals the run, run `testkit cleanup` before the upload, so the run is sealed
|
|
55
|
+
and redacted first. See
|
|
56
|
+
[Read run evidence without the CLI](#read-run-evidence-without-the-cli).
|
|
54
57
|
|
|
55
58
|
## Report a run to GitHub or Gitea
|
|
56
59
|
|
|
57
60
|
`testkit ci report` reads the last run in the checkout and posts it in three
|
|
58
61
|
places: one commit status, the job's step summary, and one pull request comment
|
|
59
|
-
that it updates in place.
|
|
62
|
+
that it updates in place. It updates only a comment that its token's user
|
|
63
|
+
wrote, and posts a new comment when the tracker won't say who that user is. It
|
|
64
|
+
leaves out a CI run or issue link that holds credentials or a secret.
|
|
60
65
|
|
|
61
66
|
```yaml
|
|
62
67
|
permissions:
|
|
@@ -80,10 +85,17 @@ steps:
|
|
|
80
85
|
it, as the `env` lines above do. Gitea Actions accepts the same lines.
|
|
81
86
|
- Testkit reads the token from `GITHUB_TOKEN` or `GH_TOKEN` for GitHub, and
|
|
82
87
|
from `GITEA_TOKEN` for Gitea. In a Gitea Actions job it also reads
|
|
83
|
-
`GITHUB_TOKEN`.
|
|
84
|
-
that `
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
`GITHUB_TOKEN`. Outside such a job, it sends `GITEA_TOKEN` only to the host
|
|
89
|
+
that `GITEA_SERVER_URL` names, such as `https://gitea.example.com`. With no
|
|
90
|
+
token set, it asks `git credential fill`. The token that `actions/checkout`
|
|
91
|
+
keeps in the checkout does not answer that request.
|
|
92
|
+
- Testkit sends a token only over HTTPS. A plain `http://` remote or repository
|
|
93
|
+
URL works only for a loopback host such as `127.0.0.1`.
|
|
94
|
+
- A job's token reaches only the job's own repository. When `bugs.repository`
|
|
95
|
+
names another one, give the step a token that may write to it.
|
|
96
|
+
- Testkit reaches GitHub Enterprise Server, and GitHub Enterprise Cloud with
|
|
97
|
+
data residency (`*.ghe.com`), only from a GitHub Actions job on that host, so
|
|
98
|
+
`testkit bug file` works there only in a job.
|
|
87
99
|
- GitHub gives a pull request from a fork a read-only token. `ci report` then
|
|
88
100
|
writes the step summary and fails when it posts the status. Add
|
|
89
101
|
`continue-on-error: true` to the step when the repository takes pull requests
|
|
@@ -99,7 +111,7 @@ steps:
|
|
|
99
111
|
Testkit files a confirmed bug as an issue in the repository's GitHub or Gitea.
|
|
100
112
|
The issue names the regression test that fails because of the bug, the commit
|
|
101
113
|
that introduced the faulty lines, and the command that reproduces the failure.
|
|
102
|
-
[
|
|
114
|
+
[`docs/bug-records.md`](https://app.unpkg.com/@gfdelarue/testkit/files/docs/bug-records.md) gives the issue format.
|
|
103
115
|
|
|
104
116
|
1. Write a Testkit test that fails because of the bug.
|
|
105
117
|
2. Write the bug record as a JSON file. `testkit bug guide` prints the fields
|
|
@@ -111,8 +123,15 @@ that introduced the faulty lines, and the command that reproduces the failure.
|
|
|
111
123
|
|
|
112
124
|
`testkit bug file` runs the test at the checked-out commit and files the issue
|
|
113
125
|
only when the test fails. It stops when tracked files have uncommitted changes,
|
|
114
|
-
when no remote
|
|
115
|
-
the test.
|
|
126
|
+
when no branch on the remote contains the commit, or when an open issue already
|
|
127
|
+
covers the test. It asks the remote which branches hold the commit, with the
|
|
128
|
+
checkout's Git credentials, so a branch that was rewound or deleted there
|
|
129
|
+
doesn't count. When it can't tell, because a remote doesn't answer or a shallow
|
|
130
|
+
clone's history stops short, it stops and says why. After it files, it reads the open issues again. If another checkout
|
|
131
|
+
filed the same test at the same time and its issue is older, Testkit closes its
|
|
132
|
+
own issue and stops, naming the older one. GitHub can list a new issue a few
|
|
133
|
+
seconds late, so Testkit waits up to 7.5 seconds for its own and says so when
|
|
134
|
+
it doesn't appear. The token needs permission to create issues and labels.
|
|
116
135
|
|
|
117
136
|
`testkit bug record` saves the `--vitest-config`, `--vitest-project`, and
|
|
118
137
|
`--vitest-runner` it was given in the draft, and asks for `--vitest-project`
|
|
@@ -131,6 +150,38 @@ fragment. It refuses `--database` and `--env`; pass those values through
|
|
|
131
150
|
root, so a relative `--environment-file` needs `testkit bug file` run from
|
|
132
151
|
there too.
|
|
133
152
|
|
|
153
|
+
`testkit bug file --batch` files several drafts in one call: the ids it is
|
|
154
|
+
given, or every unfiled draft under `.testkit/bugs/` when it is given none.
|
|
155
|
+
|
|
156
|
+
- It checks the checkout once and reads the open issues before it runs the
|
|
157
|
+
tests. It stops before it files anything when tracked files have uncommitted
|
|
158
|
+
changes, when no remote branch contains the commit, or when it cannot read
|
|
159
|
+
the open issues.
|
|
160
|
+
- It runs the drafts' tests in one `testkit run`, or one run for each
|
|
161
|
+
`--vitest-config`, `--vitest-project`, and `--vitest-runner` the drafts were
|
|
162
|
+
recorded with. Each draft is judged by its own file's result, so a file that
|
|
163
|
+
is skipped or fails outside its tests rules out only its own draft.
|
|
164
|
+
- It reads the open issues again before it files, in case another checkout
|
|
165
|
+
filed one of the tests meanwhile.
|
|
166
|
+
- It reports one line per draft: filed, already filed, rejected with the
|
|
167
|
+
reason, or not filed.
|
|
168
|
+
- A draft is rejected when its own checks fail, for example when its test
|
|
169
|
+
passes or an open issue already covers the test, and the batch carries on.
|
|
170
|
+
- When the tracker refuses a request, the batch stops filing and reports the
|
|
171
|
+
drafts it has not reached as not filed.
|
|
172
|
+
- It exits non-zero when any draft is rejected or not filed. `--json` returns
|
|
173
|
+
the same list.
|
|
174
|
+
|
|
175
|
+
Each draft records its issue as soon as the tracker returns it, so running the
|
|
176
|
+
batch again files only what is left. An open issue whose facts match the ones a
|
|
177
|
+
draft would file counts as that draft's issue, so a batch stopped between the
|
|
178
|
+
two steps does not file the bug twice. When the tracker creates an issue
|
|
179
|
+
without its labels, the batch stops, the draft stays unfiled, and Testkit
|
|
180
|
+
closes that issue. If it can't, the message says so, and you close the issue.
|
|
181
|
+
Then run the batch again with a token that may label issues. The batch creates
|
|
182
|
+
at most one issue a second. When the tracker limits the rate, it stops and
|
|
183
|
+
says when to run it again.
|
|
184
|
+
|
|
134
185
|
### Name the product's areas
|
|
135
186
|
|
|
136
187
|
Add `areas` to `testkit.config.ts` so that each issue names the part of the
|
|
@@ -147,7 +198,7 @@ export default defineConfig({
|
|
|
147
198
|
|
|
148
199
|
`testkit bug file` puts an `area:<id>` label on the issue. If no area claims
|
|
149
200
|
the fault's file, it stops and names the file. Without `areas`, Testkit files
|
|
150
|
-
bugs with no area. See
|
|
201
|
+
bugs with no area. See the `areas` section of [`testkit-config-schema.md`](https://app.unpkg.com/@gfdelarue/testkit/files/testkit-config-schema.md).
|
|
151
202
|
|
|
152
203
|
### Pass a run whose failures are all filed
|
|
153
204
|
|
|
@@ -163,7 +214,8 @@ export default defineConfig({
|
|
|
163
214
|
A run then passes when an open issue with the `testkit` label covers each
|
|
164
215
|
failure. A new failure still fails the run, and closing an issue makes its
|
|
165
216
|
test count again. When Testkit cannot read the issues, every failure is new.
|
|
166
|
-
|
|
217
|
+
The Known failures section of [`docs/bug-records.md`](https://app.unpkg.com/@gfdelarue/testkit/files/docs/bug-records.md) gives the
|
|
218
|
+
exact rule.
|
|
167
219
|
|
|
168
220
|
With `"allow"`, the run reads the open issues. In CI, pass the job's token to
|
|
169
221
|
the `testkit run` step as well:
|
|
@@ -175,7 +227,9 @@ steps:
|
|
|
175
227
|
GITHUB_TOKEN: ${{ github.token }}
|
|
176
228
|
```
|
|
177
229
|
|
|
178
|
-
The job also needs the `issues: read` permission.
|
|
230
|
+
The job also needs the `issues: read` permission. Testkit reads
|
|
231
|
+
`GITHUB_TOKEN`, `GH_TOKEN` and `GITEA_TOKEN` itself and doesn't pass them to
|
|
232
|
+
services or setup steps. A service that needs one declares it in its `env`.
|
|
179
233
|
|
|
180
234
|
### Run a campaign
|
|
181
235
|
|
|
@@ -293,10 +347,10 @@ npx @gfdelarue/testkit setup playwright --with-deps # CI and fresh hosts
|
|
|
293
347
|
```
|
|
294
348
|
|
|
295
349
|
Normal runs never install or download a browser.
|
|
296
|
-
[
|
|
297
|
-
the supported platforms, and how to recover a broken
|
|
298
|
-
|
|
299
|
-
|
|
350
|
+
The Installation section of [`docs/engines.md`](https://app.unpkg.com/@gfdelarue/testkit/files/docs/engines.md) covers
|
|
351
|
+
what an install contains, the supported platforms, and how to recover a broken
|
|
352
|
+
one. Its Typechecking section covers the supported TypeScript versions and
|
|
353
|
+
`tsconfig.playwright.json`.
|
|
300
354
|
|
|
301
355
|
Testkit bounds the output it captures from each command it runs, and asks the
|
|
302
356
|
command and its descendants to stop on cancellation or timeout.
|
|
@@ -314,19 +368,20 @@ reserves `APPDATA`, `ComSpec`, `HOME`, `LANG`, `LC_ALL`, `LOCALAPPDATA`, `PATH`,
|
|
|
314
368
|
|
|
315
369
|
- Node/Vitest reserves `BASE_URL`, `FORCE_COLOR`, `NODE_OPTIONS`, `NO_COLOR`,
|
|
316
370
|
`UPDATE_SNAPSHOT`, `VITEST_POOL_ID`, `VITEST_WORKER_ID`, and the `TESTKIT_`
|
|
317
|
-
keys it sets itself, listed in
|
|
318
|
-
[
|
|
371
|
+
keys it sets itself, listed in the Environment section of
|
|
372
|
+
[`docs/engines.md`](https://app.unpkg.com/@gfdelarue/testkit/files/docs/engines.md);
|
|
319
373
|
other `TESTKIT_` and `VITEST_` keys, such as
|
|
320
374
|
`TESTKIT_REQUIRE_COMPLETE_SUITE`, pass through, though Vitest's own `TEST`,
|
|
321
375
|
`VITEST`, `VITEST_MODE`, `FORCE_TTY`, `MODE`, `DEV`, `PROD`, and `SSR`
|
|
322
376
|
replace a group's values;
|
|
323
377
|
- Playwright reserves the `TESTKIT_`, `PLAYWRIGHT_`, `PWTEST_`, and `PW_TEST_`
|
|
324
|
-
prefixes plus `BASE_URL`, `
|
|
378
|
+
prefixes plus `BASE_URL`, `DBUS_SESSION_BUS_ADDRESS`, `FC_FONTATIONS`,
|
|
379
|
+
`FORCE_COLOR`, `NODE_OPTIONS`, and `NO_COLOR`.
|
|
325
380
|
|
|
326
381
|
Case-variant duplicates and invalid names or NUL-containing values fail instead
|
|
327
382
|
of being normalized. Node/Vitest accepts at most 512 entries and
|
|
328
383
|
4 MiB of UTF-8 key/value data. Playwright accepts at most 512 entries and 2 MiB
|
|
329
|
-
of UTF-8 key/value data. [
|
|
384
|
+
of UTF-8 key/value data. The Environment section of [`docs/engines.md`](https://app.unpkg.com/@gfdelarue/testkit/files/docs/engines.md) lists the
|
|
330
385
|
complete limits, and [Named endpoints and databases](#named-endpoints-and-databases)
|
|
331
386
|
covers the environment API.
|
|
332
387
|
|
|
@@ -402,7 +457,7 @@ which `taskConcurrency` doesn't limit.
|
|
|
402
457
|
`execution.workers`, `presets.nodeNext({ workers })` and `--workers` are
|
|
403
458
|
deprecated aliases. When both names are
|
|
404
459
|
provided in config or on the CLI, their values must match. The CLI value overrides
|
|
405
|
-
config after those checks. See the
|
|
460
|
+
config after those checks. See the `execution` section of [`testkit-config-schema.md`](https://app.unpkg.com/@gfdelarue/testkit/files/testkit-config-schema.md).
|
|
406
461
|
|
|
407
462
|
Batch `run` output stays intentionally short: one line per completed file,
|
|
408
463
|
concise failure blocks, and a final summary. Service logs, captured runtime
|
|
@@ -498,7 +553,7 @@ The environment file is JSON:
|
|
|
498
553
|
"handles": {
|
|
499
554
|
"webhook": {
|
|
500
555
|
"kind": "http-capture",
|
|
501
|
-
"url": "http://127.0.0.1:41000/webhook",
|
|
556
|
+
"url": "http://127.0.0.1:41000/__testkit__/<segment>/webhook",
|
|
502
557
|
"requestsUrl": "http://127.0.0.1:41000/__testkit__/<capability>/requests",
|
|
503
558
|
"resetUrl": "http://127.0.0.1:41000/__testkit__/<capability>/requests"
|
|
504
559
|
}
|
|
@@ -589,7 +644,7 @@ assertLocalDatabaseUrl(process.env, "seed.ts");
|
|
|
589
644
|
|
|
590
645
|
## Setup
|
|
591
646
|
|
|
592
|
-
See
|
|
647
|
+
See [`testkit-config-schema.md`](https://app.unpkg.com/@gfdelarue/testkit/files/testkit-config-schema.md) for the complete,
|
|
593
648
|
strict `testkit.config.ts` contract.
|
|
594
649
|
|
|
595
650
|
Create `testkit.config.ts` at repo root:
|
|
@@ -675,7 +730,7 @@ neither:
|
|
|
675
730
|
import { defineFile } from "@gfdelarue/testkit";
|
|
676
731
|
|
|
677
732
|
export const testkit = defineFile({
|
|
678
|
-
engine: "
|
|
733
|
+
engine: "playwright",
|
|
679
734
|
skip: "Billing is currently unavailable locally",
|
|
680
735
|
});
|
|
681
736
|
```
|
|
@@ -734,6 +789,10 @@ it. Database changes for one product run one at a time; other products don't
|
|
|
734
789
|
wait for them. Use `cleanup --resources --dry-run` to preview the selected
|
|
735
790
|
product's cleanup.
|
|
736
791
|
|
|
792
|
+
A run's start and its post-run maintenance don't list or reclaim Docker
|
|
793
|
+
resources. A run stops its retained database by name when it releases the run's
|
|
794
|
+
resources.
|
|
795
|
+
|
|
737
796
|
`database.template` is the database-side equivalent for reusable template DB
|
|
738
797
|
state. When `database.sourceSchema` is configured, Testkit treats the configured
|
|
739
798
|
source database as the schema source of truth. A normal `testkit run` resolves a
|
|
@@ -844,6 +903,8 @@ services: {
|
|
|
844
903
|
}
|
|
845
904
|
```
|
|
846
905
|
|
|
906
|
+
## Run evidence
|
|
907
|
+
|
|
847
908
|
Every terminal run is sealed under `.testkit/results/runs/<run-id>/` with a
|
|
848
909
|
canonical `run.json`, `provenance.json`, `manifest.json`, and the run's exact
|
|
849
910
|
artifact/log/setup files. Payloads are SHA-256 addressed under
|
|
@@ -869,25 +930,56 @@ workspace API.
|
|
|
869
930
|
Sealing replaces secrets with `<redacted>` in the run record and in every text
|
|
870
931
|
file of the bundle. A key names a secret when it holds `PASSWORD`, `TOKEN`,
|
|
871
932
|
`SECRET`, `PRIVATE_KEY`, `AUTH`, `COOKIE`, `SESSION`, or `API_KEY` in any letter
|
|
872
|
-
case
|
|
933
|
+
case, or when its last word is `PASS`, `PASSWD`, `PAT`, `DSN`, `CREDENTIAL`, or
|
|
934
|
+
`CREDENTIALS`. `AUTH` doesn't count in `AUTHOR` or `AUTHORITY`, so
|
|
935
|
+
`GIT_AUTHOR_NAME` names no secret, though `AUTHORIZATION` still does. `SESSION`
|
|
936
|
+
doesn't count before `COUNT`, `DURATION`, `EXPIR`, `LENGTH`, `LIMIT`, `MAX`,
|
|
937
|
+
`TIMEOUT`, or `TTL`, as in `sessionCount` and `SESSION_TIMEOUT`. A key whose
|
|
938
|
+
last two words are `API`, `PRIVATE`, `SIGNING`, or `ENCRYPTION` and then `KEY`,
|
|
939
|
+
as in `apiKey`, `x-api-key`, and `privateKey`, names a secret too, and so does
|
|
940
|
+
an upper-case key whose last word is `KEY`, such as `STRIPE_KEY`, unless the
|
|
941
|
+
word before it is `PUBLIC` or `PUBLISHABLE`. Words break at any character other
|
|
942
|
+
than a letter or a digit, and before an upper-case letter that follows a
|
|
943
|
+
lower-case letter or a digit. Sealing redacts:
|
|
873
944
|
|
|
874
945
|
- A `KEY=value` assignment under such a key, an `Authorization`,
|
|
875
|
-
`Proxy-Authorization`, `Cookie`,
|
|
876
|
-
value under such a key, even when the key is
|
|
877
|
-
single-quoted
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
946
|
+
`Proxy-Authorization`, `Cookie`, `Set-Cookie`, `Api-Key`, or `X-Api-Key`
|
|
947
|
+
header value, a JSON string value under such a key, even when the key is
|
|
948
|
+
spelled with escapes, a single-quoted string value under such a key, a token
|
|
949
|
+
of at least eight characters with a digit after `Bearer`, and a URL's
|
|
950
|
+
password, wherever they appear. A
|
|
951
|
+
quoted value ends at its closing quote and keeps its quotes, and a value that
|
|
952
|
+
starts with an auth scheme such as `Bearer` ends with its line. A secret in
|
|
953
|
+
one JSON value leaves the record's other values as they are.
|
|
954
|
+
- Every string in a JSON array or object under such a key, unless the key
|
|
955
|
+
names a secret only through `AUTH`, `COOKIE`, or `SESSION`, so `tokens`,
|
|
956
|
+
`credentials`, and `privateKey` count and `session` and `auth` settings stay.
|
|
957
|
+
In an object with a `name` string and a `value` string, only the `value` and
|
|
958
|
+
members whose own key names a secret count. Numbers and booleans stay.
|
|
959
|
+
- The value after `password:` or `passwd:`, or after a key whose last word is
|
|
960
|
+
one of those: a quoted value anywhere, as in `{ password: 'hunter2' }`, and
|
|
961
|
+
an unquoted value at the start of a line, as in YAML, when it is the line's
|
|
962
|
+
one word. A value that is one word of letters, perhaps followed by
|
|
963
|
+
punctuation such as `!` or `.`, stays, and so does an unquoted value after
|
|
964
|
+
other text, so `password: 'Required'`, `password: required!`, and
|
|
965
|
+
`rejects a password: too short` stay.
|
|
966
|
+
- A token of at least 20 characters with a digit that stands alone as the user
|
|
967
|
+
of an `http` or `https` URL, as in `https://ghp_…@github.com`. A URL's
|
|
968
|
+
password may hold `/` and `@`, and sealing also finds it in a directory name,
|
|
969
|
+
which keeps one slash of the URL, as in `postgres:/app:…@db`.
|
|
970
|
+
- A header or cookie stored as a `{"name": …, "value": …}` pair, as Playwright
|
|
971
|
+
traces and HAR files store them: the value when the name names a secret, and
|
|
972
|
+
every value in a `cookies` array.
|
|
881
973
|
- The value of each such key, and of each key that `redact` names, in the
|
|
882
|
-
environment Testkit runs in. When the value is a URL, its password
|
|
883
|
-
too.
|
|
974
|
+
environment Testkit runs in. When the value is a URL, its password, or a
|
|
975
|
+
token that stands alone as its user, counts too.
|
|
884
976
|
- The values the run's environment resolved: each value under such a key or a
|
|
885
|
-
key that `redact` names, and the password of each URL. A
|
|
886
|
-
password may also be a `password` query parameter, as libpq
|
|
887
|
-
covers config `env`, env files, `local.env`, dependency
|
|
888
|
-
readiness, UI, and database URLs, `--env`, `--env-from`, and
|
|
889
|
-
`--environment-file`. A value under such a key that is a URL counts only for
|
|
890
|
-
its password.
|
|
977
|
+
key that `redact` names, and the password or token user of each URL. A
|
|
978
|
+
PostgreSQL URL's password may also be a `password` query parameter, as libpq
|
|
979
|
+
reads it. This covers config `env`, env files, `local.env`, dependency
|
|
980
|
+
services, base, readiness, UI, and database URLs, `--env`, `--env-from`, and
|
|
981
|
+
an `--environment-file`. A value under such a key that is a URL counts only for
|
|
982
|
+
its password or token user.
|
|
891
983
|
- The unguessable segment in each [HTTP capture's](#handles) operation URLs,
|
|
892
984
|
from a setup module's capture or an `--environment-file` handle.
|
|
893
985
|
|
|
@@ -909,12 +1001,41 @@ and keys in the free-form data and metadata that tests attach. When output cuts
|
|
|
909
1001
|
the cut with `…` or `...`, sealing redacts the part that shows. `testkit run
|
|
910
1002
|
--json` and the run's progress output apply the same redaction.
|
|
911
1003
|
|
|
1004
|
+
Sealing also redacts inside `.zip` and `.gz` files, and files that start like
|
|
1005
|
+
one, such as a Playwright `trace.zip`. It applies the rules above to each text
|
|
1006
|
+
entry, such as a trace's actions, network log, and page and API responses, and
|
|
1007
|
+
writes the archive again with the redacted entries. A text entry is valid UTF-8
|
|
1008
|
+
without binary control characters, or UTF-16 with a byte order mark. Other
|
|
1009
|
+
entries keep their bytes. A ZIP with a comment or extra fields is written again
|
|
1010
|
+
without them, and a gzip file is always written again without the name and
|
|
1011
|
+
comment in its header. A trace stays readable in the Playwright trace viewer,
|
|
1012
|
+
with `<redacted>` where a typed password or a token was.
|
|
1013
|
+
|
|
1014
|
+
Sealing refuses an archive it can't redact:
|
|
1015
|
+
|
|
1016
|
+
- one it can't read: encrypted, needing ZIP64 sizes, compressed with a method
|
|
1017
|
+
other than deflate, larger than 512 MiB once expanded, or damaged, including
|
|
1018
|
+
a ZIP with bytes that none of its entries account for;
|
|
1019
|
+
- one with a binary entry, such as a screenshot, that holds one of the values
|
|
1020
|
+
above, as UTF-8 or UTF-16;
|
|
1021
|
+
- one that holds another archive.
|
|
1022
|
+
|
|
1023
|
+
Sealing removes each file it refuses from the run's results and fails the run
|
|
1024
|
+
with the reason, for example:
|
|
1025
|
+
|
|
1026
|
+
```text
|
|
1027
|
+
Refusing to archive compressed file whose entry inner/trace.zip is itself an archive, and removed it: …/.testkit/results/artifacts/frontend/task-1-01-bundle.zip
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
The run, or the `testkit stop`, `testkit cleanup` or next run that seals it
|
|
1031
|
+
later, then seals the rest of its results and records the reason.
|
|
1032
|
+
|
|
912
1033
|
Some secrets stay visible:
|
|
913
1034
|
|
|
914
1035
|
- What a setup module prints goes to the terminal as written.
|
|
915
|
-
- Sealing does not
|
|
916
|
-
|
|
917
|
-
above
|
|
1036
|
+
- Sealing does not redact images or video, including the screenshots in a
|
|
1037
|
+
trace. Text typed into a page shows in them. Sealing refuses and removes a
|
|
1038
|
+
binary file that holds one of the values above as UTF-8 or UTF-16.
|
|
918
1039
|
- When a run's process is killed before it seals the run, `testkit stop`,
|
|
919
1040
|
`testkit cleanup`, or the next run seals it from another process. That
|
|
920
1041
|
process does not have the values the run's environment resolved or the
|
|
@@ -922,6 +1043,15 @@ Some secrets stay visible:
|
|
|
922
1043
|
values under the words above. The bundle's `archive.redaction.status` is then
|
|
923
1044
|
`source-targets-unavailable` instead of `applied`.
|
|
924
1045
|
|
|
1046
|
+
Sealing also takes out local paths. In the run record, in every text file of
|
|
1047
|
+
the bundle, and in every text entry of a compressed file, a path under the
|
|
1048
|
+
product directory becomes relative to it, the product directory itself becomes
|
|
1049
|
+
`.`, and a path under your home directory starts with `~`. This covers engine
|
|
1050
|
+
reports, failure stacks, logs, and Playwright traces, and happens after
|
|
1051
|
+
redaction. Binary files and binary entries keep their paths. When sealing
|
|
1052
|
+
changes a file, the file's size and SHA-256 in `run.json` describe the sealed
|
|
1053
|
+
file.
|
|
1054
|
+
|
|
925
1055
|
Use the artifact archive config to opt into pruning or atomic publication to
|
|
926
1056
|
an external directory. Local retention is indefinite by default:
|
|
927
1057
|
|
|
@@ -963,6 +1093,73 @@ Programmatic consumers can import `listRunBundles`, `loadRunBundleArtifact`,
|
|
|
963
1093
|
`loadRunBundleManifest`, and `verifyRunBundle` from
|
|
964
1094
|
`@gfdelarue/testkit/workspace`.
|
|
965
1095
|
|
|
1096
|
+
### Read run evidence without the CLI
|
|
1097
|
+
|
|
1098
|
+
Run evidence is plain files, so you can investigate a run with `ls`, `grep`,
|
|
1099
|
+
`jq`, and `sha256sum` when `testkit` can't start, for example because the
|
|
1100
|
+
config no longer loads. A command that fails to load the config stops before it
|
|
1101
|
+
writes to `.testkit/results`, and `testkit artifacts` doesn't need the config.
|
|
1102
|
+
|
|
1103
|
+
| Path under `.testkit/results/` | What it holds |
|
|
1104
|
+
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1105
|
+
| `runs/<run-id>/run.json` | A sealed run's record: `run.status`, `run.interruptReason`, and each file's `status`, `failureDetails`, and `artifacts` |
|
|
1106
|
+
| `runs/<run-id>/manifest.json` | Every other file in the bundle, with its `sha256` and `bytes` |
|
|
1107
|
+
| `runs/<run-id>/provenance.json` | The run's CI, source, runtime, and redaction facts |
|
|
1108
|
+
| `runs/<run-id>/files/` | The run's `logs/`, `artifacts/`, and `setup/` files. Each `artifacts[].path` in `run.json` points here, relative to the product directory |
|
|
1109
|
+
| `latest.json` | The record of the last run that Testkit finished writing up, with its publication and retention results. A run killed after it was sealed can be in `runs/` but not here, so `runs/<run-id>/run.json` is the authority |
|
|
1110
|
+
| `live.json` | The record of a run that isn't sealed, with `run.status` `running` |
|
|
1111
|
+
| `logs/`, `artifacts/`, `setup/` | The files that unsealed run has written so far |
|
|
1112
|
+
|
|
1113
|
+
Anything else under `.testkit/` can change between releases, including
|
|
1114
|
+
`objects/`, which stores the bytes behind each bundle's files.
|
|
1115
|
+
|
|
1116
|
+
A run is sealed once `runs/<run-id>/` exists. Testkit writes the bundle under a
|
|
1117
|
+
hidden name in `runs/`, writes `manifest.json` last, and then renames the
|
|
1118
|
+
directory, so `runs/<run-id>/` is never partly written. Ignore names in `runs/`
|
|
1119
|
+
that start with `.`. A copy of a bundle, such as a CI upload, is whole when
|
|
1120
|
+
every file that its `manifest.json` lists has the listed `sha256`. A sealed
|
|
1121
|
+
`run.json` has `archive.status` `complete` and `run.status` `passed`, `failed`,
|
|
1122
|
+
or `interrupted`.
|
|
1123
|
+
|
|
1124
|
+
`live.json` means a run is in progress, or its process died before Testkit
|
|
1125
|
+
finished with it. If `runs/<run-id>/` exists for the `run.id` in `live.json`,
|
|
1126
|
+
the run was sealed and the bundle is its record: `live.json` and the active
|
|
1127
|
+
directories are leftovers, which the next `testkit run`, `cleanup`, or `destroy`
|
|
1128
|
+
removes. Testkit replaces `live.json` whole on each update, so it always parses.
|
|
1129
|
+
Its `generatedAt` is the time of the last update, and each file's `status` is
|
|
1130
|
+
what it was then, so a file that was still running shows `not_run`. The files
|
|
1131
|
+
in `logs/`, `artifacts/`, and `setup/` aren't redacted until the run is sealed.
|
|
1132
|
+
|
|
1133
|
+
How an unfinished run gets sealed:
|
|
1134
|
+
|
|
1135
|
+
- When Ctrl-C, `SIGTERM`, or `testkit stop` interrupts the run, or its process
|
|
1136
|
+
exits early, Testkit seals it straight away. `run.status` is `interrupted`,
|
|
1137
|
+
or `failed` when nothing asked the run to stop, and `run.interruptReason`
|
|
1138
|
+
says why.
|
|
1139
|
+
- When the process is killed with `SIGKILL`, by the out-of-memory killer, or
|
|
1140
|
+
with its host, the run stays unsealed until the next `testkit run`,
|
|
1141
|
+
`cleanup`, or `destroy` seals it, before doing anything else, with
|
|
1142
|
+
`run.status` `interrupted` and `run.interruptReason`
|
|
1143
|
+
`stale run process not active`. `testkit stop` seals it too, with its
|
|
1144
|
+
`--reason` or `stop requested`. The sealed `run.finishedAt` is when the
|
|
1145
|
+
command sealed it, not when the process died.
|
|
1146
|
+
|
|
1147
|
+
Testkit keeps what reached the disk. It can't keep output that a process hadn't
|
|
1148
|
+
written yet, anything written after the disk filled up or became read-only, or
|
|
1149
|
+
work that a killed process would have done on its way out. A log can end
|
|
1150
|
+
mid-line.
|
|
1151
|
+
|
|
1152
|
+
For example, to see what failed in the run that `latest.json` names and check
|
|
1153
|
+
its files:
|
|
1154
|
+
|
|
1155
|
+
```sh
|
|
1156
|
+
cd .testkit/results
|
|
1157
|
+
run=runs/$(jq -r .run.id latest.json)
|
|
1158
|
+
jq '.run.status, .run.interruptReason' "$run/run.json"
|
|
1159
|
+
jq '.groups[].suites[].files[] | select(.status != "passed") | {path, status, failureDetails, artifacts: [.artifacts[].path]}' "$run/run.json"
|
|
1160
|
+
jq -r '.files[] | "\(.sha256) \(.path)"' "$run/manifest.json" | (cd "$run" && sha256sum --check --quiet)
|
|
1161
|
+
```
|
|
1162
|
+
|
|
966
1163
|
## Authoring
|
|
967
1164
|
|
|
968
1165
|
### Named endpoints and databases
|
|
@@ -1044,6 +1241,14 @@ run's results, artifacts, and events, and the capture refuses any other
|
|
|
1044
1241
|
operation. When Testkit releases the file's environment, the capture closes and
|
|
1045
1242
|
the handle expires. Calling a handle never starts a sidecar or a service.
|
|
1046
1243
|
|
|
1244
|
+
The capture's `url` holds a second unguessable segment, which Testkit also
|
|
1245
|
+
keeps out of the run's records. The capture records a request only when its
|
|
1246
|
+
path starts with that segment, and answers any other path with 404. A captured
|
|
1247
|
+
request's `path` leaves the segment out: a request to `url` records `/webhook`,
|
|
1248
|
+
and one to `url` + `/42` records `/webhook/42`. The capture answers 403 to a
|
|
1249
|
+
request whose `Host` header isn't `127.0.0.1:<port>`, which keeps a web page
|
|
1250
|
+
from reaching it through DNS rebinding.
|
|
1251
|
+
|
|
1047
1252
|
- A name with no handle throws `EnvironmentValueMissingError` with `kind`
|
|
1048
1253
|
`"handle"`, and the test ends as an infrastructure failure, as for a missing
|
|
1049
1254
|
endpoint.
|
|
@@ -1068,7 +1273,8 @@ test, in any order.
|
|
|
1068
1273
|
so read them and leave them unchanged.
|
|
1069
1274
|
- When the test ends, leave its rows in place. Testkit resets the databases it
|
|
1070
1275
|
provisions. By default, every run starts from a fresh clone of the template
|
|
1071
|
-
database. See
|
|
1276
|
+
database. See `database.reset` in the `database` section of
|
|
1277
|
+
[`testkit-config-schema.md`](https://app.unpkg.com/@gfdelarue/testkit/files/testkit-config-schema.md).
|
|
1072
1278
|
- Clean up only what Testkit doesn't own, such as an account in a third-party
|
|
1073
1279
|
sandbox. Register the cleanup as soon as the test creates the account. Give
|
|
1074
1280
|
the account a unique name, so that a leftover from a cleanup that never ran
|
|
@@ -1125,8 +1331,24 @@ Files that name the same lock never run at the same time within one run.
|
|
|
1125
1331
|
Testkit doesn't share locks between runs, such as two CI jobs. Two tests that
|
|
1126
1332
|
need a lock because they share rows need their own rows instead.
|
|
1127
1333
|
|
|
1334
|
+
A plain Vitest file names its locks in its group's `requirements`:
|
|
1335
|
+
|
|
1128
1336
|
```ts
|
|
1129
|
-
|
|
1337
|
+
groups: {
|
|
1338
|
+
payments: {
|
|
1339
|
+
roots: ["src/payments"],
|
|
1340
|
+
uses: ["api"],
|
|
1341
|
+
requirements: {
|
|
1342
|
+
files: [{ path: "src/payments/webhook.test.ts", locks: ["payments-sandbox-webhook"] }],
|
|
1343
|
+
},
|
|
1344
|
+
},
|
|
1345
|
+
},
|
|
1346
|
+
```
|
|
1347
|
+
|
|
1348
|
+
A browser file can name them in `defineFile`:
|
|
1349
|
+
|
|
1350
|
+
```ts
|
|
1351
|
+
export const testkit = defineFile({ engine: "playwright", locks: ["payments-sandbox-webhook"] });
|
|
1130
1352
|
```
|
|
1131
1353
|
|
|
1132
1354
|
### Node setup beside a test
|
|
@@ -1153,9 +1375,10 @@ invocation. Files without setup still batch.
|
|
|
1153
1375
|
|
|
1154
1376
|
Setup runs with its own JavaScript state. It can use Node APIs, prepare
|
|
1155
1377
|
external resources, and start HTTP capture listeners. Tests
|
|
1156
|
-
reach a capture by its `id` through [`handle()`](#handles). A capture's
|
|
1157
|
-
`requestsUrl` and `resetUrl` hold
|
|
1158
|
-
of the run's records
|
|
1378
|
+
reach a capture by its `id` through [`handle()`](#handles). A capture's `url`,
|
|
1379
|
+
`requestsUrl` and `resetUrl` hold unguessable segments that Testkit keeps out
|
|
1380
|
+
of the run's records. Its capture `path` may not start with `/__testkit__/` or
|
|
1381
|
+
contain `.` or `..` segments.
|
|
1159
1382
|
The context contains `deadlineMs`, `signal`, `env`, `leaseDir`, `productDir`,
|
|
1160
1383
|
`serviceName`, `testFile`, and `sidecars`. `serviceName` holds the name of the
|
|
1161
1384
|
test's group. Setup has the same permissions as the Testkit process. It is not
|
|
@@ -1237,11 +1460,182 @@ test("lists one page of orders", async (context) => {
|
|
|
1237
1460
|
- A failed budget fails its test when the test finishes. The failure detail has
|
|
1238
1461
|
the kind `data-flow-budget` and a key made of the test's full title and the
|
|
1239
1462
|
budget.
|
|
1240
|
-
- Testkit writes `data-flow.json` and `data-flow.md` for a file whose tests
|
|
1463
|
+
- Testkit writes `data-flow.json` and `data-flow-report.md` for a file whose tests
|
|
1241
1464
|
recorded something. Each such test is one scenario, from its final attempt. A
|
|
1242
1465
|
test that records nothing adds nothing, and a file whose tests record
|
|
1243
1466
|
nothing gets no data-flow artifacts.
|
|
1244
1467
|
|
|
1468
|
+
### Slow the browser's network and CPU
|
|
1469
|
+
|
|
1470
|
+
`emulateNetwork(page, conditions)` and `emulateCpu(page, slowdown)` from
|
|
1471
|
+
`@gfdelarue/testkit/playwright` slow a page the way Chrome DevTools'
|
|
1472
|
+
throttling does. A `ui` test can use them to fail when a change makes a page
|
|
1473
|
+
unusable on a slow connection or a slow device:
|
|
1474
|
+
|
|
1475
|
+
```ts
|
|
1476
|
+
import { defineFile } from "@gfdelarue/testkit";
|
|
1477
|
+
import { emulateCpu, emulateNetwork, expect, test } from "@gfdelarue/testkit/playwright";
|
|
1478
|
+
|
|
1479
|
+
export const testkit = defineFile({ engine: "playwright" });
|
|
1480
|
+
|
|
1481
|
+
test("the dashboard is usable on a slow connection", async ({ page }) => {
|
|
1482
|
+
await emulateNetwork(page, "slow-4g");
|
|
1483
|
+
await page.goto("/dashboard");
|
|
1484
|
+
await expect(page.getByRole("heading", { name: "Orders" })).toBeVisible({ timeout: 10_000 });
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
test("the dashboard is usable on a slow phone", async ({ page }) => {
|
|
1488
|
+
await emulateCpu(page, 4);
|
|
1489
|
+
await page.goto("/dashboard");
|
|
1490
|
+
await expect(page.getByRole("heading", { name: "Orders" })).toBeVisible({ timeout: 10_000 });
|
|
1491
|
+
});
|
|
1492
|
+
```
|
|
1493
|
+
|
|
1494
|
+
The assertion's timeout is the time limit. The helpers add no timing or
|
|
1495
|
+
metrics API.
|
|
1496
|
+
|
|
1497
|
+
`emulateNetwork` takes a profile name or custom values. Each profile has the
|
|
1498
|
+
values of the DevTools preset of the same name in the Chromium that Testkit
|
|
1499
|
+
runs:
|
|
1500
|
+
|
|
1501
|
+
| Profile | DevTools preset | Download | Upload | Latency |
|
|
1502
|
+
| ----------- | --------------- | ---------- | ---------- | -------- |
|
|
1503
|
+
| `"fast-4g"` | Fast 4G | 8,100 kbps | 1,350 kbps | 165 ms |
|
|
1504
|
+
| `"slow-4g"` | Slow 4G | 1,440 kbps | 675 kbps | 562.5 ms |
|
|
1505
|
+
| `"3g"` | 3G | 400 kbps | 400 kbps | 2,000 ms |
|
|
1506
|
+
|
|
1507
|
+
DevTools renamed two presets in 2024: Slow 4G was Fast 3G, and 3G was Slow 3G.
|
|
1508
|
+
Custom values give speeds in kilobits per second and latency in milliseconds:
|
|
1509
|
+
|
|
1510
|
+
```ts
|
|
1511
|
+
await emulateNetwork(page, { latencyMs: 300, downloadKbps: 400, uploadKbps: 200 });
|
|
1512
|
+
```
|
|
1513
|
+
|
|
1514
|
+
`emulateCpu` makes the page's main thread, which runs its JavaScript and
|
|
1515
|
+
layout, `slowdown` times slower. DevTools' fixed CPU options are 4, 6, and 20.
|
|
1516
|
+
Testkit doesn't calibrate the slowdown to the machine, as DevTools' "Mid-tier
|
|
1517
|
+
mobile" and "Low-tier mobile" options do. `emulateCpu(page, 1)` restores full
|
|
1518
|
+
speed.
|
|
1519
|
+
|
|
1520
|
+
- Call the helpers before `page.goto()`. Both settings last across navigations
|
|
1521
|
+
until the page closes.
|
|
1522
|
+
- Neither setting reaches a cross-site iframe. Chromium runs one in a process
|
|
1523
|
+
of its own, at full speed.
|
|
1524
|
+
- The network conditions apply to that page only. A popup it opens, or another
|
|
1525
|
+
page, loads at full speed. Playwright's `browserContext.setOffline()` clears
|
|
1526
|
+
them.
|
|
1527
|
+
- The CPU slowdown applies to the browser process that runs the page. A
|
|
1528
|
+
same-site popup that keeps its `window.opener` shares that process and slows
|
|
1529
|
+
down too, and closing it ends the slowdown. Call `emulateCpu` again after
|
|
1530
|
+
closing one.
|
|
1531
|
+
- Like DevTools, `emulateNetwork` also sets `navigator.connection` until the
|
|
1532
|
+
page navigates to another site.
|
|
1533
|
+
- An unknown profile name or field, a negative or non-finite value, a speed of
|
|
1534
|
+
0, or a CPU slowdown outside 1 to 100 throws an error that names the value.
|
|
1535
|
+
|
|
1536
|
+
Three things to keep in mind:
|
|
1537
|
+
|
|
1538
|
+
- Set generous time limits. A CI runner has less CPU than a laptop, and a
|
|
1539
|
+
[campaign](#run-a-campaign) files a failing test as a bug, so a tight limit
|
|
1540
|
+
produces false bug reports.
|
|
1541
|
+
- A CPU slowdown multiplies the speed of the machine that runs the test, so 4
|
|
1542
|
+
on a slow CI runner is much slower than 4 on a laptop. A time limit under a
|
|
1543
|
+
CPU slowdown depends on the runner far more than one under network
|
|
1544
|
+
conditions, which are the same on every machine.
|
|
1545
|
+
- Chromium holds each response for at least the latency and shares the
|
|
1546
|
+
bandwidth among the page's requests, but it doesn't model TCP. The result
|
|
1547
|
+
approximates a slow connection rather than reproducing one.
|
|
1548
|
+
|
|
1549
|
+
### Check a ui test's browser traffic
|
|
1550
|
+
|
|
1551
|
+
Testkit records the requests every `ui` test's pages send, with no change to the
|
|
1552
|
+
test. The record catches bugs that live in how a frontend uses its API, such as
|
|
1553
|
+
a checkout that sends its payment twice or a list page that fetches each row
|
|
1554
|
+
separately, even when no assertion checks for them.
|
|
1555
|
+
|
|
1556
|
+
For each `ui` file whose tests sent requests, Testkit writes
|
|
1557
|
+
`browser-network.json` and `browser-network-report.md` from each test's final
|
|
1558
|
+
attempt. A test's section of the report looks like this:
|
|
1559
|
+
|
|
1560
|
+
```md
|
|
1561
|
+
## checkout › pays for the cart (passed)
|
|
1562
|
+
|
|
1563
|
+
21 requests, 452 KB received: script 312 KB, font 48 KB, document 41 KB, fetch 14 KB
|
|
1564
|
+
|
|
1565
|
+
| Service | Request | Count | Sent | Received | Status |
|
|
1566
|
+
| ------- | ----------------- | ----- | ------ | -------- | ------ |
|
|
1567
|
+
| web | GET /cart | 1 | 0 B | 41 KB | 200 |
|
|
1568
|
+
| api | GET /products/:id | 8 | 0 B | 11 KB | 200 |
|
|
1569
|
+
| api | POST /payments | 2 | 1.2 KB | 400 B | 201 |
|
|
1570
|
+
|
|
1571
|
+
Other origins: fonts.gstatic.com, 2 requests, 48 KB
|
|
1572
|
+
|
|
1573
|
+
Leads:
|
|
1574
|
+
|
|
1575
|
+
- `POST /payments` to api went out 2 times with the same body.
|
|
1576
|
+
```
|
|
1577
|
+
|
|
1578
|
+
- Rows cover page loads and API calls, which are Playwright's `document`,
|
|
1579
|
+
`fetch`, and `xhr` requests, to the services the test's group uses. A row
|
|
1580
|
+
names the service rather than its URL, because each run gives services
|
|
1581
|
+
different ports. Scripts, styles, images, and fonts count only toward the
|
|
1582
|
+
totals by type. Requests to other origins appear by host name only.
|
|
1583
|
+
- A path's numbers, UUIDs, and hex strings of 16 or more characters become
|
|
1584
|
+
`:id`, so eight product fetches make one row with a count of 8.
|
|
1585
|
+
- The record never holds query strings, headers, or bodies. Paths pass through
|
|
1586
|
+
Testkit's redaction, and sealing redacts them again with the run's secrets.
|
|
1587
|
+
To spot a repeated write, Testkit hashes each body in memory and keeps only
|
|
1588
|
+
the count.
|
|
1589
|
+
- Sizes are body bytes as they crossed the network, so a compressed response
|
|
1590
|
+
counts its compressed size and one that a `page.route()` handler fulfills
|
|
1591
|
+
counts 0. Headers don't count.
|
|
1592
|
+
- Each test keeps 200 requests in detail, 100 rows, and 20 other origins. The
|
|
1593
|
+
record counts the rest.
|
|
1594
|
+
- The leads point at two patterns: a POST, PUT, PATCH, or DELETE sent more than
|
|
1595
|
+
once to the same URL with the same body, and a page load or API call that
|
|
1596
|
+
failed or got a 4xx or 5xx status in a test that passed. A lead never fails a
|
|
1597
|
+
test.
|
|
1598
|
+
- The record covers every context a test opens, including one from
|
|
1599
|
+
`browser.newContext()`. It doesn't cover a context that a `beforeAll` hook
|
|
1600
|
+
opens.
|
|
1601
|
+
|
|
1602
|
+
To assert on the traffic, call `browserRequests()` from
|
|
1603
|
+
`@gfdelarue/testkit/playwright`. It returns the requests the test's pages have
|
|
1604
|
+
sent so far, oldest first:
|
|
1605
|
+
|
|
1606
|
+
```ts
|
|
1607
|
+
import { browserRequests, expect, test } from "@gfdelarue/testkit/playwright";
|
|
1608
|
+
|
|
1609
|
+
test("checkout charges the card once", async ({ page }) => {
|
|
1610
|
+
await page.goto("/cart");
|
|
1611
|
+
await page.getByRole("button", { name: "Pay" }).click();
|
|
1612
|
+
await expect(page.getByText("Payment received")).toBeVisible();
|
|
1613
|
+
|
|
1614
|
+
const payments = (await browserRequests()).filter(
|
|
1615
|
+
(request) => request.service === "api" && request.method === "POST" && request.path === "/payments",
|
|
1616
|
+
);
|
|
1617
|
+
expect(payments).toHaveLength(1);
|
|
1618
|
+
});
|
|
1619
|
+
```
|
|
1620
|
+
|
|
1621
|
+
Each entry has `service`, the name of the service it went to or `null`,
|
|
1622
|
+
`method`, `url`, `path`, `resourceType`, `status`, `failure`, `requestBytes`,
|
|
1623
|
+
`responseBytes`, and `durationMs`. `path` is the URL's path as sent, without
|
|
1624
|
+
`:id`. Add up entries with plain array code, for example to check a page's
|
|
1625
|
+
weight:
|
|
1626
|
+
|
|
1627
|
+
```ts
|
|
1628
|
+
const received = (await browserRequests()).reduce((total, request) => total + request.responseBytes, 0);
|
|
1629
|
+
expect(received).toBeLessThan(500_000);
|
|
1630
|
+
```
|
|
1631
|
+
|
|
1632
|
+
`browserRequests()` waits up to 5 seconds for requests already in flight, so
|
|
1633
|
+
their entries have a status and sizes. It doesn't wait for EventSource streams.
|
|
1634
|
+
A request still unfinished after that has its status if the response's headers
|
|
1635
|
+
arrived, 0 response bytes, and a `durationMs` of `null`. A WebSocket connection
|
|
1636
|
+
appears with the `resourceType` `websocket` and no status, sizes, or duration.
|
|
1637
|
+
The record counts connections, not their messages.
|
|
1638
|
+
|
|
1245
1639
|
## Discovery
|
|
1246
1640
|
|
|
1247
1641
|
Testkit discovers plain Vitest test files and typed suites.
|
|
@@ -1373,7 +1767,7 @@ engine.
|
|
|
1373
1767
|
Unsupported pairs fail discovery; for example, a `*.ui.testkit.ts` file cannot
|
|
1374
1768
|
declare `engine: "node-vitest"`.
|
|
1375
1769
|
|
|
1376
|
-
See [docs/test-types.md](docs/test-types.md) for the canonical type model.
|
|
1770
|
+
See [`docs/test-types.md`](https://app.unpkg.com/@gfdelarue/testkit/files/docs/test-types.md) for the canonical type model.
|
|
1377
1771
|
|
|
1378
1772
|
Test groups own test files by directory. Declare them under `groups` in
|
|
1379
1773
|
`testkit.config.ts`. Each group lists its repo-relative `roots` and the services
|
|
@@ -1403,7 +1797,7 @@ export default defineConfig({
|
|
|
1403
1797
|
are also available by name; see
|
|
1404
1798
|
[Named endpoints and databases](#named-endpoints-and-databases).
|
|
1405
1799
|
- `skip`, `requirements`, and `runtimeIsolation` are group fields. See the
|
|
1406
|
-
[config
|
|
1800
|
+
`groups` section of [`testkit-config-schema.md`](https://app.unpkg.com/@gfdelarue/testkit/files/testkit-config-schema.md).
|
|
1407
1801
|
|
|
1408
1802
|
Services own no test files. A config with `services` and no `groups` key
|
|
1409
1803
|
fails to load when a service's `local.cwd` or `discovery.roots` holds a typed
|
|
@@ -1472,11 +1866,16 @@ Testkit ignores an old `.testkit/history/` directory and leaves it in place.
|
|
|
1472
1866
|
`@gfdelarue/testkit` provisions Docker-managed local Postgres automatically for
|
|
1473
1867
|
services that define `database: database.postgres(...)`.
|
|
1474
1868
|
|
|
1475
|
-
Docker is also the required Postgres client runtime
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1869
|
+
Docker is also the required Postgres client runtime. Testkit doesn't use
|
|
1870
|
+
host-installed `psql` or `pg_dump`, which keeps local runs consistent across
|
|
1871
|
+
Linux, macOS, and native Windows. Testkit's own operations on the database it
|
|
1872
|
+
owns, such as declaring roles and taking schema snapshots, run inside the
|
|
1873
|
+
database container. So the `image` that `database.postgres()` uses must include
|
|
1874
|
+
`psql` and `pg_dump`, with a `pg_dump` at least as new as its server. Your SQL,
|
|
1875
|
+
in `sql-file` steps and `database-steps` helpers, runs `psql` in a separate
|
|
1876
|
+
client container from `postgres:16`. A `pg_dump` of an external database runs
|
|
1877
|
+
in `postgres:<its major version>`. `TESTKIT_POSTGRES_CLIENT_IMAGE` overrides
|
|
1878
|
+
the image of those client containers.
|
|
1480
1879
|
|
|
1481
1880
|
Template steps, schema snapshots, and `database.steps` helpers work the same
|
|
1482
1881
|
under Docker Desktop, rootless Docker, and Docker-in-Docker. Clients for
|
|
@@ -1502,7 +1901,7 @@ one with `database(name)` from `@gfdelarue/testkit/environment`. Declared `env`
|
|
|
1502
1901
|
`{dbPort}`, `{dbName}`, `{dbUser}`, `{dbPassword}`, and `{dbSsl}` tokens, with
|
|
1503
1902
|
`:serviceName` to read the database of a service listed in `dependsOn`. Testkit
|
|
1504
1903
|
resolves them from the database provisioned for the current run. They are
|
|
1505
|
-
env-only; see
|
|
1904
|
+
env-only; see the `env` section of [`testkit-config-schema.md`](https://app.unpkg.com/@gfdelarue/testkit/files/testkit-config-schema.md).
|
|
1506
1905
|
|
|
1507
1906
|
`db schema refresh` forces a source database dump into the `.testkit` source
|
|
1508
1907
|
schema cache. `db schema verify` prepares local templates and verifies local
|