Add Playwright and Tailwind workflow

This commit is contained in:
rajchales-monito
2026-08-06 17:18:10 +02:00
parent 6374e0502f
commit 86df1bf0b0
9 changed files with 1344 additions and 3 deletions
+39 -2
View File
@@ -23,10 +23,47 @@ Purpose: maintain the MaalFlows live chat widget and admin app.
Before finishing a code change, run: Before finishing a code change, run:
```powershell ```powershell
npm.cmd run check npm.cmd run verify
``` ```
If a broader behavioral change is made, add or run the smallest practical extra verification for that area. `verify` runs syntax checks, Tailwind CSS build, and Playwright UI smoke tests. If the change is documentation-only, `npm.cmd run check` is sufficient, but say that explicitly.
For any meaningful layout or styling change:
- Start or let Playwright start the local server.
- Use Playwright for visual inspection instead of judging only from code.
- Check at least desktop and mobile viewports when the UI surface can appear on both.
- Inspect screenshots for overlap, broken spacing, unreadable text, and inconsistent controls.
- Keep normal Playwright runs headless. Use headed/visible browser only while diagnosing a UI or scraping/browser-automation failure.
- If browser automation is added for external sources, choose the browser engine from source configuration, behave slowly and politely, and do not use Playwright to bypass site protections.
Playwright scripts:
```powershell
npm.cmd run test:ui
npm.cmd run test:ui:headed
```
Playwright screenshots and reports are ignored by Git.
## TailwindCSS
TailwindCSS is the main styling system for new and refactored UI. The source file is `src/styles/tailwind.css`, built to `public/tailwind.css` with:
```powershell
npm.cmd run build:css
```
Rules:
- Prefer reusable Tailwind component classes in `@layer components` over one-off CSS for repeated controls.
- Repeated controls should share the same structure: icon before text, centered text, chevron on the right, consistent icon size/color/alignment, height, padding, border, radius, hover, and focus states.
- Panel top bars should share height, border, background, text color, and button styling.
- If a master style exists, extend it instead of creating a second variant of the same control.
- Dark/light theme changes must apply consistently to sidebars, panels, tables/grids, modals, inputs, and controls.
- Data-heavy UI should stay compact, scannable, responsive, and avoid an Excel-like look unless explicitly required.
- Wide data workspaces should allow horizontal scrolling when content is wider than the viewport.
- Before finishing a UI component change, check similar components for style consistency.
## Git Workflow ## Git Workflow
+1172
View File
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -7,9 +7,19 @@
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"dev": "node --watch server.js", "dev": "node --watch server.js",
"check": "node --check server.js && node --check public/widget.js && node --check public/admin.js" "check": "node --check server.js && node --check public/widget.js && node --check public/admin.js",
"build:css": "tailwindcss -i ./src/styles/tailwind.css -o ./public/tailwind.css --minify",
"build": "npm run build:css",
"test:ui": "playwright test",
"test:ui:headed": "playwright test --headed",
"verify": "npm run check && npm run build && npm run test:ui"
}, },
"dependencies": { "dependencies": {
"better-sqlite3": "^11.10.0" "better-sqlite3": "^11.10.0"
},
"devDependencies": {
"@playwright/test": "^1.62.1",
"@tailwindcss/cli": "^4.3.3",
"tailwindcss": "^4.3.3"
} }
} }
+34
View File
@@ -0,0 +1,34 @@
const { defineConfig, devices } = require("@playwright/test");
module.exports = defineConfig({
testDir: "./tests/ui",
timeout: 30_000,
expect: {
timeout: 5_000
},
fullyParallel: false,
retries: 0,
reporter: [["list"], ["html", { open: "never" }]],
use: {
baseURL: "http://127.0.0.1:3400",
trace: "retain-on-failure",
screenshot: "only-on-failure",
video: "retain-on-failure"
},
projects: [
{
name: "desktop",
use: { ...devices["Desktop Chrome"], viewport: { width: 1366, height: 900 } }
},
{
name: "mobile",
use: { ...devices["Pixel 7"] }
}
],
webServer: {
command: "node server.js",
url: "http://127.0.0.1:3400/health",
reuseExistingServer: true,
timeout: 20_000
}
});
+1
View File
@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>MaalFlows Admin</title> <title>MaalFlows Admin</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml"> <link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/tailwind.css">
<link rel="stylesheet" href="/admin.css"> <link rel="stylesheet" href="/admin.css">
</head> </head>
<body> <body>
+1
View File
@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>MaalFlows Preview</title> <title>MaalFlows Preview</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml"> <link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/tailwind.css">
<style> <style>
body { body {
margin: 0; margin: 0;
File diff suppressed because one or more lines are too long
+23
View File
@@ -0,0 +1,23 @@
@import "tailwindcss";
@theme {
--color-brand: #0f8f6f;
--color-brand-ink: #ffffff;
--color-surface: #ffffff;
--color-ink: #17211d;
--color-muted: #647067;
}
@layer components {
.mf-ui-button {
@apply inline-flex h-10 items-center justify-center gap-2 rounded-md border border-slate-300 bg-white px-3 text-sm font-semibold text-slate-800 shadow-sm transition hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-brand/35;
}
.mf-ui-panel-header {
@apply flex min-h-14 items-center justify-between border-b border-slate-200 bg-white px-4 text-sm font-semibold text-slate-900;
}
.mf-ui-select {
@apply h-10 rounded-md border border-slate-300 bg-white px-3 text-sm text-slate-800 shadow-sm focus:border-brand focus:outline-none focus:ring-2 focus:ring-brand/25;
}
}
+61
View File
@@ -0,0 +1,61 @@
const { expect, test } = require("@playwright/test");
test("preview widget opens and sends a visitor message", async ({ page }, testInfo) => {
await page.goto("/preview");
await expect(page.getByRole("heading", { name: /9b-plus test stranka/i })).toBeVisible();
const widget = page.locator("#maalflows-widget");
await expect(widget).toBeAttached();
const launcher = page.locator("#maalflows-widget .mf-launcher");
await expect(launcher).toBeVisible();
await launcher.click({ force: true });
const panel = page.locator("#maalflows-widget .mf-panel");
await expect(panel).toHaveClass(/open/);
await expect(page.locator("#maalflows-widget textarea[name='message']")).toBeVisible();
await page.screenshot({
path: testInfo.outputPath(`preview-open-${testInfo.project.name}.png`),
fullPage: true
});
const message = `Playwright smoke ${Date.now()}`;
await page.locator("#maalflows-widget textarea[name='message']").fill(message);
await page.locator("#maalflows-widget .mf-send").click();
await expect(page.locator("#maalflows-widget .mf-message.visitor").last()).toContainText(message);
await expect(panel).toBeVisible();
const panelBox = await panel.boundingBox();
expect(panelBox, "widget panel should have a measurable rendered box").toBeTruthy();
expect(panelBox.width).toBeGreaterThan(250);
expect(panelBox.height).toBeGreaterThan(250);
await page.screenshot({
path: testInfo.outputPath(`preview-message-${testInfo.project.name}.png`),
fullPage: true
});
});
test("admin login shell renders without layout breakage", async ({ page }, testInfo) => {
await page.goto("/admin");
const loginForm = page.locator("#loginForm");
await expect(page.locator("#loginView")).toBeVisible();
await expect(page.getByRole("heading", { name: "MaalFlows" })).toBeVisible();
await expect(loginForm.locator("input[name='username']")).toBeVisible();
await expect(loginForm.locator("input[name='password']")).toBeVisible();
await expect(loginForm.getByRole("button", { name: /prihlasit/i })).toBeVisible();
const formBox = await loginForm.boundingBox();
expect(formBox, "login form should have a measurable rendered box").toBeTruthy();
expect(formBox.width).toBeGreaterThan(250);
expect(formBox.height).toBeGreaterThan(180);
await page.screenshot({
path: testInfo.outputPath(`admin-login-${testInfo.project.name}.png`),
fullPage: true
});
});