Files
webmail/lib/offline/__tests__/queue-sync.test.ts
T
Linus Rath 8d802e4601 fix: decide on offline policy only with the whole policy and the user's groups, and ask before losing edits
A persisted account id with no subject yet read as decided: Everyone's
noOffline wiped mail a group scope allowed, and a wider scope went out at
every reload. A public, partial policy counted as the organisation's word,
and turned on the desktop default in a forbidding organisation.

- an organisation's cap on one file now binds a pin
- Clear and the off switch read what would be lost when asked, messages,
  changes and drafts, and Clear waits for a connection
- turning one account off survives the last one going off, and off then on
  no longer warns of an eviction
- a tab that takes over the store sends no scope until it has heard the policy
- a dropped account is judged by what applies to anyone
- a choice reaches the store once it settles; progress announces phases only
- restored Pro tabs ask for their titles
2026-11-03 12:33:53 +01:00

54 lines
2.4 KiB
TypeScript

import 'fake-indexeddb/auto';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { setStoreBackend } from '@/lib/store/client/api';
import { InProcessStore } from '@/lib/store/__tests__/helpers/in-process-store';
import { getOfflineHooks } from '@/lib/jmap/offline-hooks';
import { usePolicyStore } from '@/stores/policy-store';
import { useAccountStore } from '@/stores/account-store';
import { DEFAULT_ORGANISATION_POLICY } from '@/lib/admin/organisation-policy';
import { queueSend, readQueue } from '../queue';
import { registerOfflineHooks } from '../offline-mode';
import { writeEnabled } from '../state';
// Whatever goes into the outbox asks for a Background Sync, once it's stored.
const seen = vi.hoisted(() => ({ calls: 0, onCall: (() => {}) as () => void }));
vi.mock('@/lib/pwa-notify/outbox-sync', () => ({ requestOutboxSync: () => { seen.calls += 1; seen.onCall(); } }));
let store: InProcessStore;
beforeEach(() => {
store = new InProcessStore();
setStoreBackend(store);
seen.calls = 0;
seen.onCall = () => {};
usePolicyStore.setState({ loaded: true, subject: { email: 'alice@example.org', groups: [] }, policy: { ...usePolicyStore.getState().policy, organisation: DEFAULT_ORGANISATION_POLICY } });
useAccountStore.setState({ accounts: [{ id: 'acc', serverUrl: 'https://mail.example.org', username: 'alice@example.org' } as never] });
});
afterEach(() => {
setStoreBackend(null);
writeEnabled([]);
});
describe('the outbox', () => {
it('asks for a sync after storing a message, and still returns its id', async () => {
let storedWhenAsked = -1;
seen.onCall = () => { void readQueue('acc').then(({ outbox }) => { storedWhenAsked = outbox.length; }); };
const id = await queueSend('acc', { subject: 'S', to: [] }, { kind: 'email', args: [] });
expect(id).toBe(1);
expect(seen.calls).toBe(1);
await vi.waitFor(() => expect(storedWhenAsked).toBe(1));
});
it('asks for a sync after keeping a change the client made, not after a read', async () => {
writeEnabled(['acc']);
registerOfflineHooks();
const client = { getServerUrl: () => 'https://mail.example.org', getUsername: () => 'alice@example.org', getAccountId: () => 'acc1' };
await getOfflineHooks()?.answer(client, [['Email/set', { update: { m: { 'keywords/$seen': true } } }, '0']]);
expect(seen.calls).toBe(1);
await getOfflineHooks()?.answer(client, [['Email/get', { ids: [] }, '0']]);
expect(seen.calls).toBe(1);
});
});