aurelia2-testing-library
v0.2.0
Published
A React Testing Library-inspired testing helper for Aurelia 2.
Maintainers
Readme
aurelia2-testing-library
A React Testing Library-inspired helper for Aurelia 2 apps, built on top of
@aurelia/testing and @testing-library/dom.
The goal is simple: render Aurelia components with almost no boilerplate, query the page the way a user would, interact through realistic async user events, and only reach for Aurelia internals when a test truly needs them.
Install
npm i -D aurelia2-testing-library @aurelia/testing@rc @aurelia/platform-browser@rc @aurelia/kernel@rc @aurelia/runtime-html@rc @testing-library/domTest Setup
If your runner already provides a DOM, such as Vitest or Jest with jsdom, set up the Aurelia platform once in your test setup file:
import { afterEach } from 'vitest';
import { installAureliaTestEnvironment } from 'aurelia2-testing-library';
installAureliaTestEnvironment({ afterEach });For Node's built-in test runner with a manual jsdom window:
import { afterEach } from 'node:test';
import { JSDOM } from 'jsdom';
import { installAureliaTestEnvironment } from 'aurelia2-testing-library';
const dom = new JSDOM('<!doctype html><html><body></body></html>', {
pretendToBeVisual: true,
});
installAureliaTestEnvironment({
window: dom.window,
copyGlobals: true,
afterEach,
});Cleanup is registered automatically when a global afterEach exists. You can
also call enableAutoCleanup(afterEach), disableAutoCleanup(), or cleanup()
manually.
Quick Start
import { CustomElement } from '@aurelia/runtime-html';
import { setupComponent, screen, waitFor } from 'aurelia2-testing-library';
class HelloWorld {
name = 'Aurelia';
}
const HelloWorldElement = CustomElement.define({
name: 'hello-world',
template: `
<label>Name<input value.bind="name"></label>
<p>Hello, \${name}!</p>
`,
}, HelloWorld);
it('renders and updates', async () => {
const { user } = await setupComponent(HelloWorldElement);
await user.type(screen.getByLabelText('Name'), ' Testing');
await waitFor(() => {
expect(screen.getByText('Hello, Aurelia Testing!')).toBeTruthy();
});
});Everyday API
setup(template, options)
Creates a userEvent instance before rendering, then returns it with the render
result.
const { user, component } = await setup(
`<button click.trigger="save()">Save</button>`,
{ component: SaveForm }
);
await user.click(screen.getByRole('button', { name: 'Save' }));The user returned by setup() / setupComponent() automatically flushes
Aurelia's task queues after every interaction, the same way React Testing
Library wraps user-event in act(). That means bound state and the DOM are
already up to date once the call resolves, so you usually don't need waitFor
for synchronous bindings:
const { user, getByRole, getByTestId } = await setup(
`<button click.trigger="count++">Add</button><span data-testid="count">\${count}</span>`,
{ component: class { count = 0; } }
);
await user.click(getByRole('button', { name: 'Add' }));
expect(getByTestId('count').textContent).toBe('1'); // no waitFor neededPass user: { settle: () => {} } to opt out, or user: existingUserInstance to
reuse one you created yourself. Still reach for waitFor / findBy* when work
is genuinely async (timeouts, promises, network).
setupComponent(component, options)
Render a CustomElement as the root without passing its template yourself.
const { user, component } = await setupComponent(ProfileEditor, {
props: { name: 'Dwayne' },
});createRender(defaults)
Create a reusable render preset for app-wide providers, wrappers, fixtures, and custom queries.
const appRender = createRender({
wrapper: '<main aria-label="Test harness"><slot></slot></main>',
registrations: [MockApi],
queries: {
getByDataState(container, state) {
const el = container.querySelector(`[data-state="${state}"]`);
if (!el) throw new Error(`No data-state="${state}" element`);
return el;
},
},
});
const { user } = await appRender.setup('<profile-form></profile-form>', {
component: ProfileForm,
});
screen.getByDataState('ready');The returned preset is callable and also exposes .setup(), .component(),
.setupComponent(), .attribute(), and .valueConverter().
Extensions
Use defineExtension() to add project-specific test helpers without forking the
library or repeating setup code in every test.
import { createRender, defineExtension } from 'aurelia2-testing-library';
const withAuthTools = defineExtension({
name: 'auth-tools',
beforeRender({ options }) {
options.props = {
...(options.props ?? {}),
currentUser: { name: 'Test User' },
};
options.wrapper = '<main aria-label="App shell"><slot></slot></main>';
},
extend(result) {
return {
getAppShell: () => result.getByLabelText('App shell'),
expectSignedIn: () => result.getByText('Test User'),
};
},
cleanup(result) {
result.container.dispatchEvent(new CustomEvent('test:cleanup'));
},
});
const appRender = createRender({
registrations: [MockApi],
extensions: [withAuthTools],
});
const result = await appRender('<profile-menu></profile-menu>', {
component: ProfileMenu,
});
result.expectSignedIn();
result.getAppShell();Extension hooks:
beforeRender(context): adjust render options before the fixture is created, or return a replacement template.extend(result, context): return helper methods/properties to merge onto the render result.cleanup(result, context): release extension resources duringunmount()or globalcleanup().
Extension helpers cannot overwrite built-in result properties such as unmount,
rerender, or Testing Library queries. Add extensions directly to a single
render() call or to a reusable createRender() preset.
render(template, options)
Creates and starts an Aurelia fixture, returning DOM Testing Library queries plus Aurelia-specific handles.
const result = await render('<my-app></my-app>', {
component: MyApp,
registrations: [MyService],
props: { mode: 'edit' },
wrapper: '<section aria-label="Test shell"><slot></slot></section>',
appConfig: { allowActionlessForm: true },
});
result.component; // root view-model instance
result.fixture; // @aurelia/testing fixture
result.container; // <app> element that Aurelia renders into
result.host; // wrapper div created by @aurelia/testing
result.baseElement; // query/screen base
await result.rerender({ mode: 'read' });
await result.unmount();Options:
component: root component class/instance.props: initial values assigned to the root component.wrapper: string or function wrapper. String wrappers use<slot></slot>,<slot />, or{{children}}.registrations: DI registrations for the fixture container.autoStart: iffalse, create the fixture without starting; callresult.start().ctx: customTestContext.appConfig: forwarded to Aurelia app root.rootElementDef: partial root element definition override.attachTo: element to move the fixture host into.baseElement: element used byscreenand bound queries.queries: app-specific DOM Testing Library queries merged with the defaults.extensions: project-specific render extensions created withdefineExtension().
renderAttribute(attribute, options)
Render and inspect a custom attribute without hand-writing the host fixture.
const result = await renderAttribute(HighlightAttribute, {
attributeBinding: 'color',
component: class {
color = 'red';
},
host: 'button',
hostAttrs: { type: 'button', 'aria-label': 'Swatch' },
content: 'Marked',
});
result.target; // host element
result.attribute; // custom attribute view-model
result.attributeController; // Aurelia custom attribute controllerUse attributeBinding for .bind expressions and attributeValue for raw
attribute values.
renderValueConverter(converter, options)
Render a value converter through Aurelia binding so you can assert the DOM and still inspect the converter instance.
class CurrencyValueConverter {
toView(value: number, currency = 'USD') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
}).format(value);
}
}
const result = await renderValueConverter(CurrencyValueConverter, {
name: 'currency',
value: 12,
args: "'AUD'",
host: 'output',
hostAttrs: { 'aria-label': 'Price' },
});
result.target; // rendered host element
result.converter; // value converter instancePass a ValueConverter.define(...) resource directly, or pass an undecorated
class with name. Use expression when the converter input should come from a
custom root property instead of the default value, and use args for raw
Aurelia argument expressions.
screen
screen is bound to the most recently rendered fixture's baseElement, then
falls back to document.body when nothing is mounted. It includes DOM Testing
Library queries plus screen.debug() and screen.logTestingPlaygroundURL().
userEvent
Use setup() / setupComponent() for most tests. For standalone DOM tests, use
the default instance or create one explicitly:
const user = userEvent.setup({ delay: 5 });
await user.click(screen.getByRole('button', { name: 'Save' }));
await user.check(screen.getByLabelText('Subscribe'));
await user.uncheck(screen.getByLabelText('Subscribe'));
await user.type(screen.getByLabelText('Name'), 'Aurelia');
await user.selectAll(screen.getByLabelText('Name'));
await user.tab();
await user.paste(screen.getByLabelText('Bio'), 'Hello!');
await user.selectOptions(screen.getByLabelText('Role'), 'Admin');
await user.upload(screen.getByLabelText('Avatar'), new File(['x'], 'avatar.png'));Available helpers:
click,check,uncheck,dblClick,tripleClick,rightClickhover,unhover,focus,blur,tab,pointerselectAll,type,keyboard,clear,paste,copy,cutselectOptions,deselectOptions,upload
Keyboard modifiers
type() and keyboard() understand the {Modifier>}…{/Modifier} press-and-hold
syntax for Shift, Control (alias Ctrl), Alt (alias Option), and Meta
(aliases Command, Cmd, Os). Held modifiers set the matching
shiftKey/ctrlKey/altKey/metaKey flags on dispatched events, uppercase
letters while Shift is down, and suppress text insertion for Ctrl/Alt/Meta
chords:
await user.type(screen.getByLabelText('Title'), '{Shift>}hello{/Shift}'); // "HELLO"
await user.keyboard('{Control>}a{/Control}'); // Ctrl+A, no text
await user.type(input, '{Shift>}{ArrowRight}{ArrowRight}{/Shift}'); // shift-selectcopy() / cut() read the current text selection (input/textarea range or the
document selection for contenteditable), dispatch the matching clipboard event,
and return the copied text. cut() also removes the selected text.
Aurelia Helpers
settle() and act()
Flush Aurelia task queues and surrounding microtasks.
await act(() => {
result.component.count += 1;
});
await result.settle();spyOnEvent(target, type)
Record DOM events without wiring throwaway arrays in every test.
const submitted = spyOnEvent(form, 'submit');
const nextSubmit = submitted.next();
await user.click(screen.getByRole('button', { name: 'Save' }));
expect(await nextSubmit).toBe(submitted.lastEvent);
expect(submitted.count).toBe(1);
submitted.dispose();Re-exports
From DOM Testing Library:
fireEvent, within, waitFor, waitForElementToBeRemoved,
getQueriesForElement, queries, queryHelpers, buildQueries, prettyDOM,
prettyFormat, logDOM, logRoles, getRoles, getNodeText,
getDefaultNormalizer, getSuggestedQuery, and configure, plus the common
matcher/query/waitForOptions types.
From @aurelia/testing:
createFixture, ensureTaskQueuesEmpty, setPlatform, TestContext, and the
IFixture type.
Notes
- Prefer
screenqueries anduserEventover direct DOM selectors and manual events. - Prefer
setupComponent()for custom elements, andrender()for inline test templates or shell/wrapper scenarios. - Use
waitFor,findBy*,settle, oractafter interactions that trigger asynchronous Aurelia binding or task-queue work.
