Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d294716
feat: Add end-to-end testing command and data-testid attribute to can…
ermolaev-frontend Jul 21, 2025
c3f5a0e
feat: Add Playwright testing dependencies and enhance canvas E2E tests
ermolaev-frontend Jul 21, 2025
72e953f
refactor: Update canvas E2E test comments to English for clarity
ermolaev-frontend Jul 21, 2025
aa2da8d
fix: Update canvas E2E tests to handle new shape management features
ermolaev-frontend Jul 21, 2025
d258d99
feat: Enhance rectangle movement E2E tests with detailed actions
ermolaev-frontend Jul 21, 2025
b1de04e
feat: Enhance E2E tests for rectangle interactions with WebSocket moc…
ermolaev-frontend Jul 21, 2025
c6c865d
feat: Enhance rectangle E2E tests with cursor overlay and consistent …
ermolaev-frontend Jul 21, 2025
8bb01fe
feat: Update rectangle E2E tests with improved cursor styling and res…
ermolaev-frontend Jul 21, 2025
ad19508
chore: Remove obsolete canvas E2E test file and associated snapshot i…
ermolaev-frontend Jul 21, 2025
f08038e
feat: Improve cursor functionality in E2E tests with enhanced styling…
ermolaev-frontend Jul 21, 2025
d25e428
feat: Refactor E2E tests for improved cursor interactions and perform…
ermolaev-frontend Jul 21, 2025
08c4949
feat: Update E2E test configurations and enhance rectangle tests
ermolaev-frontend Jul 21, 2025
5c9a93a
feat: Enhance E2E workflow with preview server and wait functionality
ermolaev-frontend Jul 21, 2025
62a9112
fix: Clean up E2E test files and improve formatting
ermolaev-frontend Jul 21, 2025
dcc533e
fix: Update Playwright configuration baseURL for E2E tests
ermolaev-frontend Jul 21, 2025
4b164c9
feat: Enhance E2E workflow with Playwright result uploads
ermolaev-frontend Jul 21, 2025
321f1a8
chore: Upgrade actions/upload-artifact to v4 in E2E workflow
ermolaev-frontend Jul 21, 2025
896753d
chore: Ensure artifact uploads in E2E workflow always occur
ermolaev-frontend Jul 21, 2025
89855d7
chore: Update E2E workflow to include conditional artifact uploads fo…
ermolaev-frontend Jul 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
34 changes: 34 additions & 0 deletions frontend/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -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'] },
// },
],
});
35 changes: 35 additions & 0 deletions frontend/e2e/rectangle/draw.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 69 additions & 0 deletions frontend/e2e/rectangle/move.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 112 additions & 0 deletions frontend/e2e/rectangle/rectangle.helpers.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Loading