49b413bf07
* feat: implement comprehensive testing infrastructure - Fix image dataURL truncation bug in security.ts with configurable size limits - Add backend integration tests (22 tests) with Vitest for API validation - Add frontend unit tests (11 tests) for JSON serialization - Implement browser-based E2E tests (8 tests) with Playwright - Create Docker setup for repeatable E2E testing environment - Add GitHub Actions CI workflow for automated testing - Update .gitignore for test artifacts and temporary files Testing Infrastructure: - Backend: Vitest + Supertest for API integration tests - Frontend: Vitest + Testing Library for component tests - E2E: Playwright with Chromium for full browser automation - CI/CD: GitHub Actions with parallel test execution Security Improvements: - Make dataURL size limit configurable (default: 10MB) - Enhanced validation for image dataURLs - Block malicious content (javascript:, script tags) All tests pass: 41 total (22 backend + 11 frontend + 8 E2E) * feat(tests): add comprehensive E2E tests for dashboard workflows and image persistence chore(env): update environment variables for consistent API URL usage fix(api): centralize API request helpers for drawing and collection management style(DrawingCard): enhance accessibility with ARIA attributes and data-testid for testing * cleanup/revise documentation * cleanup/revise documentation * Add end-to-end tests for drawing CRUD, export/import, search/sort, and theme toggle functionalities - Implemented E2E tests for drawing creation, editing, and deletion in `drawing-crud.spec.ts`. - Added tests for export and import features, including JSON and SQLite formats in `export-import.spec.ts`. - Created tests for searching and sorting drawings by name and date in `search-and-sort.spec.ts`. - Developed tests for theme toggle functionality to ensure persistence across sessions in `theme-toggle.spec.ts`. * fix: exclude test files from production build to fix Docker build * feat: implement comprehensive testing infrastructure (#19) * bump version 0.1.7 * feat: implement comprehensive testing infrastructure - Fix image dataURL truncation bug in security.ts with configurable size limits - Add backend integration tests (22 tests) with Vitest for API validation - Add frontend unit tests (11 tests) for JSON serialization - Implement browser-based E2E tests (8 tests) with Playwright - Create Docker setup for repeatable E2E testing environment - Add GitHub Actions CI workflow for automated testing - Update .gitignore for test artifacts and temporary files Testing Infrastructure: - Backend: Vitest + Supertest for API integration tests - Frontend: Vitest + Testing Library for component tests - E2E: Playwright with Chromium for full browser automation - CI/CD: GitHub Actions with parallel test execution Security Improvements: - Make dataURL size limit configurable (default: 10MB) - Enhanced validation for image dataURLs - Block malicious content (javascript:, script tags) All tests pass: 41 total (22 backend + 11 frontend + 8 E2E) * feat(tests): add comprehensive E2E tests for dashboard workflows and image persistence chore(env): update environment variables for consistent API URL usage fix(api): centralize API request helpers for drawing and collection management style(DrawingCard): enhance accessibility with ARIA attributes and data-testid for testing * Add end-to-end tests for drawing CRUD, export/import, search/sort, and theme toggle functionalities - Implemented E2E tests for drawing creation, editing, and deletion in `drawing-crud.spec.ts`. - Added tests for export and import features, including JSON and SQLite formats in `export-import.spec.ts`. - Created tests for searching and sorting drawings by name and date in `search-and-sort.spec.ts`. - Developed tests for theme toggle functionality to ensure persistence across sessions in `theme-toggle.spec.ts`. * Update backend/src/__tests__/testUtils.ts --------- Co-authored-by: Zimeng Xiong <zxzimeng@gmail.com> * version bump 0.1.8 * fix(ci): consolidate E2E server startup to prevent shell isolation issues Background processes started with & in separate GitHub Actions run steps can terminate when those steps complete because each step creates a new shell. This caused the backend and frontend servers to die before the E2E tests could run. Fixed by consolidating server startup and test execution into a single shell step with: - Proper PID tracking for cleanup - Health check loops instead of fixed sleep times - All processes run in the same shell session * fix(ci): use absolute database path for E2E tests * fix(backend): use resolved DATABASE_URL path for export/import endpoints --------- Co-authored-by: Adrian Acala <adrianacala017@gmail.com>
262 lines
8.5 KiB
TypeScript
262 lines
8.5 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
import * as fs from "fs";
|
|
import * as path from "path";
|
|
import { API_URL, createDrawing, deleteDrawing, getDrawing } from "./helpers/api";
|
|
|
|
/**
|
|
* E2E Browser Tests for Image Persistence - Issue #17 Regression
|
|
*
|
|
* These tests verify the complete user workflow:
|
|
* 1. Create a drawing with an embedded image
|
|
* 2. Save the drawing
|
|
* 3. Close and reopen the drawing
|
|
* 4. Verify the image loads correctly
|
|
*
|
|
* This tests the fix for GitHub issue #17:
|
|
* "Images don't load fully when reopening the file"
|
|
*/
|
|
|
|
function generateLargeImageDataUrl(sizeInBytes: number = 50000): string {
|
|
// Create pseudo-random data that looks like base64
|
|
const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
let base64Data = "";
|
|
for (let i = 0; i < sizeInBytes; i++) {
|
|
base64Data += base64Chars[Math.floor(Math.random() * 64)];
|
|
}
|
|
return `data:image/png;base64,${base64Data}`;
|
|
}
|
|
|
|
test.describe("Image Persistence - Browser E2E Tests", () => {
|
|
let testDrawingIds: string[] = [];
|
|
|
|
test.afterEach(async ({ request }) => {
|
|
// Clean up any drawings created during tests
|
|
for (const id of testDrawingIds) {
|
|
try {
|
|
await deleteDrawing(request, id);
|
|
} catch (e) {
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
testDrawingIds = [];
|
|
});
|
|
|
|
test("should navigate to dashboard and see drawing list", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
// Wait for the page to load
|
|
await expect(page).toHaveTitle(/ExcaliDash/i);
|
|
|
|
// The dashboard should show some UI elements
|
|
await expect(page.locator("body")).toBeVisible();
|
|
});
|
|
|
|
test("should create a new drawing via UI", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
// Look for a "New Drawing" or similar button
|
|
const newDrawingBtn = page.getByRole("button", { name: /new|create/i }).first();
|
|
|
|
if (await newDrawingBtn.isVisible()) {
|
|
await newDrawingBtn.click();
|
|
|
|
// Should navigate to editor or show a modal
|
|
await page.waitForURL(/\/(editor|drawing)/i, { timeout: 5000 }).catch(() => {
|
|
// May stay on same page with modal
|
|
});
|
|
}
|
|
});
|
|
|
|
test("should preserve large image data through save/reload cycle via API", async ({ request }) => {
|
|
// This is the core regression test for issue #17
|
|
const largeDataUrl = generateLargeImageDataUrl(50000);
|
|
expect(largeDataUrl.length).toBeGreaterThan(10000);
|
|
|
|
const files = {
|
|
"test-image-1": {
|
|
id: "test-image-1",
|
|
mimeType: "image/png",
|
|
dataURL: largeDataUrl,
|
|
created: Date.now(),
|
|
},
|
|
};
|
|
|
|
// Create drawing with large image
|
|
const createdDrawing = await createDrawing(request, {
|
|
name: "E2E Test - Large Image",
|
|
files,
|
|
});
|
|
testDrawingIds.push(createdDrawing.id);
|
|
|
|
// Retrieve the drawing
|
|
const drawing = await getDrawing(request, createdDrawing.id);
|
|
const savedFiles = drawing.files || {}; // Already parsed by API
|
|
|
|
// Verify the image data was preserved
|
|
expect(savedFiles["test-image-1"]).toBeDefined();
|
|
expect(savedFiles["test-image-1"].dataURL).toBe(largeDataUrl);
|
|
expect(savedFiles["test-image-1"].dataURL.length).toBe(largeDataUrl.length);
|
|
|
|
console.log("✓ Large image data preserved correctly through save/reload cycle");
|
|
});
|
|
|
|
test("should display drawing in editor view", async ({ page, request }) => {
|
|
// Create a test drawing first
|
|
const createdDrawing = await createDrawing(request, {
|
|
name: "E2E Test - Editor View",
|
|
});
|
|
testDrawingIds.push(createdDrawing.id);
|
|
|
|
// Navigate to the editor
|
|
await page.goto(`/editor/${createdDrawing.id}`);
|
|
|
|
// Wait for the page to load
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// The editor should be visible (Excalidraw canvas)
|
|
// Look for the Excalidraw container or canvas
|
|
const editorContainer = page.locator("[class*='excalidraw'], canvas").first();
|
|
await expect(editorContainer).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test("should import .excalidraw file with embedded image", async ({ page, request }) => {
|
|
// Load the test fixture
|
|
const fixturePath = path.join(__dirname, "..", "fixtures", "small-image.excalidraw");
|
|
const fixtureContent = fs.readFileSync(fixturePath, "utf-8");
|
|
const fixtureData = JSON.parse(fixtureContent);
|
|
|
|
// Create drawing via API with fixture data
|
|
const createdDrawing = await createDrawing(request, {
|
|
name: "E2E Test - Imported Image",
|
|
files: fixtureData.files,
|
|
});
|
|
testDrawingIds.push(createdDrawing.id);
|
|
|
|
// Verify via API that image data was preserved
|
|
const drawing = await getDrawing(request, createdDrawing.id);
|
|
const savedFiles = drawing.files || {}; // Already parsed by API
|
|
|
|
expect(savedFiles["embedded-test-image"]).toBeDefined();
|
|
expect(savedFiles["embedded-test-image"].dataURL).toBe(fixtureData.files["embedded-test-image"].dataURL);
|
|
});
|
|
|
|
test("should handle multiple images of varying sizes", async ({ request }) => {
|
|
const files = {
|
|
"small-image": {
|
|
id: "small-image",
|
|
mimeType: "image/png",
|
|
dataURL: generateLargeImageDataUrl(1000),
|
|
created: Date.now(),
|
|
},
|
|
"medium-image": {
|
|
id: "medium-image",
|
|
mimeType: "image/jpeg",
|
|
dataURL: generateLargeImageDataUrl(15000),
|
|
created: Date.now(),
|
|
},
|
|
"large-image": {
|
|
id: "large-image",
|
|
mimeType: "image/png",
|
|
dataURL: generateLargeImageDataUrl(75000),
|
|
created: Date.now(),
|
|
},
|
|
};
|
|
|
|
const createdDrawing = await createDrawing(request, {
|
|
name: "E2E Test - Multiple Images",
|
|
files,
|
|
});
|
|
testDrawingIds.push(createdDrawing.id);
|
|
|
|
const drawing = await getDrawing(request, createdDrawing.id);
|
|
const savedFiles = drawing.files || {}; // Already parsed by API
|
|
|
|
// Verify all images preserved correctly
|
|
for (const [id, originalFile] of Object.entries(files)) {
|
|
expect(savedFiles[id]).toBeDefined();
|
|
expect(savedFiles[id].dataURL).toBe((originalFile as any).dataURL);
|
|
expect(savedFiles[id].dataURL.length).toBe((originalFile as any).dataURL.length);
|
|
}
|
|
|
|
console.log("✓ Multiple images of varying sizes preserved correctly");
|
|
});
|
|
});
|
|
|
|
test.describe("Security - Malicious Content Blocking", () => {
|
|
test("should block javascript: URLs in image data", async ({ request }) => {
|
|
const maliciousFiles = {
|
|
"malicious-image": {
|
|
id: "malicious-image",
|
|
mimeType: "image/png",
|
|
dataURL: "javascript:alert('xss')",
|
|
created: Date.now(),
|
|
},
|
|
};
|
|
|
|
const response = await request.post(`${API_URL}/drawings`, {
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
data: {
|
|
name: "Security Test - JS URL",
|
|
elements: [],
|
|
appState: { viewBackgroundColor: "#ffffff" },
|
|
files: maliciousFiles,
|
|
preview: null,
|
|
},
|
|
});
|
|
|
|
if (!response.ok()) {
|
|
const text = await response.text();
|
|
console.error(`API Error: ${response.status()} - ${text}`);
|
|
}
|
|
expect(response.ok()).toBe(true);
|
|
const drawing = await response.json();
|
|
const savedFiles = drawing.files; // Already parsed by API
|
|
|
|
// The malicious URL should be blocked/cleared
|
|
expect(savedFiles["malicious-image"].dataURL).not.toContain("javascript:");
|
|
|
|
// Cleanup
|
|
await request.delete(`${API_URL}/drawings/${drawing.id}`);
|
|
});
|
|
|
|
test("should block script tags in image data", async ({ request }) => {
|
|
const maliciousFiles = {
|
|
"malicious-image": {
|
|
id: "malicious-image",
|
|
mimeType: "image/png",
|
|
dataURL: "data:image/png;base64,<script>alert('xss')</script>AAAA",
|
|
created: Date.now(),
|
|
},
|
|
};
|
|
|
|
const response = await request.post(`${API_URL}/drawings`, {
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
data: {
|
|
name: "Security Test - Script Tag",
|
|
elements: [],
|
|
appState: { viewBackgroundColor: "#ffffff" },
|
|
files: maliciousFiles,
|
|
preview: null,
|
|
},
|
|
});
|
|
|
|
if (!response.ok()) {
|
|
const text = await response.text();
|
|
console.error(`API Error: ${response.status()} - ${text}`);
|
|
}
|
|
expect(response.ok()).toBe(true);
|
|
const drawing = await response.json();
|
|
const savedFiles = drawing.files; // Already parsed by API
|
|
|
|
// The script tag should be blocked
|
|
expect(savedFiles["malicious-image"].dataURL).not.toContain("<script>");
|
|
|
|
// Cleanup
|
|
await request.delete(`${API_URL}/drawings/${drawing.id}`);
|
|
});
|
|
});
|