diff options
| author | LalleSX | 2026-10-24 14:18:00 +0200 |
|---|---|---|
| committer | LalleSX | 2026-10-24 14:18:00 +0200 |
| commit | 389fb3a592832092835da0c2cc1608a57ba5f629 (patch) | |
| tree | ad1f89333ec3c84b906fb6546cee7824f58236fa /tests | |
| parent | init (diff) | |
| download | 3D-Electrophysics-389fb3a592832092835da0c2cc1608a57ba5f629.tar.gz 3D-Electrophysics-389fb3a592832092835da0c2cc1608a57ba5f629.zip | |
Add tests for math rendering and interactive scenes; update navigation and viewport handling
- Updated `lab.spec.ts` to navigate directly to the field lesson.
- Created `math.spec.ts` with tests for math rendering, error checking, and responsive design.
- Added `test4.spec.ts` for interactive scenes, ensuring all questions are functional and accessible.
- Modified `vite.config.ts` to keep original question/answer links navigable in production.
Diffstat (limited to '')
| -rw-r--r-- | tests/lab.spec.ts | 2 | ||||
| -rw-r--r-- | tests/math.spec.ts | 65 | ||||
| -rw-r--r-- | tests/test4.spec.ts | 118 |
3 files changed, 184 insertions, 1 deletions
diff --git a/tests/lab.spec.ts b/tests/lab.spec.ts index 51645cd..e4bac8e 100644 --- a/tests/lab.spec.ts +++ b/tests/lab.spec.ts @@ -10,7 +10,7 @@ async function read(page: Page, label: string) { 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('/'); + 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); diff --git a/tests/math.spec.ts b/tests/math.spec.ts new file mode 100644 index 0000000..71b586d --- /dev/null +++ b/tests/math.spec.ts @@ -0,0 +1,65 @@ +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 new file mode 100644 index 0000000..3e6a3b5 --- /dev/null +++ b/tests/test4.spec.ts @@ -0,0 +1,118 @@ +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); +}); |