diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index e6d69583..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 } 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 }; @@ -34,9 +42,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 +57,36 @@ 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 the DOM to change. * + * For more info see https://testing-library.com/docs/dom-testing-library/api-async#waitfordomchange */ - navigate: (elementOrPath: Element | string, basePath?: string) => Promise; + waitForDomChange: typeof waitForDomChange; + /** + * @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 + */ + 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 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 4917d62b..55604a19 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -4,7 +4,16 @@ 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, + waitForDomChange, + waitForElement, + waitForElementToBeRemoved, +} from '@testing-library/dom'; import { RenderComponentOptions, RenderDirectiveOptions, RenderResult } from './models'; import { createSelectOptions, createType } from './user-events'; @@ -111,6 +120,45 @@ 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?: { + 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 +169,9 @@ 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), ...eventsWithDetectChanges, }; 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); +}); 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,