mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 08:15:41 +00:00
The review of offline mode found ways to send a message twice, to lose unsent mail without a word, and to send mail without its signing, encryption or check plugin. This fixes them and the smaller findings: - each outbox message is taken for sending in one database transaction under a Web Lock per account; only a failure before the submission request (SendNotStartedError from the client) puts it back, any other one marks it "may have gone out"; read-backs after the submission no longer fail a send; a send of a closed tab turns uncertain after a minute - every sign-out the user didn't confirm names the deleted messages on the sign-in page, or in a toast when another account stays signed in - nothing is queued while an enabled plugin isn't running (after a start without the network none can); the composer says why - the outbox waits for a fresh policy and holds messages the rules have since made stricter (a required label, a required send check) - another tab switching offline mode off sticks: only switching it on may create the database - only attachments of kept messages are kept, filed with their message, deleted with it and under the cap between syncs; file names and types are sealed; folders that went away leave the scope, and a folder that couldn't be asked keeps its mail without holding the others back - a group scope that forbids offline mode waits until the account's groups are known; one server down no longer holds another's mail - an offline start only when the server is really unreachable, and the real sign-in follows by itself once it answers - the service worker files pages and chunks under the build that served them, and the shell list of another build isn't kept under this one - a raw send counts as the composer's only from the plugin running its takeover, for that identity - errors and refusals are in all languages; the banner tells "offline" from "server can't be reached"; outbox buttons name their message; changes and messages go in the order they were made
99 lines
4.4 KiB
TypeScript
99 lines
4.4 KiB
TypeScript
import 'fake-indexeddb/auto';
|
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const offline = vi.hoisted(() => ({ enableOffline: vi.fn(async () => {}), disableOffline: vi.fn(async () => {}) }));
|
|
vi.mock('@/lib/offline/offline-mode', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('@/lib/offline/offline-mode')>()),
|
|
enableOffline: offline.enableOffline,
|
|
disableOffline: offline.disableOffline,
|
|
}));
|
|
|
|
import { OfflineSettings } from '../offline-settings';
|
|
import { useOfflineStore } from '@/lib/offline/state';
|
|
import { useAuthStore } from '@/stores/auth-store';
|
|
import { usePolicyStore } from '@/stores/policy-store';
|
|
import { DEFAULT_ORGANISATION_POLICY } from '@/lib/admin/organisation-policy';
|
|
|
|
function setNoOffline(noOffline: boolean) {
|
|
usePolicyStore.setState({
|
|
loaded: true,
|
|
subject: null,
|
|
policy: { ...usePolicyStore.getState().policy, organisation: { ...DEFAULT_ORGANISATION_POLICY, devices: { ...DEFAULT_ORGANISATION_POLICY.devices, noOffline } } },
|
|
});
|
|
}
|
|
|
|
beforeEach(() => {
|
|
offline.enableOffline.mockClear();
|
|
offline.disableOffline.mockClear();
|
|
useAuthStore.setState({ activeAccountId: 'acc' });
|
|
useOfflineStore.setState({ enabled: [], outbox: [], pendingChanges: 0, syncing: [], lastSyncedAt: {} });
|
|
setNoOffline(false);
|
|
});
|
|
|
|
describe('Settings > Offline', () => {
|
|
it('is off until the user turns it on in this browser', () => {
|
|
render(<OfflineSettings />);
|
|
const toggle = screen.getByTestId('offline-switch');
|
|
expect(toggle.getAttribute('aria-checked')).toBe('false');
|
|
fireEvent.click(toggle);
|
|
expect(offline.enableOffline).toHaveBeenCalledWith('acc');
|
|
});
|
|
|
|
it('asks before turning off with unsent messages', () => {
|
|
useOfflineStore.setState({ enabled: ['acc'], outbox: [{ id: 1, account: 'acc', createdAt: Date.now(), subject: 'Hi', to: ['b@example.org'], status: 'waiting' }] });
|
|
render(<OfflineSettings />);
|
|
fireEvent.click(screen.getByTestId('offline-switch'));
|
|
expect(offline.disableOffline).not.toHaveBeenCalled();
|
|
expect(screen.getByText('off_confirm_title')).toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole('button', { name: 'off_confirm' }));
|
|
expect(offline.disableOffline).toHaveBeenCalledWith('acc');
|
|
});
|
|
|
|
it('hides the switch and says why when the organisation forbids it', () => {
|
|
setNoOffline(true);
|
|
render(<OfflineSettings />);
|
|
expect(screen.queryByTestId('offline-switch')).toBeNull();
|
|
expect(screen.getByTestId('offline-forbidden').textContent).toContain('forbidden');
|
|
});
|
|
|
|
it('follows a policy that changes while it is open', () => {
|
|
render(<OfflineSettings />);
|
|
expect(screen.getByTestId('offline-switch')).toBeInTheDocument();
|
|
act(() => setNoOffline(true));
|
|
expect(screen.queryByTestId('offline-switch')).toBeNull();
|
|
});
|
|
|
|
it('lists the outbox', () => {
|
|
useOfflineStore.setState({ outbox: [{ id: 1, account: 'acc', createdAt: Date.now(), subject: 'Quarterly', to: ['b@example.org'], status: 'failed', error: { text: 'Rejected' } }] });
|
|
render(<OfflineSettings />);
|
|
const rows = screen.getAllByTestId('offline-outbox-row');
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].textContent).toContain('Quarterly');
|
|
expect(rows[0].getAttribute('data-status')).toBe('failed');
|
|
expect(screen.getByRole('button', { name: 'retry_label' })).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('the offline banner', () => {
|
|
it('says the app is offline and opens the outbox in place', async () => {
|
|
const { OfflineBanner } = await import('../offline-banner');
|
|
useOfflineStore.setState({
|
|
enabled: ['acc'],
|
|
browserOffline: true,
|
|
outbox: [{ id: 1, account: 'acc', createdAt: Date.now(), subject: 'Quarterly', to: ['b@example.org'], status: 'waiting' }],
|
|
});
|
|
render(<OfflineBanner />);
|
|
expect(screen.getByTestId('offline-banner').textContent).toContain('banner.offline');
|
|
fireEvent.click(screen.getByTestId('offline-banner-outbox'));
|
|
expect(screen.getByTestId('offline-outbox-row').textContent).toContain('Quarterly');
|
|
});
|
|
|
|
it('stays away for an account that doesn\'t keep mail here', async () => {
|
|
const { OfflineBanner } = await import('../offline-banner');
|
|
useOfflineStore.setState({ enabled: [], browserOffline: true, outbox: [] });
|
|
render(<OfflineBanner />);
|
|
expect(screen.queryByTestId('offline-banner')).toBeNull();
|
|
});
|
|
});
|