| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview Javascript test harness. |
| */ |
| |
| import type {TaskTimer} from '//ios/web/annotations/resources/text_tasks.js'; |
| |
| // TODO(crbug.com/40936184): move to general ts utilities. |
| |
| // Fake time TaskTimer. |
| export class FakeTaskTimer implements TaskTimer { |
| // Fake clock. |
| nowMs = 0; |
| |
| timers = new Map<number, {then: Function, at: number}>(); |
| uniqueId = 0; |
| |
| clear(id: number): void { |
| this.timers.delete(id); |
| } |
| reset(then: Function, ms: number): number { |
| const id = ++this.uniqueId; |
| this.timers.set(id, {then, at: this.now() + ms}); |
| return id; |
| } |
| now(): number { |
| return this.nowMs; |
| } |
| |
| // Fake controls. |
| |
| // Finds and returns next event id or null. |
| nextEventId(): number|null { |
| let next: number|null = null; |
| this.timers.forEach((value, key) => { |
| if (next === null || value.at < this.timers.get(next)!.at) { |
| next = key; |
| } |
| }); |
| return next; |
| } |
| |
| // Move clock and trigger all events that need triggering. |
| moveAhead(ms: number, times = 1): void { |
| while (times > 0) { |
| this.nowMs += ms; |
| let next = this.nextEventId(); |
| while (next) { |
| const event = this.timers.get(next)!; |
| if (event.at > this.nowMs) { |
| break; |
| } |
| this.timers.delete(next); |
| event.then(); |
| next = this.nextEventId(); |
| } |
| times--; |
| } |
| } |
| |
| // Clear tasks and put time back to 0. |
| restart() { |
| this.nowMs = 0; |
| this.timers.clear(); |
| } |
| } |
| |
| // Result of a single test. |
| interface TestResult { |
| name: string; |
| result: string; |
| error?: string; |
| } |
| |
| // Base class for any test suite. |
| // Example: |
| // class TestFoo extends TestSuite { |
| // override setUpSuite(): void { |
| // createFooSingleton(); |
| // } |
| // override tearDownSuite(): void { |
| // destroyFooSingleton(); |
| // } |
| // override setUp(): void { |
| // load('<foo id="bar">Bar</foo>'); |
| // fooSingleton.startMonitor(); |
| // } |
| // override tearDown(): void { |
| // fooSingleton.stopMonitor(); |
| // } |
| // |
| // testBar() { |
| // expectEq(fooSingleton.monitoredTextFor('bar'), 'Bar'); |
| // } |
| // } |
| // |
| // new TestFoo().run(); |
| export class TestSuite { |
| private results: TestResult[] = []; |
| |
| // Called once when starting suite in `run`. |
| setUpSuite(): void { |
| document.body.innerHTML = ''; |
| document.head.innerHTML = ''; |
| } |
| // Called before each test in `run`. |
| setUp(): void {} |
| |
| // Iterates and executes methods starting with 'test'. |
| run(): TestResult[] { |
| this.results = []; |
| const tryPhase = (phase: string, callback: Function) => { |
| try { |
| callback(); |
| } catch (error) { |
| this.results.push({ |
| name: phase, |
| result: 'FAILED', |
| error: '' + error + '\n' + (error as Error).stack, |
| }); |
| } |
| }; |
| tryPhase('setUpSuite', () => { |
| this.setUpSuite(); |
| }); |
| for (const method of Object.getOwnPropertyNames( |
| Object.getPrototypeOf(this))) { |
| if (method.startsWith('test')) { |
| tryPhase('setUp(' + method + ')', () => { |
| this.setUp(); |
| }); |
| tryPhase(method, () => { |
| (this as any)[method](); |
| this.results.push({name: method, result: 'OK'}); |
| }); |
| tryPhase('tearDown(' + method + ')', () => { |
| this.tearDown(); |
| }); |
| } |
| } |
| tryPhase('tearDownSuite', () => { |
| this.tearDownSuite(); |
| }); |
| return this.results; |
| } |
| |
| // Called after each test in `run`. |
| tearDown(): void {} |
| // Called once when ending suite in `run`. |
| tearDownSuite(): void {} |
| |
| // Add debug information to log. |
| log(data: any): void { |
| this.results.push({name: 'log', result: 'LOG', error: '' + data}); |
| } |
| } |
| |
| // Throws exception if `a` !== `b`. |
| export function expectEq(a: any, b: any, info = ''): void { |
| if (a !== b) { |
| throw new Error(info + `"${a}" !== "${b}"`); |
| } |
| } |
| |
| // Throws exception if `a` === `b`. |
| export function expectNeq(a: any, b: any, info = ''): void { |
| if (a === b) { |
| throw new Error(info + `"${a}" === "${b}"`); |
| } |
| } |
| |
| // Throws exception with `info`. |
| export function fail(info = ''): void { |
| throw new Error(info); |
| } |
| |
| // Loads given `html` into page body. |
| export function load(html: string): void { |
| document.body.innerHTML = html; |
| } |
| |
| // Loads given `html` into page head. |
| export function loadHead(html: string): void { |
| document.head.innerHTML = html; |
| } |