@carbon/ibm-products-web-components
Version:
Carbon for IBM Products Web Components
341 lines (338 loc) • 16.4 kB
JavaScript
/**
* Copyright IBM Corp. 2024
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { describe, it, expect, vi } from 'vitest';
import { fixture, html, oneEvent, elementUpdated } from '@open-wc/testing';
import './notification-panel.js';
import './notification.js';
import './notification-footer.js';
/**
* Copyright IBM Corp. 2025
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
const defaultProps = {
class: 'custom-class',
titleText: 'Notifications',
dismissAllLabel: 'Dismiss all',
doNotDisturbLabel: 'Do not disturb',
todayText: 'Today',
previousText: 'previous',
open: true,
dateTimeLocale: 'en-US',
};
const template = (props = defaultProps) => {
const timeStamp = new Date(new Date().getTime() - 30 * 1000);
return html `
<c4p-notification-panel
?open=${props.open}
title-text=${props.titleText}
dismiss-all-label=${props.dismissAllLabel}
donot-disturb-label=${props.doNotDisturbLabel}
today-text=${props.todayText}
previous-text=${props.previousText}
date-time-locale="${props.dateTimeLocale}"
>
<c4p-notification
slot="today"
@c4p-notification-dismiss=${() => {
console.log('dismiss notification');
}}
type="error"
?unread="true"
.timestamp=${timeStamp}
>
<h4 class="c4p--notifications-panel__notification" slot="title">
LogDNA cannot be reached.
</h4>
<div slot="description">Unable to communicate with LogDNA.</div>
</c4p-notification>
<c4p-notification
slot="previous"
@c4p-notification-dismiss=${() => {
console.log('dismiss notification');
}}
type="error"
?unread="true"
.timestamp=${timeStamp}
>
<h4 class="c4p--notifications-panel__notification" slot="title">
LogDNA cannot be reached.
</h4>
<div slot="description">Unable to communicate with LogDNA.</div>
</c4p-notification>
<c4p-notification
slot="previous"
@c4p-notification-dismiss=${() => {
console.log('dismiss notification');
}}
type="error"
?unread="true"
.timestamp=${timeStamp}
>
<h4 class="c4p--notifications-panel__notification" slot="title">
LogDNA cannot be reached.
</h4>
<div slot="description">Unable to communicate with LogDNA.</div>
</c4p-notification>
<c4p-notification-footer
slot="footer"
view-all-label="View all (10)"
@c4p-notification-view-all=${() => {
console.log('view all clicked');
}}
@c4p-notification-settings=${() => {
console.log('Settings clicked');
}}
></c4p-notification-footer>
</c4p-notification-panel>
`;
};
describe('c4p-notification-panel', () => {
it('should render the notification panel', async () => {
const panel = (await fixture(template()));
expect(panel === null || panel === void 0 ? void 0 : panel.open).toBeTruthy();
expect(panel).toBeDefined();
});
it('should have focus on Dismiss all button after render', async () => {
var _a, _b;
const panel = (await fixture(template()));
await new Promise((resolve) => requestAnimationFrame(resolve));
const dismissButton = (_a = panel.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.c4p--notifications-panel__dismiss-button');
expect((_b = panel.shadowRoot) === null || _b === void 0 ? void 0 : _b.activeElement).toBe(dismissButton);
});
it('should have titleText rendered inside the panel header', async () => {
var _a, _b;
const panel = (await fixture(template()));
const panelHeading = (_a = panel.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.c4p--notifications-panel__header');
expect((_b = panelHeading === null || panelHeading === void 0 ? void 0 : panelHeading.textContent) === null || _b === void 0 ? void 0 : _b.trim()).toBe(panel.titleText);
});
it('should have "dismissAllLabel" prop passed be the label for the dismiss all button', async () => {
var _a, _b;
const panel = (await fixture(template()));
const dismissButton = (_a = panel.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.c4p--notifications-panel__dismiss-button');
expect((_b = dismissButton === null || dismissButton === void 0 ? void 0 : dismissButton.textContent) === null || _b === void 0 ? void 0 : _b.trim()).toBe(panel.dismissAllLabel);
});
it('should have "doNotDisturbLabel" prop passed be the label for the Do not disturb toggle', async () => {
var _a, _b, _c;
const panel = (await fixture(template()));
const doNotDisturbToggle = (_a = panel.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.c4p--notifications-panel__do-not-disturb-toggle');
const toggleText = (_b = doNotDisturbToggle === null || doNotDisturbToggle === void 0 ? void 0 : doNotDisturbToggle.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('.cds--toggle__text');
expect((_c = toggleText === null || toggleText === void 0 ? void 0 : toggleText.textContent) === null || _c === void 0 ? void 0 : _c.trim()).toBe(panel.doNotDisturbLabel);
});
it('should have "todayText" prop passed be title for Today Section', async () => {
var _a, _b;
const panel = (await fixture(template()));
const todayTitle = (_a = panel.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.c4p--notifications-panel__time-section-label--today');
expect((_b = todayTitle === null || todayTitle === void 0 ? void 0 : todayTitle.textContent) === null || _b === void 0 ? void 0 : _b.trim()).toBe(panel.todayText);
});
it('should have "previousText" prop passed be title for Previous Section', async () => {
var _a, _b;
const panel = (await fixture(template()));
const previousTitle = (_a = panel.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.c4p--notifications-panel__time-section-label--previous');
expect((_b = previousTitle === null || previousTitle === void 0 ? void 0 : previousTitle.textContent) === null || _b === void 0 ? void 0 : _b.trim()).toBe(panel.previousText);
});
it('should emit "c4p-notification-click-outside" when clicking outside the panel', async () => {
const element = await fixture(html `
<div>
${template()}
<div id="outside"></div>
</div>
`);
const panel = element.querySelector('c4p-notification-panel');
const eventPromise = oneEvent(panel, 'c4p-notification-click-outside');
const outsideDiv = element.querySelector('#outside');
outsideDiv.click();
const event = await eventPromise;
expect(event).toBeDefined();
});
it('should emit "c4p-notification-click-outside" when pressing Escape', async () => {
const panel = (await fixture(template()));
await elementUpdated(panel);
const eventPromise = oneEvent(panel, 'c4p-notification-click-outside');
panel.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
const event = await eventPromise;
expect(event).toBeDefined();
});
it('should emit "c4p-notification-dismiss-all" when clicked on dismiss all button', async () => {
var _a;
const panel = (await fixture(template()));
const eventPromise = oneEvent(panel, 'c4p-notification-dismiss-all');
const dismissButton = (_a = panel.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.c4p--notifications-panel__dismiss-button');
dismissButton === null || dismissButton === void 0 ? void 0 : dismissButton.click();
const event = await eventPromise;
expect(event).toBeDefined();
});
it('should emit "c4p-notification-donot-disturb-change" when clicked on Do not disturb toggle', async () => {
var _a;
const panel = (await fixture(template()));
const eventPromise = oneEvent(panel, 'c4p-notification-donot-disturb-change');
const doNotDisturbToggle = (_a = panel.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.c4p--notifications-panel__do-not-disturb-toggle');
doNotDisturbToggle === null || doNotDisturbToggle === void 0 ? void 0 : doNotDisturbToggle.dispatchEvent(new CustomEvent('cds-toggle-changed', {
bubbles: true,
composed: true,
detail: {},
}));
const event = await eventPromise;
expect(event).toBeDefined();
});
it('should render c4p-notification inside the "today" slot', async () => {
var _a;
const panel = (await fixture(template()));
await elementUpdated(panel);
const todaySlot = (_a = panel.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('slot[name="today"]');
expect(todaySlot).toBeDefined();
const assignedElements = todaySlot.assignedElements({ flatten: true });
expect(assignedElements[0].tagName.toLowerCase()).toBe('c4p-notification');
});
it('should render c4p-notification inside the "previous" slot', async () => {
var _a;
const panel = (await fixture(template()));
await elementUpdated(panel);
const previousSlot = (_a = panel.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('slot[name="previous"]');
expect(previousSlot).toBeDefined();
const assignedElements = previousSlot.assignedElements({
flatten: true,
});
const isNotifications = assignedElements.every((el) => el.tagName.toLowerCase() === 'c4p-notification');
expect(isNotifications).toBe(true);
expect(assignedElements.length).toBeGreaterThan(0);
});
it('should render c4p-notification-footer inside the "footer" slot', async () => {
var _a;
const panel = (await fixture(template()));
const footerSlot = (_a = panel.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('slot[name="footer"]');
expect(footerSlot).toBeDefined();
const assignedElements = footerSlot.assignedElements({
flatten: true,
});
expect(assignedElements[0].tagName.toLowerCase()).toBe('c4p-notification-footer');
});
it('should update _providedLocale when dateTimeLocale changes', async () => {
const panel = (await fixture(template()));
panel.dateTimeLocale = 'fr-FR';
await panel.updateComplete;
// @ts-ignore
expect(panel._providedLocale).to.equal('fr-FR');
});
it('should correctly update _hasTodayContent and _hasPreviousContent based on slotted elements', async () => {
const panel = (await fixture(template()));
await panel.updateComplete;
// @ts-ignore
expect(panel._hasTodayContent).to.be.true;
// @ts-ignore
expect(panel._hasPreviousContent).to.be.true;
const previousItem = document.createElement('div');
previousItem.slot = 'previous';
previousItem.textContent = 'Previous item';
panel.appendChild(previousItem);
await panel.updateComplete;
// @ts-ignore
expect(panel._hasTodayContent).to.be.true;
const todayItem = panel.querySelector('[slot="today"]');
todayItem === null || todayItem === void 0 ? void 0 : todayItem.remove();
await panel.updateComplete;
// @ts-ignore
expect(panel._hasTodayContent).to.be.false;
});
it('should disconnect the mutation observer when disconnected', async () => {
const disconnectSpy = vi.fn();
const panel = (await fixture(template()));
// @ts-ignore
panel._mutationObserver = { disconnect: disconnectSpy };
panel.remove();
expect(disconnectSpy).toHaveBeenCalled();
});
it('should focus on trigger button when closed by clicking outside on non-actionable element', async () => {
const element = await fixture(html `
<div>
<button id="trigger-button">Trigger</button>
${template()}
<div id="outside"></div>
</div>
`);
const panel = element.querySelector('c4p-notification-panel');
const triggerButton = element.querySelector('#trigger-button');
const outsideDiv = element.querySelector('#outside');
panel.triggerButtonRef = triggerButton;
outsideDiv.click();
await elementUpdated(panel);
expect(document.activeElement).toBe(triggerButton);
});
it('should not focus on trigger button when closed by clicking outside on actionable element', async () => {
const element = await fixture(html `
<div>
<button id="trigger-button">Trigger</button>
${template()}
<div id="outside">
<button id="actionable-button">Actionable</button>
</div>
</div>
`);
const panel = element.querySelector('c4p-notification-panel');
const triggerButton = element.querySelector('#trigger-button');
const actionableButton = element.querySelector('#actionable-button');
panel.triggerButtonRef = triggerButton;
actionableButton.click();
await elementUpdated(panel);
expect(document.activeElement).not.toBe(triggerButton);
});
it('should render notifications empty state', async () => {
var _a;
const props = defaultProps;
const panel = await fixture(html ` <c4p-notification-panel
?open=${props.open}
title-text=${props.titleText}
dismiss-all-label=${props.dismissAllLabel}
donot-disturb-label=${props.doNotDisturbLabel}
today-text=${props.todayText}
previous-text=${props.previousText}
date-time-locale="${props.dateTimeLocale}"
></c4p-notification-panel>`);
const slot = (_a = panel.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector(`slot[name="empty-state"]`);
expect(slot).toBeTruthy();
});
it('should display the "emptyStateLabel" prop as the label for the empty state message', async () => {
var _a;
const props = defaultProps;
const panel = await fixture(html ` <c4p-notification-panel
?open=${props.open}
title-text=${props.titleText}
dismiss-all-label=${props.dismissAllLabel}
empty-state-label="No notifications yet!"
donot-disturb-label=${props.doNotDisturbLabel}
today-text=${props.todayText}
previous-text=${props.previousText}
date-time-locale="${props.dateTimeLocale}"
></c4p-notification-panel>`);
const emptyStateEl = (_a = panel.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector(`clabs-empty-state`);
expect(emptyStateEl).toBeTruthy();
expect(emptyStateEl === null || emptyStateEl === void 0 ? void 0 : emptyStateEl.getAttribute('subtitle')).to.equal('No notifications yet!');
});
it('should render custom empty state component', async () => {
var _a, _b;
const props = defaultProps;
const panel = await fixture(html ` <c4p-notification-panel
?open=${props.open}
title-text=${props.titleText}
dismiss-all-label=${props.dismissAllLabel}
empty-state-label="No notifications yet!"
donot-disturb-label=${props.doNotDisturbLabel}
today-text=${props.todayText}
previous-text=${props.previousText}
date-time-locale="${props.dateTimeLocale}"
>
<div slot="empty-state">
<span class="empty-state-message">empty state message</span>
</div>
</c4p-notification-panel>`);
const slot = (_a = panel.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector(`slot[name="empty-state"]`);
const node = slot.assignedNodes()[0];
expect((_b = node === null || node === void 0 ? void 0 : node.querySelector('.empty-state-message')) === null || _b === void 0 ? void 0 : _b.textContent).toBe('empty state message');
});
});
//# sourceMappingURL=notification-panel.test.js.map