Testing Custom Elements
Basic Component Test
Let's break down a comprehensive example of testing a custom element:
import {StageComponent} from 'aurelia-testing';
import {bootstrap} from 'aurelia-bootstrapper';
describe('UserProfile Component', () => {
let component;
// Custom element to be tested
class UserProfile {
@bindable firstName;
@bindable lastName;
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
beforeEach(() => {
// Stage the component for testing
component = StageComponent
.withResources(PLATFORM.moduleName('user-profile'))
.inView('<user-profile first-name.bind="firstName" last-name.bind="lastName"></user-profile>')
.boundTo({
firstName: 'John',
lastName: 'Doe'
});
});
it('should render full name correctly', done => {
component.create(bootstrap).then(() => {
const nameElement = document.querySelector('.full-name');
expect(nameElement.textContent).toBe('John Doe');
done();
}).catch(done.fail);
});
afterEach(() => {
component.dispose();
});
});
Note the use of PLATFORM.moduleName()
for better compatibility with module loaders like Webpack.
Detailed Binding and Property Tests
Testing bindable properties in our component.
describe('UserProfile Bindable Properties', () => {
let component;
beforeEach(() => {
component = StageComponent
.withResources(PLATFORM.moduleName('user-profile'))
.inView(`
<user-profile
first-name.bind="firstName"
last-name.bind="lastName"
age.bind="age">
</user-profile>
`)
.boundTo({
firstName: 'Jane',
lastName: 'Smith',
age: 30
});
});
it('should update when bound properties change', done => {
component.create(bootstrap).then(() => {
// Initial state check
let firstNameElement = document.querySelector('.first-name');
let lastNameElement = document.querySelector('.last-name');
let ageElement = document.querySelector('.age');
expect(firstNameElement.textContent).toBe('Jane');
expect(lastNameElement.textContent).toBe('Smith');
expect(ageElement.textContent).toBe('30');
// Update bound properties
component.viewModel.firstName = 'John';
component.viewModel.lastName = 'Doe';
component.viewModel.age = 35;
// Wait for bindings to update
return new Promise(resolve => setTimeout(resolve, 50));
}).then(() => {
let firstNameElement = document.querySelector('.first-name');
let lastNameElement = document.querySelector('.last-name');
let ageElement = document.querySelector('.age');
expect(firstNameElement.textContent).toBe('John');
expect(lastNameElement.textContent).toBe('Doe');
expect(ageElement.textContent).toBe('35');
done();
}).catch(done.fail);
});
});
Lifecycle Method Testing
Here we manually control the lifecycle so we can decide when the lifecycle methods get fired.
describe('UserProfile Lifecycle', () => {
let component;
let lifecycleTracker;
beforeEach(() => {
// Create a mock view-model with lifecycle tracking
class UserProfile {
constructor() {
this.lifecycleTracker = [];
}
bind() {
this.lifecycleTracker.push('bind');
}
attached() {
this.lifecycleTracker.push('attached');
}
detached() {
this.lifecycleTracker.push('detached');
}
unbind() {
this.lifecycleTracker.push('unbind');
}
}
component = StageComponent
.withResources(PLATFORM.moduleName('user-profile'))
.inView('<user-profile></user-profile>')
.boundTo(new UserProfile());
});
it('should trigger lifecycle methods in correct order', done => {
// Use manuallyHandleLifecycle to control method invocation
component.manuallyHandleLifecycle().create(bootstrap)
.then(() => {
// Initial state - no lifecycle methods called
expect(component.viewModel.lifecycleTracker.length).toBe(0);
// Manually trigger bind
return component.bind();
})
.then(() => {
expect(component.viewModel.lifecycleTracker).toContain('bind');
// Manually trigger attached
return component.attached();
})
.then(() => {
expect(component.viewModel.lifecycleTracker).toContain('attached');
// Manually trigger detached
return component.detached();
})
.then(() => {
expect(component.viewModel.lifecycleTracker).toContain('detached');
// Manually trigger unbind
return component.unbind();
})
.then(() => {
expect(component.viewModel.lifecycleTracker).toContain('unbind');
done();
})
.catch(done.fail);
});
});
Complex Binding Scenarios
Testing two-way and computed bindings.
describe('Complex Binding Scenarios', () => {
let component;
beforeEach(() => {
class ComplexComponent {
@bindable firstName;
@bindable lastName;
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
updateFullName(newFullName) {
const [first, last] = newFullName.split(' ');
this.firstName = first;
this.lastName = last;
}
}
component = StageComponent
.withResources(PLATFORM.moduleName('complex-component'))
.inView(`
<complex-component
first-name.bind="firstName"
last-name.bind="lastName"
full-name.two-way="fullName">
</complex-component>
`)
.boundTo({
firstName: 'John',
lastName: 'Doe',
fullName: ''
});
});
it('should handle two-way binding and computed properties', done => {
component.create(bootstrap).then(() => {
// Test computed property
expect(component.viewModel.fullName).toBe('John Doe');
// Update individual properties
component.viewModel.firstName = 'Jane';
component.viewModel.lastName = 'Smith';
// Wait for binding to update
return new Promise(resolve => setTimeout(resolve, 50));
}).then(() => {
expect(component.viewModel.fullName).toBe('Jane Smith');
// Test two-way binding update
component.viewModel.updateFullName('Alice Johnson');
// Wait for binding to update
return new Promise(resolve => setTimeout(resolve, 50));
}).then(() => {
expect(component.viewModel.firstName).toBe('Alice');
expect(component.viewModel.lastName).toBe('Johnson');
done();
}).catch(done.fail);
});
});
Best Practices for Component Testing
Always use
PLATFORM.moduleName()
for resourcesDispose of components in
afterEach()
Use
done()
or return a Promise for async testsTest various binding scenarios
Mock external dependencies
Check both initial state and state after updates
Common Pitfalls to Avoid
Don't rely on implementation details
Avoid testing private methods
Use meaningful test descriptions
Handle async operations carefully
Clean up DOM between tests
Last updated
Was this helpful?