@testing-library/angular
Version:
Test your Angular components with the dom-testing-library
531 lines (524 loc) • 23 kB
JavaScript
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