├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── deno.json
├── deno.lock
├── mod.ts
├── preview.png
└── src
├── environment.ts
├── environment_test.ts
├── mod.ts
├── mod_test.ts
├── nodes
├── mod.ts
├── nodes.ts
├── nodes_test.ts
├── tree.ts
├── tree_test.ts
├── utilities.ts
└── utilities_test.ts
├── reporter.ts
├── runner.ts
├── runner_test.ts
└── test_util.ts
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: master
6 | pull_request:
7 | branches: master
8 | workflow_dispatch:
9 |
10 | jobs:
11 | test:
12 | runs-on: ${{ matrix.os }}
13 |
14 | strategy:
15 | matrix:
16 | os: [macOS-latest, windows-latest, ubuntu-latest]
17 |
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v2
21 |
22 | - name: Setup deno
23 | uses: denoland/setup-deno@main
24 | with:
25 | deno-version: v2.x
26 |
27 | - name: Test
28 | run: |
29 | deno --version
30 | deno test
31 |
32 | lint:
33 | runs-on: ubuntu-latest
34 |
35 | steps:
36 | - name: Checkout
37 | uses: actions/checkout@v2
38 |
39 | - name: Setup deno
40 | uses: denoland/setup-deno@main
41 | with:
42 | deno-version: v2.x
43 |
44 | - name: Lint
45 | run: |
46 | deno --version
47 | deno lint
48 | deno fmt --ignore=README.md --check
49 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.vscode
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 gcaptn
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
tincan
4 |
5 |
6 | A lightweight Jest-like testing library for Deno
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ## Deprecation
16 |
17 | See the standard library's BDD testing interface instead:
18 | https://jsr.io/@std/testing/doc/bdd
19 |
20 | ## Features
21 |
22 | - Nested suites / cases
23 | - Reports cases with the full hierarchy
24 | - Hooks (`beforeAll`, `afterAll`, `beforeEach`, `afterEach`)
25 | - Focusing (`*.only()`)
26 | - Skipping (`*.skip()`)
27 | - Uses `Deno.test`, works with the built-in reporter
28 | - Lightweight
29 |
30 | ## Running
31 |
32 | ```sh
33 | deno test
34 | ```
35 |
36 | ## Usage
37 |
38 | ```ts
39 | import {
40 | beforeEach,
41 | describe,
42 | expect,
43 | it,
44 | run,
45 | } from "https://deno.land/x/tincan/mod.ts";
46 |
47 | describe("Array", () => {
48 | let array: number[];
49 |
50 | beforeEach(() => {
51 | array = [];
52 | });
53 |
54 | describe("#indexOf()", () => {
55 | it("should return the first index of an item", () => {
56 | array.push(0);
57 | expect(array.indexOf(0)).toBe(0);
58 | });
59 |
60 | it.only("should return -1 when the item isn't found", () => {
61 | expect(array.indexOf(0)).toBe(-1);
62 | });
63 | });
64 | });
65 |
66 | run();
67 | ```
68 |
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "exports": "./mod.ts",
3 | "imports": {
4 | "colors": "jsr:@std/fmt@^1.0.4/colors",
5 | "expect": "jsr:@std/expect@^1.0.11",
6 | "expect-legacy": "https://deno.land/x/expect@v0.4.0/mod.ts"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/deno.lock:
--------------------------------------------------------------------------------
1 | {
2 | "version": "4",
3 | "specifiers": {
4 | "jsr:@std/assert@^1.0.10": "1.0.10",
5 | "jsr:@std/expect@^1.0.11": "1.0.11",
6 | "jsr:@std/fmt@^1.0.4": "1.0.4",
7 | "jsr:@std/internal@^1.0.5": "1.0.5"
8 | },
9 | "jsr": {
10 | "@std/assert@1.0.10": {
11 | "integrity": "59b5cbac5bd55459a19045d95cc7c2ff787b4f8527c0dd195078ff6f9481fbb3",
12 | "dependencies": [
13 | "jsr:@std/internal"
14 | ]
15 | },
16 | "@std/expect@1.0.11": {
17 | "integrity": "5aa5d5cf891e9a3249e45ea770de15189e5a2faee2122ee5746b10d1c310a19b",
18 | "dependencies": [
19 | "jsr:@std/assert",
20 | "jsr:@std/internal"
21 | ]
22 | },
23 | "@std/fmt@1.0.4": {
24 | "integrity": "e14fe5bedee26f80877e6705a97a79c7eed599e81bb1669127ef9e8bc1e29a74"
25 | },
26 | "@std/internal@1.0.5": {
27 | "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
28 | }
29 | },
30 | "remote": {
31 | "https://deno.land/std@0.97.0/fmt/colors.ts": "db22b314a2ae9430ae7460ce005e0a7130e23ae1c999157e3bb77cf55800f7e4",
32 | "https://deno.land/std@0.97.0/testing/_diff.ts": "961eaf6d9f5b0a8556c9d835bbc6fa74f5addd7d3b02728ba7936ff93364f7a3",
33 | "https://deno.land/std@0.97.0/testing/asserts.ts": "341292d12eebc44be4c3c2ca101ba8b6b5859cef2fa69d50c217f9d0bfbcfd1f",
34 | "https://deno.land/x/expect@v0.4.0/expect.ts": "1d1856758a750f440d0b65d74f19e5d4829bb76d8e576d05546abd8e7b1dfb9e",
35 | "https://deno.land/x/expect@v0.4.0/matchers.ts": "55acf74a3c4a308d079798930f05ab11da2080ec7acd53517193ca90d1296bf7",
36 | "https://deno.land/x/expect@v0.4.0/mock.ts": "562d4b1d735d15b0b8e935f342679096b64fe452f86e96714fe8616c0c884914",
37 | "https://deno.land/x/expect@v0.4.0/mod.ts": "0304d2430e1e96ba669a8495e24ba606dcc3d152e1f81aaa8da898cea24e36c2"
38 | },
39 | "workspace": {
40 | "dependencies": [
41 | "jsr:@std/expect@^1.0.11",
42 | "jsr:@std/fmt@^1.0.4"
43 | ]
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/mod.ts:
--------------------------------------------------------------------------------
1 | export * from "./src/mod.ts";
2 |
--------------------------------------------------------------------------------
/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gcaptn/tincan/29d1bff4dd01f59c1bf91b0369429ed2f7bca86f/preview.png
--------------------------------------------------------------------------------
/src/environment.ts:
--------------------------------------------------------------------------------
1 | import { Hook, type TestFunction, Tree } from "./nodes/mod.ts";
2 | import { Runner } from "./runner.ts";
3 |
4 | export class Environment {
5 | runner = new Runner();
6 | currentTree = new Tree();
7 |
8 | // Because run() can be called multiple times and there can be more than one
9 | // test tree, calling hooks/describe/it inside a test case will register it in
10 | // another tree. This toggle will change before and after every test case to
11 | // lock the methods.
12 | private isRunningCase = false;
13 |
14 | private assertNotRunning(method: string) {
15 | if (this.isRunningCase) {
16 | throw new Error(
17 | `${method} cannot be called while a test case is running!`,
18 | );
19 | }
20 | }
21 |
22 | async run() {
23 | const tree = this.currentTree;
24 | this.currentTree = new Tree();
25 |
26 | tree.root.beforeEach.unshift(
27 | new Hook("internal", () => {
28 | this.isRunningCase = true;
29 | }),
30 | );
31 |
32 | tree.root.afterEach.push(
33 | new Hook("internal", () => {
34 | this.isRunningCase = false;
35 | }),
36 | );
37 |
38 | await this.runner.runNode(tree.root);
39 | }
40 |
41 | describe(headline: string, fn: () => void) {
42 | this.assertNotRunning("describe()");
43 | this.currentTree.describe(headline, fn);
44 | }
45 |
46 | describeOnly(headline: string, fn: () => void) {
47 | this.assertNotRunning("describe.only()");
48 | this.currentTree.describeOnly(headline, fn);
49 | }
50 |
51 | describeSkip(headline: string, fn: () => void) {
52 | this.assertNotRunning("describe.skip()");
53 | this.currentTree.describeSkip(headline, fn);
54 | }
55 |
56 | it(headline: string, fn: TestFunction) {
57 | this.assertNotRunning("it()");
58 | this.currentTree.it(headline, fn);
59 | }
60 |
61 | itOnly(headline: string, fn: TestFunction) {
62 | this.assertNotRunning("it.only()");
63 | this.currentTree.itOnly(headline, fn);
64 | }
65 |
66 | itSkip(headline: string, fn: TestFunction) {
67 | this.assertNotRunning("it.skip()");
68 | this.currentTree.itSkip(headline, fn);
69 | }
70 |
71 | beforeAll(fn: TestFunction) {
72 | this.assertNotRunning("beforeAll()");
73 | this.currentTree.beforeAll(fn);
74 | }
75 |
76 | beforeEach(fn: TestFunction) {
77 | this.assertNotRunning("beforeEach()");
78 | this.currentTree.beforeEach(fn);
79 | }
80 |
81 | afterEach(fn: TestFunction) {
82 | this.assertNotRunning("afterEach()");
83 | this.currentTree.afterEach(fn);
84 | }
85 |
86 | afterAll(fn: TestFunction) {
87 | this.assertNotRunning("afterAll()");
88 | this.currentTree.afterAll(fn);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/environment_test.ts:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Environment.run
4 | replaces the tree
5 | can run again when the previous run finishes
6 |
7 | Environment
8 | refuses to add hooks within a test case
9 | refuses to add nodes within a test case
10 |
11 | */
12 |
13 | import { Environment } from "./environment.ts";
14 | import { SilentReporter, silentTest } from "./test_util.ts";
15 | import { expect } from "expect";
16 |
17 | function noop() {}
18 |
19 | function createTestEnvironment() {
20 | const env = new Environment();
21 | env.runner.test = silentTest;
22 | env.runner.reporter = new SilentReporter();
23 | return env;
24 | }
25 |
26 | Deno.test("Environment.run replaces the tree", async () => {
27 | const env = createTestEnvironment();
28 | const tree = env.currentTree;
29 | env.it("_", noop);
30 | await env.run();
31 | expect(env.currentTree).not.toBe(tree);
32 | });
33 |
34 | Deno.test("Environment.run can run again when the previous run finishes", async () => {
35 | const env = createTestEnvironment();
36 | env.it("_", noop);
37 | await env.run();
38 |
39 | expect(() => {
40 | env.it("_", noop);
41 | env.run();
42 | }).not.toThrow();
43 | });
44 |
45 | Deno.test("Environment refuses to add hooks within a test case", async () => {
46 | const env = createTestEnvironment();
47 |
48 | env.it("_", () => {
49 | expect(() => {
50 | env.beforeAll(noop);
51 | }).toThrow();
52 |
53 | expect(() => {
54 | env.beforeEach(noop);
55 | }).toThrow();
56 |
57 | expect(() => {
58 | env.afterEach(noop);
59 | }).toThrow();
60 |
61 | expect(() => {
62 | env.afterAll(noop);
63 | }).toThrow();
64 | });
65 |
66 | await env.run();
67 | });
68 |
69 | Deno.test("Environment refuses to add nodes within a test case", async () => {
70 | const env = createTestEnvironment();
71 |
72 | env.it("_", () => {
73 | expect(() => {
74 | env.it("_", noop);
75 | }).toThrow();
76 |
77 | expect(() => {
78 | env.describe("_", () => {
79 | env.it("_", noop);
80 | });
81 | }).toThrow();
82 | });
83 |
84 | await env.run();
85 | });
86 |
--------------------------------------------------------------------------------
/src/mod.ts:
--------------------------------------------------------------------------------
1 | import type { TestFunction } from "./nodes/mod.ts";
2 | import { Environment } from "./environment.ts";
3 |
4 | export { expect } from "expect";
5 | export { mock } from "expect-legacy";
6 |
7 | const env = new Environment();
8 |
9 | /** Register a suite to group related tests. */
10 | export function describe(headline: string, fn: () => void) {
11 | env.describe(headline, fn);
12 | }
13 |
14 | /**
15 | * Run only this suite and skip all the other sibling test suites/cases. Will
16 | * not skip siblings that are also registered with `.only()`.
17 | */
18 | describe.only = function (headline: string, fn: () => void): void {
19 | env.describeOnly(headline, fn);
20 | };
21 |
22 | /** Skip this test suite. */
23 | describe.skip = function (headline: string, fn: () => void): void {
24 | env.describeSkip(headline, fn);
25 | };
26 |
27 | /** Register a test case. */
28 | export function it(headline: string, fn: TestFunction) {
29 | env.it(headline, fn);
30 | }
31 |
32 | /**
33 | * Run only this test case and skip all the other sibling test suites/cases.
34 | * Will not skip siblings that are also registered with `.only()`.
35 | */
36 | it.only = function (headline: string, fn: TestFunction): void {
37 | env.itOnly(headline, fn);
38 | };
39 |
40 | /** Skip this test case. */
41 | it.skip = function (headline: string, fn: TestFunction): void {
42 | env.itSkip(headline, fn);
43 | };
44 |
45 | /**
46 | * Register a hook to run before any of the tests in the current scope are ran.
47 | * If the hook fails, it would only report an error and test cases would still
48 | * run.
49 | */
50 | export function beforeAll(fn: TestFunction) {
51 | env.beforeAll(fn);
52 | }
53 |
54 | /**
55 | * Register a hook to run before every test case. If the hook fails, it would
56 | * only report an error and test cases would still run.
57 | */
58 | export function beforeEach(fn: TestFunction) {
59 | env.beforeEach(fn);
60 | }
61 |
62 | /** Register a hook to run after every test case. */
63 | export function afterEach(fn: TestFunction) {
64 | env.afterEach(fn);
65 | }
66 |
67 | /**
68 | * Register a hook to run after all the test suites/cases in the current scope
69 | * have completed.
70 | */
71 | export function afterAll(fn: TestFunction) {
72 | env.afterAll(fn);
73 | }
74 |
75 | /**
76 | * Call at the bottom of every test file after all the test suites/cases
77 | * have been registered.
78 | */
79 | export function run() {
80 | env.run();
81 | }
82 |
--------------------------------------------------------------------------------
/src/mod_test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | afterAll,
3 | afterEach,
4 | beforeAll,
5 | beforeEach,
6 | describe,
7 | expect,
8 | it,
9 | mock,
10 | run,
11 | } from "./mod.ts";
12 |
13 | function noop() {}
14 |
15 | describe("test", () => {
16 | it("should wait for promises", () => {
17 | return new Promise((resolve) => setTimeout(resolve, 20));
18 | });
19 |
20 | it("should refuse to add hooks within a case", () => {
21 | expect(() => {
22 | beforeAll(noop);
23 | }).toThrow();
24 |
25 | expect(() => {
26 | beforeEach(noop);
27 | }).toThrow();
28 |
29 | expect(() => {
30 | afterEach(noop);
31 | }).toThrow();
32 |
33 | expect(() => {
34 | afterAll(noop);
35 | }).toThrow();
36 | });
37 |
38 | it("should refuse to add nodes within a case", () => {
39 | expect(() => {
40 | describe("_", noop);
41 | }).toThrow();
42 |
43 | expect(() => {
44 | it("_", noop);
45 | }).toThrow();
46 | });
47 |
48 | it.skip("should log a pretty output for failing cases", () => {
49 | throw new Error("error");
50 | });
51 |
52 | describe("skip", () => {
53 | it.skip("should not run skipped tests", () => {
54 | throw new Error("this should not run");
55 | });
56 |
57 | describe.skip("skipped suites", () => {
58 | it("should mark its children as skipped", () => {
59 | throw new Error("this should not run");
60 | });
61 | });
62 |
63 | it("_", noop);
64 | });
65 |
66 | describe("only", () => {
67 | it.only("should only run focused nodes", noop);
68 |
69 | it("should mark everything else as skipped", () => {
70 | throw new Error("this should not run");
71 | });
72 |
73 | describe.only("focused suites", () => {
74 | const caseFn = mock.fn();
75 |
76 | it("_", caseFn);
77 |
78 | it("should run cases inside", () => {
79 | expect(caseFn).toHaveBeenCalled();
80 | });
81 | });
82 |
83 | describe.only("focused nested cases", () => {
84 | it("should not run other cases", () => {
85 | throw new Error("this should not run");
86 | });
87 |
88 | const caseFn = mock.fn();
89 |
90 | it.only("_", caseFn);
91 |
92 | it.only("should run focused cases", () => {
93 | expect(caseFn).toHaveBeenCalled();
94 | });
95 | });
96 | });
97 |
98 | describe("hooks", () => {
99 | describe("hook errors", () => {
100 | beforeAll(() => {
101 | throw new Error("hook error");
102 | });
103 | it("should catch and report hook errors", noop);
104 | });
105 | });
106 | });
107 |
108 | run();
109 |
--------------------------------------------------------------------------------
/src/nodes/mod.ts:
--------------------------------------------------------------------------------
1 | export * from "./nodes.ts";
2 | export * from "./tree.ts";
3 | export * from "./utilities.ts";
4 |
--------------------------------------------------------------------------------
/src/nodes/nodes.ts:
--------------------------------------------------------------------------------
1 | export type TestResult = "FAIL" | "PASS";
2 |
3 | export type TestFunction = () => void | Promise;
4 |
5 | type HookType =
6 | | "internal"
7 | | "beforeAll"
8 | | "beforeEach"
9 | | "afterEach"
10 | | "afterAll";
11 |
12 | type ParentNode = {
13 | children: (DescribeNode | ItNode)[];
14 | hasFocused: boolean;
15 | updateFocusedChildren: () => void;
16 | };
17 |
18 | type ChildNode = {
19 | parent: DescribeNode | RootNode;
20 | skipped: boolean;
21 | focused: boolean;
22 | skip: () => void;
23 | focus: () => void;
24 | };
25 |
26 | function assertNotEmpty(headline: string) {
27 | if (!headline.match(/\S/g)) {
28 | throw new Error("Headline cannot be empty!");
29 | }
30 | }
31 |
32 | function hasCase(node: DescribeNode | RootNode): boolean {
33 | for (const child of node.children) {
34 | if (child instanceof ItNode) {
35 | if (!child.skipped) {
36 | return true;
37 | }
38 | } else if (hasCase(child)) {
39 | return true;
40 | }
41 | }
42 | return false;
43 | }
44 |
45 | export function assertHasCase(node: DescribeNode | RootNode, method: string) {
46 | if (!hasCase(node)) {
47 | throw new Error(`${method} should have at least one test case!`);
48 | }
49 | }
50 |
51 | export class Hook {
52 | type: HookType;
53 | fn: TestFunction;
54 | constructor(type: HookType, fn: TestFunction) {
55 | this.type = type;
56 | this.fn = fn;
57 | }
58 | }
59 |
60 | export class RootNode implements ParentNode {
61 | children: (DescribeNode | ItNode)[] = [];
62 | beforeAll: Hook[] = [];
63 | afterAll: Hook[] = [];
64 | beforeEach: Hook[] = [];
65 | afterEach: Hook[] = [];
66 | result: TestResult = "PASS";
67 | timeTaken = 0;
68 | startTime = 0;
69 | hasFocused = false;
70 |
71 | updateFocusedChildren() {
72 | this.hasFocused = true;
73 | this.children.forEach((child) => {
74 | if (child.focused === false) {
75 | child.skip();
76 | }
77 | });
78 | }
79 |
80 | start() {
81 | assertHasCase(this, "Tests");
82 | this.startTime = Date.now();
83 | }
84 |
85 | fail() {
86 | this.result = "FAIL";
87 | }
88 |
89 | finish() {
90 | this.timeTaken = Date.now() - this.startTime;
91 | }
92 | }
93 |
94 | export class DescribeNode implements ParentNode, ChildNode {
95 | children: (DescribeNode | ItNode)[] = [];
96 | parent: RootNode | DescribeNode;
97 | headline: string;
98 | result: TestResult = "PASS";
99 | beforeAll: Hook[] = [];
100 | afterAll: Hook[] = [];
101 | beforeEach: Hook[] = [];
102 | afterEach: Hook[] = [];
103 | skipped = false;
104 | focused = false;
105 | hasFocused = false;
106 |
107 | constructor(headline: string, parent: RootNode | DescribeNode) {
108 | assertNotEmpty(headline);
109 | this.headline = headline;
110 | this.parent = parent;
111 | }
112 |
113 | skip() {
114 | this.skipped = true;
115 | this.children.forEach((child) => child.skip());
116 | }
117 |
118 | focus() {
119 | this.focused = true;
120 | this.parent.updateFocusedChildren();
121 | }
122 |
123 | updateFocusedChildren() {
124 | this.hasFocused = true;
125 | this.children.forEach((child) => {
126 | if (child.focused === false) {
127 | child.skip();
128 | }
129 | });
130 | }
131 |
132 | fail() {
133 | this.result = "FAIL";
134 | this.parent.fail();
135 | }
136 | }
137 |
138 | export class ItNode implements ChildNode {
139 | parent: DescribeNode | RootNode;
140 | headline: string;
141 | fn: TestFunction;
142 | result: TestResult = "PASS";
143 | error: unknown;
144 | timeTaken = 0;
145 | startTime = 0;
146 | skipped = false;
147 | focused = false;
148 |
149 | constructor(
150 | headline: string,
151 | fn: TestFunction,
152 | parent: DescribeNode | RootNode,
153 | ) {
154 | assertNotEmpty(headline);
155 | this.parent = parent;
156 | this.headline = headline;
157 | this.fn = fn;
158 | }
159 |
160 | skip() {
161 | this.skipped = true;
162 | }
163 |
164 | focus() {
165 | this.focused = true;
166 | this.parent.updateFocusedChildren();
167 | }
168 |
169 | start() {
170 | this.startTime = Date.now();
171 | }
172 |
173 | fail(error?: unknown) {
174 | this.result = "FAIL";
175 | this.error = error;
176 | this.parent.fail();
177 | }
178 |
179 | finish() {
180 | this.timeTaken = Date.now() - this.startTime;
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/src/nodes/nodes_test.ts:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | node.skip()
4 | skips all of its children
5 |
6 | node.focus()
7 | calls updateFocusedChildren on the parent
8 |
9 | node.updateFocusedChildren()
10 | skips non-focused children
11 |
12 | node.fail()
13 | sets the result to FAIL
14 | on an ItNode sets the error to the given value
15 | calls fail on the parent
16 |
17 | node.start()
18 | sets the start time
19 |
20 | node.finish()
21 | sets the time taken
22 |
23 | */
24 |
25 | import { expect } from "expect";
26 | import { mock } from "expect-legacy";
27 | import { type DescribeNode, RootNode } from "./nodes.ts";
28 | import { addDescribeNode, addItNode } from "../test_util.ts";
29 |
30 | Deno.test("node.skip() skips all of its children", () => {
31 | const root = new RootNode();
32 | const unskippedNode = addItNode(root);
33 | const describeNode = addDescribeNode(root);
34 |
35 | const childIt = addItNode(describeNode);
36 | const childDescribe = addDescribeNode(describeNode);
37 |
38 | const nestedChildIt = addItNode(childDescribe);
39 |
40 | describeNode.skip();
41 |
42 | expect(unskippedNode.skipped).toBe(false);
43 | expect(childIt.skipped).toBe(true);
44 | expect(childDescribe.skipped).toBe(true);
45 | expect(nestedChildIt.skipped).toBe(true);
46 | });
47 |
48 | Deno.test("node.focus() calls updateFocusedChildren on the parent", () => {
49 | const root = new RootNode();
50 | root.updateFocusedChildren = mock.fn();
51 | const node = addItNode(root);
52 | node.focus();
53 | expect(root.updateFocusedChildren).toHaveBeenCalled();
54 | });
55 |
56 | Deno.test("node.updateFocusedChildren() skips non-focused children", () => {
57 | function updateFocusedChildrenTest(node: DescribeNode | RootNode) {
58 | const child = addItNode(node);
59 | const notFocusedChild = addItNode(node);
60 | child.focused = true;
61 | node.updateFocusedChildren();
62 | expect(notFocusedChild.skipped).toBe(true);
63 | }
64 |
65 | updateFocusedChildrenTest(new RootNode());
66 | updateFocusedChildrenTest(addDescribeNode(new RootNode()));
67 | });
68 |
69 | Deno.test("node.fail() sets the result to FAIL", () => {
70 | const root = new RootNode();
71 | root.fail();
72 | expect(root.result).toBe("FAIL");
73 |
74 | const describeNode = addDescribeNode(root);
75 | describeNode.fail();
76 | expect(describeNode.result).toBe("FAIL");
77 | });
78 |
79 | Deno.test("node.fail() on an ItNode sets the error to the given value", () => {
80 | const itNode = addItNode(new RootNode());
81 | const value = new Error();
82 | itNode.fail(value);
83 | expect(itNode.error).toBe(value);
84 | });
85 |
86 | Deno.test("node.fail() calls .fail() on the parent", () => {
87 | const parent = new RootNode();
88 | const itNode = addItNode(parent);
89 | const describeNode = addDescribeNode(parent);
90 |
91 | parent.fail = mock.fn();
92 | itNode.fail();
93 | describeNode.fail();
94 |
95 | expect(parent.fail).toHaveBeenCalledTimes(2);
96 | });
97 |
98 | Deno.test("node.start() on the root throws if there are no cases", () => {
99 | const rootNode = new RootNode();
100 | expect(rootNode.start).toThrow();
101 |
102 | addItNode(rootNode);
103 | expect(() => {
104 | rootNode.start();
105 | }).not.toThrow();
106 | });
107 |
108 | Deno.test("node.start() sets the start time", () => {
109 | const rootNode = new RootNode();
110 | const itNode = addItNode(rootNode);
111 |
112 | rootNode.start();
113 | expect(rootNode.startTime).not.toBe(0);
114 |
115 | itNode.start();
116 | expect(itNode.startTime).not.toBe(0);
117 | });
118 |
119 | Deno.test("node.finish() sets the time taken", () => {
120 | const rootNode = new RootNode();
121 | const itNode = addItNode(rootNode);
122 |
123 | rootNode.finish();
124 | expect(rootNode.timeTaken).not.toBe(0);
125 |
126 | itNode.finish();
127 | expect(itNode.timeTaken).not.toBe(0);
128 | });
129 |
--------------------------------------------------------------------------------
/src/nodes/tree.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertHasCase,
3 | DescribeNode,
4 | Hook,
5 | ItNode,
6 | RootNode,
7 | type TestFunction,
8 | } from "./nodes.ts";
9 |
10 | export class Tree {
11 | root: RootNode = new RootNode();
12 | private currentNode: RootNode | DescribeNode;
13 |
14 | constructor() {
15 | this.currentNode = this.root;
16 | }
17 |
18 | addDescribeNode(headline: string, fn: () => void) {
19 | const parent = this.currentNode;
20 | const node = new DescribeNode(headline, parent);
21 | parent.children.push(node);
22 | this.currentNode = node;
23 | fn();
24 | assertHasCase(node, "describe()");
25 | this.currentNode = parent;
26 | return node;
27 | }
28 |
29 | describe(headline: string, fn: () => void) {
30 | const node = this.addDescribeNode(headline, fn);
31 | if (node.parent.hasFocused) {
32 | node.skip();
33 | }
34 | }
35 |
36 | describeSkip(headline: string, fn: () => void) {
37 | const node = this.addDescribeNode(headline, fn);
38 | node.skip();
39 | }
40 |
41 | describeOnly(headline: string, fn: () => void) {
42 | const node = this.addDescribeNode(headline, fn);
43 | node.focus();
44 | }
45 |
46 | addItNode(headline: string, fn: TestFunction) {
47 | const parent = this.currentNode;
48 | const node = new ItNode(headline, fn, parent);
49 | parent.children.push(node);
50 | return node;
51 | }
52 |
53 | it(headline: string, fn: TestFunction) {
54 | const node = this.addItNode(headline, fn);
55 | if (node.parent.hasFocused) {
56 | node.skip();
57 | }
58 | }
59 |
60 | itSkip(headline: string, fn: TestFunction) {
61 | const node = this.addItNode(headline, fn);
62 | node.skip();
63 | }
64 |
65 | itOnly(headline: string, fn: TestFunction) {
66 | const node = this.addItNode(headline, fn);
67 | node.focus();
68 | }
69 |
70 | beforeAll(fn: TestFunction) {
71 | this.currentNode.beforeAll.push(new Hook("beforeAll", fn));
72 | }
73 |
74 | beforeEach(fn: TestFunction) {
75 | this.currentNode.beforeEach.push(new Hook("beforeEach", fn));
76 | }
77 |
78 | afterEach(fn: TestFunction) {
79 | this.currentNode.afterEach.unshift(new Hook("afterEach", fn));
80 | }
81 |
82 | afterAll(fn: TestFunction) {
83 | this.currentNode.afterAll.unshift(new Hook("afterAll", fn));
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/nodes/tree_test.ts:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | addDescribeNode
4 | 's function is called on construction
5 | throws when there are no cases
6 |
7 | addItNode
8 | 's function is not called on construction
9 |
10 | addDescribeNode or addItNode
11 | adds to the current parent
12 |
13 | describe() or it()
14 | skips itself if it has a focused sibling
15 | throws when the headline is empty
16 |
17 | calling a hook creator adds a hook to the current parent
18 |
19 | */
20 |
21 | import { expect } from "expect";
22 | import { mock } from "expect-legacy";
23 | import { Tree } from "./tree.ts";
24 | import type { ItNode } from "./nodes.ts";
25 |
26 | function noop() {}
27 |
28 | // describe nodes should have at least one case
29 | function noopDescribe(tree: Tree) {
30 | return () => {
31 | tree.it("_", noop);
32 | };
33 | }
34 |
35 | Deno.test("addDescribeNode's function is called on construction", () => {
36 | const tree = new Tree();
37 | const fn = mock.fn(noopDescribe(tree));
38 | tree.addDescribeNode("_", fn);
39 | expect(fn).toHaveBeenCalled();
40 | });
41 |
42 | Deno.test("addDescribeNode throws when there are no running cases", () => {
43 | const tree = new Tree();
44 |
45 | expect(() => {
46 | tree.addDescribeNode("_", noop);
47 | }).toThrow();
48 |
49 | expect(() => {
50 | tree.addDescribeNode("_", () => {
51 | tree.addDescribeNode("_", noop);
52 | });
53 | }).toThrow();
54 |
55 | expect(() => {
56 | tree.addDescribeNode("_", () => {
57 | tree.itSkip("_", noop);
58 | });
59 | }).toThrow();
60 |
61 | expect(() => {
62 | tree.addDescribeNode("_", noopDescribe(tree));
63 | }).not.toThrow();
64 |
65 | expect(() => {
66 | tree.addDescribeNode("_", () => {
67 | tree.addDescribeNode("_", noopDescribe(tree));
68 | });
69 | }).not.toThrow();
70 |
71 | expect(() => {
72 | tree.describeSkip("_", () => {
73 | tree.it("_", noop);
74 | });
75 | }).not.toThrow();
76 |
77 | expect(() => {
78 | tree.describe("_", () => {
79 | tree.describeSkip("_", noopDescribe(tree));
80 | tree.it("_", noop);
81 | });
82 | }).not.toThrow();
83 | });
84 |
85 | Deno.test("addItNode's function is not called on construction", () => {
86 | const tree = new Tree();
87 | const fn = mock.fn();
88 | tree.addItNode("_", fn);
89 | expect(fn).not.toHaveBeenCalled();
90 | });
91 |
92 | Deno.test("addDescribeNode or addItNode adds to the current parent's children", () => {
93 | const tree = new Tree();
94 | let describeNode, itNode;
95 |
96 | const parentNode = tree.addDescribeNode("_", () => {
97 | describeNode = tree.addDescribeNode("_", noopDescribe(tree));
98 | itNode = tree.addItNode("_", noop);
99 | });
100 |
101 | expect(parentNode.children).toContain(describeNode);
102 | expect(parentNode.children).toContain(itNode);
103 | });
104 |
105 | Deno.test("describe() or it() throws when the headline is empty", () => {
106 | const tree = new Tree();
107 |
108 | expect(() => {
109 | tree.describe("", noopDescribe(tree));
110 | }).toThrow();
111 |
112 | expect(() => {
113 | tree.describe(" ", noopDescribe(tree));
114 | }).toThrow();
115 |
116 | expect(() => {
117 | tree.it("", noop);
118 | }).toThrow();
119 |
120 | expect(() => {
121 | tree.it(" ", noop);
122 | }).toThrow();
123 |
124 | expect(() => {
125 | tree.describe(" _ ", noopDescribe(tree));
126 | }).not.toThrow();
127 |
128 | expect(() => {
129 | tree.it(" _ ", noop);
130 | }).not.toThrow();
131 | });
132 |
133 | Deno.test("describe() or it() skips itself if it has a focused sibling", () => {
134 | const tree = new Tree();
135 | let focused: ItNode;
136 | const parentNode = tree.addDescribeNode("_", () => {
137 | focused = tree.addItNode("_", noop);
138 | focused.focus();
139 | tree.describe("_", noopDescribe(tree));
140 | tree.it("_", noop);
141 | });
142 |
143 | parentNode.children.forEach((child) => {
144 | if (child !== focused) {
145 | expect(child.skipped).toBe(true);
146 | }
147 | });
148 | });
149 |
150 | Deno.test("calling a hook creator adds a hook to the current parent", () => {
151 | const tree = new Tree();
152 | const parentNode = tree.addDescribeNode("_", () => {
153 | tree.beforeAll(noop);
154 | tree.beforeEach(noop);
155 | tree.afterEach(noop);
156 | tree.afterAll(noop);
157 |
158 | noopDescribe(tree)();
159 | });
160 | expect(parentNode.beforeAll.length).toBe(1);
161 | expect(parentNode.beforeEach.length).toBe(1);
162 | expect(parentNode.afterEach.length).toBe(1);
163 | expect(parentNode.afterAll.length).toBe(1);
164 | });
165 |
--------------------------------------------------------------------------------
/src/nodes/utilities.ts:
--------------------------------------------------------------------------------
1 | import { DescribeNode, ItNode, type RootNode } from "./nodes.ts";
2 |
3 | type FindChildResult = ItNode | DescribeNode | undefined;
4 |
5 | function findChildWithCase(
6 | children: (ItNode | DescribeNode)[],
7 | recursiveSearch: (node: DescribeNode | RootNode) => FindChildResult,
8 | ): FindChildResult {
9 | for (const child of children) {
10 | if (
11 | child.skipped === false &&
12 | (child instanceof ItNode || recursiveSearch(child as DescribeNode))
13 | ) {
14 | return child;
15 | }
16 | }
17 | }
18 |
19 | // used by the runner to find the child that runs the beforeAll / afterAll hooks
20 |
21 | export function findChildWithFirstCase(
22 | node: DescribeNode | RootNode,
23 | ): FindChildResult {
24 | return findChildWithCase(node.children, findChildWithFirstCase);
25 | }
26 |
27 | export function findChildWithLastCase(
28 | node: DescribeNode | RootNode,
29 | ): FindChildResult {
30 | return findChildWithCase([...node.children].reverse(), findChildWithLastCase);
31 | }
32 |
33 | export function getAncestry(node: DescribeNode | ItNode): DescribeNode[] {
34 | const ancestors: DescribeNode[] = [];
35 | let lastAncestor = node.parent;
36 | while (lastAncestor instanceof DescribeNode) {
37 | ancestors.push(lastAncestor);
38 | lastAncestor = lastAncestor.parent;
39 | }
40 | return ancestors.reverse();
41 | }
42 |
43 | export function getAllCases(node: RootNode | DescribeNode) {
44 | let nodes: ItNode[] = [];
45 | node.children.forEach((child) => {
46 | if (child instanceof ItNode) {
47 | nodes.push(child);
48 | } else {
49 | nodes = [...nodes, ...getAllCases(child)];
50 | }
51 | });
52 | return nodes;
53 | }
54 |
--------------------------------------------------------------------------------
/src/nodes/utilities_test.ts:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | getAncestry
4 | returns a node's describe ancestors starting from the highest
5 |
6 | findChildWithFirstCase
7 | finds the child that will run the first case
8 |
9 | findChildWithLastCase
10 | finds the child that will run the last case
11 |
12 | getAllCases
13 | gets every ItNode descendant in order of execution
14 |
15 | */
16 |
17 | import { expect } from "expect";
18 | import { RootNode } from "./nodes.ts";
19 | import { Tree } from "./tree.ts";
20 | import { addDescribeNode, addItNode } from "../test_util.ts";
21 | import {
22 | findChildWithFirstCase,
23 | findChildWithLastCase,
24 | getAllCases,
25 | getAncestry,
26 | } from "./utilities.ts";
27 |
28 | function noop() {}
29 |
30 | Deno.test("getAncestry returns a node's describe ancestors starting from the highest", () => {
31 | const firstNode = addDescribeNode(new RootNode());
32 | const secondNode = addDescribeNode(firstNode);
33 | expect(getAncestry(addItNode(secondNode))).toEqual([
34 | firstNode,
35 | secondNode,
36 | ]);
37 | });
38 |
39 | function findChildEnv() {
40 | const tree = new Tree();
41 |
42 | tree.describeSkip("_", () => {
43 | tree.it("_", noop);
44 | });
45 |
46 | const first = tree.addDescribeNode("_", () => {
47 | tree.it("_", noop);
48 | });
49 |
50 | const last = tree.addDescribeNode("_", () => {
51 | tree.itSkip("_", noop);
52 | tree.it("_", noop);
53 | });
54 |
55 | return { tree, first, last };
56 | }
57 |
58 | Deno.test("findChildWithFirstCase finds the child that will run the first case", () => {
59 | const { tree, first } = findChildEnv();
60 | expect(findChildWithFirstCase(tree.root)).toBe(first);
61 | });
62 |
63 | Deno.test("findChildWithLastCase finds the child that will run the first case", () => {
64 | const { tree, last } = findChildEnv();
65 | expect(findChildWithLastCase(tree.root)).toBe(last);
66 | });
67 |
68 | Deno.test("getAllCases gets every ItNode descendant in order of execution", () => {
69 | const root = new RootNode();
70 |
71 | const firstIt = addItNode(root);
72 | const firstDescribe = addDescribeNode(root);
73 | const secondIt = addItNode(firstDescribe);
74 | const secondDescribe = addDescribeNode(firstDescribe);
75 | // thirdIt, although nested will run first because
76 | // its parent describe node comes before foruthIt
77 | const thirdIt = addItNode(secondDescribe);
78 | const fourthIt = addItNode(firstDescribe);
79 |
80 | expect(getAllCases(root)).toEqual([firstIt, secondIt, thirdIt, fourthIt]);
81 | });
82 |
--------------------------------------------------------------------------------
/src/reporter.ts:
--------------------------------------------------------------------------------
1 | import * as colors from "colors";
2 | import {
3 | DescribeNode,
4 | getAncestry,
5 | type Hook,
6 | ItNode,
7 | type RootNode,
8 | } from "./nodes/mod.ts";
9 |
10 | export type TestReporter = {
11 | reportStart: (node: RootNode) => void;
12 | reportHookError: (hook: Hook, error: unknown) => void;
13 | getTestCaseName: (node: ItNode) => string;
14 | };
15 |
16 | const log = console.log;
17 | const logError = console.error;
18 |
19 | function indent(depth: number) {
20 | return " ".repeat(depth);
21 | }
22 |
23 | function skipTag(isSkipped: boolean) {
24 | return isSkipped ? colors.yellow(" [SKIP]") : "";
25 | }
26 |
27 | function formatNode(
28 | node: ItNode | DescribeNode | RootNode,
29 | depth = 1,
30 | ): string {
31 | let str = "";
32 | if (node instanceof ItNode) {
33 | str += colors.gray(
34 | `${indent(depth - 1)}• ${node.headline}${skipTag(node.skipped)}\n`,
35 | );
36 | } else if (node instanceof DescribeNode) {
37 | str += `${indent(depth)}${node.headline}${skipTag(node.skipped)}\n`;
38 | node.children.forEach((child) => str += formatNode(child, depth + 1));
39 | } else {
40 | str = "\n";
41 | node.children.forEach((child) => str += formatNode(child, depth));
42 | }
43 | return str;
44 | }
45 |
46 | export class Reporter implements TestReporter {
47 | getTestCaseName(node: ItNode) {
48 | const hierarchy = getAncestry(node)
49 | .map((node: DescribeNode | ItNode) => colors.gray(node.headline));
50 | hierarchy.push(colors.bold(node.headline));
51 | return hierarchy.join(" > ");
52 | }
53 |
54 | reportStart(node: RootNode) {
55 | log(formatNode(node));
56 | }
57 |
58 | reportHookError(hook: Hook, error: unknown) {
59 | log(`\n${colors.red("ERROR")} in ${hook.type} hook:`);
60 | if (hook.type === "internal") {
61 | log(
62 | "This is probably a bug. Please file an issue if you see this message.",
63 | );
64 | }
65 | logError(error);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/runner.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DescribeNode,
3 | findChildWithFirstCase,
4 | findChildWithLastCase,
5 | Hook,
6 | type ItNode,
7 | RootNode,
8 | type TestFunction,
9 | } from "./nodes/mod.ts";
10 | import { Reporter, type TestReporter } from "./reporter.ts";
11 |
12 | export class Runner {
13 | reporter: TestReporter = new Reporter();
14 |
15 | async runNode(
16 | node: RootNode | DescribeNode | ItNode,
17 | beforeHooks: Hook[] = [],
18 | beforeEachHooks: Hook[] = [],
19 | afterEachHooks: Hook[] = [],
20 | afterHooks: Hook[] = [],
21 | ) {
22 | if (node instanceof RootNode) {
23 | await this.runRoot(node);
24 | } else if (node instanceof DescribeNode) {
25 | await this.runDescribe(
26 | node,
27 | beforeHooks,
28 | beforeEachHooks,
29 | afterEachHooks,
30 | afterHooks,
31 | );
32 | } else {
33 | await this.runIt(
34 | node,
35 | beforeHooks,
36 | beforeEachHooks,
37 | afterEachHooks,
38 | afterHooks,
39 | );
40 | }
41 | }
42 |
43 | async runHook(hook: Hook) {
44 | try {
45 | await hook.fn();
46 | } catch (error) {
47 | this.reporter.reportHookError(hook, error);
48 | }
49 | }
50 |
51 | async runRoot(node: RootNode) {
52 | node.start();
53 | this.reporter.reportStart(node);
54 |
55 | const childWithFirstCase = findChildWithFirstCase(node);
56 | const childWithLastCase = findChildWithLastCase(node);
57 |
58 | for (const child of node.children) {
59 | let childBeforeHooks: Hook[] = [];
60 | let childAfterHooks: Hook[] = [];
61 |
62 | if (child === childWithFirstCase) {
63 | childBeforeHooks = [...node.beforeAll];
64 | }
65 |
66 | if (child === childWithLastCase) {
67 | childAfterHooks = [...childAfterHooks, ...node.afterAll];
68 | childAfterHooks.push(
69 | new Hook("internal", () => {
70 | node.finish();
71 | }),
72 | );
73 | }
74 |
75 | await this.runNode(
76 | child,
77 | childBeforeHooks,
78 | [...node.beforeEach],
79 | [...node.afterEach],
80 | childAfterHooks,
81 | );
82 | }
83 | }
84 |
85 | async runDescribe(
86 | node: DescribeNode,
87 | beforeHooks: Hook[],
88 | beforeEachHooks: Hook[],
89 | afterEachHooks: Hook[],
90 | afterHooks: Hook[],
91 | ) {
92 | const childWithFirstCase = findChildWithFirstCase(node);
93 | const childWithLastCase = findChildWithLastCase(node);
94 |
95 | for (const child of node.children) {
96 | let childBeforeHooks: Hook[] = [];
97 | let childAfterHooks: Hook[] = [];
98 |
99 | if (child === childWithFirstCase) {
100 | childBeforeHooks = [...beforeHooks, ...node.beforeAll];
101 | }
102 |
103 | if (child === childWithLastCase) {
104 | childAfterHooks = [...node.afterAll, ...afterHooks];
105 | }
106 |
107 | await this.runNode(
108 | child,
109 | childBeforeHooks,
110 | [...beforeEachHooks, ...node.beforeEach],
111 | [...node.afterEach, ...afterEachHooks],
112 | childAfterHooks,
113 | );
114 | }
115 | }
116 |
117 | async runIt(
118 | node: ItNode,
119 | beforeHooks: Hook[],
120 | beforeEachHooks: Hook[],
121 | afterEachHooks: Hook[],
122 | afterHooks: Hook[],
123 | ) {
124 | // Deno.test() *registers* tests and runs them separately. Hooks
125 | // have to be passsed down and ran in one function with the test
126 |
127 | const wrappedFn = async () => {
128 | for (const hook of beforeHooks) {
129 | await this.runHook(hook);
130 | }
131 |
132 | for (const hook of beforeEachHooks) {
133 | await this.runHook(hook);
134 | }
135 |
136 | node.start();
137 |
138 | let didThrow = false;
139 |
140 | try {
141 | await node.fn();
142 | } catch (error) {
143 | node.fail(error);
144 | didThrow = true;
145 | }
146 |
147 | node.finish();
148 |
149 | for (const hook of afterEachHooks) {
150 | await this.runHook(hook);
151 | }
152 |
153 | for (const hook of afterHooks) {
154 | await this.runHook(hook);
155 | }
156 |
157 | if (didThrow) {
158 | throw node.error;
159 | }
160 | };
161 |
162 | await this.test(node, wrappedFn);
163 | }
164 |
165 | test(node: ItNode, fn: TestFunction) {
166 | Deno.test({
167 | name: this.reporter.getTestCaseName(node),
168 | fn,
169 | ignore: node.skipped,
170 | });
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/runner_test.ts:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Runner.runRoot
4 | calls .start() on the node
5 |
6 | Runner.runIt
7 | 's function calls the node's function and hooks
8 | 's function catches hook errors
9 | 's function still throws when the test function fails
10 |
11 | Runner.runNode
12 | runs hooks in the correct order
13 |
14 | */
15 |
16 | import { expect } from "expect";
17 | import { mock } from "expect-legacy";
18 | import { Runner } from "./runner.ts";
19 | import {
20 | Hook,
21 | ItNode,
22 | RootNode,
23 | type TestFunction,
24 | Tree,
25 | } from "./nodes/mod.ts";
26 | import { SilentReporter, silentTest } from "./test_util.ts";
27 |
28 | function makeTestRunner() {
29 | const runner = new Runner();
30 | runner.test = silentTest;
31 | runner.reporter = new SilentReporter();
32 | return runner;
33 | }
34 |
35 | Deno.test("Runner.runRoot calls .start() on the node", () => {
36 | const root = new RootNode();
37 | root.start = mock.fn();
38 | makeTestRunner().runRoot(root);
39 | expect(root.start).toHaveBeenCalled();
40 | });
41 |
42 | function makeItNode(fn: TestFunction) {
43 | return new ItNode("_", fn, new RootNode());
44 | }
45 |
46 | Deno.test("Runner.runIt's function calls the node's function and hooks", async () => {
47 | const order: string[] = [];
48 |
49 | const it = makeItNode(() => {
50 | order.push("3");
51 | });
52 |
53 | const runner = makeTestRunner();
54 |
55 | await runner.runIt(
56 | it,
57 | [
58 | new Hook("beforeAll", () => {
59 | order.push("1");
60 | }),
61 | ],
62 | [
63 | new Hook("beforeEach", () => {
64 | order.push("2");
65 | }),
66 | ],
67 | [
68 | new Hook("afterEach", () => {
69 | order.push("4");
70 | }),
71 | ],
72 | [
73 | new Hook("afterAll", () => {
74 | order.push("5");
75 | }),
76 | ],
77 | );
78 |
79 | expect(order).toEqual(["1", "2", "3", "4", "5"]);
80 | });
81 |
82 | Deno.test("Runner.runIt's function catches hook errors", () => {
83 | const hook = new Hook("internal", () => {
84 | throw new Error("case error");
85 | });
86 | return makeTestRunner().runIt(makeItNode(() => {}), [], [], [], [hook]);
87 | });
88 |
89 | Deno.test("Runner.runIt's function still throws when the case fails", async () => {
90 | const toThrow = new Error("case error");
91 | const it = makeItNode(() => {
92 | throw toThrow;
93 | });
94 |
95 | try {
96 | await makeTestRunner().runIt(it, [], [], [], []);
97 | throw new Error("above should throw");
98 | } catch (err) {
99 | expect(err).toBe(toThrow);
100 | }
101 | });
102 |
103 | Deno.test("Runner.runNode runs hooks in the correct order", async () => {
104 | const order: string[] = [];
105 | const tree = new Tree();
106 |
107 | tree.beforeAll(() => {
108 | order.push("1 - beforeAll");
109 | });
110 | tree.beforeEach(() => {
111 | order.push("1 - beforeEach");
112 | });
113 | tree.afterEach(() => {
114 | order.push("1 - afterEach");
115 | });
116 | tree.afterAll(() => {
117 | order.push("1 - afterAll");
118 | });
119 | tree.it("__", () => {
120 | order.push("1 - it");
121 | });
122 |
123 | tree.describe("_", () => {
124 | tree.beforeAll(() => {
125 | order.push("2 - beforeAll");
126 | });
127 | tree.beforeEach(() => {
128 | order.push("2 - beforeEach");
129 | });
130 | tree.afterEach(() => {
131 | order.push("2 - afterEach");
132 | });
133 | tree.afterAll(() => {
134 | order.push("2 - afterAll");
135 | });
136 | tree.it("__", () => {
137 | order.push("2 - it");
138 | });
139 | });
140 |
141 | await makeTestRunner().runNode(tree.root);
142 |
143 | expect(order).toEqual([
144 | "1 - beforeAll",
145 | "1 - beforeEach",
146 | "1 - it",
147 | "1 - afterEach",
148 | "2 - beforeAll",
149 | "1 - beforeEach",
150 | "2 - beforeEach",
151 | "2 - it",
152 | "2 - afterEach",
153 | "1 - afterEach",
154 | "2 - afterAll",
155 | "1 - afterAll",
156 | ]);
157 | });
158 |
--------------------------------------------------------------------------------
/src/test_util.ts:
--------------------------------------------------------------------------------
1 | import type { TestReporter } from "./reporter.ts";
2 | import {
3 | DescribeNode,
4 | ItNode,
5 | type RootNode,
6 | type TestFunction,
7 | } from "./nodes/mod.ts";
8 |
9 | export class SilentReporter implements TestReporter {
10 | getTestCaseName = () => "";
11 | reportStart() {}
12 | reportHookError() {}
13 | }
14 |
15 | export async function silentTest(node: ItNode, wrappedFn: TestFunction) {
16 | if (!node.skipped) {
17 | await wrappedFn();
18 | }
19 | }
20 |
21 | export function addItNode(parent: DescribeNode | RootNode) {
22 | const node = new ItNode("_", () => {}, parent);
23 | parent.children.push(node);
24 | return node;
25 | }
26 |
27 | export function addDescribeNode(parent: DescribeNode | RootNode) {
28 | const node = new DescribeNode("_", parent);
29 | parent.children.push(node);
30 | return node;
31 | }
32 |
--------------------------------------------------------------------------------