401 lines
13 KiB
TypeScript
401 lines
13 KiB
TypeScript
import { test, expect } from "./fixtures";
|
|
import {
|
|
API_URL,
|
|
createDrawing,
|
|
deleteDrawing,
|
|
getCsrfHeaders,
|
|
listDrawings,
|
|
deleteCollection,
|
|
} from "./helpers/api";
|
|
|
|
/**
|
|
* E2E Tests for Export/Import functionality
|
|
*
|
|
* Tests the export/import feature mentioned in README:
|
|
* - Export drawings as JSON
|
|
* - Export database backup (SQLite)
|
|
* - Import .excalidraw files
|
|
* - Import JSON files
|
|
* - Import database backup
|
|
*/
|
|
|
|
test.describe("Export Functionality", () => {
|
|
let createdDrawingIds: string[] = [];
|
|
let createdCollectionIds: string[] = [];
|
|
|
|
test.afterEach(async ({ request }) => {
|
|
for (const id of createdDrawingIds) {
|
|
try {
|
|
await deleteDrawing(request, id);
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
createdDrawingIds = [];
|
|
|
|
for (const id of createdCollectionIds) {
|
|
try {
|
|
await deleteCollection(request, id);
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
createdCollectionIds = [];
|
|
});
|
|
|
|
test("should export database as SQLite via Settings page", async ({ page, request }) => {
|
|
// Create a drawing to ensure there's data to export
|
|
const drawing = await createDrawing(request, { name: `Export_SQLite_${Date.now()}` });
|
|
createdDrawingIds.push(drawing.id);
|
|
|
|
// Navigate to Settings
|
|
await page.goto("/settings");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Find and verify the export button exists
|
|
const exportSqliteButton = page.getByRole("button", { name: /Export Data \(.sqlite\)/i });
|
|
await expect(exportSqliteButton).toBeVisible();
|
|
|
|
// Verify the button links to the correct endpoint
|
|
// We can't easily test the actual download, but we can verify the UI
|
|
const exportDbButton = page.getByRole("button", { name: /Export Data \(.db\)/i });
|
|
await expect(exportDbButton).toBeVisible();
|
|
});
|
|
|
|
test("should export database as JSON via Settings page", async ({ page, request }) => {
|
|
// Create test data
|
|
const drawing = await createDrawing(request, { name: `Export_JSON_${Date.now()}` });
|
|
createdDrawingIds.push(drawing.id);
|
|
|
|
await page.goto("/settings");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Find the JSON export button
|
|
const exportJsonButton = page.getByRole("button", { name: /Export Data \(JSON\)/i });
|
|
await expect(exportJsonButton).toBeVisible();
|
|
});
|
|
|
|
test("should have export endpoints accessible via API", async ({ request }) => {
|
|
// Create test data
|
|
const drawing = await createDrawing(request, { name: `Export_API_${Date.now()}` });
|
|
createdDrawingIds.push(drawing.id);
|
|
|
|
// Test JSON/ZIP export endpoint - it returns a ZIP file with .excalidraw files
|
|
const zipResponse = await request.get(`${API_URL}/export/json`);
|
|
expect(zipResponse.ok()).toBe(true);
|
|
|
|
// Check it's a ZIP file
|
|
const contentType = zipResponse.headers()["content-type"];
|
|
expect(contentType).toMatch(/application\/zip/);
|
|
|
|
// Check content-disposition header
|
|
const contentDisposition = zipResponse.headers()["content-disposition"];
|
|
expect(contentDisposition).toContain("attachment");
|
|
expect(contentDisposition).toMatch(/excalidraw-drawings.*\.zip/);
|
|
});
|
|
|
|
test("should download SQLite export via API", async ({ request }) => {
|
|
const drawing = await createDrawing(request, { name: `SQLite_Export_${Date.now()}` });
|
|
createdDrawingIds.push(drawing.id);
|
|
|
|
// Test SQLite export endpoint
|
|
const sqliteResponse = await request.get(`${API_URL}/export`);
|
|
expect(sqliteResponse.ok()).toBe(true);
|
|
|
|
// Check content-type header indicates a file download
|
|
const contentType = sqliteResponse.headers()["content-type"];
|
|
expect(contentType).toMatch(/application\/octet-stream|application\/x-sqlite3/);
|
|
|
|
// Check content-disposition header
|
|
const contentDisposition = sqliteResponse.headers()["content-disposition"];
|
|
expect(contentDisposition).toContain("attachment");
|
|
expect(contentDisposition).toMatch(/excalidash-db.*\.sqlite/);
|
|
});
|
|
|
|
test("should download .db export via API", async ({ request }) => {
|
|
const drawing = await createDrawing(request, { name: `DB_Export_${Date.now()}` });
|
|
createdDrawingIds.push(drawing.id);
|
|
|
|
// Test .db export endpoint
|
|
const dbResponse = await request.get(`${API_URL}/export?format=db`);
|
|
expect(dbResponse.ok()).toBe(true);
|
|
|
|
const contentDisposition = dbResponse.headers()["content-disposition"];
|
|
expect(contentDisposition).toContain("attachment");
|
|
expect(contentDisposition).toMatch(/\.db/);
|
|
});
|
|
});
|
|
|
|
test.describe.serial("Import Functionality", () => {
|
|
let createdDrawingIds: string[] = [];
|
|
|
|
test.afterEach(async ({ request }) => {
|
|
// Clean up any drawings created via import
|
|
const testDrawings = await listDrawings(request, { search: "Import_" });
|
|
for (const drawing of testDrawings) {
|
|
try {
|
|
await deleteDrawing(request, drawing.id);
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
|
|
for (const id of createdDrawingIds) {
|
|
try {
|
|
await deleteDrawing(request, id);
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
createdDrawingIds = [];
|
|
});
|
|
|
|
test("should show Import Data button on Settings page", async ({ page }) => {
|
|
await page.goto("/settings");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Find the import button
|
|
const importButton = page.getByRole("button", { name: /Import Data/i });
|
|
await expect(importButton).toBeVisible();
|
|
});
|
|
|
|
test("should import .excalidraw file from Dashboard", async ({ page }) => {
|
|
await page.goto("/");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Create fixture content
|
|
const fixtureContent = JSON.stringify({
|
|
type: "excalidraw",
|
|
version: 2,
|
|
source: "e2e-test",
|
|
elements: [
|
|
{
|
|
id: "test-rect-1",
|
|
type: "rectangle",
|
|
x: 100,
|
|
y: 100,
|
|
width: 200,
|
|
height: 100,
|
|
angle: 0,
|
|
strokeColor: "#000000",
|
|
backgroundColor: "transparent",
|
|
fillStyle: "solid",
|
|
strokeWidth: 1,
|
|
strokeStyle: "solid",
|
|
roughness: 1,
|
|
opacity: 100,
|
|
groupIds: [],
|
|
frameId: null,
|
|
roundness: { type: 3 },
|
|
seed: 12345,
|
|
version: 1,
|
|
versionNonce: 67890,
|
|
isDeleted: false,
|
|
boundElements: null,
|
|
updated: Date.now(),
|
|
link: null,
|
|
locked: false,
|
|
}
|
|
],
|
|
appState: {
|
|
viewBackgroundColor: "#ffffff"
|
|
},
|
|
files: {}
|
|
});
|
|
|
|
// Write temp file
|
|
// tempFile was here
|
|
|
|
// Use page.evaluate to check if we can proceed
|
|
// Actually, Playwright has setInputFiles which can handle this
|
|
|
|
// Find the import file input
|
|
const fileInput = page.locator("#dashboard-import");
|
|
|
|
// Create a buffer from the fixture content
|
|
await fileInput.setInputFiles({
|
|
name: `Import_ExcalidrawTest_${Date.now()}.excalidraw`,
|
|
mimeType: "application/json",
|
|
buffer: Buffer.from(fixtureContent),
|
|
});
|
|
|
|
// Wait for upload to complete - the UploadStatus component shows "Done" when finished
|
|
await expect(page.getByText("Uploads (Done)")).toBeVisible({ timeout: 10000 });
|
|
|
|
// Reload to ensure dashboard state reflects the newly imported drawing
|
|
await page.reload({ waitUntil: "networkidle" });
|
|
|
|
// Verify the drawing was imported - the drawing name is the filename without extension
|
|
await page.getByPlaceholder("Search drawings...").fill("Import_ExcalidrawTest");
|
|
await page.waitForTimeout(1000);
|
|
|
|
const importedCards = page.locator("[id^='drawing-card-']");
|
|
await expect(importedCards.first()).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test("should import JSON drawing file from Dashboard", async ({ page }) => {
|
|
await page.goto("/");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const timestamp = Date.now();
|
|
const testName = `Import_JSONTest_${timestamp}`;
|
|
|
|
// Create a valid excalidraw JSON file with required fields
|
|
const jsonContent = JSON.stringify({
|
|
type: "excalidraw",
|
|
version: 2,
|
|
source: "e2e-test",
|
|
elements: [
|
|
{
|
|
id: "test-element",
|
|
type: "rectangle",
|
|
x: 100,
|
|
y: 100,
|
|
width: 100,
|
|
height: 100,
|
|
angle: 0,
|
|
strokeColor: "#000000",
|
|
backgroundColor: "transparent",
|
|
fillStyle: "solid",
|
|
strokeWidth: 1,
|
|
strokeStyle: "solid",
|
|
roughness: 1,
|
|
opacity: 100,
|
|
groupIds: [],
|
|
frameId: null,
|
|
roundness: null,
|
|
seed: 12345,
|
|
version: 1,
|
|
versionNonce: 12345,
|
|
isDeleted: false,
|
|
boundElements: null,
|
|
updated: Date.now(),
|
|
link: null,
|
|
locked: false,
|
|
}
|
|
],
|
|
appState: { viewBackgroundColor: "#ffffff" },
|
|
files: {}
|
|
});
|
|
|
|
const fileInput = page.locator("#dashboard-import");
|
|
|
|
await fileInput.setInputFiles({
|
|
name: `${testName}.json`,
|
|
mimeType: "application/json",
|
|
buffer: Buffer.from(jsonContent),
|
|
});
|
|
|
|
// Wait for upload to complete - the UploadStatus component shows "Done" when finished
|
|
await expect(page.getByText("Uploads (Done)")).toBeVisible({ timeout: 15000 });
|
|
|
|
// Check if upload failed (shows "Failed" text in the upload status)
|
|
const failedIndicator = page.getByText("Failed");
|
|
if (await failedIndicator.isVisible()) {
|
|
console.log("Import failed - skipping rest of test");
|
|
return;
|
|
}
|
|
|
|
// Reload to force a fresh fetch of drawings after import
|
|
await page.reload({ waitUntil: "networkidle" });
|
|
|
|
// Clear any existing search and search for the imported drawing
|
|
const searchInput = page.getByPlaceholder("Search drawings...");
|
|
await searchInput.clear();
|
|
await searchInput.fill(testName);
|
|
await page.waitForTimeout(1500);
|
|
|
|
// Wait for the card to appear - the drawing should be visible in the UI
|
|
const importedCards = page.locator("[id^='drawing-card-']");
|
|
await expect(importedCards.first()).toBeVisible({ timeout: 15000 });
|
|
});
|
|
|
|
test("should show error for invalid import file", async ({ page }) => {
|
|
await page.goto("/");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
// Create an invalid file
|
|
const invalidContent = "this is not valid JSON or excalidraw format {}{}";
|
|
|
|
const fileInput = page.locator("#dashboard-import");
|
|
|
|
await fileInput.setInputFiles({
|
|
name: `Import_Invalid_${Date.now()}.excalidraw`,
|
|
mimeType: "application/json",
|
|
buffer: Buffer.from(invalidContent),
|
|
});
|
|
|
|
// Wait for upload to complete and check for failure indicator
|
|
await expect(page.getByText("Uploads (Done)")).toBeVisible({ timeout: 10000 });
|
|
// Should show "Failed" status in the upload status component
|
|
await expect(page.getByText("Failed")).toBeVisible();
|
|
});
|
|
|
|
test("should import multiple drawings at once", async ({ page }) => {
|
|
await page.goto("/");
|
|
await page.waitForLoadState("networkidle");
|
|
|
|
const timestamp = Date.now();
|
|
const searchPrefix = `Import_Multi_${timestamp}`;
|
|
const files = [
|
|
{
|
|
name: `${searchPrefix}_A.excalidraw`,
|
|
mimeType: "application/json",
|
|
buffer: Buffer.from(JSON.stringify({
|
|
type: "excalidraw",
|
|
version: 2,
|
|
elements: [],
|
|
appState: { viewBackgroundColor: "#ffffff" },
|
|
files: {}
|
|
})),
|
|
},
|
|
{
|
|
name: `${searchPrefix}_B.excalidraw`,
|
|
mimeType: "application/json",
|
|
buffer: Buffer.from(JSON.stringify({
|
|
type: "excalidraw",
|
|
version: 2,
|
|
elements: [],
|
|
appState: { viewBackgroundColor: "#f0f0f0" },
|
|
files: {}
|
|
})),
|
|
},
|
|
];
|
|
|
|
const fileInput = page.locator("#dashboard-import");
|
|
await fileInput.setInputFiles(files);
|
|
|
|
// Wait for upload to complete - the UploadStatus component shows "Done" when finished
|
|
await expect(page.getByText("Uploads (Done)")).toBeVisible({ timeout: 10000 });
|
|
|
|
// Verify both were imported by searching for the unique prefix
|
|
await page.getByPlaceholder("Search drawings...").fill(searchPrefix);
|
|
await page.waitForTimeout(500);
|
|
|
|
const importedCards = page.locator("[id^='drawing-card-']");
|
|
await expect(importedCards).toHaveCount(2);
|
|
});
|
|
});
|
|
|
|
test.describe("Database Import Verification", () => {
|
|
test("should verify SQLite import endpoint exists", async ({ request }) => {
|
|
// Test that the verification endpoint responds
|
|
// We don't actually import a database as that would affect the test environment
|
|
const response = await request.post(`${API_URL}/import/sqlite/verify`, {
|
|
headers: await getCsrfHeaders(request),
|
|
// Send empty form data to test endpoint exists
|
|
multipart: {
|
|
db: {
|
|
name: "test.sqlite",
|
|
mimeType: "application/x-sqlite3",
|
|
buffer: Buffer.from(""),
|
|
},
|
|
},
|
|
});
|
|
|
|
// Should get an error response since the file is empty/invalid
|
|
// But the endpoint should exist
|
|
expect([400, 500]).toContain(response.status());
|
|
});
|
|
});
|