diff --git a/.env.development b/.env.development index fe1a1ae..bbad5b3 100644 --- a/.env.development +++ b/.env.development @@ -6,6 +6,6 @@ NEXT_PUBLIC_USE_MOCK_BACKEND_LOOP_START=false NEXT_PUBLIC_EXPORT_STATIC=false NEXT_PUBLIC_USE_CGI=false # App-Versionsnummer -NEXT_PUBLIC_APP_VERSION=1.6.719 +NEXT_PUBLIC_APP_VERSION=1.6.720 NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter) diff --git a/.env.production b/.env.production index 0c987e8..8665b16 100644 --- a/.env.production +++ b/.env.production @@ -5,5 +5,5 @@ NEXT_PUBLIC_CPL_API_PATH=/CPL NEXT_PUBLIC_EXPORT_STATIC=true NEXT_PUBLIC_USE_CGI=true # App-Versionsnummer -NEXT_PUBLIC_APP_VERSION=1.6.719 +NEXT_PUBLIC_APP_VERSION=1.6.720 NEXT_PUBLIC_CPL_MODE=production \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 129e7f0..43d1e14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [1.6.720] – 2025-08-14 + +- doc: comment in test for analog inputs + +--- ## [1.6.719] – 2025-08-14 - refactor: playwright and tests in one folder diff --git a/package-lock.json b/package-lock.json index da5548f..be52224 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cpl-v4", - "version": "1.6.719", + "version": "1.6.720", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cpl-v4", - "version": "1.6.719", + "version": "1.6.720", "dependencies": { "@fontsource/roboto": "^5.1.0", "@headlessui/react": "^2.2.4", diff --git a/package.json b/package.json index 5fbd937..7b92d86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cpl-v4", - "version": "1.6.719", + "version": "1.6.720", "private": true, "scripts": { "dev": "next dev", @@ -15,6 +15,9 @@ "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:debug": "playwright test --debug", + "test:e2e:chromium": "playwright test --project=chromium", + "test:e2e:firefox": "playwright test --project=firefox", + "test:e2e:webkit": "playwright test --project=webkit", "test:e2e:report": "playwright show-report playwright/report", "test:e2e:clean": "rimraf playwright/report playwright/test-results playwright/.cache blob-report test-results playwright-report", "prepare": "husky install", diff --git a/playwright.config.ts b/playwright.config.ts index 3a372b9..f9452f2 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -5,6 +5,10 @@ import { defineConfig, devices } from "@playwright/test"; */ export default defineConfig({ testDir: "./playwright/tests", + // Increase default timeouts so UI can run visibly with slowMo + timeout: 90_000, + expect: { timeout: 10_000 }, + globalSetup: "./playwright/global-setup", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -23,48 +27,24 @@ export default defineConfig({ use: { /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: "http://localhost:3000", + headless: false, + launchOptions: { slowMo: 300 }, + video: "retain-on-failure", + screenshot: "only-on-failure", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", }, /* Configure projects for major browsers */ - projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - - { - name: "firefox", - use: { ...devices["Desktop Firefox"] }, - }, - - { - name: "webkit", - use: { ...devices["Desktop Safari"] }, - }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, - ], + projects: + process.env.CI || process.env.ALL_BROWSERS + ? [ + { name: "chromium", use: { ...devices["Desktop Chrome"] } }, + { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { name: "webkit", use: { ...devices["Desktop Safari"] } }, + ] + : [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], /* Run your local dev server before starting the tests */ webServer: { diff --git a/playwright/fixtures.ts b/playwright/fixtures.ts new file mode 100644 index 0000000..1716ca3 --- /dev/null +++ b/playwright/fixtures.ts @@ -0,0 +1,14 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import { test as base } from "@playwright/test"; +import { installElementHighlighter } from "./utils/element-highlighter"; +import { installMouseOverlay } from "./utils/mouse-overlay"; + +export const test = base.extend({ + page: async ({ page }, use) => { + await installElementHighlighter(page); + await installMouseOverlay(page); + await use(page); + }, +}); + +export const expect = base.expect; diff --git a/playwright/global-setup.ts b/playwright/global-setup.ts new file mode 100644 index 0000000..284075a --- /dev/null +++ b/playwright/global-setup.ts @@ -0,0 +1,3 @@ +export default async function globalSetup() { + // Placeholder for possible future setup (auth seeding, etc.) +} diff --git a/playwright/tests/analogInputs.spec.ts b/playwright/tests/analogInputs.spec.ts index 9d098a4..fbadf43 100644 --- a/playwright/tests/analogInputs.spec.ts +++ b/playwright/tests/analogInputs.spec.ts @@ -1,6 +1,106 @@ -import { test, expect } from "@playwright/test"; +import { test, expect } from "../fixtures"; +import type { Locator, Page } from "@playwright/test"; + +// Force-highlighting helpers (explicit red outline via class) +async function ensureForceCss(page: Page) { + await page.addStyleTag({ + content: ` + .pw-force-outline { outline: 3px solid #ff1744 !important; outline-offset: 2px !important; box-shadow: 0 0 0 3px rgba(224,0,43,.35) !important; } + `, + }); +} + +async function forceHighlight(page: Page, locator: Locator, durationMs = 800) { + await ensureForceCss(page); + const els = await locator.elementHandles(); + for (const el of els) { + await el.evaluate((node: unknown, ms: number) => { + const n = node as HTMLElement; + n.classList.add("pw-force-outline"); + window.setTimeout(() => n.classList.remove("pw-force-outline"), ms); + }, durationMs); + } +} + +async function forceHighlightRow( + page: Page, + rowLocator: Locator, + durationMs = 800 +) { + await ensureForceCss(page); + const rows = await rowLocator.elementHandles(); + for (const row of rows) { + await row.evaluate((r: unknown, ms: number) => { + const root = r as HTMLElement; + const all = [ + root, + ...Array.from(root.querySelectorAll("*")), + ]; + all.forEach((el) => el.classList.add("pw-force-outline")); + window.setTimeout( + () => all.forEach((el) => el.classList.remove("pw-force-outline")), + ms + ); + }, durationMs); + } +} + +async function installLocalHighlighter(page: import("@playwright/test").Page) { + await page.addInitScript(() => { + type PWWindow = Window & { __pw_local_highlighter__?: boolean }; + const w = window as unknown as PWWindow; + if (w.__pw_local_highlighter__) return; + w.__pw_local_highlighter__ = true; + + const style = document.createElement("style"); + style.innerHTML = ` + .__pw-highlight__ { outline: 3px solid #ff1744 !important; outline-offset: 2px !important; box-shadow: 0 0 0 2px rgba(255,23,68,.25) !important; } + .__pw-highlight-click__ { outline: 4px solid #e0002b !important; outline-offset: 2px !important; box-shadow: 0 0 0 3px rgba(224,0,43,.35) !important; } + .__pw-highlight-row__ * { outline: 2px dashed rgba(255,23,68,.8) !important; outline-offset: 1px !important; } + `; + document.head.appendChild(style); + + let lastEl: Element | null = null; + let lastRow: Element | null = null; + let clickTimeout: number | undefined; + + function mark(el: EventTarget | null) { + if (!(el instanceof Element)) return; + if (lastEl && lastEl !== el) lastEl.classList.remove("__pw-highlight__"); + lastEl = el; + lastEl.classList.add("__pw-highlight__"); + const row = (el.closest && + (el.closest("tr") || el.closest('[role="row"]'))) as Element | null; + if (lastRow && lastRow !== row) + lastRow.classList.remove("__pw-highlight-row__"); + if (row) { + row.classList.add("__pw-highlight-row__"); + lastRow = row; + } + } + + window.addEventListener("mouseover", (e) => mark(e.target), true); + window.addEventListener("focus", (e) => mark(e.target), true); + window.addEventListener( + "click", + (e) => { + mark(e.target); + if (lastEl instanceof Element) { + lastEl.classList.add("__pw-highlight-click__"); + if (clickTimeout) window.clearTimeout(clickTimeout); + const el = lastEl; + clickTimeout = window.setTimeout( + () => el.classList.remove("__pw-highlight-click__"), + 600 + ); + } + }, + true + ); + }); +} /* -Zum ausführen und Aufzeichnen von Tests +import { test, expect } from "../fixtures"; npx playwright codegen http://localhost:3000/analogInputs --target=ts -o tests/e2e/analog-inputs.spec.ts ob ein Element sichtbar ist dann Auge Icon klicken ansonsten nimmt automatich die klicks auf @@ -15,111 +115,205 @@ Zum ausführen und Aufzeichnen von Tests npm run test:e2e:ui -- tests/e2e/analog-inputs.spec.ts */ +test.slow(); test("test", async ({ page }) => { - await page.goto("http://localhost:3000/analogInputs"); + await installLocalHighlighter(page); + await page.goto("/analogInputs"); + await expect( page.getByRole("heading", { name: "Messwerteingänge" }).nth(1) ).toBeVisible(); + await forceHighlight( + page, + page.getByRole("heading", { name: "Messwerteingänge" }).nth(1) + ); + await expect(page.getByRole("cell", { name: "Eingang" })).toBeVisible(); + await forceHighlight(page, page.getByRole("cell", { name: "Eingang" })); + await expect(page.getByRole("cell", { name: "Messwert" })).toBeVisible(); + await forceHighlight(page, page.getByRole("cell", { name: "Messwert" })); + await expect(page.getByRole("cell", { name: "Einheit" })).toBeVisible(); + await forceHighlight(page, page.getByRole("cell", { name: "Einheit" })); + await expect(page.getByRole("cell", { name: "Bezeichnung" })).toBeVisible(); + await forceHighlight(page, page.getByRole("cell", { name: "Bezeichnung" })); + await expect(page.getByRole("cell", { name: "Einstellungen" })).toBeVisible(); + await forceHighlight(page, page.getByRole("cell", { name: "Einstellungen" })); + await expect( page.getByRole("cell", { name: "Messkurve", exact: true }) ).toBeVisible(); + await forceHighlight( + page, + page.getByRole("cell", { name: "Messkurve", exact: true }) + ); + await expect(page.getByText("1", { exact: true })).toBeVisible(); + await expect(page.getByText("2", { exact: true })).toBeVisible(); + await expect(page.getByText("3", { exact: true })).toBeVisible(); + await expect( page.getByRole("cell", { name: "4", exact: true }).locator("path") ).toBeVisible(); + await expect( page.getByRole("cell", { name: "5", exact: true }) ).toBeVisible(); + await expect(page.getByText("6", { exact: true })).toBeVisible(); + await expect( page.getByRole("cell", { name: "7", exact: true }) ).toBeVisible(); + await page.waitForTimeout(3000); + await expect( page.getByRole("cell", { name: "8", exact: true }) ).toBeVisible(); + await page.waitForTimeout(3000); + await expect(page.locator(".border.p-2.text-center").first()).toBeVisible(); + // Markiere die gesamte erste Datenzeile (Row mit "AE 1" falls vorhanden) + const rowAE1 = page.getByRole("row", { name: /\bAE\s*1\b/ }); + await forceHighlightRow(page, rowAE1); + await page.waitForTimeout(3000); + await expect( page .getByRole("row", { name: "2 5.67 °C Temperatur" }) .getByRole("button") .first() ).toBeVisible(); + await page.waitForTimeout(3000); + await expect(page.locator("tr:nth-child(3) > td:nth-child(5)")).toBeVisible(); + await page.waitForTimeout(3000); + await expect( page .getByRole("row", { name: "0.01 V AE 4 Messkurve anzeigen" }) .getByRole("button") .first() ).toBeVisible(); + await page.waitForTimeout(3000); + await expect( page .getByRole("row", { name: "8 -0.00 mA AE 8 Messkurve" }) .getByLabel("Messkurve anzeigen") ).toBeVisible(); + await page.waitForTimeout(3000); + await page.getByRole("cell", { name: "1", exact: true }).click(); + await page.locator(".border.p-2.text-center").first().click(); + await expect( page.getByRole("heading", { name: "Einstellungen Messwerteingang" }) ).toBeVisible(); + await forceHighlight(page, page.getByRole("dialog")); + await expect(page.getByText("Bezeichnung:")).toBeVisible(); + await expect(page.getByText("Offset:")).toBeVisible(); + await expect(page.getByText("Faktor:")).toBeVisible(); + await expect(page.getByText("Einheit:")).toBeVisible(); + await expect(page.getByText("Speicherintervall:")).toBeVisible(); + await expect(page.getByRole("button", { name: "Speichern" })).toBeVisible(); + await page.waitForTimeout(3000); + await expect( page.getByRole("button", { name: "Modal schließen" }) ).toBeVisible(); + await page.waitForTimeout(3000); + await expect( page.getByText( "Einstellungen Messwerteingang 1Bezeichnung:Offset:Faktor:Einheit:" ) ).toBeVisible(); + await page.waitForTimeout(3000); + await page.getByRole("button", { name: "Modal schließen" }).click(); + await page .getByRole("row", { name: "1 126.63 V AE 1 Messkurve" }) .getByLabel("Messkurve anzeigen") .click(); + await expect( page.getByText( "Messkurve Messwerteingang 1Eingang 1VonBisAlle MesswerteDaten laden" ) ).toBeVisible(); + await expect( page.getByRole("heading", { name: "Messkurve Messwerteingang" }) ).toBeVisible(); + await forceHighlight(page, page.getByRole("dialog")); + await forceHighlight(page, page.locator("canvas")); + await expect(page.getByText("Eingang 1VonBisAlle")).toBeVisible(); + await expect(page.getByRole("button", { name: "Daten laden" })).toBeVisible(); + await expect( page.getByRole("button", { name: "Alle Messwerte " }) ).toBeVisible(); + await expect(page.getByText("Von")).toBeVisible(); + await expect(page.getByText("Bis")).toBeVisible(); + await expect(page.locator("div").filter({ hasText: /^Von$/ })).toBeVisible(); + await expect( page.locator("div").filter({ hasText: /^Von$/ }).getByRole("textbox") ).toBeVisible(); + await page.waitForTimeout(3000); + await expect(page.locator("div").filter({ hasText: /^Bis$/ })).toBeVisible(); + await page.waitForTimeout(3000); + await expect( page.locator("div").filter({ hasText: /^Bis$/ }).getByRole("textbox") ).toBeVisible(); + await page.waitForTimeout(3000); + await expect(page.getByRole("img")).toBeVisible(); + await page.waitForTimeout(3000); + await page.getByRole("button", { name: "Alle Messwerte " }).click(); + await page.waitForTimeout(3000); + await page.getByRole("option", { name: "Stündlich" }).click(); + await page.waitForTimeout(3000); + await page.getByRole("button", { name: "Stündlich " }).click(); + await page.waitForTimeout(3000); + await page.getByRole("option", { name: "Täglich" }).click(); + await page.waitForTimeout(3000); + await page.getByRole("button", { name: "Fullscreen" }).click(); + await page.getByRole("button", { name: "Exit fullscreen" }).click(); + await expect(page.getByRole("button", { name: "Fullscreen" })).toBeVisible(); + await expect( page.getByRole("button", { name: "Modal schließen" }) ).toBeVisible(); + await page.waitForTimeout(3000); + await page.getByRole("button", { name: "Modal schließen" }).click(); }); diff --git a/playwright/utils/element-highlighter.ts b/playwright/utils/element-highlighter.ts new file mode 100644 index 0000000..c0905e2 --- /dev/null +++ b/playwright/utils/element-highlighter.ts @@ -0,0 +1,72 @@ +// Adds a highlight to the currently hovered / focused / clicked element +export async function installElementHighlighter( + page: import("@playwright/test").Page +) { + await page.addInitScript(() => { + type PWWindow = Window & { __pw_highlighter_installed__?: boolean }; + const w = window as unknown as PWWindow; + if (w.__pw_highlighter_installed__) return; + w.__pw_highlighter_installed__ = true; + + const style = document.createElement("style"); + style.innerHTML = ` + .__pw-highlight__ { + outline: 3px solid #ff1744 !important; /* kräftiges Rot */ + outline-offset: 2px !important; + box-shadow: 0 0 0 2px rgba(255, 23, 68, 0.25) !important; /* leichte Aura */ + transition: box-shadow 80ms ease-out; + } + .__pw-highlight-click__ { + outline: 4px solid #e0002b !important; /* noch stärkeres Rot beim Klick */ + outline-offset: 2px !important; + box-shadow: 0 0 0 3px rgba(224, 0, 43, 0.35) !important; + } + /* Ganze Tabellenzeile hervorheben */ + .__pw-highlight-row__ * { + outline: 2px dashed rgba(255, 23, 68, 0.8) !important; + outline-offset: 1px !important; + } + `; + document.head.appendChild(style); + + let lastEl: Element | null = null; + let lastRow: Element | null = null; + function mark(el: EventTarget | null) { + if (!(el instanceof Element)) return; + if (lastEl === el) return; + if (lastEl) lastEl.classList.remove("__pw-highlight__"); + lastEl = el; + lastEl.classList.add("__pw-highlight__"); + + // Tabellenzeile (tr oder role="row") komplett markieren + const row = (el.closest && + (el.closest("tr") || el.closest('[role="row"]'))) as Element | null; + if (lastRow && lastRow !== row) + lastRow.classList.remove("__pw-highlight-row__"); + if (row) { + row.classList.add("__pw-highlight-row__"); + lastRow = row; + } + } + + let clickTimeout: number | undefined; + + window.addEventListener("mouseover", (e) => mark(e.target), true); + window.addEventListener("focus", (e) => mark(e.target), true); + window.addEventListener( + "click", + (e) => { + mark(e.target); + if (lastEl instanceof Element) { + lastEl.classList.add("__pw-highlight-click__"); + if (clickTimeout) window.clearTimeout(clickTimeout); + const el = lastEl; + clickTimeout = window.setTimeout(() => { + el.classList.remove("__pw-highlight-click__"); + }, 600); + } + }, + true + ); + }); +} diff --git a/playwright/utils/mouse-overlay.ts b/playwright/utils/mouse-overlay.ts new file mode 100644 index 0000000..43c4ee0 --- /dev/null +++ b/playwright/utils/mouse-overlay.ts @@ -0,0 +1,24 @@ +// Adds a visual mouse cursor overlay so movements are visible in videos/traces +// Source idea: Playwright docs / community gist +export async function installMouseOverlay( + page: import("@playwright/test").Page +) { + await page.addInitScript(() => { + const style = document.createElement("style"); + style.innerHTML = ` + .__pw-mouse__ { pointer-events: none; position: fixed; left: 0; top: 0; z-index: 2147483647; width: 20px; height: 20px; background: rgba(0,0,0,0.7); border-radius: 50%; transform: translate(-50%, -50%); transition: transform 0.02s linear; } + .__pw-mouse__.down { background: rgba(0,0,0,0.95); } + `; + document.head.appendChild(style); + + const dot = document.createElement("div"); + dot.className = "__pw-mouse__"; + document.body.appendChild(dot); + + window.addEventListener("mousemove", (e) => { + dot.style.transform = `translate(${e.clientX}px, ${e.clientY}px)`; + }); + window.addEventListener("mousedown", () => dot.classList.add("down")); + window.addEventListener("mouseup", () => dot.classList.remove("down")); + }); +}