Skip to content

Commit b2c62f4

Browse files
committed
test: add new test files for math modules, data, and e2e specs
1 parent 037e232 commit b2c62f4

13 files changed

Lines changed: 2742 additions & 0 deletions

src/e2e/tools/dof-simulator.spec.ts

Lines changed: 425 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
function sidebar(page: import('@playwright/test').Page) {
4+
return page.locator('[class*="sidebar"]').first()
5+
}
6+
7+
test.describe('Exposure Simulator', () => {
8+
test.beforeEach(async ({ page }) => {
9+
await page.goto('/exposure-simulator')
10+
})
11+
12+
test('aperture slider updates display value', async ({ page }) => {
13+
const panel = sidebar(page)
14+
15+
// Get the aperture slider (first range input)
16+
const apertureSlider = panel.locator('input[type="range"]').first()
17+
const initialValue = await apertureSlider.inputValue()
18+
19+
// Move aperture slider to index 0 (f/1.4)
20+
await apertureSlider.fill('0')
21+
22+
// The label should show f/1.4
23+
await expect(panel.getByText('f/1.4')).toBeVisible()
24+
expect(await apertureSlider.inputValue()).not.toBe(initialValue)
25+
})
26+
27+
test('shutter speed slider updates display value', async ({ page }) => {
28+
const panel = sidebar(page)
29+
30+
// Shutter speed is the second range input
31+
const shutterSlider = panel.locator('input[type="range"]').nth(1)
32+
33+
// Move to index 0 (30s — slowest)
34+
await shutterSlider.fill('0')
35+
await expect(panel.getByText('30s')).toBeVisible()
36+
37+
// Move to fastest (index 18 = 1/8000)
38+
await shutterSlider.fill('18')
39+
await expect(panel.getByText('1/8000')).toBeVisible()
40+
})
41+
42+
test('ISO slider updates display value', async ({ page }) => {
43+
const panel = sidebar(page)
44+
45+
// ISO is locked by default — unlock it first by locking aperture instead
46+
const apertureLockBtn = panel.locator('button[aria-pressed]', { hasText: 'Aperture' }).first()
47+
await apertureLockBtn.click()
48+
49+
const isoSlider = panel.locator('input[type="range"]').nth(2)
50+
await expect(isoSlider).toBeEnabled()
51+
52+
// Move to highest (index 8 = ISO 25600)
53+
await isoSlider.fill('8')
54+
await expect(panel.getByText('25600')).toBeVisible()
55+
56+
// Move back to low (index 1 = ISO 200)
57+
await isoSlider.fill('1')
58+
await expect(panel.getByText('200', { exact: true })).toBeVisible()
59+
})
60+
61+
test('exposure value updates when controls change', async ({ page }) => {
62+
const panel = sidebar(page)
63+
64+
// Read initial EV
65+
const evValue = panel.locator('[class*="resultValue"]').first()
66+
const initial = await evValue.textContent()
67+
68+
// Change shutter speed (not locked by default) to a very different value
69+
await panel.locator('input[type="range"]').nth(1).fill('0')
70+
await page.waitForTimeout(100)
71+
72+
const updated = await evValue.textContent()
73+
expect(updated).not.toBe(initial)
74+
})
75+
76+
test('lock buttons toggle correctly', async ({ page }) => {
77+
const panel = sidebar(page)
78+
79+
// Default lock is ISO
80+
const isoLockBtn = panel.locator('button[aria-pressed]', { hasText: 'ISO' }).first()
81+
const apertureLockBtn = panel.locator('button[aria-pressed]', { hasText: 'Aperture' }).first()
82+
const shutterLockBtn = panel.locator('button[aria-pressed]', { hasText: 'Shutter' }).first()
83+
84+
await expect(isoLockBtn).toHaveAttribute('aria-pressed', 'true')
85+
await expect(apertureLockBtn).toHaveAttribute('aria-pressed', 'false')
86+
87+
// Lock aperture instead
88+
await apertureLockBtn.click()
89+
await expect(apertureLockBtn).toHaveAttribute('aria-pressed', 'true')
90+
await expect(isoLockBtn).toHaveAttribute('aria-pressed', 'false')
91+
await expect(shutterLockBtn).toHaveAttribute('aria-pressed', 'false')
92+
})
93+
94+
test('locked slider is disabled', async ({ page }) => {
95+
const panel = sidebar(page)
96+
97+
// Default lock is ISO — ISO slider should be disabled
98+
const isoSlider = panel.locator('input[type="range"]').nth(2)
99+
await expect(isoSlider).toBeDisabled()
100+
101+
// Aperture and shutter sliders should be enabled
102+
await expect(panel.locator('input[type="range"]').first()).toBeEnabled()
103+
await expect(panel.locator('input[type="range"]').nth(1)).toBeEnabled()
104+
105+
// Lock aperture — aperture slider should now be disabled
106+
const apertureLockBtn = panel.locator('button[aria-pressed]', { hasText: 'Aperture' }).first()
107+
await apertureLockBtn.click()
108+
109+
await expect(panel.locator('input[type="range"]').first()).toBeDisabled()
110+
await expect(isoSlider).toBeEnabled()
111+
})
112+
113+
test('effect indicators are displayed', async ({ page }) => {
114+
const panel = sidebar(page)
115+
116+
// Should show Depth of Field, Motion, and Noise effect rows
117+
await expect(panel.getByText('Depth of Field')).toBeVisible()
118+
await expect(panel.getByText('Motion')).toBeVisible()
119+
await expect(panel.getByText('Noise')).toBeVisible()
120+
})
121+
122+
test('effect labels change with settings', async ({ page }) => {
123+
const panel = sidebar(page)
124+
125+
// Set aperture to wide open (f/1.4 — Very Shallow DOF)
126+
await panel.locator('input[type="range"]').first().fill('0')
127+
await expect(panel.getByText('Very Shallow')).toBeVisible()
128+
129+
// Set aperture to narrow (f/22 — Very Deep DOF)
130+
await panel.locator('input[type="range"]').first().fill('8')
131+
await expect(panel.getByText('Very Deep')).toBeVisible()
132+
})
133+
134+
test('canvas renders (WebGL preview)', async ({ page }) => {
135+
const canvas = page.locator('canvas').first()
136+
await expect(canvas).toBeVisible()
137+
const box = await canvas.boundingBox()
138+
expect(box!.width).toBeGreaterThan(0)
139+
expect(box!.height).toBeGreaterThan(0)
140+
})
141+
142+
test('scene selector changes preview', async ({ page }) => {
143+
const canvas = page.locator('canvas').first()
144+
145+
// Wait for initial render
146+
await page.waitForTimeout(500)
147+
const before = await canvas.screenshot()
148+
149+
// Click a different scene thumbnail (second one)
150+
const sceneThumbs = page.locator('[class*="sceneThumb"]')
151+
const count = await sceneThumbs.count()
152+
if (count > 1) {
153+
await sceneThumbs.nth(1).click()
154+
await page.waitForTimeout(1000) // WebGL needs time to load new scene textures
155+
const after = await canvas.screenshot()
156+
expect(Buffer.compare(before, after)).not.toBe(0)
157+
}
158+
})
159+
160+
test('URL state persistence', async ({ page }) => {
161+
const panel = sidebar(page)
162+
163+
// Set aperture to f/2 (index 1), lock to shutter
164+
await panel.locator('input[type="range"]').first().fill('1')
165+
await panel.locator('button[aria-pressed]', { hasText: 'Shutter' }).first().click()
166+
await page.waitForTimeout(300)
167+
168+
const url = page.url()
169+
expect(url).toContain('ai=1')
170+
expect(url).toContain('lock=shutter')
171+
172+
// Navigate to the URL and verify state is restored
173+
await page.goto(url)
174+
await page.waitForTimeout(500)
175+
176+
const restoredPanel = sidebar(page)
177+
const apertureSlider = restoredPanel.locator('input[type="range"]').first()
178+
await expect(apertureSlider).toHaveValue('1')
179+
180+
const shutterLockBtn = restoredPanel.locator('button[aria-pressed]', { hasText: 'Shutter' }).first()
181+
await expect(shutterLockBtn).toHaveAttribute('aria-pressed', 'true')
182+
})
183+
})

