committed by
dermotduffy
parent
3fe5e4004a
commit
d833edb65b
@@ -1,33 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { dataToContext } from '../../../src/components-lib/notification/data-to-context';
|
||||
|
||||
describe('dataToContext', () => {
|
||||
it('should return an array of string items unchanged when input is an array of strings', () => {
|
||||
expect(dataToContext(['line one', 'line two'])).toEqual(['line one', 'line two']);
|
||||
});
|
||||
|
||||
it('should YAML-dump object items when input is an array containing objects', () => {
|
||||
const result = dataToContext([{ key: 'value' }]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toContain('key: value');
|
||||
});
|
||||
|
||||
it('should handle a mixed array of strings and objects', () => {
|
||||
const result = dataToContext(['plain string', { foo: 'bar' }]);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toBe('plain string');
|
||||
expect(result[1]).toContain('foo: bar');
|
||||
});
|
||||
|
||||
it('should return a single YAML-dumped string for a plain object', () => {
|
||||
const result = dataToContext({ error: 'something went wrong', code: 42 });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toContain('error: something went wrong');
|
||||
expect(result[0]).toContain('code: 42');
|
||||
});
|
||||
|
||||
it('should return an empty array for an empty array input', () => {
|
||||
expect(dataToContext([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -67,8 +67,7 @@ describe('createNotificationFromText', () => {
|
||||
const notification = createNotificationFromText('oops', {
|
||||
context: { detail: 'extra info' },
|
||||
});
|
||||
expect(notification.context).toBeDefined();
|
||||
expect(notification.context?.join(' ')).toContain('detail: extra info');
|
||||
expect(notification.context).toEqual([{ detail: 'extra info' }]);
|
||||
});
|
||||
|
||||
it('should omit context when not provided', () => {
|
||||
@@ -136,8 +135,7 @@ describe('createNotificationFromError', () => {
|
||||
const error = new AdvancedCameraCardError('boom', { reason: 'network' });
|
||||
const notification = createNotificationFromError(error);
|
||||
assert(notification);
|
||||
expect(notification.context).toBeDefined();
|
||||
expect(notification.context?.join(' ')).toContain('reason: network');
|
||||
expect(notification.context).toEqual([{ reason: 'network' }]);
|
||||
});
|
||||
|
||||
it('should use explicit context option over AdvancedCameraCardError context', () => {
|
||||
@@ -146,8 +144,7 @@ describe('createNotificationFromError', () => {
|
||||
context: { override: 'explicit' },
|
||||
});
|
||||
assert(notification);
|
||||
expect(notification.context?.join(' ')).toContain('override: explicit');
|
||||
expect(notification.context?.join(' ')).not.toContain('reason: network');
|
||||
expect(notification.context).toEqual([{ override: 'explicit' }]);
|
||||
});
|
||||
|
||||
it('should not include context when AdvancedCameraCardError has a non-object context', () => {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { Notification } from '../../../src/config/schema/actions/types';
|
||||
import { createLitElement, flushPromises } from '../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('NotificationContextController', () => {
|
||||
// Each test re-imports the controller after resetting the module registry so its
|
||||
// module-level `js-yaml` singleton starts unloaded.
|
||||
const loadController = async () => {
|
||||
const module = await import(
|
||||
'../../../src/components-lib/notification/notification-context-controller'
|
||||
);
|
||||
return module.NotificationContextController;
|
||||
};
|
||||
|
||||
const createNotification = (context?: Notification['context']): Notification => ({
|
||||
body: { text: 'oops' },
|
||||
...(context && { context }),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.doUnmock('js-yaml');
|
||||
});
|
||||
|
||||
it('should return an empty array when the notification has no context', async () => {
|
||||
const NotificationContextController = await loadController();
|
||||
const controller = new NotificationContextController(createLitElement());
|
||||
|
||||
expect(controller.getContext(createNotification())).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return string context items unchanged without loading the library', async () => {
|
||||
const NotificationContextController = await loadController();
|
||||
const host = createLitElement();
|
||||
const controller = new NotificationContextController(host);
|
||||
|
||||
expect(controller.getContext(createNotification(['line one', 'line two']))).toEqual([
|
||||
'line one',
|
||||
'line two',
|
||||
]);
|
||||
expect(host.requestUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should YAML-dump object context items once the library has loaded', async () => {
|
||||
const NotificationContextController = await loadController();
|
||||
const host = createLitElement();
|
||||
const controller = new NotificationContextController(host);
|
||||
const notification = createNotification([{ foo: 'bar' }]);
|
||||
|
||||
// The library is not yet loaded, so the first render defers.
|
||||
expect(controller.getContext(notification)).toEqual([]);
|
||||
|
||||
await vi.waitFor(() => expect(host.requestUpdate).toHaveBeenCalled());
|
||||
|
||||
const result = controller.getContext(notification);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toContain('foo: bar');
|
||||
});
|
||||
|
||||
it('should defer the whole context until the library loads when any item is an object', async () => {
|
||||
const NotificationContextController = await loadController();
|
||||
const host = createLitElement();
|
||||
const controller = new NotificationContextController(host);
|
||||
const notification = createNotification(['plain string', { foo: 'bar' }]);
|
||||
|
||||
expect(controller.getContext(notification)).toEqual([]);
|
||||
|
||||
await vi.waitFor(() => expect(host.requestUpdate).toHaveBeenCalled());
|
||||
|
||||
const result = controller.getContext(notification);
|
||||
expect(result[0]).toBe('plain string');
|
||||
expect(result[1]).toContain('foo: bar');
|
||||
});
|
||||
|
||||
it('should swallow a failed library load and render without the dumped context', async () => {
|
||||
vi.doMock('js-yaml', () => {
|
||||
throw new Error('chunk load failed');
|
||||
});
|
||||
const NotificationContextController = await loadController();
|
||||
const host = createLitElement();
|
||||
const controller = new NotificationContextController(host);
|
||||
const notification = createNotification([{ foo: 'bar' }]);
|
||||
|
||||
expect(controller.getContext(notification)).toEqual([]);
|
||||
|
||||
// Allow the rejected dynamic import to settle.
|
||||
await flushPromises();
|
||||
|
||||
expect(host.requestUpdate).not.toHaveBeenCalled();
|
||||
expect(controller.getContext(notification)).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { describe, expect, it, onTestFinished, vi } from 'vitest';
|
||||
|
||||
import { NotificationPopupController } from '../../../src/components-lib/notification/notification-popup-controller';
|
||||
import { POP_OUT_ANIMATION_NAME } from '../../../src/const';
|
||||
import { createLitElement } from '../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('NotificationPopupController', () => {
|
||||
const create = (getNotificationElement?: () => HTMLElement | null) => {
|
||||
const host = createLitElement();
|
||||
document.body.appendChild(host);
|
||||
const popup = document.createElement('div');
|
||||
|
||||
const controller = new NotificationPopupController(
|
||||
host,
|
||||
getNotificationElement ?? (() => popup),
|
||||
);
|
||||
controller.hostConnected();
|
||||
|
||||
// Cleanup (disconnecting the window listeners and clearing the DOM) is
|
||||
// registered per test, so leaked listeners cannot bleed into later tests.
|
||||
onTestFinished(() => {
|
||||
controller.hostDisconnected();
|
||||
document.body.replaceChildren();
|
||||
});
|
||||
|
||||
return { host, popup, controller };
|
||||
};
|
||||
|
||||
it('should add itself to the host', () => {
|
||||
const { host, controller } = create();
|
||||
expect(host.addController).toHaveBeenCalledWith(controller);
|
||||
});
|
||||
|
||||
describe('dismiss', () => {
|
||||
it('should mark the notification element as exiting', () => {
|
||||
const { popup, controller } = create();
|
||||
controller.dismiss();
|
||||
expect(popup.classList.contains('exiting')).toBe(true);
|
||||
});
|
||||
|
||||
it('should do nothing when there is no notification element', () => {
|
||||
const controller = new NotificationPopupController(createLitElement(), () => null);
|
||||
expect(() => controller.dismiss()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('outside interaction', () => {
|
||||
it('should dismiss on a click outside the host', () => {
|
||||
const { popup } = create();
|
||||
const outside = document.createElement('div');
|
||||
document.body.appendChild(outside);
|
||||
outside.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
||||
expect(popup.classList.contains('exiting')).toBe(true);
|
||||
});
|
||||
|
||||
it('should dismiss on a focus outside the host', () => {
|
||||
const { popup } = create();
|
||||
const outside = document.createElement('div');
|
||||
document.body.appendChild(outside);
|
||||
outside.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
expect(popup.classList.contains('exiting')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not dismiss on an interaction inside the host', () => {
|
||||
const { host, popup } = create();
|
||||
host.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
||||
expect(popup.classList.contains('exiting')).toBe(false);
|
||||
});
|
||||
|
||||
it('should stop listening once disconnected', () => {
|
||||
const { popup, controller } = create();
|
||||
controller.hostDisconnected();
|
||||
|
||||
const outside = document.createElement('div');
|
||||
document.body.appendChild(outside);
|
||||
outside.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
||||
expect(popup.classList.contains('exiting')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('keydown', () => {
|
||||
it('should dismiss and consume the Escape key', () => {
|
||||
const { popup } = create();
|
||||
const ev = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
document.body.dispatchEvent(ev);
|
||||
expect(popup.classList.contains('exiting')).toBe(true);
|
||||
expect(ev.defaultPrevented).toBe(true);
|
||||
});
|
||||
|
||||
it('should ignore other keys', () => {
|
||||
const { popup } = create();
|
||||
const ev = new KeyboardEvent('keydown', {
|
||||
key: 'a',
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
document.body.dispatchEvent(ev);
|
||||
expect(popup.classList.contains('exiting')).toBe(false);
|
||||
expect(ev.defaultPrevented).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('animation end', () => {
|
||||
// Dispatch a real `animationend` event on the element the handler is bound
|
||||
// to, so `target` and `currentTarget` are genuinely the same node.
|
||||
const dispatchAnimationEnd = (
|
||||
controller: NotificationPopupController,
|
||||
animationName: string,
|
||||
): void => {
|
||||
const element = document.createElement('div');
|
||||
element.addEventListener('animationend', controller.handleAnimationEnd);
|
||||
|
||||
const ev = new Event('animationend');
|
||||
Object.defineProperty(ev, 'animationName', { value: animationName });
|
||||
element.dispatchEvent(ev);
|
||||
};
|
||||
|
||||
it('should dispatch the dismiss event when the pop-out animation ends', () => {
|
||||
const { host, controller } = create();
|
||||
const dismissed = vi.fn();
|
||||
host.addEventListener('advanced-camera-card:notification:dismiss', dismissed);
|
||||
dispatchAnimationEnd(controller, POP_OUT_ANIMATION_NAME);
|
||||
expect(dismissed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should ignore other animations ending', () => {
|
||||
const { host, controller } = create();
|
||||
const dismissed = vi.fn();
|
||||
host.addEventListener('advanced-camera-card:notification:dismiss', dismissed);
|
||||
dispatchAnimationEnd(controller, 'pop-in');
|
||||
expect(dismissed).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user