UNPKG

ember-test-waiters

Version:

The default blueprint for ember-cli addons.

168 lines (135 loc) 3.91 kB
import { Primitive, TestWaiter, TestWaiterDebugInfo, WaiterName } from './'; import { DEBUG } from '@glimmer/env'; import Token from './token'; import { register } from './waiter-manager'; import { warn } from '@ember/debug'; const WAITER_NAME_PATTERN = /^[^:]*:?.*/; let WAITER_NAMES = DEBUG ? new Set() : undefined; export function _resetWaiterNames() { WAITER_NAMES = new Set(); } function getNextToken(): Token { return new Token(); } class TestWaiterImpl<T extends object | Primitive = Token> implements TestWaiter<T> { public name: WaiterName; private nextToken: () => T; private isRegistered = false; items = new Map<T, TestWaiterDebugInfo>(); completedOperationsForTokens = new WeakMap<Token, boolean>(); completedOperationsForPrimitives = new Map<Primitive, boolean>(); constructor(name: WaiterName, nextToken?: () => T) { this.name = name; // @ts-ignore this.nextToken = nextToken || getNextToken; } beginAsync(token: T = this.nextToken(), label?: string): T { this._register(); if (this.items.has(token)) { throw new Error(`beginAsync called for ${token} but it is already pending.`); } let error = new Error(); this.items.set(token, { get stack() { return error.stack; }, label, }); return token; } endAsync(token: T): void { if (!this.items.has(token) && !this._getCompletedOperations(token).has(token)) { throw new Error(`endAsync called with no preceding beginAsync call.`); } this.items.delete(token); // Mark when a waiter operation has completed so we can distinguish // whether endAsync is being called before a prior beginAsync call above. this._getCompletedOperations(token).set(token, true); } waitUntil(): boolean { return this.items.size === 0; } debugInfo(): TestWaiterDebugInfo[] { return [...this.items.values()]; } reset(): void { this.items.clear(); } private _register(): void { if (!this.isRegistered) { register(this); this.isRegistered = true; } } private _getCompletedOperations(token: T) { let type = typeof token; return token !== null || (type !== 'function' && type !== 'object') ? this.completedOperationsForPrimitives : this.completedOperationsForTokens; } } class NoopTestWaiter implements TestWaiter { name: string; constructor(name: string) { this.name = name; } beginAsync(): Token { return this; } endAsync(): void {} waitUntil(): boolean { return true; } debugInfo(): TestWaiterDebugInfo[] { return []; } reset(): void {} } /** * Builds and returns a test waiter. The type of the * returned waiter is dependent on whether the app or * addon is in `DEBUG` mode or not. * * @public * * @param name {string} The name of the test waiter * @returns {TestWaiter} * * @example * * import Component from '@ember/component'; * import { buildWaiter } from '@ember/test-waiters'; * * if (DEBUG) { * let waiter = buildWaiter('friend-waiter'); * } * * export default class Friendz extends Component { * didInsertElement() { * let token = waiter.beginAsync(this); * * someAsyncWork().then(() => { * waiter.endAsync(token); * }); * } * } */ export default function buildWaiter(name: string): TestWaiter { if (DEBUG) { warn(`The waiter name '${name}' is already in use`, !WAITER_NAMES!.has(name), { id: 'ember-test-waiters.duplicate-waiter-name', }); WAITER_NAMES!.add(name); } if (!DEBUG) { return new NoopTestWaiter(name); } warn( `You must provide a name that contains a descriptive prefix separated by a colon. Example: ember-fictitious-addon:some-file You passed: ${name}`, WAITER_NAME_PATTERN.test(name), { id: 'ember-test-waiters.invalid-waiter-name' } ); return new TestWaiterImpl(name); }