From dfac53e9fde812b8b7acda90c7ae55a96402de34 Mon Sep 17 00:00:00 2001 From: timdeschryver <28659384+timdeschryver@users.noreply.github.com> Date: Tue, 3 Dec 2019 08:02:23 +0100 Subject: [PATCH 1/2] feat: add waitForElement and waitForElementToBeRemoved --- projects/testing-library/src/lib/models.ts | 30 ++++++++++---- .../src/lib/testing-library.ts | 40 ++++++++++++++++++- .../wait-for-element-to-be-removed.spec.ts | 32 +++++++++++++++ .../tests/wait-for-element.spec.ts | 37 +++++++++++++++++ projects/testing-library/tsconfig.lib.json | 2 +- 5 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 projects/testing-library/tests/wait-for-element-to-be-removed.spec.ts create mode 100644 projects/testing-library/tests/wait-for-element.spec.ts diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index e6d69583..9c123277 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -1,7 +1,7 @@ import { Type, DebugElement } from '@angular/core'; import { ComponentFixture } from '@angular/core/testing'; import { Routes } from '@angular/router'; -import { BoundFunction, FireObject, Queries, queries } from '@testing-library/dom'; +import { BoundFunction, FireObject, Queries, queries, waitForElement } from '@testing-library/dom'; import { UserEvents } from './user-events'; export type RenderResultQueries = { [P in keyof Q]: BoundFunction }; @@ -34,9 +34,11 @@ export interface RenderResult detectChanges: () => void; /** * @description - * Re-render the same component with different props. + * The Angular `DebugElement` of the component. + * + * For more info see https://angular.io/api/core/DebugElement */ - rerender: (componentProperties: Partial) => void; + debugElement: DebugElement; /** * @description * The Angular `ComponentFixture` of the component or the wrapper. @@ -47,17 +49,29 @@ export interface RenderResult fixture: ComponentFixture; /** * @description - * The Angular `DebugElement` of the component. + * Navigates to the href of the element or to the path. * - * For more info see https://angular.io/api/core/DebugElement */ - debugElement: DebugElement; + navigate: (elementOrPath: Element | string, basePath?: string) => Promise; /** * @description - * Navigates to the href of the element or to the path. + * Re-render the same component with different props. + */ + rerender: (componentProperties: Partial) => void; + /** + * @description + * Wait for DOM elements to appear, disappear, or change. * + * For more info see https://testing-library.com/docs/dom-testing-library/api-async#waitforelement */ - navigate: (elementOrPath: Element | string, basePath?: string) => Promise; + waitForElement: typeof waitForElement; + /** + * @description + * Wait for the removal of element(s) from the DOM. + * + * For more info see https://testing-library.com/docs/dom-testing-library/api-async#waitForElementToBeRemoved + */ + waitForElementToBeRemoved: typeof waitForElement; } export interface RenderComponentOptions { diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 4917d62b..b7bfcc6f 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -4,7 +4,15 @@ import { By } from '@angular/platform-browser'; import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { fireEvent, FireFunction, FireObject, getQueriesForElement, prettyDOM } from '@testing-library/dom'; +import { + fireEvent, + FireFunction, + FireObject, + getQueriesForElement, + prettyDOM, + waitForElement, + waitForElementToBeRemoved, +} from '@testing-library/dom'; import { RenderComponentOptions, RenderDirectiveOptions, RenderResult } from './models'; import { createSelectOptions, createType } from './user-events'; @@ -111,6 +119,34 @@ export async function render( return result; }; + function componentWaitForElement( + callback: () => Result, + options?: { + container?: HTMLElement; + timeout?: number; + mutationObserverOptions?: MutationObserverInit; + }, + ): Promise { + const interval = setInterval(detectChanges, 10); + return waitForElement(callback, { container: fixture.nativeElement, ...options }).finally(() => + clearInterval(interval), + ); + } + + function componentWaitForElementToBeRemoved( + callback: () => Result, + options?: { + container?: HTMLElement; + timeout?: number; + mutationObserverOptions?: MutationObserverInit; + }, + ): Promise { + const interval = setInterval(detectChanges, 10); + return waitForElementToBeRemoved(callback, { container: fixture.nativeElement, ...options }).finally(() => + clearInterval(interval), + ); + } + return { fixture, detectChanges, @@ -121,6 +157,8 @@ export async function render( debug: (element = fixture.nativeElement) => console.log(prettyDOM(element)), type: createType(eventsWithDetectChanges), selectOptions: createSelectOptions(eventsWithDetectChanges), + waitForElement: componentWaitForElement, + waitForElementToBeRemoved: componentWaitForElementToBeRemoved, ...getQueriesForElement(fixture.nativeElement, queries), ...eventsWithDetectChanges, }; diff --git a/projects/testing-library/tests/wait-for-element-to-be-removed.spec.ts b/projects/testing-library/tests/wait-for-element-to-be-removed.spec.ts new file mode 100644 index 00000000..b68e629d --- /dev/null +++ b/projects/testing-library/tests/wait-for-element-to-be-removed.spec.ts @@ -0,0 +1,32 @@ +import { Component, OnInit } from '@angular/core'; +import { render } from '../src/public_api'; +import { timer } from 'rxjs'; + +@Component({ + selector: 'fixture', + template: ` +
👋
+ `, +}) +class FixtureComponent implements OnInit { + visible = true; + ngOnInit() { + timer(500).subscribe(() => (this.visible = false)); + } +} + +test('waits for element to be removed', async () => { + const { queryByTestId, getByTestId, waitForElementToBeRemoved } = await render(FixtureComponent); + + await waitForElementToBeRemoved(() => getByTestId('im-here')); + + expect(queryByTestId('im-here')).toBeNull(); +}); + +test('allows to override options', async () => { + const { getByTestId, waitForElementToBeRemoved } = await render(FixtureComponent); + + await expect(waitForElementToBeRemoved(() => getByTestId('im-here'), { timeout: 200 })).rejects.toThrow( + /Timed out in waitForElementToBeRemoved/i, + ); +}); diff --git a/projects/testing-library/tests/wait-for-element.spec.ts b/projects/testing-library/tests/wait-for-element.spec.ts new file mode 100644 index 00000000..24686467 --- /dev/null +++ b/projects/testing-library/tests/wait-for-element.spec.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; +import { render } from '../src/public_api'; +import { timer } from 'rxjs'; + +@Component({ + selector: 'fixture', + template: ` + +
{{ result }}
+ `, +}) +class FixtureComponent { + result = ''; + + load() { + timer(500).subscribe(() => (this.result = 'Success')); + } +} + +test('waits for element to be visible', async () => { + const { getByTestId, click, waitForElement, getByText } = await render(FixtureComponent); + + click(getByTestId('button')); + + await waitForElement(() => getByText('Success')); + getByText('Success'); +}); + +test('allows to override options', async () => { + const { getByTestId, click, waitForElement, getByText } = await render(FixtureComponent); + + click(getByTestId('button')); + + await expect(waitForElement(() => getByText('Success'), { timeout: 200 })).rejects.toThrow( + /Unable to find an element with the text: Success/i, + ); +}); diff --git a/projects/testing-library/tsconfig.lib.json b/projects/testing-library/tsconfig.lib.json index 8be8cbcd..4a59e8ca 100644 --- a/projects/testing-library/tsconfig.lib.json +++ b/projects/testing-library/tsconfig.lib.json @@ -12,7 +12,7 @@ "experimentalDecorators": true, "importHelpers": true, "types": [], - "lib": ["dom", "es2015"] + "lib": ["dom", "es2015", "es2018.promise"] }, "angularCompilerOptions": { "annotateForClosureCompiler": true, From 4abb77ae3361c569602118a2d4b73775e5d7e9df Mon Sep 17 00:00:00 2001 From: timdeschryver <28659384+timdeschryver@users.noreply.github.com> Date: Tue, 3 Dec 2019 19:49:57 +0100 Subject: [PATCH 2/2] feat: add waitForDomChange --- projects/testing-library/src/lib/models.ts | 21 ++++++++-- .../src/lib/testing-library.ts | 13 ++++++ .../tests/wait-for-dom-change.spec.ts | 40 +++++++++++++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 projects/testing-library/tests/wait-for-dom-change.spec.ts diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 9c123277..10acde5c 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -1,7 +1,15 @@ import { Type, DebugElement } from '@angular/core'; import { ComponentFixture } from '@angular/core/testing'; import { Routes } from '@angular/router'; -import { BoundFunction, FireObject, Queries, queries, waitForElement } from '@testing-library/dom'; +import { + BoundFunction, + FireObject, + Queries, + queries, + waitForElement, + waitForElementToBeRemoved, + waitForDomChange, +} from '@testing-library/dom'; import { UserEvents } from './user-events'; export type RenderResultQueries = { [P in keyof Q]: BoundFunction }; @@ -58,6 +66,13 @@ export interface RenderResult * Re-render the same component with different props. */ rerender: (componentProperties: Partial) => void; + /** + * @description + * Wait for the DOM to change. + * + * For more info see https://testing-library.com/docs/dom-testing-library/api-async#waitfordomchange + */ + waitForDomChange: typeof waitForDomChange; /** * @description * Wait for DOM elements to appear, disappear, or change. @@ -69,9 +84,9 @@ export interface RenderResult * @description * Wait for the removal of element(s) from the DOM. * - * For more info see https://testing-library.com/docs/dom-testing-library/api-async#waitForElementToBeRemoved + * For more info see https://testing-library.com/docs/dom-testing-library/api-async#waitforelementtoberemoved */ - waitForElementToBeRemoved: typeof waitForElement; + waitForElementToBeRemoved: typeof waitForElementToBeRemoved; } export interface RenderComponentOptions { diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index b7bfcc6f..55604a19 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -10,6 +10,7 @@ import { FireObject, getQueriesForElement, prettyDOM, + waitForDomChange, waitForElement, waitForElementToBeRemoved, } from '@testing-library/dom'; @@ -119,6 +120,17 @@ export async function render( return result; }; + function componentWaitForDomChange(options?: { + container?: HTMLElement; + timeout?: number; + mutationObserverOptions?: MutationObserverInit; + }): Promise { + const interval = setInterval(detectChanges, 10); + return waitForDomChange({ container: fixture.nativeElement, ...options }).finally(() => + clearInterval(interval), + ); + } + function componentWaitForElement( callback: () => Result, options?: { @@ -157,6 +169,7 @@ export async function render( debug: (element = fixture.nativeElement) => console.log(prettyDOM(element)), type: createType(eventsWithDetectChanges), selectOptions: createSelectOptions(eventsWithDetectChanges), + waitForDomChange: componentWaitForDomChange, waitForElement: componentWaitForElement, waitForElementToBeRemoved: componentWaitForElementToBeRemoved, ...getQueriesForElement(fixture.nativeElement, queries), diff --git a/projects/testing-library/tests/wait-for-dom-change.spec.ts b/projects/testing-library/tests/wait-for-dom-change.spec.ts new file mode 100644 index 00000000..300295b7 --- /dev/null +++ b/projects/testing-library/tests/wait-for-dom-change.spec.ts @@ -0,0 +1,40 @@ +import { Component, OnInit } from '@angular/core'; +import { render } from '../src/public_api'; +import { timer } from 'rxjs'; + +@Component({ + selector: 'fixture', + template: ` +
One
+
Two
+ `, +}) +class FixtureComponent implements OnInit { + oneVisible = false; + twoVisible = false; + + ngOnInit() { + timer(200).subscribe(() => (this.oneVisible = true)); + timer(400).subscribe(() => (this.twoVisible = true)); + } +} + +test('waits for the DOM to change', async () => { + const { queryByTestId, getByTestId, waitForDomChange } = await render(FixtureComponent); + + await waitForDomChange(); + + getByTestId('block-one'); + expect(queryByTestId('block-two')).toBeNull(); + + await waitForDomChange(); + + getByTestId('block-one'); + getByTestId('block-two'); +}); + +test('allows to override options', async () => { + const { waitForDomChange } = await render(FixtureComponent); + + await expect(waitForDomChange({ timeout: 100 })).rejects.toThrow(/Timed out in waitForDomChange/i); +});