Who started the simulation, "Pakete verwerfen" and the dropped packets move from the toolbar into a properties sheet beside the surface. New in it: restart the simulation, after asking. The room learns a restart message: stopped as by stop, every capture, dropped frame and program window emptied, started anew and named after who restarted; refused for readers and frozen projects, logged, and waiting for asked blobs like a start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
404 lines
22 KiB
TypeScript
404 lines
22 KiB
TypeScript
// The simulation rooms on F (F5.3a) with real sockets, the real room thread
|
|
// and a real Postgres, on FILIUS' example internetwork_komplett: one room
|
|
// for everyone in a project — started by one, running for all, named after
|
|
// who started it; a ping in one person's command line seen by the other;
|
|
// what the simulation leaves going into the shared document; a reader who
|
|
// watches and changes nothing; the refusals, with their reasons; the room
|
|
// staying a while after the last person left, then stopping and ending;
|
|
// freezing, deleting and revoking reaching the room; a failed thread
|
|
// ending its rooms; the service stopping with rooms running.
|
|
|
|
import { randomUUID } from 'node:crypto';
|
|
import { readFileSync } from 'node:fs';
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
import WebSocket from 'ws';
|
|
import * as Y from 'yjs';
|
|
import { changeFiles, nodeFiles, readProject, writeProject } from '@mdg-filius/doc';
|
|
import { openProjectArchive } from '@mdg-filius/fls';
|
|
import { SIM_CLOSED_CODE, SIM_REFUSED_CODE, SIM_ROOM_FAILED_CODE, WRITING_KINDS, type HostMessage } from '@mdg-filius/protocol';
|
|
import { LINGER_MILLISECONDS, PostgresSessions, PostgresStore } from '../src/index.js';
|
|
import { testDatabase, type TestDatabase } from './database.js';
|
|
import { EDITOR_ORIGIN, join, SERVICE_KEY, signIn, startTestServer, until, type TestServer } from './harness.js';
|
|
|
|
let database: TestDatabase;
|
|
let store: PostgresStore;
|
|
let sessions: PostgresSessions;
|
|
|
|
beforeAll(async () => {
|
|
database = await testDatabase();
|
|
store = PostgresStore.connect(database.url);
|
|
sessions = new PostgresSessions(store.pool);
|
|
await store.migrate();
|
|
await sessions.migrate();
|
|
}, 60_000);
|
|
|
|
afterAll(async () => {
|
|
await store?.close();
|
|
await database?.stop();
|
|
});
|
|
|
|
const EXAMPLE = new URL('../../../corpus/java-examples/internetwork_komplett.fls', import.meta.url);
|
|
|
|
/** FILIUS' example as a project of F; the id of a device by its name. */
|
|
async function exampleProject(): Promise<{ readonly project: string; readonly node: (name: string) => string }> {
|
|
const project = randomUUID();
|
|
const imported = openProjectArchive(new Uint8Array(readFileSync(EXAMPLE)), () => randomUUID());
|
|
const doc = new Y.Doc();
|
|
writeProject(doc, imported);
|
|
expect(await store.create(project, Y.encodeStateAsUpdate(doc))).toBe(true);
|
|
return { project, node: (name) => imported.graph.nodes.find((each) => each.displayName === name)!.id };
|
|
}
|
|
|
|
const start = (options: { readonly linger?: number } = {}) => startTestServer(store, sessions, options.linger === undefined ? {} : { lingerMilliseconds: options.linger });
|
|
|
|
interface SimClient {
|
|
readonly socket: WebSocket;
|
|
readonly got: HostMessage[];
|
|
/** The socket's closing: its code and reason. */
|
|
readonly closed: Promise<{ readonly code: number; readonly reason: string }>;
|
|
send(message: object): void;
|
|
/** The last state the room sent. */
|
|
state(): Extract<HostMessage, { kind: 'state' }> | undefined;
|
|
}
|
|
|
|
function enterRoom(server: TestServer, project: string, { cookie, origin }: { readonly cookie?: string; readonly origin?: string } = {}): Promise<SimClient> {
|
|
const socket = new WebSocket(`ws://127.0.0.1:${server.port}/sim?project=${project}`, { headers: { ...(cookie === undefined ? {} : { cookie }), ...(origin === undefined ? {} : { origin }) } });
|
|
const got: HostMessage[] = [];
|
|
socket.on('message', (data) => got.push(JSON.parse(data.toString()) as HostMessage));
|
|
const closed = new Promise<{ code: number; reason: string }>((resolve) => socket.on('close', (code, reason) => resolve({ code, reason: reason.toString() })));
|
|
return new Promise((resolve, reject) => {
|
|
socket.on('open', () =>
|
|
resolve({
|
|
socket,
|
|
got,
|
|
closed,
|
|
send: (message) => socket.send(JSON.stringify({ v: 1, ...message })),
|
|
state: () => got.filter((message) => message.kind === 'state').at(-1) as Extract<HostMessage, { kind: 'state' }> | undefined,
|
|
}),
|
|
);
|
|
socket.on('unexpected-response', (_request, response) => reject(new Error(`refused: ${response.statusCode}`)));
|
|
socket.on('error', reject);
|
|
});
|
|
}
|
|
|
|
/** Signed in with a fresh ticket (by name), then in the room. */
|
|
async function person(
|
|
server: TestServer,
|
|
project: string,
|
|
name: string,
|
|
level: 'read' | 'write' = 'write',
|
|
user = randomUUID(),
|
|
): Promise<SimClient & { readonly cookie: string; readonly user: string }> {
|
|
const cookie = await signIn(server, project, { user, login: name.toLowerCase(), level, name });
|
|
return Object.assign(await enterRoom(server, project, { cookie }), { cookie, user });
|
|
}
|
|
|
|
const appLines = (client: SimClient) =>
|
|
client.got.filter((message): message is Extract<HostMessage, { kind: 'app' }> => message.kind === 'app').flatMap((message) => message.lines as { text?: string }[]);
|
|
const sleep = (milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
function service(server: TestServer, method: string, path: string, body?: unknown): Promise<Response> {
|
|
return fetch(`${server.base}${path}`, { method, headers: { 'x-filius-service-key': SERVICE_KEY }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
|
|
}
|
|
|
|
describe('a simulation room on F', () => {
|
|
it('runs for everyone in the project once one starts it, names who did, and shows a ping in one command line to the other', async () => {
|
|
const server = await start();
|
|
const { project, node } = await exampleProject();
|
|
const mia = await person(server, project, 'Mia');
|
|
const ben = await person(server, project, 'Ben');
|
|
await until(() => mia.state() !== undefined && ben.state() !== undefined);
|
|
expect(ben.state()).toMatchObject({ mode: 'design' });
|
|
const notebook = node('Notebook 1.10');
|
|
ben.send({ kind: 'sub', topics: [`app:${notebook}/terminal`] });
|
|
mia.send({ kind: 'start' });
|
|
await until(() => ben.state()?.mode === 'simulation');
|
|
expect(ben.state()!.startedBy).toBe('Mia');
|
|
mia.send({ kind: 'input', node: notebook, app: 'terminal', action: 'enter', args: { line: 'ping 141.99.5.10' }, reqId: 'r1' });
|
|
await until(() => mia.got.some((message) => message.kind === 'ack' && message.reqId === 'r1'));
|
|
await until(() => appLines(ben).some((line) => /4 Paket\(e\) gesendet, 4 Paket\(e\) empfangen/.test(line.text ?? '')), 20_000);
|
|
// Started anew by Ben: named him, the command line as at the start.
|
|
const sent = ben.got.length;
|
|
ben.send({ kind: 'restart' });
|
|
await until(() => mia.state()?.startedBy === 'Ben');
|
|
await until(() => ben.got.slice(sent).some((message) => message.kind === 'app' && message.from === 0));
|
|
const anew = ben.got.slice(sent).filter((message): message is Extract<HostMessage, { kind: 'app' }> => message.kind === 'app' && message.from === 0).at(-1)!;
|
|
expect((anew.lines as { text?: string }[]).some((line) => /Paket/.test(line.text ?? ''))).toBe(false);
|
|
ben.send({ kind: 'stop' });
|
|
await until(() => mia.state()?.mode === 'design');
|
|
expect(mia.state()).not.toHaveProperty('startedBy');
|
|
expect(server.log.lines.filter((line) => ['sim.start', 'sim.stop', 'sim.restart'].includes(line.event as string)).map(({ event, user }) => [event, user])).toEqual([
|
|
['sim.start', 'mia'],
|
|
['sim.restart', 'ben'],
|
|
['sim.stop', 'ben'],
|
|
]);
|
|
expect(server.sims.stats()).toEqual({ rooms: 1, people: 2, threads: [1] });
|
|
mia.socket.close();
|
|
ben.socket.close();
|
|
await server.close();
|
|
}, 30_000);
|
|
|
|
it('writes what the simulation leaves into the shared document, for whoever has it open', async () => {
|
|
const server = await start();
|
|
const { project, node } = await exampleProject();
|
|
const mia = await person(server, project, 'Mia');
|
|
const editor = await join(server, project, { cookie: mia.cookie });
|
|
const notebook = node('Notebook 1.10');
|
|
mia.send({ kind: 'start' });
|
|
await until(() => mia.state()?.mode === 'simulation');
|
|
mia.send({ kind: 'input', node: notebook, app: 'terminal', action: 'enter', args: { line: 'touch probe.txt' }, reqId: 'r1' });
|
|
await until(() => (nodeFiles(editor.doc, notebook)?.allEntries() ?? []).some((entry) => entry.path.endsWith('probe.txt')));
|
|
// And back: a file a person writes into the document is there for the command line.
|
|
mia.send({ kind: 'sub', topics: [`app:${notebook}/terminal`] });
|
|
changeFiles(editor.doc, notebook, 'person', (files) => files.writeTextFile('/from-doc.txt', 'x'));
|
|
await until(() => editor.provider.unsyncedChanges === 0);
|
|
mia.send({ kind: 'input', node: notebook, app: 'terminal', action: 'enter', args: { line: 'ls' }, reqId: 'r2' });
|
|
await until(() => appLines(mia).some((line) => (line.text ?? '').includes('from-doc.txt')));
|
|
mia.socket.close();
|
|
editor.close();
|
|
await server.close();
|
|
});
|
|
|
|
it('lets a reader watch and change nothing', async () => {
|
|
const server = await start();
|
|
const { project, node } = await exampleProject();
|
|
const reader = await person(server, project, 'Rita', 'read');
|
|
await until(() => reader.state() !== undefined);
|
|
for (const kind of WRITING_KINDS) reader.send({ kind, ...(kind === 'input' ? { node: node('Notebook 1.10'), app: 'terminal', action: 'enter', args: { line: 'ls' }, reqId: 'r' } : {}) });
|
|
await until(() => reader.got.filter((message) => message.kind === 'error').length === WRITING_KINDS.length);
|
|
expect(reader.got.filter((message) => message.kind === 'error')).toEqual([
|
|
...WRITING_KINDS.slice(0, -1).map(() => ({ v: 1, kind: 'error', code: 'readOnly' })),
|
|
{ v: 1, kind: 'error', code: 'readOnly', reqId: 'r' },
|
|
]);
|
|
expect(reader.state()).toMatchObject({ mode: 'design' });
|
|
reader.send({ kind: 'sub', topics: ['activity'] });
|
|
reader.socket.send('not json');
|
|
await until(() => reader.got.some((message) => message.kind === 'error' && message.code === 'badMessage'));
|
|
expect(server.log.lines.find((line) => line.event === 'sim.joined')).toMatchObject({ project, user: 'rita', write: false });
|
|
reader.socket.close();
|
|
await server.close();
|
|
});
|
|
|
|
it('refuses without a session, without a grant, a name that is no project, and a page of another origin', async () => {
|
|
const server = await start();
|
|
const { project } = await exampleProject();
|
|
const other = (await exampleProject()).project;
|
|
const cookie = await signIn(server, other, { login: 'ab12' });
|
|
expect(await (await enterRoom(server, project)).closed).toEqual({ code: SIM_REFUSED_CODE, reason: 'notSignedIn' });
|
|
expect(await (await enterRoom(server, project, { cookie })).closed).toEqual({ code: SIM_REFUSED_CODE, reason: 'noAccess' });
|
|
expect(await (await enterRoom(server, 'nothing', { cookie })).closed).toEqual({ code: SIM_REFUSED_CODE, reason: 'notAProject' });
|
|
await expect(enterRoom(server, project, { cookie, origin: 'https://elsewhere.test' })).rejects.toThrow('refused: 403');
|
|
expect(server.log.lines.filter((line) => line.event === 'sim.refused').map(({ reason, user }) => [reason, user])).toEqual([
|
|
['notSignedIn', undefined],
|
|
['noAccess', 'ab12'],
|
|
['notAProject', undefined],
|
|
]);
|
|
await server.close();
|
|
});
|
|
|
|
it('refuses a project its store no longer has when the room is made', async () => {
|
|
const server = await start();
|
|
const { project } = await exampleProject();
|
|
const mia = await person(server, project, 'Mia');
|
|
await until(() => mia.state() !== undefined);
|
|
mia.socket.close();
|
|
await server.close();
|
|
const forgetful = Object.assign(Object.create(store) as PostgresStore, { load: async () => undefined });
|
|
const again = await startTestServer(forgetful, sessions);
|
|
expect(await (await enterRoom(again, project, { cookie: mia.cookie })).closed).toEqual({ code: SIM_REFUSED_CODE, reason: 'unknownProject' });
|
|
expect(again.log.lines.find((line) => line.event === 'sim.openFailed')).toMatchObject({ project });
|
|
await again.close();
|
|
});
|
|
|
|
it(`stays ${LINGER_MILLISECONDS / 1000} s after the last person left, running, then stops, writes back and ends`, async () => {
|
|
expect(LINGER_MILLISECONDS).toBe(60_000);
|
|
const server = await start({ linger: 600 });
|
|
const { project, node } = await exampleProject();
|
|
const mia = await person(server, project, 'Mia');
|
|
mia.send({ kind: 'start' });
|
|
await until(() => mia.state()?.mode === 'simulation');
|
|
mia.socket.close();
|
|
await sleep(200);
|
|
// Back within the while: the same room, still running.
|
|
const again = await enterRoom(server, project, { cookie: mia.cookie });
|
|
await until(() => again.state() !== undefined);
|
|
expect(again.state()).toMatchObject({ mode: 'simulation', startedBy: 'Mia' });
|
|
// Staying past the while it had: coming back called it off.
|
|
await sleep(700);
|
|
expect(server.sims.stats().rooms).toBe(1);
|
|
again.send({ kind: 'input', node: node('Notebook 1.10'), app: 'terminal', action: 'enter', args: { line: 'touch left.txt' }, reqId: 'r' });
|
|
await until(() => again.got.some((message) => message.kind === 'ack'));
|
|
again.socket.close();
|
|
await until(() => server.log.lines.some((line) => line.event === 'sim.roomClosed'), 5_000);
|
|
expect(server.sims.stats().rooms).toBe(0);
|
|
expect(server.log.lines.filter((line) => line.event === 'sim.roomClosed')).toEqual([{ level: 'info', event: 'sim.roomClosed', project, writeBack: true }]);
|
|
// The document let go once the room had written back.
|
|
await until(() => server.sync.hocuspocus.getDocumentsCount() === 0);
|
|
const stored = await store.load(project);
|
|
const doc = new Y.Doc();
|
|
Y.applyUpdate(doc, stored!.state);
|
|
for (const update of stored!.updates) Y.applyUpdate(doc, update);
|
|
expect(
|
|
nodeFiles(doc, node('Notebook 1.10'))!
|
|
.allEntries()
|
|
.some((entry) => entry.path.endsWith('left.txt')),
|
|
).toBe(true);
|
|
// Whoever comes later finds a new room, in design mode.
|
|
const later = await enterRoom(server, project, { cookie: mia.cookie });
|
|
await until(() => later.state() !== undefined);
|
|
expect(later.state()).toMatchObject({ mode: 'design' });
|
|
later.socket.close();
|
|
await server.close();
|
|
});
|
|
|
|
it('stops a running simulation when the project is frozen, refuses to start it then, and starts it once thawed', async () => {
|
|
const server = await start();
|
|
const { project, node } = await exampleProject();
|
|
const mia = await person(server, project, 'Mia');
|
|
mia.send({ kind: 'start' });
|
|
await until(() => mia.state()?.mode === 'simulation');
|
|
mia.send({ kind: 'input', node: node('Notebook 1.10'), app: 'terminal', action: 'enter', args: { line: 'touch before.txt' }, reqId: 'r' });
|
|
await until(() => mia.got.some((message) => message.kind === 'ack'));
|
|
expect((await service(server, 'POST', `/svc/projects/${project}/freeze`, { frozen: true })).status).toBe(200);
|
|
expect(mia.state()).toMatchObject({ mode: 'design' });
|
|
mia.send({ kind: 'start', reqId: 'x' });
|
|
await until(() => mia.got.some((message) => message.kind === 'error'));
|
|
expect(mia.got.filter((message) => message.kind === 'error')).toEqual([{ v: 1, kind: 'error', code: 'frozen', reqId: 'x' }]);
|
|
// Nor anew.
|
|
mia.send({ kind: 'restart', reqId: 'y' });
|
|
await until(() => mia.got.filter((message) => message.kind === 'error').length === 2);
|
|
expect(mia.got.filter((message) => message.kind === 'error').at(-1)).toEqual({ v: 1, kind: 'error', code: 'frozen', reqId: 'y' });
|
|
expect(mia.state()).toMatchObject({ mode: 'design' });
|
|
// A reader is told it reads, not that it is frozen.
|
|
const late = await person(server, project, 'Ben');
|
|
late.send({ kind: 'start' });
|
|
await until(() => late.got.some((message) => message.kind === 'error'));
|
|
expect(late.got.find((message) => message.kind === 'error')).toMatchObject({ code: 'frozen' });
|
|
expect((await service(server, 'POST', `/svc/projects/${project}/freeze`, { frozen: false })).status).toBe(200);
|
|
mia.send({ kind: 'start' });
|
|
await until(() => mia.state()?.mode === 'simulation');
|
|
mia.socket.close();
|
|
late.socket.close();
|
|
await server.close();
|
|
});
|
|
|
|
it('freezes a project whose room nobody made, and makes its room frozen', async () => {
|
|
const server = await start();
|
|
const { project } = await exampleProject();
|
|
expect((await service(server, 'POST', `/svc/projects/${project}/freeze`, { frozen: true })).status).toBe(200);
|
|
const mia = await person(server, project, 'Mia');
|
|
mia.send({ kind: 'start' });
|
|
await until(() => mia.got.some((message) => message.kind === 'error'));
|
|
expect(mia.got.find((message) => message.kind === 'error')).toMatchObject({ code: 'frozen' });
|
|
mia.socket.close();
|
|
await server.close();
|
|
});
|
|
|
|
it('lets everyone in the room go when the project is deleted, and writes nothing back', async () => {
|
|
const server = await start();
|
|
const { project } = await exampleProject();
|
|
const mia = await person(server, project, 'Mia');
|
|
mia.send({ kind: 'start' });
|
|
await until(() => mia.state()?.mode === 'simulation');
|
|
const response = await service(server, 'DELETE', `/svc/projects/${project}`);
|
|
expect(await response.json()).toEqual({ deleted: true, disconnected: 1 });
|
|
expect(await mia.closed).toEqual({ code: SIM_CLOSED_CODE, reason: 'deleted' });
|
|
expect(server.log.lines.find((line) => line.event === 'sim.roomClosed')).toMatchObject({ project, writeBack: false });
|
|
await until(() => server.sync.hocuspocus.getDocumentsCount() === 0);
|
|
expect(server.log.lines.filter((line) => line.event === 'store.failed')).toEqual([]);
|
|
expect(await store.load(project)).toBeUndefined();
|
|
await server.close();
|
|
});
|
|
|
|
it('lets a person go whose grant ends, to one project or to all, and no one else', async () => {
|
|
const server = await start();
|
|
const { project } = await exampleProject();
|
|
const other = (await exampleProject()).project;
|
|
const user = randomUUID();
|
|
const mia = await person(server, project, 'Mia', 'write', user);
|
|
const miaThere = await person(server, other, 'Mia', 'write', user);
|
|
const ben = await person(server, project, 'Ben');
|
|
expect(await (await service(server, 'POST', '/svc/revoke', { user, project })).json()).toEqual({ revoked: 1, disconnected: 1 });
|
|
expect(await mia.closed).toEqual({ code: SIM_CLOSED_CODE, reason: 'revoked' });
|
|
expect(miaThere.socket.readyState).toBe(WebSocket.OPEN);
|
|
// Two sessions: one ticket each.
|
|
expect(await (await service(server, 'POST', '/svc/revoke', { user })).json()).toEqual({ revoked: 2, disconnected: 1 });
|
|
expect(await miaThere.closed).toEqual({ code: SIM_CLOSED_CODE, reason: 'revoked' });
|
|
expect(ben.socket.readyState).toBe(WebSocket.OPEN);
|
|
ben.socket.close();
|
|
await server.close();
|
|
});
|
|
|
|
it('ends the rooms of a thread that fails, and gives whoever comes back a new room', async () => {
|
|
const server = await start();
|
|
const { project } = await exampleProject();
|
|
const mia = await person(server, project, 'Mia');
|
|
mia.send({ kind: 'start' });
|
|
await until(() => mia.state()?.mode === 'simulation');
|
|
await server.sims.workers[0]!.terminate();
|
|
expect(await mia.closed).toEqual({ code: SIM_ROOM_FAILED_CODE, reason: 'roomFailed' });
|
|
expect(server.log.lines.find((line) => line.event === 'sim.threadFailed')).toMatchObject({ rooms: 1 });
|
|
const again = await enterRoom(server, project, { cookie: mia.cookie });
|
|
await until(() => again.state() !== undefined);
|
|
expect(again.state()).toMatchObject({ mode: 'design' });
|
|
expect(server.sims.stats()).toEqual({ rooms: 1, people: 1, threads: [1] });
|
|
again.socket.close();
|
|
await server.close();
|
|
});
|
|
|
|
it('stops every room when the service stops, writing back what they left, and lets everyone go', async () => {
|
|
const server = await start();
|
|
const { project, node } = await exampleProject();
|
|
const mia = await person(server, project, 'Mia');
|
|
mia.send({ kind: 'start' });
|
|
await until(() => mia.state()?.mode === 'simulation');
|
|
mia.send({ kind: 'input', node: node('Notebook 1.10'), app: 'terminal', action: 'enter', args: { line: 'touch last.txt' }, reqId: 'r' });
|
|
await until(() => mia.got.some((message) => message.kind === 'ack'));
|
|
await server.close();
|
|
expect(await mia.closed).toEqual({ code: 1001, reason: 'service stopping' });
|
|
const stored = await store.load(project);
|
|
const doc = new Y.Doc();
|
|
Y.applyUpdate(doc, stored!.state);
|
|
for (const update of stored!.updates) Y.applyUpdate(doc, update);
|
|
expect(
|
|
nodeFiles(doc, node('Notebook 1.10'))!
|
|
.allEntries()
|
|
.some((entry) => entry.path.endsWith('last.txt')),
|
|
).toBe(true);
|
|
expect(readProject(doc).graph.nodes.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('lets go a socket that answers no ping', async () => {
|
|
const server = await startTestServer(store, sessions, { pingMilliseconds: 100 });
|
|
const { project } = await exampleProject();
|
|
const mia = await person(server, project, 'Mia');
|
|
await until(() => mia.state() !== undefined);
|
|
// A socket that went away says nothing: ws answers pings by itself, so its answering is stopped.
|
|
(mia.socket as unknown as { _receiver: { removeAllListeners(name: string): void } })._receiver.removeAllListeners('ping');
|
|
expect((await mia.closed).code).toBe(1006);
|
|
await server.close();
|
|
});
|
|
|
|
it('gives a new room to the thread with the fewest', async () => {
|
|
const server = await startTestServer(store, sessions, { simThreads: 2 });
|
|
const [first, second, third] = [await exampleProject(), await exampleProject(), await exampleProject()];
|
|
const people = [await person(server, first.project, 'Mia'), await person(server, second.project, 'Ben')];
|
|
await until(() => people.every((each) => each.state() !== undefined));
|
|
expect(server.sims.stats().threads).toEqual([1, 1]);
|
|
people.push(await person(server, third.project, 'Rita'));
|
|
await until(() => people[2]!.state() !== undefined);
|
|
expect([...server.sims.stats().threads].sort()).toEqual([1, 2]);
|
|
for (const each of people) each.socket.close();
|
|
await server.close();
|
|
});
|
|
|
|
it('opens a room for the editor with its origin', async () => {
|
|
const server = await start();
|
|
const { project } = await exampleProject();
|
|
const cookie = await signIn(server, project);
|
|
const client = await enterRoom(server, project, { cookie, origin: EDITOR_ORIGIN });
|
|
await until(() => client.state() !== undefined);
|
|
client.socket.close();
|
|
await server.close();
|
|
});
|
|
});
|