(
project: ProjectDir,
opts: { timeout?: number; contrast?: boolean },
)
| 196 | } |
| 197 | |
| 198 | async function validateInBrowser( |
| 199 | project: ProjectDir, |
| 200 | opts: { timeout?: number; contrast?: boolean }, |
| 201 | ): Promise<{ errors: ConsoleEntry[]; warnings: ConsoleEntry[]; contrast?: ContrastEntry[] }> { |
| 202 | const projectDir = project.dir; |
| 203 | const { bundleToSingleHtml } = await import("@hyperframes/core/compiler"); |
| 204 | const { ensureBrowser } = await import("../browser/manager.js"); |
| 205 | const { serveStaticProjectHtml } = await import("../utils/staticProjectServer.js"); |
| 206 | const { lintProject } = await import("../utils/lintProject.js"); |
| 207 | |
| 208 | // Fail fast on missing/empty/unparsable data-composition-src references |
| 209 | // before spending time bundling and launching a browser. The bundler |
| 210 | // (bundleToSingleHtml → inlineSubCompositions) is intentionally tolerant of |
| 211 | // these — it skips the broken scene and keeps going, silently, with only a |
| 212 | // console.warn — so validate would otherwise report "No console errors" |
| 213 | // for a project that renders a materially broken video. Surface it as a |
| 214 | // real validate failure instead. |
| 215 | const lintResult = await lintProject(projectDir); |
| 216 | const compositionErrors = extractCompositionErrorsFromLint(lintResult); |
| 217 | |
| 218 | // `bundleToSingleHtml` now inlines the runtime IIFE by default, so the |
| 219 | // previous post-bundle regex substitution (which matched `src="..."` on the |
| 220 | // runtime tag) is no longer needed — there's no `src` attribute to match. |
| 221 | const html = await bundleToSingleHtml(projectDir); |
| 222 | |
| 223 | const server = await serveStaticProjectHtml(projectDir, html); |
| 224 | |
| 225 | const errors: ConsoleEntry[] = [...compositionErrors]; |
| 226 | const warnings: ConsoleEntry[] = []; |
| 227 | let contrast: ContrastEntry[] | undefined; |
| 228 | const viewport = resolveCompositionViewportFromHtml(html); |
| 229 | |
| 230 | try { |
| 231 | const browser = await ensureBrowser(); |
| 232 | const puppeteer = await import("puppeteer-core"); |
| 233 | const { buildChromeArgs, analyzeClipMediaFit } = await import("@hyperframes/engine"); |
| 234 | const browserGpuMode = |
| 235 | process.env.PRODUCER_BROWSER_GPU_MODE === "software" ? "software" : "hardware"; |
| 236 | const chromeBrowser = await puppeteer.default.launch({ |
| 237 | headless: true, |
| 238 | executablePath: browser.executablePath, |
| 239 | args: buildChromeArgs({ ...viewport, captureMode: "screenshot" }, { browserGpuMode }), |
| 240 | }); |
| 241 | |
| 242 | const page = await chromeBrowser.newPage(); |
| 243 | await page.setViewport(viewport); |
| 244 | |
| 245 | page.on("console", (msg) => { |
| 246 | const type = msg.type(); |
| 247 | const loc = msg.location(); |
| 248 | const text = msg.text(); |
| 249 | if (type === "error") { |
| 250 | if (text.startsWith("Failed to load resource")) return; |
| 251 | errors.push({ level: "error", text, url: loc.url, line: loc.lineNumber }); |
| 252 | } else if (type === "warn") { |
| 253 | warnings.push({ level: "warning", text, url: loc.url, line: loc.lineNumber }); |
| 254 | } |
| 255 | }); |
no test coverage detected