src/e2e/tools/hyperfocal.spec.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
function sidebar(page: import('@playwright/test').Page) {
4+
return page.locator('[class*="sidebar"]').first()
5+
}
6+
7+
test.describe('Hyperfocal Simulator', () => {
8+
test.beforeEach(async ({ page }) => {
9+
await page.goto('/hyperfocal-simulator')
10+
})
11+
12+
test('focal length dropdown changes hyperfocal distance', async ({ page }) => {
13+
const panel = sidebar(page)
14+
15+
// Target the hyperfocal distance value (inside the large monospace display, after the label)
16+
const hyperfocalLabel = panel.getByText('Hyperfocal Distance', { exact: true })
17+
const hyperfocalValue = hyperfocalLabel.locator('..').locator('div[style*="font-mono"]')
18+
const initial = await hyperfocalValue.textContent()
19+
20+
// Change focal length to 200mm
21+
const flSelect = panel.locator('select').first()
22+
await flSelect.selectOption('200')
23+
24+
const updated = await hyperfocalValue.textContent()
25+
expect(updated).not.toBe(initial)
26+
})
27+
28+
test('aperture dropdown changes hyperfocal distance', async ({ page }) => {
29+
const panel = sidebar(page)
30+
31+
const hyperfocalLabel = panel.getByText('Hyperfocal Distance', { exact: true })
32+
const hyperfocalValue = hyperfocalLabel.locator('..').locator('div[style*="font-mono"]')
33+
const initial = await hyperfocalValue.textContent()
34+
35+
// Change aperture to f/2.8
36+
const apSelect = panel.locator('select').nth(1)
37+
await apSelect.selectOption('2.8')
38+
39+
const updated = await hyperfocalValue.textContent()
40+
expect(updated).not.toBe(initial)
41+
})
42+
43+
test('sensor dropdown changes hyperfocal distance', async ({ page }) => {
44+
const panel = sidebar(page)
45+
46+
const hyperfocalLabel = panel.getByText('Hyperfocal Distance', { exact: true })
47+
const hyperfocalValue = hyperfocalLabel.locator('..').locator('div[style*="font-mono"]')
48+
const initial = await hyperfocalValue.textContent()
49+
50+
// Change sensor to APS-C (Nikon)
51+
const sensorSelect = panel.locator('select').nth(2)
52+
await sensorSelect.selectOption('apsc_n')
53+
54+
const updated = await hyperfocalValue.textContent()
55+
expect(updated).not.toBe(initial)
56+
})
57+
58+
test('results panel displays hyperfocal, near limit, far limit, and focus distance', async ({ page }) => {
59+
const panel = sidebar(page)
60+
61+
// Hyperfocal distance label (exact match to avoid matching page title)
62+
await expect(panel.getByText('Hyperfocal Distance', { exact: true })).toBeVisible()
63+
64+
// Near and far limit labels
65+
await expect(panel.getByText('Near Limit')).toBeVisible()
66+
await expect(panel.getByText('Far Limit')).toBeVisible()
67+
68+
// Focus distance result
69+
await expect(panel.getByText('Focus Distance').last()).toBeVisible()
70+
})
71+
72+
test('canvas/diagram renders', async ({ page }) => {
73+
// DoFCanvas renders a canvas element
74+
const canvas = page.locator('canvas').first()
75+
await expect(canvas).toBeVisible()
76+
const box = await canvas.boundingBox()
77+
expect(box!.width).toBeGreaterThan(0)
78+
expect(box!.height).toBeGreaterThan(0)
79+
})
80+
81+
test('scene preset buttons toggle active state', async ({ page }) => {
82+
const topbar = page.locator('[class*="canvasTopbar"]').first()
83+
84+
// Find scene buttons
85+
const landscapeBtn = topbar.locator('button[aria-pressed]', { hasText: 'Landscape' }).first()
86+
const streetBtn = topbar.locator('button[aria-pressed]', { hasText: 'Street' }).first()
87+
88+
// Default should be landscape active
89+
await expect(landscapeBtn).toHaveAttribute('aria-pressed', 'true')
90+
await expect(streetBtn).toHaveAttribute('aria-pressed', 'false')
91+
92+
// Click street
93+
await streetBtn.click()
94+
await expect(streetBtn).toHaveAttribute('aria-pressed', 'true')
95+
await expect(landscapeBtn).toHaveAttribute('aria-pressed', 'false')
96+
})
97+
98+
test('scene change updates canvas', async ({ page }) => {
99+
const canvas = page.locator('canvas').first()
100+
const before = await canvas.screenshot()
101+
102+
// Switch to Street scene
103+
const topbar = page.locator('[class*="canvasTopbar"]').first()
104+
await topbar.locator('button[aria-pressed]', { hasText: 'Street' }).first().click()
105+
await page.waitForTimeout(500)
106+
107+
const after = await canvas.screenshot()
108+
expect(Buffer.compare(before, after)).not.toBe(0)
109+
})
110+
111+
test('mini reference table shows all apertures', async ({ page }) => {
112+
const panel = sidebar(page)
113+
114+
// Mini table should list all 9 standard apertures
115+
await expect(panel.getByText('Hyperfocal Reference')).toBeVisible()
116+
const table = panel.locator('table').first()
117+
const rows = table.locator('tbody tr')
118+
expect(await rows.count()).toBe(9) // APERTURES has 9 entries
119+
})
120+
121+
test('URL state persistence', async ({ page }) => {
122+
const panel = sidebar(page)
123+
124+
// Set focal length to 85mm, aperture to f/2
125+
await panel.locator('select').first().selectOption('85')
126+
await panel.locator('select').nth(1).selectOption('2')
127+
await page.waitForTimeout(300)
128+
129+
const url = page.url()
130+
expect(url).toContain('fl=85')
131+
expect(url).toContain('f=2')
132+
133+
// Reload and verify state is restored
134+
await page.goto(url)
135+
await page.waitForTimeout(500)
136+
137+
const restoredPanel = sidebar(page)
138+
await expect(restoredPanel.locator('select').first()).toHaveValue('85')
139+
await expect(restoredPanel.locator('select').nth(1)).toHaveValue('2')
140+
})
141+
})

0 commit comments

Comments
 (0)