afv-library/skills/experience-lwc-generate/assets/jest-test/componentName.test.js.example

372 lines
16 KiB
Plaintext

/**
* JEST TEST TEMPLATE FOR LWC
*
* This template demonstrates Jest testing patterns for LWC:
* - Component rendering
* - Wire service mocking
* - Apex method mocking
* - Event handling
* - DOM queries
* - Async testing
*
* This file ships with a .example suffix so Jest does not auto-discover it
* inside installed skill folders. Copy it into your project and rename it to:
* force-app/main/default/lwc/<yourComponent>/__tests__/<yourComponent>.test.js
*
* Replace: componentName → yourComponentName
* Replace: ComponentName → YourComponentName
*/
import { createElement } from 'lwc';
import ComponentName from 'c/componentName';
// Mock Apex methods
import getRecords from '@salesforce/apex/ComponentController.getRecords';
import saveRecord from '@salesforce/apex/ComponentController.saveRecord';
// Mock wire adapters
import { getRecord } from 'lightning/uiRecordApi';
// ═══════════════════════════════════════════════════════════════════════════
// MOCK SETUP
// ═══════════════════════════════════════════════════════════════════════════
// Mock Apex methods
jest.mock(
'@salesforce/apex/ComponentController.getRecords',
() => ({ default: jest.fn() }),
{ virtual: true }
);
jest.mock(
'@salesforce/apex/ComponentController.saveRecord',
() => ({ default: jest.fn() }),
{ virtual: true }
);
// ═══════════════════════════════════════════════════════════════════════════
// TEST DATA
// ═══════════════════════════════════════════════════════════════════════════
const MOCK_RECORDS = [
{ Id: '001xx000003DGFAAA4', Name: 'Test Account 1', Industry: 'Technology' },
{ Id: '001xx000003DGFBBB4', Name: 'Test Account 2', Industry: 'Finance' },
{ Id: '001xx000003DGFCCC4', Name: 'Test Account 3', Industry: 'Healthcare' }
];
const MOCK_RECORD = {
Id: '001xx000003DGFAAA4',
Name: { value: 'Test Account' },
Industry: { value: 'Technology' },
Phone: { value: '555-1234' }
};
const MOCK_ERROR = {
body: { message: 'An error occurred' },
ok: false,
status: 500,
statusText: 'Internal Server Error'
};
// ═══════════════════════════════════════════════════════════════════════════
// HELPER FUNCTIONS
// ═══════════════════════════════════════════════════════════════════════════
/**
* Flush all pending promises in the event queue
*/
const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0));
/**
* Create component with optional public properties
*/
function createComponent(props = {}) {
const element = createElement('c-component-name', { is: ComponentName });
// Set public properties
Object.keys(props).forEach(key => {
element[key] = props[key];
});
document.body.appendChild(element);
return element;
}
// ═══════════════════════════════════════════════════════════════════════════
// TEST SUITE
// ═══════════════════════════════════════════════════════════════════════════
describe('c-component-name', () => {
// Reset DOM and mocks after each test
afterEach(() => {
// Remove all created elements
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
// Reset all mocks
jest.clearAllMocks();
});
// ═══════════════════════════════════════════════════════════════════════
// RENDERING TESTS
// ═══════════════════════════════════════════════════════════════════════
describe('rendering', () => {
it('renders component with default state', () => {
const element = createComponent();
// Check component is rendered
const card = element.shadowRoot.querySelector('lightning-card');
expect(card).not.toBeNull();
});
it('renders with custom title property', () => {
const element = createComponent({ title: 'Custom Title' });
const card = element.shadowRoot.querySelector('lightning-card');
expect(card.title).toBe('Custom Title');
});
it('displays loading spinner initially', () => {
const element = createComponent();
const spinner = element.shadowRoot.querySelector('lightning-spinner');
expect(spinner).not.toBeNull();
});
});
// ═══════════════════════════════════════════════════════════════════════
// DATA FETCHING TESTS
// ═══════════════════════════════════════════════════════════════════════
describe('data fetching', () => {
it('displays records when data is returned', async () => {
// Setup mock response
getRecords.mockResolvedValue(MOCK_RECORDS);
const element = createComponent({ recordId: '001xx000000000' });
// Wait for async operations
await flushPromises();
// Check records are displayed
const items = element.shadowRoot.querySelectorAll('.record-item');
expect(items.length).toBe(3);
});
it('displays error message when fetch fails', async () => {
// Setup mock error
getRecords.mockRejectedValue(MOCK_ERROR);
const element = createComponent({ recordId: '001xx000000000' });
await flushPromises();
// Check error is displayed
const error = element.shadowRoot.querySelector('.error-message');
expect(error).not.toBeNull();
expect(error.textContent).toContain('An error occurred');
});
it('displays empty state when no records returned', async () => {
getRecords.mockResolvedValue([]);
const element = createComponent({ recordId: '001xx000000000' });
await flushPromises();
const emptyState = element.shadowRoot.querySelector('.empty-state');
expect(emptyState).not.toBeNull();
});
});
// ═══════════════════════════════════════════════════════════════════════
// WIRE SERVICE TESTS
// ═══════════════════════════════════════════════════════════════════════
describe('wire service', () => {
it('fetches record data via wire adapter', async () => {
const element = createComponent({ recordId: '001xx000000000' });
// Emit wire data
getRecord.emit(MOCK_RECORD);
await flushPromises();
// Verify data is displayed
const nameField = element.shadowRoot.querySelector('.account-name');
expect(nameField.textContent).toBe('Test Account');
});
it('handles wire adapter error', async () => {
const element = createComponent({ recordId: '001xx000000000' });
// Emit error
getRecord.error(MOCK_ERROR);
await flushPromises();
const error = element.shadowRoot.querySelector('.error-message');
expect(error).not.toBeNull();
});
});
// ═══════════════════════════════════════════════════════════════════════
// EVENT HANDLING TESTS
// ═══════════════════════════════════════════════════════════════════════
describe('event handling', () => {
it('dispatches recordselected event on row click', async () => {
getRecords.mockResolvedValue(MOCK_RECORDS);
const element = createComponent({ recordId: '001xx000000000' });
await flushPromises();
// Create event handler mock
const handler = jest.fn();
element.addEventListener('recordselected', handler);
// Click on first record
const firstRow = element.shadowRoot.querySelector('.record-item');
firstRow.click();
// Verify event was dispatched
expect(handler).toHaveBeenCalled();
expect(handler.mock.calls[0][0].detail).toEqual({
recordId: '001xx000003DGFAAA4',
recordName: 'Test Account 1'
});
});
it('handles refresh button click', async () => {
getRecords.mockResolvedValue(MOCK_RECORDS);
const element = createComponent({ recordId: '001xx000000000' });
await flushPromises();
// Clear mock to track new calls
getRecords.mockClear();
// Click refresh
const refreshButton = element.shadowRoot.querySelector('lightning-button-icon[icon-name="utility:refresh"]');
refreshButton.click();
await flushPromises();
// Verify data was re-fetched
expect(getRecords).toHaveBeenCalled();
});
});
// ═══════════════════════════════════════════════════════════════════════
// FORM SUBMISSION TESTS
// ═══════════════════════════════════════════════════════════════════════
describe('form submission', () => {
it('saves record successfully', async () => {
saveRecord.mockResolvedValue({ Id: '001xx000000NEW', Name: 'New Account' });
const element = createComponent();
await flushPromises();
// Fill form fields
const nameInput = element.shadowRoot.querySelector('lightning-input[name="Name"]');
nameInput.value = 'New Account';
nameInput.dispatchEvent(new CustomEvent('change'));
// Submit form
const submitButton = element.shadowRoot.querySelector('lightning-button[type="submit"]');
submitButton.click();
await flushPromises();
// Verify save was called
expect(saveRecord).toHaveBeenCalledWith({
record: expect.objectContaining({ Name: 'New Account' })
});
});
it('displays validation error for required fields', async () => {
const element = createComponent();
await flushPromises();
// Submit without filling required fields
const submitButton = element.shadowRoot.querySelector('lightning-button[type="submit"]');
submitButton.click();
await flushPromises();
// Check validation message
const input = element.shadowRoot.querySelector('lightning-input[required]');
expect(input.reportValidity).toHaveBeenCalled;
});
});
// ═══════════════════════════════════════════════════════════════════════
// ACCESSIBILITY TESTS
// ═══════════════════════════════════════════════════════════════════════
describe('accessibility', () => {
it('has accessible button labels', async () => {
const element = createComponent();
await flushPromises();
const buttons = element.shadowRoot.querySelectorAll('lightning-button, lightning-button-icon');
buttons.forEach(button => {
// Each button should have label or alternative-text
const hasLabel = button.label || button.alternativeText;
expect(hasLabel).toBeTruthy();
});
});
it('announces loading state to screen readers', async () => {
const element = createComponent();
const spinner = element.shadowRoot.querySelector('lightning-spinner');
expect(spinner.alternativeText).toBe('Loading');
});
});
// ═══════════════════════════════════════════════════════════════════════
// EDGE CASES
// ═══════════════════════════════════════════════════════════════════════
describe('edge cases', () => {
it('handles null recordId gracefully', async () => {
const element = createComponent({ recordId: null });
await flushPromises();
// Should not crash
expect(element).toBeTruthy();
});
it('handles special characters in data', async () => {
const specialRecords = [
{ Id: '001xx000000001', Name: "Test's <Account> & \"Quotes\"" }
];
getRecords.mockResolvedValue(specialRecords);
const element = createComponent({ recordId: '001xx000000000' });
await flushPromises();
const name = element.shadowRoot.querySelector('.record-name');
expect(name.textContent).toContain("Test's <Account>");
});
it('handles rapid successive updates', async () => {
const element = createComponent();
await flushPromises();
// Simulate rapid changes
for (let i = 0; i < 10; i++) {
element.recordId = `001xx00000000${i}`;
}
await flushPromises();
// Component should still be stable
expect(element.shadowRoot.querySelector('lightning-card')).not.toBeNull();
});
});
});