From 8c2d0d7706d3318314ff921780df2e612db500dd Mon Sep 17 00:00:00 2001 From: Yohann Rabatel Date: Wed, 25 Mar 2026 10:45:45 +0100 Subject: [PATCH 01/23] merge build and tests workflows --- .../{test-ui.yaml => build-and-test.yaml} | 30 +++++++++++++++++-- .github/workflows/build-build.yaml | 28 ----------------- .github/workflows/build-tests.yaml | 28 ----------------- .github/workflows/build-tsc.yaml | 28 ----------------- 4 files changed, 28 insertions(+), 86 deletions(-) rename .github/workflows/{test-ui.yaml => build-and-test.yaml} (52%) delete mode 100644 .github/workflows/build-build.yaml delete mode 100644 .github/workflows/build-tests.yaml delete mode 100644 .github/workflows/build-tsc.yaml diff --git a/.github/workflows/test-ui.yaml b/.github/workflows/build-and-test.yaml similarity index 52% rename from .github/workflows/test-ui.yaml rename to .github/workflows/build-and-test.yaml index bbc9470..8e2c5f8 100644 --- a/.github/workflows/test-ui.yaml +++ b/.github/workflows/build-and-test.yaml @@ -1,4 +1,4 @@ -name: UI Tests +name: Build and Test on: pull_request: @@ -6,6 +6,29 @@ on: - main jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "24.x" + + - name: Install dependencies + run: pnpm install + + - name: Run build + run: pnpm build + + - name: Run tsc check + run: pnpm tsc --project tsconfig.json + test: runs-on: ubuntu-latest container: @@ -27,5 +50,8 @@ jobs: - name: Install dependencies run: pnpm install - - name: Run tests + - name: Run unit tests + run: pnpm test + + - name: Run UI tests run: pnpm test-storybook diff --git a/.github/workflows/build-build.yaml b/.github/workflows/build-build.yaml deleted file mode 100644 index 35ecb6e..0000000 --- a/.github/workflows/build-build.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: Run build - -on: - pull_request: - branches: - - main - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "24.x" - - - name: Install dependencies - run: pnpm install - - - name: Run build - run: pnpm build diff --git a/.github/workflows/build-tests.yaml b/.github/workflows/build-tests.yaml deleted file mode 100644 index 457500b..0000000 --- a/.github/workflows/build-tests.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: Run unit tests - -on: - pull_request: - branches: - - main - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "24.x" - - - name: Install dependencies - run: pnpm install - - - name: Run tests - run: pnpm test diff --git a/.github/workflows/build-tsc.yaml b/.github/workflows/build-tsc.yaml deleted file mode 100644 index a4779fd..0000000 --- a/.github/workflows/build-tsc.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: Run tsc check - -on: - pull_request: - branches: - - main - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "24.x" - - - name: Install dependencies - run: pnpm install - - - name: Run tests - run: pnpm tsc --project tsconfig.json From 86b776d5087f06ccd23c83e50e2a7ccfe84ae481 Mon Sep 17 00:00:00 2001 From: Yohann Rabatel Date: Wed, 25 Mar 2026 10:53:14 +0100 Subject: [PATCH 02/23] add badges --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 738da69..fdaf894 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # React Paint +[![npm version](https://img.shields.io/npm/v/@yorab/react-paint)](https://www.npmjs.com/package/@yorab/react-paint) +[![License: MIT](https://img.shields.io/npm/l/@yorab/react-paint)](https://github.com/YoRab/react-paint-editor/blob/main/LICENSE.txt) +[![Build and Test](https://github.com/YoRab/react-paint-editor/actions/workflows/build-and-test.yaml/badge.svg)](https://github.com/YoRab/react-paint-editor/actions/workflows/build-and-test.yaml) + An open-source canvas-based library for React used for image annotation or as a digital whiteboard. ## Features From 495f9771830a097e7575ddabb41b5e4959ae5e5a Mon Sep 17 00:00:00 2001 From: Yohann Rabatel Date: Wed, 25 Mar 2026 11:33:06 +0100 Subject: [PATCH 03/23] add simple draw tests --- stories/tests/Canvas.stories.tsx | 103 -------- stories/tests/Draw.stories.tsx | 419 +++++++++++++++++++++++++++++++ 2 files changed, 419 insertions(+), 103 deletions(-) delete mode 100644 stories/tests/Canvas.stories.tsx create mode 100644 stories/tests/Draw.stories.tsx diff --git a/stories/tests/Canvas.stories.tsx b/stories/tests/Canvas.stories.tsx deleted file mode 100644 index 631dc60..0000000 --- a/stories/tests/Canvas.stories.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react-vite' -import { expect, userEvent, within } from 'storybook/test' -import { Canvas, Editor, type StateData, useReactPaint } from '../../src/index' - -const getCurrentDataRef = { current: null as null | (() => StateData) } - -const ReactPaintWrapper = (args: Parameters[0]) => { - const { editorProps, canvasProps, getCurrentData } = useReactPaint(args) - getCurrentDataRef.current = getCurrentData - - return ( - - - - ) -} - -const meta = { - title: 'Tests/Canvas', - component: ReactPaintWrapper, - parameters: { - layout: 'centered' - } -} satisfies Meta - -export default meta -type Story = StoryObj - -export const DrawBrushShape: Story = { - args: {}, - play: async ({ canvasElement }) => { - const view = within(canvasElement) - - // 1. Sélectionner la brosse (gestion bouton direct ou sous-menu) - let brushButton = view.queryByTestId('tool-brush') - if (!brushButton) { - const toggleBtn = await view.findByRole('button', { name: 'Toggle tools' }) - await userEvent.click(toggleBtn) - brushButton = await view.findByTestId('tool-brush') - } - await userEvent.click(brushButton) - - // 2. Localiser le draw canvas - const drawCanvas = await view.findByTestId('draw-canvas') - - // Coordonnées relatives à la position du canvas dans le viewport - const rect = drawCanvas.getBoundingClientRect() - const cx = rect.left + rect.width / 2 - const cy = rect.top + rect.height / 2 - const r = Math.min(rect.width, rect.height) * 0.2 - const STEPS = 16 - - // 3. Mousedown sur le canvas (déclenche focusin → isInsideCanvas = true) - const user = userEvent.setup() - await user.pointer({ - target: drawCanvas, - keys: '[MouseLeft>]', - coords: { x: cx + r, y: cy } - }) - - // Laisser React traiter focusin et enregistrer le listener mouseup sur document - await new Promise(res => setTimeout(res, 50)) - - // 4. Tracé du cercle - for (let i = 1; i <= STEPS; i++) { - const angle = (i / STEPS) * 2 * Math.PI - await user.pointer({ - target: drawCanvas, - coords: { x: cx + r * Math.cos(angle), y: cy + r * Math.sin(angle) } - }) - } - - // 5. Mouseup → sauvegarde du tracé - await user.pointer({ - target: drawCanvas, - keys: '[/MouseLeft]', - coords: { x: cx + r, y: cy } - }) - await new Promise(res => setTimeout(res, 100)) - - // 6. Comparer les données exportées - expect(getCurrentDataRef.current).not.toBeNull() - const data = getCurrentDataRef.current!() - - expect(data.shapes).toHaveLength(1) - expect(data.shapes![0]).toMatchObject({ - type: 'brush', - style: { - strokeColor: 'black', - opacity: 100, - lineWidth: 10, - lineDash: 0 - } - }) - expect(data.shapes![0]).not.toHaveProperty('id') - expect(data.shapes![0]).not.toHaveProperty('path') - expect(data.shapes![0]).not.toHaveProperty('computed') - expect(data.shapes![0]).not.toHaveProperty('selection') - const brushShape = data.shapes![0] as { points: [number, number][][] } - expect(brushShape.points).toHaveLength(1) - expect(brushShape.points[0]!.length).toBeGreaterThan(STEPS * 0.5) - } -} diff --git a/stories/tests/Draw.stories.tsx b/stories/tests/Draw.stories.tsx new file mode 100644 index 0000000..d15680d --- /dev/null +++ b/stories/tests/Draw.stories.tsx @@ -0,0 +1,419 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { expect, userEvent, within } from 'storybook/test' +import { Canvas, Editor, type StateData, useReactPaint } from '../../src/index' + +const getCurrentDataRef = { current: null as null | (() => StateData) } + +const ReactPaintWrapper = (args: Parameters[0]) => { + const { editorProps, canvasProps, getCurrentData } = useReactPaint(args) + getCurrentDataRef.current = getCurrentData + + return ( + + + + ) +} + +const meta = { + title: 'Tests/Draw', + component: ReactPaintWrapper, + parameters: { + layout: 'centered' + } +} satisfies Meta + +export default meta +type Story = StoryObj + +// Some tools are grouped inside ToolbarGroup panels +const TOOL_GROUPS: Record = { + line: 'lines', + curve: 'lines', + polygon: 'lines', + rect: 'shapes', + square: 'shapes', + circle: 'shapes', + ellipse: 'shapes' +} + +async function selectTool(view: ReturnType, toolId: string) { + let toolButton = view.queryByTestId(`tool-${toolId}`) + + if (!toolButton) { + // Case 1: toolbar too narrow — "Toggle tools" button is shown + const toggleBtn = view.queryByRole('button', { name: 'Toggle tools' }) + if (toggleBtn) { + await userEvent.click(toggleBtn) + } else { + // Case 2: tool is inside a group panel — open the group first + const groupTitle = TOOL_GROUPS[toolId] + if (groupTitle) { + const groupBtn = await view.findByRole('button', { name: groupTitle }) + await userEvent.click(groupBtn) + } + } + toolButton = await view.findByTestId(`tool-${toolId}`) + } + + await userEvent.click(toolButton) +} + +function assertNoInternalFields(shape: unknown) { + expect(shape).not.toHaveProperty('id') + expect(shape).not.toHaveProperty('path') + expect(shape).not.toHaveProperty('computed') + expect(shape).not.toHaveProperty('selection') +} + +export const DrawBrushShape: Story = { + args: {}, + play: async ({ canvasElement }) => { + const view = within(canvasElement) + + // 1. Select the brush tool + await selectTool(view, 'brush') + + // 2. Find the draw canvas + const drawCanvas = await view.findByTestId('draw-canvas') + + // Coordinates relative to the canvas position in the viewport + const rect = drawCanvas.getBoundingClientRect() + const cx = rect.left + rect.width / 2 + const cy = rect.top + rect.height / 2 + const r = Math.min(rect.width, rect.height) * 0.2 + const STEPS = 16 + + // 3. Mousedown on the canvas (triggers focusin → isInsideCanvas = true) + const user = userEvent.setup() + await user.pointer({ + target: drawCanvas, + keys: '[MouseLeft>]', + coords: { x: cx + r, y: cy } + }) + + // Let React process focusin and register the mouseup listener on document + await new Promise(res => setTimeout(res, 50)) + + // 4. Draw a circle + for (let i = 1; i <= STEPS; i++) { + const angle = (i / STEPS) * 2 * Math.PI + await user.pointer({ + target: drawCanvas, + coords: { x: cx + r * Math.cos(angle), y: cy + r * Math.sin(angle) } + }) + } + + // 5. Mouseup → save the stroke + await user.pointer({ + target: drawCanvas, + keys: '[/MouseLeft]', + coords: { x: cx + r, y: cy } + }) + await new Promise(res => setTimeout(res, 100)) + + // 6. Assert exported data + expect(getCurrentDataRef.current).not.toBeNull() + const data = getCurrentDataRef.current!() + + expect(data.shapes).toHaveLength(1) + expect(data.shapes![0]).toMatchObject({ + type: 'brush', + style: { + strokeColor: 'black', + opacity: 100, + lineWidth: 10, + lineDash: 0 + } + }) + assertNoInternalFields(data.shapes![0]) + const brushShape = data.shapes![0] as { points: [number, number][][] } + expect(brushShape.points).toHaveLength(1) + expect(brushShape.points[0]!.length).toBeGreaterThan(STEPS * 0.5) + } +} + +export const DrawLine: Story = { + args: {}, + play: async ({ canvasElement }) => { + const view = within(canvasElement) + await selectTool(view, 'line') + + const drawCanvas = await view.findByTestId('draw-canvas') + const rect = drawCanvas.getBoundingClientRect() + const cx = rect.left + rect.width / 2 + const cy = rect.top + rect.height / 2 + + const user = userEvent.setup() + await user.pointer({ target: drawCanvas, keys: '[MouseLeft>]', coords: { x: cx - 60, y: cy } }) + await new Promise(res => setTimeout(res, 50)) + await user.pointer({ target: drawCanvas, coords: { x: cx + 60, y: cy } }) + await new Promise(res => setTimeout(res, 50)) + await user.pointer({ target: drawCanvas, keys: '[/MouseLeft]', coords: { x: cx + 60, y: cy } }) + await new Promise(res => setTimeout(res, 100)) + + expect(getCurrentDataRef.current).not.toBeNull() + const data = getCurrentDataRef.current!() + + expect(data.shapes).toHaveLength(1) + expect(data.shapes![0]).toMatchObject({ + type: 'line', + style: { strokeColor: 'black', opacity: 100, lineWidth: 1, lineDash: 0, lineArrow: 0 } + }) + assertNoInternalFields(data.shapes![0]) + const lineShape = data.shapes![0] as { points: [number, number][] } + expect(lineShape.points).toHaveLength(2) + } +} + +export const DrawRect: Story = { + args: {}, + play: async ({ canvasElement }) => { + const view = within(canvasElement) + await selectTool(view, 'rect') + + const drawCanvas = await view.findByTestId('draw-canvas') + const rect = drawCanvas.getBoundingClientRect() + const cx = rect.left + rect.width / 2 + const cy = rect.top + rect.height / 2 + + const user = userEvent.setup() + await user.pointer({ target: drawCanvas, keys: '[MouseLeft>]', coords: { x: cx - 60, y: cy - 40 } }) + await new Promise(res => setTimeout(res, 50)) + await user.pointer({ target: drawCanvas, coords: { x: cx + 60, y: cy + 40 } }) + await new Promise(res => setTimeout(res, 50)) + await user.pointer({ target: drawCanvas, keys: '[/MouseLeft]', coords: { x: cx + 60, y: cy + 40 } }) + await new Promise(res => setTimeout(res, 100)) + + expect(getCurrentDataRef.current).not.toBeNull() + const data = getCurrentDataRef.current!() + + expect(data.shapes).toHaveLength(1) + expect(data.shapes![0]).toMatchObject({ + type: 'rect', + style: { strokeColor: 'black', fillColor: 'transparent', opacity: 100, lineWidth: 1, lineDash: 0 } + }) + assertNoInternalFields(data.shapes![0]) + const rectShape = data.shapes![0] as { width: number; height: number } + expect(rectShape.width).toBeGreaterThan(0) + expect(rectShape.height).toBeGreaterThan(0) + } +} + +export const DrawSquare: Story = { + args: {}, + play: async ({ canvasElement }) => { + const view = within(canvasElement) + await selectTool(view, 'square') + + const drawCanvas = await view.findByTestId('draw-canvas') + const rect = drawCanvas.getBoundingClientRect() + const cx = rect.left + rect.width / 2 + const cy = rect.top + rect.height / 2 + + const user = userEvent.setup() + await user.pointer({ target: drawCanvas, keys: '[MouseLeft>]', coords: { x: cx - 50, y: cy - 50 } }) + await new Promise(res => setTimeout(res, 50)) + await user.pointer({ target: drawCanvas, coords: { x: cx + 50, y: cy + 50 } }) + await new Promise(res => setTimeout(res, 50)) + await user.pointer({ target: drawCanvas, keys: '[/MouseLeft]', coords: { x: cx + 50, y: cy + 50 } }) + await new Promise(res => setTimeout(res, 100)) + + expect(getCurrentDataRef.current).not.toBeNull() + const data = getCurrentDataRef.current!() + + expect(data.shapes).toHaveLength(1) + expect(data.shapes![0]).toMatchObject({ + type: 'square', + style: { strokeColor: 'black', fillColor: 'transparent', opacity: 100, lineWidth: 1, lineDash: 0 } + }) + assertNoInternalFields(data.shapes![0]) + const squareShape = data.shapes![0] as { width: number; height: number } + expect(squareShape.width).toBeGreaterThan(0) + expect(squareShape.width).toBe(squareShape.height) + } +} + +export const DrawCircle: Story = { + args: {}, + play: async ({ canvasElement }) => { + const view = within(canvasElement) + await selectTool(view, 'circle') + + const drawCanvas = await view.findByTestId('draw-canvas') + const rect = drawCanvas.getBoundingClientRect() + const cx = rect.left + rect.width / 2 + const cy = rect.top + rect.height / 2 + + const user = userEvent.setup() + await user.pointer({ target: drawCanvas, keys: '[MouseLeft>]', coords: { x: cx - 50, y: cy } }) + await new Promise(res => setTimeout(res, 50)) + await user.pointer({ target: drawCanvas, coords: { x: cx + 50, y: cy } }) + await new Promise(res => setTimeout(res, 50)) + await user.pointer({ target: drawCanvas, keys: '[/MouseLeft]', coords: { x: cx + 50, y: cy } }) + await new Promise(res => setTimeout(res, 100)) + + expect(getCurrentDataRef.current).not.toBeNull() + const data = getCurrentDataRef.current!() + + expect(data.shapes).toHaveLength(1) + expect(data.shapes![0]).toMatchObject({ + type: 'circle', + style: { strokeColor: 'black', fillColor: 'transparent', opacity: 100, lineWidth: 1, lineDash: 0 } + }) + assertNoInternalFields(data.shapes![0]) + const circleShape = data.shapes![0] as { radius: number } + expect(circleShape.radius).toBeGreaterThan(0) + } +} + +export const DrawEllipse: Story = { + args: {}, + play: async ({ canvasElement }) => { + const view = within(canvasElement) + await selectTool(view, 'ellipse') + + const drawCanvas = await view.findByTestId('draw-canvas') + const rect = drawCanvas.getBoundingClientRect() + const cx = rect.left + rect.width / 2 + const cy = rect.top + rect.height / 2 + + const user = userEvent.setup() + await user.pointer({ target: drawCanvas, keys: '[MouseLeft>]', coords: { x: cx - 60, y: cy - 30 } }) + await new Promise(res => setTimeout(res, 50)) + await user.pointer({ target: drawCanvas, coords: { x: cx + 60, y: cy + 30 } }) + await new Promise(res => setTimeout(res, 50)) + await user.pointer({ target: drawCanvas, keys: '[/MouseLeft]', coords: { x: cx + 60, y: cy + 30 } }) + await new Promise(res => setTimeout(res, 100)) + + expect(getCurrentDataRef.current).not.toBeNull() + const data = getCurrentDataRef.current!() + + expect(data.shapes).toHaveLength(1) + expect(data.shapes![0]).toMatchObject({ + type: 'ellipse', + style: { strokeColor: 'black', fillColor: 'transparent', opacity: 100, lineWidth: 1, lineDash: 0 } + }) + assertNoInternalFields(data.shapes![0]) + const ellipseShape = data.shapes![0] as { radiusX: number; radiusY: number } + expect(ellipseShape.radiusX).toBeGreaterThan(0) + expect(ellipseShape.radiusY).toBeGreaterThan(0) + } +} + +export const DrawPolygon: Story = { + args: {}, + play: async ({ canvasElement }) => { + const view = within(canvasElement) + await selectTool(view, 'polygon') + + const drawCanvas = await view.findByTestId('draw-canvas') + const rect = drawCanvas.getBoundingClientRect() + const cx = rect.left + rect.width / 2 + const cy = rect.top + rect.height / 2 + + const user = userEvent.setup() + + // Click 3 vertices + await user.pointer({ target: drawCanvas, keys: '[MouseLeft]', coords: { x: cx, y: cy - 60 } }) + await new Promise(res => setTimeout(res, 50)) + await user.pointer({ target: drawCanvas, keys: '[MouseLeft]', coords: { x: cx + 60, y: cy + 40 } }) + await new Promise(res => setTimeout(res, 50)) + // Double-click to close the polygon + await user.pointer({ target: drawCanvas, keys: '[MouseLeft][MouseLeft]', coords: { x: cx - 60, y: cy + 40 } }) + await new Promise(res => setTimeout(res, 100)) + + expect(getCurrentDataRef.current).not.toBeNull() + const data = getCurrentDataRef.current!() + + expect(data.shapes).toHaveLength(1) + expect(data.shapes![0]).toMatchObject({ + type: 'polygon', + style: { strokeColor: 'black', fillColor: 'transparent', opacity: 100, lineWidth: 1, lineDash: 0 } + }) + assertNoInternalFields(data.shapes![0]) + const polygonShape = data.shapes![0] as { points: [number, number][] } + expect(polygonShape.points.length).toBeGreaterThanOrEqual(3) + } +} + +export const DrawCurve: Story = { + args: {}, + play: async ({ canvasElement }) => { + const view = within(canvasElement) + await selectTool(view, 'curve') + + const drawCanvas = await view.findByTestId('draw-canvas') + const rect = drawCanvas.getBoundingClientRect() + const cx = rect.left + rect.width / 2 + const cy = rect.top + rect.height / 2 + + const user = userEvent.setup() + + // Click 3 control points + await user.pointer({ target: drawCanvas, keys: '[MouseLeft]', coords: { x: cx - 60, y: cy } }) + await new Promise(res => setTimeout(res, 50)) + await user.pointer({ target: drawCanvas, keys: '[MouseLeft]', coords: { x: cx, y: cy - 60 } }) + await new Promise(res => setTimeout(res, 50)) + // Double-click to finish the curve + await user.pointer({ target: drawCanvas, keys: '[MouseLeft][MouseLeft]', coords: { x: cx + 60, y: cy } }) + await new Promise(res => setTimeout(res, 100)) + + expect(getCurrentDataRef.current).not.toBeNull() + const data = getCurrentDataRef.current!() + + expect(data.shapes).toHaveLength(1) + expect(data.shapes![0]).toMatchObject({ + type: 'curve', + style: { strokeColor: 'black', fillColor: 'transparent', opacity: 100, lineWidth: 1, lineDash: 0 } + }) + assertNoInternalFields(data.shapes![0]) + const curveShape = data.shapes![0] as { points: [number, number][] } + expect(curveShape.points.length).toBeGreaterThanOrEqual(3) + } +} + +export const DrawText: Story = { + args: {}, + play: async ({ canvasElement }) => { + const view = within(canvasElement) + await selectTool(view, 'text') + + const drawCanvas = await view.findByTestId('draw-canvas') + const rect = drawCanvas.getBoundingClientRect() + const cx = rect.left + rect.width / 2 + const cy = rect.top + rect.height / 2 + + const user = userEvent.setup() + + // First click: creates the text shape (tool automatically switches back to selection) + await user.pointer({ target: drawCanvas, keys: '[MouseLeft]', coords: { x: cx, y: cy } }) + + // Second click on the same shape within 300ms → triggers double-click → enters textedition mode + await new Promise(res => setTimeout(res, 50)) + await user.pointer({ target: drawCanvas, keys: '[MouseLeft]', coords: { x: cx, y: cy } }) + await new Promise(res => setTimeout(res, 100)) + + // The contentEditable is now visible — clear the default value and type + const textbox = await view.findByRole('textbox') + await userEvent.clear(textbox) + await userEvent.type(textbox, 'Hello') + await new Promise(res => setTimeout(res, 50)) + + // Click outside to finalize + await user.pointer({ target: drawCanvas, keys: '[MouseLeft]', coords: { x: cx - 150, y: cy - 150 } }) + await new Promise(res => setTimeout(res, 100)) + + expect(getCurrentDataRef.current).not.toBeNull() + const data = getCurrentDataRef.current!() + + expect(data.shapes).toHaveLength(1) + expect(data.shapes![0]).toMatchObject({ + type: 'text', + style: { strokeColor: 'black', opacity: 100 } + }) + assertNoInternalFields(data.shapes![0]) + const textShape = data.shapes![0] as { value: string[] } + expect(textShape.value).toEqual(['Hello']) + } +} From 1088c0f2b2a958303969314c6167cf1322774049 Mon Sep 17 00:00:00 2001 From: Yohann Rabatel Date: Wed, 25 Mar 2026 13:37:11 +0100 Subject: [PATCH 04/23] interaction tests with custom settings --- .../components/settings/SettingsBar.tsx | 4 + .../components/settings/ToggleField.tsx | 5 +- stories/tests/Draw.stories.tsx | 166 ++++++++++++++++-- 3 files changed, 154 insertions(+), 21 deletions(-) diff --git a/src/editor/components/settings/SettingsBar.tsx b/src/editor/components/settings/SettingsBar.tsx index 9df7479..db382b3 100644 --- a/src/editor/components/settings/SettingsBar.tsx +++ b/src/editor/components/settings/SettingsBar.tsx @@ -142,6 +142,7 @@ const SettingsItems = ({ disabled={disabled} field='fontBold' icon={boldIcon} + title='Gras' valueChanged={handleShapeFontFamilyChange} values={selectedShapeTool.settings.fontBold.values} value={selectedShape.style?.fontBold} @@ -154,6 +155,7 @@ const SettingsItems = ({ disabled={disabled} field='fontItalic' icon={italicIcon} + title='Italique' valueChanged={handleShapeFontFamilyChange} values={selectedShapeTool.settings.fontItalic.values} value={selectedShape.style?.fontItalic} @@ -275,6 +277,7 @@ const SettingsItems = ({ disabled={disabled} field='fontBold' icon={boldIcon} + title='Gras' valueChanged={handleShapeFontFamilyChange} values={activeTool.settings.fontBold.values} value={activeTool.settings.fontBold.default} @@ -287,6 +290,7 @@ const SettingsItems = ({ disabled={disabled} field='fontItalic' icon={italicIcon} + title='Italique' valueChanged={handleShapeFontFamilyChange} values={activeTool.settings.fontItalic.values} value={activeTool.settings.fontItalic.default} diff --git a/src/editor/components/settings/ToggleField.tsx b/src/editor/components/settings/ToggleField.tsx index 73d79f9..4189830 100644 --- a/src/editor/components/settings/ToggleField.tsx +++ b/src/editor/components/settings/ToggleField.tsx @@ -8,12 +8,13 @@ type ShapeStyleSelectType = { disabled?: boolean | undefined field: string icon: string + title?: string values: boolean[] value?: boolean | undefined valueChanged: (field: string, value: boolean) => void } -const ToggleField = ({ setSelectedSettings, disabled = false, field, icon, values, value = false, valueChanged }: ShapeStyleSelectType) => { +const ToggleField = ({ setSelectedSettings, disabled = false, field, icon, title, values, value = false, valueChanged }: ShapeStyleSelectType) => { const [customKey] = useState(uniqueId('settings_')) const handleClick = () => { @@ -25,7 +26,7 @@ const ToggleField = ({ setSelectedSettings, disabled = false, field, icon, value if (values.length !== 2) return null - return