diff --git a/packages/editor/browser/dashboardFiles.spec.ts b/packages/editor/browser/dashboardFiles.spec.ts index d91df34..fbc5c98 100644 --- a/packages/editor/browser/dashboardFiles.spec.ts +++ b/packages/editor/browser/dashboardFiles.spec.ts @@ -5,9 +5,14 @@ // 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 { createHash } from 'node:crypto'; +import { mkdtempSync, readFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { expect, test, type Browser, type Page } from '@playwright/test'; +import { BLOB_THRESHOLD_BYTES } from '@mdg-filius/doc'; +import { IMPORT_MAXIMUM_BYTES } from '@mdg-filius/core'; import { serveEditor, showDesktop } from './editorPage.js'; import { startServerF, type ServerF } from './serverF.js'; @@ -27,26 +32,29 @@ test.afterAll(async () => { 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 { +async function embedded(browser: Browser, project: string, name: string, level: 'read' | 'write', picture: number[] = PICTURE, pictureName = 'Logo Schule.png'): Promise { 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); + await page.addInitScript( + ([picture, pictureName]: [number[], string]) => { + 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: pictureName, 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: data.name!.replace('.png', ' (2).png') }, window.location.origin); + } + }); + }, + [picture, pictureName] as [number[], string], + ); 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'); @@ -132,3 +140,66 @@ test('offers neither way on its own, without a dashboard', async ({ browser }) = await expect(explorer.getByRole('button', { name: 'Aus meinen Dateien …' })).toHaveCount(0); await context.close(); }); + +/** Installs programs on a computer's desktop (Java's installation window). */ +async function install(desktop: ReturnType, programs: readonly string[]): Promise { + await desktop.locator('[data-program="installation"]').click(); + const installation = desktop.locator('[data-program-window="installation"]'); + for (const program of programs) await installation.locator('[data-list="available"] [role="option"]').filter({ hasText: program }).click(); + await installation.getByRole('button', { name: 'Installieren' }).click(); + await installation.getByRole('button', { name: 'Änderungen annehmen' }).click(); +} + +test("F8's acceptance: a picture of one's own files on the simulated web server, fetched in another computer's browser, and back into the files", async ({ browser }) => { + test.setTimeout(180_000); + const project = await f.project('java-examples/internetwork_komplett.fls'); + // A picture between the blob threshold and Java's limit: it goes to F as a blob. + const shooter = await browser.newPage({ viewport: { width: 480, height: 360 } }); + await shooter.setContent('

MDG Logo

'); + const file = join(mkdtempSync(join(tmpdir(), 'filius-f8-')), 'logo.png'); + await shooter.screenshot({ path: file }); + await shooter.close(); + const picture = [...readFileSync(file)]; + expect(picture.length).toBeGreaterThan(BLOB_THRESHOLD_BYTES); + expect(picture.length).toBeLessThanOrEqual(IMPORT_MAXIMUM_BYTES); + + const mia = await embedded(browser, project.id, 'Mia', 'write', picture, 'logo.png'); + await mia.locator('button[name="simulationMode"]').click(); + await expect(mia.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation'); + const server = await showDesktop(mia, project.node('Webserver 5.10')); + await install(server, ['Datei-Explorer']); + await server.locator('[data-program="fileExplorer"]').click(); + const explorer = server.locator('[data-program-window="fileExplorer"]'); + await explorer.locator('.file-explorer-tree button[data-path="/webserver"]').click(); + await explorer.getByRole('toolbar', { name: 'Aktionen' }).getByRole('button', { name: 'Importieren' }).click(); + const importing = explorer.getByRole('dialog', { name: 'Datei importieren' }); + await importing.getByRole('button', { name: 'Aus meinen Dateien …' }).click(); + await importing.getByRole('button', { name: 'Datei importieren' }).click(); + await expect(importing.getByRole('status')).toHaveText('Datei erfolgreich importiert!'); + await importing.getByRole('button', { name: 'Schließen' }).click(); + await expect(explorer.locator('.file-explorer-entries button', { hasText: 'logo.png' })).toBeVisible(); + // On F under its SHA-256, as large as it came. + await expect.poll(() => f.blobSize(createHash('sha256').update(Buffer.from(picture)).digest('hex'))).toBe(picture.length); + + // Ben, in a browser of his own, on another computer of the network: the web server hands the picture out. + const ben = await embedded(browser, project.id, 'Ben', 'write'); + await expect(ben.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation'); + const notebook = await showDesktop(ben, project.node('Notebook 1.10')); + await notebook.locator('[data-program="webBrowser"]').click(); + const web = notebook.locator('[data-program-window="webBrowser"]'); + const address = web.getByRole('textbox', { name: 'Adresse' }); + await address.fill('141.99.5.10/logo.png'); + await address.press('Enter'); + const shown = web.frameLocator('iframe[title="Seite"]').locator('img'); + await expect(shown).toHaveAttribute('src', /^data:image\/png;base64,/, { timeout: 60_000 }); + const src = await shown.getAttribute('src'); + expect([...Buffer.from(src!.slice(src!.indexOf(',') + 1), 'base64')]).toEqual(picture); + + // And back into the files: the bytes as they came in. + await explorer.locator('.file-explorer-entries button', { hasText: 'logo.png' }).click(); + await explorer.getByRole('toolbar', { name: 'Aktionen' }).getByRole('button', { name: 'In meine Dateien speichern …' }).click(); + await expect(explorer.getByRole('alert')).toHaveText('„logo (2).png“ steht jetzt in deinen Dateien.'); + expect((await asked(mia)).at(-1)).toEqual({ type: 'saveFile', name: 'logo.png', bytes: picture }); + await mia.context().close(); + await ben.context().close(); +});