/** * 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//__tests__/.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 & \"Quotes\"" } ]; getRecords.mockResolvedValue(specialRecords); const element = createComponent({ recordId: '001xx000000000' }); await flushPromises(); const name = element.shadowRoot.querySelector('.record-name'); expect(name.textContent).toContain("Test's "); }); 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(); }); }); });