diff --git a/frontend/e2e/smoke.spec.ts b/frontend/e2e/smoke.spec.ts index 954d01c..2ee6f83 100644 --- a/frontend/e2e/smoke.spec.ts +++ b/frontend/e2e/smoke.spec.ts @@ -47,6 +47,45 @@ test('layouts move the nodes', async ({ page }) => { .toBe(true) }) +test('Kamada-Kawai and Stress layouts run in the background worker on a big network', async ({ + page, + request, +}) => { + // Regression test: getFrWorker() (metrics.ts) builds the layout worker by + // stringifying module-private functions and dispatching them by their + // literal source names. This suite runs against the production build + // (see playwright.config.ts), where the bundler's minifier renames those + // functions; the dispatcher's hardcoded names didn't follow, so "kk" and + // "stress" threw "distanceLayout is not defined" inside the worker and + // silently failed (runComputedLayout only reports it into #layoutStatus, + // it never throws or logs). That only happens once a network is big + // enough to take the worker path (> 60 nodes) - a small example never + // exercised it. + const n = 80 + const edges = Array.from({ length: n - 1 }, (_, i) => ({ source: `N${i}`, target: `N${i + 1}` })) + const r = await request.post('/api/external', { data: { name: 'Layout worker test', edges } }) + expect(r.ok()).toBe(true) + const { url } = (await r.json()) as { url: string } + await page.goto(url) + await expect.poll(() => nodeCount(page)).toBe(n) + + await page.locator('#sideTabDisplay').click() + await page.locator('#layoutSection h3').click() + await expect(page.locator('#btnRunLayout')).toBeVisible() + + for (const kind of ['kk', 'stress']) { + const before = await positions(page) + await selectValue(page, '#layoutSelect', kind) + await page.locator('#btnRunLayout').click() + await expect + .poll(async () => JSON.stringify(await positions(page)) !== JSON.stringify(before), { + timeout: 20_000, + }) + .toBe(true) + await expect(page.locator('#layoutStatus')).toBeEmpty() + } +}) + test('tabs switch views, 3D view activates', async ({ page }) => { await openExample(page) for (const [tab, view] of [ @@ -135,3 +174,31 @@ test('unknown example shows an error, not a crash', async ({ page }) => { expect(await nodeCount(page)).toBe(0) void EXAMPLE }) + +test('uploading a non-NORMA file is rejected with a warning before it reaches the canvas', async ({ + page, +}) => { + await page.goto('/') + await page.locator('#filesSection h3').click() + await page.setInputFiles('#normaFileInput', { + name: 'export.json', + mimeType: 'application/json', + buffer: Buffer.from('{"nodes":["A","B"],"edges":[["A","B"]]}'), + }) + await expect(page.locator('#normaStatus .note.error')).toContainText(/looks like JSON/) + await expect(page.locator('#libNetworks .lib-empty')).toBeVisible() + await expect(page.locator('#libNetworks .lib-row')).toHaveCount(0) + expect(await nodeCount(page)).toBe(0) +}) + +test('uploading a valid network file is accepted and ready to show', async ({ page }) => { + await page.goto('/') + await page.locator('#filesSection h3').click() + await page.setInputFiles('#normaFileInput', { + name: 'network.txt', + mimeType: 'text/plain', + buffer: Buffer.from('Source\tTarget\tWeight\nA\tB\t1\nB\tC\t2\n'), + }) + await expect(page.locator('#normaStatus .note.ok').first()).toContainText(/added/i) + await expect(page.locator('#libNetworks')).not.toBeEmpty() +}) diff --git a/frontend/norma.html b/frontend/norma.html index d2f6c2c..92907c0 100644 --- a/frontend/norma.html +++ b/frontend/norma.html @@ -321,6 +321,7 @@