docs: Sprint 4 visual tour with 19 Playwright screenshots
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Mock backend for screenshot tour.
|
||||
* Returns a valid auth response for any login attempt.
|
||||
* Run: node e2e/mock-backend.mjs
|
||||
*/
|
||||
import http from "node:http"
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
// CORS headers
|
||||
res.setHeader("Access-Control-Allow-Origin", "*")
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(204)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
// Login endpoint
|
||||
if (req.url === "/api/v1/auth/login" && req.method === "POST") {
|
||||
let body = ""
|
||||
req.on("data", (chunk) => (body += chunk))
|
||||
req.on("end", () => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" })
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
accessToken: "mock-jwt-token-for-screenshots",
|
||||
refreshToken: "mock-refresh-token",
|
||||
expiresIn: 3600,
|
||||
member: {
|
||||
id: "1",
|
||||
email: "admin@cannamanage.de",
|
||||
clubName: "Grüner Daumen e.V.",
|
||||
role: "ADMIN",
|
||||
clubId: "club-1",
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Catch-all for other API requests
|
||||
res.writeHead(200, { "Content-Type": "application/json" })
|
||||
res.end(JSON.stringify({ status: "ok" }))
|
||||
})
|
||||
|
||||
server.listen(8080, () => {
|
||||
console.log("🟢 Mock backend running on http://localhost:8080")
|
||||
})
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* CannaManage Sprint 4 — Visual Screenshot Tour
|
||||
*
|
||||
* Takes screenshots of every page in both dark and light mode.
|
||||
* Requires: pnpm exec playwright install chromium
|
||||
* Usage: pnpm exec playwright test e2e/screenshot-tour.spec.ts --reporter=list
|
||||
*
|
||||
* Prerequisites:
|
||||
* 1. Start mock backend: node e2e/mock-backend.mjs
|
||||
* 2. Start dev server: pnpm dev
|
||||
*/
|
||||
import path from "path"
|
||||
|
||||
import { expect, test } from "@playwright/test"
|
||||
|
||||
import type { Page } from "@playwright/test"
|
||||
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "..", "docs", "screenshots")
|
||||
|
||||
// Pages accessible without auth
|
||||
const PUBLIC_PAGES = [
|
||||
{ route: "/login", name: "01-login", title: "Admin Login" },
|
||||
{ route: "/portal-login", name: "02-portal-login", title: "Member Portal Login" },
|
||||
]
|
||||
|
||||
// Admin pages (require auth session)
|
||||
const ADMIN_PAGES = [
|
||||
{ route: "/dashboard", name: "03-dashboard", title: "Club Dashboard" },
|
||||
{ route: "/members", name: "04-members", title: "Member Management" },
|
||||
{ route: "/distributions", name: "05-distributions", title: "Distribution History" },
|
||||
{ route: "/distributions/new", name: "06-distribution-new", title: "New Distribution (Multi-Step)" },
|
||||
{ route: "/stock", name: "07-stock", title: "Stock & Batch Management" },
|
||||
{ route: "/stock/new", name: "08-stock-new", title: "Add New Batch" },
|
||||
{ route: "/reports", name: "09-reports", title: "Compliance Reports" },
|
||||
]
|
||||
|
||||
// Portal pages (no admin auth needed per middleware)
|
||||
const PORTAL_PAGES = [
|
||||
{ route: "/portal/dashboard", name: "10-portal-dashboard", title: "Member Quota Overview" },
|
||||
{ route: "/portal/history", name: "11-portal-history", title: "My Distribution History" },
|
||||
{ route: "/portal/profile", name: "12-portal-profile", title: "Profile & Settings" },
|
||||
]
|
||||
|
||||
async function setTheme(page: Page, theme: "dark" | "light") {
|
||||
await page.evaluate((t) => {
|
||||
localStorage.setItem("theme", t)
|
||||
document.documentElement.classList.remove("dark", "light")
|
||||
document.documentElement.classList.add(t)
|
||||
// Also set the next-themes cookie
|
||||
document.cookie = `theme=${t}; path=/`
|
||||
}, theme)
|
||||
}
|
||||
|
||||
async function capturePageScreenshot(
|
||||
page: Page,
|
||||
route: string,
|
||||
name: string,
|
||||
theme: "dark" | "light"
|
||||
) {
|
||||
await page.goto(route, { waitUntil: "domcontentloaded" })
|
||||
await page.waitForTimeout(2000) // Wait for hydration + animations
|
||||
|
||||
await setTheme(page, theme)
|
||||
await page.waitForTimeout(500) // Let theme apply
|
||||
|
||||
const filename = `${name}-${theme}.png`
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, filename),
|
||||
fullPage: true,
|
||||
})
|
||||
|
||||
return filename
|
||||
}
|
||||
|
||||
test.describe("CannaManage Screenshot Tour", () => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
test("capture all pages in dark and light mode", async ({ page }) => {
|
||||
const results: { name: string; title: string; dark: string; light: string }[] = []
|
||||
|
||||
// --- PUBLIC PAGES ---
|
||||
for (const p of PUBLIC_PAGES) {
|
||||
const dark = await capturePageScreenshot(page, p.route, p.name, "dark")
|
||||
const light = await capturePageScreenshot(page, p.route, p.name, "light")
|
||||
results.push({ name: p.name, title: p.title, dark, light })
|
||||
}
|
||||
|
||||
// --- LOGIN to get admin session ---
|
||||
await page.goto("/login", { waitUntil: "domcontentloaded" })
|
||||
await page.waitForTimeout(2000)
|
||||
|
||||
const emailField = page.locator('input[id="email"]')
|
||||
const passwordField = page.locator('input[id="password"]')
|
||||
const submitButton = page.locator('button[type="submit"]')
|
||||
|
||||
if (await emailField.isVisible({ timeout: 5000 })) {
|
||||
await emailField.fill("admin@cannamanage.de")
|
||||
await passwordField.fill("admin123")
|
||||
await submitButton.click()
|
||||
// Wait for redirect after login
|
||||
await page.waitForTimeout(5000)
|
||||
}
|
||||
|
||||
// Check if we got authenticated (redirected to dashboard)
|
||||
const isAuthenticated = page.url().includes("/dashboard") || page.url().includes("/login")
|
||||
|
||||
if (page.url().includes("/dashboard")) {
|
||||
// --- ADMIN PAGES (authenticated) ---
|
||||
for (const p of ADMIN_PAGES) {
|
||||
const dark = await capturePageScreenshot(page, p.route, p.name, "dark")
|
||||
const light = await capturePageScreenshot(page, p.route, p.name, "light")
|
||||
results.push({ name: p.name, title: p.title, dark, light })
|
||||
}
|
||||
} else {
|
||||
// Auth failed — still capture what we can see (login page with error)
|
||||
console.log("⚠️ Auth failed — admin pages will show login redirect")
|
||||
for (const p of ADMIN_PAGES) {
|
||||
const dark = await capturePageScreenshot(page, p.route, p.name, "dark")
|
||||
results.push({ name: p.name, title: `${p.title} (auth required)`, dark, light: dark })
|
||||
}
|
||||
}
|
||||
|
||||
// --- PORTAL PAGES (no auth needed) ---
|
||||
for (const p of PORTAL_PAGES) {
|
||||
const dark = await capturePageScreenshot(page, p.route, p.name, "dark")
|
||||
const light = await capturePageScreenshot(page, p.route, p.name, "light")
|
||||
results.push({ name: p.name, title: p.title, dark, light })
|
||||
}
|
||||
|
||||
// Generate markdown index
|
||||
let md = "# CannaManage — Visual Tour (Sprint 4)\n\n"
|
||||
md += `**Generated:** ${new Date().toISOString().split("T")[0]}\n\n`
|
||||
md += "---\n\n"
|
||||
|
||||
for (const r of results) {
|
||||
md += `## ${r.title}\n\n`
|
||||
md += `| Dark Mode | Light Mode |\n`
|
||||
md += `|-----------|------------|\n`
|
||||
md += `|  |  |\n\n`
|
||||
}
|
||||
|
||||
// Write the markdown file
|
||||
const fs = await import("fs")
|
||||
const docsDir = path.join(__dirname, "..", "docs")
|
||||
if (!fs.existsSync(path.join(docsDir, "screenshots"))) {
|
||||
fs.mkdirSync(path.join(docsDir, "screenshots"), { recursive: true })
|
||||
}
|
||||
fs.writeFileSync(path.join(docsDir, "visual-tour.md"), md)
|
||||
|
||||
console.log(`\n✅ Screenshot tour complete! ${results.length} pages captured.`)
|
||||
console.log(`📄 Markdown: docs/visual-tour.md`)
|
||||
console.log(`📸 Screenshots: docs/screenshots/`)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user