├── .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 | ci 10 | CodeFactor 11 | 12 | deno.land/x 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 | --------------------------------------------------------------------------------