Files
ExcaliDash/e2e/tests/image-persistence.spec.ts
T
Zimeng Xiong 49b413bf07 Testing infrastructure, fix truncating of dataURLs (#26)
* 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>
2025-12-19 15:09:15 -08:00

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}`);
});
});