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>
266 lines
9.1 KiB
TypeScript
266 lines
9.1 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
import {
|
|
createDrawing,
|
|
deleteDrawing,
|
|
listDrawings,
|
|
} from "./helpers/api";
|
|
|
|
/**
|
|
* E2E Tests for Search and Sort functionality
|
|
*
|
|
* Tests the search drawings feature mentioned in README:
|
|
* - Search by drawing name
|
|
* - Sort by name, created date, modified date
|
|
* - Clear search
|
|
*/
|
|
|
|
test.describe("Search Drawings", () => {
|
|
let createdDrawingIds: string[] = [];
|
|
|
|
test.afterEach(async ({ request }) => {
|
|
for (const id of createdDrawingIds) {
|
|
try {
|
|
await deleteDrawing(request, id);
|
|
} catch (e) {
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
createdDrawingIds = [];
|
|
});
|
|
|
|
test("should filter drawings by search term", async ({ page, request }) => {
|
|
// Create test drawings with distinct names
|
|
const prefix = `SearchTest_${Date.now()}`;
|
|
const [drawing1, drawing2, drawing3] = await Promise.all([
|
|
createDrawing(request, { name: `${prefix}_Alpha` }),
|
|
createDrawing(request, { name: `${prefix}_Beta` }),
|
|
createDrawing(request, { name: `DifferentName_${Date.now()}` }),
|
|
]);
|
|
createdDrawingIds.push(drawing1.id, drawing2.id, drawing3.id);
|
|
|
|
await page.goto("/");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Verify all drawings are visible initially
|
|
const searchInput = page.getByPlaceholder("Search drawings...");
|
|
await searchInput.waitFor();
|
|
|
|
// Search for the prefix - should show only matching drawings
|
|
await searchInput.fill(prefix);
|
|
|
|
// Wait for search to apply (debounced)
|
|
await page.waitForTimeout(500);
|
|
|
|
// Verify only matching drawings are shown
|
|
await expect(page.locator(`#drawing-card-${drawing1.id}`)).toBeVisible();
|
|
await expect(page.locator(`#drawing-card-${drawing2.id}`)).toBeVisible();
|
|
await expect(page.locator(`#drawing-card-${drawing3.id}`)).not.toBeVisible();
|
|
|
|
// Search for specific drawing
|
|
await searchInput.fill(`${prefix}_Alpha`);
|
|
await page.waitForTimeout(500);
|
|
|
|
await expect(page.locator(`#drawing-card-${drawing1.id}`)).toBeVisible();
|
|
await expect(page.locator(`#drawing-card-${drawing2.id}`)).not.toBeVisible();
|
|
});
|
|
|
|
test("should show empty state when no drawings match search", async ({ page, request }) => {
|
|
const drawing = await createDrawing(request, { name: `ExistingDrawing_${Date.now()}` });
|
|
createdDrawingIds.push(drawing.id);
|
|
|
|
await page.goto("/");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const searchInput = page.getByPlaceholder("Search drawings...");
|
|
await searchInput.fill("NonExistentDrawingName12345");
|
|
await page.waitForTimeout(500);
|
|
|
|
// Should show empty state
|
|
await expect(page.getByText("No drawings found")).toBeVisible();
|
|
await expect(page.getByText('No results for "NonExistentDrawingName12345"')).toBeVisible();
|
|
});
|
|
|
|
test("should clear search and show all drawings", async ({ page, request }) => {
|
|
const prefix = `ClearSearchTest_${Date.now()}`;
|
|
const [drawing1, drawing2] = await Promise.all([
|
|
createDrawing(request, { name: `${prefix}_One` }),
|
|
createDrawing(request, { name: `${prefix}_Two` }),
|
|
]);
|
|
createdDrawingIds.push(drawing1.id, drawing2.id);
|
|
|
|
await page.goto("/");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const searchInput = page.getByPlaceholder("Search drawings...");
|
|
|
|
// Search for one drawing
|
|
await searchInput.fill(`${prefix}_One`);
|
|
await page.waitForTimeout(500);
|
|
await expect(page.locator(`#drawing-card-${drawing2.id}`)).not.toBeVisible();
|
|
|
|
// Clear search
|
|
await searchInput.fill("");
|
|
await page.waitForTimeout(500);
|
|
|
|
// Search for prefix to find both
|
|
await searchInput.fill(prefix);
|
|
await page.waitForTimeout(500);
|
|
|
|
// Both should be visible now
|
|
await expect(page.locator(`#drawing-card-${drawing1.id}`)).toBeVisible();
|
|
await expect(page.locator(`#drawing-card-${drawing2.id}`)).toBeVisible();
|
|
});
|
|
|
|
test("should use keyboard shortcut Cmd+K to focus search", async ({ page, request }) => {
|
|
const drawing = await createDrawing(request, { name: `KeyboardTest_${Date.now()}` });
|
|
createdDrawingIds.push(drawing.id);
|
|
|
|
await page.goto("/");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const searchInput = page.getByPlaceholder("Search drawings...");
|
|
|
|
// Use keyboard shortcut (Cmd+K on Mac, Ctrl+K on Windows/Linux)
|
|
await page.keyboard.press("Meta+k");
|
|
|
|
// Search input should be focused
|
|
await expect(searchInput).toBeFocused();
|
|
});
|
|
});
|
|
|
|
test.describe("Sort Drawings", () => {
|
|
let createdDrawingIds: string[] = [];
|
|
|
|
test.afterEach(async ({ request }) => {
|
|
for (const id of createdDrawingIds) {
|
|
try {
|
|
await deleteDrawing(request, id);
|
|
} catch (e) {
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
createdDrawingIds = [];
|
|
});
|
|
|
|
test("should sort drawings by name", async ({ page, request }) => {
|
|
const prefix = `SortTest_${Date.now()}`;
|
|
|
|
// Create drawings with names that sort in a specific order
|
|
const [drawingC, drawingA, drawingB] = await Promise.all([
|
|
createDrawing(request, { name: `${prefix}_Charlie` }),
|
|
createDrawing(request, { name: `${prefix}_Alpha` }),
|
|
createDrawing(request, { name: `${prefix}_Bravo` }),
|
|
]);
|
|
createdDrawingIds.push(drawingC.id, drawingA.id, drawingB.id);
|
|
|
|
await page.goto("/");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Filter to only our test drawings
|
|
const searchInput = page.getByPlaceholder("Search drawings...");
|
|
await searchInput.fill(prefix);
|
|
await page.waitForTimeout(500);
|
|
|
|
// Click Name sort button
|
|
const nameSortButton = page.getByRole("button", { name: "Name" });
|
|
await nameSortButton.click();
|
|
|
|
// Get the order of cards
|
|
const cards = page.locator("[id^='drawing-card-']");
|
|
await expect(cards).toHaveCount(3);
|
|
|
|
// Verify order is alphabetical (Alpha, Bravo, Charlie)
|
|
const firstCard = cards.first();
|
|
await expect(firstCard).toHaveId(`drawing-card-${drawingA.id}`);
|
|
});
|
|
|
|
test("should toggle sort direction on repeated clicks", async ({ page, request }) => {
|
|
const prefix = `ToggleSortTest_${Date.now()}`;
|
|
|
|
const [drawingA, drawingZ] = await Promise.all([
|
|
createDrawing(request, { name: `${prefix}_AAA` }),
|
|
createDrawing(request, { name: `${prefix}_ZZZ` }),
|
|
]);
|
|
createdDrawingIds.push(drawingA.id, drawingZ.id);
|
|
|
|
await page.goto("/");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const searchInput = page.getByPlaceholder("Search drawings...");
|
|
await searchInput.fill(prefix);
|
|
await page.waitForTimeout(500);
|
|
|
|
const nameSortButton = page.getByRole("button", { name: "Name" });
|
|
|
|
// First click - ascending (A first)
|
|
await nameSortButton.click();
|
|
await page.waitForTimeout(200);
|
|
|
|
let cards = page.locator("[id^='drawing-card-']");
|
|
let firstCard = cards.first();
|
|
await expect(firstCard).toHaveId(`drawing-card-${drawingA.id}`);
|
|
|
|
// Second click - descending (Z first)
|
|
await nameSortButton.click();
|
|
await page.waitForTimeout(200);
|
|
|
|
cards = page.locator("[id^='drawing-card-']");
|
|
firstCard = cards.first();
|
|
await expect(firstCard).toHaveId(`drawing-card-${drawingZ.id}`);
|
|
});
|
|
|
|
test("should sort by date created", async ({ page, request }) => {
|
|
const prefix = `DateSortTest_${Date.now()}`;
|
|
|
|
// Create drawings sequentially to ensure different creation times
|
|
const drawing1 = await createDrawing(request, { name: `${prefix}_First` });
|
|
createdDrawingIds.push(drawing1.id);
|
|
|
|
await page.waitForTimeout(100); // Ensure different timestamps
|
|
|
|
const drawing2 = await createDrawing(request, { name: `${prefix}_Second` });
|
|
createdDrawingIds.push(drawing2.id);
|
|
|
|
await page.goto("/");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const searchInput = page.getByPlaceholder("Search drawings...");
|
|
await searchInput.fill(prefix);
|
|
await page.waitForTimeout(500);
|
|
|
|
// Click Date Created sort button
|
|
const dateCreatedButton = page.getByRole("button", { name: "Date Created" });
|
|
await dateCreatedButton.click();
|
|
await page.waitForTimeout(200);
|
|
|
|
// Default should be descending (newest first)
|
|
const cards = page.locator("[id^='drawing-card-']");
|
|
const firstCard = cards.first();
|
|
await expect(firstCard).toHaveId(`drawing-card-${drawing2.id}`);
|
|
});
|
|
|
|
test("should sort by date modified", async ({ page, request }) => {
|
|
const prefix = `ModifiedSortTest_${Date.now()}`;
|
|
|
|
const [drawing1, drawing2] = await Promise.all([
|
|
createDrawing(request, { name: `${prefix}_One` }),
|
|
createDrawing(request, { name: `${prefix}_Two` }),
|
|
]);
|
|
createdDrawingIds.push(drawing1.id, drawing2.id);
|
|
|
|
await page.goto("/");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const searchInput = page.getByPlaceholder("Search drawings...");
|
|
await searchInput.fill(prefix);
|
|
await page.waitForTimeout(500);
|
|
|
|
// Click Date Modified sort button
|
|
const dateModifiedButton = page.getByRole("button", { name: "Date Modified" });
|
|
await dateModifiedButton.click();
|
|
|
|
// Verify the button shows active state
|
|
await expect(dateModifiedButton).toHaveClass(/bg-indigo-100|bg-neutral-800/);
|
|
});
|
|
});
|