@zerotal/arch 1.7.0
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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/api-surface.md +283 -0
- package/docs/about.md +676 -0
- package/docs/admin/actions.md +183 -0
- package/docs/admin/auth.md +89 -0
- package/docs/admin/dashboard.md +155 -0
- package/docs/admin/extending-ui.md +229 -0
- package/docs/admin/extending.md +205 -0
- package/docs/admin/forms.md +155 -0
- package/docs/admin/index.md +144 -0
- package/docs/admin/operations.md +224 -0
- package/docs/admin/references.md +174 -0
- package/docs/admin/resources.md +132 -0
- package/docs/admin/structure.md +158 -0
- package/docs/admin/tables.md +185 -0
- package/docs/admin/testing.md +138 -0
- package/docs/ai.md +373 -0
- package/docs/application.md +435 -0
- package/docs/arch.md +263 -0
- package/docs/assets.md +755 -0
- package/docs/audit.md +367 -0
- package/docs/authentication.md +1000 -0
- package/docs/authorization.md +529 -0
- package/docs/broadcasting/channels.md +170 -0
- package/docs/broadcasting/client.md +108 -0
- package/docs/broadcasting/events.md +190 -0
- package/docs/broadcasting/index.md +163 -0
- package/docs/broadcasting/references.md +84 -0
- package/docs/broadcasting/testing.md +132 -0
- package/docs/cache.md +387 -0
- package/docs/carbon.md +830 -0
- package/docs/changelog.md +465 -0
- package/docs/client/auth.md +113 -0
- package/docs/client/errors.md +139 -0
- package/docs/client/files.md +118 -0
- package/docs/client/index.md +128 -0
- package/docs/client/references.md +58 -0
- package/docs/client/requests.md +131 -0
- package/docs/client/resilience.md +141 -0
- package/docs/client/testing.md +146 -0
- package/docs/commands.md +450 -0
- package/docs/components.md +2427 -0
- package/docs/config-system.md +258 -0
- package/docs/container.md +596 -0
- package/docs/context.md +579 -0
- package/docs/contributing.md +164 -0
- package/docs/controllers.md +354 -0
- package/docs/conventions.md +466 -0
- package/docs/cookies.md +101 -0
- package/docs/csrf.md +206 -0
- package/docs/database.md +476 -0
- package/docs/deployment.md +434 -0
- package/docs/devtools.md +1013 -0
- package/docs/email-verification.md +320 -0
- package/docs/encryption.md +380 -0
- package/docs/errors.md +317 -0
- package/docs/events.md +420 -0
- package/docs/flow/components.md +846 -0
- package/docs/flow/decorators.md +458 -0
- package/docs/flow/events.md +420 -0
- package/docs/flow/forms.md +551 -0
- package/docs/flow/index.md +568 -0
- package/docs/flow/layouts.md +793 -0
- package/docs/flow/lifecycle.md +336 -0
- package/docs/flow/pagination.md +402 -0
- package/docs/flow/performance.md +262 -0
- package/docs/flow/references.md +317 -0
- package/docs/flow/routing.md +550 -0
- package/docs/flow/testing.md +447 -0
- package/docs/getting-started.md +286 -0
- package/docs/health.md +269 -0
- package/docs/helpers.md +423 -0
- package/docs/i18n.md +414 -0
- package/docs/index.md +65 -0
- package/docs/inertia/build.md +95 -0
- package/docs/inertia/devtools.md +197 -0
- package/docs/inertia/index.md +267 -0
- package/docs/inertia/middleware.md +81 -0
- package/docs/inertia/props.md +507 -0
- package/docs/inertia/references.md +144 -0
- package/docs/inertia/rendering.md +189 -0
- package/docs/inertia/ssr.md +102 -0
- package/docs/inspirations.md +257 -0
- package/docs/lifecycle.md +331 -0
- package/docs/lock.md +454 -0
- package/docs/logger.md +517 -0
- package/docs/media.md +582 -0
- package/docs/middleware.md +509 -0
- package/docs/migrations.md +529 -0
- package/docs/monitor.md +377 -0
- package/docs/notifications.md +848 -0
- package/docs/orm/casts.md +365 -0
- package/docs/orm/factories.md +231 -0
- package/docs/orm/index.md +663 -0
- package/docs/orm/lifecycle.md +421 -0
- package/docs/orm/queries.md +692 -0
- package/docs/orm/relationships.md +440 -0
- package/docs/orm/serialization.md +251 -0
- package/docs/package-development.md +536 -0
- package/docs/pagination.md +340 -0
- package/docs/password-reset.md +335 -0
- package/docs/providers.md +447 -0
- package/docs/query-builder.md +465 -0
- package/docs/queue.md +549 -0
- package/docs/rate-limiting.md +253 -0
- package/docs/responses.md +336 -0
- package/docs/roles-and-2fa.md +418 -0
- package/docs/routing.md +1008 -0
- package/docs/scaffolding.md +335 -0
- package/docs/scheduler.md +533 -0
- package/docs/seeding.md +229 -0
- package/docs/session.md +451 -0
- package/docs/social.md +524 -0
- package/docs/storage.md +622 -0
- package/docs/structure.md +230 -0
- package/docs/support-policy.md +114 -0
- package/docs/telemetry.md +505 -0
- package/docs/tenancy.md +608 -0
- package/docs/testing/browser.md +153 -0
- package/docs/testing/console.md +130 -0
- package/docs/testing/database.md +238 -0
- package/docs/testing/flow-browser.md +216 -0
- package/docs/testing/http.md +441 -0
- package/docs/testing/index.md +240 -0
- package/docs/testing/mocking.md +410 -0
- package/docs/upgrade.md +192 -0
- package/docs/validator.md +549 -0
- package/docs/view.md +453 -0
- package/package.json +60 -0
- package/src/bin/mcp.ts +73 -0
- package/src/config.ts +63 -0
- package/src/errors.ts +27 -0
- package/src/index.ts +56 -0
- package/src/install/ArchInstallCommand.ts +189 -0
- package/src/install/detect.ts +98 -0
- package/src/install/guidelines.ts +290 -0
- package/src/install/markers.ts +77 -0
- package/src/install/mcpConfig.ts +81 -0
- package/src/mcp/index.ts +27 -0
- package/src/mcp/jsonrpc.ts +133 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/stdio.ts +111 -0
- package/src/mcp/types.ts +146 -0
- package/src/probe/ArchProbeCommand.ts +48 -0
- package/src/probe/sentinel.ts +13 -0
- package/src/probe/topics.ts +291 -0
- package/src/provider/ArchProvider.ts +101 -0
- package/src/tools/_probe.ts +164 -0
- package/src/tools/apiSurface.ts +241 -0
- package/src/tools/appInfo.ts +88 -0
- package/src/tools/baselines.ts +210 -0
- package/src/tools/context.ts +37 -0
- package/src/tools/doctor.ts +93 -0
- package/src/tools/index.ts +50 -0
- package/src/tools/logs.ts +265 -0
- package/src/tools/routes.ts +133 -0
- package/src/tools/schema.ts +119 -0
- package/src/tools/searchDocs.ts +345 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Browser Tests
|
|
3
|
+
description: Drive a real browser against a running Zerotal app with Playwright to test Flow and Inertia UIs end-to-end.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Browser Tests
|
|
7
|
+
|
|
8
|
+
Browser tests exercise the parts of your app that only exist in a real browser, by
|
|
9
|
+
driving a live server with Playwright and asserting on what the user sees.
|
|
10
|
+
|
|
11
|
+
Some behavior only exists in a real browser — [Flow](/docs/flow)'s WebSocket
|
|
12
|
+
bridge and Alpine runtime, [Inertia](/docs/inertia) client navigation, focus and
|
|
13
|
+
keyboard handling. This page covers **[Playwright](https://playwright.dev)** pointed
|
|
14
|
+
at a running server, which is what to use for cross-browser end-to-end coverage.
|
|
15
|
+
|
|
16
|
+
> **Testing Flow's bridge?** [`FlowBrowser`](/docs/testing/flow-browser) ships in
|
|
17
|
+
> `@zerotal/testing`, needs no extra dependency, and runs inside `bun test` alongside
|
|
18
|
+
> the rest of your suite. It drives a headless page against a real server and can
|
|
19
|
+
> assert on the WebSocket transport itself — whether the socket upgraded, what frames
|
|
20
|
+
> crossed the wire. Reach for Playwright when you need several browsers, or coverage
|
|
21
|
+
> beyond Flow.
|
|
22
|
+
|
|
23
|
+
> **Note** — Unit and [HTTP tests](/docs/testing/http) run on Bun's test runner. Browser tests
|
|
24
|
+
> run under Playwright's own runner (`*.e2e.ts` / `*.spec.ts`), separately from
|
|
25
|
+
> `bun test`.
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
Install Playwright in your app:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# in your project root
|
|
33
|
+
bun add -d @playwright/test
|
|
34
|
+
bunx playwright install --with-deps
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Add a `playwright.config.ts` that boots your app with the `webServer` option, so the
|
|
38
|
+
server starts once for the whole run and is torn down after:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// playwright.config.ts
|
|
42
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
43
|
+
|
|
44
|
+
const PORT = 3000;
|
|
45
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
46
|
+
|
|
47
|
+
export default defineConfig({
|
|
48
|
+
testDir: "./e2e",
|
|
49
|
+
testMatch: "**/*.e2e.ts",
|
|
50
|
+
timeout: 30_000,
|
|
51
|
+
use: { baseURL: BASE_URL, trace: "on-first-retry" },
|
|
52
|
+
projects: [
|
|
53
|
+
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
|
54
|
+
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
|
|
55
|
+
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
|
|
56
|
+
],
|
|
57
|
+
// Boot the server once for the run (no --hot, to avoid reload flakiness)
|
|
58
|
+
webServer: {
|
|
59
|
+
command: "bun run start",
|
|
60
|
+
url: BASE_URL,
|
|
61
|
+
reuseExistingServer: !process.env.CI,
|
|
62
|
+
timeout: 120_000,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Wire up scripts in `package.json`:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
// package.json
|
|
71
|
+
{
|
|
72
|
+
"scripts": {
|
|
73
|
+
"test:e2e": "playwright test",
|
|
74
|
+
"test:e2e:ui": "playwright test --ui"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Writing a test
|
|
80
|
+
|
|
81
|
+
Specs live in `e2e/` and use Playwright's `test`/`expect`. Drive the page through
|
|
82
|
+
roles and assert on what the user sees:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// e2e/navigate.e2e.ts
|
|
86
|
+
import { test, expect } from "@playwright/test";
|
|
87
|
+
|
|
88
|
+
test.describe("SPA navigation", () => {
|
|
89
|
+
test("swaps content without a full page reload", async ({ page }) => {
|
|
90
|
+
await page.goto("/pulse/counter");
|
|
91
|
+
// A full reload would clear this marker; an SPA swap keeps it.
|
|
92
|
+
await page.evaluate(() => ((window as any).__spa = true));
|
|
93
|
+
|
|
94
|
+
await page.getByRole("link", { name: "Components" }).click();
|
|
95
|
+
await expect(page).toHaveURL(/\/pulse\/components/);
|
|
96
|
+
await expect(page.getByRole("heading", { name: "Native Components" })).toBeVisible();
|
|
97
|
+
|
|
98
|
+
const kept = await page.evaluate(() => (window as any).__spa);
|
|
99
|
+
expect(kept).toBe(true); // no full reload happened
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# in your project root
|
|
106
|
+
bun run test:e2e
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Waiting for the client to boot
|
|
110
|
+
|
|
111
|
+
Flow pages finish wiring once Alpine has walked the DOM and fired
|
|
112
|
+
`alpine:initialized`. A fast click can land before that and silently no-op (a flaky,
|
|
113
|
+
browser-dependent failure). Wait for readiness before interacting:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// e2e/support/gotoReady.ts
|
|
117
|
+
import type { Page } from "@playwright/test";
|
|
118
|
+
|
|
119
|
+
export async function gotoReady(page: Page, url: string): Promise<void> {
|
|
120
|
+
await page.addInitScript(() => {
|
|
121
|
+
(window as any).__alpineReady = false;
|
|
122
|
+
document.addEventListener("alpine:initialized", () => ((window as any).__alpineReady = true));
|
|
123
|
+
});
|
|
124
|
+
await page.goto(url);
|
|
125
|
+
await page.waitForFunction(() => (window as any).__alpineReady === true, null, {
|
|
126
|
+
timeout: 10_000,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Then `await gotoReady(page, "/pulse/counter")` instead of `page.goto(...)` for
|
|
132
|
+
interactive Flow pages.
|
|
133
|
+
|
|
134
|
+
> **Warning** — Clicking before `alpine:initialized` fires is the most common source
|
|
135
|
+
> of browser-test flakiness. Use `gotoReady` (or an equivalent readiness wait) on any
|
|
136
|
+
> page driven by Flow's Alpine runtime.
|
|
137
|
+
|
|
138
|
+
## Tips
|
|
139
|
+
|
|
140
|
+
- **Seed deterministic state.** If a flow needs data, seed it before the run (or use
|
|
141
|
+
pages backed by in-memory state). Browser tests don't share the transactional
|
|
142
|
+
rollback that [database tests](/docs/testing/database) use.
|
|
143
|
+
- **Prefer role/label selectors** (`getByRole`, `getByLabel`) over CSS — they're
|
|
144
|
+
resilient to markup changes and assert accessibility.
|
|
145
|
+
- **Run cross-browser in CI.** The three projects above cover Chromium, Firefox, and
|
|
146
|
+
WebKit; enable retries on CI to absorb transient flakiness.
|
|
147
|
+
|
|
148
|
+
## Next steps
|
|
149
|
+
|
|
150
|
+
- [HTTP Tests](/docs/testing/http) — faster tests for anything not needing a browser.
|
|
151
|
+
- [Database Tests](/docs/testing/database) — seed and roll back data for backed flows.
|
|
152
|
+
- [Flow](/docs/flow) / [Inertia](/docs/inertia) — the client runtimes these tests exercise.
|
|
153
|
+
- [Deployment](/docs/deployment) — `bun run start`, the command the `webServer` boots.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Console Tests
|
|
3
|
+
description: Run CLI commands in-process from a test and assert their output, exit code, and side effects.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Console Tests
|
|
7
|
+
|
|
8
|
+
Console tests exercise your [CLI commands](/docs/commands) — generators, migrations,
|
|
9
|
+
maintenance tasks — without spawning a subprocess. `Artisan.call()` runs a registered
|
|
10
|
+
command **in-process**, captures everything it printed, and returns its exit code.
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// in a test file
|
|
14
|
+
import { Artisan } from "zerotal";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> **Note** — `Artisan.call()` needs a `CommandRunner` bound under `commands` in the
|
|
18
|
+
> container — present in `web` and `console` runtime modes. When you boot the app with
|
|
19
|
+
> [`createTestApp()`](/docs/testing/http), commands are registered for you.
|
|
20
|
+
|
|
21
|
+
## Running a command
|
|
22
|
+
|
|
23
|
+
`Artisan.call(name, parameters?)` returns `{ code, output }` — `code` is `0` on
|
|
24
|
+
success, and `output` is everything the command wrote via `info`/`warn`/`error`/etc.:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// src/tests/CacheClearTest.ts
|
|
28
|
+
import { describe, it, expect } from "bun:test";
|
|
29
|
+
import { Artisan } from "zerotal";
|
|
30
|
+
|
|
31
|
+
describe("cache:clear", () => {
|
|
32
|
+
it("clears the cache and reports success", async () => {
|
|
33
|
+
const { code, output } = await Artisan.call("cache:clear");
|
|
34
|
+
|
|
35
|
+
expect(code).toBe(0);
|
|
36
|
+
expect(output).toContain("Cache cleared");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Pass arguments and flags as a parameter map. Booleans become bare flags when `true`;
|
|
42
|
+
everything else becomes a `key=value` token, mirroring how the CLI parses argv:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// in a test file
|
|
46
|
+
await Artisan.call("migrate", { "--fresh": true });
|
|
47
|
+
await Artisan.call("make:model", { name: "Post", "--migration": true });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Output is captured, so nothing leaks to the test runner's console.
|
|
51
|
+
|
|
52
|
+
## Asserting side effects
|
|
53
|
+
|
|
54
|
+
Commands usually do work — touch the database, queue a job, write a file. Assert the
|
|
55
|
+
_effect_, not just the output, using the [database](/docs/testing/database) and
|
|
56
|
+
[mocking](/docs/testing/mocking) helpers:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// src/tests/PruneUsersTest.ts
|
|
60
|
+
import { Artisan } from "zerotal";
|
|
61
|
+
import { assertDatabaseCount, QueueFake } from "@zerotal/testing";
|
|
62
|
+
|
|
63
|
+
it("prune:users deletes stale accounts and queues a report", async () => {
|
|
64
|
+
const queue = QueueFake.install();
|
|
65
|
+
|
|
66
|
+
const { code } = await Artisan.call("prune:users", { "--days": 30 });
|
|
67
|
+
|
|
68
|
+
expect(code).toBe(0);
|
|
69
|
+
await assertDatabaseCount("users", 5);
|
|
70
|
+
queue.assertDispatched(PruneReportJob);
|
|
71
|
+
|
|
72
|
+
queue.restore();
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Unit-testing a command class
|
|
77
|
+
|
|
78
|
+
For a focused test of a single command's logic, instantiate it directly and inject a
|
|
79
|
+
`BufferWriter` to capture output. The `_writer`, `args`, and `flags` fields are
|
|
80
|
+
internal — the runner normally sets them — so a unit test reaches in with a cast:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// src/tests/GreetCommandTest.ts
|
|
84
|
+
import { expect } from "bun:test";
|
|
85
|
+
import { BufferWriter } from "zerotal";
|
|
86
|
+
import { GreetCommand } from "../app/commands/GreetCommand.ts";
|
|
87
|
+
|
|
88
|
+
const cmd = new GreetCommand();
|
|
89
|
+
const buffer = new BufferWriter();
|
|
90
|
+
|
|
91
|
+
// These are internal fields the CommandRunner usually populates:
|
|
92
|
+
(cmd as any)._writer = buffer; // capture output instead of printing
|
|
93
|
+
(cmd as any).args = { name: "Alice" };
|
|
94
|
+
(cmd as any).flags = {};
|
|
95
|
+
|
|
96
|
+
await cmd.run();
|
|
97
|
+
|
|
98
|
+
expect(buffer.flush()).toContain("Hello, Alice");
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
> **Tip** — Prefer `Artisan.call()` for end-to-end command behavior — it wires the
|
|
102
|
+
> runner for you. Reach for direct instantiation only when you deliberately want to
|
|
103
|
+
> bypass the runner.
|
|
104
|
+
|
|
105
|
+
## Which approach should I use?
|
|
106
|
+
|
|
107
|
+
- **`Artisan.call(name, params?)`** — the default. Use it whenever you want the real
|
|
108
|
+
command behavior: argument parsing, flag defaults, container resolution, and the
|
|
109
|
+
command's full `run()`. Assert on `code` and `output`, plus any side effects.
|
|
110
|
+
- **Direct instantiation + `BufferWriter`** — only for unit-testing one command's
|
|
111
|
+
logic in isolation. You bypass the runner, so you must set `_writer`, `args`, and
|
|
112
|
+
`flags` yourself.
|
|
113
|
+
|
|
114
|
+
## References
|
|
115
|
+
|
|
116
|
+
The console-testing surface lives in `@zerotal/core`:
|
|
117
|
+
|
|
118
|
+
| Member | Signature | Description |
|
|
119
|
+
| -------------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
|
|
120
|
+
| `Artisan.call` | `call(name: string, parameters?: Record<string, string \| boolean \| number>): Promise<ArtisanResult>` | Run a registered command in-process and capture its output. |
|
|
121
|
+
| `ArtisanResult` | `{ code: number; output: string }` | Exit code (`0` = success) and the captured output string. |
|
|
122
|
+
| `BufferWriter` | `class BufferWriter implements OutputWriter` | Capturing writer; collects output in memory instead of printing. |
|
|
123
|
+
| `BufferWriter.flush` | `flush(): string` | Return all captured output as one string and clear the buffer. |
|
|
124
|
+
|
|
125
|
+
## Next steps
|
|
126
|
+
|
|
127
|
+
- [Commands](/docs/commands) — writing and registering commands.
|
|
128
|
+
- [Database](/docs/testing/database) — asserting a command's DB effects.
|
|
129
|
+
- [Mocking](/docs/testing/mocking) — asserting queued jobs and sent mail.
|
|
130
|
+
- [HTTP Tests](/docs/testing/http) — boot the app and exercise it over real requests.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Database Testing
|
|
3
|
+
description: Keep database tests isolated with per-test transactional rollback and assert on rows and stored files.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Database Testing
|
|
7
|
+
|
|
8
|
+
Tests that touch the database must stay isolated — one test's writes can't leak into
|
|
9
|
+
the next. Zerotal wraps each test in a transaction that **rolls back** when it finishes,
|
|
10
|
+
and ships assertions for checking rows and stored files.
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// in a test file
|
|
14
|
+
import {
|
|
15
|
+
migrateDatabase,
|
|
16
|
+
refreshDatabase,
|
|
17
|
+
withDatabase,
|
|
18
|
+
assertDatabaseHas,
|
|
19
|
+
assertDatabaseMissing,
|
|
20
|
+
assertDatabaseCount,
|
|
21
|
+
} from "@zerotal/testing";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> **Note** — These helpers are part of the [testing toolkit](/docs/testing/index) and
|
|
25
|
+
> only run inside `bun test`. Install the package as a dev dependency if it isn't
|
|
26
|
+
> already: `bun add -d @zerotal/testing`.
|
|
27
|
+
|
|
28
|
+
## Building the schema
|
|
29
|
+
|
|
30
|
+
A test needs tables before it needs rows. Build them by running the project's own
|
|
31
|
+
[migrations](/docs/migrations) rather than by writing the schema a second time:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// tests/Feature/PostTest.ts
|
|
35
|
+
import { migrateDatabase } from "@zerotal/testing";
|
|
36
|
+
|
|
37
|
+
beforeAll(async () => {
|
|
38
|
+
app = await createApp();
|
|
39
|
+
await migrateDatabase();
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A hand-written `CREATE TABLE` in a test is a second definition of the same tables,
|
|
44
|
+
and the two drift. A column added in a migration is missing from the test schema
|
|
45
|
+
until something fails for a reason that has nothing to do with the change you
|
|
46
|
+
made. Running the migrations themselves means the schema under test is the schema
|
|
47
|
+
that ships.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// signature
|
|
51
|
+
function migrateDatabase(options?: MigrateDatabaseOptions): Promise<string[]>;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Option | Type | Default | Purpose |
|
|
55
|
+
| ------------ | ------------- | ----------------------- | ------------------------------- |
|
|
56
|
+
| `connection` | `SQLInstance` | the active connection | Which database to migrate. |
|
|
57
|
+
| `path` | `string` | `"database/migrations"` | Where the migration files live. |
|
|
58
|
+
| `table` | `string` | `"migrations"` | Tracking-table name. |
|
|
59
|
+
|
|
60
|
+
It returns the names of the migrations it applied, and is idempotent — a second
|
|
61
|
+
call applies nothing. `refreshDatabase({ migrate: true })` runs it for you, which
|
|
62
|
+
is usually what you want:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// in a test file
|
|
66
|
+
describe("Post", () => {
|
|
67
|
+
refreshDatabase({ connection: db, migrate: true });
|
|
68
|
+
// …the real schema, and every test rolled back after itself
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
For a file-backed or server test database — where migrating once up front beats
|
|
73
|
+
migrating per test file — `bun zt test --migrate` applies them before the suite
|
|
74
|
+
starts. An `:memory:` database belongs to the process that opened it, so there is
|
|
75
|
+
nothing for a parent process to migrate; use `refreshDatabase({ migrate: true })`
|
|
76
|
+
there.
|
|
77
|
+
|
|
78
|
+
## Which rollback helper do I use?
|
|
79
|
+
|
|
80
|
+
Both run your test inside a transaction that rolls back, leaving the database
|
|
81
|
+
untouched. Choose by scope:
|
|
82
|
+
|
|
83
|
+
- **`refreshDatabase`** — call once in a `describe` block to roll back _every_ test in
|
|
84
|
+
it, and (optionally) install a connection and run schema setup once. Reach for this
|
|
85
|
+
in feature suites where many tests share the same tables.
|
|
86
|
+
- **`withDatabase`** — wrap a _single_ `it()` body when only that one test needs
|
|
87
|
+
rollback. No `describe`-level setup.
|
|
88
|
+
|
|
89
|
+
## refreshDatabase — suite-level rollback
|
|
90
|
+
|
|
91
|
+
Call it once inside a `describe` block. Every `it()` in that block runs inside a
|
|
92
|
+
transaction that rolls back when the test ends, so each test starts from the same
|
|
93
|
+
clean baseline:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// tests/Feature/UserTest.ts
|
|
97
|
+
import { describe, it } from "bun:test";
|
|
98
|
+
import { refreshDatabase, assertDatabaseHas, assertDatabaseMissing } from "@zerotal/testing";
|
|
99
|
+
import { SQL } from "bun";
|
|
100
|
+
import { User } from "../app/models/User.ts";
|
|
101
|
+
|
|
102
|
+
const db = new SQL(":memory:");
|
|
103
|
+
|
|
104
|
+
describe("User", () => {
|
|
105
|
+
refreshDatabase({ connection: db, migrate: true });
|
|
106
|
+
|
|
107
|
+
it("creates a user", async () => {
|
|
108
|
+
await User.create({ email: "a@b.com" });
|
|
109
|
+
await assertDatabaseHas("users", { email: "a@b.com" });
|
|
110
|
+
}); // ← rolled back; next test starts clean
|
|
111
|
+
|
|
112
|
+
it("starts clean", async () => {
|
|
113
|
+
await assertDatabaseMissing("users", { email: "a@b.com" });
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// signature
|
|
120
|
+
function refreshDatabase(options?: RefreshDatabaseOptions): void;
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
| Option | Type | Required | Purpose |
|
|
124
|
+
| ------------ | ------------------------------ | -------- | ------------------------------------------------------------------------------------ |
|
|
125
|
+
| `connection` | `SQLInstance` | no | Install as the model connection for this suite. Omit to use the existing connection. |
|
|
126
|
+
| `migrate` | `boolean \| string` | no | Run the project's migrations first. A string names a different directory. |
|
|
127
|
+
| `setup` | `(db: SQLInstance) => unknown` | no | Runs once before the suite and is **committed**, so every test sees it. |
|
|
128
|
+
| `teardown` | `(db: SQLInstance) => unknown` | no | Runs once after all tests complete. |
|
|
129
|
+
|
|
130
|
+
Both `migrate` and `setup` are committed (they are the schema and any shared
|
|
131
|
+
fixtures), while each test's own writes happen inside the rolled-back transaction.
|
|
132
|
+
|
|
133
|
+
> **Warning** — `refreshDatabase()` only works inside a `bun test` run; it pulls
|
|
134
|
+
> `beforeAll`/`afterEach` from `bun:test`. Calling it outside the test runner throws.
|
|
135
|
+
|
|
136
|
+
## withDatabase — per-test wrapper
|
|
137
|
+
|
|
138
|
+
When you want rollback for a single test rather than a whole block, wrap the test body:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// in a test file
|
|
142
|
+
import { withDatabase } from "@zerotal/testing";
|
|
143
|
+
|
|
144
|
+
it(
|
|
145
|
+
"creates a user",
|
|
146
|
+
withDatabase(async () => {
|
|
147
|
+
await User.create({ email: "a@b.com" });
|
|
148
|
+
await assertDatabaseHas("users", { email: "a@b.com" });
|
|
149
|
+
}),
|
|
150
|
+
); // ← transaction rolled back here
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// signature
|
|
155
|
+
function withDatabase(fn: () => Promise<void>): () => Promise<void>;
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
A test failure inside the callback still rolls the transaction back, then re-throws so
|
|
159
|
+
the assertion is reported.
|
|
160
|
+
|
|
161
|
+
## Database assertions
|
|
162
|
+
|
|
163
|
+
Each assertion runs a `COUNT(*)` against the current connection and throws a
|
|
164
|
+
descriptive error when the expectation fails:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// in a test file
|
|
168
|
+
import { assertDatabaseHas, assertDatabaseMissing, assertDatabaseCount } from "@zerotal/testing";
|
|
169
|
+
|
|
170
|
+
await assertDatabaseHas("users", { email: "alice@example.com", role: "admin" });
|
|
171
|
+
await assertDatabaseMissing("users", { email: "deleted@example.com" });
|
|
172
|
+
await assertDatabaseCount("posts", 5);
|
|
173
|
+
await assertDatabaseCount("posts", 2, { published: 1 }); // optional where filter
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
| Function | Throws when |
|
|
177
|
+
| --------------------------------------- | --------------------------------- |
|
|
178
|
+
| `assertDatabaseHas(table, where)` | No row matches `where`. |
|
|
179
|
+
| `assertDatabaseMissing(table, where)` | A matching row exists. |
|
|
180
|
+
| `assertDatabaseCount(table, n, where?)` | The matching row count isn't `n`. |
|
|
181
|
+
|
|
182
|
+
## Storage assertions
|
|
183
|
+
|
|
184
|
+
For tests that write to a [storage](/docs/storage) disk, assert file presence without
|
|
185
|
+
reading bytes. Pass a disk name or a `StorageDriver` instance:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// in a test file
|
|
189
|
+
import { assertStoredFile, assertMissingFile } from "@zerotal/testing";
|
|
190
|
+
|
|
191
|
+
await assertStoredFile("local", "uploads/avatar.jpg");
|
|
192
|
+
await assertMissingFile("local", "uploads/old.jpg");
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
> **Note** — A disk name routes through the `Storage` facade (needs
|
|
196
|
+
> `StorageProvider` registered); a `StorageDriver` instance lets isolated unit tests
|
|
197
|
+
> skip the facade entirely.
|
|
198
|
+
|
|
199
|
+
## Arranging rows
|
|
200
|
+
|
|
201
|
+
Use [factories](/docs/orm/factories) to build the records a test acts on, and
|
|
202
|
+
[seeders](/docs/seeding) for shared fixtures:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// in a test file
|
|
206
|
+
import { UserFactory } from "../database/factories/UserFactory.ts";
|
|
207
|
+
import { PostFactory } from "../database/factories/PostFactory.ts";
|
|
208
|
+
|
|
209
|
+
const editor = await UserFactory.state("editor").create();
|
|
210
|
+
const posts = await PostFactory.for(editor).count(3).create();
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Factories suppress model observers by default, so seeding rows doesn't fire emails or
|
|
214
|
+
jobs — call `.dispatchEvents()` when a test needs the full lifecycle.
|
|
215
|
+
|
|
216
|
+
> **Tip** — `.for(editor)` injects the parent's primary key as the `<Model>Id`
|
|
217
|
+
> foreign key, so child rows line up with the record you created.
|
|
218
|
+
|
|
219
|
+
## References
|
|
220
|
+
|
|
221
|
+
| Function | Signature | Description |
|
|
222
|
+
| ----------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
223
|
+
| `migrateDatabase` | `(options?: MigrateDatabaseOptions) => Promise<string[]>` | Build the schema by running the project's migrations. |
|
|
224
|
+
| `refreshDatabase` | `(options?: RefreshDatabaseOptions) => void` | Roll back every test in a `describe` block; optional connection + setup. |
|
|
225
|
+
| `withDatabase` | `(fn: () => Promise<void>) => () => Promise<void>` | Wrap one `it()` body in a transaction that always rolls back. |
|
|
226
|
+
| `assertDatabaseHas` | `(table: string, where: Record<string, unknown>) => Promise<void>` | Pass when at least one row matches `where`. |
|
|
227
|
+
| `assertDatabaseMissing` | `(table: string, where: Record<string, unknown>) => Promise<void>` | Pass when no row matches `where`. |
|
|
228
|
+
| `assertDatabaseCount` | `(table: string, expected: number, where?: Record<string, unknown>) => Promise<void>` | Pass when the matching row count equals `expected`. |
|
|
229
|
+
| `assertStoredFile` | `(diskOrDriver: string \| StorageDriver, path: string) => Promise<void>` | Pass when the file exists on the disk or driver. |
|
|
230
|
+
| `assertMissingFile` | `(diskOrDriver: string \| StorageDriver, path: string) => Promise<void>` | Pass when the file is absent from the disk or driver. |
|
|
231
|
+
|
|
232
|
+
## Next steps
|
|
233
|
+
|
|
234
|
+
- [Testing overview](/docs/testing/index) — the full toolkit and a first test.
|
|
235
|
+
- [Factories](/docs/orm/factories) — generating model records for tests.
|
|
236
|
+
- [Seeding](/docs/seeding) — reusable fixtures across dev and tests.
|
|
237
|
+
- [Database](/docs/database) — transactions and the raw `DB` layer being rolled back.
|
|
238
|
+
- [HTTP Tests](/docs/testing/http) — drive these rolled-back rows through real requests.
|