// @ts-nocheck import { test, expect } from "playwright/test"; test("catalog maker shell loads without page errors", async ({ page }) => { const pageErrors = []; page.on("pageerror", (error) => pageErrors.push(error.message)); await page.goto("/"); await expect(page.locator("#settingsButton")).toBeVisible(); await expect(page.locator("#recordId")).toBeVisible(); await expect(page.locator("#variantHeader")).toBeVisible(); await page.screenshot({ path: "test-results/catalog-maker-shell.png", fullPage: true }); expect(pageErrors).toEqual([]); }); test("settings modal opens and shows local database controls", async ({ page }) => { await page.goto("/"); await page.locator("#settingsButton").click(); await expect(page.locator("#settingsModal")).toBeVisible(); await expect(page.locator("#localDbHost")).toBeVisible(); await expect(page.locator("#testLocalDbButton")).toBeVisible(); }); test("manufacturer picker shows ten results and supports search", async ({ page }) => { await page.goto("/"); await page.locator("#manufacturerTrigger").click(); await expect(page.locator("#manufacturerOptions .manufacturer-option")).toHaveCount(10); await expect(page.locator("#manufacturerSearch")).toBeVisible(); await page.locator("#manufacturerSearch").fill("La Sportiva"); await expect(page.locator("#manufacturerOptions .manufacturer-option")).toHaveText(["La Sportiva"]); }); test("mapping uses cache on reload and reloads from DB on manual manufacturer change", async ({ page }) => { let mappingRequests = 0; await page.route("**/api/catalog-maker/mapping-sources**", async (route) => { mappingRequests += 1; await route.fulfill({ contentType: "application/json", body: JSON.stringify({ mapped: true, items: [{ key: "hudy", name: "Hudy", enabled: true }], }), }); }); await page.addInitScript(() => { localStorage.setItem("catalog-maker:selected-manufacturer", "13"); localStorage.setItem( "catalog-maker:mapping-cache", JSON.stringify({ 13: { mapped: true, items: [{ key: "cached-hudy", name: "Cached Hudy", enabled: true }], }, }), ); }); await page.goto("/"); await page.waitForFunction(() => document.querySelector("#manufacturerSelect")?.value === "13"); await expect(page.locator("#mappingStatusText")).toContainText("Mapping OK"); await page.waitForTimeout(300); expect(mappingRequests).toBe(0); await page.locator("#manufacturerSelect").evaluate((select) => { const nextOption = [...select.options].find((option) => option.value && option.value !== "13"); select.value = nextOption?.value || ""; select.dispatchEvent(new Event("change", { bubbles: true })); }); await expect.poll(() => mappingRequests).toBe(1); await page.locator("#manufacturerSelect").evaluate((select) => { select.value = "13"; select.dispatchEvent(new Event("change", { bubbles: true })); }); await expect.poll(() => mappingRequests).toBe(2); }); test("catalog maker remains usable on a narrow viewport", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await page.goto("/"); await expect(page.locator("#sidebarDrawer")).toHaveAttribute("aria-hidden", "true"); await page.locator("#sidebarOpenButton").click(); await expect(page.locator("#findSourcesButton")).toBeVisible(); await expect(page.locator("#settingsButton")).toBeVisible(); await expect(page.locator("#sidebarOverlay")).toBeVisible(); await expect(page.locator(".secondary-panel")).toHaveCSS("order", "1"); await expect(page.locator(".main-workspace")).toHaveCSS("order", "2"); await page.screenshot({ path: "test-results/catalog-maker-mobile.png", fullPage: true }); }); test("left controls open in a responsive sidebar drawer", async ({ page }) => { await page.goto("/"); await expect(page.locator("#sidebarDrawer")).toBeVisible(); await page.locator("#sidebarCloseButton").click(); await expect(page.locator("#sidebarDrawer")).toHaveAttribute("aria-hidden", "true"); await page.locator("#sidebarOpenButton").click(); await expect(page.locator("#sidebarDrawer")).toBeVisible(); await expect(page.locator("#sidebarOverlay")).toBeHidden(); }); test("workspace columns collapse and expand without leaving a gap", async ({ page }) => { await page.goto("/"); await page.locator("#sidebarCloseButton").click(); await expect(page.locator(".catalog-screen")).toHaveClass(/sidebar-collapsed/); const secondaryPanel = await page.locator(".secondary-panel").boundingBox(); expect(secondaryPanel?.x).toBeGreaterThanOrEqual(44); await page.locator("#secondaryPanelToggle").click(); await expect(page.locator(".catalog-screen")).toHaveClass(/secondary-collapsed/); await page.locator("#mainWorkspaceToggle").click(); await expect(page.locator(".catalog-screen")).toHaveClass(/workspace-collapsed/); await page.locator("#secondaryPanelToggle").click(); await page.locator("#mainWorkspaceToggle").click(); await expect(page.locator(".catalog-screen")).not.toHaveClass(/secondary-collapsed/); await expect(page.locator(".catalog-screen")).not.toHaveClass(/workspace-collapsed/); }); test("main workspace keeps a bottom horizontal scrollbar for wide data", async ({ page }) => { await page.setViewportSize({ width: 1061, height: 912 }); await page.goto("/"); const scrollState = await page.locator(".main-workspace .table-wrap").evaluate((element) => { const style = getComputedStyle(element); return { overflowX: style.overflowX, clientWidth: element.clientWidth, scrollWidth: element.scrollWidth, }; }); expect(scrollState.overflowX).toBe("auto"); expect(scrollState.scrollWidth).toBeGreaterThan(scrollState.clientWidth); }); test("picture candidates show source, dimensions, target assignment and save action", async ({ page }) => { await page.route("**/api/manufacturers", async (route) => { await route.fulfill({ contentType: "application/json", body: JSON.stringify({ items: [{ id: 13, name: "La Sportiva" }] }), }); }); await page.route("**/api/languages", async (route) => { await route.fulfill({ contentType: "application/json", body: JSON.stringify({ items: [] }) }); }); await page.route("**/api/catalog-maker/mapping-sources**", async (route) => { await route.fulfill({ contentType: "application/json", body: JSON.stringify({ mapped: true, items: [{ key: "hudy", name: "Hudy", enabled: true, health: "ok" }], }), }); }); await page.route("**/api/catalog-maker/product**", async (route) => { await route.fulfill({ contentType: "application/json", body: JSON.stringify({ product: { idProductCatalog: 135520, idProduct: 110927, idManufacturer: 13, codeManufacture: "LAS", supplierReference: "ZFHS099", name: "Aequilibrium Trek Woman GTX", position: 1, total: 380, }, combinations: [ { idProductCatalog: 110923, status: "SS25/Boty", color: "Carbon/Malibu Blue", combination: "36", ean13: "8058428191284", supplierStock: 1, }, ], }), }); }); await page.route("**/api/catalog-maker/get-pictures**", async (route) => { await route.fulfill({ contentType: "application/json", body: JSON.stringify({ items: [ { source: { key: "hudy", name: "Hudy", type: "supplier" }, searchUrl: "https://www.hudy.cz/vyhledavani?q=8058428191284", candidates: [ { rawTitle: "Aequilibrium Trek Woman GTX", url: "https://www.hudy.cz/product", pictures: [ "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='64'%3E%3Crect width='80' height='64' fill='red'/%3E%3C/svg%3E", "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='90'%3E%3Crect width='120' height='90' fill='blue'/%3E%3C/svg%3E", ], }, ], }, ], }), }); }); let saveRequests = 0; await page.route("**/api/catalog-maker/save-picture", async (route) => { saveRequests += 1; await route.fulfill({ contentType: "application/json", body: JSON.stringify({ saved: true, candidateId: saveRequests, duplicateStatus: "unique", }), }); }); await page.addInitScript(() => { localStorage.setItem("catalog-maker:selected-manufacturer", "13"); }); await page.goto("/"); await page.waitForFunction(() => document.querySelector("#manufacturerSelect")?.value === "13"); await page.locator("#getPicturesButton").click(); await expect(page.locator("#sourceModal")).toBeVisible(); await expect(page.locator(".picture-candidate-title").first()).toHaveText("Hudy picture #1"); await expect(page.locator(".picture-candidate-dimensions").first()).toContainText("Size:"); await expect(page.locator(".picture-candidate-target").first()).toContainText("Target: color Carbon/Malibu Blue"); await expect(page.locator(".picture-candidate-save")).toHaveCount(2); await expect(page.locator("#sourceBatchActionButton")).toBeVisible(); await expect(page.locator("#sourceBatchActionButton")).toContainText("Save all"); await page.locator("#sourceBatchActionButton").click(); await expect(page.locator(".picture-candidate-save")).toHaveText(["Saved", "Saved"]); expect(saveRequests).toBe(2); }); test("source candidates can fetch all pictures before saving candidates", async ({ page }) => { await page.route("**/api/manufacturers", async (route) => { await route.fulfill({ contentType: "application/json", body: JSON.stringify({ items: [{ id: 13, name: "La Sportiva" }] }), }); }); await page.route("**/api/languages", async (route) => { await route.fulfill({ contentType: "application/json", body: JSON.stringify({ items: [] }) }); }); await page.route("**/api/catalog-maker/mapping-sources**", async (route) => { await route.fulfill({ contentType: "application/json", body: JSON.stringify({ mapped: true, items: [{ key: "hudy", name: "Hudy", enabled: true, health: "ok" }], }), }); }); await page.route("**/api/catalog-maker/product**", async (route) => { await route.fulfill({ contentType: "application/json", body: JSON.stringify({ product: { idProductCatalog: 135520, idProduct: 110927, idManufacturer: 13, codeManufacture: "LAS", supplierReference: "ZFHS099", name: "Aequilibrium Trek Woman GTX", position: 1, total: 380, }, combinations: [ { idProductCatalog: 110923, status: "SS25/Boty", color: "Carbon/Malibu Blue", combination: "36", ean13: "8058428191284", supplierStock: 1, }, ], }), }); }); await page.route("**/api/catalog-maker/find-sources**", async (route) => { await route.fulfill({ contentType: "application/json", body: JSON.stringify({ items: [ { status: "found", source: { key: "hudy", name: "Hudy", type: "supplier" }, candidates: [ { title: "Direct Hudy / EAN: 8058428191284 / Color: Carbon/Malibu Blue", url: "https://www.hudy.cz/product", }, ], }, ], }), }); }); await page.route("**/api/catalog-maker/get-pictures**", async (route) => { expect(route.request().url()).not.toContain("sourceKey="); await route.fulfill({ contentType: "application/json", body: JSON.stringify({ items: [ { source: { key: "hudy", name: "Hudy", type: "supplier" }, candidates: [ { rawTitle: "Aequilibrium Trek Woman GTX", url: "https://www.hudy.cz/product", pictures: [ "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='64'%3E%3Crect width='80' height='64' fill='red'/%3E%3C/svg%3E", ], }, ], }, ], }), }); }); await page.addInitScript(() => { localStorage.setItem("catalog-maker:selected-manufacturer", "13"); }); await page.goto("/"); await page.waitForFunction(() => document.querySelector("#manufacturerSelect")?.value === "13"); await page.locator("#findSourcesButton").click(); await expect(page.locator("#sourceModal")).toBeVisible(); await expect(page.locator("#sourceBatchActionButton")).toBeVisible(); await expect(page.locator("#sourceBatchActionButton")).toContainText("Get all pictures"); await page.locator("#sourceBatchActionButton").click(); await expect(page.locator(".picture-candidate-title").first()).toHaveText("Hudy picture #1"); await expect(page.locator("#sourceBatchActionButton")).toContainText("Save all"); }); test("uses one shared height for the top panel headers", async ({ page }) => { await page.goto("/"); const heights = await page.locator("#sidebarDrawer .sidebar-drawer-header, .secondary-panel .column-header, .main-workspace .column-header").evaluateAll((elements) => elements.map((element) => Math.round(element.getBoundingClientRect().height))); expect(new Set(heights).size).toBe(1); }); test("uses one shared visual style for the top panel headers", async ({ page }) => { await page.goto("/"); const styles = await page.locator(".panel-header").evaluateAll((elements) => elements.map((element) => { const style = getComputedStyle(element); return [style.height, style.backgroundColor, style.borderBottomColor, style.fontSize, style.fontWeight, style.paddingLeft, style.paddingRight]; })); expect(new Set(styles.map((style) => JSON.stringify(style))).size).toBe(1); }); test("keeps the top panel headers on one baseline", async ({ page }) => { await page.goto("/"); const positions = await page.locator(".panel-header").evaluateAll((elements) => elements.map((element) => { const rect = element.getBoundingClientRect(); return [rect.top, rect.height]; })); expect(new Set(positions.map((position) => JSON.stringify(position))).size).toBe(1); expect(positions[0][0]).toBe(0); }); test("uses one shared visual style for panel controls", async ({ page }) => { await page.goto("/"); const styles = await page.locator("#sidebarCloseButton, #secondaryPanelToggle, #mainWorkspaceToggle").evaluateAll((elements) => elements.map((element) => { const style = getComputedStyle(element); return [style.width, style.height, style.borderColor, style.backgroundColor, style.borderRadius]; })); expect(new Set(styles.map((style) => JSON.stringify(style))).size).toBe(1); }); test("theme toggle switches and persists dark mode", async ({ page }) => { await page.goto("/"); await page.evaluate(() => localStorage.clear()); await page.reload(); await expect(page.locator("html")).not.toHaveClass(/(?:^| )dark(?: |$)/); const lightPanelBackground = await page.locator("#sidebarDrawer").evaluate((element) => getComputedStyle(element).backgroundColor); await page.locator("#themeToggle").click(); await expect(page.locator("html")).toHaveClass(/(?:^| )dark(?: |$)/); await expect(page.locator("#themeToggle")).toHaveText("Light theme"); const darkPanelBackground = await page.locator("#sidebarDrawer").evaluate((element) => getComputedStyle(element).backgroundColor); expect(darkPanelBackground).not.toBe(lightPanelBackground); await page.locator("#settingsButton").click(); await page.locator("#darkContrastSelect").selectOption("high"); await expect(page.locator("html")).toHaveClass(/contrast-dark-high/); await page.reload(); await expect(page.locator("html")).toHaveClass(/(?:^| )dark(?: |$)/); });