UNPKG

@testing-library/angular

Version:
531 lines (524 loc) 23 kB
import * as i0 from '@angular/core'; import { NgZone, ChangeDetectorRef, ApplicationInitStatus, isStandalone, SimpleChange, Component } from '@angular/core'; import { TestBed, DeferBlockBehavior, tick } from '@angular/core/testing'; import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { configure as configure$1, getQueriesForElement, prettyDOM, waitFor as waitFor$1, waitForElementToBeRemoved as waitForElementToBeRemoved$1, screen as screen$1, within as within$1 } from '@testing-library/dom'; export { buildQueries, createEvent, findAllByAltText, findAllByDisplayValue, findAllByLabelText, findAllByPlaceholderText, findAllByRole, findAllByTestId, findAllByText, findAllByTitle, findByAltText, findByDisplayValue, findByLabelText, findByPlaceholderText, findByRole, findByTestId, findByText, findByTitle, fireEvent, getAllByAltText, getAllByDisplayValue, getAllByLabelText, getAllByPlaceholderText, getAllByRole, getAllByTestId, getAllByText, getAllByTitle, getByAltText, getByDisplayValue, getByLabelText, getByPlaceholderText, getByRole, getByTestId, getByText, getByTitle, getDefaultNormalizer, getElementError, getNodeText, getQueriesForElement, getRoles, isInaccessible, logDOM, logRoles, prettyDOM, queries, queryAllByAltText, queryAllByAttribute, queryAllByDisplayValue, queryAllByLabelText, queryAllByPlaceholderText, queryAllByRole, queryAllByTestId, queryAllByText, queryAllByTitle, queryByAltText, queryByAttribute, queryByDisplayValue, queryByLabelText, queryByPlaceholderText, queryByRole, queryByTestId, queryByText, queryByTitle, queryHelpers } from '@testing-library/dom'; /** * @description * Creates an aliased input branded type with a value * */ function aliasedInput(alias, value) { return { [alias]: value }; } let config = { dom: {}, defaultImports: [], }; function configure(newConfig) { if (typeof newConfig === 'function') { // Pass the existing config out to the provided function // and accept a delta in return newConfig = newConfig(config); } // Merge the incoming config delta config = { ...config, ...newConfig, }; } function getConfig() { return config; } const mountedFixtures = new Set(); const safeInject = TestBed.inject || TestBed.get; async function render(sut, renderOptions = {}) { const { dom: domConfig, ...globalConfig } = getConfig(); const { detectChangesOnRender = true, autoDetectChanges = true, declarations = [], imports = [], providers = [], schemas = [], queries, wrapper = WrapperComponent, componentProperties = {}, componentInputs = {}, componentOutputs = {}, inputs: newInputs = {}, on = {}, componentProviders = [], childComponentOverrides = [], componentImports, excludeComponentDeclaration = false, routes = [], removeAngularAttributes = false, defaultImports = [], initialRoute = '', deferBlockStates = undefined, deferBlockBehavior = undefined, configureTestBed = () => { /* noop*/ }, } = { ...globalConfig, ...renderOptions }; configure$1({ eventWrapper: (cb) => { const result = cb(); if (autoDetectChanges) { detectChangesForMountedFixtures(); } return result; }, ...domConfig, }); TestBed.configureTestingModule({ declarations: addAutoDeclarations(sut, { declarations, excludeComponentDeclaration, wrapper, }), imports: addAutoImports(sut, { imports: imports.concat(defaultImports), routes, }), providers: [...providers], schemas: [...schemas], deferBlockBehavior: deferBlockBehavior ?? DeferBlockBehavior.Manual, }); overrideComponentImports(sut, componentImports); overrideChildComponentProviders(childComponentOverrides); configureTestBed(TestBed); await TestBed.compileComponents(); // Angular supports nested arrays of providers, so we need to flatten them to emulate the same behavior. for (const { provide, ...provider } of componentProviders.flat(Infinity)) { TestBed.overrideProvider(provide, provider); } const componentContainer = createComponentFixture(sut, wrapper); const zone = safeInject(NgZone); const router = safeInject(Router); const _navigate = async (elementOrPath, basePath = '') => { const href = typeof elementOrPath === 'string' ? elementOrPath : elementOrPath.getAttribute('href'); const [path, params] = (basePath + href).split('?'); const queryParams = params ? params.split('&').reduce((qp, q) => { const [key, value] = q.split('='); const currentValue = qp[key]; if (typeof currentValue === 'undefined') { qp[key] = value; } else if (Array.isArray(currentValue)) { qp[key] = [...currentValue, value]; } else { qp[key] = [currentValue, value]; } return qp; }, {}) : undefined; const navigateOptions = queryParams ? { queryParams, } : undefined; const doNavigate = () => { return navigateOptions ? router?.navigate([path], navigateOptions) : router?.navigate([path]); }; let result; if (zone) { await zone.run(() => { result = doNavigate(); }); } else { result = doNavigate(); } return result ?? false; }; if (initialRoute) await _navigate(initialRoute); if (typeof router?.initialNavigation === 'function') { if (zone) { zone.run(() => router.initialNavigation()); } else { router.initialNavigation(); } } let detectChanges; const allInputs = { ...componentInputs, ...newInputs }; let renderedPropKeys = Object.keys(componentProperties); let renderedInputKeys = Object.keys(allInputs); let renderedOutputKeys = Object.keys(componentOutputs); let subscribedOutputs = []; const renderFixture = async (properties, inputs, outputs, subscribeTo) => { const createdFixture = await createComponent(componentContainer); setComponentProperties(createdFixture, properties); setComponentInputs(createdFixture, inputs); setComponentOutputs(createdFixture, outputs); subscribedOutputs = subscribeToComponentOutputs(createdFixture, subscribeTo); if (removeAngularAttributes) { createdFixture.nativeElement.removeAttribute('ng-version'); const idAttribute = createdFixture.nativeElement.getAttribute('id'); if (idAttribute?.startsWith('root')) { createdFixture.nativeElement.removeAttribute('id'); } } mountedFixtures.add(createdFixture); let isAlive = true; createdFixture.componentRef.onDestroy(() => { isAlive = false; }); if (hasOnChangesHook(createdFixture.componentInstance) && Object.keys(properties).length > 0) { const changes = getChangesObj(null, componentProperties); createdFixture.componentInstance.ngOnChanges(changes); } detectChanges = () => { if (isAlive) { createdFixture.detectChanges(); } }; if (detectChangesOnRender) { detectChanges(); } return createdFixture; }; const fixture = await renderFixture(componentProperties, allInputs, componentOutputs, on); if (deferBlockStates) { if (Array.isArray(deferBlockStates)) { for (const deferBlockState of deferBlockStates) { await renderDeferBlock(fixture, deferBlockState.deferBlockState, deferBlockState.deferBlockIndex); } } else { await renderDeferBlock(fixture, deferBlockStates); } } const rerender = async (properties) => { const newComponentInputs = { ...properties?.componentInputs, ...properties?.inputs }; const changesInComponentInput = update(fixture, renderedInputKeys, newComponentInputs, setComponentInputs, properties?.partialUpdate ?? false); renderedInputKeys = Object.keys(newComponentInputs); const newComponentOutputs = properties?.componentOutputs ?? {}; for (const outputKey of renderedOutputKeys) { if (!Object.prototype.hasOwnProperty.call(newComponentOutputs, outputKey)) { delete fixture.componentInstance[outputKey]; } } setComponentOutputs(fixture, newComponentOutputs); renderedOutputKeys = Object.keys(newComponentOutputs); // first unsubscribe the no longer available or changed callback-fns const newObservableSubscriptions = properties?.on ?? {}; for (const [key, cb, subscription] of subscribedOutputs) { // when no longer provided or when the callback has changed if (!(key in newObservableSubscriptions) || cb !== newObservableSubscriptions[key]) { subscription.unsubscribe(); } } // then subscribe the new callback-fns subscribedOutputs = Object.entries(newObservableSubscriptions).map(([key, cb]) => { const existing = subscribedOutputs.find(([k]) => k === key); return existing && existing[1] === cb ? existing // nothing to do : subscribeToComponentOutput(fixture, key, cb); }); const newComponentProps = properties?.componentProperties ?? {}; const changesInComponentProps = update(fixture, renderedPropKeys, newComponentProps, setComponentProperties, properties?.partialUpdate ?? false); renderedPropKeys = Object.keys(newComponentProps); if (hasOnChangesHook(fixture.componentInstance)) { fixture.componentInstance.ngOnChanges({ ...changesInComponentInput, ...changesInComponentProps, }); } if (properties?.detectChangesOnRender !== false) { fixture.componentRef.injector.get(ChangeDetectorRef).detectChanges(); } }; const navigate = async (elementOrPath, basePath = '') => { const result = await _navigate(elementOrPath, basePath); detectChanges(); return result; }; return { fixture, detectChanges: () => detectChanges(), navigate, rerender, renderDeferBlock: async (deferBlockState, deferBlockIndex) => { await renderDeferBlock(fixture, deferBlockState, deferBlockIndex); }, debugElement: fixture.debugElement, container: fixture.nativeElement, debug: (element = fixture.nativeElement, maxLength, options) => { if (Array.isArray(element)) { for (const e of element) { console.log(prettyDOM(e, maxLength, options)); } } else { console.log(prettyDOM(element, maxLength, options)); } }, ...replaceFindWithFindAndDetectChanges(getQueriesForElement(fixture.nativeElement, queries)), }; } async function createComponent(component) { /* Make sure angular application is initialized before creating component */ await safeInject(ApplicationInitStatus).donePromise; return TestBed.createComponent(component); } function createComponentFixture(sut, wrapper) { if (typeof sut === 'string') { TestBed.overrideTemplate(wrapper, sut); return wrapper; } return sut; } function setComponentProperties(fixture, componentProperties = {}) { for (const key of Object.keys(componentProperties)) { const descriptor = Object.getOwnPropertyDescriptor(fixture.componentInstance.constructor.prototype, key); let _value = componentProperties[key]; const defaultGetter = () => _value; const extendedSetter = (value) => { _value = value; descriptor?.set?.call(fixture.componentInstance, _value); fixture.detectChanges(); }; Object.defineProperty(fixture.componentInstance, key, { get: descriptor?.get || defaultGetter, set: extendedSetter, // Allow the property to be defined again later. // This happens when the component properties are updated after initial render. // For Jest this is `true` by default, for Karma and a real browser the default is `false` configurable: true, }); descriptor?.set?.call(fixture.componentInstance, _value); } return fixture; } function setComponentOutputs(fixture, componentOutputs = {}) { for (const [name, value] of Object.entries(componentOutputs)) { fixture.componentInstance[name] = value; } } function setComponentInputs(fixture, componentInputs = {}) { for (const [name, value] of Object.entries(componentInputs)) { fixture.componentRef.setInput(name, value); } } function subscribeToComponentOutputs(fixture, listeners) { // with Object.entries we lose the type information of the key and callback, therefore we need to cast them return Object.entries(listeners).map(([key, cb]) => subscribeToComponentOutput(fixture, key, cb)); } function subscribeToComponentOutput(fixture, key, cb) { const eventEmitter = fixture.componentInstance[key]; const subscription = eventEmitter.subscribe(cb); fixture.componentRef.onDestroy(subscription.unsubscribe.bind(subscription)); return [key, cb, subscription]; } function overrideComponentImports(sut, imports) { if (imports) { if (typeof sut === 'function' && isStandalone(sut)) { TestBed.overrideComponent(sut, { set: { imports } }); } else { throw new Error(`Error while rendering ${sut}: Cannot specify componentImports on a template or non-standalone component.`); } } } function overrideChildComponentProviders(componentOverrides) { if (componentOverrides) { for (const { component, providers } of componentOverrides) { TestBed.overrideComponent(component, { set: { providers } }); } } } function hasOnChangesHook(componentInstance) { return (componentInstance !== null && typeof componentInstance === 'object' && 'ngOnChanges' in componentInstance && typeof componentInstance.ngOnChanges === 'function'); } function getChangesObj(oldProps, newProps) { const isFirstChange = oldProps === null; return Object.keys(newProps).reduce((changes, key) => { changes[key] = new SimpleChange(isFirstChange ? null : oldProps[key], newProps[key], isFirstChange); return changes; }, {}); } function update(fixture, prevRenderedKeys, newValues, updateFunction, partialUpdate) { const componentInstance = fixture.componentInstance; const simpleChanges = {}; if (!partialUpdate) { for (const key of prevRenderedKeys) { if (!Object.prototype.hasOwnProperty.call(newValues, key)) { simpleChanges[key] = new SimpleChange(componentInstance[key], undefined, false); delete componentInstance[key]; } } } for (const [key, value] of Object.entries(newValues)) { if (value !== componentInstance[key]) { simpleChanges[key] = new SimpleChange(componentInstance[key], value, false); } } updateFunction(fixture, newValues); return simpleChanges; } function addAutoDeclarations(sut, { declarations = [], excludeComponentDeclaration, wrapper, }) { const nonStandaloneDeclarations = declarations?.filter((d) => !isStandalone(d)); if (typeof sut === 'string') { if (wrapper && isStandalone(wrapper)) { return nonStandaloneDeclarations; } return [...nonStandaloneDeclarations, wrapper]; } const components = () => (excludeComponentDeclaration || isStandalone(sut) ? [] : [sut]); return [...nonStandaloneDeclarations, ...components()]; } function addAutoImports(sut, { imports = [], routes }) { const animations = () => { const animationIsDefined = imports.indexOf(NoopAnimationsModule) > -1 || imports.indexOf(BrowserAnimationsModule) > -1; return animationIsDefined ? [] : [NoopAnimationsModule]; }; const routing = () => (routes ? [RouterTestingModule.withRoutes(routes)] : []); const components = () => (typeof sut !== 'string' && isStandalone(sut) ? [sut] : []); return [...imports, ...components(), ...animations(), ...routing()]; } async function renderDeferBlock(fixture, deferBlockState, deferBlockIndex) { const deferBlockFixtures = await fixture.getDeferBlocks(); if (deferBlockIndex !== undefined) { if (deferBlockIndex < 0) { throw new Error('deferBlockIndex must be a positive number'); } const deferBlockFixture = deferBlockFixtures[deferBlockIndex]; if (!deferBlockFixture) { throw new Error(`Could not find a deferrable block with index '${deferBlockIndex}'`); } await deferBlockFixture.render(deferBlockState); } else { for (const deferBlockFixture of deferBlockFixtures) { await deferBlockFixture.render(deferBlockState); } } } /** * Wrap waitFor to invoke the Angular change detection cycle before invoking the callback */ async function waitForWrapper(detectChanges, callback, options) { let inFakeAsync = true; try { tick(0); } catch { inFakeAsync = false; } return await waitFor$1(() => { setTimeout(() => detectChanges(), 0); if (inFakeAsync) { tick(0); } return callback(); }, options); } /** * Wrap waitForElementToBeRemovedWrapper to poke the Angular change detection cycle before invoking the callback */ async function waitForElementToBeRemovedWrapper(detectChanges, callback, options) { let cb; if (typeof callback !== 'function') { const elements = (Array.isArray(callback) ? callback : [callback]); 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()).find(Boolean); } else { cb = callback; } return await waitForElementToBeRemoved$1(() => { const result = cb(); detectChanges(); return result; }, options); } function cleanup() { mountedFixtures.forEach(cleanupAtFixture); } function cleanupAtFixture(fixture) { fixture.destroy(); if (!fixture.nativeElement.getAttribute('ng-version') && fixture.nativeElement.parentNode === document.body) { document.body.removeChild(fixture.nativeElement); } else if (!fixture.nativeElement.getAttribute('id') && document.body.children?.[0] === fixture.nativeElement) { document.body.removeChild(fixture.nativeElement); } mountedFixtures.delete(fixture); } // if we're running in a test runner that supports afterEach // then we'll automatically run cleanup afterEach test // this ensures that tests run in isolation from each other // if you don't like this, set the ATL_SKIP_AUTO_CLEANUP env variable to 'true' if (typeof process === 'undefined' || !process.env?.ATL_SKIP_AUTO_CLEANUP) { if (typeof afterEach === 'function') { afterEach(() => { cleanup(); }); } } class WrapperComponent { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WrapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: WrapperComponent, isStandalone: false, selector: "atl-wrapper-component", ngImport: i0, template: '', isInline: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WrapperComponent, decorators: [{ type: Component, args: [{ selector: 'atl-wrapper-component', template: '', standalone: false }] }] }); /** * Wrap findBy queries to poke the Angular change detection cycle */ function replaceFindWithFindAndDetectChanges(originalQueriesForContainer) { return Object.keys(originalQueriesForContainer).reduce((newQueries, key) => { const getByQuery = originalQueriesForContainer[key.replace('find', 'get')]; if (key.startsWith('find') && getByQuery) { newQueries[key] = async (...queryOptions) => { const waitOptions = queryOptions.length === 3 ? queryOptions.pop() : undefined; // original implementation at https://github.com/testing-library/dom-testing-library/blob/main/src/query-helpers.js return await waitForWrapper(detectChangesForMountedFixtures, () => getByQuery(...queryOptions), waitOptions); }; } else { newQueries[key] = originalQueriesForContainer[key]; } return newQueries; }, {}); } /** * Call detectChanges for all fixtures */ function detectChangesForMountedFixtures() { for (const fixture of mountedFixtures) { try { fixture.detectChanges(); } catch (err) { if (!err.message.startsWith('ViewDestroyedError')) { throw err; } } } } /** * Re-export screen with patched queries */ const screen = replaceFindWithFindAndDetectChanges(screen$1); /** * Re-export within with patched queries */ const within = (element, queriesToBind) => { const container = within$1(element, queriesToBind); return replaceFindWithFindAndDetectChanges(container); }; /** * Re-export waitFor with patched waitFor */ async function waitFor(callback, options) { return waitForWrapper(detectChangesForMountedFixtures, callback, options); } /** * Re-export waitForElementToBeRemoved with patched waitForElementToBeRemoved */ async function waitForElementToBeRemoved(callback, options) { return waitForElementToBeRemovedWrapper(detectChangesForMountedFixtures, callback, options); } /* * Public API Surface of testing-library */ /** * Generated bundle index. Do not edit. */ export { aliasedInput, configure, getConfig, render, screen, waitFor, waitForElementToBeRemoved, within }; //# sourceMappingURL=testing-library-angular.mjs.map