Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ yarn-error.log*

# Dependency directories
node_modules/
package-lock.json

# Eslint cache
.eslintcache
Expand Down
Comment thread
ethandunzer marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,12 +1,54 @@
import { createElement } from 'lwc';
import ScheduledMaintenanceComponent from 'c/scheduledMaintenanceComponent';
import getActiveScheduledMaintenances from '@salesforce/apex/ScheduledMaintenanceService.getActiveScheduledMaintenances';
import getUserLocaleInfo from '@salesforce/apex/ScheduledMaintenanceService.getUserLocaleInfo';
import getAppIdByDeveloperName from '@salesforce/apex/ScheduledMaintenanceService.getAppIdByDeveloperName';

jest.mock(
'@salesforce/apex/ScheduledMaintenanceService.getActiveScheduledMaintenances',
() => ({ default: jest.fn() }),
{ virtual: true }
);
jest.mock(
'@salesforce/apex/ScheduledMaintenanceService.getUserLocaleInfo',
() => ({ default: jest.fn() }),
{ virtual: true }
);
jest.mock(
'@salesforce/apex/ScheduledMaintenanceService.getAppIdByDeveloperName',
() => ({ default: jest.fn() }),
{ virtual: true }
);

// Helper to flush all pending promises
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0));

const MOCK_MAINTENANCE = [
{
Id: 'a001',
Subject__c: 'Test Maintenance',
Description__c: 'Maintenance description',
Start_Date_Time__c: '2020-01-01T00:00:00.000Z',
End_Date_Time__c: '2099-12-31T23:59:59.000Z',
Dismissible__c: true,
Alert_Frequency__c: 'Every Visit',
Applicable_Apps__c: 'TestApp'
}
];

describe('c-scheduled-maintenance-component', () => {
beforeEach(() => {
getActiveScheduledMaintenances.mockResolvedValue(MOCK_MAINTENANCE);
getUserLocaleInfo.mockResolvedValue({ timeZone: 'UTC', locale: 'en-US' });
getAppIdByDeveloperName.mockResolvedValue('testAppId');
});

afterEach(() => {
// The jsdom instance is shared across test cases in a single file so reset the DOM
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
jest.clearAllMocks();
});

it('TODO: test case generated by CLI command, please fill in test logic', () => {
Expand All @@ -22,4 +64,58 @@ describe('c-scheduled-maintenance-component', () => {
// const div = element.shadowRoot.querySelector('div');
expect(1).toBe(1);
});

it('modal section has aria-labelledby and aria-describedby when open', async () => {
const element = createElement('c-scheduled-maintenance-component', {
is: ScheduledMaintenanceComponent
});
document.body.appendChild(element);
await flushPromises();

const section = element.shadowRoot.querySelector('section[role="dialog"]');
expect(section).not.toBeNull();
// LWC scopes IDs with a suffix, so check with toContain
expect(section.getAttribute('aria-labelledby')).toContain('modal-heading-01');
expect(section.getAttribute('aria-describedby')).toContain('modal-content-01');
expect(section.getAttribute('aria-modal')).toBe('true');
});

it('modal title h2 has id matching aria-labelledby', async () => {
const element = createElement('c-scheduled-maintenance-component', {
is: ScheduledMaintenanceComponent
});
document.body.appendChild(element);
await flushPromises();

// LWC scopes IDs with a suffix, so use an attribute contains selector
const heading = element.shadowRoot.querySelector('h2[id*="modal-heading-01"]');
expect(heading).not.toBeNull();
expect(heading.id).toContain('modal-heading-01');
});

it('modal content div has id matching aria-describedby', async () => {
const element = createElement('c-scheduled-maintenance-component', {
is: ScheduledMaintenanceComponent
});
document.body.appendChild(element);
await flushPromises();

// LWC scopes IDs with a suffix, so use an attribute contains selector
const contentDiv = element.shadowRoot.querySelector('div[id*="modal-content-01"]');
expect(contentDiv).not.toBeNull();
expect(contentDiv.id).toContain('modal-content-01');
});

it('pressing Escape key on the modal section does not throw an error', async () => {
const element = createElement('c-scheduled-maintenance-component', {
is: ScheduledMaintenanceComponent
});
document.body.appendChild(element);
await flushPromises();

const section = element.shadowRoot.querySelector('section[role="dialog"]');
expect(section).not.toBeNull();
const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true });
expect(() => section.dispatchEvent(escapeEvent)).not.toThrow();
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
<!-- sldsValidatorIgnore -->
<template>
<template if:true={isModalOpen}>
<section role="dialog" tabindex="-1" aria-modal="true" class="slds-modal slds-fade-in-open">
<section
role="dialog"
tabindex="-1"
aria-modal="true"
aria-labelledby="modal-heading-01"
aria-describedby="modal-content-01"
class="slds-modal slds-fade-in-open"
onkeydown={handleKeyDown}
>
<div class="slds-modal__container">
<header class="slds-modal__header">
<template if:true={isDismissible}>
Expand All @@ -12,15 +20,15 @@
</button>
</template>
</template>
<h2 class="slds-text-heading_large">{title}</h2>
<h2 id="modal-heading-01" class="slds-text-heading_large">{title}</h2>
<template if:false={isDismissible}>
<div class="slds-m-top_large">
<lightning-badge label="This App is Closed" class="slds-m-left_medium slds-badge slds-theme_warning"></lightning-badge>
</div>
<p class="slds-m-top_x-small">To see why this app is closed please review the below items.</p>
</template>
</header>
<div class="slds-modal__content slds-p-around_medium" style="overflow: auto;">
<div id="modal-content-01" class="slds-modal__content slds-p-around_medium" style="overflow: auto;">
<!-- Render accordion if isInMaintenance is true -->
<template if:true={isInMaintenance}>
<lightning-accordion allow-multiple-sections-open active-section-name={activeSectionName}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export default class ScheduledMaintenanceComponent extends NavigationMixin(Light
@track userTimeZone = null;
@track userLocale = null;
intervalId = null;
_previousIsModalOpen = false;
_focusableSelectors =
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';

// Lifecycle hook that's called after the component is inserted into the DOM.
connectedCallback() {
Expand Down Expand Up @@ -45,6 +48,39 @@ export default class ScheduledMaintenanceComponent extends NavigationMixin(Light
}
}

// Moves focus to the first focusable element when the modal opens
renderedCallback() {
if (this.isModalOpen && !this._previousIsModalOpen) {
const firstFocusable = this.template.querySelector(this._focusableSelectors);
if (firstFocusable) {
firstFocusable.focus();
}
}
this._previousIsModalOpen = this.isModalOpen;
}

// Handles keyboard events for Escape (dismiss) and Tab (focus trapping)
handleKeyDown(event) {
if (event.key === 'Escape' && this.isDismissible && !this.isFullLock) {
this.dismissAllRecords();
return;
}
if (event.key === 'Tab') {
const focusable = [...this.template.querySelectorAll(this._focusableSelectors)];
if (focusable.length === 0) return;
const firstEl = focusable[0];
const lastEl = focusable[focusable.length - 1];
const activeEl = this.template.activeElement;
if (event.shiftKey && activeEl === firstEl) {
event.preventDefault();
lastEl.focus();
} else if (!event.shiftKey && activeEl === lastEl) {
event.preventDefault();
firstEl.focus();
}
}
}

// Fetches the scheduled maintenances from Apex
fetchScheduledMaintenances() {
console.log('Fetching scheduled maintenances at', new Date().toLocaleString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: true }));
Expand Down