mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
138 lines
6.8 KiB
TypeScript
138 lines
6.8 KiB
TypeScript
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import type { Mailbox } from '@/lib/jmap/types';
|
|
import type { InstalledPlugin } from '@/lib/plugin-types';
|
|
|
|
const createSlotInstance = vi.fn((..._args: unknown[]) => ({ destroy: vi.fn(), updateProps: vi.fn(), setTheme: vi.fn() }));
|
|
vi.mock('@/lib/plugin-sandbox/host-bridge', () => ({
|
|
createSlotInstance: (...args: unknown[]) => createSlotInstance(...args),
|
|
}));
|
|
vi.mock('@/lib/plugin-sandbox/host-theme', () => ({ snapshotHostTheme: () => ({}) }));
|
|
vi.mock('@/components/tour/tour-provider', () => ({
|
|
useTour: () => ({ startTour: vi.fn(), resetTourCompletion: vi.fn(), activeTour: null, endTour: vi.fn(), registerStep: vi.fn() }),
|
|
}));
|
|
vi.mock('@/contexts/drag-drop-context', () => ({
|
|
useDragDropContext: () => ({ isDragging: false }),
|
|
}));
|
|
vi.mock('@/hooks/use-mailbox-drop', () => ({
|
|
useMailboxDrop: () => ({ dropHandlers: {}, isValidDropTarget: false, isInvalidDropTarget: false }),
|
|
}));
|
|
vi.mock('@/hooks/use-tag-drop', () => ({
|
|
useTagDrop: () => ({ dropHandlers: {}, isValidDropTarget: false }),
|
|
}));
|
|
vi.mock('../account-switcher', () => ({ AccountSwitcher: () => null }));
|
|
|
|
const { Sidebar } = await import('../sidebar');
|
|
const registry = await import('@/lib/plugin-sandbox/registry');
|
|
const { dispatchApiCall } = await import('@/lib/plugin-sandbox/host-api');
|
|
const { clearSectionTitle } = await import('@/lib/plugin-sandbox/section-titles');
|
|
|
|
const mailboxes: Mailbox[] = [
|
|
{ id: 'inbox', name: 'Inbox', role: 'inbox', parentId: null, totalEmails: 0, unreadEmails: 0, myRights: { mayAddItems: true } },
|
|
] as unknown as Mailbox[];
|
|
|
|
function offer(id: string, name: string, slot: string, extraSlots: string[] = []) {
|
|
const plugin: InstalledPlugin = {
|
|
id, name, version: '1.0.0', author: '', description: '', type: 'ui-extension',
|
|
permissions: ['ui:sidebar-widget'], grantedPermissions: ['ui:sidebar-widget'], entrypoint: 'index.js',
|
|
enabled: true, status: 'running', settings: {},
|
|
};
|
|
registry.register({
|
|
plugin,
|
|
code: '',
|
|
tier: 'untrusted',
|
|
background: { evaluateShouldShow: vi.fn(async () => true) } as never,
|
|
slotOffers: [{ name: slot as never, order: 100, hasShouldShow: false }, ...extraSlots.map((name) => ({ name: name as never, order: 100, hasShouldShow: false }))],
|
|
hookDisposables: [],
|
|
});
|
|
}
|
|
|
|
beforeEach(() => {
|
|
createSlotInstance.mockClear();
|
|
localStorage.clear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
for (const p of registry.all()) registry.deregister(p.plugin.id);
|
|
clearSectionTitle('cal');
|
|
});
|
|
|
|
describe('Sidebar plugin sections', () => {
|
|
it('draws nothing when no plugin offers a section', () => {
|
|
offer('w', 'Widget only', 'sidebar-widget');
|
|
const { container } = render(<Sidebar mailboxes={mailboxes} selectedMailbox="inbox" />);
|
|
|
|
expect(container.querySelector('[data-plugin-slot="sidebar-section"]')).toBeNull();
|
|
expect(screen.queryByText('Widget only')).toBeNull();
|
|
});
|
|
|
|
it('gives each plugin a section headed by its name, below the folders, with the open folder', async () => {
|
|
offer('cal', 'Agenda', 'sidebar-section');
|
|
const { container } = render(<Sidebar mailboxes={mailboxes} selectedMailbox="inbox" />);
|
|
|
|
const header = screen.getByRole('button', { name: 'Agenda' });
|
|
expect(header).toHaveAttribute('aria-expanded', 'true');
|
|
const section = container.querySelector('[data-plugin-slot="sidebar-section"]')!;
|
|
// Below the folder list.
|
|
const inbox = container.querySelector('[data-mailbox-id="inbox"]')!;
|
|
expect(inbox.compareDocumentPosition(section) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
|
|
await waitFor(() => expect(createSlotInstance).toHaveBeenCalled());
|
|
const opts = createSlotInstance.mock.calls[0][0] as { slot: string; extraProps: Record<string, unknown>; plugin: { id: string } };
|
|
expect(opts.slot).toBe('sidebar-section');
|
|
expect(opts.plugin.id).toBe('cal');
|
|
expect(opts.extraProps).toEqual({ mailboxId: 'inbox' });
|
|
});
|
|
|
|
it('places inline plugin actions next to the collapsible heading, never inside its button', async () => {
|
|
offer('cal', 'Agenda', 'sidebar-section', ['sidebar-section-actions']);
|
|
const { container } = render(<Sidebar mailboxes={mailboxes} selectedMailbox="inbox" />);
|
|
await waitFor(() => expect(createSlotInstance).toHaveBeenCalledTimes(2));
|
|
|
|
const section = container.querySelector('[data-plugin-sidebar-section="cal"]')!;
|
|
const header = screen.getByRole('button', { name: 'Agenda' });
|
|
const actions = section.querySelector('[data-plugin-sidebar-actions="cal"]')!;
|
|
expect(actions).toBeInTheDocument();
|
|
expect(header.contains(actions)).toBe(false);
|
|
expect(header.parentElement).toBe(actions.parentElement);
|
|
expect(section.querySelector('[data-plugin-iframe-slot="cal:sidebar-section-actions"]')).toBeInTheDocument();
|
|
|
|
fireEvent.click(header);
|
|
expect(header).toHaveAttribute('aria-expanded', 'false');
|
|
expect(section.querySelector('[data-plugin-sidebar-actions="cal"]')).toBeNull();
|
|
});
|
|
|
|
it('folds a section and remembers it per plugin', async () => {
|
|
offer('cal', 'Agenda', 'sidebar-section');
|
|
const first = render(<Sidebar mailboxes={mailboxes} selectedMailbox="inbox" />);
|
|
await waitFor(() => expect(createSlotInstance).toHaveBeenCalledTimes(1));
|
|
|
|
act(() => { fireEvent.click(screen.getByRole('button', { name: 'Agenda' })); });
|
|
expect(screen.getByRole('button', { name: 'Agenda' })).toHaveAttribute('aria-expanded', 'false');
|
|
first.unmount();
|
|
|
|
createSlotInstance.mockClear();
|
|
render(<Sidebar mailboxes={mailboxes} selectedMailbox="inbox" />);
|
|
expect(screen.getByRole('button', { name: 'Agenda' })).toHaveAttribute('aria-expanded', 'false');
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
expect(createSlotInstance).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('heads a section with the title the plugin set, and with its name again after null', async () => {
|
|
offer('cal', 'Agenda', 'sidebar-section');
|
|
render(<Sidebar mailboxes={mailboxes} selectedMailbox="inbox" />);
|
|
const plugin = registry.get('cal')!.plugin;
|
|
|
|
await act(async () => { await dispatchApiCall(plugin, 'ui.setSectionTitle', ['Upcoming birthdays']); });
|
|
expect(screen.getByRole('button', { name: 'Upcoming birthdays' })).toBeInTheDocument();
|
|
expect(screen.queryByRole('button', { name: 'Agenda' })).toBeNull();
|
|
|
|
await expect(dispatchApiCall(plugin, 'ui.setSectionTitle', ['x'.repeat(41)])).rejects.toThrow(/at most 40/);
|
|
await act(async () => { await dispatchApiCall(plugin, 'ui.setSectionTitle', [null]); });
|
|
expect(screen.getByRole('button', { name: 'Agenda' })).toBeInTheDocument();
|
|
|
|
const noPerm = { ...plugin, id: 'other', permissions: [], grantedPermissions: [] };
|
|
await expect(dispatchApiCall(noPerm, 'ui.setSectionTitle', ['Mine'])).rejects.toThrow(/lacks permission "ui:sidebar-widget"/);
|
|
});
|
|
});
|