mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 16:16:59 +00:00
121 lines
5.1 KiB
TypeScript
121 lines
5.1 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { fireEvent, renderHook } from '@testing-library/react';
|
|
import { toastWithUndo } from '../email-action-toast';
|
|
import { clearUndoHistory, MAX_UNDO_HISTORY, recordUndo, undoHistory, undoLatest } from '../undo-history';
|
|
import { useToastStore } from '@/stores/toast-store';
|
|
import { useSettingsStore } from '@/stores/settings-store';
|
|
import { useUndoShortcut, isUndoKey } from '@/hooks/use-undo-shortcut';
|
|
|
|
const toastTitles = () => useToastStore.getState().toasts.map((t) => t.title);
|
|
|
|
beforeEach(() => {
|
|
clearUndoHistory();
|
|
useToastStore.setState({ toasts: [] });
|
|
useSettingsStore.setState({ singleKeyShortcuts: true });
|
|
});
|
|
|
|
describe('undo history', () => {
|
|
it('undoes the newest action first, then the one before it, closing their toasts', () => {
|
|
const first = vi.fn();
|
|
const second = vi.fn();
|
|
toastWithUndo('Email archived', { label: 'Undo', onClick: first });
|
|
toastWithUndo('Email deleted', { label: 'Undo', onClick: second });
|
|
expect(toastTitles()).toEqual(['Email archived', 'Email deleted']);
|
|
|
|
undoLatest();
|
|
expect(second).toHaveBeenCalledTimes(1);
|
|
expect(first).not.toHaveBeenCalled();
|
|
expect(toastTitles()).toEqual(['Email archived']);
|
|
|
|
undoLatest();
|
|
expect(first).toHaveBeenCalledTimes(1);
|
|
expect(undoLatest()).toBeNull();
|
|
});
|
|
|
|
it('does not undo twice when the toast\'s Undo is used after Ctrl+Z', () => {
|
|
const run = vi.fn();
|
|
toastWithUndo('Email archived', { label: 'Undo', onClick: run });
|
|
const action = useToastStore.getState().toasts[0].action!;
|
|
undoLatest();
|
|
action.onClick();
|
|
expect(run).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('takes an action undone from its toast off the list', () => {
|
|
const run = vi.fn();
|
|
toastWithUndo('Email archived', { label: 'Undo', onClick: run });
|
|
useToastStore.getState().toasts[0].action!.onClick();
|
|
expect(run).toHaveBeenCalledTimes(1);
|
|
expect(undoHistory()).toHaveLength(0);
|
|
});
|
|
|
|
it(`keeps the last ${MAX_UNDO_HISTORY} actions`, () => {
|
|
for (let i = 0; i < MAX_UNDO_HISTORY + 3; i++) recordUndo(`action ${i}`, () => {});
|
|
const labels = undoHistory().map((e) => e.label);
|
|
expect(labels).toHaveLength(MAX_UNDO_HISTORY);
|
|
expect(labels[0]).toBe('action 3');
|
|
expect(labels.at(-1)).toBe(`action ${MAX_UNDO_HISTORY + 2}`);
|
|
});
|
|
|
|
it('tells screen readers how to undo', () => {
|
|
toastWithUndo('Email archived', { label: 'Undo', onClick: () => {}, hint: 'Press Ctrl+Z to undo.' });
|
|
expect(useToastStore.getState().toasts[0].srHint).toBe('Press Ctrl+Z to undo.');
|
|
});
|
|
});
|
|
|
|
describe('undo keys', () => {
|
|
const key = (init: KeyboardEventInit, target: EventTarget = document.body) => {
|
|
const e = new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init });
|
|
Object.defineProperty(e, 'target', { value: target });
|
|
return e;
|
|
};
|
|
const extra: HTMLElement[] = [];
|
|
afterEach(() => { for (const el of extra.splice(0)) el.remove(); });
|
|
|
|
it('takes Ctrl+Z and Cmd+Z, and z only with single-key shortcuts on', () => {
|
|
expect(isUndoKey(key({ code: 'KeyZ', key: 'z', ctrlKey: true }), false)).toBe(true);
|
|
expect(isUndoKey(key({ code: 'KeyZ', key: 'z', metaKey: true }), false)).toBe(true);
|
|
expect(isUndoKey(key({ code: 'KeyZ', key: 'z' }), true)).toBe(true);
|
|
expect(isUndoKey(key({ code: 'KeyZ', key: 'z' }), false)).toBe(false);
|
|
expect(isUndoKey(key({ code: 'KeyZ', key: 'Z', ctrlKey: true, shiftKey: true }), true)).toBe(false);
|
|
expect(isUndoKey(key({ code: 'KeyY', key: 'y', ctrlKey: true }), true)).toBe(false);
|
|
});
|
|
|
|
it('leaves Ctrl+Z to a text field', () => {
|
|
const run = vi.fn();
|
|
recordUndo('Email archived', run);
|
|
const input = document.createElement('input');
|
|
document.body.appendChild(input);
|
|
extra.push(input);
|
|
renderHook(() => useUndoShortcut({ enabled: true, signedIn: true }));
|
|
fireEvent.keyDown(input, { code: 'KeyZ', key: 'z', ctrlKey: true });
|
|
expect(run).not.toHaveBeenCalled();
|
|
fireEvent.keyDown(document.body, { code: 'KeyZ', key: 'z', ctrlKey: true });
|
|
expect(run).toHaveBeenCalled();
|
|
});
|
|
|
|
it('ignores the keys while a modal dialog is open', () => {
|
|
const dialog = document.createElement('div');
|
|
dialog.setAttribute('aria-modal', 'true');
|
|
document.body.appendChild(dialog);
|
|
extra.push(dialog);
|
|
expect(isUndoKey(key({ code: 'KeyZ', key: 'z', ctrlKey: true }), true)).toBe(false);
|
|
});
|
|
|
|
it('undoes from the keyboard in Mail, and says so when there is nothing left', () => {
|
|
const run = vi.fn();
|
|
toastWithUndo('Email archived', { label: 'Undo', onClick: run });
|
|
renderHook(() => useUndoShortcut({ enabled: true, signedIn: true }));
|
|
fireEvent.keyDown(document.body, { code: 'KeyZ', key: 'z', ctrlKey: true });
|
|
expect(run).toHaveBeenCalledTimes(1);
|
|
fireEvent.keyDown(document.body, { code: 'KeyZ', key: 'z', ctrlKey: true });
|
|
expect(toastTitles()).toContain('nothing_to_undo');
|
|
});
|
|
|
|
it('forgets the history when signed out', () => {
|
|
recordUndo('Email archived', () => {});
|
|
renderHook(() => useUndoShortcut({ enabled: true, signedIn: false }));
|
|
expect(undoHistory()).toHaveLength(0);
|
|
});
|
|
});
|