diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index c171a4a1..00231def 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -9,8 +9,8 @@ import { FireObject, getQueriesForElement, prettyDOM, - waitFor, - waitForElementToBeRemoved, + waitFor as dtlWaitFor, + waitForElementToBeRemoved as dtlWaitForElementToBeRemoved, fireEvent as dtlFireEvent, screen as dtlScreen, queries as dtlQueries, @@ -153,38 +153,22 @@ export async function render( container?: HTMLElement; timeout?: number; interval?: number; - mutationObserverOptions?: { - subtree: boolean; - childList: boolean; - attributes: boolean; - characterData: boolean; - }; + mutationObserverOptions?: MutationObserverInit; } = { container: fixture.nativeElement }, ): Promise { - return waitFor(() => { - detectChanges(); - return callback(); - }, options); + return waitForWrapper(detectChanges, callback, options); } function componentWaitForElementToBeRemoved( - callback: () => T, + callback: (() => T) | T, options: { container?: HTMLElement; timeout?: number; interval?: number; - mutationObserverOptions?: { - subtree: boolean; - childList: boolean; - attributes: boolean; - characterData: boolean; - }; + mutationObserverOptions?: MutationObserverInit; } = { container: fixture.nativeElement }, ): Promise { - return waitForElementToBeRemoved(() => { - detectChanges(); - return callback(); - }, options); + return waitForElementToBeRemovedWrapper(detectChanges, callback, options); } return { @@ -266,28 +250,57 @@ function addAutoImports({ imports, routes }: Pick, ' return [...imports, ...animations(), ...routing()]; } -// for the findBy queries we first want to run a change detection cycle -function replaceFindWithFindAndDetectChanges(container: HTMLElement, originalQueriesForContainer: T): T { - return Object.keys(originalQueriesForContainer).reduce( - (newQueries, key) => { - if (key.startsWith('find')) { - const getByQuery = dtlQueries[key.replace('find', 'get')]; - newQueries[key] = async (text, options, waitForOptions) => { - // original implementation at https://github.com/testing-library/dom-testing-library/blob/master/src/query-helpers.js - const result = await waitFor(() => { - detectChangesForMountedFixtures(); - return getByQuery(container, text, options); - }, waitForOptions); - return result; - }; - } else { - newQueries[key] = originalQueriesForContainer[key]; +/** + * Wrap waitFor to poke the Angular change detection cycle before invoking the callback + */ +async function waitForWrapper( + detectChanges: () => void, + callback: () => T, + options?: { + container?: HTMLElement; + timeout?: number; + interval?: number; + mutationObserverOptions?: MutationObserverInit; + }, +): Promise { + return await dtlWaitFor(() => { + detectChanges(); + return callback(); + }, options); +} + +/** + * Wrap waitForElementToBeRemovedWrapper to poke the Angular change detection cycle before invoking the callback + */ +async function waitForElementToBeRemovedWrapper( + detectChanges: () => void, + callback: (() => T) | T, + options?: { + container?: HTMLElement; + timeout?: number; + interval?: number; + mutationObserverOptions?: MutationObserverInit; + }, +): Promise { + let cb; + if (typeof callback !== 'function') { + const elements = (Array.isArray(callback) ? callback : [callback]) as HTMLElement[]; + const getRemainingElements = elements.map(element => { + let parent = element.parentElement; + while (parent.parentElement) { + parent = parent.parentElement; } + return () => (parent.contains(element) ? element : null); + }); + cb = () => getRemainingElements.map(c => c()).filter(Boolean); + } else { + cb = callback; + } - return newQueries; - }, - {} as T, - ); + return await dtlWaitForElementToBeRemoved(() => { + detectChanges(); + return cb(); + }, options); } function cleanup() { @@ -307,11 +320,43 @@ if (typeof afterEach === 'function' && !process.env.ATL_SKIP_AUTO_CLEANUP) { }); } +/** + * Wrap findBy queries to poke the Angular change detection cycle + */ +function replaceFindWithFindAndDetectChanges(container: HTMLElement, originalQueriesForContainer: T): T { + return Object.keys(originalQueriesForContainer).reduce( + (newQueries, key) => { + if (key.startsWith('find')) { + const getByQuery = dtlQueries[key.replace('find', 'get')]; + newQueries[key] = async (text, options, waitForOptions) => { + // original implementation at https://github.com/testing-library/dom-testing-library/blob/master/src/query-helpers.js + const result = await waitForWrapper( + detectChangesForMountedFixtures, + () => getByQuery(container, text, options), + waitForOptions, + ); + return result; + }; + } else { + newQueries[key] = originalQueriesForContainer[key]; + } + + return newQueries; + }, + {} as T, + ); +} + +/** + * Call detectChanges for all fixtures + */ function detectChangesForMountedFixtures() { mountedFixtures.forEach(fixture => fixture.detectChanges()); } -// wrap dom-fireEvent with a change detection cycle +/** + * Wrap dom-fireEvent to poke the Angular change detection cycle after an event is fired + */ const fireEvent = Object.keys(dtlFireEvent).reduce( (events, key) => { events[key] = (element: HTMLElement, options?: {}) => { @@ -324,18 +369,55 @@ const fireEvent = Object.keys(dtlFireEvent).reduce( {} as typeof dtlFireEvent, ); +/** + * Re-export screen with patched queries + */ const screen = replaceFindWithFindAndDetectChanges(document.body, dtlScreen); -// wrap user-events with the correct fireEvents +/** + * Re-export waitFor with patched waitFor + */ +async function waitFor( + callback: () => T, + options?: { + container?: HTMLElement; + timeout?: number; + interval?: number; + mutationObserverOptions?: MutationObserverInit; + }, +): Promise { + return waitForWrapper(detectChangesForMountedFixtures, callback, options); +} + +/** + * Re-export waitForElementToBeRemoved with patched waitForElementToBeRemoved + */ +async function waitForElementToBeRemoved( + callback: (() => T) | T, + options?: { + container?: HTMLElement; + timeout?: number; + interval?: number; + mutationObserverOptions?: MutationObserverInit; + }, +): Promise { + return waitForElementToBeRemovedWrapper(detectChangesForMountedFixtures, callback, options); +} + +/** + * Re-export userEvent with the patched fireEvent + */ const userEvent = { type: createType(fireEvent), selectOptions: createSelectOptions(fireEvent), tab: tab, }; -// manually export otherwise we get the following error while running Jest tests -// TypeError: Cannot set property fireEvent of [object Object] which has only a getter -// exports.fireEvent = fireEvent; +/** + * Manually export otherwise we get the following error while running Jest tests + * TypeError: Cannot set property fireEvent of [object Object] which has only a getter + * exports.fireEvent = fireEvent + */ export { buildQueries, configure, @@ -401,12 +483,7 @@ export { queryAllByAttribute, queryByAttribute, queryHelpers, - wait, - waitFor, - waitForDomChange, - waitForElement, - waitForElementToBeRemoved, within, } from '@testing-library/dom'; -export { fireEvent, screen, userEvent }; +export { fireEvent, screen, userEvent, waitFor, waitForElementToBeRemoved }; 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 index b68e629d..39b6fa68 100644 --- 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 @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { render } from '../src/public_api'; +import { render, screen, waitForElementToBeRemoved as waitForElementToBeRemovedATL } from '../src/public_api'; import { timer } from 'rxjs'; @Component({ @@ -15,18 +15,54 @@ class FixtureComponent implements OnInit { } } -test('waits for element to be removed', async () => { - const { queryByTestId, getByTestId, waitForElementToBeRemoved } = await render(FixtureComponent); +describe('from import', () => { + test('waits for element to be removed (callback)', async () => { + await render(FixtureComponent); - await waitForElementToBeRemoved(() => getByTestId('im-here')); + await waitForElementToBeRemovedATL(() => screen.getByTestId('im-here')); - expect(queryByTestId('im-here')).toBeNull(); + expect(screen.queryByTestId('im-here')).toBeNull(); + }); + + test('waits for element to be removed (element)', async () => { + await render(FixtureComponent); + + await waitForElementToBeRemovedATL(screen.getByTestId('im-here')); + + expect(screen.queryByTestId('im-here')).toBeNull(); + }); + + test('allows to override options', async () => { + await render(FixtureComponent); + + await expect(waitForElementToBeRemovedATL(() => screen.getByTestId('im-here'), { timeout: 200 })).rejects.toThrow( + /Timed out in waitForElementToBeRemoved/i, + ); + }); }); +describe('from render', () => { + test('waits for element to be removed (callback)', async () => { + const { queryByTestId, getByTestId, waitForElementToBeRemoved } = await render(FixtureComponent); + + await waitForElementToBeRemoved(() => getByTestId('im-here')); + + expect(queryByTestId('im-here')).toBeNull(); + }); + + test('waits for element to be removed (element)', async () => { + const { queryByTestId, getByTestId, waitForElementToBeRemoved } = await render(FixtureComponent); + + const node = getByTestId('im-here'); + await waitForElementToBeRemoved(node); + + expect(queryByTestId('im-here')).toBeNull(); + }); -test('allows to override options', async () => { - const { getByTestId, waitForElementToBeRemoved } = await render(FixtureComponent); + 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, - ); + await expect(waitForElementToBeRemoved(() => getByTestId('im-here'), { timeout: 200 })).rejects.toThrow( + /Timed out in waitForElementToBeRemoved/i, + ); + }); }); diff --git a/projects/testing-library/tests/wait-for.spec.ts b/projects/testing-library/tests/wait-for.spec.ts index 5f36280b..24cd4e5b 100644 --- a/projects/testing-library/tests/wait-for.spec.ts +++ b/projects/testing-library/tests/wait-for.spec.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { timer } from 'rxjs'; -import { render } from '../src/public_api'; +import { render, screen, fireEvent, waitFor as waitForATL } from '../src/public_api'; @Component({ selector: 'fixture', @@ -17,23 +17,48 @@ class FixtureComponent { } } -test('waits for assertion to become true', async () => { - const { queryByText, getByTestId, click, waitFor, getByText } = await render(FixtureComponent); +describe('from import', () => { + test('waits for assertion to become true', async () => { + await render(FixtureComponent); - expect(queryByText('Success')).toBeNull(); + expect(screen.queryByText('Success')).toBeNull(); - click(getByTestId('button')); + fireEvent.click(screen.getByTestId('button')); - await waitFor(() => getByText('Success')); - getByText('Success'); + await waitForATL(() => screen.getByText('Success')); + screen.getByText('Success'); + }); + + test('allows to override options', async () => { + await render(FixtureComponent); + + fireEvent.click(screen.getByTestId('button')); + + await expect(waitForATL(() => screen.getByText('Success'), { timeout: 200 })).rejects.toThrow( + /Unable to find an element with the text: Success/i, + ); + }); }); -test('allows to override options', async () => { - const { getByTestId, click, waitFor, getByText } = await render(FixtureComponent); +describe('from render', () => { + test('waits for assertion to become true', async () => { + const { queryByText, getByTestId, click, waitFor, getByText } = await render(FixtureComponent); + + expect(queryByText('Success')).toBeNull(); + + click(getByTestId('button')); + + await waitFor(() => getByText('Success')); + getByText('Success'); + }); + + test('allows to override options', async () => { + const { getByTestId, click, waitFor, getByText } = await render(FixtureComponent); - click(getByTestId('button')); + click(getByTestId('button')); - await expect(waitFor(() => getByText('Success'), { timeout: 200 })).rejects.toThrow( - /Unable to find an element with the text: Success/i, - ); + await expect(waitFor(() => getByText('Success'), { timeout: 200 })).rejects.toThrow( + /Unable to find an element with the text: Success/i, + ); + }); });