Added tests for the opportunities' catch-up and the toasts
This commit is contained in:
1 parent
f9064511b0
commit
13232bb15c
2 files changed
+231
No files matched your search
@@ -0,0 +1,172 @@
|
|||||||
|
import { createRoot } from 'solid-js';
|
||||||
|
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import type * as clientModule from '@/api/client';
|
||||||
|
import type { OpportunityChanges } from '@/api/client';
|
||||||
|
import { ApiError } from '@/api/client';
|
||||||
|
import { testOpportunity } from '@/lib/testOpportunity';
|
||||||
|
import type { OpportunityList } from './createOpportunities';
|
||||||
|
import { createOpportunities } from './createOpportunities';
|
||||||
|
|
||||||
|
// The two requests createOpportunities makes; each test answers them.
|
||||||
|
const requests = vi.hoisted(() => ({
|
||||||
|
listOpportunities: vi.fn<(signal?: AbortSignal) => Promise<OpportunityChanges>>(),
|
||||||
|
listChangedOpportunities:
|
||||||
|
vi.fn<(batch: string, signal?: AbortSignal) => Promise<OpportunityChanges>>(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/api/client', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<typeof clientModule>()),
|
||||||
|
api: requests,
|
||||||
|
}));
|
||||||
|
|
||||||
|
/** A promise the test settles when it chooses. */
|
||||||
|
interface Deferred<T> {
|
||||||
|
promise: Promise<T>;
|
||||||
|
resolve: (value: T) => void;
|
||||||
|
reject: (err: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deferred<T>(): Deferred<T> {
|
||||||
|
let resolve!: (value: T) => void;
|
||||||
|
let reject!: (err: unknown) => void;
|
||||||
|
const promise = new Promise<T>((res, rej) => {
|
||||||
|
resolve = res;
|
||||||
|
reject = rej;
|
||||||
|
});
|
||||||
|
return { promise, resolve, reject };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lets pending promises and Solid's effects run. */
|
||||||
|
const settle = async (): Promise<void> => {
|
||||||
|
for (let i = 0; i < 5; i++) await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
};
|
||||||
|
|
||||||
|
const changes = (batch: string, ...uuids: string[]): OpportunityChanges => ({
|
||||||
|
opportunities: uuids.map((uuid) => testOpportunity(uuid, { title: `${uuid} ${batch}` })),
|
||||||
|
batch,
|
||||||
|
});
|
||||||
|
|
||||||
|
let loads: Deferred<OpportunityChanges>[];
|
||||||
|
let catchUps: { batch: string; result: Deferred<OpportunityChanges> }[];
|
||||||
|
let list: OpportunityList;
|
||||||
|
let dispose: () => void;
|
||||||
|
|
||||||
|
const titles = (): string[] => list.items().map((opportunity) => opportunity.title);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
loads = [];
|
||||||
|
catchUps = [];
|
||||||
|
requests.listOpportunities.mockImplementation(() => {
|
||||||
|
const load = deferred<OpportunityChanges>();
|
||||||
|
loads.push(load);
|
||||||
|
return load.promise;
|
||||||
|
});
|
||||||
|
requests.listChangedOpportunities.mockImplementation((batch) => {
|
||||||
|
const result = deferred<OpportunityChanges>();
|
||||||
|
catchUps.push({ batch, result });
|
||||||
|
return result.promise;
|
||||||
|
});
|
||||||
|
createRoot((disposeRoot) => {
|
||||||
|
dispose = disposeRoot;
|
||||||
|
list = createOpportunities();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
dispose();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Finishes the first load with ocds-1 and ocds-2, at batch b1. */
|
||||||
|
async function loaded(): Promise<void> {
|
||||||
|
loads[0]?.resolve(changes('b1', 'ocds-1', 'ocds-2'));
|
||||||
|
await settle();
|
||||||
|
}
|
||||||
|
|
||||||
|
test('catches up from the loaded batch, then from the newest it got', async () => {
|
||||||
|
await loaded();
|
||||||
|
|
||||||
|
list.catchUp();
|
||||||
|
await settle();
|
||||||
|
expect(catchUps.map((c) => c.batch)).toEqual(['b1']);
|
||||||
|
catchUps[0]?.result.resolve(changes('b2', 'ocds-2', 'ocds-3'));
|
||||||
|
await settle();
|
||||||
|
expect(titles()).toEqual(['ocds-1 b1', 'ocds-2 b2', 'ocds-3 b2']);
|
||||||
|
|
||||||
|
list.catchUp();
|
||||||
|
await settle();
|
||||||
|
expect(catchUps.map((c) => c.batch)).toEqual(['b1', 'b2']);
|
||||||
|
expect(loads).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reloads when there is no batch to catch up from', async () => {
|
||||||
|
loads[0]?.resolve(changes('', 'ocds-1'));
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
list.catchUp();
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
expect(catchUps).toHaveLength(0);
|
||||||
|
expect(loads).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ name: 'an unknown batch', err: new ApiError('/opportunities', 410, undefined) },
|
||||||
|
{ name: 'a failure', err: new Error('offline') },
|
||||||
|
])('reloads after $name', async ({ err }) => {
|
||||||
|
await loaded();
|
||||||
|
|
||||||
|
list.catchUp();
|
||||||
|
await settle();
|
||||||
|
catchUps[0]?.result.reject(err);
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
expect(loads).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('waits for a load before catching up from its batch', async () => {
|
||||||
|
list.catchUp();
|
||||||
|
await settle();
|
||||||
|
expect(catchUps).toHaveLength(0);
|
||||||
|
|
||||||
|
await loaded();
|
||||||
|
expect(catchUps.map((c) => c.batch)).toEqual(['b1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runs one catch-up at a time, and one more for calls meanwhile', async () => {
|
||||||
|
await loaded();
|
||||||
|
|
||||||
|
list.catchUp();
|
||||||
|
await settle();
|
||||||
|
list.catchUp();
|
||||||
|
list.catchUp();
|
||||||
|
await settle();
|
||||||
|
expect(catchUps).toHaveLength(1);
|
||||||
|
|
||||||
|
catchUps[0]?.result.resolve(changes('b2'));
|
||||||
|
await settle();
|
||||||
|
expect(catchUps.map((c) => c.batch)).toEqual(['b1', 'b2']);
|
||||||
|
catchUps[1]?.result.resolve(changes('b3'));
|
||||||
|
await settle();
|
||||||
|
expect(catchUps).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('drops a catch-up that a reload overtook', async () => {
|
||||||
|
await loaded();
|
||||||
|
|
||||||
|
list.catchUp();
|
||||||
|
await settle();
|
||||||
|
list.reload();
|
||||||
|
await settle();
|
||||||
|
// The reload finishes first; the catch-up's older changes then arrive.
|
||||||
|
loads[1]?.resolve(changes('b5', 'ocds-1'));
|
||||||
|
await settle();
|
||||||
|
catchUps[0]?.result.resolve(changes('b2', 'ocds-9'));
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
expect(titles()).toEqual(['ocds-1 b5']);
|
||||||
|
list.catchUp();
|
||||||
|
await settle();
|
||||||
|
expect(catchUps.at(-1)?.batch).toBe('b5');
|
||||||
|
});
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { createRoot } from 'solid-js';
|
||||||
|
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import type { Toasts } from './createToasts';
|
||||||
|
import { createToasts } from './createToasts';
|
||||||
|
|
||||||
|
let toasts: Toasts;
|
||||||
|
let dispose: () => void;
|
||||||
|
|
||||||
|
const messages = (): string[] => toasts.items().map((toast) => toast.message);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
createRoot((disposeRoot) => {
|
||||||
|
dispose = disposeRoot;
|
||||||
|
toasts = createToasts();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
dispose();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows toasts oldest first, at most five', () => {
|
||||||
|
for (let i = 1; i <= 6; i++) toasts.show(`message ${i}`);
|
||||||
|
expect(messages()).toEqual(['message 2', 'message 3', 'message 4', 'message 5', 'message 6']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('each toast goes after 15 seconds', () => {
|
||||||
|
toasts.show('first');
|
||||||
|
vi.advanceTimersByTime(5_000);
|
||||||
|
toasts.show('second');
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(9_999);
|
||||||
|
expect(messages()).toEqual(['first', 'second']);
|
||||||
|
vi.advanceTimersByTime(1);
|
||||||
|
expect(messages()).toEqual(['second']);
|
||||||
|
vi.advanceTimersByTime(5_000);
|
||||||
|
expect(messages()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dismissing a toast removes it and its timer', () => {
|
||||||
|
toasts.show('first');
|
||||||
|
toasts.show('second');
|
||||||
|
const first = toasts.items()[0];
|
||||||
|
if (!first) throw new Error('no toast');
|
||||||
|
|
||||||
|
toasts.dismiss(first.id);
|
||||||
|
|
||||||
|
expect(messages()).toEqual(['second']);
|
||||||
|
expect(vi.getTimerCount()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cleaning up clears the timers', () => {
|
||||||
|
toasts.show('first');
|
||||||
|
dispose();
|
||||||
|
expect(vi.getTimerCount()).toBe(0);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user