Add Playwright and Tailwind workflow
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
Generated
+1172
File diff suppressed because it is too large
Load Diff
+11
-1
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user