import 'dotenv/config' import { mkdir } from 'node:fs/promises' import path from 'node:path' import { pathToFileURL } from 'node:url' import type { Browser, BrowserContext, Page } from '@playwright/test' import { SUITE_DEMO_SAMPLE, SUITE_DEMO_SCREENSHOTS, type SuiteDemoApp, type SuiteDemoScreenshot, } from '@/lib/suite-demo/manifest' import { buildPrintLabPrintersFixture, resolveSuiteDemoPaths } from './suite-demo-seed' type Env = Record export type CaptureConfig = { makerworksRoot: string outputDir: string allowSkips: boolean urls: Record, string> } export type CaptureTarget = SuiteDemoScreenshot & { url: string outputPath: string } function cleanBaseUrl(value: string | undefined, fallback: string) { const raw = (value || fallback).trim().replace(/\/+$/, '') return raw || fallback } export function resolveCaptureConfig(options: { makerworksRoot?: string; env?: Env } = {}): CaptureConfig { const env = options.env || process.env const makerworksRoot = path.resolve(options.makerworksRoot || process.cwd()) const paths = resolveSuiteDemoPaths({ makerworksRoot, env }) return { makerworksRoot, outputDir: paths.screenshotDir, allowSkips: (env.SUITE_DEMO_ALLOW_SKIPS || '').trim() === '1', urls: { MakerWorks: cleanBaseUrl(env.SUITE_DEMO_MAKERWORKS_URL, 'http://localhost:3000'), StockWorks: cleanBaseUrl(env.SUITE_DEMO_STOCKWORKS_URL, 'http://localhost:8000'), PrintLab: cleanBaseUrl(env.SUITE_DEMO_PRINTLAB_URL, 'http://localhost:8289'), }, } } export function resolveScreenshotPath(config: CaptureConfig, filename: string) { if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) { throw new Error(`Invalid screenshot filename: ${filename}`) } const output = path.resolve(config.outputDir, filename) const root = path.resolve(config.outputDir) if (output !== root && !output.startsWith(`${root}${path.sep}`)) { throw new Error(`Screenshot output escaped asset directory: ${filename}`) } return output } export function isOptionalCapture(entry: Pick) { return entry.optional === true } export function buildCaptureTargets(config: CaptureConfig): CaptureTarget[] { return SUITE_DEMO_SCREENSHOTS .filter((entry) => entry.app !== 'Suite Flow') .map((entry) => { const baseUrl = config.urls[entry.app as Exclude] return { ...entry, url: new URL(entry.path, `${baseUrl}/`).toString(), outputPath: resolveScreenshotPath(config, entry.filename), } }) } async function appReachable(url: string) { try { const response = await fetch(url, { method: 'GET' }) return response.status < 500 } catch { return false } } async function loginMakerWorks(context: BrowserContext, baseUrl: string, env: Env) { const email = env.SUITE_DEMO_MAKERWORKS_EMAIL || SUITE_DEMO_SAMPLE.adminEmail const password = env.SUITE_DEMO_MAKERWORKS_PASSWORD || 'SuiteDemoPassword123!' await context.request.post(new URL('/api/login', `${baseUrl}/`).toString(), { data: { email, password }, }).catch(() => null) } async function loginStockWorks(page: Page, baseUrl: string, env: Env) { const username = env.STOCKWORKS_ADMIN_USERNAME || env.STOCKWORKS_USERNAME const password = env.STOCKWORKS_ADMIN_PASSWORD || env.STOCKWORKS_PASSWORD if (!username || !password) return await page.goto(new URL('/login', `${baseUrl}/`).toString(), { waitUntil: 'domcontentloaded' }).catch(() => null) const usernameInput = page.locator('input[name="username"], #username').first() if (!(await usernameInput.count())) return await usernameInput.fill(username) await page.locator('input[name="password"], #password').first().fill(password) await page.locator('button[type="submit"], input[type="submit"]').first().click() await page.waitForLoadState('domcontentloaded').catch(() => null) } async function loginPrintLab(page: Page, baseUrl: string, env: Env) { const username = env.PRINTLAB_ADMIN_USERNAME || env.ADMIN_USERNAME const password = env.PRINTLAB_ADMIN_PASSWORD || env.ADMIN_PASSWORD if (!username || !password) return await page.goto(new URL('/login', `${baseUrl}/`).toString(), { waitUntil: 'domcontentloaded' }).catch(() => null) const usernameInput = page.locator('input[name="username"], #username').first() if (!(await usernameInput.count())) return await usernameInput.fill(username) await page.locator('input[name="password"], #password').first().fill(password) await page.locator('button[type="submit"], input[type="submit"]').first().click() await page.waitForLoadState('domcontentloaded').catch(() => null) } async function prepareMakerWorksCart(page: Page) { await page.addInitScript((sample) => { const item = { cartItemId: 'mw-demo-cart-item-1001', modelId: sample.modelId, title: sample.modelTitle, priceUsd: 42, size: { x: 142, y: 88, z: 38 }, colorSlotCount: 2, allowedColors: ['Black', 'Blue', 'White'], options: { qty: 1, scale: 1, material: 'PLA', colors: ['Black', 'Translucent Blue'], toleranceClass: 'standard', finish: 'standard', infillPct: 25, productTemplateId: 'mw-demo-template-enclosure', }, } window.localStorage.setItem('mwv2:cart', JSON.stringify([item])) }, SUITE_DEMO_SAMPLE) } async function dismissTransientOverlays(page: Page) { for (let attempt = 0; attempt < 3; attempt += 1) { const dismissButtons = await page.getByRole('button', { name: /^Dismiss$/ }).all() if (dismissButtons.length === 0) break for (const button of dismissButtons) { await button.click({ timeout: 1_000 }).catch(() => null) } await page.waitForTimeout(150) } } async function preparePageForTarget(page: Page, target: CaptureTarget) { if (target.app === 'MakerWorks') { await prepareMakerWorksCart(page) } } export function buildSyntheticPrintLabHtml(target: CaptureTarget) { const isDetail = target.filename.includes('detail') const isLibrary = target.filename.includes('library') const isRouting = target.filename.includes('preflight') || target.filename.includes('jobs') const printers = buildPrintLabPrintersFixture() const printerRows = printers.map((printer, index) => { const config = printer.config as Record const material = String(config.demo_material || (printer.id === SUITE_DEMO_SAMPLE.printerId ? SUITE_DEMO_SAMPLE.primaryMaterial : 'PLA Basic White')) const queueDepth = Number(config.demo_queue_depth ?? (printer.id === SUITE_DEMO_SAMPLE.printerId ? 1 : index % 4)) const state = String(config.demo_state || (index % 3 === 0 ? 'Ready' : index % 3 === 1 ? 'Queued' : 'Printing')) return { printer, material, queueDepth, state } }) const fleetCards = printerRows.map(({ printer, material, queueDepth, state }) => `

${printer.name}

${state}

Fixture printer profile for routing, queue depth, and loaded filament demos.

Health${state === 'Maintenance due' ? '72' : '100'}
Queue${queueDepth}
Material${material.split(' ')[0]}
`).join('') const routingRows = printerRows.map(({ printer, material, queueDepth, state }, index) => ` ${printer.name} ${material} ${queueDepth} jobs ${index < 3 ? 'Qualified' : state === 'Maintenance due' ? 'Blocked: maintenance' : 'Approval required'} `).join('') return ` ${target.title}

Synthetic local demo view. No live printer controls or real printer private data.

${SUITE_DEMO_SAMPLE.printLabJobId}
${ isLibrary ? `

MakerWorks Library Handoff

${SUITE_DEMO_SAMPLE.modelTitle} is ready for routing with ${SUITE_DEMO_SAMPLE.primaryMaterial} and a queue-supported 3MF asset.

ModelMaterialStatusEstimate
${SUITE_DEMO_SAMPLE.modelTitle}${SUITE_DEMO_SAMPLE.primaryMaterial}Queue ready3h 15m
` : isRouting ? `

Preflight Routing

Printer candidates are ranked for compatibility, loaded filament, health, and queue wait. This is a safe synthetic hold, not a submitted print.

${routingRows}
PrinterFilamentQueueDecision
` : `
${isDetail ? fleetCards : fleetCards}
` }
` } async function finishTargetNavigation(page: Page, target: CaptureTarget) { if (target.tabTarget) { const tabButton = page.locator(`[data-tab-target="${target.tabTarget}"]`).first() if (await tabButton.count()) { await tabButton.click() await page.waitForTimeout(350) } } if (target.waitForText) { await page.getByText(target.waitForText, { exact: false }).first().waitFor({ timeout: 8_000 }).catch(() => null) } } async function captureTarget(page: Page, target: CaptureTarget, config: CaptureConfig) { try { if (target.app === 'PrintLab' && process.env.SUITE_DEMO_PRINTLAB_CAPTURE_LIVE !== '1') { await page.setContent(buildSyntheticPrintLabHtml(target), { waitUntil: 'domcontentloaded' }) await page.screenshot({ path: target.outputPath, fullPage: true }) console.log(`captured ${target.filename}`) return } await preparePageForTarget(page, target) await page.goto(target.url, { waitUntil: 'networkidle', timeout: 25_000 }) await finishTargetNavigation(page, target) await dismissTransientOverlays(page) await page.screenshot({ path: target.outputPath, fullPage: true }) console.log(`captured ${target.filename}`) } catch (error) { if (isOptionalCapture(target) || config.allowSkips) { console.log(`skipped ${target.filename}: ${error instanceof Error ? error.message : String(error)}`) return } throw error } } export async function runSuiteDemoScreenshotCapture(env: Env = process.env) { const { chromium } = await import('@playwright/test') const config = resolveCaptureConfig({ env }) const targets = buildCaptureTargets(config) await mkdir(config.outputDir, { recursive: true }) const requiredApps = new Set( targets .filter((target) => !isOptionalCapture(target)) .map((target) => target.app as Exclude), ) for (const app of requiredApps) { const reachable = await appReachable(config.urls[app]) if (!reachable && !config.allowSkips) { throw new Error(`${app} is not reachable at ${config.urls[app]}. Start local services or set SUITE_DEMO_ALLOW_SKIPS=1.`) } } let browser: Browser | undefined try { browser = await chromium.launch() const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }) await loginMakerWorks(context, config.urls.MakerWorks, env) const page = await context.newPage() await loginStockWorks(page, config.urls.StockWorks, env) await loginPrintLab(page, config.urls.PrintLab, env) for (const target of targets) { await captureTarget(page, target, config) } } finally { await browser?.close() } return { outputDir: config.outputDir, count: targets.length } } async function main() { const result = await runSuiteDemoScreenshotCapture() console.log(`Suite demo screenshots written to ${result.outputDir}`) } const isCli = process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url if (isCli) { main().catch((error) => { console.error(error instanceof Error ? error.message : error) process.exitCode = 1 }) }