Skip to content

Commit 72ca85e

Browse files
authored
Feat/improve runner (#67)
* feat: improve export files * test: refactor api * feat: add new runner version improved * feat: improve runner and ui * feat: refactor code to remove old runner * refactor: remove old runner refenrences * test: update all ui tests related to the new runner migration * test: error user event
1 parent fef815f commit 72ca85e

25 files changed

Lines changed: 784 additions & 512 deletions

examples/my-twd-app/public/mock-sw.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/my-twd-app/src/twd-tests/app.twd.test.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
1-
import { describe, it, itOnly, itSkip, beforeEach, twd, expect, userEvent } from "../../../../src";
1+
import { twd, expect, userEvent } from "../../../../src";
2+
import { describe, it, beforeEach } from "../../../../src/runner";
23

3-
beforeEach(() => {
4-
console.log("Reset state before each test");
5-
});
64

75
describe("App interactions", () => {
8-
it("clicks the button", async () => {
9-
const btn = await twd.get("button");
10-
userEvent.click(btn.el);
6+
beforeEach(() => {
7+
console.log("Reset state before each test");
8+
});
9+
10+
describe("nested level 1", () => {
11+
beforeEach(() => {
12+
console.log("Reset state before each test 1");
13+
});
14+
describe("nested level 2", () => {
15+
beforeEach(() => {
16+
console.log("Reset state before each test 2");
17+
});
18+
it("clicks the button", async () => {
19+
const btn = await twd.get("button");
20+
userEvent.click(btn.el);
21+
});
22+
});
1123
});
1224

13-
itSkip("skipped test", () => {
25+
it.skip("skipped test", () => {
1426
throw new Error("Should not run");
1527
});
1628

17-
itOnly("only this one runs if present and long text to check the layout", async () => {
29+
it.only("only this one runs if present and long text to check the layout", async () => {
1830
const user = userEvent.setup();
1931
const btn = await twd.get("button");
2032
await user.click(btn.el);

examples/my-twd-app/src/twd-tests/assertion-examples.twd.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { it, twd, userEvent } from "../../../../src";
1+
import { twd, userEvent } from "../../../../src";
2+
import { it } from "../../../../src/runner";
23

34
it("assertion examples", async () => {
45
await twd.visit("/assertions");

examples/my-twd-app/vite.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { defineConfig } from 'vite'
22
import react from '@vitejs/plugin-react'
3-
// import { removeMockServiceWorker } from '../../src';
3+
import { removeMockServiceWorker } from '../../src/vite-plugin';
44

55
// https://vite.dev/config/
66
export default defineConfig({
7-
plugins: [react()],
7+
plugins: [react(), removeMockServiceWorker()],
88
})

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@
2323
"types": "./dist/index.d.ts",
2424
"import": "./dist/twd.es.js",
2525
"require": "./dist/twd.umd.js"
26+
},
27+
"./runner": {
28+
"types": "./dist/runner.d.ts",
29+
"import": "./dist/runner.es.js",
30+
"require": "./dist/runner.umd.js"
31+
},
32+
"./vite-plugin": {
33+
"types": "./dist/vite-plugin.d.ts",
34+
"import": "./dist/vite-plugin.es.js",
35+
"require": "./dist/vite-plugin.umd.js"
2636
}
2737
},
2838
"files": [

src/index.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
1-
export {
2-
beforeEach,
3-
describe,
4-
it,
5-
itOnly,
6-
itSkip,
7-
twd,
8-
} from './twd';
1+
export { twd } from './twd';
92
import { config } from 'chai';
103
export { TWDSidebar } from './ui/TWDSidebar';
114
export { initTests } from './initializers/initTests';
125
export { expect } from 'chai';
136
export { userEvent } from './proxies/userEvent';
14-
export { removeMockServiceWorker } from './plugin/removeMockServiceWorker';
157

168
config.truncateThreshold = 0;

src/runner.ts

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
export interface Handler {
2+
id: string;
3+
name: string;
4+
parent?: string;
5+
handler: () => void | Promise<void>;
6+
children?: string[];
7+
type: 'suite' | 'test';
8+
status?: 'idle' | 'pass' | 'fail' | 'skip' | 'running';
9+
logs: string[];
10+
depth: number;
11+
only?: boolean;
12+
skip?: boolean;
13+
}
14+
15+
type HookFn = () => void | Promise<void>;
16+
17+
export const handlers = new Map<string, Handler>();
18+
const beforeEachHooks = new Map<string, HookFn[]>();
19+
const afterEachHooks = new Map<string, HookFn[]>();
20+
const stack: string[] = [];
21+
22+
const generateId = () => Math.random().toString(36).substr(2, 9);
23+
24+
//
25+
// Suite definition
26+
//
27+
export const describe = (name: string, handler: () => void) => {
28+
const id = generateId();
29+
const parent = stack.at(-1);
30+
handlers.set(id, {
31+
id,
32+
name,
33+
type: 'suite',
34+
children: [],
35+
logs: [],
36+
depth: stack.length,
37+
parent,
38+
handler,
39+
});
40+
if (parent) handlers.get(parent)!.children!.push(id);
41+
42+
stack.push(id);
43+
handler();
44+
stack.pop();
45+
};
46+
47+
//
48+
// Test definition
49+
//
50+
export const it = (name: string, handler: () => void | Promise<void>) => {
51+
const id = generateId();
52+
const parent = stack.at(-1);
53+
const h: Handler = {
54+
id,
55+
name,
56+
type: 'test',
57+
depth: stack.length,
58+
handler,
59+
logs: [],
60+
parent,
61+
};
62+
if (parent) handlers.get(parent)!.children!.push(id);
63+
handlers.set(id, h);
64+
};
65+
66+
// Aliases for it.only and it.skip
67+
it.only = (name: string, handler: () => void | Promise<void>) => {
68+
const id = generateId();
69+
const parent = stack.at(-1);
70+
const h: Handler = {
71+
id,
72+
name,
73+
type: 'test',
74+
depth: stack.length,
75+
handler,
76+
logs: [],
77+
parent,
78+
only: true,
79+
};
80+
if (parent) handlers.get(parent)!.children!.push(id);
81+
handlers.set(id, h);
82+
};
83+
84+
it.skip = (name: string, handler?: () => void | Promise<void>) => {
85+
const id = generateId();
86+
const parent = stack.at(-1);
87+
const h: Handler = {
88+
id,
89+
name,
90+
type: 'test',
91+
depth: stack.length,
92+
handler: handler || (() => {}),
93+
logs: [],
94+
parent,
95+
skip: true,
96+
};
97+
if (parent) handlers.get(parent)!.children!.push(id);
98+
handlers.set(id, h);
99+
};
100+
101+
//
102+
// Hooks
103+
//
104+
export const beforeEach = (fn: HookFn) => {
105+
const currentSuite = stack.at(-1);
106+
if (!currentSuite) throw new Error('beforeEach() must be inside a describe()');
107+
if (!beforeEachHooks.has(currentSuite)) beforeEachHooks.set(currentSuite, []);
108+
beforeEachHooks.get(currentSuite)!.push(fn);
109+
};
110+
111+
export const afterEach = (fn: HookFn) => {
112+
const currentSuite = stack.at(-1);
113+
if (!currentSuite) throw new Error('afterEach() must be inside a describe()');
114+
if (!afterEachHooks.has(currentSuite)) afterEachHooks.set(currentSuite, []);
115+
afterEachHooks.get(currentSuite)!.push(fn);
116+
};
117+
118+
const collectHooks = (suiteId: string) => {
119+
const before: HookFn[] = [];
120+
const after: HookFn[] = [];
121+
let current: string | undefined = suiteId;
122+
while (current) {
123+
if (beforeEachHooks.has(current)) before.unshift(...beforeEachHooks.get(current)!);
124+
if (afterEachHooks.has(current)) after.push(...afterEachHooks.get(current)!);
125+
current = handlers.get(current)?.parent;
126+
}
127+
return { before, after };
128+
};
129+
130+
export const clearTests = () => {
131+
handlers.clear();
132+
beforeEachHooks.clear();
133+
afterEachHooks.clear();
134+
};
135+
136+
export interface RunnerEvents {
137+
onStart: (test: Handler) => void;
138+
onPass: (test: Handler) => void;
139+
onFail: (test: Handler, error: Error) => void;
140+
onSkip: (test: Handler) => void;
141+
onSuiteStart?: (suite: Handler) => void;
142+
onSuiteEnd?: (suite: Handler) => void;
143+
}
144+
145+
export class TestRunner {
146+
private events: RunnerEvents;
147+
148+
constructor(events: RunnerEvents) {
149+
this.events = events;
150+
}
151+
152+
async runAll() {
153+
const rootSuites = Array.from(handlers.values()).filter(
154+
(h) => !h.parent && h.type === "suite"
155+
);
156+
const hasOnly = Array.from(handlers.values()).some((h) => h.only);
157+
for (const suite of rootSuites) {
158+
await this.runSuite(suite, hasOnly);
159+
}
160+
}
161+
162+
async runSingle(id: string) {
163+
const handler = handlers.get(id);
164+
if (!handler || handler.type !== "test") return;
165+
const hasOnly = false; // Single run ignores .only logic
166+
await this.runTest(handler, hasOnly);
167+
}
168+
169+
private async runSuite(suite: Handler, hasOnly: boolean) {
170+
this.events.onSuiteStart?.(suite);
171+
const children = (suite.children || []).map((id) => handlers.get(id)!);
172+
173+
for (const child of children) {
174+
if (child.type === "suite") {
175+
await this.runSuite(child, hasOnly);
176+
} else if (child.type === "test") {
177+
await this.runTest(child, hasOnly);
178+
}
179+
}
180+
this.events.onSuiteEnd?.(suite);
181+
}
182+
183+
private async runTest(test: Handler, hasOnly: boolean) {
184+
if (test.skip) {
185+
this.events.onSkip(test);
186+
return;
187+
}
188+
189+
if (hasOnly && !test.only) return;
190+
191+
this.events.onStart?.(test);
192+
const hooks = collectHooks(test.parent!);
193+
194+
try {
195+
for (const hook of hooks.before) await hook();
196+
await test.handler();
197+
this.events.onPass(test);
198+
} catch (err) {
199+
this.events.onFail(test, err as Error);
200+
} finally {
201+
for (const hook of hooks.after) await hook();
202+
}
203+
}
204+
}

0 commit comments

Comments
 (0)