feat: F5.4 presence: who else is in the project
Over Yjs awareness each editor tells who it is (F names the person in the access message), the device it has chosen and the windows it has open, and nothing else: no pointer, nothing that changes without a cause, sent only when it changed. Others' states are read with care. A frame in their colour round a device they chose, a dot on a window they have open too, a circle with initials each beside the mode buttons; the bridge tells the dashboard who is there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
// The editor on a project of a real server F (F5.3b), three people in three
|
||||
// browsers: Mia and Ben write, Rita only reads. What one moves the others
|
||||
// see; Rita gets everything shown and nothing to change; Mia starts the
|
||||
// see, and each sees the others (a circle each, a frame round a device
|
||||
// another has chosen, a dot on a window another has open too); Rita gets
|
||||
// everything shown and nothing to change; Mia starts the
|
||||
// simulation and it runs for all, named after her; Ben's ping in a command
|
||||
// line is in Mia's too (one window per computer, as in Java); Rita's typing
|
||||
// there is refused, and she is told; the project frozen, Ben reads and is
|
||||
@@ -9,6 +11,7 @@
|
||||
|
||||
import { expect, test, type Browser, type Page } from '@playwright/test';
|
||||
import { drag, onDevice, serveEditor } from './editorPage.js';
|
||||
import { colorOf } from '../src/remote/presence.js';
|
||||
import { startServerF, type ServerF } from './serverF.js';
|
||||
|
||||
let editor: { readonly base: string; readonly close: () => Promise<void> };
|
||||
@@ -50,16 +53,40 @@ async function commandLine(page: Page, id: string) {
|
||||
test('three people in one project of F: moving, reading only, one simulation for all, a frozen project, an ended grant', async ({ browser }) => {
|
||||
test.setTimeout(120_000);
|
||||
const project = await f.project('java-examples/internetwork_komplett.fls');
|
||||
const benUser = crypto.randomUUID();
|
||||
const mia = await person(browser, project.id, 'Mia');
|
||||
const [miaUser, benUser, ritaUser] = [crypto.randomUUID(), crypto.randomUUID(), crypto.randomUUID()];
|
||||
const mia = await person(browser, project.id, 'Mia', 'write', miaUser);
|
||||
const ben = await person(browser, project.id, 'Ben', 'write', benUser);
|
||||
const rita = await person(browser, project.id, 'Rita', 'read');
|
||||
const rita = await person(browser, project.id, 'Rita', 'read', ritaUser);
|
||||
const devices = await mia.locator('g.device').count();
|
||||
expect(devices).toBeGreaterThan(5);
|
||||
for (const page of [ben, rita]) await expect(page.locator('g.device')).toHaveCount(devices);
|
||||
|
||||
// Mia moves a device; Ben and Rita see it where she put it.
|
||||
// Each sees the others, by their initials.
|
||||
for (const [page, others] of [
|
||||
[mia, [benUser, ritaUser]],
|
||||
[ben, [miaUser, ritaUser]],
|
||||
[rita, [miaUser, benUser]],
|
||||
] as const) {
|
||||
await expect(page.locator('[data-person]')).toHaveCount(2);
|
||||
for (const other of others) await expect(page.locator(`[data-person="${other}"]`)).toBeVisible();
|
||||
}
|
||||
await expect(mia.locator(`[data-person="${benUser}"]`)).toHaveText('B');
|
||||
|
||||
// Mia chooses a device: Ben sees a frame round it in her colour, until she lets it go.
|
||||
const notebook = project.node('Notebook 1.10');
|
||||
const router = project.node('Switch 1');
|
||||
const routerAt = await onDevice(mia, router);
|
||||
await mia.mouse.click(routerAt.x, routerAt.y);
|
||||
const frame = ben.locator(`g.device[data-node="${router}"] rect.chosen-by`);
|
||||
await expect(frame).toHaveCount(1);
|
||||
// The ticket gave Mia no colour: she has the one her id gives, the same in every editor.
|
||||
await expect(frame).toHaveAttribute('data-color', colorOf({ id: miaUser, name: 'Mia', color: '' }));
|
||||
await mia.getByRole('complementary', { name: 'Einstellungen' }).getByRole('button', { name: 'Schließen' }).click();
|
||||
await expect(frame).toHaveCount(0);
|
||||
// Nobody sees their own frame.
|
||||
await expect(mia.locator('rect.chosen-by')).toHaveCount(0);
|
||||
|
||||
// Mia moves a device; Ben and Rita see it where she put it.
|
||||
const before = await box(mia, notebook);
|
||||
const from = await onDevice(mia, notebook);
|
||||
await drag(mia, from, { x: from.x + 120, y: from.y + 40 });
|
||||
@@ -95,8 +122,13 @@ test('three people in one project of F: moving, reading only, one simulation for
|
||||
await bens.input.press('Enter');
|
||||
const mias = await commandLine(mia, notebook);
|
||||
for (const each of [bens, mias]) await expect(each.text).toContainText('4 Paket(e) gesendet, 4 Paket(e) empfangen', { timeout: 30_000 });
|
||||
// Rita may look, not type.
|
||||
// Both have the notebook's desktop open: each sees the other's dot on it.
|
||||
await expect(desktop(mia, notebook).locator(`[data-watcher="${benUser}"]`)).toBeVisible();
|
||||
await expect(desktop(ben, notebook).locator(`[data-watcher="${miaUser}"]`)).toBeVisible();
|
||||
// Rita may look, not type; she sees both on the desktop, and they see her.
|
||||
const ritas = await commandLine(rita, notebook);
|
||||
await expect(desktop(rita, notebook).locator('[data-watcher]')).toHaveCount(2);
|
||||
await expect(desktop(mia, notebook).locator(`[data-watcher="${ritaUser}"]`)).toBeVisible();
|
||||
await expect(ritas.text).toContainText('ping 141.99.5.10');
|
||||
await ritas.input.fill('ipconfig');
|
||||
await ritas.input.press('Enter');
|
||||
@@ -126,6 +158,37 @@ test('three people in one project of F: moving, reading only, one simulation for
|
||||
await expect(canvas(mia)).toBeVisible();
|
||||
});
|
||||
|
||||
test('tells the dashboard over the bridge who else is in the project, each time it changes', async ({ browser }) => {
|
||||
const project = await f.project('java-examples/zwei_rechnernetze_komplett.fls');
|
||||
const context = await browser.newContext();
|
||||
await f.signIn(context, project.id, { name: 'Mia' });
|
||||
const page = await context.newPage();
|
||||
// The page is its own parent here: what it tells the dashboard comes back to it.
|
||||
const origin = new URL(editor.base).origin;
|
||||
await page.addInitScript(() => {
|
||||
(window as unknown as { told: unknown[] }).told = [];
|
||||
window.addEventListener('message', (event) => (window as unknown as { told: unknown[] }).told.push(event.data));
|
||||
});
|
||||
await page.goto(`${editor.base}?project=${project.id}&server=${encodeURIComponent(f.base)}&parent=${encodeURIComponent(origin)}`);
|
||||
await page.waitForSelector('svg.filius-canvas');
|
||||
const people = () => page.evaluate(() => (window as unknown as { told: { type?: string; people?: { name: string }[] }[] }).told.filter((message) => message.type === 'people').map((message) => message.people!.map((each) => each.name)));
|
||||
await expect.poll(people).toEqual([[]]);
|
||||
const benUser = crypto.randomUUID();
|
||||
const ben = await person(browser, project.id, 'Ben', 'write', benUser);
|
||||
await expect.poll(people).toEqual([[], ['Ben']]);
|
||||
// Mia in a second tab: she is not one of the others for herself, and Ben sees her once.
|
||||
const second = await context.newPage();
|
||||
await second.goto(`${editor.base}?project=${project.id}&server=${encodeURIComponent(f.base)}`);
|
||||
await second.waitForSelector('svg.filius-canvas');
|
||||
await expect(ben.locator('[data-person]')).toHaveCount(1);
|
||||
await expect(second.locator('[data-person]')).toHaveCount(1);
|
||||
await expect(second.locator(`[data-person="${benUser}"]`)).toBeVisible();
|
||||
await second.close();
|
||||
await ben.context().close();
|
||||
await expect.poll(people, { timeout: 40_000 }).toEqual([[], ['Ben'], []]);
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('says why when F ends only the room: the editor goes as when the document is let go', async ({ browser }) => {
|
||||
const project = await f.project('java-examples/zwei_rechnernetze_komplett.fls');
|
||||
const context = await browser.newContext();
|
||||
|
||||
Reference in New Issue
Block a user