diff options
Diffstat (limited to '')
| -rw-r--r-- | tests/lab.spec.ts | 152 | ||||
| -rw-r--r-- | tests/language.spec.ts | 24 | ||||
| -rw-r--r-- | tests/math.spec.ts | 65 | ||||
| -rw-r--r-- | tests/test4.spec.ts | 118 |
4 files changed, 0 insertions, 359 deletions
diff --git a/tests/lab.spec.ts b/tests/lab.spec.ts deleted file mode 100644 index e4bac8e..0000000 --- a/tests/lab.spec.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -async function setRange(page: Page, label: string, value: number) { - await page.getByRole('slider', { name: label, exact: true }).fill(String(value)); -} -async function read(page: Page, label: string) { - return Number((await page.getByLabel(label, { exact: true }).innerText()).replaceAll(',', '')); -} - -test('all eight lessons render 3D content with measurements and explanations', async ({ page }) => { - const errors: string[] = []; - page.on('pageerror', e => errors.push(e.message)); - await page.goto('/#field'); - const subjects = ['Electric charge, force and field', 'Electric flux and charge', 'Electric potential energy', 'Electric potential and gradient', 'Capacitance', 'Dielectric materials', 'Current', 'Semiconducting materials']; - const tabs = page.getByRole('tab'); - await expect(tabs).toHaveCount(8); - for (let i = 0; i < subjects.length; i++) { - await tabs.nth(i).click(); - await expect(page.getByRole('heading', { name: subjects[i], exact: true })).toBeVisible(); - await expect(page.locator('canvas')).toBeVisible(); - await expect(page.locator('#measurements')).not.toContainText('Undefined'); - await expect(page.locator('#measurements .measurement')).toHaveCount(i === 5 ? 5 : i >= 4 ? 4 : 3); - await expect(page.locator('#challenge')).not.toBeEmpty(); - // A rendered frame must contain geometry beyond the solid background. - const colours = await page.locator('canvas').evaluate(canvas => new Promise<number>(resolve => requestAnimationFrame(() => { - const small = document.createElement('canvas'); - small.width = 80; small.height = 60; - const context = small.getContext('2d')!; - context.drawImage(canvas as HTMLCanvasElement, 0, 0, 80, 60); - const data = context.getImageData(0, 0, 80, 60).data; - const unique = new Set<number>(); - for (let k = 0; k < data.length; k += 4) unique.add((data[k] << 16) | (data[k + 1] << 8) | data[k + 2]); - resolve(unique.size); - }))); - expect(colours).toBeGreaterThan(12); - } - expect(errors).toEqual([]); -}); - -test('charge separation changes force by inverse square and reset restores defaults', async ({ page }) => { - await page.goto('/#field'); - const force = await read(page, '|Force on right charge|'); - await setRange(page, 'Charge separation', 6); - expect(await read(page, '|Force on right charge|')).toBeCloseTo(force / 4, 1); - await page.getByRole('button', { name: 'Reset experiment' }).click(); - await expect(page.getByRole('slider', { name: 'Charge separation', exact: true })).toHaveValue('3'); - await page.getByLabel('Charge signs', { exact: true }).selectOption('like'); - await expect(page.getByLabel('Interaction', { exact: true })).toHaveText('Repulsive'); -}); - -test('Gaussian flux distinguishes enclosed, outside and surface charges', async ({ page }) => { - await page.goto('/#flux'); - expect(await read(page, 'Net electric flux')).toBeGreaterThan(200); - await setRange(page, 'Charge x position', 3); - expect(await read(page, 'Net electric flux')).toBe(0); - await setRange(page, 'Charge x position', 2); - await expect(page.getByLabel('Net electric flux', { exact: true })).toHaveText('Undefined'); - await expect(page.getByLabel('Source location', { exact: true })).toHaveText('On surface'); -}); - -test('capacitance scales with area and dielectric conditions preserve the correct quantity', async ({ page }) => { - await page.goto('/#capacitance'); - const initial = await read(page, 'Capacitance'); - await setRange(page, 'Plate area', 200); - expect(await read(page, 'Capacitance')).toBeCloseTo(2 * initial, 1); - await page.getByRole('tab', { name: /Dielectrics/ }).click(); - const q = await read(page, 'Free charge |Q|'); - await setRange(page, 'Relative permittivity κ', 8); - expect(await read(page, 'Free charge |Q|')).toBeCloseTo(2 * q, 1); - expect(await read(page, 'Plate voltage')).toBe(10); - await page.getByLabel('Electrical condition', { exact: true }).selectOption('isolated'); - const isolatedCharge = await read(page, 'Free charge |Q|'); - expect(await read(page, 'Plate voltage')).toBe(1.25); - await setRange(page, 'Relative permittivity κ', 4); - expect(await read(page, 'Free charge |Q|')).toBe(isolatedCharge); - expect(await read(page, 'Plate voltage')).toBe(2.5); -}); - -test('reversed voltage reverses drift and animation can be paused', async ({ page }) => { - await page.goto('/#current'); - expect(await read(page, 'Conventional current')).toBeGreaterThan(0); - expect(await read(page, 'Electron drift, x')).toBeLessThan(0); - await setRange(page, 'Voltage across wire', -0.025); - expect(await read(page, 'Conventional current')).toBeLessThan(0); - expect(await read(page, 'Electron drift, x')).toBeGreaterThan(0); - await page.getByRole('button', { name: 'Pause animation' }).click(); - await expect(page.getByRole('button', { name: 'Resume animation' })).toBeVisible(); - await setRange(page, 'Voltage across wire', 0); - expect(await read(page, 'Conventional current')).toBe(0); -}); - -test('doping selects majority carriers without modifying intrinsic silicon', async ({ page }) => { - await page.goto('/#semiconductor'); - const doping = page.getByLabel('Silicon sample at 300 K', { exact: true }); - const concentration = page.getByRole('slider', { name: /Dopant density/ }); - await expect(concentration).toBeDisabled(); - expect(await read(page, 'Electron density n')).toBe(await read(page, 'Hole density p')); - await doping.selectOption('n'); - await expect(concentration).toBeEnabled(); - expect(await read(page, 'Electron density n')).toBeGreaterThan(await read(page, 'Hole density p')); - expect(await read(page, 'Fermi shift EF − Ei')).toBeGreaterThan(0); - await doping.selectOption('p'); - expect(await read(page, 'Hole density p')).toBeGreaterThan(await read(page, 'Electron density n')); - expect(await read(page, 'Fermi shift EF − Ei')).toBeLessThan(0); -}); - -test('lesson navigation preserves parameters, supports history, and fits a mobile viewport', async ({ page }) => { - await page.goto('/#energy'); - await setRange(page, 'Moving charge', 3); - await page.getByRole('tab', { name: /Potential & gradient/ }).click(); - await page.goBack(); - await expect(page.getByRole('slider', { name: 'Moving charge', exact: true })).toHaveValue('3'); - await page.setViewportSize({ width: 390, height: 844 }); - await expect(page.locator('canvas')).toBeVisible(); - expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); -}); - -test('WebGL failure leaves lessons and numeric experiments usable', async ({ page }) => { - await page.addInitScript(() => { - const original = HTMLCanvasElement.prototype.getContext; - HTMLCanvasElement.prototype.getContext = function (type: string, ...args: unknown[]) { - if (type.startsWith('webgl')) return null; - return original.call(this, type as '2d', ...args as []) as never; - } as typeof original; - }); - await page.goto('/#capacitance'); - await expect(page.getByRole('alert')).toContainText('WebGL'); - expect(await read(page, 'Capacitance')).toBeGreaterThan(80); - await setRange(page, 'Plate area', 200); - expect(await read(page, 'Capacitance')).toBeGreaterThan(170); -}); - -test('energy sign, distance and field work agree', async ({ page }) => { - await page.goto('/#energy'); - expect(await read(page, 'Interaction energy')).toBeCloseTo(-26.96, 2); - expect(await read(page, 'Work by field, from 6 m')).toBeCloseTo(17.98, 2); - await setRange(page, 'Distance from +3 nC source', 4); - expect(await read(page, 'Interaction energy')).toBeCloseTo(-13.48, 2); - await setRange(page, 'Moving charge', 2); - expect(await read(page, 'Interaction energy')).toBeCloseTo(13.48, 2); - expect(await read(page, 'Work by field, from 6 m')).toBeLessThan(0); -}); - -test('zero dipole potential can coexist with nonzero field', async ({ page }) => { - await page.goto('/#potential'); - expect(await read(page, 'Potential at probe')).toBe(0); - expect(await read(page, 'Ex at probe')).toBeGreaterThan(0); - await setRange(page, 'Probe x position (z = 1 m)', 2); - expect(await read(page, 'Potential at probe')).toBeLessThan(0); - await page.getByLabel('Charge signs', { exact: true }).selectOption('like'); - expect(await read(page, 'Potential at probe')).toBeGreaterThan(0); -}); diff --git a/tests/language.spec.ts b/tests/language.spec.ts deleted file mode 100644 index 5ff86ca..0000000 --- a/tests/language.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { expect, test } from '@playwright/test'; - -test('the language picker switches the whole lesson between English and Danish', async ({ page }) => { - await page.goto('/#field'); - await expect(page.locator('html')).toHaveAttribute('lang', 'en'); - await page.locator('#language').selectOption('da'); - await expect(page.locator('html')).toHaveAttribute('lang', 'da'); - await expect(page).toHaveTitle('Test 4 i 3D · Elektrofysiklaboratoriet'); - await expect(page.getByRole('heading', { name: 'Elektrisk ladning, kraft og felt', exact: true })).toBeVisible(); - await expect(page.getByRole('slider', { name: 'Ladningsafstand', exact: true })).toBeVisible(); - await expect(page.getByLabel('Vekselvirkning', { exact: true })).toHaveText('Tiltrækkende'); - await page.getByLabel('Ladningstegn', { exact: true }).selectOption('like'); - await expect(page.getByLabel('Vekselvirkning', { exact: true })).toHaveText('Frastødende'); - await page.getByRole('link', { name: 'Test 4 i 3D', exact: true }).click(); - await expect(page.getByRole('heading', { name: 'Arbejde langs en bane', exact: true })).toBeVisible(); - await expect(page.getByRole('button', { name: 'Vis forklaring' })).toBeVisible(); - await expect(page.locator('#observation')).toContainText('arbejde'); - await page.reload(); - await expect(page.getByRole('heading', { name: 'Arbejde langs en bane', exact: true })).toBeVisible(); - await page.locator('#language').selectOption('en'); - await expect(page.locator('html')).toHaveAttribute('lang', 'en'); - await expect(page.getByRole('heading', { name: 'Work along a path', exact: true })).toBeVisible(); - await expect(page.getByRole('button', { name: 'Reveal explanation' })).toBeVisible(); -}); diff --git a/tests/math.spec.ts b/tests/math.spec.ts deleted file mode 100644 index 71b586d..0000000 --- a/tests/math.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('work equations typeset vector arrows and integral limits with accessible math', async ({ page }) => { - await page.goto('/#test-work'); - const equations = page.locator('#equations'); - await expect(equations.locator('.katex')).toHaveCount(2); - await expect(equations.locator('math')).toHaveCount(2); - await expect(equations.locator('math msubsup').first()).toBeAttached(); - await expect(equations.locator('math mover').first()).toBeAttached(); - await expect(page.locator('label[for="parameter-q0"] .katex')).toBeVisible(); - await expect(page.getByRole('slider', { name: 'Moving charge q₀', exact: true })).toBeVisible(); - await page.locator('#solution summary').click(); - await expect(page.locator('.math-step .katex')).toHaveCount(3); - await expect(page.locator('.katex-error')).toHaveCount(0); -}); - -test('every lesson summary and derivation renders without math parsing errors', async ({ page }) => { - const errors: string[] = []; - page.on('pageerror', error => errors.push(error.message)); - for (const id of ['test-work', 'test-conservation', 'test-central', 'test-assembly', 'test-uniform', 'test-dipole', 'field', 'flux', 'energy', 'potential', 'capacitance', 'dielectric', 'current', 'semiconductor']) { - await page.goto(`/#${id}`); - await expect(page.locator('#equations .katex').first()).toBeVisible(); - expect(await page.locator('#equations .katex').count()).toBe(await page.locator('#equations > p').count()); - if (id.startsWith('test-')) { - await page.locator('#solution details').evaluateAll(details => details.forEach(detail => detail.setAttribute('open', ''))); - expect(await page.locator('.math-step .katex').count()).toBe(await page.locator('.math-step').count()); - } - await expect(page.locator('.katex-error')).toHaveCount(0); - } - await page.goto('/#test-dipole'); - await expect(page.locator('#test-question .katex').first()).toBeVisible(); - await expect(page.locator('#explanation .katex').first()).toBeVisible(); - expect(errors).toEqual([]); -}); - -test('long formulas stay contained and usable on a narrow screen', async ({ page }) => { - await page.setViewportSize({ width: 390, height: 844 }); - await page.goto('/#test-assembly'); - await page.locator('#solution summary').first().click(); - await expect(page.locator('#equations .katex').first()).toBeVisible(); - expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); - const equation = page.locator('#equations > p').first(); - const layout = await equation.evaluate(element => { - const style = getComputedStyle(element); - return { overflow: style.overflowX, width: element.getBoundingClientRect().width, available: element.parentElement!.getBoundingClientRect().width }; - }); - expect(layout.width).toBeLessThanOrEqual(layout.available); - expect(layout.overflow).toBe('auto'); -}); - -test('3D mathematical annotations track the scene and are removed when switching lessons', async ({ page }) => { - await page.goto('/#test-dipole'); - const labels = page.locator('#viewport .scene-math-label'); - await expect(labels.locator('math mfrac').first()).toBeAttached(); - await expect(labels.filter({ hasText: 'cos' }).first()).toBeVisible(); - const before = await labels.count(); - await page.getByRole('slider', { name: 'Final angle θᵦ', exact: true }).fill('180'); - await expect(labels).toHaveCount(before); - await page.getByRole('tab', { name: /Work & paths/ }).click(); - await expect(labels.filter({ hasText: 'cos' })).toHaveCount(0); - await expect(labels.locator('.katex').first()).toBeVisible(); - await expect(page.locator('#viewport .katex-error')).toHaveCount(0); - // Labels are an overlay; orbiting the underlying canvas remains possible. - expect(await page.locator('.scene-labels').evaluate(el => getComputedStyle(el).pointerEvents)).toBe('none'); -}); diff --git a/tests/test4.spec.ts b/tests/test4.spec.ts deleted file mode 100644 index 3e6a3b5..0000000 --- a/tests/test4.spec.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -async function read(page: Page, label: string) { - return Number((await page.getByLabel(label, { exact: true }).innerText()).replaceAll(',', '')); -} - -test('six original test questions have interactive scenes and every answer part', async ({ page }) => { - const errors: string[] = []; - page.on('pageerror', e => errors.push(e.message)); - await page.goto('/'); - const ids = ['work', 'conservation', 'central', 'assembly', 'uniform', 'dipole']; - const parts = [1, 1, 2, 2, 2, 3]; - await expect(page.getByRole('tab')).toHaveCount(6); - for (let i = 0; i < ids.length; i++) { - await page.getByRole('tab').nth(i).click(); - await expect(page).toHaveURL(new RegExp(`#test-${ids[i]}$`)); - await expect(page.locator('#test-question')).not.toBeEmpty(); - await expect(page.locator('#solution details')).toHaveCount(parts[i]); - await page.locator('#solution summary').first().click(); - await expect(page.locator('#solution details[open] .derivation')).toBeVisible(); - await expect(page.locator('#measurements')).not.toContainText('Undefined'); - await expect(page.locator('canvas').first()).toBeVisible(); - // Check actual rendered pixels, including scenes constructed after hash navigation. - await expect.poll(() => page.locator('#viewport > canvas').evaluate(canvas => new Promise<number>(resolve => { - requestAnimationFrame(() => requestAnimationFrame(() => { - const small = document.createElement('canvas'); small.width = 80; small.height = 60; - const context = small.getContext('2d')!; - context.drawImage(canvas as HTMLCanvasElement, 0, 0, 80, 60); - const data = context.getImageData(0, 0, 80, 60).data; - const colours = new Set<number>(); - for (let k = 0; k < data.length; k += 4) colours.add((data[k] << 16) | (data[k + 1] << 8) | data[k + 2]); - resolve(colours.size); - })); - }))).toBeGreaterThan(12); - } - expect(errors).toEqual([]); -}); - -test('path changes preserve work while signed charge and angle change it', async ({ page }) => { - await page.goto('/#test-work'); - const initial = await read(page, 'Field work a → b'); - await page.getByLabel('Path shape', { exact: true }).selectOption('curved'); - expect(await read(page, 'Field work a → b')).toBe(initial); - await page.getByRole('slider', { name: 'Moving charge q₀', exact: true }).fill('-2'); - expect(await read(page, 'Field work a → b')).toBe(-initial); - await page.goto('/#test-uniform'); - await page.getByRole('slider', { name: 'Displacement angle α', exact: true }).fill('90'); - expect(Math.abs(await read(page, 'Field work a → b'))).toBeLessThan(1e-10); -}); - -test('energy conservation, infinity and dipole equilibrium stay physically consistent', async ({ page }) => { - await page.goto('/#test-conservation'); - const total = await read(page, 'Total K + U'); - await page.getByRole('slider', { name: 'Journey progress', exact: true }).fill('100'); - expect(await read(page, 'Total K + U')).toBe(total); - expect(await read(page, 'Kinetic energy K')).toBeGreaterThan(0); - await page.goto('/#test-central'); - await page.getByLabel('Destination b', { exact: true }).selectOption('infinity'); - expect(await read(page, 'Potential energy Uᵦ')).toBe(0); - await expect(page.locator('#observation')).toContainText('infinity'); - await page.goto('/#test-dipole'); - await page.getByRole('slider', { name: 'Final angle θᵦ', exact: true }).fill('0'); - await page.getByRole('slider', { name: 'Rotation progress', exact: true }).fill('100'); - expect(await read(page, 'Potential energy U')).toBeLessThan(0); - expect(await read(page, 'Torque τz')).toBe(0); - await expect(page.locator('#observation')).toContainText('stable'); -}); - -test('assembly adds q₀ without double counting and the prediction can be revealed', async ({ page }) => { - await page.goto('/#test-assembly'); - await page.getByLabel('Assembly stage', { exact: true }).selectOption('3'); - const base = await read(page, 'Energy of current assembly'); - await page.getByLabel('Assembly stage', { exact: true }).selectOption('4'); - expect(await read(page, 'Energy of current assembly')).toBeCloseTo(base + await read(page, 'Additional energy ΔU₀'), 1); - await expect(page.locator('#prediction-answer')).toBeHidden(); - await page.getByRole('button', { name: 'Reveal explanation' }).click(); - await expect(page.locator('#prediction-answer')).toBeVisible(); -}); - -test('collection navigation retains settings, works with history, and fits mobile', async ({ page }) => { - await page.goto('/#test-dipole'); - await page.getByRole('slider', { name: 'Final angle θᵦ', exact: true }).fill('180'); - await page.getByRole('link', { name: 'All experiments', exact: true }).click(); - await expect(page.getByRole('tab')).toHaveCount(8); - await page.goBack(); - await expect(page.getByRole('slider', { name: 'Final angle θᵦ', exact: true })).toHaveValue('180'); - await page.getByRole('button', { name: 'Reset experiment' }).click(); - await expect(page.getByRole('slider', { name: 'Final angle θᵦ', exact: true })).toHaveValue('15'); - await page.setViewportSize({ width: 390, height: 844 }); - await expect(page.locator('canvas')).toBeVisible(); - expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); -}); - -test('next question returns a mobile learner to the new heading', async ({ page }) => { - await page.setViewportSize({ width: 390, height: 844 }); - await page.goto('/#test-work'); - await page.getByRole('link', { name: 'Next question →', exact: true }).click(); - await expect(page).toHaveURL(/#test-conservation$/); - await expect(page.getByRole('heading', { name: 'Where does the energy go?' })).toBeInViewport(); - await expect(page.locator('#lesson-title')).toBeFocused(); -}); - -test('test answers and controls remain accessible without WebGL', async ({ page }) => { - await page.addInitScript(() => { - const original = HTMLCanvasElement.prototype.getContext; - HTMLCanvasElement.prototype.getContext = function (type: string, ...args: unknown[]) { - if (type.startsWith('webgl')) return null; - return original.call(this, type as '2d', ...args as []) as never; - } as typeof original; - }); - await page.goto('/#test-dipole'); - await expect(page.getByRole('alert')).toContainText('WebGL'); - await page.getByRole('slider', { name: 'Rotation progress', exact: true }).fill('100'); - expect(await read(page, 'Potential energy U')).toBeLessThan(0); - await page.locator('#solution summary').last().click(); - await expect(page.locator('#solution details[open] math').first()).toBeAttached(); - await expect(page.locator('#solution details[open] .katex-error')).toHaveCount(0); -}); |