/** * lib/qa/measure.ts * * Puppeteer-based slide layout measurement. * Opens the HTML file with a headless Chrome, navigates to each slide, * and records the bounding boxes of all visible elements inside the * slide canvas (1920×1080). * * Returns raw per-slide geometry data consumed by checks.ts. */ import { pathToFileURL } from "url" import { launchChrome } from "../browser/chrome" // ── Constants ──────────────────────────────────────────────────────────────── /** The canonical slide canvas size (matches the design system). */ export const CANVAS_W = 1920 export const CANVAS_H = 1080 // ── Types ──────────────────────────────────────────────────────────────────── export interface Rect { left: number top: number right: number bottom: number width: number height: number } export interface ElementInfo { /** CSS selector path (tag + nth-child chain), for human-readable reports */ selector: string rect: Rect /** true if element is considered "visible" (non-zero size, not hidden) */ visible: boolean /** direct children that are also visible */ children: ElementInfo[] /** all CSS class names on this element */ classList: string[] /** visible text excerpt for text overflow diagnostics */ text?: string /** whether text content is clipped inside this element */ textOverflow?: boolean } export interface SlideContentStats { /** Non-title effective text points: English words + CJK characters. */ bodyTextPoints: number /** Recognizable semantic content units such as boxes, cards, evidence, charts, tables, media, metrics, bullets. */ contentUnits: number /** Evidence/source/caveat-like references visible on the slide. */ supportReferences: number } export interface ScrollbarMetrics { documentHorizontal: boolean documentVertical: boolean bodyHorizontal: boolean bodyVertical: boolean slideHorizontal: boolean slideVertical: boolean } export interface SlideNavigationMetrics { totalSlides: number initialTop: number initialLeft: number position: string visibility: string display: string ariaHidden: string | null bodyOverflowY: string documentOverflowY: string documentScrollHeight: number viewportHeight: number } export interface SlideMetrics { /** 0-based slide index */ index: number /** slide title extracted from the first h1/h2 inside the slide */ title: string /** * Whether this slide is marked as QA-relevant deck metadata. * Read from the `slide-qa` attribute on `
`. * Defaults to `false` when the attribute is absent. * Content-heavy layouts set `slide-qa="true"`; structural/sparse slides omit or use `"false"`. */ slideQa: boolean /** Number of direct .slide-canvas children on the .slide element. */ directSlideCanvasCount: number /** bounding box of the slide-canvas element itself (post-scale) */ canvasRect: Rect /** bounding box of the .slide element itself (post-scale) */ slideRect: Rect /** whether document/body/slide has scrollbars at 1920x1080 */ hasScrollbars: boolean /** detailed scrollbar source signals for document/body/slide */ scrollbars?: ScrollbarMetrics /** deck navigation model signals captured before per-slide scrolling */ navigation?: SlideNavigationMetrics /** top-level visible children of .slide-canvas */ elements: ElementInfo[] /** union bounding box of all visible leaf elements */ contentRect: Rect /** text/content-density signals for content slides */ contentStats: SlideContentStats } /** * Result returned by measureSlides(). * Contains per-slide geometry data and CSS class names defined in