mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-25 13:10:03 +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:
@@ -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>
|
||||||
@@ -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,
|
"version": 1,
|
||||||
"base_sha": "fdb2be15befb4fbcd34b4e0399b147bfa098b355",
|
"base_sha": "eb47a4e653581661d4b267710d9c1bb60a904733",
|
||||||
"verified_at": "2026-09-21T05:50:47Z",
|
"verified_at": "2026-09-21T14:31:00Z",
|
||||||
"result": "passed",
|
"result": "passed",
|
||||||
"changed_paths": [
|
"changed_paths": [
|
||||||
"frontend-modern/src/features/docs/docMarkdown.ts",
|
"frontend-modern/src/utils/alertOverviewPresentation.ts"
|
||||||
"frontend-modern/src/pages/Docs.tsx"
|
|
||||||
],
|
],
|
||||||
"content_sha256": {
|
"content_sha256": {
|
||||||
"frontend-modern/src/features/docs/docMarkdown.ts": "e114a4c2e49a8dc0f2549a704af6ceac54d4eca0cfcae28c8ab8cb6cc1b0718c",
|
"frontend-modern/src/utils/alertOverviewPresentation.ts": "7f68a70800deb0cb8214d4fd25c7454c5b0fde1084e08365eb93bc361b80846a"
|
||||||
"frontend-modern/src/pages/Docs.tsx": "c6c4b066c6af8a4eb8f01a9e03c2f939ee1592867dea2bf0f328bf111f99f2eb"
|
|
||||||
},
|
},
|
||||||
"routes": [
|
"routes": [
|
||||||
"/docs/API",
|
"/browser-tests/alert-card-footer.html"
|
||||||
"/docs/API#resource-maintenance-and-operator-state",
|
|
||||||
"/docs/README"
|
|
||||||
],
|
],
|
||||||
"viewports": [
|
"viewports": [
|
||||||
{
|
{
|
||||||
"width": 1440,
|
"width": 2010,
|
||||||
"height": 900
|
"height": 1072
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"width": 390,
|
"width": 480,
|
||||||
"height": 844
|
"height": 800
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"states": [
|
"states": [
|
||||||
"API reference renders 149 headings, every one with a unique generated id; the maintenance section id is resource-maintenance-and-operator-state",
|
"Alerts overview alert-card footer with the Started timestamp and the adjacent delivery-status run",
|
||||||
"The in-document link to #resource-maintenance-and-operator-state resolves to the generated heading",
|
"The Started run classed by getAlertOverviewStartedAtClass() inside the items-center footer flex row",
|
||||||
"Direct fragment load focuses and scrolls the maintenance heading after the asynchronous Markdown fetch",
|
"Desktop 2010x1072 and narrow 480x800 layouts"
|
||||||
"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"
|
|
||||||
],
|
],
|
||||||
"interactions": [
|
"interactions": [
|
||||||
"Mount the production Docs page under the production router and serve the real public/docs/API.md asset from the Vite dev server",
|
"Mount the production footer markup from AlertOverviewAlertCard.tsx with the class returned by getAlertOverviewStartedAtClass() and the project CSS",
|
||||||
"Load /docs/API, /docs/API#resource-maintenance-and-operator-state, a malformed fragment and a missing fragment, and reload the direct fragment",
|
"Measure the text bounding rects of the Started run and the status run and assert their top and bottom edges match within 1px",
|
||||||
"Assert document.activeElement, tabIndex, scrollIntoView position and window.location.hash",
|
"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",
|
||||||
"Focus the first documentation link and activate it with Enter, confirming router navigation without a full reload",
|
"Inspect the desktop and narrow screenshots"
|
||||||
"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"
|
|
||||||
],
|
],
|
||||||
"command": "pulse-worker-browser scripts/check-docs-fragment-navigation.cjs",
|
"command": "pulse-worker-browser scripts/check-alert-card-footer.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."
|
"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(
|
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',
|
'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(
|
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',
|
'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', () => {
|
it('localizes alert overview helper copy through the active locale', () => {
|
||||||
setActiveLocale('es');
|
setActiveLocale('es');
|
||||||
|
|
||||||
|
|||||||
@@ -432,7 +432,9 @@ export function getAlertOverviewAcknowledgedBadgeClass(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getAlertOverviewStartedAtClass(): 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 {
|
export function getAlertOverviewPrimaryActionClass(acknowledged: boolean): string {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user