test-cleanup-stack 1.0.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/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # cleanup-stack
2
+
3
+ Zero-dependency LIFO cleanup queue for TypeScript test suites. Register teardown callbacks at the moment you create a resource — the stack runs them all in reverse order when the test ends, even if the test throws.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install test-cleanup-stack
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```typescript
14
+ import { createRegistry } from 'test-cleanup-stack'
15
+
16
+ const cleanup = createRegistry()
17
+
18
+ // Inside your test — register cleanup right next to creation
19
+ const user = await api.createUser({ email: 'test@example.com' })
20
+ cleanup.add(() => api.deleteUser(user.id), { label: `delete user ${user.id}` })
21
+
22
+ const order = await api.createOrder({ userId: user.id })
23
+ cleanup.add(() => api.cancelOrder(order.id), { label: `cancel order ${order.id}` })
24
+
25
+ // At test end — runs: cancel order → delete user (LIFO)
26
+ await cleanup.flush()
27
+ ```
28
+
29
+ ## Playwright fixture
30
+
31
+ ```typescript
32
+ // core/fixtures/CleanupFixture.ts
33
+ import { test as base } from '@playwright/test'
34
+ import { createRegistry, TeardownRegistry } from 'test-cleanup-stack'
35
+
36
+ export const test = base.extend<{ cleanup: TeardownRegistry }>({
37
+ cleanup: async ({}, use) => {
38
+ const cleanup = createRegistry()
39
+ await use(cleanup)
40
+ await cleanup.flush() // runs after every test, pass or fail
41
+ },
42
+ })
43
+ ```
44
+
45
+ ```typescript
46
+ // your.spec.ts
47
+ import { test } from './CleanupFixture'
48
+
49
+ test('create and cancel order', async ({ cleanup, page }) => {
50
+ const order = await createOrder()
51
+ cleanup.add(() => cancelOrder(order.id), { label: `cancel ${order.id}` })
52
+
53
+ // test logic — if this throws, cleanup.flush() still runs in the fixture
54
+ })
55
+ ```
56
+
57
+ ## Jest / Vitest
58
+
59
+ ```typescript
60
+ import { createRegistry } from 'test-cleanup-stack'
61
+
62
+ let cleanup: ReturnType<typeof createRegistry>
63
+
64
+ beforeEach(() => { cleanup = createRegistry() })
65
+ afterEach(() => cleanup.flush())
66
+
67
+ test('example', async () => {
68
+ const user = await createUser()
69
+ cleanup.add(() => deleteUser(user.id))
70
+ // ...
71
+ })
72
+ ```
73
+
74
+ ## API
75
+
76
+ | Method | Description |
77
+ |--------|-------------|
78
+ | `createRegistry(options?)` | Create an isolated registry (use one per test) |
79
+ | `registry.add(fn, { label?, priority? })` | Register a cleanup callback. Returns a handle with `.remove()` |
80
+ | `registry.flush()` | Run all teardowns LIFO, throw `AggregateError` if any fail |
81
+ | `registry.flushSafe()` | Same as flush but swallows errors — for `afterAll` / best-effort cleanup |
82
+ | `registry.pending()` | List labels of registered-but-not-yet-run teardowns |
83
+ | `registry.reset()` | Clear all without running — for test setup scenarios |
84
+ | `handle.remove()` | Deregister a specific callback without running it |
85
+
86
+ ## License
87
+
88
+ MIT
@@ -0,0 +1,53 @@
1
+ /** Async or sync teardown callback. */
2
+ type TeardownFn = () => Promise<void> | void;
3
+ /** Options for registry.add(). */
4
+ interface AddOptions {
5
+ /** Human-readable label shown in errors and pending(). */
6
+ label?: string;
7
+ /** Lower priority runs later (default 0). Same-priority entries run LIFO. */
8
+ priority?: number;
9
+ }
10
+ /** Options for createRegistry(). */
11
+ interface RegistryOptions {
12
+ /** Called for each failed teardown callback before errors are collected. */
13
+ onError?: (err: unknown, label: string) => void;
14
+ }
15
+ /** Handle returned by registry.add() — use to remove a callback early. */
16
+ interface TeardownHandle {
17
+ remove(): void;
18
+ }
19
+ /** A per-test cleanup registry. Create one per test via createRegistry(). */
20
+ interface TeardownRegistry {
21
+ /**
22
+ * Register a cleanup callback. Returns a handle to remove it before flush.
23
+ * Callbacks registered during an active flush are queued for a second pass.
24
+ */
25
+ add(fn: TeardownFn, options?: AddOptions): TeardownHandle;
26
+ /**
27
+ * Run all callbacks in LIFO/priority order. Collects all errors and throws
28
+ * an AggregateError at the end so every callback always gets a chance to run.
29
+ */
30
+ flush(): Promise<void>;
31
+ /**
32
+ * Like flush() but swallows all errors — logs via onError if provided.
33
+ * Use in afterAll or CI teardown where you want best-effort cleanup.
34
+ */
35
+ flushSafe(): Promise<void>;
36
+ /** Labels of all registered-but-not-yet-flushed callbacks. */
37
+ pending(): string[];
38
+ /** Remove all entries without running them. */
39
+ reset(): void;
40
+ }
41
+ /**
42
+ * Create an isolated teardown registry.
43
+ * Always use createRegistry() per test — never share a singleton across
44
+ * parallel tests or retries.
45
+ */
46
+ declare function createRegistry(options?: RegistryOptions): TeardownRegistry;
47
+ /**
48
+ * Module-level singleton. Only use this in single-worker, non-parallel scripts.
49
+ * For Playwright/WDIO/Vitest parallel tests always use createRegistry() per test.
50
+ */
51
+ declare const registry: TeardownRegistry;
52
+
53
+ export { type AddOptions, type RegistryOptions, type TeardownFn, type TeardownHandle, type TeardownRegistry, createRegistry, registry };
@@ -0,0 +1,53 @@
1
+ /** Async or sync teardown callback. */
2
+ type TeardownFn = () => Promise<void> | void;
3
+ /** Options for registry.add(). */
4
+ interface AddOptions {
5
+ /** Human-readable label shown in errors and pending(). */
6
+ label?: string;
7
+ /** Lower priority runs later (default 0). Same-priority entries run LIFO. */
8
+ priority?: number;
9
+ }
10
+ /** Options for createRegistry(). */
11
+ interface RegistryOptions {
12
+ /** Called for each failed teardown callback before errors are collected. */
13
+ onError?: (err: unknown, label: string) => void;
14
+ }
15
+ /** Handle returned by registry.add() — use to remove a callback early. */
16
+ interface TeardownHandle {
17
+ remove(): void;
18
+ }
19
+ /** A per-test cleanup registry. Create one per test via createRegistry(). */
20
+ interface TeardownRegistry {
21
+ /**
22
+ * Register a cleanup callback. Returns a handle to remove it before flush.
23
+ * Callbacks registered during an active flush are queued for a second pass.
24
+ */
25
+ add(fn: TeardownFn, options?: AddOptions): TeardownHandle;
26
+ /**
27
+ * Run all callbacks in LIFO/priority order. Collects all errors and throws
28
+ * an AggregateError at the end so every callback always gets a chance to run.
29
+ */
30
+ flush(): Promise<void>;
31
+ /**
32
+ * Like flush() but swallows all errors — logs via onError if provided.
33
+ * Use in afterAll or CI teardown where you want best-effort cleanup.
34
+ */
35
+ flushSafe(): Promise<void>;
36
+ /** Labels of all registered-but-not-yet-flushed callbacks. */
37
+ pending(): string[];
38
+ /** Remove all entries without running them. */
39
+ reset(): void;
40
+ }
41
+ /**
42
+ * Create an isolated teardown registry.
43
+ * Always use createRegistry() per test — never share a singleton across
44
+ * parallel tests or retries.
45
+ */
46
+ declare function createRegistry(options?: RegistryOptions): TeardownRegistry;
47
+ /**
48
+ * Module-level singleton. Only use this in single-worker, non-parallel scripts.
49
+ * For Playwright/WDIO/Vitest parallel tests always use createRegistry() per test.
50
+ */
51
+ declare const registry: TeardownRegistry;
52
+
53
+ export { type AddOptions, type RegistryOptions, type TeardownFn, type TeardownHandle, type TeardownRegistry, createRegistry, registry };
package/dist/index.js ADDED
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createRegistry: () => createRegistry,
24
+ registry: () => registry
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+ var _seq = 0;
28
+ function createRegistry(options = {}) {
29
+ const { onError } = options;
30
+ const entries = [];
31
+ let midFlushQueue = [];
32
+ let flushing = false;
33
+ function add(fn, opts = {}) {
34
+ const id = ++_seq;
35
+ const entry = {
36
+ fn,
37
+ label: opts.label ?? `teardown-${id}`,
38
+ priority: opts.priority ?? 0,
39
+ id
40
+ };
41
+ if (flushing) {
42
+ midFlushQueue.push(entry);
43
+ } else {
44
+ entries.push(entry);
45
+ }
46
+ return {
47
+ remove() {
48
+ let idx = entries.findIndex((e) => e.id === id);
49
+ if (idx !== -1) {
50
+ entries.splice(idx, 1);
51
+ return;
52
+ }
53
+ idx = midFlushQueue.findIndex((e) => e.id === id);
54
+ if (idx !== -1) midFlushQueue.splice(idx, 1);
55
+ }
56
+ };
57
+ }
58
+ async function runBatch(batch) {
59
+ const sorted = [...batch].sort(
60
+ (a, b) => b.priority !== a.priority ? b.priority - a.priority : b.id - a.id
61
+ );
62
+ const failures = [];
63
+ for (const entry of sorted) {
64
+ try {
65
+ await entry.fn();
66
+ } catch (err) {
67
+ onError?.(err, entry.label);
68
+ failures.push({ err, label: entry.label });
69
+ }
70
+ }
71
+ return failures;
72
+ }
73
+ async function flush() {
74
+ flushing = true;
75
+ const allFailures = [];
76
+ const primary = [...entries];
77
+ entries.length = 0;
78
+ allFailures.push(...await runBatch(primary));
79
+ while (midFlushQueue.length > 0) {
80
+ const secondary = [...midFlushQueue];
81
+ midFlushQueue = [];
82
+ allFailures.push(...await runBatch(secondary));
83
+ }
84
+ flushing = false;
85
+ if (allFailures.length > 0) {
86
+ const aggregate = new AggregateError(
87
+ allFailures.map((f) => f.err),
88
+ `cleanup-stack: ${allFailures.length} teardown(s) failed: ${allFailures.map((f) => f.label).join(", ")}`
89
+ );
90
+ throw aggregate;
91
+ }
92
+ }
93
+ async function flushSafe() {
94
+ try {
95
+ await flush();
96
+ } catch {
97
+ }
98
+ }
99
+ function pending() {
100
+ return [...entries, ...midFlushQueue].map((e) => e.label);
101
+ }
102
+ function reset() {
103
+ entries.length = 0;
104
+ midFlushQueue = [];
105
+ }
106
+ return { add, flush, flushSafe, pending, reset };
107
+ }
108
+ var registry = createRegistry();
109
+ // Annotate the CommonJS export names for ESM import in node:
110
+ 0 && (module.exports = {
111
+ createRegistry,
112
+ registry
113
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,87 @@
1
+ // src/index.ts
2
+ var _seq = 0;
3
+ function createRegistry(options = {}) {
4
+ const { onError } = options;
5
+ const entries = [];
6
+ let midFlushQueue = [];
7
+ let flushing = false;
8
+ function add(fn, opts = {}) {
9
+ const id = ++_seq;
10
+ const entry = {
11
+ fn,
12
+ label: opts.label ?? `teardown-${id}`,
13
+ priority: opts.priority ?? 0,
14
+ id
15
+ };
16
+ if (flushing) {
17
+ midFlushQueue.push(entry);
18
+ } else {
19
+ entries.push(entry);
20
+ }
21
+ return {
22
+ remove() {
23
+ let idx = entries.findIndex((e) => e.id === id);
24
+ if (idx !== -1) {
25
+ entries.splice(idx, 1);
26
+ return;
27
+ }
28
+ idx = midFlushQueue.findIndex((e) => e.id === id);
29
+ if (idx !== -1) midFlushQueue.splice(idx, 1);
30
+ }
31
+ };
32
+ }
33
+ async function runBatch(batch) {
34
+ const sorted = [...batch].sort(
35
+ (a, b) => b.priority !== a.priority ? b.priority - a.priority : b.id - a.id
36
+ );
37
+ const failures = [];
38
+ for (const entry of sorted) {
39
+ try {
40
+ await entry.fn();
41
+ } catch (err) {
42
+ onError?.(err, entry.label);
43
+ failures.push({ err, label: entry.label });
44
+ }
45
+ }
46
+ return failures;
47
+ }
48
+ async function flush() {
49
+ flushing = true;
50
+ const allFailures = [];
51
+ const primary = [...entries];
52
+ entries.length = 0;
53
+ allFailures.push(...await runBatch(primary));
54
+ while (midFlushQueue.length > 0) {
55
+ const secondary = [...midFlushQueue];
56
+ midFlushQueue = [];
57
+ allFailures.push(...await runBatch(secondary));
58
+ }
59
+ flushing = false;
60
+ if (allFailures.length > 0) {
61
+ const aggregate = new AggregateError(
62
+ allFailures.map((f) => f.err),
63
+ `cleanup-stack: ${allFailures.length} teardown(s) failed: ${allFailures.map((f) => f.label).join(", ")}`
64
+ );
65
+ throw aggregate;
66
+ }
67
+ }
68
+ async function flushSafe() {
69
+ try {
70
+ await flush();
71
+ } catch {
72
+ }
73
+ }
74
+ function pending() {
75
+ return [...entries, ...midFlushQueue].map((e) => e.label);
76
+ }
77
+ function reset() {
78
+ entries.length = 0;
79
+ midFlushQueue = [];
80
+ }
81
+ return { add, flush, flushSafe, pending, reset };
82
+ }
83
+ var registry = createRegistry();
84
+ export {
85
+ createRegistry,
86
+ registry
87
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "test-cleanup-stack",
3
+ "version": "1.0.0",
4
+ "description": "Zero-dependency LIFO cleanup queue for TypeScript test suites — survives test failures and retries",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "require": "./dist/index.js",
11
+ "import": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "files": ["dist", "README.md"],
15
+ "scripts": {
16
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": ["testing", "qa", "teardown", "cleanup", "playwright", "webdriverio", "jest", "vitest", "typescript"],
20
+ "license": "MIT",
21
+ "author": "Biswarak Raktim",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/raktim1988/cleanup-stack.git"
25
+ },
26
+ "homepage": "https://github.com/raktim1988/cleanup-stack#readme",
27
+ "bugs": {
28
+ "url": "https://github.com/raktim1988/cleanup-stack/issues"
29
+ },
30
+ "engines": { "node": ">=16" },
31
+ "devDependencies": {
32
+ "tsup": "^8.0.0",
33
+ "typescript": "^5.4.0"
34
+ }
35
+ }