feat: add adapter conformance suite
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { MessagingAdapter } from './adapter';
|
||||
import type { MessageEvent } from './types';
|
||||
|
||||
export interface ConformanceOptions {
|
||||
/** Build a fresh, isolated adapter per test. */
|
||||
makeAdapter: () => Promise<MessagingAdapter> | MessagingAdapter;
|
||||
/** A thread id that exists in the fixture. */
|
||||
seededThreadId: string;
|
||||
/** Participant ids openThread can legally be called with. */
|
||||
openWith: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The definition of a correct MessagingAdapter. Every adapter runs this.
|
||||
*
|
||||
* Imports vitest, so it ships from the './conformance' subpath ONLY and is never
|
||||
* reachable from the main barrel. Consumers supply their own vitest.
|
||||
*
|
||||
* Usage from another package:
|
||||
* import { runAdapterConformance } from '@insignia/iios-messaging-ui/conformance';
|
||||
* runAdapterConformance({ makeAdapter: () => new MockAdapter(), seededThreadId: 'th_1', openWith: ['pp_a'] });
|
||||
*/
|
||||
export function runAdapterConformance(opts: ConformanceOptions): void {
|
||||
const make = async () => await opts.makeAdapter();
|
||||
|
||||
describe('MessagingAdapter conformance', () => {
|
||||
it('lists conversations', async () => {
|
||||
const a = await make();
|
||||
const list = await a.listConversations();
|
||||
expect(Array.isArray(list)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns history for a seeded thread', async () => {
|
||||
const a = await make();
|
||||
const msgs = await a.history(opts.seededThreadId);
|
||||
expect(Array.isArray(msgs)).toBe(true);
|
||||
});
|
||||
|
||||
it('send resolves with a message carrying the sent text and a stable id', async () => {
|
||||
const a = await make();
|
||||
const m = await a.send(opts.seededThreadId, 'conformance hello');
|
||||
expect(m.text).toBe('conformance hello');
|
||||
expect(typeof m.id).toBe('string');
|
||||
expect(m.id.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('a sent message is attributed to the current actor', async () => {
|
||||
const a = await make();
|
||||
const m = await a.send(opts.seededThreadId, 'whose is this');
|
||||
expect(m.actorId).toBe(a.currentActorId());
|
||||
});
|
||||
|
||||
it('currentActorId is known BEFORE any message is sent', async () => {
|
||||
// The bug this SDK exists to kill: identity must come from auth, never be
|
||||
// inferred from history. A fresh adapter already knows who you are.
|
||||
const a = await make();
|
||||
expect(a.currentActorId()).not.toBeNull();
|
||||
});
|
||||
|
||||
it('a sent message appears in history', async () => {
|
||||
const a = await make();
|
||||
await a.send(opts.seededThreadId, 'persist me');
|
||||
const msgs = await a.history(opts.seededThreadId);
|
||||
expect(msgs.some((m) => m.text === 'persist me')).toBe(true);
|
||||
});
|
||||
|
||||
it('subscribe delivers a message event on send', async () => {
|
||||
const a = await make();
|
||||
const seen: MessageEvent[] = [];
|
||||
const off = a.subscribe(opts.seededThreadId, (e) => seen.push(e));
|
||||
await a.send(opts.seededThreadId, 'live one');
|
||||
off();
|
||||
const msgs = seen.filter((e) => e.kind === 'message');
|
||||
expect(msgs.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('unsubscribe stops delivery', async () => {
|
||||
const a = await make();
|
||||
const seen: MessageEvent[] = [];
|
||||
const off = a.subscribe(opts.seededThreadId, (e) => seen.push(e));
|
||||
off();
|
||||
await a.send(opts.seededThreadId, 'should not be heard');
|
||||
expect(seen).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('unsubscribe is idempotent', async () => {
|
||||
const a = await make();
|
||||
const off = a.subscribe(opts.seededThreadId, () => {});
|
||||
off();
|
||||
expect(() => off()).not.toThrow();
|
||||
});
|
||||
|
||||
it('openThread returns a thread id', async () => {
|
||||
const a = await make();
|
||||
const { threadId } = await a.openThread({ participantIds: opts.openWith });
|
||||
expect(typeof threadId).toBe('string');
|
||||
expect(threadId.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('markRead resolves', async () => {
|
||||
const a = await make();
|
||||
const m = await a.send(opts.seededThreadId, 'read me');
|
||||
await expect(a.markRead(opts.seededThreadId, m.id)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('sendTyping does not throw', async () => {
|
||||
const a = await make();
|
||||
expect(() => a.sendTyping(opts.seededThreadId)).not.toThrow();
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user