Add color picker regression tests
This commit is contained in:
parent
1f11392da6
commit
e65b56820b
94
tests/e2e/color-picker-demo-card.spec.ts
Normal file
94
tests/e2e/color-picker-demo-card.spec.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '../helpers/canvas'
|
||||
|
||||
async function dragSlider(page: Parameters<typeof test>[0]['page'], canvas: CanvasHelper, testId: string, ratio: number) {
|
||||
const slider = page.locator(`[data-test-id="${testId}"] input[type="range"]`)
|
||||
const box = await slider.boundingBox()
|
||||
if (!box) throw new Error(`Missing slider: ${testId}`)
|
||||
const y = box.y + box.height / 2
|
||||
await page.mouse.move(box.x + 2, y)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(box.x + Math.max(2, Math.min(box.width - 2, box.width * ratio)), y, {
|
||||
steps: 8
|
||||
})
|
||||
await page.mouse.up()
|
||||
await canvas.waitForRender()
|
||||
}
|
||||
|
||||
async function selectDemoCard(page: Parameters<typeof test>[0]['page'], canvas: CanvasHelper) {
|
||||
await page.goto('/demo')
|
||||
await canvas.waitForInit()
|
||||
|
||||
await page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
const nodes = Array.from(store.graph.nodes.values())
|
||||
const card = nodes.find((node) => node.name === 'Card' && node.type === 'COMPONENT')
|
||||
?? nodes.find((node) => node.name === 'Card')
|
||||
if (!card) throw new Error(`Card not found. Available: ${nodes.slice(0, 20).map((n) => `${n.name}:${n.type}`).join(', ')}`)
|
||||
store.state.selectedIds = new Set([card.id])
|
||||
store.requestRender()
|
||||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
await expect(page.locator('[data-test-id="design-panel-single"]')).toBeVisible()
|
||||
await expect(page.locator('[data-test-id="design-node-header"]')).toContainText('Card')
|
||||
}
|
||||
|
||||
async function getSelectedFill(page: Parameters<typeof test>[0]['page']) {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
const id = [...store.state.selectedIds][0]
|
||||
const node = store.graph.getNode(id)
|
||||
return node
|
||||
? {
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
fill: node.fills?.[0] ?? null
|
||||
}
|
||||
: null
|
||||
})
|
||||
}
|
||||
|
||||
test('demo card fill changes through color picker', async ({ page }) => {
|
||||
const canvas = new CanvasHelper(page)
|
||||
await selectDemoCard(page, canvas)
|
||||
|
||||
const before = await getSelectedFill(page)
|
||||
|
||||
await page.locator('[data-test-id="fill-picker-swatch"]').first().click()
|
||||
await expect(page.locator('[data-test-id="fill-picker-tab-solid"]')).toBeVisible()
|
||||
|
||||
await dragSlider(page, canvas, 'color-slider-hue', 0.7)
|
||||
|
||||
const after = await getSelectedFill(page)
|
||||
|
||||
expect(after).not.toBeNull()
|
||||
expect(before?.fill?.color.r !== after?.fill?.color.r || before?.fill?.color.g !== after?.fill?.color.g || before?.fill?.color.b !== after?.fill?.color.b).toBe(true)
|
||||
})
|
||||
|
||||
|
||||
test('demo card fill changes from hsb saturation and brightness sliders', async ({ page }) => {
|
||||
const canvas = new CanvasHelper(page)
|
||||
await selectDemoCard(page, canvas)
|
||||
|
||||
await page.locator('[data-test-id="fill-picker-swatch"]').first().click()
|
||||
await expect(page.locator('[data-test-id="fill-picker-tab-solid"]')).toBeVisible()
|
||||
await page.locator('[data-test-id="color-format-select"]').click()
|
||||
await page.getByRole('option', { name: 'HSB', exact: true }).click()
|
||||
|
||||
const beforeS = await getSelectedFill(page)
|
||||
await dragSlider(page, canvas, 'color-slider-hsb-s', 0.5)
|
||||
const afterS = await getSelectedFill(page)
|
||||
|
||||
expect(afterS).not.toBeNull()
|
||||
expect(beforeS?.fill?.color.r !== afterS?.fill?.color.r || beforeS?.fill?.color.g !== afterS?.fill?.color.g || beforeS?.fill?.color.b !== afterS?.fill?.color.b).toBe(true)
|
||||
|
||||
const beforeB = await getSelectedFill(page)
|
||||
await dragSlider(page, canvas, 'color-slider-hsb-b', 0.25)
|
||||
const afterB = await getSelectedFill(page)
|
||||
|
||||
expect(afterB).not.toBeNull()
|
||||
expect(beforeB?.fill?.color.r !== afterB?.fill?.color.r || beforeB?.fill?.color.g !== afterB?.fill?.color.g || beforeB?.fill?.color.b !== afterB?.fill?.color.b).toBe(true)
|
||||
})
|
||||
126
tests/e2e/color-picker.spec.ts
Normal file
126
tests/e2e/color-picker.spec.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '../helpers/canvas'
|
||||
|
||||
let page: Page
|
||||
let canvas: CanvasHelper
|
||||
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await page.goto('/')
|
||||
canvas = new CanvasHelper(page)
|
||||
await canvas.waitForInit()
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close()
|
||||
})
|
||||
|
||||
async function getSelectedId() {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
return [...store.state.selectedIds][0] ?? null
|
||||
})
|
||||
}
|
||||
|
||||
async function getSelectedFill() {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
const id = [...store.state.selectedIds][0]
|
||||
if (!id) return null
|
||||
const node = store.graph.getNode(id)
|
||||
return node?.fills?.[0] ?? null
|
||||
})
|
||||
}
|
||||
|
||||
async function openFillPicker() {
|
||||
const solidTab = page.locator('[data-test-id="fill-picker-tab-solid"]')
|
||||
if (await solidTab.isVisible().catch(() => false)) return
|
||||
const swatch = page.locator('[data-test-id="fill-picker-swatch"]').first()
|
||||
await swatch.click()
|
||||
await expect(solidTab).toBeVisible()
|
||||
}
|
||||
|
||||
async function chooseFormat(label: 'RGB' | 'HSL' | 'HSB' | 'OkHCL') {
|
||||
await page.locator('[data-test-id="color-format-select"]').click()
|
||||
await page.getByRole('option', { name: label, exact: true }).click()
|
||||
}
|
||||
|
||||
async function dragSlider(testId: string, ratio: number) {
|
||||
const slider = page.locator(`[data-test-id="${testId}"] input[type="range"]`)
|
||||
const box = await slider.boundingBox()
|
||||
if (!box) throw new Error(`Missing slider: ${testId}`)
|
||||
const y = box.y + box.height / 2
|
||||
await page.mouse.move(box.x + 2, y)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(box.x + Math.max(2, Math.min(box.width - 2, box.width * ratio)), y, {
|
||||
steps: 20
|
||||
})
|
||||
await page.mouse.up()
|
||||
await canvas.waitForRender()
|
||||
}
|
||||
|
||||
test('rgb hue slider updates selected fill color', async () => {
|
||||
await canvas.clearCanvas()
|
||||
await canvas.drawRect(100, 100, 160, 120)
|
||||
await canvas.waitForRender()
|
||||
|
||||
await openFillPicker()
|
||||
const before = await getSelectedFill()
|
||||
await dragSlider('color-slider-hue', 0.65)
|
||||
const after = await getSelectedFill()
|
||||
|
||||
expect(after).not.toBeNull()
|
||||
expect(before?.color.r !== after?.color.r || before?.color.g !== after?.color.g || before?.color.b !== after?.color.b).toBe(true)
|
||||
})
|
||||
|
||||
test('rgb alpha slider updates fill opacity and alpha', async () => {
|
||||
await openFillPicker()
|
||||
await dragSlider('color-slider-alpha', 0.3)
|
||||
const after = await getSelectedFill()
|
||||
|
||||
expect(after).not.toBeNull()
|
||||
expect(after?.opacity).toBeLessThan(1)
|
||||
expect(after?.color.a).toBeCloseTo(after?.opacity ?? 0, 3)
|
||||
})
|
||||
|
||||
test('hsl saturation slider changes saturation', async () => {
|
||||
await openFillPicker()
|
||||
await chooseFormat('HSL')
|
||||
const before = await getSelectedFill()
|
||||
await dragSlider('color-slider-hsl-s', 0.2)
|
||||
const after = await getSelectedFill()
|
||||
|
||||
expect(after).not.toBeNull()
|
||||
expect(before?.color.r !== after?.color.r || before?.color.g !== after?.color.g || before?.color.b !== after?.color.b).toBe(true)
|
||||
})
|
||||
|
||||
test('hsl lightness slider changes color independently', async () => {
|
||||
await openFillPicker()
|
||||
await chooseFormat('HSL')
|
||||
const before = await getSelectedFill()
|
||||
await dragSlider('color-slider-hsl-l', 0.8)
|
||||
const after = await getSelectedFill()
|
||||
|
||||
expect(after).not.toBeNull()
|
||||
expect(before?.color.r !== after?.color.r || before?.color.g !== after?.color.g || before?.color.b !== after?.color.b).toBe(true)
|
||||
})
|
||||
|
||||
test('hsb saturation and brightness sliders both affect fill color', async () => {
|
||||
await openFillPicker()
|
||||
await chooseFormat('HSB')
|
||||
|
||||
const beforeS = await getSelectedFill()
|
||||
await dragSlider('color-slider-hsb-s', 0.15)
|
||||
const afterS = await getSelectedFill()
|
||||
expect(afterS).not.toBeNull()
|
||||
expect(beforeS?.color.r !== afterS?.color.r || beforeS?.color.g !== afterS?.color.g || beforeS?.color.b !== afterS?.color.b).toBe(true)
|
||||
|
||||
const beforeB = await getSelectedFill()
|
||||
await dragSlider('color-slider-hsb-b', 0.9)
|
||||
const afterB = await getSelectedFill()
|
||||
expect(afterB).not.toBeNull()
|
||||
expect(beforeB?.color.r !== afterB?.color.r || beforeB?.color.g !== afterB?.color.g || beforeB?.color.b !== afterB?.color.b).toBe(true)
|
||||
})
|
||||
108
tests/e2e/stroke-picker.spec.ts
Normal file
108
tests/e2e/stroke-picker.spec.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
import { CanvasHelper } from '../helpers/canvas'
|
||||
|
||||
async function dragSlider(page: Page, canvas: CanvasHelper, testId: string, ratio: number) {
|
||||
const slider = page.locator(`[data-test-id="${testId}"] input[type="range"]`)
|
||||
const box = await slider.boundingBox()
|
||||
if (!box) throw new Error(`Missing slider: ${testId}`)
|
||||
const y = box.y + box.height / 2
|
||||
await page.mouse.move(box.x + 2, y)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(box.x + Math.max(2, Math.min(box.width - 2, box.width * ratio)), y, {
|
||||
steps: 20
|
||||
})
|
||||
await page.mouse.up()
|
||||
await canvas.waitForRender()
|
||||
}
|
||||
|
||||
async function openStrokePicker(page: Page) {
|
||||
await page.locator('[data-test-id="stroke-item"] [data-test-id="color-picker-popover"]').waitFor({ state: 'detached' }).catch(() => {})
|
||||
await page.locator('[data-test-id="stroke-item"] button').first().click()
|
||||
await expect(page.locator('[data-test-id="color-picker-popover"]')).toBeVisible()
|
||||
}
|
||||
|
||||
async function chooseFormat(page: Page, label: 'RGB' | 'HSL' | 'HSB' | 'OkHCL') {
|
||||
await page.locator('[data-test-id="color-format-select"]').click()
|
||||
await page.getByRole('option', { name: label, exact: true }).click()
|
||||
}
|
||||
|
||||
async function getSelectedStroke(page: Page) {
|
||||
return page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
const id = [...store.state.selectedIds][0]
|
||||
const node = store.graph.getNode(id)
|
||||
return node?.strokes?.[0] ?? null
|
||||
})
|
||||
}
|
||||
|
||||
test('stroke picker updates stroke color on a rectangle', async ({ page }) => {
|
||||
const canvas = new CanvasHelper(page)
|
||||
await page.goto('/')
|
||||
await canvas.waitForInit()
|
||||
|
||||
await canvas.drawRect(120, 120, 180, 120)
|
||||
await page.locator('[data-test-id="stroke-section-add"]').click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
const before = await getSelectedStroke(page)
|
||||
await openStrokePicker(page)
|
||||
await dragSlider(page, canvas, 'color-slider-hue', 0.7)
|
||||
const after = await getSelectedStroke(page)
|
||||
|
||||
expect(after).not.toBeNull()
|
||||
expect(before?.color.r !== after?.color.r || before?.color.g !== after?.color.g || before?.color.b !== after?.color.b).toBe(true)
|
||||
})
|
||||
|
||||
test('stroke picker alpha slider updates stroke opacity and alpha', async ({ page }) => {
|
||||
const canvas = new CanvasHelper(page)
|
||||
await page.goto('/')
|
||||
await canvas.waitForInit()
|
||||
|
||||
await canvas.drawRect(120, 120, 180, 120)
|
||||
await page.locator('[data-test-id="stroke-section-add"]').click()
|
||||
await canvas.waitForRender()
|
||||
|
||||
await openStrokePicker(page)
|
||||
await dragSlider(page, canvas, 'color-slider-alpha', 0.25)
|
||||
const after = await getSelectedStroke(page)
|
||||
|
||||
expect(after).not.toBeNull()
|
||||
expect(after?.opacity).toBeLessThan(1)
|
||||
expect(after?.color.a).toBeCloseTo(after?.opacity ?? 0, 3)
|
||||
})
|
||||
|
||||
test('stroke picker hsb saturation and brightness sliders update stroke color on demo card', async ({ page }) => {
|
||||
const canvas = new CanvasHelper(page)
|
||||
await page.goto('/demo')
|
||||
await canvas.waitForInit()
|
||||
|
||||
await page.evaluate(() => {
|
||||
const store = window.__OPEN_PENCIL_STORE__!
|
||||
const nodes = Array.from(store.graph.nodes.values())
|
||||
const card = nodes.find((node) => node.name === 'Card' && node.type === 'COMPONENT')
|
||||
?? nodes.find((node) => node.name === 'Card')
|
||||
if (!card) throw new Error('Card not found')
|
||||
const stroke = { color: { r: 0.9, g: 0.9, b: 0.92, a: 1 }, weight: 1, opacity: 1, visible: true, align: 'INSIDE' }
|
||||
store.updateNodeWithUndo(card.id, { strokes: [stroke] }, 'Add demo card stroke')
|
||||
store.state.selectedIds = new Set([card.id])
|
||||
store.requestRender()
|
||||
})
|
||||
await canvas.waitForRender()
|
||||
|
||||
await expect(page.locator('[data-test-id="stroke-item"]')).toBeVisible()
|
||||
await openStrokePicker(page)
|
||||
await chooseFormat(page, 'HSB')
|
||||
|
||||
const beforeS = await getSelectedStroke(page)
|
||||
await dragSlider(page, canvas, 'color-slider-hsb-s', 0.6)
|
||||
const afterS = await getSelectedStroke(page)
|
||||
expect(afterS).not.toBeNull()
|
||||
expect(beforeS?.color.r !== afterS?.color.r || beforeS?.color.g !== afterS?.color.g || beforeS?.color.b !== afterS?.color.b).toBe(true)
|
||||
|
||||
const beforeB = await getSelectedStroke(page)
|
||||
await dragSlider(page, canvas, 'color-slider-hsb-b', 0.2)
|
||||
const afterB = await getSelectedStroke(page)
|
||||
expect(afterB).not.toBeNull()
|
||||
expect(beforeB?.color.r !== afterB?.color.r || beforeB?.color.g !== afterB?.color.g || beforeB?.color.b !== afterB?.color.b).toBe(true)
|
||||
})
|
||||
67
tests/engine/color-picker-model.test.ts
Normal file
67
tests/engine/color-picker-model.test.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import {
|
||||
createColorPickerModel,
|
||||
fromPercent,
|
||||
toPercent,
|
||||
updateAlpha,
|
||||
updateHSBChannel,
|
||||
updateHSLChannel,
|
||||
updateHue,
|
||||
updateRGBChannel
|
||||
} from '@/components/color-picker/model'
|
||||
|
||||
import type { Color } from '@open-pencil/core'
|
||||
|
||||
describe('color picker model', () => {
|
||||
const base: Color = { r: 0.4, g: 0.2, b: 0.6, a: 0.75 }
|
||||
|
||||
test('updates hue from the shared slider', () => {
|
||||
const model = createColorPickerModel(base)
|
||||
const updated = updateHue(model, 180)
|
||||
expect(updated).not.toEqual(base)
|
||||
expect(updated.a).toBeCloseTo(base.a, 5)
|
||||
})
|
||||
|
||||
test('updates hue from grayscale colors without staying neutral', () => {
|
||||
const grayscale: Color = { r: 1, g: 1, b: 1, a: 1 }
|
||||
const updated = updateHue(createColorPickerModel(grayscale), 220)
|
||||
expect(updated.r === updated.g && updated.g === updated.b).toBe(false)
|
||||
})
|
||||
|
||||
test('updates alpha independently', () => {
|
||||
const updated = updateAlpha(base, 0.25)
|
||||
expect(updated.a).toBeCloseTo(0.25, 5)
|
||||
expect(updated.r).toBeCloseTo(base.r, 5)
|
||||
})
|
||||
|
||||
test('updates rgb channels in 0-255 space', () => {
|
||||
const updated = updateRGBChannel(base, 'r', 255)
|
||||
expect(updated.r).toBeCloseTo(1, 5)
|
||||
expect(updated.g).toBeCloseTo(base.g, 5)
|
||||
})
|
||||
|
||||
test('updates hsl normalized channels smoothly', () => {
|
||||
const model = createColorPickerModel(base)
|
||||
const updated = updateHSLChannel(model, 's', 12.3)
|
||||
expect(updated.a).toBeCloseTo(base.a, 5)
|
||||
expect(updated).not.toEqual(base)
|
||||
})
|
||||
|
||||
test('updates hsl saturation from grayscale colors without staying neutral', () => {
|
||||
const grayscale: Color = { r: 1, g: 1, b: 1, a: 1 }
|
||||
const updated = updateHSLChannel(createColorPickerModel(grayscale), 's', 40)
|
||||
expect(updated.r === updated.g && updated.g === updated.b).toBe(false)
|
||||
})
|
||||
|
||||
test('updates hsb normalized channels smoothly', () => {
|
||||
const model = createColorPickerModel(base)
|
||||
const updated = updateHSBChannel(model, 'b', 45.6)
|
||||
expect(updated.a).toBeCloseTo(base.a, 5)
|
||||
expect(updated).not.toEqual(base)
|
||||
})
|
||||
|
||||
test('percent helpers roundtrip safely', () => {
|
||||
expect(fromPercent(toPercent(0.347))).toBeCloseTo(0.35, 2)
|
||||
})
|
||||
})
|
||||
34
tests/engine/solid-color-commit.test.ts
Normal file
34
tests/engine/solid-color-commit.test.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { applySolidFillColor, applySolidStrokeColor } from '@/components/color-picker/solid-color-commit'
|
||||
|
||||
import type { Fill, Stroke } from '@open-pencil/core'
|
||||
|
||||
describe('solid color commit helpers', () => {
|
||||
test('syncs fill opacity with color alpha', () => {
|
||||
const fill: Fill = {
|
||||
type: 'SOLID',
|
||||
visible: true,
|
||||
opacity: 1,
|
||||
color: { r: 1, g: 0, b: 0, a: 1 }
|
||||
}
|
||||
|
||||
const updated = applySolidFillColor(fill, { r: 0, g: 1, b: 0, a: 0.4 })
|
||||
expect(updated.color.a).toBeCloseTo(0.4, 5)
|
||||
expect(updated.opacity).toBeCloseTo(0.4, 5)
|
||||
})
|
||||
|
||||
test('syncs stroke opacity with color alpha', () => {
|
||||
const stroke: Stroke = {
|
||||
color: { r: 0, g: 0, b: 1, a: 1 },
|
||||
weight: 1,
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
align: 'CENTER'
|
||||
}
|
||||
|
||||
const updated = applySolidStrokeColor({ r: 1, g: 1, b: 0, a: 0.2 })
|
||||
expect(updated.color?.a).toBeCloseTo(0.2, 5)
|
||||
expect(updated.opacity).toBeCloseTo(0.2, 5)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue