mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: restore missing text options and font loader, wire text tool options bar, fix e2e test selectors
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
// apps/web/src/components/editor/common/font-loader.ts
|
||||
|
||||
const SYSTEM_FONTS = [
|
||||
"Arial",
|
||||
"Helvetica",
|
||||
"Georgia",
|
||||
"Times New Roman",
|
||||
"Verdana",
|
||||
"Courier New",
|
||||
"Trebuchet MS",
|
||||
"Impact",
|
||||
"Comic Sans MS",
|
||||
] as const;
|
||||
|
||||
const GOOGLE_FONTS = [
|
||||
"Inter",
|
||||
"Roboto",
|
||||
"Open Sans",
|
||||
"Lato",
|
||||
"Montserrat",
|
||||
"Poppins",
|
||||
"Source Sans 3",
|
||||
"Playfair Display",
|
||||
"Merriweather",
|
||||
"Raleway",
|
||||
"Oswald",
|
||||
"Nunito",
|
||||
"Ubuntu",
|
||||
"PT Sans",
|
||||
"Fira Sans",
|
||||
"Work Sans",
|
||||
"Barlow",
|
||||
"DM Sans",
|
||||
"Space Grotesk",
|
||||
"Bebas Neue",
|
||||
"Caveat",
|
||||
"Pacifico",
|
||||
"Dancing Script",
|
||||
"Permanent Marker",
|
||||
"Press Start 2P",
|
||||
] as const;
|
||||
|
||||
const loadedFonts = new Set<string>();
|
||||
|
||||
export function isSystemFont(name: string): boolean {
|
||||
return (SYSTEM_FONTS as readonly string[]).includes(name);
|
||||
}
|
||||
|
||||
export function getAllFonts(): { system: string[]; google: string[] } {
|
||||
return {
|
||||
system: [...SYSTEM_FONTS],
|
||||
google: [...GOOGLE_FONTS],
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadGoogleFont(name: string): Promise<void> {
|
||||
if (isSystemFont(name) || loadedFonts.has(name)) return;
|
||||
|
||||
const slug = name.replace(/ /g, "+");
|
||||
const url = `https://fonts.googleapis.com/css2?family=${slug}:wght@400;700&display=swap`;
|
||||
|
||||
// Add the stylesheet link so the browser fetches the font files
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = url;
|
||||
document.head.appendChild(link);
|
||||
|
||||
// Use the CSS Font Loading API to detect when the font is actually ready
|
||||
try {
|
||||
await document.fonts.load(`16px "${name}"`);
|
||||
loadedFonts.add(name);
|
||||
} catch {
|
||||
// Font may still load via the stylesheet even if the API rejects;
|
||||
// mark as loaded so we don't retry endlessly.
|
||||
loadedFonts.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
export function isFontLoaded(name: string): boolean {
|
||||
return isSystemFont(name) || loadedFonts.has(name);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { MoveOptions } from "./options/move-options";
|
||||
import { PixelBrushOptions } from "./options/pixel-brush-options";
|
||||
import { SelectionOptions } from "./options/selection-options";
|
||||
import { ShapeOptions } from "./options/shape-options";
|
||||
import { TextOptions } from "./options/text-options";
|
||||
|
||||
function getOptionsComponent(tool: ToolType): React.ComponentType | null {
|
||||
switch (tool) {
|
||||
@@ -50,11 +51,12 @@ function getOptionsComponent(tool: ToolType): React.ComponentType | null {
|
||||
case "shape-polygon":
|
||||
case "shape-star":
|
||||
return ShapeOptions;
|
||||
case "text":
|
||||
return TextOptions;
|
||||
case "transform":
|
||||
case "eyedropper":
|
||||
case "hand":
|
||||
case "zoom":
|
||||
case "eyedropper":
|
||||
case "text":
|
||||
case "transform":
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
// apps/web/src/components/editor/options/text-options.tsx
|
||||
|
||||
import {
|
||||
AlignCenter,
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
Bold,
|
||||
Italic,
|
||||
Strikethrough,
|
||||
Type,
|
||||
Underline,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEditorStore } from "@/stores/editor-store";
|
||||
import type { TextAttrs } from "@/types/editor";
|
||||
import { getAllFonts, isSystemFont, loadGoogleFont } from "../common/font-loader";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getSelectedTextAttrs(): TextAttrs | null {
|
||||
const { selectedObjectIds, objects } = useEditorStore.getState();
|
||||
if (selectedObjectIds.length !== 1) return null;
|
||||
const obj = objects.find((o) => o.id === selectedObjectIds[0] && o.type === "text");
|
||||
return obj ? (obj.attrs as TextAttrs) : null;
|
||||
}
|
||||
|
||||
function updateSelected(partial: Partial<TextAttrs>) {
|
||||
const { selectedObjectIds } = useEditorStore.getState();
|
||||
for (const id of selectedObjectIds) {
|
||||
useEditorStore.getState().updateObject(id, partial);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sub-components
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ToggleButton({
|
||||
active,
|
||||
onClick,
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
title={label}
|
||||
aria-label={label}
|
||||
aria-pressed={active}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"flex items-center justify-center w-7 h-7 rounded transition-colors",
|
||||
active ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function NumberInput({
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
label,
|
||||
onChange,
|
||||
width = "w-16",
|
||||
}: {
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
label: string;
|
||||
onChange: (v: number) => void;
|
||||
width?: string;
|
||||
}) {
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
aria-label={label}
|
||||
title={label}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
onChange={(e) => {
|
||||
const n = Number.parseFloat(e.target.value);
|
||||
if (!Number.isNaN(n)) onChange(Math.max(min, Math.min(max, n)));
|
||||
}}
|
||||
className={cn(
|
||||
"h-7 rounded border border-border bg-background px-1.5 text-xs text-center",
|
||||
"focus:outline-none focus:ring-1 focus:ring-ring",
|
||||
width,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Font Dropdown
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function FontDropdown({ value, onChange }: { value: string; onChange: (name: string) => void }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const fonts = useMemo(() => getAllFonts(), []);
|
||||
|
||||
// Close on outside click
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handler);
|
||||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, [open]);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
async (name: string) => {
|
||||
if (!isSystemFont(name)) {
|
||||
await loadGoogleFont(name);
|
||||
}
|
||||
onChange(name);
|
||||
setOpen(false);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Font family"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 h-7 px-2 rounded border border-border bg-background",
|
||||
"text-xs hover:bg-muted truncate max-w-[160px]",
|
||||
)}
|
||||
style={{ fontFamily: value }}
|
||||
>
|
||||
<Type size={12} className="shrink-0 text-muted-foreground" />
|
||||
<span className="truncate">{value}</span>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full left-0 mt-1 z-50 w-56 max-h-72 overflow-y-auto",
|
||||
"rounded-md border border-border bg-popover shadow-md",
|
||||
)}
|
||||
>
|
||||
{/* System fonts */}
|
||||
<div className="px-2 py-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
System Fonts
|
||||
</div>
|
||||
{fonts.system.map((name) => (
|
||||
<button
|
||||
type="button"
|
||||
key={name}
|
||||
onClick={() => handleSelect(name)}
|
||||
className={cn(
|
||||
"w-full text-left px-3 py-1.5 text-sm hover:bg-muted transition-colors",
|
||||
value === name && "bg-muted font-medium",
|
||||
)}
|
||||
style={{ fontFamily: name }}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<div className="h-px bg-border my-1" />
|
||||
|
||||
{/* Google fonts */}
|
||||
<div className="px-2 py-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
Google Fonts
|
||||
</div>
|
||||
{fonts.google.map((name) => (
|
||||
<button
|
||||
type="button"
|
||||
key={name}
|
||||
onClick={() => handleSelect(name)}
|
||||
className={cn(
|
||||
"w-full text-left px-3 py-1.5 text-sm hover:bg-muted transition-colors",
|
||||
value === name && "bg-muted font-medium",
|
||||
)}
|
||||
style={{ fontFamily: name }}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function TextOptions() {
|
||||
const selectedObjectIds = useEditorStore((s) => s.selectedObjectIds);
|
||||
const objects = useEditorStore((s) => s.objects);
|
||||
|
||||
// Derive current attrs from the selected text object
|
||||
const attrs = useMemo(() => {
|
||||
if (selectedObjectIds.length !== 1) return null;
|
||||
const obj = objects.find((o) => o.id === selectedObjectIds[0] && o.type === "text");
|
||||
return obj ? (obj.attrs as TextAttrs) : null;
|
||||
}, [selectedObjectIds, objects]);
|
||||
|
||||
if (!attrs) return null;
|
||||
|
||||
const isBold = attrs.fontStyle.includes("bold");
|
||||
const isItalic = attrs.fontStyle.includes("italic");
|
||||
const hasUnderline = attrs.textDecoration.includes("underline");
|
||||
const hasStrikethrough = attrs.textDecoration.includes("line-through");
|
||||
const isAreaText = attrs.wrap !== undefined;
|
||||
|
||||
const toggleBold = () => {
|
||||
const current = getSelectedTextAttrs();
|
||||
if (!current) return;
|
||||
const wasBold = current.fontStyle.includes("bold");
|
||||
const parts = current.fontStyle.split(" ").filter((p) => p !== "bold" && p !== "");
|
||||
if (!wasBold) parts.push("bold");
|
||||
updateSelected({ fontStyle: parts.join(" ") || "normal" });
|
||||
};
|
||||
|
||||
const toggleItalic = () => {
|
||||
const current = getSelectedTextAttrs();
|
||||
if (!current) return;
|
||||
const wasItalic = current.fontStyle.includes("italic");
|
||||
const parts = current.fontStyle.split(" ").filter((p) => p !== "italic" && p !== "");
|
||||
if (!wasItalic) parts.push("italic");
|
||||
updateSelected({ fontStyle: parts.join(" ") || "normal" });
|
||||
};
|
||||
|
||||
const toggleUnderline = () => {
|
||||
const current = getSelectedTextAttrs();
|
||||
if (!current) return;
|
||||
const had = current.textDecoration.includes("underline");
|
||||
const parts = current.textDecoration.split(" ").filter((p) => p !== "underline" && p !== "");
|
||||
if (!had) parts.push("underline");
|
||||
updateSelected({ textDecoration: parts.join(" ") });
|
||||
};
|
||||
|
||||
const toggleStrikethrough = () => {
|
||||
const current = getSelectedTextAttrs();
|
||||
if (!current) return;
|
||||
const had = current.textDecoration.includes("line-through");
|
||||
const parts = current.textDecoration.split(" ").filter((p) => p !== "line-through" && p !== "");
|
||||
if (!had) parts.push("line-through");
|
||||
updateSelected({ textDecoration: parts.join(" ") });
|
||||
};
|
||||
|
||||
const toggleTextMode = () => {
|
||||
const current = getSelectedTextAttrs();
|
||||
if (!current) return;
|
||||
if (current.wrap !== undefined) {
|
||||
// Switch to point text: remove width/height/wrap
|
||||
updateSelected({
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
wrap: undefined,
|
||||
});
|
||||
} else {
|
||||
// Switch to area text
|
||||
updateSelected({
|
||||
width: 200,
|
||||
height: 100,
|
||||
wrap: "word",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-wrap" data-testid="text-options">
|
||||
{/* Font family */}
|
||||
<FontDropdown
|
||||
value={attrs.fontFamily}
|
||||
onChange={(name) => updateSelected({ fontFamily: name })}
|
||||
/>
|
||||
|
||||
{/* Font size */}
|
||||
<NumberInput
|
||||
value={attrs.fontSize}
|
||||
min={1}
|
||||
max={999}
|
||||
step={1}
|
||||
label="Font size"
|
||||
onChange={(v) => updateSelected({ fontSize: v })}
|
||||
width="w-14"
|
||||
/>
|
||||
|
||||
<div className="h-4 w-px bg-border" />
|
||||
|
||||
{/* Bold / Italic / Underline / Strikethrough */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
<ToggleButton active={isBold} onClick={toggleBold} label="Bold">
|
||||
<Bold size={14} />
|
||||
</ToggleButton>
|
||||
<ToggleButton active={isItalic} onClick={toggleItalic} label="Italic">
|
||||
<Italic size={14} />
|
||||
</ToggleButton>
|
||||
<ToggleButton active={hasUnderline} onClick={toggleUnderline} label="Underline">
|
||||
<Underline size={14} />
|
||||
</ToggleButton>
|
||||
<ToggleButton active={hasStrikethrough} onClick={toggleStrikethrough} label="Strikethrough">
|
||||
<Strikethrough size={14} />
|
||||
</ToggleButton>
|
||||
</div>
|
||||
|
||||
<div className="h-4 w-px bg-border" />
|
||||
|
||||
{/* Alignment */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
<ToggleButton
|
||||
active={attrs.align === "left"}
|
||||
onClick={() => updateSelected({ align: "left" })}
|
||||
label="Align left"
|
||||
>
|
||||
<AlignLeft size={14} />
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
active={attrs.align === "center"}
|
||||
onClick={() => updateSelected({ align: "center" })}
|
||||
label="Align center"
|
||||
>
|
||||
<AlignCenter size={14} />
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
active={attrs.align === "right"}
|
||||
onClick={() => updateSelected({ align: "right" })}
|
||||
label="Align right"
|
||||
>
|
||||
<AlignRight size={14} />
|
||||
</ToggleButton>
|
||||
</div>
|
||||
|
||||
<div className="h-4 w-px bg-border" />
|
||||
|
||||
{/* Line height */}
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] text-muted-foreground">LH</span>
|
||||
<NumberInput
|
||||
value={attrs.lineHeight}
|
||||
min={0.5}
|
||||
max={3.0}
|
||||
step={0.1}
|
||||
label="Line height"
|
||||
onChange={(v) => updateSelected({ lineHeight: v })}
|
||||
width="w-14"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Letter spacing */}
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] text-muted-foreground">LS</span>
|
||||
<NumberInput
|
||||
value={attrs.letterSpacing}
|
||||
min={-5}
|
||||
max={20}
|
||||
step={0.5}
|
||||
label="Letter spacing"
|
||||
onChange={(v) => updateSelected({ letterSpacing: v })}
|
||||
width="w-14"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-4 w-px bg-border" />
|
||||
|
||||
{/* Color swatch */}
|
||||
<label className="relative flex items-center" title="Text color">
|
||||
<div
|
||||
className="w-6 h-6 rounded border border-border cursor-pointer"
|
||||
style={{ backgroundColor: attrs.fill }}
|
||||
/>
|
||||
<input
|
||||
type="color"
|
||||
aria-label="Text color"
|
||||
value={attrs.fill}
|
||||
onChange={(e) => updateSelected({ fill: e.target.value })}
|
||||
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="h-4 w-px bg-border" />
|
||||
|
||||
{/* Point / Area text toggle */}
|
||||
<button
|
||||
type="button"
|
||||
title={isAreaText ? "Switch to Point Text" : "Switch to Area Text"}
|
||||
aria-label={isAreaText ? "Switch to Point Text" : "Switch to Area Text"}
|
||||
onClick={toggleTextMode}
|
||||
className={cn(
|
||||
"h-7 px-2 rounded border border-border text-xs transition-colors",
|
||||
"hover:bg-muted",
|
||||
)}
|
||||
>
|
||||
{isAreaText ? "Area" : "Point"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,712 @@
|
||||
import path from "node:path";
|
||||
import { expect, type Page, test } from "@playwright/test";
|
||||
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "screenshots");
|
||||
let screenshotIndex = 0;
|
||||
|
||||
async function snap(page: Page, name: string) {
|
||||
screenshotIndex++;
|
||||
const filename = `${String(screenshotIndex).padStart(2, "0")}-${name}.png`;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, filename) });
|
||||
}
|
||||
|
||||
async function login(page: Page) {
|
||||
await page.goto("/login");
|
||||
await page.waitForTimeout(2000);
|
||||
await page.fill('input[placeholder*="username" i]', "admin");
|
||||
await page.fill('input[placeholder*="password" i]', "admin");
|
||||
await page.click('button:has-text("Login")');
|
||||
await page.waitForURL("**/", { timeout: 10000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
async function createNewDocument(page: Page) {
|
||||
const newDocBtn = page.getByText("New Document");
|
||||
if (await newDocBtn.isVisible().catch(() => false)) {
|
||||
await newDocBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function getCanvasBox(page: Page) {
|
||||
const canvas = page.locator("canvas").first();
|
||||
const box = await canvas.boundingBox();
|
||||
if (!box) throw new Error("Canvas not found");
|
||||
return box;
|
||||
}
|
||||
|
||||
test.describe("Image Editor - Full GUI Test Suite", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
screenshotIndex = 0;
|
||||
await login(page);
|
||||
});
|
||||
|
||||
test("01 - Navigation & Layout", async ({ page }) => {
|
||||
// Check sidebar has Editor item
|
||||
await page.goto("/");
|
||||
await page.waitForTimeout(2000);
|
||||
const editorLink = page.locator('a:has-text("Editor")');
|
||||
await expect(editorLink).toBeVisible();
|
||||
await snap(page, "sidebar-with-editor");
|
||||
|
||||
// Navigate to editor
|
||||
await editorLink.click();
|
||||
await page.waitForTimeout(2000);
|
||||
await expect(page).toHaveURL(/\/editor/);
|
||||
await snap(page, "editor-welcome-screen");
|
||||
|
||||
// Verify four-zone layout elements
|
||||
const toolbar = page.locator('[data-tool="move"]');
|
||||
await expect(toolbar).toBeVisible();
|
||||
|
||||
const layersTab = page.locator('[data-testid="tab-layers"]');
|
||||
await expect(layersTab).toBeVisible();
|
||||
|
||||
const adjustmentsTab = page.locator('[data-testid="tab-adjustments"]');
|
||||
await expect(adjustmentsTab).toBeVisible();
|
||||
|
||||
const historyTab = page.locator('[data-testid="tab-history"]');
|
||||
await expect(historyTab).toBeVisible();
|
||||
});
|
||||
|
||||
test("02 - Welcome Screen & New Document", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Welcome screen visible
|
||||
await expect(page.getByText("Image Editor")).toBeVisible();
|
||||
await expect(page.getByText("Open Image")).toBeVisible();
|
||||
await expect(page.getByText("New Document")).toBeVisible();
|
||||
await snap(page, "welcome-screen");
|
||||
|
||||
// Open new document dialog
|
||||
await page.getByText("New Document").click();
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.getByText("Preset")).toBeVisible();
|
||||
await snap(page, "new-doc-dialog");
|
||||
|
||||
// Change preset to Instagram
|
||||
await page.locator("select").first().selectOption("1080x1080 (Instagram)");
|
||||
await page.waitForTimeout(300);
|
||||
await snap(page, "new-doc-instagram-preset");
|
||||
|
||||
// Select transparent background
|
||||
await page.getByText("Transparent").click();
|
||||
await page.waitForTimeout(300);
|
||||
await snap(page, "new-doc-transparent");
|
||||
|
||||
// Create the document
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Canvas should be visible with checkerboard (transparent)
|
||||
const canvas = page.locator("canvas").first();
|
||||
await expect(canvas).toBeVisible();
|
||||
await snap(page, "canvas-after-create");
|
||||
});
|
||||
|
||||
test("03 - Brush Tool", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
|
||||
// Activate brush via toolbar
|
||||
await page.locator('[data-tool="brush"]').click();
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page.locator('[data-tool-active="true"]')).toHaveAttribute("data-tool", "brush");
|
||||
await snap(page, "brush-tool-active");
|
||||
|
||||
// Verify options bar shows size/opacity/hardness
|
||||
await expect(page.getByText("Size", { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByText("Opacity", { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByText("Hardness", { exact: true }).first()).toBeVisible();
|
||||
|
||||
// Draw a brush stroke
|
||||
const box = await getCanvasBox(page);
|
||||
const before = await page.locator("canvas").first().screenshot();
|
||||
await page.mouse.move(box.x + 100, box.y + 200);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + 500, box.y + 300, { steps: 20 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(500);
|
||||
const after = await page.locator("canvas").first().screenshot();
|
||||
expect(Buffer.compare(before, after)).not.toBe(0);
|
||||
await snap(page, "brush-stroke-drawn");
|
||||
|
||||
// Change brush size via [ and ] keys
|
||||
await page.keyboard.press("]");
|
||||
await page.keyboard.press("]");
|
||||
await page.keyboard.press("]");
|
||||
await page.waitForTimeout(300);
|
||||
await snap(page, "brush-size-increased");
|
||||
});
|
||||
|
||||
test("04 - Eraser Tool", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
|
||||
// Draw something first with brush
|
||||
await page.locator('[data-tool="brush"]').click();
|
||||
await page.waitForTimeout(300);
|
||||
const box = await getCanvasBox(page);
|
||||
await page.mouse.move(box.x + 200, box.y + 200);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + 500, box.y + 200, { steps: 15 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Activate eraser via E key
|
||||
await page.keyboard.press("e");
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page.locator('[data-tool-active="true"]')).toHaveAttribute("data-tool", "eraser");
|
||||
await snap(page, "eraser-tool-active");
|
||||
|
||||
// Erase over the stroke
|
||||
await page.mouse.move(box.x + 300, box.y + 190);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + 400, box.y + 210, { steps: 10 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "eraser-stroke");
|
||||
});
|
||||
|
||||
test("05 - Shape Tools", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
const box = await getCanvasBox(page);
|
||||
|
||||
// Rectangle
|
||||
await page.locator('[data-tool="shape-rect"]').click();
|
||||
await page.waitForTimeout(300);
|
||||
await snap(page, "shape-rect-options");
|
||||
|
||||
await page.mouse.move(box.x + 100, box.y + 100);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + 300, box.y + 250, { steps: 10 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "shape-rect-drawn");
|
||||
|
||||
// Switch to ellipse via shape dropdown if available
|
||||
const shapeDropdown = page.locator("select").first();
|
||||
if (await shapeDropdown.isVisible().catch(() => false)) {
|
||||
await shapeDropdown.selectOption("Ellipse");
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
// Draw another shape
|
||||
await page.mouse.move(box.x + 400, box.y + 100);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + 600, box.y + 250, { steps: 10 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "shape-ellipse-drawn");
|
||||
|
||||
// Select and move with move tool
|
||||
await page.locator('[data-tool="move"]').click();
|
||||
await page.waitForTimeout(300);
|
||||
await page.mouse.click(box.x + 200, box.y + 175);
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "shape-selected-with-handles");
|
||||
});
|
||||
|
||||
test("06 - Text Tool", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
const box = await getCanvasBox(page);
|
||||
|
||||
// Activate text tool via T key
|
||||
await page.keyboard.press("t");
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page.locator('[data-tool-active="true"]')).toHaveAttribute("data-tool", "text");
|
||||
await snap(page, "text-tool-active");
|
||||
|
||||
// Click on canvas to place text
|
||||
await page.mouse.click(box.x + 200, box.y + 200);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Type some text (textarea should be active)
|
||||
await page.keyboard.type("Hello SnapOtter!");
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "text-typing");
|
||||
|
||||
// Click away to finalize
|
||||
await page.mouse.click(box.x + 50, box.y + 50);
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "text-finalized");
|
||||
});
|
||||
|
||||
test("07 - Crop Tool", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
const box = await getCanvasBox(page);
|
||||
|
||||
// Activate crop via C key
|
||||
await page.keyboard.press("c");
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.locator('[data-tool-active="true"]')).toHaveAttribute("data-tool", "crop");
|
||||
await snap(page, "crop-tool-active");
|
||||
|
||||
// Verify crop options show aspect ratio
|
||||
const cropOptions = page.getByText(/Free|1:1|4:3|16:9/);
|
||||
await snap(page, "crop-options-bar");
|
||||
});
|
||||
|
||||
test("08 - Layer System", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
|
||||
// Layers tab should be active by default
|
||||
await expect(page.locator('[data-testid="tab-layers"]')).toBeVisible();
|
||||
await page.locator('[data-testid="tab-layers"]').click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Should see Layer 1
|
||||
await expect(page.getByText("Layer 1")).toBeVisible();
|
||||
await snap(page, "layers-panel-default");
|
||||
|
||||
// Add a new layer via + button
|
||||
const addLayerBtn = page
|
||||
.locator("button")
|
||||
.filter({ has: page.locator("svg") })
|
||||
.locator("xpath=//button[contains(@class, 'items-center')]")
|
||||
.first();
|
||||
// Try clicking the + icon at bottom of layers panel
|
||||
const plusButtons = page
|
||||
.locator('[data-testid="tab-layers"]')
|
||||
.locator("..")
|
||||
.locator("..")
|
||||
.locator("button");
|
||||
await snap(page, "layers-before-add");
|
||||
|
||||
// Use keyboard shortcut to add layer
|
||||
await page.keyboard.press("Control+Shift+n");
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "layers-after-add");
|
||||
|
||||
// Check blend mode dropdown
|
||||
const blendDropdown = page.locator("select").filter({ hasText: /Normal/ });
|
||||
if (await blendDropdown.isVisible().catch(() => false)) {
|
||||
await blendDropdown.click();
|
||||
await page.waitForTimeout(300);
|
||||
await snap(page, "blend-mode-dropdown");
|
||||
}
|
||||
|
||||
// Toggle layer visibility
|
||||
const eyeIcons = page.locator(
|
||||
'[aria-label*="visibility" i], [aria-label*="toggle" i], [title*="visibility" i]',
|
||||
);
|
||||
if (
|
||||
await eyeIcons
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false)
|
||||
) {
|
||||
await eyeIcons.first().click();
|
||||
await page.waitForTimeout(300);
|
||||
await snap(page, "layer-visibility-toggled");
|
||||
}
|
||||
});
|
||||
|
||||
test("09 - Color System", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
|
||||
// Foreground/background swatches should be visible
|
||||
await expect(page.getByText("Foreground")).toBeVisible();
|
||||
await snap(page, "color-panel-default");
|
||||
|
||||
// Click foreground swatch to open picker
|
||||
const fgSwatch = page.locator('[data-testid="color-foreground"]').or(
|
||||
page
|
||||
.locator("button")
|
||||
.filter({ hasText: /Foreground/ })
|
||||
.locator("..")
|
||||
.locator("button")
|
||||
.first(),
|
||||
);
|
||||
|
||||
// Try clicking the color swatch area
|
||||
const colorArea = page.getByText("Foreground").locator("..").locator("button").first();
|
||||
if (await colorArea.isVisible().catch(() => false)) {
|
||||
await colorArea.click();
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "color-picker-open");
|
||||
}
|
||||
|
||||
// Test D key resets to black/white
|
||||
await page.keyboard.press("d");
|
||||
await page.waitForTimeout(300);
|
||||
await snap(page, "colors-reset-to-default");
|
||||
|
||||
// Test X key swaps colors
|
||||
await page.keyboard.press("x");
|
||||
await page.waitForTimeout(300);
|
||||
await snap(page, "colors-swapped");
|
||||
});
|
||||
|
||||
test("10 - Adjustments Panel", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
|
||||
// Switch to Adjustments tab
|
||||
await page.locator('[data-testid="tab-adjustments"]').click();
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "adjustments-panel");
|
||||
|
||||
// Verify adjustment sliders present
|
||||
await expect(page.getByText("Brightness", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Contrast", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("Saturation", { exact: true })).toBeVisible();
|
||||
await snap(page, "adjustments-sliders");
|
||||
|
||||
// Verify auto buttons
|
||||
await expect(page.getByText("Auto Tone")).toBeVisible();
|
||||
await expect(page.getByText("Auto Contrast")).toBeVisible();
|
||||
await snap(page, "auto-buttons");
|
||||
|
||||
// Scroll to see levels section
|
||||
const panel = page
|
||||
.locator('[data-testid="tab-adjustments"]')
|
||||
.locator("..")
|
||||
.locator("..")
|
||||
.locator("div.overflow-y-auto");
|
||||
if (await panel.isVisible().catch(() => false)) {
|
||||
await panel.evaluate((el) => (el.scrollTop = 400));
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "levels-section");
|
||||
|
||||
await panel.evaluate((el) => (el.scrollTop = 800));
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "curves-section");
|
||||
|
||||
await panel.evaluate((el) => (el.scrollTop = 1200));
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "filters-section");
|
||||
}
|
||||
});
|
||||
|
||||
test("11 - History & Undo/Redo", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
const box = await getCanvasBox(page);
|
||||
|
||||
// Draw a shape
|
||||
await page.locator('[data-tool="shape-rect"]').click();
|
||||
await page.waitForTimeout(300);
|
||||
await page.mouse.move(box.x + 100, box.y + 100);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + 300, box.y + 250, { steps: 10 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Draw a brush stroke
|
||||
await page.keyboard.press("b");
|
||||
await page.waitForTimeout(300);
|
||||
await page.mouse.move(box.x + 400, box.y + 150);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + 600, box.y + 300, { steps: 15 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Open history panel
|
||||
await page.locator('[data-testid="tab-history"]').click();
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "history-with-actions");
|
||||
|
||||
// Verify history entries exist
|
||||
const historyEntries = page
|
||||
.locator('[data-testid="tab-history"]')
|
||||
.locator("..")
|
||||
.locator("..")
|
||||
.locator("div.overflow-y-auto");
|
||||
await snap(page, "history-panel-entries");
|
||||
|
||||
// Undo with Ctrl+Z
|
||||
const beforeUndo = await page.locator("canvas").first().screenshot();
|
||||
await page.keyboard.press("Control+z");
|
||||
await page.waitForTimeout(500);
|
||||
const afterUndo = await page.locator("canvas").first().screenshot();
|
||||
expect(Buffer.compare(beforeUndo, afterUndo)).not.toBe(0);
|
||||
await snap(page, "after-undo");
|
||||
|
||||
// Redo with Ctrl+Shift+Z
|
||||
await page.keyboard.press("Control+Shift+z");
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "after-redo");
|
||||
});
|
||||
|
||||
test("12 - Keyboard Shortcuts", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
|
||||
const toolShortcuts: [string, string][] = [
|
||||
["v", "move"],
|
||||
["b", "brush"],
|
||||
["e", "eraser"],
|
||||
["u", "shape-rect"],
|
||||
["t", "text"],
|
||||
["c", "crop"],
|
||||
["i", "eyedropper"],
|
||||
["h", "hand"],
|
||||
["z", "zoom"],
|
||||
["m", "marquee-rect"],
|
||||
["g", "fill"],
|
||||
];
|
||||
|
||||
for (const [key, expectedTool] of toolShortcuts) {
|
||||
await page.keyboard.press(key);
|
||||
await page.waitForTimeout(200);
|
||||
const activeTool = page.locator('[data-tool-active="true"]');
|
||||
const toolAttr = await activeTool.getAttribute("data-tool");
|
||||
// Just verify the active tool changed (some tools may have different sub-types)
|
||||
expect(toolAttr).toBeTruthy();
|
||||
}
|
||||
await snap(page, "keyboard-shortcuts-tested");
|
||||
|
||||
// Test D resets colors
|
||||
await page.keyboard.press("d");
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Test X swaps
|
||||
await page.keyboard.press("x");
|
||||
await page.waitForTimeout(200);
|
||||
await snap(page, "color-shortcuts-tested");
|
||||
});
|
||||
|
||||
test("13 - Right Panel Toggle", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
|
||||
// Panel should be visible
|
||||
await expect(page.locator('[data-testid="tab-layers"]')).toBeVisible();
|
||||
await snap(page, "panel-visible");
|
||||
|
||||
// Press Tab to toggle panel
|
||||
await page.keyboard.press("Tab");
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "panel-collapsed");
|
||||
|
||||
// Press Tab again to restore
|
||||
await page.keyboard.press("Tab");
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "panel-restored");
|
||||
});
|
||||
|
||||
test("14 - Multiple Shapes & Move", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
const box = await getCanvasBox(page);
|
||||
|
||||
// Draw 3 shapes
|
||||
await page.locator('[data-tool="shape-rect"]').click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Shape 1
|
||||
await page.mouse.move(box.x + 50, box.y + 50);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + 200, box.y + 150, { steps: 5 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Shape 2
|
||||
await page.mouse.move(box.x + 250, box.y + 50);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + 400, box.y + 150, { steps: 5 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Shape 3
|
||||
await page.mouse.move(box.x + 450, box.y + 50);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + 600, box.y + 150, { steps: 5 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(300);
|
||||
await snap(page, "three-shapes-drawn");
|
||||
|
||||
// Switch to move tool and select shape
|
||||
await page.keyboard.press("v");
|
||||
await page.waitForTimeout(300);
|
||||
await page.mouse.click(box.x + 125, box.y + 100);
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "shape-selected");
|
||||
|
||||
// Drag it
|
||||
await page.mouse.move(box.x + 125, box.y + 100);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + 125, box.y + 300, { steps: 10 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "shape-moved");
|
||||
|
||||
// Delete with Delete key
|
||||
await page.mouse.click(box.x + 325, box.y + 100);
|
||||
await page.waitForTimeout(300);
|
||||
await page.keyboard.press("Delete");
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "shape-deleted");
|
||||
});
|
||||
|
||||
test("15 - Export Dialog", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
const box = await getCanvasBox(page);
|
||||
|
||||
// Draw something
|
||||
await page.locator('[data-tool="shape-rect"]').click();
|
||||
await page.waitForTimeout(300);
|
||||
await page.mouse.move(box.x + 200, box.y + 150);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + 500, box.y + 350, { steps: 10 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Open export dialog via Ctrl+Shift+S
|
||||
await page.keyboard.press("Control+Shift+s");
|
||||
await page.waitForTimeout(1000);
|
||||
await snap(page, "export-dialog");
|
||||
|
||||
// Check format options
|
||||
const pngBtn = page.getByText("PNG");
|
||||
const jpegBtn = page.getByText("JPEG");
|
||||
const webpBtn = page.getByText("WebP");
|
||||
|
||||
if (await pngBtn.isVisible().catch(() => false)) {
|
||||
await expect(pngBtn).toBeVisible();
|
||||
await expect(jpegBtn).toBeVisible();
|
||||
await expect(webpBtn).toBeVisible();
|
||||
|
||||
// Select JPEG to see quality slider
|
||||
await jpegBtn.click();
|
||||
await page.waitForTimeout(300);
|
||||
await snap(page, "export-jpeg-quality");
|
||||
}
|
||||
});
|
||||
|
||||
test("16 - Clone Stamp Tool", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
|
||||
// Activate clone stamp via S key
|
||||
await page.keyboard.press("s");
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page.locator('[data-tool-active="true"]')).toHaveAttribute(
|
||||
"data-tool",
|
||||
"clone-stamp",
|
||||
);
|
||||
await snap(page, "clone-stamp-active");
|
||||
|
||||
// Verify options bar
|
||||
await expect(page.getByText("Size")).toBeVisible();
|
||||
});
|
||||
|
||||
test("17 - Dodge/Burn/Sponge Tool", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
|
||||
// Activate dodge via O key
|
||||
await page.keyboard.press("o");
|
||||
await page.waitForTimeout(300);
|
||||
await snap(page, "dodge-tool-active");
|
||||
|
||||
// Check options show range and exposure
|
||||
const optionsBar = page
|
||||
.locator("div")
|
||||
.filter({ hasText: /Range|Exposure|Dodge|Burn|Sponge/ })
|
||||
.first();
|
||||
await snap(page, "dodge-burn-options");
|
||||
});
|
||||
|
||||
test("18 - Selection Tools", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
const box = await getCanvasBox(page);
|
||||
|
||||
// Marquee selection via M key
|
||||
await page.keyboard.press("m");
|
||||
await page.waitForTimeout(300);
|
||||
await snap(page, "marquee-tool-active");
|
||||
|
||||
// Draw a selection
|
||||
await page.mouse.move(box.x + 100, box.y + 100);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + 400, box.y + 300, { steps: 10 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(500);
|
||||
await snap(page, "marquee-selection-drawn");
|
||||
|
||||
// Lasso via L key
|
||||
await page.keyboard.press("l");
|
||||
await page.waitForTimeout(300);
|
||||
await snap(page, "lasso-tool-active");
|
||||
|
||||
// Magic wand via W key
|
||||
await page.keyboard.press("w");
|
||||
await page.waitForTimeout(300);
|
||||
await snap(page, "magic-wand-active");
|
||||
});
|
||||
|
||||
test("19 - Gradient & Fill Tools", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
|
||||
// Fill tool via G key
|
||||
await page.keyboard.press("g");
|
||||
await page.waitForTimeout(300);
|
||||
await snap(page, "fill-tool-active");
|
||||
|
||||
// Check tolerance option
|
||||
await expect(page.getByText("Tolerance")).toBeVisible();
|
||||
await snap(page, "fill-options");
|
||||
});
|
||||
|
||||
test("20 - Navigator Panel", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
|
||||
// Navigator should show minimap
|
||||
await page.waitForTimeout(1000);
|
||||
await snap(page, "navigator-minimap");
|
||||
});
|
||||
|
||||
test("21 - Status Bar", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
await page.waitForTimeout(2000);
|
||||
await createNewDocument(page);
|
||||
|
||||
// Verify zoom percentage
|
||||
const zoomInput = page.locator('[data-testid="status-zoom"] input');
|
||||
if (await zoomInput.isVisible().catch(() => false)) {
|
||||
const zoomValue = await zoomInput.inputValue();
|
||||
expect(Number(zoomValue)).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// Verify dimensions
|
||||
const dimensions = page.locator('[data-testid="status-dimensions"]');
|
||||
if (await dimensions.isVisible().catch(() => false)) {
|
||||
const text = await dimensions.textContent();
|
||||
expect(text).toContain("1920");
|
||||
expect(text).toContain("1080");
|
||||
}
|
||||
await snap(page, "status-bar");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user