mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: production Docker, Playwright tests, settings API, and bug fixes
- Add user management endpoints (register, list, delete, change password) - Add API key management (create, list, delete) - Add settings persistence endpoints (get, put) - Wire settings dialog to real backend (People, API Keys, System, Security) - Fix login auth flow (window.location.href for full reload) - Fix download URLs returning 401 (make public since UUIDs are unguessable) - Fix border tool shadowColor validation (accept 6-8 hex digits) - Fix remove-bg alpha matting fallback (retry without on failure) - Fix AI tool silent fallbacks (report errors instead of no-ops) - Add checkerboard background to before/after slider for transparency - Add progress bars to all AI tool components - Add Playwright E2E test suite (131 tests across 9 test files) - Rewrite Dockerfile for production (tsx runtime, pre-baked AI models) - Add .dockerignore for faster builds - Add proper accessible labels to login form
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { test as base, expect, type Page } from "@playwright/test";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// login() — fill the login form and submit (for tests that need fresh login)
|
||||
// ---------------------------------------------------------------------------
|
||||
export async function login(
|
||||
page: Page,
|
||||
username = "admin",
|
||||
password = "admin",
|
||||
) {
|
||||
await page.goto("/login");
|
||||
await page.getByLabel("Username").fill(username);
|
||||
await page.getByLabel("Password").fill(password);
|
||||
await page.getByRole("button", { name: /login/i }).click();
|
||||
await page.waitForURL("/", { timeout: 15_000 });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// createTestImageFile() — create a small test PNG on disk and return its path
|
||||
// ---------------------------------------------------------------------------
|
||||
let _testImagePath: string | null = null;
|
||||
|
||||
export function getTestImagePath(): string {
|
||||
if (_testImagePath && fs.existsSync(_testImagePath)) return _testImagePath;
|
||||
|
||||
const dir = path.join(process.cwd(), "test-results");
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
_testImagePath = path.join(dir, "test-image.png");
|
||||
|
||||
try {
|
||||
const script = [
|
||||
"const sharp = require('sharp');",
|
||||
`sharp({create:{width:100,height:100,channels:4,background:{r:255,g:0,b:0,alpha:1}}}).png().toFile('${_testImagePath.replace(/'/g, "\\'")}')`,
|
||||
].join(" ");
|
||||
execFileSync("node", ["-e", script], {
|
||||
cwd: process.cwd(),
|
||||
timeout: 5000,
|
||||
});
|
||||
} catch {
|
||||
// Fallback: write a minimal 1x1 PNG manually
|
||||
const minimalPng = Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwADhQGAWjR9awAAAABJRU5ErkJggg==",
|
||||
"base64",
|
||||
);
|
||||
fs.writeFileSync(_testImagePath, minimalPng);
|
||||
}
|
||||
|
||||
return _testImagePath;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// uploadTestImage() — upload a test image via the file chooser on a tool page
|
||||
// ---------------------------------------------------------------------------
|
||||
export async function uploadTestImage(page: Page): Promise<void> {
|
||||
const testImagePath = getTestImagePath();
|
||||
|
||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||
const dropzone = page.locator("[class*='border-dashed']").first();
|
||||
await dropzone.click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(testImagePath);
|
||||
|
||||
// Wait for React state to update
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// waitForProcessing() — wait for processing to complete
|
||||
// ---------------------------------------------------------------------------
|
||||
export async function waitForProcessing(page: Page, timeoutMs = 30_000) {
|
||||
try {
|
||||
const spinner = page.locator("[class*='animate-spin']");
|
||||
if (await spinner.isVisible({ timeout: 2000 })) {
|
||||
await spinner.waitFor({ state: "hidden", timeout: timeoutMs });
|
||||
}
|
||||
} catch {
|
||||
// No spinner appeared — processing may have been instant
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Custom test fixture — loggedInPage uses the saved storageState
|
||||
// (all "chromium" project tests already have auth via storageState,
|
||||
// but this provides backward compatibility for tests that use it)
|
||||
// ---------------------------------------------------------------------------
|
||||
export const test = base.extend<{ loggedInPage: Page }>({
|
||||
loggedInPage: async ({ page }, use) => {
|
||||
// storageState is already loaded by the project config, just navigate
|
||||
await page.goto("/");
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
|
||||
export { expect };
|
||||
Reference in New Issue
Block a user