├── .github └── CODEOWNERS ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── helpers │ └── spy.ts ├── index.ts ├── mixins │ ├── conditional-spec.ts │ ├── conditional.ts │ ├── fixturable-spec.ts │ ├── fixturable.ts │ ├── isolatable-spec.ts │ └── isolatable.ts ├── reporter-spec.ts ├── reporter.ts ├── reporters │ ├── console-reporter.ts │ └── test-reporter.ts ├── spec-spec.ts ├── spec.ts ├── suite-spec.ts ├── suite.ts ├── test-spec.ts ├── test.ts ├── topic-spec.ts ├── topic.ts ├── util-spec.ts └── util.ts ├── test.html ├── tsconfig.json └── tslint.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code owners for ristretto 2 | * @cdata -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | ristretto.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, The Polymer Authors. All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *🚨 **PROJECT STATUS: EXPERIMENTAL** 🚨This product is in the Experimentation phase. Someone on the team thinks it’s an idea worth exploring, but it may not go any further than this. Use at your own risk.* 2 | 3 | # Ristretto 4 | 5 | Ristretto is an extensible test runner. 6 | 7 | There are several established test runners in the JavaScript ecosystem 8 | ([Mocha](https://mochajs.org/), [Jasmine](https://jasmine.github.io/) and 9 | [Jest](https://facebook.github.io/jest/) to name a few of the more prominent 10 | ones). Ristretto was created to address a feature sweet spot left unaddressed 11 | by incumbent projects. 12 | 13 | Ristretto has the following qualities: 14 | 15 | - Simple, concise, class-based factorization 16 | - Consumable as modules with browser-compatible path specifiers OOB 17 | - Batteries included for the most popular testing syntaxes (BDD/TDD) 18 | - Designed for extensibility (mixins, specialized reporters and a 19 | spec-as-data-structure philosophy) 20 | - Ships with mixins that enhance specs with powerful features (e.g., fixtures 21 | and test isolation) 22 | - Authored in TypeScript 23 | - No build tooling required to use 24 | - "JavaScript is the native language, The Web is the native land" 25 | 26 | Ristretto is intended to be a single-responsibility detail of a broader, more 27 | comprehensive testing regime. In this regard, it is most similar to Mocha in its 28 | breadth of capabilities. 29 | 30 | Currently, Ristretto only supports direct usage in a web browser with ESM 31 | support. Other browsers and platforms (such as Node.js) should work fine with 32 | sufficient code transformations applied to this module. 33 | 34 | ## Installing 35 | 36 | ```sh 37 | npm install @polymer/ristretto 38 | ``` 39 | 40 | ## Writing tests 41 | 42 | All tests start with crafting a spec and exporting that spec for consumption 43 | elsewhere. Here is an example of a spec written with Ristretto: 44 | 45 | ```javascript 46 | // Import the Spec class from Ristretto: 47 | import { Spec } from '../../@polymer/ristretto/lib/spec.js'; 48 | 49 | // Create a Spec instance that represents our spec: 50 | const spec = new Spec(); 51 | 52 | // These "describe" and "it" methods work as you would expect when writing 53 | // a test in Mocha, Jasmine or Jest: 54 | const { describe, it } = spec; 55 | 56 | // Author your spec as you would in any other test runner: 57 | describe('My spec', () => { 58 | it('never fails', () => {}); 59 | }); 60 | 61 | // Export the spec as a module: 62 | export { spec as mySpec }; 63 | ``` 64 | 65 | ## Running tests 66 | 67 | In order to run the tests in your spec, you will need to craft a test suite. 68 | A test suite is a collection of specs, and an optional specialized test 69 | reporter. Here is a basic example of crafting a test suite and running tests: 70 | 71 | ```javascript 72 | // Import the Suite class from Ristretto: 73 | import { Suite } from '../../@polymer/ristretto/lib/suite.js'; 74 | 75 | // Import the spec we crafted above: 76 | import { mySpec } from './my-spec.js'; 77 | 78 | // Craft a suite that includes our spec: 79 | const suite = new Suite([ mySpec /*, other, specs, go, here */ ]); 80 | 81 | // Run the test suite at your leisure. It will return a promise that resolves 82 | // when all tests have been run: 83 | suite.run(); 84 | ``` 85 | 86 | ## Reporting results 87 | 88 | By default, Ristretto will use a basic `console`-based reporter called 89 | `ConsoleReporter`. However, it is very easy to craft a custom reporter to 90 | suite your needs. Let's write a custom reporter that counts tests and 91 | reports how many failed at the end of the test suite run: 92 | 93 | ```javascript 94 | // Import the base Reporter class from Ristretto: 95 | import { Reporter } from '../../@polymer/ristretto/lib/reporter.js'; 96 | 97 | // We export a class that extends the base Reporter class. The reporter has 98 | // a series of test suite life-cycle callbacks that can be optionally 99 | // implemented by child classes to perform specialized reporting. We implement 100 | // a few of them here: 101 | export class CountingReporter extends Reporter { 102 | 103 | onSuiteStart(suite) { 104 | // Initialize some state when the test suite begins: 105 | this.totalTestCount = 0; 106 | this.failedTestCount = 0; 107 | } 108 | 109 | onTestStart(test, suite) { 110 | // When a test starts, increment the total test counter: 111 | this.totalTestCount++; 112 | } 113 | 114 | onTestEnd(result, test, suite) { 115 | // If there is an error in the result when a test ends, increment the 116 | // failed test counter: 117 | if (result.error) { 118 | this.failedTestCount++; 119 | } 120 | } 121 | 122 | onSuiteEnd(suite) { 123 | // When the test suite run has completed, announce the total number of 124 | // tests, and if there were any failed tests announce that number as well: 125 | console.log(`Total tests: ${this.totalTestCount}`); 126 | if (this.failedTestCount > 0) { 127 | console.error(`Failed tests: ${this.failedTestCount}`); 128 | } else { 129 | console.log('All tests pass!'); 130 | } 131 | } 132 | } 133 | ``` 134 | 135 | Now that we have a custom reporter, let's use it in a test suite: 136 | 137 | ```javascript 138 | // Import the Suite class from Ristretto: 139 | import { Suite } from '../../@polymer/ristretto/lib/suite.js'; 140 | 141 | // Import the custom reporter we created: 142 | import { CountingReporter } from './counting-reporter.js'; 143 | 144 | // Craft a suite that includes any specs and our custom reporter: 145 | const suite = new Suite([ /* specs, go, here */ ], new CountingReporter()); 146 | 147 | // Run the test suite at your leisure. The suite will invoke reporter callbacks 148 | // is testing progresses to completion: 149 | suite.run(); 150 | ``` 151 | 152 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@polymer/ristretto", 3 | "version": "0.3.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/chai": { 8 | "version": "4.1.2", 9 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.2.tgz", 10 | "integrity": "sha512-D8uQwKYUw2KESkorZ27ykzXgvkDJYXVEihGklgfp5I4HUP8D6IxtcdLTMB1emjQiWzV7WZ5ihm1cxIzVwjoleQ==", 11 | "dev": true 12 | }, 13 | "assertion-error": { 14 | "version": "1.1.0", 15 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 16 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 17 | "dev": true 18 | }, 19 | "chai": { 20 | "version": "4.1.2", 21 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", 22 | "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", 23 | "dev": true, 24 | "requires": { 25 | "assertion-error": "1.1.0", 26 | "check-error": "1.0.2", 27 | "deep-eql": "3.0.1", 28 | "get-func-name": "2.0.0", 29 | "pathval": "1.1.0", 30 | "type-detect": "4.0.8" 31 | } 32 | }, 33 | "check-error": { 34 | "version": "1.0.2", 35 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 36 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 37 | "dev": true 38 | }, 39 | "deep-eql": { 40 | "version": "3.0.1", 41 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 42 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 43 | "dev": true, 44 | "requires": { 45 | "type-detect": "4.0.8" 46 | } 47 | }, 48 | "get-func-name": { 49 | "version": "2.0.0", 50 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 51 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 52 | "dev": true 53 | }, 54 | "pathval": { 55 | "version": "1.1.0", 56 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", 57 | "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", 58 | "dev": true 59 | }, 60 | "rollup": { 61 | "version": "0.55.5", 62 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.55.5.tgz", 63 | "integrity": "sha512-2hke9NOy332kxvnmMQOgl7DHm94zihNyYJNd8ZLWo4U0EjFvjUkeWa0+ge+70bTg+mY0xJ7NUsf5kIhDtrGrtA==", 64 | "dev": true 65 | }, 66 | "type-detect": { 67 | "version": "4.0.8", 68 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 69 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 70 | "dev": true 71 | }, 72 | "typescript": { 73 | "version": "2.7.1", 74 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.1.tgz", 75 | "integrity": "sha512-bqB1yS6o9TNA9ZC/MJxM0FZzPnZdtHj0xWK/IZ5khzVqdpGul/R/EIiHRgFXlwTD7PSIaYVnGKq1QgMCu2mnqw==", 76 | "dev": true 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@polymer/ristretto", 3 | "version": "0.3.1", 4 | "license": "BSD-3-Clause", 5 | "flat": true, 6 | "types": "lib/index.d.ts", 7 | "main": "ristretto.js", 8 | "module": "lib/index.js", 9 | "devDependencies": { 10 | "@types/chai": "^4.1.2", 11 | "chai": "^4.1.2", 12 | "rollup": "^0.55.5", 13 | "typescript": "^2.6.2" 14 | }, 15 | "scripts": { 16 | "build": "tsc", 17 | "watch": "tsc -w", 18 | "bundle": "rollup -c ./rollup.config.js", 19 | "prepublishOnly": "npm run build && npm run bundle" 20 | }, 21 | "description": "Ristretto runs your tests.", 22 | "files": [ 23 | "lib" 24 | ], 25 | "dependencies": {}, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/PolymerLabs/ristretto.git" 29 | }, 30 | "author": "The Polymer Authors", 31 | "bugs": { 32 | "url": "https://github.com/PolymerLabs/ristretto/issues" 33 | }, 34 | "homepage": "https://github.com/PolymerLabs/ristretto#readme", 35 | "publishConfig": { 36 | "access": "public" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | export default { 16 | input: './lib/index.js', 17 | output: { 18 | format: 'umd', 19 | file: './ristretto.js', 20 | name: 'Ristretto' 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/helpers/spy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | /** 16 | * This helper is used to create spy functions, and replace methods on objects 17 | * in with those spy functions in hygeinic ways. 18 | * 19 | * A spy function records the number of times it has been called, the arguments 20 | * it was called with in call order, and has a method of its own to restore the 21 | * original method it replaced (if any). 22 | */ 23 | // TODO(cdata): consider just ripping this out entirely and using Sinon instead. 24 | export const spy = (context?: any, methodName?: string) => { 25 | const callArgs: any[] = []; 26 | let callCount = 0; 27 | let originalMethod: Function | null = null; 28 | 29 | const spyMethod = (...args: any[]) => { 30 | callArgs.push(args); 31 | callCount++; 32 | if (originalMethod != null) { 33 | return originalMethod.call(context, ...args); 34 | } 35 | }; 36 | 37 | const restore = () => { 38 | if (originalMethod != null) { 39 | context[methodName!] = originalMethod; 40 | } 41 | }; 42 | 43 | if (context != null && methodName != null) { 44 | originalMethod = context[methodName] || null; 45 | context[methodName] = spyMethod; 46 | } 47 | 48 | 49 | Object.defineProperty(spyMethod, 'args', { get() { return callArgs; } }); 50 | Object.defineProperty(spyMethod, 'callCount', { 51 | get() { 52 | return callCount; 53 | } 54 | }); 55 | 56 | (spyMethod as any).restore = restore; 57 | 58 | return spyMethod; 59 | }; 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Spec } from './spec.js'; 16 | import { Conditional, ConditionalSpec } from './mixins/conditional.js'; 17 | import { Fixturable, FixturedSpec } from './mixins/fixturable.js'; 18 | import { Isolatable, IsolatedSpec } from './mixins/isolatable.js'; 19 | import { Constructor } from './util.js'; 20 | 21 | const UberSpec: Constructor = 22 | Fixturable(Isolatable(Conditional(Spec))); 23 | 24 | export { UberSpec as Spec, Spec as BasicSpec, Fixturable, Isolatable, Conditional }; 25 | export { Suite } from './suite.js'; 26 | export { Reporter } from './reporter.js'; 27 | export { ConsoleReporter } from './reporters/console-reporter.js'; 28 | export { timePasses, timeLimit } from './util.js'; 29 | export { spy } from './helpers/spy.js'; -------------------------------------------------------------------------------- /src/mixins/conditional-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Spec } from '../spec.js'; 16 | import { Suite } from '../suite.js'; 17 | import { Conditional } from './conditional.js'; 18 | import { Fixturable } from '../index.js'; 19 | import { describeSpecSpec } from '../spec-spec.js'; 20 | import { TestReporter } from '../reporters/test-reporter.js'; 21 | import '../../../../chai/chai.js'; 22 | 23 | const ConditionalSpec = Conditional(Spec); 24 | const spec = new (Fixturable(Spec))(); 25 | 26 | const { expect } = (self as any).chai; 27 | const { describe, it, before } = spec; 28 | 29 | describe('Conditional', () => { 30 | before((context: any) => ({ 31 | ...context, 32 | spec: new ConditionalSpec() 33 | })); 34 | 35 | describe('with tests', () => { 36 | before((context: any) => { 37 | const {spec} = context; 38 | const {describe, it} = spec; 39 | const suite = new Suite([spec], new TestReporter()); 40 | 41 | describe('conditional spec', () => { 42 | it('runs by default', () => {}); 43 | it('has a skipped test', () => {}, {condition: () => false}); 44 | }); 45 | 46 | return {...context, spec, suite}; 47 | }); 48 | 49 | describe('conditional tests', () => { 50 | it ('marked as skipped', async ({suite}: any) => { 51 | await suite.run(); 52 | const results = (suite.reporter as TestReporter).results; 53 | expect(results).to.have.length.greaterThan(0); 54 | expect(results[0].passed).to.be.equal(true); 55 | expect(results[1].passed).to.be.equal(false); 56 | expect(results[1]).to.have.property('skipped'); 57 | }); 58 | }) 59 | }); 60 | 61 | describeSpecSpec(spec, ConditionalSpec); 62 | }); 63 | 64 | export const conditionalSpec: Spec = spec; 65 | -------------------------------------------------------------------------------- /src/mixins/conditional.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Spec } from '../spec.js'; 16 | import { Topic } from '../topic.js'; 17 | import { Test, TestRunContext, TestConfig, TestResult } from '../test.js'; 18 | import { Constructor } from '../util'; 19 | 20 | export interface ConditionalTestConfig extends TestConfig { 21 | condition?: () => boolean; 22 | } 23 | 24 | export interface ConditionalTestResult extends TestResult { 25 | skipped?: boolean; 26 | } 27 | 28 | export interface ConditionalTestRunContext extends TestRunContext { 29 | skipped?: boolean; 30 | } 31 | 32 | export interface ConditionalTest { 33 | readonly condition: () => boolean; 34 | } 35 | 36 | /** 37 | * A conditional test is one that runs only if supplied conditions for the test 38 | * are satisfied. 39 | * In particular, ConditionalTest adds a `condition` configuration property as a 40 | * function that returns `true` or `false`. 41 | * If `condition` returns `true`, the test will run, and if it returns `false`, 42 | * the test will not run. 43 | * 44 | * Example: 45 | * ```javascript 46 | * describe('a topic with conditions', () => { 47 | * it('a conditional test', () => {}, {condition: () => {...}} 48 | * }); 49 | * ``` 50 | */ 51 | export function ConditionalTest>(TestImplementation: T) { 52 | return class extends TestImplementation { 53 | protected config!: ConditionalTestConfig; 54 | 55 | /** 56 | * True if the test should run. 57 | */ 58 | get condition(): () => boolean { 59 | return this.config.condition || (() => true); 60 | } 61 | 62 | protected async postProcess(context: ConditionalTestRunContext, result: TestResult): Promise { 63 | const superResult = await super.postProcess(context, result); 64 | return { 65 | ...superResult, 66 | skipped: context.skipped, 67 | passed: context.skipped ? false : superResult.passed 68 | }; 69 | } 70 | 71 | protected async windUp(context: TestRunContext): Promise { 72 | const skipped = !this.condition(); 73 | context = await super.windUp(context); 74 | if (skipped) { 75 | return { 76 | ...context, 77 | // provide a void implementation to "skip" the test 78 | implementation: () => {}, 79 | skipped 80 | }; 81 | } else { 82 | return context; 83 | } 84 | } 85 | 86 | } as Constructor; 87 | } 88 | 89 | export interface ConditionalTopic {} 90 | 91 | export function ConditionalTopic>(TopicImplementation: T) { 92 | return class extends TopicImplementation { 93 | protected get TestImplementation() { 94 | return ConditionalTest(super.TestImplementation); 95 | } 96 | } as Constructor 97 | } 98 | 99 | export interface ConditionalSpec {} 100 | 101 | /** 102 | * A conditional spec is an extension to `Spec` that allows 103 | * individual tests to be conditionally run based on a 104 | * `condition` function in the test config. 105 | */ 106 | export function ConditionalSpec>(SpecImplementation: S) { 107 | return class extends SpecImplementation { 108 | protected get TopicImplementation() { 109 | return ConditionalTopic(super.TopicImplementation); 110 | } 111 | } as Constructor 112 | } 113 | 114 | export const Conditional = ConditionalSpec; -------------------------------------------------------------------------------- /src/mixins/fixturable-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Spec } from '../spec.js'; 16 | import { Suite } from '../suite.js'; 17 | import { Fixturable } from './fixturable.js'; 18 | import { describeSpecSpec } from '../spec-spec.js'; 19 | import '../../../../chai/chai.js'; 20 | 21 | const FixturableSpec = Fixturable(Spec); 22 | const spec = new FixturableSpec(); 23 | 24 | const { expect } = (self as any).chai; 25 | const { describe, it, before } = spec; 26 | 27 | describe('Fixturable', () => { 28 | before((context: any) => ({ 29 | ...context, 30 | spec: new FixturableSpec() 31 | })); 32 | 33 | describe('with tests and fixtures', () => { 34 | describe('invocation order', () => { 35 | before((context: any) => { 36 | const { spec } = context; 37 | const { describe, it, before, after, setup, teardown } = spec; 38 | const suite = new Suite([spec]); 39 | const callOrder: string[] = []; 40 | const record = (event: string) => () => callOrder.push(event); 41 | 42 | suite.reporter.disabled = true; 43 | 44 | describe('fixturable spec', () => { 45 | before(record('before1')); 46 | after(record('after1')); 47 | setup(record('before2')); 48 | teardown(record('after2')); 49 | 50 | it('has a test', record('test1')); 51 | 52 | describe('nested topic', () => { 53 | after(record('after4')); 54 | it('has a nested test', record('test2')); 55 | before(record('before4')); 56 | }); 57 | 58 | before(record('before3')); 59 | after(record('after3')); 60 | }); 61 | 62 | return { ...context, spec, suite, callOrder }; 63 | }); 64 | 65 | it('invokes before, after, test hooks in deterministic order', 66 | async ({ suite, callOrder }: any) => { 67 | await suite.run(); 68 | expect(callOrder).to.be.eql([ 69 | 'before1', 70 | 'before2', 71 | 'before3', 72 | 'test1', 73 | 'after3', 74 | 'after2', 75 | 'after1', 76 | 'before1', 77 | 'before2', 78 | 'before3', 79 | 'before4', 80 | 'test2', 81 | 'after4', 82 | 'after3', 83 | 'after2', 84 | 'after1' 85 | ]); 86 | }); 87 | }); 88 | }); 89 | 90 | describeSpecSpec(spec, FixturableSpec); 91 | }); 92 | 93 | export const fixturableSpec: Spec = spec; 94 | -------------------------------------------------------------------------------- /src/mixins/fixturable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Spec } from '../spec.js'; 16 | import { Test, TestRunContext } from '../test.js'; 17 | import { Topic } from '../topic.js'; 18 | import { Constructor } from '../util.js'; 19 | 20 | /** 21 | * FixturedTest 22 | */ 23 | export interface FixturedTestRunContext extends TestRunContext { 24 | fixtureContext?: any 25 | }; 26 | 27 | export interface FixturedTest { 28 | readonly topic: Topic & FixturedTopic; 29 | }; 30 | 31 | /** 32 | * Decorates a `Test` implementation with the necessary "wind-up" and 33 | * "wind-down" behavior to support fixtured contexts in tests. 34 | */ 35 | export function FixturedTest>(TestImplementation: T) { 36 | return class extends TestImplementation { 37 | readonly topic!: Topic & FixturedTopic; 38 | 39 | /** 40 | * When winding up, a fixtured context is generated from a topic, and 41 | * the test invocation is wrapped so that the context is passed in 42 | * to the test implementation when it is invoked. 43 | */ 44 | protected async windUp(context: TestRunContext): 45 | Promise { 46 | const testContext = await super.windUp(context); 47 | const { implementation } = testContext; 48 | 49 | const { topic } = this; 50 | const fixtureContext = topic != null ? topic.createContext() : {}; 51 | 52 | return { 53 | ...testContext, 54 | fixtureContext, 55 | implementation: (...args: any[]) => implementation(...args, fixtureContext) 56 | }; 57 | } 58 | 59 | /** 60 | * When winding down, a fixtured context is cleaned up by the topic that 61 | * created it. 62 | */ 63 | protected async windDown(context: FixturedTestRunContext) { 64 | const { fixtureContext } = context; 65 | const { topic } = this; 66 | 67 | await super.windDown(context); 68 | 69 | if (topic != null) { 70 | topic.disposeContext(fixtureContext); 71 | } 72 | } 73 | } as Constructor; 74 | }; 75 | 76 | 77 | /** 78 | * FixturedTopic 79 | */ 80 | export interface FixturedTopic { 81 | readonly fixtures: Function[]; 82 | readonly cleanups: Function[]; 83 | 84 | createContext(): object; 85 | disposeContext(context: object): void; 86 | } 87 | 88 | /** 89 | * Decorates a `Topic` with the implementation necessary to support describing 90 | * fixture steps and cleanup steps on a test, and generating a fixtured context 91 | * from those steps. 92 | */ 93 | export function FixturedTopic>(TopicImplementation: T) { 94 | return class extends TopicImplementation { 95 | parentTopic!: (Topic & FixturedTopic) | void; 96 | 97 | readonly fixtures: Function[] = []; 98 | readonly cleanups: Function[] = []; 99 | 100 | /** 101 | * The `Test` implementation used for fixture-capable `Topic`s has to 102 | * include its own specialized fixture-capable implementation. 103 | */ 104 | get TestImplementation() { 105 | return FixturedTest(super.TestImplementation); 106 | } 107 | 108 | /** 109 | * Generates a fixture context by generating its parent `Topic`'s fixture 110 | * context (if there is a parent `Topic`) and passing it through all 111 | * fixture steps configured for itself. 112 | */ 113 | createContext(): object { 114 | const context = this.parentTopic != null 115 | ? this.parentTopic.createContext() 116 | : {}; 117 | 118 | return this.fixtures.reduce( 119 | (context, fixture) => (fixture(context) || context), context); 120 | } 121 | 122 | /** 123 | * Cleans up a fixture context by passing it through all of the configured 124 | * cleanup steps on itself, and then passing that context through the 125 | * parent `Topic`'s cleanup steps (if there is a parent `Topic`). 126 | */ 127 | disposeContext(context: object): void { 128 | for (let i = this.cleanups.length - 1; i > -1; --i) { 129 | this.cleanups[i](context); 130 | } 131 | 132 | if (this.parentTopic != null) { 133 | this.parentTopic.disposeContext(context); 134 | } 135 | } 136 | } as Constructor; 137 | } 138 | 139 | /** 140 | * FixturedSpec 141 | */ 142 | export interface FixturedSpec { 143 | fixture: Function; 144 | before: Function; 145 | setup: Function; 146 | 147 | cleanup: Function; 148 | after: Function; 149 | teardown: Function; 150 | } 151 | 152 | export type FixtureFunction = (context: object) => any; 153 | export type CleanupFunction = (context: object) => void; 154 | 155 | /** 156 | * Decorates a `Spec` implementation with the necessary behavior to support 157 | * fixtures in tests. For the `Spec`, this primarily consists of adding two 158 | * new "tear-off" methods: `fixture` and `cleanup`. 159 | */ 160 | export function FixturedSpec>(SpecImplementation: S) { 161 | return class extends SpecImplementation { 162 | 163 | /** 164 | * The `fixture` method is one of two added "tear-off" methods offered in 165 | * the fixture implementation for `Spec`. Invoking `fixture` causes a 166 | * step to be added that is run before each test in the immediate parent 167 | * topic and all subtopics of the immediate parent topic. 168 | * 169 | * Additionally, a fixture step function receives a context argument which 170 | * can be decorated or substituted and returned by the step. This same 171 | * context object will be passed in to all test implementations for which 172 | * the fixture step is applicable. 173 | * 174 | * An example fixture step looks like this: 175 | * 176 | * ```javascript 177 | * describe('some spec', () => { 178 | * fixture(context => ({ 179 | * ...context, 180 | * message: 'foo' 181 | * })); 182 | * 183 | * it('has context', ({ message }) => { 184 | * console.log(message); // prints 'foo' 185 | * }); 186 | * }); 187 | * ``` 188 | * 189 | * The `fixture` method has two aliases that can be used interchangeably, 190 | * even within the same `Spec` if desired (though this is not recommended): 191 | * 192 | * - BDD style `before` 193 | * - TDD style `setup` 194 | */ 195 | fixture: Function; 196 | before: Function; 197 | setup: Function; 198 | 199 | /** 200 | * The `cleanup` method is one of two added "tear-off" methods offered in 201 | * the fixture implementation for `Spec`. Invoking `cleanup` causes a step 202 | * to be added that is run after each test in the immediate parent topic 203 | * all subtopics of the immediate parent topic. 204 | * 205 | * A cleanup step is intended to unset any critical state that is set by a 206 | * correspondign fixture step, such as cleaning up mocked global state or 207 | * shutting down a server. 208 | * 209 | * The `cleanup` method has two aliases that can be used interchangeably, 210 | * even within the same `Spec` if desired (though this is not recommended): 211 | * 212 | * - BDD style `after` 213 | * - TDD style `teardown` 214 | */ 215 | cleanup: Function; 216 | after: Function; 217 | teardown: Function; 218 | 219 | rootTopic!: (Topic & FixturedTopic) | null; 220 | protected currentTopic!: (Topic & FixturedTopic) | null; 221 | 222 | /** 223 | * The `Topic` implementation used by a fixture-capable spec requires 224 | * the special behavior added by the `FixturedTopic` mixin. 225 | */ 226 | protected get TopicImplementation() { 227 | return FixturedTopic(super.TopicImplementation); 228 | } 229 | 230 | constructor(...args: any[]) { 231 | super(...args); 232 | 233 | this.fixture = this.before = this.setup = 234 | (fixture: FixtureFunction): void => { 235 | if (this.currentTopic != null) { 236 | this.currentTopic.fixtures.push(fixture); 237 | } 238 | }; 239 | 240 | this.cleanup = this.after = this.teardown = 241 | (cleanup: CleanupFunction): void => { 242 | if (this.currentTopic != null) { 243 | this.currentTopic.cleanups.push(cleanup); 244 | } 245 | }; 246 | } 247 | } as Constructor; 248 | } 249 | 250 | /** 251 | * This is an alias for the `FixturedSpec` mixin, added for convenience and 252 | * subjective readability for spec authors. 253 | */ 254 | export const Fixturable = FixturedSpec; 255 | -------------------------------------------------------------------------------- /src/mixins/isolatable-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Spec } from '../spec.js'; 16 | import { Suite } from '../suite.js'; 17 | import { Isolatable } from './isolatable.js'; 18 | import { Fixturable } from './fixturable.js'; 19 | import { describeSpecSpec } from '../spec-spec.js'; 20 | import '../../../../chai/chai.js'; 21 | 22 | const IsolatableSpec = Isolatable(Spec); 23 | const spec = new (Fixturable(Spec))(); 24 | 25 | const { expect } = (self as any).chai; 26 | const { describe, it, before } = spec; 27 | 28 | describe('Isolatable', () => { 29 | before((context: any) => ({ 30 | ...context, 31 | spec: new IsolatableSpec() 32 | })); 33 | 34 | describe('with tests', () => { 35 | before((context: any) => { 36 | const { spec } = context; 37 | const { describe, it } = spec; 38 | const suite = new Suite([spec]); 39 | 40 | describe('isolated spec', () => { 41 | it('has an isolated test', () => {}, { isolated: true }); 42 | }); 43 | 44 | return { ...context, spec, suite }; 45 | }); 46 | 47 | describe('an isolated test', () => { 48 | it('is marked as isolated', ({ spec }: any) => { 49 | const test = spec.rootTopic.tests[0]; 50 | expect(test.isolated).to.be.equal(true); 51 | }); 52 | }); 53 | }); 54 | 55 | // TODO(cdata): It will probably take specialty testing to more thoroughly 56 | // test isolation. The current strategy for isolating tests in the browser 57 | // is not compatible with a spec being run within another spec. 58 | 59 | describeSpecSpec(spec, IsolatableSpec); 60 | }); 61 | 62 | export const isolatableSpec: Spec = spec; 63 | -------------------------------------------------------------------------------- /src/mixins/isolatable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Constructor, cloneableResult } from '../util.js'; 16 | import { Suite } from '../suite.js'; 17 | import { Spec } from '../spec.js'; 18 | import { Topic } from '../topic.js'; 19 | import { Test, TestRunContext, TestConfig, TestResult } from '../test.js'; 20 | import { SuiteAddress } from '../suite.js'; 21 | 22 | /** 23 | * IsolatedTest 24 | */ 25 | export interface IsolatedTestConfig extends TestConfig { 26 | isolated?: boolean; 27 | } 28 | 29 | export interface IsolatedTestResult extends TestResult { 30 | isolated?: boolean; 31 | } 32 | 33 | export interface IsolatedTestRunContext extends TestRunContext { 34 | isolated?: boolean; 35 | } 36 | 37 | export interface IsolatedTest { 38 | readonly isolated: boolean; 39 | } 40 | 41 | enum IsolatedTestMessage { 42 | messagePort = 'RistrettoIsolatedTestMessagePort', 43 | ready = 'RistrettoIsolatedTestReady' 44 | } 45 | 46 | /** 47 | * An isolated test is one that runs in a "clean room" context. Currently, this 48 | * means running in an iframe in a browser. This is useful to help test authors 49 | * control for global state that persists across test runs (for example, custom 50 | * element registration). 51 | * 52 | * For test runs isolated by iframes, the following steps are taken: 53 | * 54 | * 1. On wind-up, if the test is to be isolated, the implementation is replaced 55 | * by one that generates an iframe pointing to the same URL but with specially 56 | * crafted query params that inform the suite to run only one test, and informs 57 | * the test that it is running in an isolated context. 58 | * 2. The test implementation is invoked in the main frame, causing an iframe 59 | * (the isolated context) to be loaded. 60 | * 3. The test in the isolated frame notifies that it is ready to run. 61 | * 4. The test in the main frame posts a `MessagePort` to the test in the 62 | * isolated frame. 63 | * 5. The test in the isolated frame receives the `MessagePort`, and invokes 64 | * the actual test implementation. 65 | * 6. The test in the isolated frame posts the `TestResult` back through the 66 | * `MessagePort` to the main frame. 67 | * 7. The test in the main frame resolves its implementation with the 68 | * `TestResult` received from the isolated frame. 69 | */ 70 | export function IsolatedTest>(TestImplementation: T) { 71 | return class extends TestImplementation { 72 | /** 73 | * True if the test is configured to be isolated. 74 | */ 75 | get isolated(): boolean { 76 | return !!(this.config && this.config.isolated); 77 | } 78 | 79 | protected config!: IsolatedTestConfig; 80 | 81 | /** 82 | * An isolated test run must post its results to the parent frame in order 83 | * to be completed. We override run to do this if we detect that we are 84 | * isolated. 85 | */ 86 | async run(suite: Suite, ...args: any[]) { 87 | const { queryParams } = suite; 88 | let port: MessagePort | undefined; 89 | 90 | if ('ristretto_isolated' in queryParams) { 91 | // Step 5: The isolated test receives the `MessagePort` and proceeds to 92 | // invoke the actual test implementation: 93 | port = await this.receiveMessagePort(); 94 | } 95 | 96 | const result = await super.run(suite, ...args); 97 | 98 | if ('ristretto_isolated' in queryParams && port != null) { 99 | // Step 6: The isolated test posts the results through the `MessagePort` 100 | // back to the main frame: 101 | port.postMessage(cloneableResult(result)); 102 | } 103 | 104 | return result; 105 | } 106 | 107 | protected async windUp(context: TestRunContext) 108 | : Promise { 109 | const { suite } = context; 110 | const { queryParams } = suite; 111 | const isIsolated = 'ristretto_isolated' in queryParams; 112 | const shouldBeIsolated = !!this.isolated; 113 | 114 | context = await super.windUp(context); 115 | 116 | if (!shouldBeIsolated || isIsolated) { 117 | return { ...context, isolated: false }; 118 | } 119 | 120 | // Step 1: replace the implementation with one that generates an isolated 121 | // context: 122 | const address = suite.getAddressForTest(this); 123 | const implementation = async () => { 124 | // Step 2: an isolated run is invoked: 125 | const isolatedResult = await this.isolatedRun(address); 126 | 127 | // Step 7: report an error result, if any, from the isolated test 128 | // result: 129 | if (!!isolatedResult.error) { 130 | throw isolatedResult.error; 131 | } 132 | }; 133 | 134 | return { 135 | ...context, 136 | isolated: true, 137 | implementation 138 | }; 139 | } 140 | 141 | protected async postProcess(context: IsolatedTestRunContext, 142 | result: TestResult) : Promise { 143 | const superResult = await super.postProcess(context, result); 144 | return { ...superResult, isolated: context.isolated }; 145 | } 146 | 147 | protected async receiveMessagePort(): Promise { 148 | // Step 3: the isolated context notifies that it is ready to proceed: 149 | window.parent.postMessage(IsolatedTestMessage.ready, window.location.origin); 150 | 151 | return new Promise(resolve => { 152 | const receiveMessage = (event: MessageEvent) => { 153 | if (event.data !== IsolatedTestMessage.messagePort) { 154 | return; 155 | } 156 | 157 | resolve(event.ports[0]); 158 | window.removeEventListener('message', receiveMessage); 159 | }; 160 | 161 | window.addEventListener('message', receiveMessage); 162 | }) as Promise; 163 | } 164 | 165 | protected async isolatedRun(address: SuiteAddress): Promise { 166 | return new Promise(resolve => { 167 | const channel = new MessageChannel(); 168 | const { port1, port2 } = channel; 169 | const url = new URL(window.location.toString()); 170 | const iframe = document.createElement('iframe'); 171 | const receiveMessage = (event: MessageEvent) => { 172 | if (event.source !== iframe.contentWindow && 173 | event.data !== IsolatedTestMessage.ready) { 174 | return; 175 | } 176 | 177 | window.removeEventListener('message', receiveMessage); 178 | // Step 4: the main frame responds to the isolated frame with a 179 | // `MessagePort` for hygeinically communicating test results: 180 | iframe.contentWindow.postMessage( 181 | IsolatedTestMessage.messagePort, '*', [port2]); 182 | }; 183 | 184 | const receiveResult = (event: MessageEvent) => { 185 | if (event.data == null) { 186 | return; 187 | } 188 | 189 | port1.removeEventListener('message', receiveResult); 190 | document.body.removeChild(iframe); 191 | resolve(event.data); 192 | }; 193 | 194 | port1.addEventListener('message', receiveResult); 195 | port1.start(); 196 | 197 | iframe.style.position = 'absolute'; 198 | iframe.style.top = '-1000px'; 199 | iframe.style.left = '-1000px'; 200 | 201 | window.addEventListener('message', receiveMessage); 202 | 203 | const searchPrefix = url.search ? `${url.search}&` : '?'; 204 | const uriAddress = encodeURIComponent(JSON.stringify(address)); 205 | url.search = 206 | `${searchPrefix}ristretto_suite_address=${uriAddress}&ristretto_isolated&ristretto_disable_reporting`; 207 | 208 | document.body.appendChild(iframe); 209 | iframe.src = url.toString(); 210 | }) as Promise; 211 | } 212 | } as Constructor 213 | }; 214 | 215 | 216 | /** 217 | * IsolatedTopic 218 | */ 219 | export interface IsolatedTopic {} 220 | 221 | /** 222 | * An isolated topic is a trivial extension of a `Topic` that mixes-in an 223 | * exstension to the `Topic`'s `Test` implementation. 224 | */ 225 | export function IsolatedTopic>(TopicImplementation: T) { 226 | return class extends TopicImplementation { 227 | protected get TestImplementation() { 228 | return IsolatedTest(super.TestImplementation); 229 | } 230 | } as Constructor 231 | }; 232 | 233 | 234 | /** 235 | * IsolatedSpec 236 | */ 237 | export interface IsolatedSpec {} 238 | 239 | /** 240 | * An isolatable spec is a trivial extension of a `Spec` that mixes-in an 241 | * extension to the `Spec`'s `Topic` implementation. 242 | */ 243 | export function IsolatedSpec>(SpecImplementation: S) { 244 | return class extends SpecImplementation { 245 | protected get TopicImplementation() { 246 | return IsolatedTopic(super.TopicImplementation); 247 | } 248 | } as Constructor 249 | }; 250 | 251 | export const Isolatable = IsolatedSpec; 252 | -------------------------------------------------------------------------------- /src/reporter-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Spec } from './spec.js'; 16 | import { Reporter, ReporterEvent } from './reporter.js'; 17 | import { Fixturable } from './mixins/fixturable.js'; 18 | import { spy } from './helpers/spy.js'; 19 | import '../../../chai/chai.js'; 20 | 21 | const spec = new (Fixturable(Spec))(); 22 | 23 | const { expect } = (self as any).chai; 24 | const { describe, it, before } = spec; 25 | 26 | describe('Reporter', () => { 27 | describe('when extended', () => { 28 | before((context: any) => { 29 | class TestReporter extends Reporter { 30 | events: { event: ReporterEvent, args: any[] }[] = []; 31 | 32 | reporter(event: ReporterEvent, ...args: any[]) { 33 | this.events.push({ event, args }); 34 | super.report(event, ...args); 35 | } 36 | } 37 | 38 | Object.keys(ReporterEvent).forEach((key: string) => { 39 | const event = (ReporterEvent as any)[key]; 40 | spy(TestReporter.prototype, `on${event}`); 41 | }); 42 | 43 | return { 44 | ...context, 45 | TestReporter 46 | }; 47 | }); 48 | 49 | describe('when reporting an event', () => { 50 | it('invokes the corresponding callback', ({ TestReporter }: any) => { 51 | const reporter = new TestReporter(); 52 | reporter.report(ReporterEvent.testStart); 53 | expect(reporter.onTestStart.callCount).to.be.equal(1); 54 | }); 55 | 56 | it('forwards arguments to the callback', ({ TestReporter }: any) => { 57 | const reporter = new TestReporter(); 58 | reporter.report(ReporterEvent.specStart, 'foo'); 59 | expect(reporter.onSpecStart.args).to.be.eql([['foo']]); 60 | }); 61 | }); 62 | }); 63 | }); 64 | 65 | export const reporterSpec: Spec = spec; 66 | -------------------------------------------------------------------------------- /src/reporter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Suite } from './suite.js'; 16 | import { Spec } from './spec.js'; 17 | import { Test, TestResult } from './test.js'; 18 | 19 | /** 20 | * These are the events that represent the different stages of the reporting 21 | * lifecycle. 22 | */ 23 | export enum ReporterEvent { 24 | suiteStart = 'SuiteStart', 25 | suiteEnd = 'SuiteEnd', 26 | specStart = 'SpecStart', 27 | specEnd = 'SpecEnd', 28 | testStart = 'TestStart', 29 | testEnd = 'TestEnd', 30 | unexpectedError = 'UnexpectedError' 31 | }; 32 | 33 | /** 34 | * A reporter is an object that implements some callbacks associated with 35 | * reporting lifecycle stages of interest. The default reporter has none 36 | * of these callbacks implemented. 37 | */ 38 | export abstract class Reporter { 39 | /** 40 | * If set to true, the reporter will not dispatch lifecycle event details 41 | * to its associated callbacks. This is useful for disabling reporting for 42 | * some stretch of time (for example, when a test is isolated). 43 | */ 44 | disabled: boolean = false; 45 | 46 | /** 47 | * Dispatches an event's details to the appropriate lifecycle callback. 48 | */ 49 | report(eventName: ReporterEvent, ...args: any[]): boolean { 50 | const methodName = `on${eventName}` as keyof this; 51 | 52 | if (this.disabled || this[methodName] == null) { 53 | return false; 54 | } 55 | // TODO(dfreedm): make a argument mapping for each method of the reporter, someday 56 | (this[methodName] as any)(...args); 57 | return true; 58 | } 59 | 60 | /** 61 | * Invoked just before a suite begins iterating over specs and invoking tests. 62 | */ 63 | onSuiteStart?(suite: Suite): void; 64 | 65 | /** 66 | * Invoked after a suite has finished iterating over specs and invoking all 67 | * tests. 68 | */ 69 | onSuiteEnd?(suite: Suite): void; 70 | 71 | /** 72 | * Invoked before each spec in the suite, before ivoking tests. 73 | */ 74 | onSpecStart?(spec: Spec, suite: Suite): void; 75 | 76 | /** 77 | * Invoked for each spec in the suite, after all tests have been invoked. 78 | */ 79 | onSpecEnd?(spec: Spec, suite: Suite): void; 80 | 81 | /** 82 | * Invoked for each test, before its implementation is invoked. 83 | */ 84 | onTestStart?(test: Test, suite: Suite): void; 85 | 86 | /** 87 | * Invoked for each test, after its implementation has been invoked. Receives 88 | * the result of the test. 89 | */ 90 | onTestEnd?(result: TestResult, test: Test, suite: Suite): void; 91 | 92 | /** 93 | * Invoked when there is an out-of-band error, such as an internal exception 94 | * of the test runner or one of its related mixins. 95 | */ 96 | onUnexpectedError?(message: string, error: Error, suite: Suite): void; 97 | }; 98 | -------------------------------------------------------------------------------- /src/reporters/console-reporter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Reporter } from '../reporter.js'; 16 | import { Suite } from '../suite.js'; 17 | import { Test, TestResult } from '../test.js'; 18 | import { IsolatedTestResult } from '../mixins/isolatable.js'; 19 | import { ConditionalTestResult } from '../mixins/conditional.js'; 20 | import { Spec } from '../spec.js'; 21 | 22 | export class ConsoleReporter extends Reporter { 23 | onSpecStart(spec: Spec, _suite: Suite): void { 24 | if (spec.rootTopic != null) { 25 | console.log(`%c ${spec.rootTopic!.description} `, 26 | `background-color: #bef; color: #246; 27 | font-weight: bold; font-size: 24px;`); 28 | } 29 | } 30 | 31 | onTestEnd(result: TestResult, test: Test, _suite: Suite): void { 32 | let resultString: string; 33 | let resultColor: string; 34 | 35 | if (result.passed) { 36 | resultString = ' PASSED '; 37 | resultColor = 'green'; 38 | } else if ((result as ConditionalTestResult).skipped) { 39 | resultString = ' SKIPPED '; 40 | resultColor = 'yellow'; 41 | } else { 42 | resultString = ' FAILED '; 43 | resultColor = 'red'; 44 | } 45 | 46 | const resultLog = [ 47 | `${test.behaviorText}... %c${resultString}`, 48 | `color: #fff; font-weight: bold; background-color: ${resultColor}` 49 | ]; 50 | 51 | // TODO(cdata): `isolated` is specific to `IsolatableTestConfig`. It would 52 | // be nice to generalize this somehow, perhaps with some kind of "flags" 53 | // array generated from the config or something. 54 | if ((result as IsolatedTestResult).isolated) { 55 | resultLog[0] = `%c ISOLATED %c ${resultLog[0]}`; 56 | resultLog.splice(1, 0, 57 | `background-color: #fd0; font-weight: bold; color: #830`, ``); 58 | } 59 | 60 | if (result.error && (result.error as any).stack) { 61 | console.error((result.error as any).stack); 62 | } 63 | 64 | console.log(...resultLog); 65 | } 66 | 67 | onUnexpectedError(message: string, error: Error, _suite: Suite): void { 68 | console.error(message); 69 | console.error(error.stack); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/reporters/test-reporter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Reporter } from '../reporter.js'; 16 | import { TestResult } from '../test.js'; 17 | 18 | export interface SpecResult { 19 | testResults: TestResult[]; 20 | } 21 | 22 | export interface SuiteResult { 23 | specResults: SpecResult[]; 24 | } 25 | 26 | /** 27 | * TestReporter provides a Reporter that accumulates test results 28 | * from a Suite run into the `results` property as an array. 29 | * 30 | * This is intended to be used for testing mixins that modify test results. 31 | */ 32 | export class TestReporter extends Reporter { 33 | suiteResult: SuiteResult = {specResults: []}; 34 | 35 | /** 36 | * An array of test results from the Suite run. 37 | * Example: 38 | * ```js 39 | * const reporter = new TestReporter(); 40 | * const suite = new Suite([spec], reporter); 41 | * await suite.run(); 42 | * console.log(reporter.results); 43 | * ``` 44 | */ 45 | get results(): TestResult[] { 46 | const results: TestResult[] = []; 47 | for (const spec of this.suiteResult.specResults) { 48 | for (const test of spec.testResults) { 49 | results.push(test); 50 | } 51 | } 52 | return results; 53 | } 54 | 55 | onSpecStart() { 56 | this.suiteResult.specResults.push({ 57 | testResults: [] 58 | }); 59 | } 60 | 61 | onTestEnd(result: TestResult) { 62 | const srs = this.suiteResult.specResults; 63 | const spec = srs[srs.length - 1]; 64 | spec.testResults.push(result); 65 | } 66 | } -------------------------------------------------------------------------------- /src/spec-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Spec } from './spec.js'; 16 | import { Fixturable, FixturedSpec } from './mixins/fixturable.js'; 17 | import { Constructor } from './util.js'; 18 | import '../../../chai/chai.js'; 19 | 20 | const { expect } = (self as any).chai; 21 | 22 | export const describeSpecSpec = 23 | (specToExtend: Spec & FixturedSpec, Spec: Constructor) => { 24 | const { describe, it, before } = specToExtend; 25 | 26 | describe('Spec', () => { 27 | describe('with topics and tests', () => { 28 | before((context: any) => { 29 | const spec = new Spec(); 30 | const { describe, it } = spec; 31 | 32 | describe('a spec', () => { 33 | it('has a test', () => {}); 34 | describe('nested topic', () => { 35 | it('also has a test', () => {}); 36 | it('may have another test', () => {}); 37 | }); 38 | it('may include trailing tests', () => {}); 39 | }); 40 | 41 | return { ...context, spec }; 42 | }); 43 | 44 | it('counts the total tests in all topics', ({ spec }: any) => { 45 | expect(spec.totalTestCount).to.be.equal(4); 46 | }); 47 | }); 48 | }); 49 | }; 50 | 51 | const spec = new (Fixturable(Spec))(); 52 | 53 | describeSpecSpec(spec, Spec); 54 | 55 | export const specSpec: Spec = spec; 56 | -------------------------------------------------------------------------------- /src/spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Topic } from './topic.js'; 16 | import { Test, TestConfig } from './test.js'; 17 | import { SuiteAddress } from './suite.js'; 18 | 19 | /** 20 | * The `Spec` is the main point of entry to this library for most test suite 21 | * authors. A `Spec` holds the relevant "tear-off" test helpers. The base 22 | * implementation includes only the most basic BDD helpers (`describe` and 23 | * `it`), but it is extensible, and additional tearoffs can be added by 24 | * using mixins. An contrived example usage of a `Spec` looks like this: 25 | * 26 | * ```javascript 27 | * export const spec = new Spec(); 28 | * const { describe, it } = spec; 29 | * 30 | * describe('some test suite', () => { 31 | * it('has a test!', () => {}); 32 | * }); 33 | * ``` 34 | * 35 | * Note that the instance of the `Spec` is exported by the test module. 36 | * 37 | * `Spec` 38 | */ 39 | export class Spec { 40 | // NOTE(cdata): `it` and `describe` are annotated as class fields so that 41 | // they can be "torn off" when used in a test: 42 | 43 | /** 44 | * The `it` method is one of two "tear-off" methods offered in the base 45 | * `Spec` implementation. This method is used by a spec author to describe 46 | * a test. It takes a string description of the test, a function that is 47 | * the actual script used to run the test, and an optional configuration 48 | * object as arguments. 49 | */ 50 | it: Function; 51 | 52 | /** 53 | * The `describe` method is one of two "tear-off" methods offered in 54 | * the base `Spec` implementation. This method is used by the spec author to 55 | * describe a topic. It takes a string description of the topic, and a 56 | * function that, when invoked, describes any additional details of the 57 | * topic via additional `it`, `describe` or other specialized tear-off 58 | * function invokcations. 59 | */ 60 | describe: Function; 61 | 62 | /** 63 | * The `rootTopic` of a spec is the entry point to the tree of all `Topic`s 64 | * and `Test`s in a `Spec`. 65 | */ 66 | rootTopic: Topic | null; 67 | protected currentTopic: Topic | null; 68 | 69 | /** 70 | * The implementation of `Topic` to be used when creating topics in this 71 | * `Spec`. This is useful for specializing the test suite with mixins. 72 | */ 73 | protected get TopicImplementation() { 74 | return Topic; 75 | } 76 | 77 | constructor() { 78 | this.rootTopic = null; 79 | this.currentTopic = null; 80 | 81 | this.it = (description: string, implementation: Function, 82 | config?: TestConfig): void => { 83 | if (this.currentTopic != null) { 84 | this.currentTopic.addTest(description, implementation, config); 85 | } 86 | } 87 | 88 | this.describe = (description: string, factory: Function): void => { 89 | const { currentTopic } = this as Spec; 90 | const nextTopic = currentTopic == null 91 | ? new this.TopicImplementation(description) 92 | : currentTopic!.addSubtopic(description); 93 | 94 | this.currentTopic = nextTopic; 95 | 96 | try { 97 | factory(); 98 | } catch (error) { 99 | console.error(`Error invoking topic "${nextTopic.description}"`); 100 | console.error(error); 101 | } 102 | 103 | if (currentTopic == null) { 104 | this.rootTopic = nextTopic; 105 | } 106 | 107 | this.currentTopic = currentTopic; 108 | } 109 | } 110 | 111 | /** 112 | * The total number of tests in this spec, including the root topic and 113 | * all sub-topics. 114 | */ 115 | get totalTestCount() { 116 | let count = 0; 117 | 118 | if (this.rootTopic != null) { 119 | count += this.rootTopic.totalTestCount; 120 | } 121 | 122 | return count; 123 | } 124 | 125 | /** 126 | * Iterate over all tests in the spec. 127 | */ 128 | *[Symbol.iterator](): IterableIterator { 129 | if (this.rootTopic != null) { 130 | for (const test of this.rootTopic) { 131 | yield test; 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * All tests in the `Spec` are retrievable by `SuiteAddress`. Any 138 | * retrieved `Test` can be invoked on an individual basis exactly 139 | * as it would be invoked if it were running inline with the rest of the 140 | * test suite that it is a part of. 141 | */ 142 | getTestByAddress(address: SuiteAddress): Test | null { 143 | let topic = this.rootTopic; 144 | 145 | for (let i = 0; i < address.topic.length; ++i) { 146 | if (topic != null) { 147 | topic = topic.topics[address.topic[i]]; 148 | } 149 | } 150 | 151 | if (topic != null) { 152 | return topic.tests[address.test]; 153 | } 154 | 155 | return null; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/suite-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Spec } from './spec.js'; 16 | import { Fixturable } from './mixins/fixturable.js'; 17 | import { Suite, SuiteAddress } from './suite.js'; 18 | import '../../../chai/chai.js'; 19 | 20 | const spec = new (Fixturable(Spec))(); 21 | 22 | const { expect } = (self as any).chai; 23 | const { describe, it, before } = spec; 24 | 25 | describe('Suite', () => { 26 | before((context: any) => ({ 27 | ...context, 28 | makeSpec: (topic: string = 'topic') => { 29 | const spec = new Spec(); 30 | const { describe, it } = spec; 31 | describe(topic, () => { 32 | it('has a test', () => {}); 33 | }); 34 | return spec; 35 | } 36 | })); 37 | 38 | describe('with specs', () => { 39 | before((context: any) => { 40 | const { makeSpec } = context; 41 | const suite = new Suite([ 42 | makeSpec('foo'), 43 | makeSpec('bar') 44 | ]); 45 | 46 | return { 47 | ...context, 48 | suite 49 | }; 50 | }); 51 | 52 | it('iterates over all tests in all specs', ({ suite }: any) => { 53 | const tests = ['foo has a test', 'bar has a test']; 54 | let count = 0; 55 | for (const test of suite) { 56 | expect(test.behaviorText).to.be.equal(tests[count++]); 57 | } 58 | expect(count).to.be.equal(2); 59 | }); 60 | 61 | it('resolves an address for a test', ({ suite }: any) => { 62 | const test = suite.specs[0].rootTopic.tests[0]; 63 | const address = suite.getAddressForTest(test); 64 | 65 | expect(address).to.be.eql({ 66 | spec: 0, 67 | topic: [], 68 | test: 0 69 | }); 70 | }); 71 | 72 | it('resolves a test for an address', ({ suite }: any) => { 73 | const address: SuiteAddress = { 74 | spec: 1, 75 | topic: [], 76 | test: 0 77 | }; 78 | const test = suite.getTestByAddress(address); 79 | 80 | expect(test.behaviorText).to.be.equal('bar has a test'); 81 | }); 82 | }); 83 | }); 84 | 85 | export const suiteSpec: Spec = spec; 86 | -------------------------------------------------------------------------------- /src/suite.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Spec } from './spec.js'; 16 | import { Test } from './test.js'; 17 | import { Reporter, ReporterEvent } from './reporter.js'; 18 | import { ConsoleReporter } from './reporters/console-reporter.js'; 19 | 20 | /** 21 | * These are the query params that are observed and used as configuration by a 22 | * `Suite` if they are present in the URL. In the base implementation, these 23 | * are primarily used when running tests in a specialized fashion. 24 | */ 25 | export interface SuiteQueryParams { 26 | [index:string]: string | void; 27 | ristretto_suite_address?: string; 28 | ristretto_disable_reporting?: void; 29 | } 30 | 31 | /** 32 | * A `SuiteAddress` describes the logical position of a `Test` in the hierarchy 33 | * of a given `Suite`. It always points to a `Spec` by index, a `Topic` by array 34 | * of indicies (e.g., `[0, 1, 2]` would refer to the second subtopic of the 35 | * first subtopic of the zeroeth root topic of a given `Spec`), and a `Test` by 36 | * index. 37 | */ 38 | export interface SuiteAddress { 39 | spec: number; 40 | topic: number[]; 41 | test: number; 42 | } 43 | 44 | /** 45 | * A `Suite` represents an ordered set of `Spec` instances. Typically, `Spec` 46 | * instances are created for each module in a library to represent their 47 | * relevant topics and tests, and then imported and composed into a `Suite` in 48 | * order to be invoked. For example: 49 | * 50 | * ```javascript 51 | * import { Suite } from '../../@polymer/ristretto/suite.js'; 52 | * import { fooSpec } from './lib/foo-spec.js'; 53 | * import { barSpec } from './lib/bar-spec.js'; 54 | * 55 | * const suite = new Suite([ 56 | * fooSpec, 57 | * barSpec 58 | * ]); 59 | * 60 | * // Start the test suite: 61 | * suite.run(); 62 | * ``` 63 | */ 64 | export class Suite { 65 | protected specs: Spec[]; 66 | protected address: SuiteAddress | null; 67 | 68 | readonly reporter: Reporter; 69 | readonly queryParams: SuiteQueryParams; 70 | 71 | /** 72 | * The only argument that the base implementation receives is an array of 73 | * the specs it consists of, in the order that they should be invoked. 74 | */ 75 | constructor(specs: Spec[] = [], reporter: Reporter = new ConsoleReporter()) { 76 | this.specs = specs; 77 | this.reporter = reporter; 78 | 79 | const queryParams: SuiteQueryParams = {}; 80 | if (window.location != null && window.location.search != null) { 81 | window.location.search.slice(1).split('&').reduce((map, part) => { 82 | const parts = part.split('='); 83 | map[parts[0]] = decodeURIComponent(parts[1]); 84 | return map; 85 | }, queryParams); 86 | }; 87 | 88 | this.queryParams = queryParams; 89 | this.address = queryParams.ristretto_suite_address 90 | ? JSON.parse(queryParams.ristretto_suite_address) as SuiteAddress 91 | : null; 92 | 93 | if ('ristretto_disable_reporting' in queryParams) { 94 | this.reporter.disabled = true; 95 | } 96 | } 97 | 98 | /** 99 | * Looks up a test within the current suite hierarchy by address. Returns 100 | * `null` if no test is found at the given address. 101 | */ 102 | getTestByAddress(address: SuiteAddress): Test | null { 103 | const spec = this.specs[address.spec]; 104 | const test = spec ? spec.getTestByAddress(address) : null; 105 | 106 | return test; 107 | } 108 | 109 | /** 110 | * Resolves an address for a given test within the current suite hierarchy. 111 | */ 112 | getAddressForTest(test: Test): SuiteAddress { 113 | let { topic } = test; 114 | const testIndex = topic ? topic.tests.indexOf(test) : -1; 115 | const topicAddress = []; 116 | let specIndex = -1; 117 | 118 | while (topic != null) { 119 | const { parentTopic } = topic; 120 | 121 | if (parentTopic != null) { 122 | topicAddress.unshift(parentTopic.topics.indexOf(topic)); 123 | } else { 124 | for (let i = 0; i < this.specs.length; ++i) { 125 | const spec = this.specs[i]; 126 | 127 | if (spec.rootTopic === topic) { 128 | specIndex = i; 129 | } 130 | } 131 | } 132 | 133 | topic = parentTopic; 134 | } 135 | 136 | return { spec: specIndex, topic: topicAddress, test: testIndex }; 137 | } 138 | 139 | /** 140 | * This method invokes the `Test`s in the `Spec`s in the `Suite`. If there 141 | * is a `SuiteAddress` described in the query parameters of the current 142 | * URL, it will invoke only the test that corresponds to that address. 143 | * Otherwise, it will invoke all tests sequentially in a deterministic order. 144 | * The returned promise resolves when all test invocations have completed. 145 | * 146 | * TODO(cdata): This method should probably also accept and respect an address 147 | * for a given test to run as an argument. 148 | */ 149 | async run() { 150 | const { reporter, address } = this; 151 | const soloTest = address ? this.getTestByAddress(address) : null; 152 | const soloSpec = address ? this.specs[address.spec] : null; 153 | 154 | reporter.report(ReporterEvent.suiteStart, this); 155 | 156 | for (const spec of this.specs) { 157 | reporter.report(ReporterEvent.specStart, spec, this); 158 | 159 | if (soloSpec != null && spec !== soloSpec) { 160 | continue; 161 | } 162 | 163 | for (const test of spec) { 164 | if (soloTest != null && test !== soloTest) { 165 | continue; 166 | } 167 | 168 | reporter.report(ReporterEvent.testStart, test, this); 169 | 170 | const result = await test.run(this); 171 | 172 | reporter.report(ReporterEvent.testEnd, result, test, this); 173 | } 174 | 175 | reporter.report(ReporterEvent.specEnd, spec, this); 176 | } 177 | 178 | reporter.report(ReporterEvent.suiteEnd, this); 179 | } 180 | 181 | /** 182 | * The total number of tests in suite, including in all spec topics and their 183 | * sub-topics. 184 | */ 185 | get totalTestCount() { 186 | let count = 0; 187 | 188 | for (const spec of this.specs) { 189 | count += spec.totalTestCount; 190 | } 191 | 192 | return count; 193 | } 194 | 195 | /** 196 | * Iterate over all tests in the suite. 197 | */ 198 | *[Symbol.iterator](): IterableIterator { 199 | const { specs } = this; 200 | 201 | for (let i = 0; i < specs.length; ++i) { 202 | const spec = specs[i]; 203 | 204 | for (const test of spec) { 205 | yield test; 206 | } 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/test-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Spec } from './spec.js'; 16 | import { Fixturable } from './mixins/fixturable.js'; 17 | import { Suite } from './suite.js'; 18 | import { Test, TestResult, TestRunContext } from './test.js'; 19 | import { timePasses } from './util.js'; 20 | import '../../../chai/chai.js'; 21 | 22 | const spec = new (Fixturable(Spec))(); 23 | 24 | const { expect } = (self as any).chai; 25 | const { describe, it, before, after } = spec; 26 | 27 | describe('Test', () => { 28 | before((context: any) => { 29 | let testRunCount = 0; 30 | const testSpy = { 31 | get runCount() { return testRunCount; }, 32 | implementation: () => { testRunCount++ } 33 | }; 34 | const suite = new Suite([]); 35 | 36 | return { 37 | ...context, 38 | suite, 39 | testSpy, 40 | test: new Test('is awesome', testSpy.implementation, {}) 41 | }; 42 | }); 43 | 44 | it('always assumes tests could be asynchronous', ({ test, suite }: any) => { 45 | expect(test.run(suite)).to.be.instanceof(Promise); 46 | }); 47 | 48 | it('invokes the implementation when run', 49 | async ({ test, testSpy, suite }: any) => { 50 | await test.run(suite); 51 | expect(testSpy.runCount).to.be.equal(1); 52 | }); 53 | 54 | describe('postProcess', () => { 55 | before((context: any) => { 56 | const originalPostProcess = (Test as any).prototype.postProcess; 57 | 58 | Object.defineProperty(Test.prototype, 'postProcess', { 59 | value: (_context: TestRunContext, result: TestResult) => { 60 | return { ...result, special: true }; 61 | } 62 | }); 63 | 64 | return { 65 | ...context, 66 | originalPostProcess 67 | }; 68 | }); 69 | 70 | after(({ originalPostProcess }: any) => { 71 | Object.defineProperty(Test.prototype, 72 | 'postProcess', { value: originalPostProcess }); 73 | }); 74 | 75 | it('manipulates the TestResult after the test is run', 76 | async ({ test, suite }: any) => { 77 | const result = await test.run(suite); 78 | expect(result.special).to.be.equal(true); 79 | }); 80 | }); 81 | 82 | describe('when timing out', () => { 83 | before((context: any) => ({ 84 | ...context, 85 | test: new Test('is timing out', 86 | async () => await timePasses(100), { timeout: 50 }) 87 | })); 88 | 89 | it('eventually fails', async ({ test, suite }: any) => { 90 | const result = await test.run(suite); 91 | expect(result.passed).to.be.equal(false); 92 | expect(result.error).to.be.instanceof(Error); 93 | }); 94 | }); 95 | }); 96 | 97 | export const testSpec: Spec = spec; 98 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { timeLimit } from './util.js'; 16 | import { Topic } from './topic.js'; 17 | import { Suite } from './suite.js'; 18 | import { ReporterEvent } from './reporter.js'; 19 | 20 | /** 21 | * The basic test configuration object supports a timeout in milliseconds. 22 | */ 23 | // TODO(cdata): even timeout should be factored out as a mixin. 24 | export interface TestConfig { 25 | timeout?: number; 26 | } 27 | 28 | /** 29 | * A `TestRunContext` represents state that can be shared across test setup 30 | * and teardown routines in the `Test` classes internal implementation. It 31 | * is intended to be useful for specializations of the `Test` class, but not 32 | * directly exposed to the author of a test suite. 33 | */ 34 | export interface TestRunContext { 35 | suite: Suite, 36 | implementation: Function, 37 | cancelTimeLimit?: Function 38 | } 39 | 40 | /** 41 | * A `TestResult` represents the pass or failure of a given test run. The 42 | * result contains a boolean `passed`, and also an `error` property that 43 | * contains the relevant error object if the test failed. Note that some 44 | * kinds of tests cannot report the error object in its original form, so a 45 | * sparse representation containing only the error stack will be availabe in 46 | * that case. 47 | */ 48 | export interface TestResult { 49 | passed: boolean; 50 | error?: Error | { stack: string | void }; 51 | }; 52 | 53 | /** 54 | * `Test`s are the bread and butter of any good test suite. They represent some 55 | * chunk of script - referred to as the `implementation` - that asserts some 56 | * thing related to whatever you are testing. In addition to the test 57 | * `implementation`, a `Test` also has a related human-readable `description` 58 | * and a reference to its immediate parent `Topic`. A test can be configured 59 | * with a `timeout` after which the a test run will automatically fail. 60 | * 61 | * A `Test` is typically created when test details are added to a topic. An 62 | * example of this is whenever `it` is invoked in a test suite: 63 | * 64 | * ```javascript 65 | * it('creates a test', () => {}); 66 | * ``` 67 | * 68 | * If the `it` invocation is "nested" in a `describe` invocation, it will 69 | * cause a `Test` to be added to the `Topic` that is created by the `describe`. 70 | */ 71 | export class Test { 72 | protected config: TestConfig; 73 | protected implementation: Function; 74 | 75 | /** 76 | * The immediate parent `Topic` of the `Test`. 77 | */ 78 | readonly topic: Topic | void; 79 | 80 | /** 81 | * A human readable description of the `Test`. 82 | */ 83 | readonly description: string; 84 | 85 | /** 86 | * The `behaviorText` of a `Test` is its `description` appended to the 87 | * `behaviorText` of its immediate parent `Topic`. 88 | */ 89 | get behaviorText(): string { 90 | return this.topic != null 91 | ? `${this.topic.behaviorText} ${this.description}` 92 | : this.description; 93 | 94 | } 95 | 96 | /** 97 | * A time in ms after which a test run will automatically fail. If left 98 | * unconfigured, the default timeout is 10 seconds. 99 | */ 100 | get timeout(): number { 101 | return this.config.timeout || 10000; 102 | } 103 | 104 | /** 105 | * A `Test` is created by providing a human readable `description`, a 106 | * function implementating the actual test routine, an optional configuration 107 | * object and an optional reference to a related parent `Topic`. 108 | */ 109 | constructor(description: string, 110 | implementation: Function, 111 | config: TestConfig = {}, 112 | topic?: Topic) { 113 | this.config = config; 114 | this.description = description; 115 | this.implementation = implementation; 116 | this.topic = topic; 117 | } 118 | 119 | /** 120 | * When preparing to invoke a test `implementation`, there is a "wind-up" 121 | * phase that allows the `Test` class and any specializations to prepare 122 | * the `implementation` based on the configured `TestRunContext`. The 123 | * default `TestRunContext` contains only one key: the current 124 | * `implementation` of the test. It is important to refer to this 125 | * `implementation`, as it may be modified by specialized overridden 126 | * implementations of `windUp`. 127 | * 128 | * The `windUp` method should return a modified `TestRunContext`, if 129 | * any modifications are deemed necessary. The default implementation, 130 | * for example, modifies the test `implementation` to include a time limit 131 | * after which the test will automatically fail. 132 | */ 133 | protected async windUp(context: TestRunContext): Promise { 134 | const { timeout } = this; 135 | const { implementation } = context; 136 | const { 137 | promise: timeLimitPromise, 138 | cancel: cancelTimeLimit 139 | } = timeLimit(timeout); 140 | 141 | return { 142 | ...context, 143 | cancelTimeLimit, 144 | implementation: (...args: any[]) => 145 | Promise.race([implementation(...args), timeLimitPromise]) 146 | }; 147 | } 148 | 149 | /** 150 | * Once a test `implementation` has run and the result has been measured, 151 | * there is an opportunity to manipulate the result based on the 152 | * `TestRunContext`. This is useful in cases where the context of the 153 | * test run may have a specialized effect on the result of the test. 154 | * 155 | * For example, a test that has been "skipped" may technically have passed, 156 | * but it may be desirable to mark the result of such a test as not passed, 157 | * or to decorate the result with a flag indicating that the test had 158 | * passed but had been skipped. 159 | */ 160 | protected async postProcess(_context: TestRunContext, result: TestResult) 161 | : Promise { 162 | return result; 163 | } 164 | 165 | /** 166 | * When a test invokation has completed, there is a "wind-down" phase 167 | * that allows the `Test` class and any specializations to clean up the 168 | * `TestRunContext` that was created during the "wind-up" phase. The 169 | * `windDown` method is always invoked at the end of a test run, regardless 170 | * of whether the test passed or failed. However, it is not invoked if an 171 | * exception is thrown during the "wind-up" phase. 172 | * 173 | * The default `windDown` method, for example, cancels the time limit 174 | * created by the default `windUp` method. 175 | */ 176 | protected async windDown(context: TestRunContext): Promise { 177 | context.cancelTimeLimit!(); 178 | } 179 | 180 | /** 181 | * The `run` method initiates a test run. A test run consists of four 182 | * phases: 183 | * 184 | * 1. Wind-up phase, during which the `TestRunContext` is created. 185 | * 2. Test run phase, when the test `implementation` is invoked and measured. 186 | * 3. Post-process phase, where `TestResult` may be changed based on the 187 | * `TestRunContext`. 188 | * 4. Wind-down phase, during which the `TestRunContext` is cleaned up. 189 | * 190 | * The "wind-up" and "wind-down" phases are described in detail by the 191 | * related `windUp` and `windDown` methods in this class. 192 | * 193 | * The test run phase consists of taking the `implementation` generated 194 | * by the "wind-up" phase, invoking it, and measuring it for exceptions. If 195 | * no exceptions are measured, the method returns a passing `TestResult`. If 196 | * an exception is measured, the method returns a non-passing `TestResult`. 197 | */ 198 | async run(suite: Suite, ..._args: any[]): Promise { 199 | const { reporter } = suite; 200 | 201 | let context: TestRunContext; 202 | let result: TestResult; 203 | 204 | try { 205 | context = await this.windUp({ 206 | suite, 207 | implementation: this.implementation 208 | }); 209 | 210 | try { 211 | const { implementation } = context; 212 | await implementation(); 213 | result = { passed: true }; 214 | } catch (error) { 215 | result = { passed: false, error }; 216 | } 217 | 218 | await this.windDown(context); 219 | result = await this.postProcess(context, result!); 220 | } catch (error) { 221 | reporter.report(ReporterEvent.unexpectedError, 222 | 'Error preparing test context.', error, suite); 223 | 224 | result = { passed: false, error }; 225 | } 226 | 227 | return result; 228 | } 229 | }; 230 | -------------------------------------------------------------------------------- /src/topic-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Spec } from './spec.js'; 16 | import { Fixturable } from './mixins/fixturable.js'; 17 | import { Topic } from './topic.js'; 18 | import '../../../chai/chai.js'; 19 | 20 | const spec = new (Fixturable(Spec))(); 21 | 22 | const { expect } = (self as any).chai; 23 | const { describe, it, before } = spec; 24 | 25 | describe('Topic', () => { 26 | describe('without a parent topic', () => { 27 | before((context: any) => ({ 28 | ...context, 29 | topic: new Topic('some topic') 30 | })); 31 | 32 | it('has behavior text that matches the description', ({ topic }: any) => { 33 | expect(topic.behaviorText).to.be.equal(topic.description); 34 | }); 35 | }); 36 | 37 | describe('with a parent topic', () => { 38 | before((context: any) => ({ 39 | ...context, 40 | topic: new Topic('some topic', new Topic('parented')) 41 | })); 42 | 43 | it('has behavior text that joins the parent and child descriptions', 44 | ({ topic }: any) => { 45 | const { parentTopic } = topic; 46 | expect(topic.behaviorText).to.be.equal( 47 | `${parentTopic.description} ${topic.description}`); 48 | }); 49 | }); 50 | }); 51 | 52 | export const topicSpec: Spec = spec; 53 | 54 | -------------------------------------------------------------------------------- /src/topic.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Test, TestConfig } from './test.js'; 16 | 17 | /** 18 | * A `Topic` is created to hold a set of tests with related context. The 19 | * base `Topic` implementation includes a "description" of the set of tests 20 | * as the only context information. You can think of a `Topic` as what is 21 | * represented in a BDD test suite by what you find inside of a `describe` 22 | * call. For example, here is a `Topic` with a description "some topic" and 23 | * one test: 24 | * 25 | * ```javascript 26 | * describe('some topic', () => { 27 | * it('has a test', () => {}); 28 | * }); 29 | * ``` 30 | * 31 | * `Topic`s can also have subtopics, which are created when `describe` calls 32 | * are nested: 33 | * 34 | * ```javascript 35 | * describe('some topic', () => { 36 | * describe('some subtopic', () => {}); 37 | * }); 38 | * ``` 39 | */ 40 | export class Topic { 41 | readonly parentTopic: Topic | void; 42 | 43 | /** 44 | * The set of tests directly associated with this topic. Subtopic tests 45 | * are associated with their immediate topic, and not their ancestor topics. 46 | */ 47 | readonly tests: Test[] = []; 48 | 49 | /** 50 | * The set of subtopics directly associated with this topic. Sub-subtopics 51 | * are associated with their immediate parent topic, and not other ancestor 52 | * topics. 53 | */ 54 | readonly topics: Topic[] = []; 55 | 56 | /** 57 | * The description for this topic. 58 | */ 59 | readonly description: string; 60 | 61 | protected get TestImplementation() { 62 | return Test; 63 | } 64 | 65 | /** 66 | * The `behaviorText` is the topic description appended to its parent topic 67 | * description. For nested topics, this leads to useful, human-readable chains 68 | * of descriptions. 69 | */ 70 | get behaviorText(): string { 71 | return this.parentTopic != null 72 | ? `${this.parentTopic.behaviorText} ${this.description}` 73 | : this.description; 74 | } 75 | 76 | /** 77 | * A `Topic` receives a description, and an optional reference to a parent 78 | * topic. 79 | */ 80 | constructor(description: string, parentTopic?: Topic) { 81 | this.description = description; 82 | this.parentTopic = parentTopic; 83 | } 84 | 85 | /** 86 | * A subtopic can be added by offering a relevant description string. It 87 | * will automatically be parented to the current topic and returned. 88 | */ 89 | addSubtopic(description: string): Topic { 90 | const subtopic = new (this.constructor)(description, this); 91 | this.topics.push(subtopic); 92 | return subtopic; 93 | } 94 | 95 | /** 96 | * Add a test to this topic. The test is described by its description, an 97 | * implementation function and an optional configuration object. 98 | */ 99 | addTest(description: string, implementation: Function, 100 | config?: TestConfig): Test { 101 | const test = new (this.TestImplementation)(description, implementation, 102 | config, this); 103 | this.tests.push(test); 104 | return test; 105 | } 106 | 107 | /** 108 | * The total number of tests in this topic, including all tests in all 109 | * sub-topics. 110 | */ 111 | get totalTestCount() { 112 | let count = this.tests.length; 113 | 114 | for (const topic of this.topics) { 115 | count += topic.totalTestCount; 116 | } 117 | 118 | return count; 119 | } 120 | 121 | /** 122 | * Iterate over all tests in the topic, including all sub-topics. 123 | */ 124 | *[Symbol.iterator](): IterableIterator { 125 | for (const test of this.tests) { 126 | yield test; 127 | } 128 | 129 | for (const topic of this.topics) { 130 | for (const test of topic) { 131 | yield test; 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/util-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { Spec } from './spec.js'; 16 | import { Fixturable } from './mixins/fixturable.js'; 17 | import { timePasses, timeLimit, cloneableResult } from './util.js'; 18 | import '../../../chai/chai.js'; 19 | 20 | const spec = new (Fixturable(Spec))(); 21 | 22 | const { expect } = (self as any).chai; 23 | const { describe, it, before } = spec; 24 | 25 | describe('util', () => { 26 | describe('timePasses', () => { 27 | it('returns a promise', () => { 28 | const timePassagePromise = timePasses(0); 29 | expect(timePassagePromise).to.be.instanceof(Promise); 30 | }); 31 | 32 | describe('when invoked with some time in ms', () => { 33 | it('allows that time to pass before resolving', async () => { 34 | const timeStart = performance.now(); 35 | await timePasses(30); 36 | const timeEnd = performance.now(); 37 | const delta = timeEnd - timeStart; 38 | 39 | expect(delta).to.be.greaterThan(30); 40 | }); 41 | }); 42 | }); 43 | 44 | describe('timeLimit', () => { 45 | describe('when invoked with some time in ms', () => { 46 | it('eventually throws', async () => { 47 | let threw = false; 48 | 49 | try { 50 | await timeLimit(100).promise; 51 | } catch (e) { 52 | threw = true; 53 | } 54 | 55 | expect(threw).to.be.equal(true); 56 | }); 57 | 58 | describe('and then cancelled', () => { 59 | it('does not throw', async () => { 60 | let threw = false; 61 | 62 | try { 63 | const limit = timeLimit(100); 64 | limit.cancel(); 65 | await timePasses(150); 66 | } catch (e) { 67 | threw = true; 68 | } 69 | 70 | expect(threw).to.be.equal(false); 71 | }); 72 | }); 73 | }); 74 | }); 75 | 76 | describe('cloneableResult', () => { 77 | describe('when invoked', () => { 78 | describe('with a test result', () => { 79 | before((context: any) => ({ 80 | ...context, 81 | result: { passed: true, error: false } 82 | })); 83 | 84 | describe('without an error object', () => { 85 | it('returns the same result', ({ result }: any) => { 86 | expect(cloneableResult(result)).to.be.equal(result); 87 | }); 88 | }); 89 | 90 | describe('with an error object', () => { 91 | before((context: any) => ({ 92 | ...context, 93 | result: { passed: false, error: new Error('hi') } 94 | })); 95 | 96 | it('returns a copy of the object', ({ result }: any) => { 97 | const clonedResult = cloneableResult(result); 98 | 99 | expect(clonedResult).to.not.be.equal(result); 100 | expect(clonedResult.passed).to.be.equal(result.passed); 101 | }); 102 | 103 | it('only copies the stack from the error subkey', 104 | ({ result }: any) => { 105 | const clonedResult = cloneableResult(result); 106 | const { error } = clonedResult as any; 107 | 108 | expect(error.stack).to.be.equal(result.error.stack); 109 | expect(error.message).to.not.be.ok; 110 | }); 111 | }); 112 | }); 113 | }); 114 | }); 115 | }); 116 | 117 | export const utilSpec: Spec = spec; 118 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | import { TestResult } from './test.js'; 16 | 17 | export interface TimeLimitContext { 18 | promise: Promise; 19 | cancel(): void; 20 | }; 21 | 22 | /** 23 | * This helper makes it easy to express some amount of time as a 24 | * promise. The amount of time is not guaranteed to be exactly what is 25 | * requested (it will usually be a bit longer due to the timing details 26 | * of promises). 27 | * 28 | * A contrived usage in a test might look like this: 29 | * 30 | * ```javascript 31 | * it('requires some time to pass', async () => { 32 | * await timePasses(100); 33 | * }); 34 | * ``` 35 | */ 36 | export const timePasses = (ms: number): Promise => new Promise( 37 | resolve => setTimeout(() => { 38 | resolve(); 39 | }, ms)); 40 | 41 | /** 42 | * This helper works as a guard against tests that will never end. As part 43 | * of its return value, it includes a promise that will reject after the 44 | * specified amount of time has passed. It also includes a cancel function 45 | * that "defuses" the eventually rejected promise. 46 | */ 47 | export const timeLimit = (ms: number): TimeLimitContext => { 48 | let cancelled = false; 49 | 50 | return { 51 | promise: timePasses(ms).then(() => cancelled 52 | ? Promise.resolve() 53 | : Promise.reject(new Error(`Time ran out after ${ms}ms`))), 54 | cancel() { cancelled = true; } 55 | }; 56 | }; 57 | 58 | /** 59 | * When a `TestResult` is broadcast using `postMessage`, it can throw because 60 | * the origin of an error doesn't match the origin of the context posting 61 | * the message. This helper pulls the most critical piece of information 62 | * out of the `TestResult` in order to avoid such errors. 63 | */ 64 | export const cloneableResult = (result: TestResult): TestResult => { 65 | if (result.error instanceof Error) { 66 | return { 67 | ...result, 68 | error: { 69 | stack: result.error.stack 70 | } 71 | }; 72 | } 73 | 74 | return result; 75 | }; 76 | 77 | /** 78 | * The Constructor type is used when describing the type annotations for 79 | * mixins. See src/mixins/fixturable.ts for an example of usage. 80 | */ 81 | export type Constructor = { new(...args: any[]): T }; 82 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "es2015", 5 | "lib": ["es2017", "esnext.asynciterable", "dom"], 6 | "declaration": true, 7 | "sourceMap": true, 8 | "inlineSources": true, 9 | "outDir": "./lib", 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true 15 | }, 16 | "include": [ 17 | "src/**/*.ts" 18 | ], 19 | "exclude": [] 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "arrow-parens": true, 4 | "class-name": true, 5 | "indent": [ 6 | true, 7 | "spaces", 8 | 2 9 | ], 10 | "prefer-const": true, 11 | "no-duplicate-variable": true, 12 | "no-eval": true, 13 | "no-internal-module": true, 14 | "no-trailing-whitespace": true, 15 | "no-var-keyword": true, 16 | "one-line": [ 17 | true, 18 | "check-open-brace", 19 | "check-whitespace" 20 | ], 21 | "quotemark": [ 22 | true, 23 | "single", 24 | "avoid-escape" 25 | ], 26 | "semicolon": [ 27 | true, 28 | "always" 29 | ], 30 | "trailing-comma": [ 31 | true, 32 | "multiline" 33 | ], 34 | "triple-equals": [ 35 | true, 36 | "allow-null-check" 37 | ], 38 | "typedef-whitespace": [ 39 | true, 40 | { 41 | "call-signature": "nospace", 42 | "index-signature": "nospace", 43 | "parameter": "nospace", 44 | "property-declaration": "nospace", 45 | "variable-declaration": "nospace" 46 | } 47 | ], 48 | "variable-name": [ 49 | true, 50 | "ban-keywords" 51 | ], 52 | "whitespace": [ 53 | true, 54 | "check-branch", 55 | "check-decl", 56 | "check-operator", 57 | "check-separator", 58 | "check-type" 59 | ] 60 | } 61 | } 62 | --------------------------------------------------------------------------------