mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-03 12:47:49 +00:00
fix(alerts): align the Started run with the alert-card footer (#2119)
The Alerts overview alert-card footer is an items-center flex row whose first child, the Started timestamp, carried its own mt-1. Under items-center that margin shifted the Started run 2px below the adjacent delivery-status run, which is the small text misalignment the reporter underlined in #2119. Remove the child margin; the footer row already carries the top margin, so the two runs share a baseline. Add a presentation regression test and an offline browser proof that measures the text rects of both runs at desktop and narrow widths. Contract-Neutral: Behavioral footer-alignment fix; no public-contract or subsystem-shape delta. Change-source: pulse-maintainer
This commit is contained in:
parent
eb47a4e653
commit
41fa79d52b
6 changed files with 205 additions and 30 deletions
10
frontend-modern/browser-tests/alert-card-footer.html
Normal file
10
frontend-modern/browser-tests/alert-card-footer.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/browser-tests/alert-card-footer.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
43
frontend-modern/browser-tests/alert-card-footer.tsx
Normal file
43
frontend-modern/browser-tests/alert-card-footer.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Browser fixture for issue #2119: the Alerts overview alert-card footer.
|
||||
//
|
||||
// It reproduces the production footer from AlertOverviewAlertCard.tsx: a flex
|
||||
// row (items-center) whose first child is the "Started" line classed by
|
||||
// getAlertOverviewStartedAtClass(), followed by the delivery-status span. The
|
||||
// reporter's screenshot underlines the Started timestamp and the status text at
|
||||
// different heights, so the fixture exists to measure whether the two runs share
|
||||
// a baseline. Synthetic props only; no backend, API or WebSocket path.
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
import { getAlertOverviewStartedAtClass } from '../src/utils/alertOverviewPresentation';
|
||||
import '../src/index.css';
|
||||
|
||||
function AlertCardFooter() {
|
||||
return (
|
||||
<div
|
||||
id="alert-card"
|
||||
style={{
|
||||
width: '960px',
|
||||
margin: '24px',
|
||||
padding: '16px',
|
||||
border: '1px solid #f59e0b',
|
||||
'border-radius': '8px',
|
||||
background: '#111827',
|
||||
color: '#9ca3af',
|
||||
}}
|
||||
>
|
||||
<p style={{ margin: '0', 'font-size': '14px', color: '#e5e7eb' }}>
|
||||
Alert notifications are not reaching their destinations.
|
||||
</p>
|
||||
<div id="alert-card-footer" class="flex flex-wrap items-center gap-x-3 gap-y-0.5 mt-1">
|
||||
<p class={getAlertOverviewStartedAtClass()} data-testid="started">
|
||||
Started: 9/18/2026, 12:15:58 PM
|
||||
</p>
|
||||
<span class="text-xs text-muted" data-testid="status">
|
||||
Notified 12:15 PM
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(() => <AlertCardFooter />, document.getElementById('root')!);
|
||||
|
|
@ -1,48 +1,38 @@
|
|||
{
|
||||
"version": 1,
|
||||
"base_sha": "fdb2be15befb4fbcd34b4e0399b147bfa098b355",
|
||||
"verified_at": "2026-09-21T05:50:47Z",
|
||||
"base_sha": "eb47a4e653581661d4b267710d9c1bb60a904733",
|
||||
"verified_at": "2026-09-21T14:31:00Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/docs/docMarkdown.ts",
|
||||
"frontend-modern/src/pages/Docs.tsx"
|
||||
"frontend-modern/src/utils/alertOverviewPresentation.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/docs/docMarkdown.ts": "e114a4c2e49a8dc0f2549a704af6ceac54d4eca0cfcae28c8ab8cb6cc1b0718c",
|
||||
"frontend-modern/src/pages/Docs.tsx": "c6c4b066c6af8a4eb8f01a9e03c2f939ee1592867dea2bf0f328bf111f99f2eb"
|
||||
"frontend-modern/src/utils/alertOverviewPresentation.ts": "7f68a70800deb0cb8214d4fd25c7454c5b0fde1084e08365eb93bc361b80846a"
|
||||
},
|
||||
"routes": [
|
||||
"/docs/API",
|
||||
"/docs/API#resource-maintenance-and-operator-state",
|
||||
"/docs/README"
|
||||
"/browser-tests/alert-card-footer.html"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
"width": 2010,
|
||||
"height": 1072
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844
|
||||
"width": 480,
|
||||
"height": 800
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"API reference renders 149 headings, every one with a unique generated id; the maintenance section id is resource-maintenance-and-operator-state",
|
||||
"The in-document link to #resource-maintenance-and-operator-state resolves to the generated heading",
|
||||
"Direct fragment load focuses and scrolls the maintenance heading after the asynchronous Markdown fetch",
|
||||
"Reload on the same fragment restores focus and target",
|
||||
"Malformed (#%invalid) and missing (#does-not-exist) fragments leave the document rendered and do not move focus",
|
||||
"Keyboard activation of the All documentation link routes to /docs/README inside the SPA shell",
|
||||
"Wide API tables scroll inside their own container; neither viewport overflows horizontally"
|
||||
"Alerts overview alert-card footer with the Started timestamp and the adjacent delivery-status run",
|
||||
"The Started run classed by getAlertOverviewStartedAtClass() inside the items-center footer flex row",
|
||||
"Desktop 2010x1072 and narrow 480x800 layouts"
|
||||
],
|
||||
"interactions": [
|
||||
"Mount the production Docs page under the production router and serve the real public/docs/API.md asset from the Vite dev server",
|
||||
"Load /docs/API, /docs/API#resource-maintenance-and-operator-state, a malformed fragment and a missing fragment, and reload the direct fragment",
|
||||
"Assert document.activeElement, tabIndex, scrollIntoView position and window.location.hash",
|
||||
"Focus the first documentation link and activate it with Enter, confirming router navigation without a full reload",
|
||||
"Resize to 390x844 and check page overflow and table scroll wrappers",
|
||||
"Record Chromium 141.0.7390.37 with no uncaught page errors and no failed responses"
|
||||
"Mount the production footer markup from AlertOverviewAlertCard.tsx with the class returned by getAlertOverviewStartedAtClass() and the project CSS",
|
||||
"Measure the text bounding rects of the Started run and the status run and assert their top and bottom edges match within 1px",
|
||||
"Before the fix the same run reported the Started run 2px below the status run (started top 70 / bottom 86 vs status top 68 / bottom 84); after the fix both report top 66 / bottom 82 at both viewports",
|
||||
"Inspect the desktop and narrow screenshots"
|
||||
],
|
||||
"command": "pulse-worker-browser scripts/check-docs-fragment-navigation.cjs",
|
||||
"notes": "Offline Playwright Chromium run of the production Docs page. Synthetic backend: /api/security/status is answered with hasAuthentication=false; the shipped API markdown asset is served by the Vite dev server, not a live Pulse. Heading ids follow GitHub's algorithm (lower-case, punctuation removed, spaces to hyphens, per-document duplicate numbering) so repository fragment links resolve. The renderer keeps the documentation route dependency-free. Chromium 141.0.7390.37 matches the offline runtime. This receipt is content-addressed to the changed frontend sources at this candidate base."
|
||||
"command": "pulse-worker-browser scripts/check-alert-card-footer.cjs",
|
||||
"notes": "Offline Playwright Chromium 141.0.7390.37 run of the production Alerts overview alert-card footer markup with the class returned by getAlertOverviewStartedAtClass() and the project CSS; no backend, API or WebSocket path. The footer is an items-center flex row that already carries the top margin, so the child 'mt-1' pushed the Started run 2px below the delivery-status span; this matches the two runs the reporter underlined at different heights in #2119. Removing the child margin aligns both runs (top 66 / bottom 82 at desktop and narrow, delta 0). No page errors and no failed responses. This receipt is content-addressed to alertOverviewPresentation.ts sha256 7f68a708…."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -324,7 +324,7 @@ describe('alertOverviewPresentation', () => {
|
|||
expect(getAlertOverviewAcknowledgedBadgeClass()).toBe(
|
||||
'px-2 py-0.5 text-xs bg-yellow-200 dark:bg-yellow-800 text-yellow-800 dark:text-yellow-200 rounded',
|
||||
);
|
||||
expect(getAlertOverviewStartedAtClass()).toBe('mt-1 text-xs text-muted');
|
||||
expect(getAlertOverviewStartedAtClass()).toBe('text-xs text-muted');
|
||||
expect(getAlertOverviewPrimaryActionClass(true)).toBe(
|
||||
'px-3 py-1.5 text-xs font-medium border rounded-md transition-all disabled:opacity-50 disabled:cursor-not-allowed text-base-content border-border hover:bg-surface-hover',
|
||||
);
|
||||
|
|
@ -336,6 +336,16 @@ describe('alertOverviewPresentation', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('keeps the Started run on the footer baseline (#2119)', () => {
|
||||
// The alert-card footer is an items-center flex row that already carries the
|
||||
// top margin. A vertical margin on this child shifted the Started run below
|
||||
// the adjacent delivery-status span, which the reporter underlined.
|
||||
const startedClass = getAlertOverviewStartedAtClass();
|
||||
expect(startedClass.split(/\s+/)).not.toContain('mt-1');
|
||||
expect(startedClass).toContain('text-xs');
|
||||
expect(startedClass).toContain('text-muted');
|
||||
});
|
||||
|
||||
it('localizes alert overview helper copy through the active locale', () => {
|
||||
setActiveLocale('es');
|
||||
|
||||
|
|
|
|||
|
|
@ -432,7 +432,9 @@ export function getAlertOverviewAcknowledgedBadgeClass(): string {
|
|||
}
|
||||
|
||||
export function getAlertOverviewStartedAtClass(): string {
|
||||
return 'mt-1 text-xs text-muted';
|
||||
// The footer flex row already carries the top margin; a margin on this child
|
||||
// shifts the Started run below its siblings under items-center (#2119).
|
||||
return 'text-xs text-muted';
|
||||
}
|
||||
|
||||
export function getAlertOverviewPrimaryActionClass(acknowledged: boolean): string {
|
||||
|
|
|
|||
120
scripts/check-alert-card-footer.cjs
Normal file
120
scripts/check-alert-card-footer.cjs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
// Offline real-browser regression for issue #2119 (Alerts overview card footer).
|
||||
//
|
||||
// Mounts the production footer markup with the class returned by
|
||||
// getAlertOverviewStartedAtClass() and measures whether the "Started" run and
|
||||
// the adjacent delivery-status run share a vertical baseline. Run with:
|
||||
// pulse-worker-browser scripts/check-alert-card-footer.cjs
|
||||
const path = require("node:path");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const ROOT = path.resolve(process.cwd(), "frontend-modern");
|
||||
|
||||
const launchOptions = {
|
||||
headless: true,
|
||||
channel: "chromium",
|
||||
args: ["--no-sandbox"],
|
||||
};
|
||||
|
||||
const measure = () =>
|
||||
(() => {
|
||||
const started = document.querySelector('[data-testid="started"]');
|
||||
const status = document.querySelector('[data-testid="status"]');
|
||||
if (!started || !status) return null;
|
||||
const textRect = (el) => {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
const rect = range.getBoundingClientRect();
|
||||
return { top: rect.top, bottom: rect.bottom };
|
||||
};
|
||||
const startedText = textRect(started);
|
||||
const statusText = textRect(status);
|
||||
return {
|
||||
startedClass: started.getAttribute("class"),
|
||||
statusClass: status.getAttribute("class"),
|
||||
startedText,
|
||||
statusText,
|
||||
baselineDelta: Math.abs(startedText.bottom - statusText.bottom),
|
||||
topDelta: Math.abs(startedText.top - statusText.top),
|
||||
};
|
||||
})();
|
||||
|
||||
(async () => {
|
||||
process.chdir(ROOT);
|
||||
const { createServer } = await import(
|
||||
path.join(ROOT, "node_modules", "vite", "dist", "node", "index.js")
|
||||
);
|
||||
const server = await createServer({
|
||||
root: ROOT,
|
||||
configFile: path.join(ROOT, "vite.config.ts"),
|
||||
server: { host: "127.0.0.1", port: 5198, strictPort: true },
|
||||
});
|
||||
let browser;
|
||||
const errors = [];
|
||||
const httpErrors = [];
|
||||
try {
|
||||
await server.listen();
|
||||
browser = await chromium.launch(launchOptions);
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 2010, height: 1072 },
|
||||
});
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
page.on("console", (message) => {
|
||||
if (message.type() !== "error") return;
|
||||
if (message.text().includes("Failed to load resource")) return;
|
||||
errors.push(`console: ${message.text()}`);
|
||||
});
|
||||
page.on("response", (response) => {
|
||||
if (response.status() >= 400)
|
||||
httpErrors.push(`${response.status()} ${response.url()}`);
|
||||
});
|
||||
|
||||
await page.goto(
|
||||
"http://127.0.0.1:5198/browser-tests/alert-card-footer.html",
|
||||
);
|
||||
await page.waitForSelector('[data-testid="started"]');
|
||||
await page.waitForSelector('[data-testid="status"]');
|
||||
|
||||
const desktop = await page.evaluate(measure);
|
||||
if (!desktop) throw new Error("footer runs not rendered");
|
||||
await page.screenshot({
|
||||
path: path.join(ROOT, "browser-tests", "alert-card-footer-desktop.png"),
|
||||
});
|
||||
|
||||
await page.setViewportSize({ width: 480, height: 800 });
|
||||
await page.waitForTimeout(150);
|
||||
const narrow = await page.evaluate(measure);
|
||||
await page.screenshot({
|
||||
path: path.join(ROOT, "browser-tests", "alert-card-footer-narrow.png"),
|
||||
});
|
||||
|
||||
if (errors.length > 0)
|
||||
throw new Error(`page errors: ${errors.join(" | ")}`);
|
||||
|
||||
const worst = Math.max(
|
||||
desktop.baselineDelta,
|
||||
desktop.topDelta,
|
||||
narrow.baselineDelta,
|
||||
narrow.topDelta,
|
||||
);
|
||||
if (worst > 1) {
|
||||
throw new Error(
|
||||
`Started and status runs are vertically misaligned: desktop ${JSON.stringify(desktop)} ` +
|
||||
`narrow ${JSON.stringify(narrow)}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{ result: "passed", desktop, narrow, httpErrors },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
await server.close();
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error("FAILED:", error && error.stack ? error.stack : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue