@zerotal/testing 1.0.4 → 1.3.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 +10 -0
- package/package.json +6 -6
- package/src/TestApp.ts +77 -2
- package/src/index.ts +1 -1
- package/src/shared-app/app.ts +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,16 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.1.0] — 2026-08-08
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **Test files no longer tear the database out from under each other.** Bun runs a whole suite in one process and the ORM connection is process-global, so one file's `afterAll(() => app.close())` closed the connection every later file depended on — and the file that failed was a correct one that merely ran second, dying with "No database connection. Is DatabaseProvider registered?". `createTestApp()` now shares one app per process, keyed by the resolved `Application` (the scaffolded pattern passes a fresh arrow each time, so the callback identity is no key). `close()` on a shared app resets per-test state and leaves it running.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `closeSharedTestApps()` — tears the shared app down explicitly, for a global teardown or a suite asserting no timers leak. Passing a `setup` callback still opts out of sharing, since routes cannot be registered twice against a running server.
|
|
20
|
+
|
|
11
21
|
## [1.0.3] — 2026-08-07
|
|
12
22
|
|
|
13
23
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/testing",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
"typecheck": "tsc --noEmit"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@zerotal/core": "1.0
|
|
34
|
-
"@zerotal/orm": "1.0
|
|
35
|
-
"@zerotal/queue": "1.0
|
|
36
|
-
"@zerotal/notifications": "1.0
|
|
33
|
+
"@zerotal/core": "1.3.0",
|
|
34
|
+
"@zerotal/orm": "1.3.0",
|
|
35
|
+
"@zerotal/queue": "1.3.0",
|
|
36
|
+
"@zerotal/notifications": "1.3.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"typescript": "^5.8.0",
|
|
40
|
-
"@zerotal/session": "1.0
|
|
40
|
+
"@zerotal/session": "1.3.0"
|
|
41
41
|
},
|
|
42
42
|
"description": "Testing utilities for Zerotal — an in-process test app, HTTP helpers, and database refresh.",
|
|
43
43
|
"keywords": [
|
package/src/TestApp.ts
CHANGED
|
@@ -415,8 +415,26 @@ export class TestApp {
|
|
|
415
415
|
return this.request(url, { method, headers, body: form });
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
/**
|
|
418
|
+
/** @internal Set when this app is the per-process shared instance (see createTestApp). */
|
|
419
|
+
_shared = false;
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Stop the test server and shut the app's providers down. Call in `afterAll()`.
|
|
423
|
+
*
|
|
424
|
+
* When this app is the per-process shared instance — the normal case, where each
|
|
425
|
+
* test file calls `createTestApp()` with the same `bootstrap/app.ts` — this only
|
|
426
|
+
* resets per-test state (auth, flash, captured mail) and leaves the app running for
|
|
427
|
+
* the files that come after. Tearing it down here would close the process-global
|
|
428
|
+
* database connection out from under them, and the file that broke would be an
|
|
429
|
+
* entirely correct one that merely ran second.
|
|
430
|
+
*
|
|
431
|
+
* The shared instance is torn down once, by {@link closeSharedTestApps}.
|
|
432
|
+
*/
|
|
419
433
|
async close(): Promise<void> {
|
|
434
|
+
if (this._shared) {
|
|
435
|
+
resetTestState();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
420
438
|
// Run the full provider teardown (onStopping/onStopped), not just the HTTP
|
|
421
439
|
// server: otherwise queue polling intervals, monitor timers, worker threads,
|
|
422
440
|
// and DB connections from this test file keep running while the next file
|
|
@@ -548,10 +566,45 @@ function _toFile(input: TestFileInput | File | Blob): File {
|
|
|
548
566
|
* () => { Router.get('/ping', PingController, 'handle'); },
|
|
549
567
|
* );
|
|
550
568
|
*/
|
|
569
|
+
/**
|
|
570
|
+
* The per-process shared apps, keyed by the {@link Application} they wrap.
|
|
571
|
+
*
|
|
572
|
+
* Bun runs an entire test suite in one process, and the ORM connection is
|
|
573
|
+
* process-global — so a second file that boots its own app inherits a connection the
|
|
574
|
+
* first file's `afterAll` already closed, and dies in `migrateDatabase()` with
|
|
575
|
+
* "No database connection". Booting once and handing the same instance to every file
|
|
576
|
+
* removes the interference without asking each test file to know about it.
|
|
577
|
+
*
|
|
578
|
+
* Keyed by the Application and not by the `bootstrap` callback: the scaffolded pattern
|
|
579
|
+
* is `createTestApp(() => import('../bootstrap/app.ts').then((m) => m.default))`, a
|
|
580
|
+
* fresh arrow on every call. The module it imports is cached, so the *Application* is
|
|
581
|
+
* the thing that is genuinely the same across files.
|
|
582
|
+
*/
|
|
583
|
+
const _sharedApps = new Map<Application, TestApp>();
|
|
584
|
+
|
|
551
585
|
export async function createTestApp(
|
|
552
586
|
bootstrap: () => Application | Promise<Application>,
|
|
553
587
|
setup?: () => void,
|
|
554
588
|
): Promise<TestApp> {
|
|
589
|
+
// A `setup` callback registers routes, and registering them twice against an
|
|
590
|
+
// already-started server is not idempotent — so those callers always get a fresh
|
|
591
|
+
// app and own its teardown, exactly as before.
|
|
592
|
+
//
|
|
593
|
+
// Only probe once an app exists: on the first call `bootstrap()` has real side
|
|
594
|
+
// effects (Application.create + provider registration) and must run after the
|
|
595
|
+
// reset below, not before it.
|
|
596
|
+
if (!setup && _sharedApps.size > 0) {
|
|
597
|
+
const booted = await bootstrap();
|
|
598
|
+
const existing = _sharedApps.get(booted);
|
|
599
|
+
if (existing) {
|
|
600
|
+
// Re-adopt: an earlier file's close() reset the app scope even though the app
|
|
601
|
+
// itself is still running, so facades need pointing back at it.
|
|
602
|
+
booted.adoptAsCurrent();
|
|
603
|
+
resetTestState();
|
|
604
|
+
return existing;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
555
608
|
resetTestState();
|
|
556
609
|
const app = await bootstrap();
|
|
557
610
|
// A module-cached `bootstrap/app.ts` returns its top-level app on re-import
|
|
@@ -569,5 +622,27 @@ export async function createTestApp(
|
|
|
569
622
|
errors.inner = app._swapExceptionHandler(errors);
|
|
570
623
|
|
|
571
624
|
await app.start(0);
|
|
572
|
-
|
|
625
|
+
const testApp = new TestApp(app, errors);
|
|
626
|
+
|
|
627
|
+
if (!setup) {
|
|
628
|
+
testApp._shared = true;
|
|
629
|
+
_sharedApps.set(app, testApp);
|
|
630
|
+
}
|
|
631
|
+
return testApp;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Tear down every shared app created by {@link createTestApp}.
|
|
636
|
+
*
|
|
637
|
+
* Only needed when something must be released before the process exits — a global
|
|
638
|
+
* teardown file, or a suite that asserts no timers are left running. Individual test
|
|
639
|
+
* files should keep calling `app.close()`; on a shared app that resets per-test state
|
|
640
|
+
* and leaves the app up for the files still to run.
|
|
641
|
+
*/
|
|
642
|
+
export async function closeSharedTestApps(): Promise<void> {
|
|
643
|
+
for (const testApp of _sharedApps.values()) {
|
|
644
|
+
testApp._shared = false;
|
|
645
|
+
await testApp.close();
|
|
646
|
+
}
|
|
647
|
+
_sharedApps.clear();
|
|
573
648
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createTestApp, TestApp } from "./TestApp.ts";
|
|
1
|
+
export { createTestApp, closeSharedTestApps, TestApp } from "./TestApp.ts";
|
|
2
2
|
export type { TestFileInput, TestFormValue } from "./TestApp.ts";
|
|
3
3
|
export { TestResponse } from "./TestResponse.ts";
|
|
4
4
|
export type { SessionDecoder, TestResponseContext, InertiaPage } from "./TestResponse.ts";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal bootstrap module shared by the two regression files beside it.
|
|
3
|
+
*
|
|
4
|
+
* The point is that this module is *cached*: both files import it, so both get the
|
|
5
|
+
* same Application instance — which is what `createTestApp` keys its per-process
|
|
6
|
+
* sharing on.
|
|
7
|
+
*/
|
|
8
|
+
import { Application, Router } from "@zerotal/core";
|
|
9
|
+
import { DatabaseProvider } from "@zerotal/orm";
|
|
10
|
+
|
|
11
|
+
const app = Application.create({ env: "test" })
|
|
12
|
+
.register([DatabaseProvider])
|
|
13
|
+
.useConfig({ database: { driver: "sqlite", url: ":memory:" } });
|
|
14
|
+
|
|
15
|
+
Router.get("/ping", () => new Response("pong"));
|
|
16
|
+
|
|
17
|
+
export default app;
|