mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-07-30 03:09:50 +08:00
* Migrating Core Salesforce Skills * Updating pr comments * updat reference * Updating a skill * Migrating Datacloud skills * Migrating Industries cloud skills * Validating - skills fixing --------- Co-authored-by: Sandip Kumar Yadav <sandipkumar.yadav+sfemu@salesforce.com>
372 lines
16 KiB
Plaintext
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();
|
|
});
|
|
});
|
|
});
|