diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..1e88565 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,65 @@ +name: E2E Tests + +on: + push: + branches: [main] + pull_request: + types: [opened, reopened, edited, synchronize] + branches: [main] + paths: + - 'frontend/**' + - '.github/workflows/e2e.yml' + +jobs: + e2e: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Install pnpm + run: npm install -g pnpm + + - name: Install dependencies + run: pnpm install + working-directory: frontend + + - name: Build frontend + run: pnpm build + working-directory: frontend + + - name: Run Playwright browsers install + run: pnpm exec playwright install --with-deps + working-directory: frontend + + - name: Start preview server + run: pnpm preview --host & + working-directory: frontend + + - name: Wait for frontend + run: npx wait-on http://localhost:4173 + + - name: Run E2E tests + run: pnpm run e2e + working-directory: frontend + + - name: Upload Playwright test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-test-results + path: frontend/test-results/ + if-no-files-found: ignore + + - name: Upload Playwright snapshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-snapshots + path: frontend/e2e/rectangle/*-snapshots/ + if-no-files-found: ignore \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..14ab9e0 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,16 @@ +# Node.js dependencies +node_modules/ + +# Build output +/dist/ + +# Playwright test artifacts (DO NOT ignore *-snapshots/!) +test-results/ +playwright-report/ +playwright-traces/ + +# Ignore Vite cache (optional) +.vite/ + +# Ignore coverage if used +coverage/ \ No newline at end of file diff --git a/frontend/e2e/playwright.config.ts b/frontend/e2e/playwright.config.ts new file mode 100644 index 0000000..47689be --- /dev/null +++ b/frontend/e2e/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ +// workers: 1, + testDir: './', + timeout: 60 * 1000, + retries: 0, + use: { + baseURL: 'http://localhost:4173', + headless: true, + viewport: { width: 1280, height: 800 }, + ignoreHTTPSErrors: true, + video: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + // { + // name: 'webkit-iphone', + // use: { ...devices['iPhone 12'] }, + // }, + ], +}); diff --git a/frontend/e2e/rectangle/draw.e2e.spec.ts b/frontend/e2e/rectangle/draw.e2e.spec.ts new file mode 100644 index 0000000..95967b8 --- /dev/null +++ b/frontend/e2e/rectangle/draw.e2e.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from '@playwright/test'; + +import { CANVAS_SELECTOR, SNAPSHOT_TOLERANCE, start, end, mockWebSocket, showCursor, DELAY } from './rectangle.helpers'; + +test('should draw a new rectangle', async ({ page }) => { + // Mock WebSocket to prevent real connections during tests + await mockWebSocket(page); + // Show a visible cursor overlay for debugging + await showCursor(page); + + // Open the main page + await page.goto('/'); + const canvas = page.locator(CANVAS_SELECTOR); + await expect(canvas).toBeVisible(); + await page.waitForTimeout(DELAY); + + // Clear the canvas + await page.getByRole('button', { name: 'Clear Canvas' }).click(); + await page.waitForTimeout(DELAY); + + // Select the Rectangle tool + await page.getByRole('button', { name: 'Rectangle' }).click(); + await page.waitForTimeout(DELAY); + + // Draw a rectangle + await page.mouse.move(start.x, start.y); + await page.mouse.down(); + await page.mouse.move(end.x, end.y, { steps: 10 }); + await page.mouse.up(); + await page.waitForTimeout(DELAY); + + // Take a screenshot and compare with the snapshot + const afterDraw = await canvas.screenshot(); + expect(afterDraw).toMatchSnapshot('after-draw.png', { maxDiffPixelRatio: SNAPSHOT_TOLERANCE }); +}); diff --git a/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-chromium-darwin.png b/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-chromium-darwin.png new file mode 100644 index 0000000..02a0c1b Binary files /dev/null and b/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-chromium-darwin.png differ diff --git a/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-chromium-linux.png b/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-chromium-linux.png new file mode 100644 index 0000000..8ffe14d Binary files /dev/null and b/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-chromium-linux.png differ diff --git a/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-firefox-darwin.png b/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-firefox-darwin.png new file mode 100644 index 0000000..f69c4ab Binary files /dev/null and b/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-firefox-darwin.png differ diff --git a/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-firefox-linux.png b/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-firefox-linux.png new file mode 100644 index 0000000..2fe086e Binary files /dev/null and b/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-firefox-linux.png differ diff --git a/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-webkit-darwin.png b/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-webkit-darwin.png new file mode 100644 index 0000000..4cfca49 Binary files /dev/null and b/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-webkit-darwin.png differ diff --git a/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-webkit-linux.png b/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-webkit-linux.png new file mode 100644 index 0000000..92135a2 Binary files /dev/null and b/frontend/e2e/rectangle/draw.e2e.spec.ts-snapshots/after-draw-webkit-linux.png differ diff --git a/frontend/e2e/rectangle/move.e2e.spec.ts b/frontend/e2e/rectangle/move.e2e.spec.ts new file mode 100644 index 0000000..11d60dd --- /dev/null +++ b/frontend/e2e/rectangle/move.e2e.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@playwright/test'; + +import { + CANVAS_SELECTOR, + SNAPSHOT_TOLERANCE, + start, + end, + moveFrom, + mockWebSocket, + showCursor, + DELAY, +} from './rectangle.helpers'; + +test('should move rectangle: up 10px, right 20px, down 30px, left 40px', async ({ page }) => { + // Mock WebSocket to prevent real connections during tests + await mockWebSocket(page); + // Show a visible cursor overlay for debugging + await showCursor(page); + + // Open the main page + await page.goto('/'); + const canvas = page.locator(CANVAS_SELECTOR); + await expect(canvas).toBeVisible(); + await page.waitForTimeout(DELAY); + + // Clear the canvas + await page.getByRole('button', { name: 'Clear Canvas' }).click(); + await page.waitForTimeout(DELAY); + + // Select the Rectangle tool and draw a rectangle + await page.getByRole('button', { name: 'Rectangle' }).click(); + await page.waitForTimeout(DELAY); + await page.mouse.move(start.x, start.y); + await page.mouse.down(); + await page.mouse.move(end.x, end.y, { steps: 10 }); + await page.mouse.up(); + await page.waitForTimeout(DELAY); + + // Switch to Select tool + await page.getByTestId('toolbar-select').click(); + await page.waitForTimeout(DELAY); + + // Select the rectangle + await page.mouse.click(moveFrom.x, moveFrom.y); + await page.waitForTimeout(DELAY); + + // Move up 10px + await page.mouse.move(moveFrom.x, moveFrom.y); + await page.mouse.down(); + await page.mouse.move(moveFrom.x, moveFrom.y - 10, { steps: 10 }); + await page.waitForTimeout(DELAY); + + // Move right 20px + await page.mouse.move(moveFrom.x + 20, moveFrom.y - 10, { steps: 10 }); + await page.waitForTimeout(DELAY); + + // Move down 30px + await page.mouse.move(moveFrom.x + 20, moveFrom.y - 10 + 30, { steps: 10 }); + await page.waitForTimeout(DELAY); + + // Move left 40px + await page.mouse.move(moveFrom.x + 20 - 40, moveFrom.y - 10 + 30, { steps: 10 }); + await page.mouse.up(); + await page.waitForTimeout(DELAY); + + // Take a single screenshot and compare with the snapshot + const afterAllMoves = await canvas.screenshot(); + expect(afterAllMoves).toMatchSnapshot('move-all.png', { maxDiffPixelRatio: SNAPSHOT_TOLERANCE }); +}); diff --git a/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-chromium-darwin.png b/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-chromium-darwin.png new file mode 100644 index 0000000..1d054ce Binary files /dev/null and b/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-chromium-darwin.png differ diff --git a/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-chromium-linux.png b/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-chromium-linux.png new file mode 100644 index 0000000..a12390a Binary files /dev/null and b/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-chromium-linux.png differ diff --git a/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-firefox-darwin.png b/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-firefox-darwin.png new file mode 100644 index 0000000..5bb9b32 Binary files /dev/null and b/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-firefox-darwin.png differ diff --git a/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-firefox-linux.png b/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-firefox-linux.png new file mode 100644 index 0000000..82c47f2 Binary files /dev/null and b/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-firefox-linux.png differ diff --git a/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-webkit-darwin.png b/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-webkit-darwin.png new file mode 100644 index 0000000..ee0c11a Binary files /dev/null and b/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-webkit-darwin.png differ diff --git a/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-webkit-linux.png b/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-webkit-linux.png new file mode 100644 index 0000000..298977d Binary files /dev/null and b/frontend/e2e/rectangle/move.e2e.spec.ts-snapshots/move-all-webkit-linux.png differ diff --git a/frontend/e2e/rectangle/rectangle.helpers.ts b/frontend/e2e/rectangle/rectangle.helpers.ts new file mode 100644 index 0000000..ed10a2b --- /dev/null +++ b/frontend/e2e/rectangle/rectangle.helpers.ts @@ -0,0 +1,112 @@ +// Rectangle test helpers +export const CANVAS_SELECTOR = '[data-testid="canvas"]'; +export const SNAPSHOT_TOLERANCE = 0.02; +export const DELAY = 50; + +export const start = { x: 200, y: 200 }; +export const end = { x: 400, y: 350 }; +export const moveFrom = { x: 300, y: 275 }; // center of the rectangle + +export const centerX = (start.x + end.x) / 2; +export const centerY = (start.y + end.y) / 2; +export const topY = Math.min(start.y, end.y); +export const bottomY = Math.max(start.y, end.y); +export const leftX = Math.min(start.x, end.x); +export const rightX = Math.max(start.x, end.x); + +// Handles for resize (corners and sides) +export const handles = { + 'top-left': { x: leftX, y: topY }, + 'top': { x: centerX, y: topY }, + 'top-right': { x: rightX, y: topY }, + 'right': { x: rightX, y: centerY }, + 'bottom-right': { x: rightX, y: bottomY }, + 'bottom': { x: centerX, y: bottomY }, + 'bottom-left': { x: leftX, y: bottomY }, + 'left': { x: leftX, y: centerY }, +}; + +// Rotate handle +export const rotateHandle = { x: centerX, y: topY - 30 }; + +/** + * Mocks the global WebSocket object to prevent real connections during e2e tests. + * Call this before page.goto(). + */ +export async function mockWebSocket(page: import('@playwright/test').Page) { + await page.addInitScript(() => { + // @ts-expect-error: Overriding global WebSocket for test mocking + window.WebSocket = class { + static CONNECTING = 0; + static OPEN = 1; + static CLOSING = 2; + static CLOSED = 3; + constructor() {} + close() {} + send() {} + addEventListener() {} + removeEventListener() {} + readyState = 1; + }; + }); +} + +/** + * Injects a visible cursor overlay for Playwright tests. + * Call this before page.goto() if you want to see the cursor in the browser, screenshots, or video. + */ +export async function showCursor(page: import('@playwright/test').Page) { + const cursorStyle = ` + .playwright-cursor { + pointer-events: none; + position: fixed; + z-index: 2147483647; + width: 24px; + height: 24px; + background: rgba(255, 0, 100, 0.4); + border: 2px solid rgba(255, 0, 100, 0.9); + border-radius: 50%; + box-shadow: 0 0 10px rgba(255, 0, 100, 0.5), + 0 0 20px rgba(255, 0, 100, 0.3); + transform: translate(-50%, -50%); + transition: + transform 0.05s cubic-bezier(0.18, 0.89, 0.32, 1.28), + width 0.2s ease, + height 0.2s ease; + mix-blend-mode: difference; + } + .playwright-cursor.click-effect { + animation: click-pulse 0.3s ease-out; + width: 40px; + height: 40px; + background: transparent; + border: 3px solid rgba(255, 255, 0, 0.8); + } + @keyframes click-pulse { + 0% { opacity: 1; transform: translate(-50%, -50%) scale(0.8); } + 100% { opacity: 0; transform: translate(-50%, -50%) scale(1.5); } + } + `; + + await page.addInitScript(cursorStyle => { + document.addEventListener('DOMContentLoaded', () => { + const style = document.createElement('style'); + style.innerHTML = cursorStyle; + document.head.appendChild(style); + + const cursor = document.createElement('div'); + cursor.className = 'playwright-cursor'; + document.body.appendChild(cursor); + + document.addEventListener('mousemove', e => { + cursor.style.left = `${e.clientX}px`; + cursor.style.top = `${e.clientY}px`; + }); + + document.addEventListener('click', () => { + cursor.classList.add('click-effect'); + setTimeout(() => cursor.classList.remove('click-effect'), 300); + }); + }); + }, cursorStyle); +} diff --git a/frontend/e2e/rectangle/resize.e2e.spec.ts b/frontend/e2e/rectangle/resize.e2e.spec.ts new file mode 100644 index 0000000..0e1ff14 --- /dev/null +++ b/frontend/e2e/rectangle/resize.e2e.spec.ts @@ -0,0 +1,132 @@ +import { test, expect } from '@playwright/test'; + +import { + CANVAS_SELECTOR, + SNAPSHOT_TOLERANCE, + start, + end, + mockWebSocket, + showCursor, + DELAY, +} from './rectangle.helpers'; + +const resizeOffsets = { + 'top-left': { dx: -20, dy: -20 }, + 'top': { dx: 0, dy: -20 }, + 'top-right': { dx: 20, dy: -20 }, + 'right': { dx: 20, dy: 0 }, + 'bottom-right': { dx: 20, dy: 20 }, + 'bottom': { dx: 0, dy: 20 }, + 'bottom-left': { dx: -20, dy: 20 }, + 'left': { dx: -20, dy: 0 }, +}; + +// Функция для пересчёта координат handle по текущим координатам прямоугольника +function getHandles(rect) { + const { start, end } = rect; + const centerX = (start.x + end.x) / 2; + const centerY = (start.y + end.y) / 2; + const topY = Math.min(start.y, end.y); + const bottomY = Math.max(start.y, end.y); + const leftX = Math.min(start.x, end.x); + const rightX = Math.max(start.x, end.x); + + return { + 'top-left': { x: leftX, y: topY }, + 'top': { x: centerX, y: topY }, + 'top-right': { x: rightX, y: topY }, + 'right': { x: rightX, y: centerY }, + 'bottom-right': { x: rightX, y: bottomY }, + 'bottom': { x: centerX, y: bottomY }, + 'bottom-left': { x: leftX, y: bottomY }, + 'left': { x: leftX, y: centerY }, + }; +} + +test('should resize rectangle using all 8 handles', async ({ page }) => { + // Mock WebSocket to prevent real connections during tests + await mockWebSocket(page); + // Show a visible cursor overlay for debugging + await showCursor(page); + + // Open the main page + await page.goto('/'); + const canvas = page.locator(CANVAS_SELECTOR); + await expect(canvas).toBeVisible(); + await page.waitForTimeout(DELAY); + + // Clear the canvas + await page.getByRole('button', { name: 'Clear Canvas' }).click(); + await page.waitForTimeout(DELAY); + + // Select the Rectangle tool and draw a rectangle + await page.getByRole('button', { name: 'Rectangle' }).click(); + await page.waitForTimeout(DELAY); + await page.mouse.move(start.x, start.y); + await page.mouse.down(); + await page.mouse.move(end.x, end.y, { steps: 10 }); + await page.mouse.up(); + await page.waitForTimeout(DELAY); + + // Switch to Select tool + await page.getByTestId('toolbar-select').click(); + await page.waitForTimeout(DELAY); + + // Select the rectangle by clicking its center + const rect = { start: { ...start }, end: { ...end } }; + await page.mouse.click((rect.start.x + rect.end.x) / 2, (rect.start.y + rect.end.y) / 2); + await page.waitForTimeout(DELAY); // Wait for handles to appear + + // Resize using all 8 handles + for (const handle of Object.keys(resizeOffsets)) { + const handlesNow = getHandles(rect); + const { x, y } = handlesNow[handle]; + const { dx, dy } = resizeOffsets[handle]; + // Move to the handle + await page.mouse.move(x, y); + await page.waitForTimeout(DELAY); // Let cursor update + await page.mouse.down(); + await page.mouse.move(x + dx, y + dy, { steps: 10 }); + await page.mouse.up(); + await page.waitForTimeout(DELAY); + + // Обновляем координаты прямоугольника в зависимости от handle + switch (handle) { + case 'top-left': + rect.start.x += dx; + rect.start.y += dy; + break; + case 'top': + rect.start.y += dy; + break; + case 'top-right': + rect.end.x += dx; + rect.start.y += dy; + break; + case 'right': + rect.end.x += dx; + break; + case 'bottom-right': + rect.end.x += dx; + rect.end.y += dy; + break; + case 'bottom': + rect.end.y += dy; + break; + case 'bottom-left': + rect.start.x += dx; + rect.end.y += dy; + break; + case 'left': + rect.start.x += dx; + break; + } + } + + // Take a single screenshot and compare with the snapshot + const afterResize = await canvas.screenshot(); + + // await page.waitForTimeout(1000 * 60); + + expect(afterResize).toMatchSnapshot('resize-all-handles.png', { maxDiffPixelRatio: SNAPSHOT_TOLERANCE }); +}); diff --git a/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-chromium-darwin.png b/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-chromium-darwin.png new file mode 100644 index 0000000..db1a212 Binary files /dev/null and b/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-chromium-darwin.png differ diff --git a/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-chromium-linux.png b/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-chromium-linux.png new file mode 100644 index 0000000..172778e Binary files /dev/null and b/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-chromium-linux.png differ diff --git a/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-firefox-darwin.png b/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-firefox-darwin.png new file mode 100644 index 0000000..642a2c2 Binary files /dev/null and b/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-firefox-darwin.png differ diff --git a/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-firefox-linux.png b/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-firefox-linux.png new file mode 100644 index 0000000..dc3c169 Binary files /dev/null and b/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-firefox-linux.png differ diff --git a/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-webkit-darwin.png b/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-webkit-darwin.png new file mode 100644 index 0000000..6b62d1c Binary files /dev/null and b/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-webkit-darwin.png differ diff --git a/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-webkit-linux.png b/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-webkit-linux.png new file mode 100644 index 0000000..fc2bfaa Binary files /dev/null and b/frontend/e2e/rectangle/resize.e2e.spec.ts-snapshots/resize-all-handles-webkit-linux.png differ diff --git a/frontend/e2e/rectangle/rotate.e2e.spec.ts b/frontend/e2e/rectangle/rotate.e2e.spec.ts new file mode 100644 index 0000000..4c141cc --- /dev/null +++ b/frontend/e2e/rectangle/rotate.e2e.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from '@playwright/test'; + +import { + CANVAS_SELECTOR, + SNAPSHOT_TOLERANCE, + start, + end, + moveFrom, + rotateHandle, + mockWebSocket, + showCursor, + DELAY, +} from './rectangle.helpers'; + +const rotateTo = { x: rotateHandle.x + 40, y: rotateHandle.y }; + +test('should rotate rectangle using rotate handle', async ({ page }) => { + // Mock WebSocket to prevent real connections during tests + await mockWebSocket(page); + // Show a visible cursor overlay for debugging + await showCursor(page); + + // Open the main page + await page.goto('/'); + const canvas = page.locator(CANVAS_SELECTOR); + await expect(canvas).toBeVisible(); + await page.waitForTimeout(DELAY); + + // Clear the canvas + await page.getByRole('button', { name: 'Clear Canvas' }).click(); + await page.waitForTimeout(DELAY); + + // Select the Rectangle tool and draw a rectangle + await page.getByRole('button', { name: 'Rectangle' }).click(); + await page.waitForTimeout(DELAY); + await page.mouse.move(start.x, start.y); + await page.mouse.down(); + await page.mouse.move(end.x, end.y, { steps: 10 }); + await page.mouse.up(); + await page.waitForTimeout(DELAY); + + // Switch to Select tool + await page.getByTestId('toolbar-select').click(); + await page.waitForTimeout(DELAY); + + // Select the rectangle by clicking its center + await page.mouse.click(moveFrom.x, moveFrom.y); + await page.waitForTimeout(DELAY); // Wait for handles to appear + + // Rotate using the rotate handle + await page.mouse.move(rotateHandle.x, rotateHandle.y); + await page.waitForTimeout(DELAY); + await page.mouse.down(); + await page.mouse.move(rotateTo.x, rotateTo.y, { steps: 10 }); + await page.mouse.up(); + await page.waitForTimeout(DELAY); + + // Take a screenshot and compare with the snapshot + const afterRotate = await canvas.screenshot(); + expect(afterRotate).toMatchSnapshot('after-rotate-rectangle.png', { maxDiffPixelRatio: SNAPSHOT_TOLERANCE }); +}); diff --git a/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-chromium-darwin.png b/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-chromium-darwin.png new file mode 100644 index 0000000..b91be6e Binary files /dev/null and b/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-chromium-darwin.png differ diff --git a/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-chromium-linux.png b/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-chromium-linux.png new file mode 100644 index 0000000..dc04bea Binary files /dev/null and b/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-chromium-linux.png differ diff --git a/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-firefox-darwin.png b/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-firefox-darwin.png new file mode 100644 index 0000000..2549a3a Binary files /dev/null and b/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-firefox-darwin.png differ diff --git a/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-firefox-linux.png b/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-firefox-linux.png new file mode 100644 index 0000000..6821826 Binary files /dev/null and b/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-firefox-linux.png differ diff --git a/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-webkit-darwin.png b/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-webkit-darwin.png new file mode 100644 index 0000000..b1ed25d Binary files /dev/null and b/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-webkit-darwin.png differ diff --git a/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-webkit-linux.png b/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-webkit-linux.png new file mode 100644 index 0000000..916d5d2 Binary files /dev/null and b/frontend/e2e/rectangle/rotate.e2e.spec.ts-snapshots/after-rotate-rectangle-webkit-linux.png differ diff --git a/frontend/package.json b/frontend/package.json index 7c93a99..ac2d839 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,8 @@ "test:watch": "jest --watch", "test:coverage": "jest --coverage", "test:ci": "jest --ci --coverage --watchAll=false", - "ci": "pnpm run lint && pnpm run test:ci && pnpm run build" + "ci": "pnpm run lint && pnpm run test:ci && pnpm run build", + "e2e": "playwright test --config=e2e/playwright.config.ts" }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.7.2", @@ -27,6 +28,7 @@ }, "devDependencies": { "@eslint/js": "^9.29.0", + "@playwright/test": "^1.54.1", "@stylistic/eslint-plugin": "^5.1.0", "@types/jest": "^30.0.0", "@types/react": "^19.1.8", @@ -39,6 +41,7 @@ "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.2.0", "jest": "^30.0.4", + "playwright": "^1.54.1", "ts-jest": "^29.4.0", "typescript": "~5.8.3", "typescript-eslint": "^8.34.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 6602ce0..912ca04 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -39,6 +39,9 @@ importers: '@eslint/js': specifier: ^9.29.0 version: 9.30.0 + '@playwright/test': + specifier: ^1.54.1 + version: 1.54.1 '@stylistic/eslint-plugin': specifier: ^5.1.0 version: 5.1.0(eslint@9.30.0) @@ -75,6 +78,9 @@ importers: jest: specifier: ^30.0.4 version: 30.0.4(@types/node@24.0.10) + playwright: + specifier: ^1.54.1 + version: 1.54.1 ts-jest: specifier: ^29.4.0 version: 29.4.0(@babel/core@7.27.7)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.7))(jest-util@30.0.2)(jest@30.0.4(@types/node@24.0.10))(typescript@5.8.3) @@ -1188,6 +1194,11 @@ packages: resolution: {integrity: sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.54.1': + resolution: {integrity: sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==} + engines: {node: '>=18'} + hasBin: true + '@rolldown/pluginutils@1.0.0-beta.19': resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} @@ -2177,6 +2188,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2940,6 +2956,16 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + playwright-core@1.54.1: + resolution: {integrity: sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.54.1: + resolution: {integrity: sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==} + engines: {node: '>=18'} + hasBin: true + points-on-curve@0.2.0: resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} @@ -4897,6 +4923,10 @@ snapshots: '@pkgr/core@0.2.7': {} + '@playwright/test@1.54.1': + dependencies: + playwright: 1.54.1 + '@rolldown/pluginutils@1.0.0-beta.19': {} '@rollup/plugin-babel@5.3.1(@babel/core@7.27.7)(@types/babel__core@7.20.5)(rollup@2.79.2)': @@ -6050,6 +6080,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -6969,6 +7002,14 @@ snapshots: dependencies: find-up: 4.1.0 + playwright-core@1.54.1: {} + + playwright@1.54.1: + dependencies: + playwright-core: 1.54.1 + optionalDependencies: + fsevents: 2.3.2 + points-on-curve@0.2.0: {} points-on-path@0.2.1: diff --git a/frontend/src/entities/canvas/CanvasWrapper/index.tsx b/frontend/src/entities/canvas/CanvasWrapper/index.tsx index 82ecbe0..9d63c66 100644 --- a/frontend/src/entities/canvas/CanvasWrapper/index.tsx +++ b/frontend/src/entities/canvas/CanvasWrapper/index.tsx @@ -21,6 +21,7 @@ export const DraftlyWrapper = forwardRef(({ onShap return (
= ({ } onClick={() => onToolChange(tool as ToolType)} title={title} + data-testid={`toolbar-${tool}`} > diff --git a/frontend/test-results/.last-run.json b/frontend/test-results/.last-run.json new file mode 100644 index 0000000..cbcc1fb --- /dev/null +++ b/frontend/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file