aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorLalleSX2026-10-24 04:35:56 +0200
committerLalleSX2026-10-24 04:35:56 +0200
commitc4c1c840f8761ea45a3d2dcc912a59d8ced78cb5 (patch)
tree8fcbbfffb6a34af73697a703f77e6b82f191b91f /tests
download3D-Electrophysics-c4c1c840f8761ea45a3d2dcc912a59d8ced78cb5.tar.gz
3D-Electrophysics-c4c1c840f8761ea45a3d2dcc912a59d8ced78cb5.zip
init
Diffstat (limited to 'tests')
-rw-r--r--tests/lab.spec.ts152
1 files changed, 152 insertions, 0 deletions
diff --git a/tests/lab.spec.ts b/tests/lab.spec.ts
new file mode 100644
index 0000000..51645cd
--- /dev/null
+++ b/tests/lab.spec.ts
@@ -0,0 +1,152 @@
+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('/');
+ 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);
+});