godcrm/scripts/smoke-adr0060-p5d.mjs
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

154 lines
6.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* ADR-0060 P5d — Playwright UI smoke for the public project page.
*
* Verifies the live render against devcrm.hltrn.cc:
* 1. /s/help/projects/146 loads, no console errors / no 401/403/5xx.
* 2. Sidebar renders + the help project is clickable.
* 3. The seeded table_view widget (4139) is mounted and shows rows.
* 4. No edit affordances (textarea, input-as-cell, "+ Add row", dnd handles)
* are interactable on the public surface.
* 5. Mobile (<768px) viewport: sidebar collapses, content still rendered.
*
* Run with:
* node scripts/smoke-adr0060-p5d.mjs
* Optional env:
* SMOKE_URL=https://devcrm.hltrn.cc (default)
* SMOKE_SLUG=help
* SMOKE_PROJECT_ID=146
*/
import { chromium } from 'playwright';
const BASE = process.env.SMOKE_URL || 'https://devcrm.hltrn.cc';
const SLUG = process.env.SMOKE_SLUG || 'help';
const PROJECT = process.env.SMOKE_PROJECT_ID || '146';
const URL = `${BASE}/s/${SLUG}/projects/${PROJECT}`;
const results = [];
function record(name, ok, detail = '') {
results.push({ name, ok, detail });
// eslint-disable-next-line no-console
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? `${detail}` : ''}`);
}
async function run() {
const browser = await chromium.launch({ headless: true });
// ---------- Desktop pass ----------
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
const page = await ctx.newPage();
const errors = [];
const badStatuses = [];
page.on('console', (msg) => {
if (msg.type() === 'error') errors.push(msg.text());
});
page.on('response', (r) => {
const s = r.status();
const u = r.url();
if (s >= 400 && u.startsWith(BASE)) badStatuses.push(`${s} ${u}`);
});
await page.goto(URL, { waitUntil: 'networkidle', timeout: 30_000 });
// Wait for either a data row OR the explicit "no widgets" empty state to
// settle — useQuery chains projects → dashboard → widget → data, which
// resolves after networkidle.
await page
.locator('text=GOD CRM User Guide, text=В этом проекте')
.first()
.waitFor({ timeout: 15_000 })
.catch(() => {});
await page.waitForTimeout(500);
// The auth-refresh probe is part of every page (not a P5d artefact);
// filter it out so the AC sees a clean signal for "no 4xx/5xx".
const realBadStatuses = badStatuses.filter((s) => !s.includes('/auth/refresh'));
const realErrors = errors.filter((e) => !e.includes('/auth/refresh') && !/401/.test(e));
record('HTML shell loads (desktop)', page.url().includes('/projects/'));
record('No 4xx/5xx on first-paint requests (auth-refresh exempted)', realBadStatuses.length === 0, realBadStatuses.slice(0, 5).join(' | '));
record('No console errors (auth-refresh exempted)', realErrors.length === 0, realErrors.slice(0, 3).join(' | '));
// The page mounts inside PublicLayout which renders the sidebar via
// PublicSpaceSidebar; the project is listed in the tree.
const sidebarMarker = await page.locator('text=Knowledge Base').first().count();
record('Sidebar shows project link', sidebarMarker > 0);
// The seeded widget 4139 is a table_view; its title is "Knowledge Base Documents".
const widgetMarker = await page.locator('text=Knowledge Base Documents').first().count();
record('Seeded table_view widget rendered', widgetMarker > 0);
// A row from the data endpoint should appear (we asserted via curl that 47 rows exist).
const rowMarker = await page.locator('text=GOD CRM User Guide').first().count();
record('At least one data row rendered', rowMarker > 0);
// No edit affordances: there should be NO input/textarea inside the dashboard
// body and no "Add row" / "Add column" buttons. We're permissive — the
// public layout has a search input in its chrome that's allowed.
const editableCells = await page
.locator('[role="grid"] input, [role="grid"] textarea, [data-cell-editable="true"]')
.count();
record('No editable cells in public viewer', editableCells === 0, `count=${editableCells}`);
const addRowBtn = await page.getByRole('button', { name: /\+\s*Add row|Add row|Добавить строку/i }).count();
record('No "Add row" button visible', addRowBtn === 0);
const addColBtn = await page.getByRole('button', { name: /Add column|Добавить колонку/i }).count();
record('No "Add column" button visible', addColBtn === 0);
// Bundle hash advanced (snapshot from current HTML)
const html = await page.content();
const bundleHash = (html.match(/index-([A-Za-z0-9]+)\.js/) || [])[1] || '(not-found)';
record('Bundle hash advanced from index-CQEbzHAo.js', bundleHash !== 'CQEbzHAo', `current=${bundleHash}`);
await page.screenshot({ path: '/tmp/p5d-desktop.png', fullPage: true });
await ctx.close();
// ---------- Mobile pass ----------
const mctx = await browser.newContext({ viewport: { width: 375, height: 667 } });
const mpage = await mctx.newPage();
await mpage.goto(URL, { waitUntil: 'networkidle', timeout: 30_000 });
await mpage
.locator('text=Knowledge Base Documents')
.first()
.waitFor({ timeout: 15_000 })
.catch(() => {});
await mpage.waitForTimeout(500);
const mobileWidget = await mpage.locator('text=Knowledge Base Documents').first().count();
record('[mobile 375px] widget still rendered', mobileWidget > 0);
// Sidebar must collapse on mobile — assert that no full-width sidebar nav is visible
// by checking a typical desktop sidebar landmark is either hidden or off-screen.
// PublicSpaceSidebar has a hamburger toggle on mobile; the project list is not in
// the initial DOM until the user expands it.
const visibleLinks = await mpage.locator('aside a, [data-sidebar] a').count();
record('[mobile 375px] sidebar links not exposed by default', visibleLinks < 10, `count=${visibleLinks}`);
await mpage.screenshot({ path: '/tmp/p5d-mobile.png', fullPage: true });
await mctx.close();
await browser.close();
// ---------- Summary ----------
const total = results.length;
const passed = results.filter((r) => r.ok).length;
// eslint-disable-next-line no-console
console.log(`\n${'='.repeat(60)}`);
console.log(`SUMMARY: ${passed}/${total} passed`);
console.log(`Screenshots: /tmp/p5d-desktop.png, /tmp/p5d-mobile.png`);
if (passed !== total) {
console.log(`FAILED ITEMS:`);
for (const r of results.filter((x) => !x.ok)) {
console.log(` - ${r.name}${r.detail ? ` (${r.detail})` : ''}`);
}
process.exit(1);
}
}
run().catch((err) => {
// eslint-disable-next-line no-console
console.error('SMOKE_ERROR', err);
process.exit(2);
});