feat(editor): a computer's files to and from the person's own files (F8.1)
Beside the dashboard only: Java's import window gets "Aus meinen Dateien" (D4) — the dashboard is asked over the bridge for a file of at most Java's 150 000 bytes and hands its bytes over — and a file's menu gets "In meine Dateien speichern" (D5), its bytes handed to the dashboard to save. - Bridge: chooseFile/chosenFile and saveFile/savedFile, each question with its number; an answer from elsewhere, of another version, for no question or of the other kind is no answer. - Whoever reads or finds the project frozen may save, not bring in. - doc fileBytes: a text as UTF-8, a picture as it is, and "missing" while a blob's bytes have not come (read so, the file would be empty). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
// The person's own files over the bridge (F8): the page is its own parent
|
||||
// here, and a little script stands in for the dashboard — it answers
|
||||
// "chooseFile" with a picture and "saveFile" with a new name, and keeps what
|
||||
// it was asked. Mia, who writes, brings the picture into a computer's file
|
||||
// explorer and takes it out again; Rita, who only reads, may take a file out,
|
||||
// but bring nothing in.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { expect, test, type Browser, type Page } from '@playwright/test';
|
||||
import { serveEditor, showDesktop } from './editorPage.js';
|
||||
import { startServerF, type ServerF } from './serverF.js';
|
||||
|
||||
let editor: { readonly base: string; readonly close: () => Promise<void> };
|
||||
let f: ServerF;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
editor = await serveEditor();
|
||||
f = await startServerF(editor.base);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await f?.close();
|
||||
await editor?.close();
|
||||
});
|
||||
|
||||
const PICTURE = [...readFileSync(fileURLToPath(new URL('../src/assets/desktop/datei.png', import.meta.url)))];
|
||||
|
||||
/** A person with the editor embedded as the dashboard embeds it: `parent` is the page itself, and it answers. */
|
||||
async function embedded(browser: Browser, project: string, name: string, level: 'read' | 'write'): Promise<Page> {
|
||||
const context = await browser.newContext();
|
||||
await f.signIn(context, project, { name, level });
|
||||
const page = await context.newPage();
|
||||
await page.addInitScript((picture: number[]) => {
|
||||
const asked: unknown[] = [];
|
||||
(window as unknown as { asked: unknown[] }).asked = asked;
|
||||
window.addEventListener('message', (event) => {
|
||||
const data = event.data as { type?: string; request?: number; name?: string; data?: ArrayBuffer; maxBytes?: number };
|
||||
if (data?.type === 'chooseFile') {
|
||||
asked.push({ type: data.type, maxBytes: data.maxBytes });
|
||||
const bytes = new Uint8Array(picture).buffer;
|
||||
window.postMessage({ v: 1, type: 'chosenFile', request: data.request, name: 'Logo Schule.png', data: bytes }, window.location.origin, [bytes]);
|
||||
}
|
||||
if (data?.type === 'saveFile') {
|
||||
asked.push({ type: data.type, name: data.name, bytes: [...new Uint8Array(data.data!)] });
|
||||
window.postMessage({ v: 1, type: 'savedFile', request: data.request, result: 'saved', name: 'Logo Schule (2).png' }, window.location.origin);
|
||||
}
|
||||
});
|
||||
}, PICTURE);
|
||||
const origin = new URL(editor.base).origin;
|
||||
await page.goto(`${editor.base}?project=${project}&server=${encodeURIComponent(f.base)}&parent=${encodeURIComponent(origin)}`);
|
||||
await page.waitForSelector('svg.filius-canvas');
|
||||
return page;
|
||||
}
|
||||
|
||||
const asked = (page: Page) => page.evaluate(() => (window as unknown as { asked: unknown[] }).asked);
|
||||
|
||||
test("brings a picture of the person's own files into a computer and takes it out again; who only reads takes out only", async ({ browser }) => {
|
||||
test.setTimeout(120_000);
|
||||
const project = await f.project('java-examples/internetwork_komplett.fls');
|
||||
const notebook = project.node('Notebook 1.10');
|
||||
const mia = await embedded(browser, project.id, 'Mia', 'write');
|
||||
await mia.locator('button[name="simulationMode"]').click();
|
||||
await expect(mia.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
|
||||
const desktop = await showDesktop(mia, notebook);
|
||||
await desktop.locator('[data-program="installation"]').click();
|
||||
const installation = desktop.locator('[data-program-window="installation"]');
|
||||
await installation.locator('[data-list="available"] [role="option"]').filter({ hasText: 'Datei-Explorer' }).click();
|
||||
await installation.getByRole('button', { name: 'Installieren' }).click();
|
||||
await installation.getByRole('button', { name: 'Änderungen annehmen' }).click();
|
||||
await desktop.locator('[data-program="fileExplorer"]').click();
|
||||
const explorer = desktop.locator('[data-program-window="fileExplorer"]');
|
||||
const toolbar = explorer.getByRole('toolbar', { name: 'Aktionen' });
|
||||
|
||||
// D4: Java's import window, the file from her own files — with Java's limit.
|
||||
await toolbar.getByRole('button', { name: 'Importieren' }).click();
|
||||
const importing = explorer.getByRole('dialog', { name: 'Datei importieren' });
|
||||
await importing.getByRole('button', { name: 'Aus meinen Dateien …' }).click();
|
||||
await expect(importing.locator('input[readonly]')).toHaveValue('Logo Schule.png');
|
||||
await importing.getByRole('button', { name: 'Datei importieren' }).click();
|
||||
await expect(importing.getByRole('status')).toHaveText('Datei erfolgreich importiert!');
|
||||
expect(await asked(mia)).toEqual([{ type: 'chooseFile', maxBytes: 150_000 }]);
|
||||
await importing.getByRole('button', { name: 'Schließen' }).click();
|
||||
|
||||
// D5: nothing chosen, nothing to save; the file chosen, its bytes go out as they came in.
|
||||
const save = toolbar.getByRole('button', { name: 'In meine Dateien speichern …' });
|
||||
await expect(save).toBeDisabled();
|
||||
await explorer.locator('.file-explorer-entries button', { hasText: 'Logo Schule.png' }).click();
|
||||
await save.click();
|
||||
await expect(explorer.getByRole('alert')).toHaveText('„Logo Schule (2).png“ steht jetzt in deinen Dateien.');
|
||||
expect((await asked(mia))[1]).toEqual({ type: 'saveFile', name: 'Logo Schule.png', bytes: PICTURE });
|
||||
// The entry's menu has it too — after Java's own items.
|
||||
await explorer.locator('.file-explorer-entries button', { hasText: 'Logo Schule.png' }).click({ button: 'right' });
|
||||
await expect(explorer.getByRole('menuitem').last()).toHaveText('In meine Dateien speichern …');
|
||||
await mia.keyboard.press('Escape');
|
||||
|
||||
// Rita reads: the explorer of the running simulation, saving yes, bringing in no.
|
||||
const rita = await embedded(browser, project.id, 'Rita', 'read');
|
||||
await expect(rita.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
|
||||
const ritaDesktop = await showDesktop(rita, notebook);
|
||||
await ritaDesktop.locator('[data-program="fileExplorer"]').click();
|
||||
const ritaExplorer = ritaDesktop.locator('[data-program-window="fileExplorer"]');
|
||||
const ritaBar = ritaExplorer.getByRole('toolbar', { name: 'Aktionen' });
|
||||
await ritaExplorer.locator('.file-explorer-entries button', { hasText: 'Logo Schule.png' }).click();
|
||||
await expect(ritaBar.getByRole('button', { name: 'In meine Dateien speichern …' })).toBeEnabled();
|
||||
await ritaBar.getByRole('button', { name: 'Importieren' }).click();
|
||||
await expect(ritaExplorer.getByRole('dialog', { name: 'Datei importieren' }).getByRole('button', { name: 'Datei wählen' })).toBeVisible();
|
||||
await expect(ritaExplorer.getByRole('button', { name: 'Aus meinen Dateien …' })).toHaveCount(0);
|
||||
await mia.context().close();
|
||||
await rita.context().close();
|
||||
});
|
||||
|
||||
test('offers neither way on its own, without a dashboard', async ({ browser }) => {
|
||||
const project = await f.project('java-examples/internetwork_komplett.fls');
|
||||
const context = await browser.newContext();
|
||||
await f.signIn(context, project.id, { name: 'Mia' });
|
||||
const page = await context.newPage();
|
||||
await page.goto(`${editor.base}?project=${project.id}&server=${encodeURIComponent(f.base)}`);
|
||||
await page.waitForSelector('svg.filius-canvas');
|
||||
await page.locator('button[name="simulationMode"]').click();
|
||||
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
|
||||
const desktop = await showDesktop(page, project.node('Notebook 1.10'));
|
||||
await desktop.locator('[data-program="installation"]').click();
|
||||
const installation = desktop.locator('[data-program-window="installation"]');
|
||||
await installation.locator('[data-list="available"] [role="option"]').filter({ hasText: 'Datei-Explorer' }).click();
|
||||
await installation.getByRole('button', { name: 'Installieren' }).click();
|
||||
await installation.getByRole('button', { name: 'Änderungen annehmen' }).click();
|
||||
await desktop.locator('[data-program="fileExplorer"]').click();
|
||||
const explorer = desktop.locator('[data-program-window="fileExplorer"]');
|
||||
await expect(explorer.getByRole('button', { name: 'In meine Dateien speichern …' })).toHaveCount(0);
|
||||
await explorer.getByRole('toolbar', { name: 'Aktionen' }).getByRole('button', { name: 'Importieren' }).click();
|
||||
await expect(explorer.getByRole('button', { name: 'Aus meinen Dateien …' })).toHaveCount(0);
|
||||
await context.close();
|
||||
});
|
||||
Reference in New Issue
Block a user