import { expect, test } from '@playwright/test' import fs from 'node:fs' import path from 'node:path' import { buildExplodeOffsets, createBomCsv, createEngineeringSheetHtml, createSnapshotDataUrl, detectPanelHoles, filterManufacturingBodies, getOverallBounds, groupBodiesIntoBom, localizePartName, parseManufacturingLengthInMillimeters, reconcileManufacturingDimensions, type BodyMeasurement, type CircularEdgeSample, type ManufacturingLengthParameter, } from '../../vendor/zoo-modeling-app/manufacturing' const bodies: BodyMeasurement[] = [ { id: 'left-leg', name: 'leg001', center: { x: -50, y: 0, z: 20 }, dimensions: { x: 10, y: 10, z: 80 }, volume: 8_000, surfaceArea: 3_400, }, { id: 'right-leg', name: 'leg002', center: { x: 50, y: 0, z: 20 }, dimensions: { x: 10, y: 10, z: 80 }, volume: 8_000, surfaceArea: 3_400, }, { id: 'top', name: 'tableTop', center: { x: 0, y: 0, z: 65 }, dimensions: { x: 120, y: 60, z: 12 }, volume: 86_400, surfaceArea: 18_720, }, ] function circularEdge( centerX: number, centerY: number, z: number, radius: number ): CircularEdgeSample { return { controlPoints: [ { x: centerX + radius, y: centerY, z }, { x: centerX, y: centerY + radius, z }, { x: centerX - radius, y: centerY, z }, { x: centerX, y: centerY - radius, z }, ], length: Math.PI * radius * 2, } } function hardwareAssemblyFixture() { const holeCenters = [ [-160, -100], [160, -100], [-160, 100], [160, 100], ] as const const panel: BodyMeasurement = { id: 'mounting-panel', name: 'mountingPanel', center: { x: 0, y: 0, z: 9 }, dimensions: { x: 400, y: 300, z: 18 }, holes: detectPanelHoles( { id: 'mounting-panel', name: 'mountingPanel', center: { x: 0, y: 0, z: 9 }, dimensions: { x: 400, y: 300, z: 18 }, }, holeCenters.flatMap(([x, y]) => [ circularEdge(x, y, 0, 4), circularEdge(x, y, 18, 4), ]) ), } const plate: BodyMeasurement = { id: 'connector-plate', name: 'connectorPlate', center: { x: 0, y: 0, z: 19.5 }, dimensions: { x: 360, y: 260, z: 3 }, holes: detectPanelHoles( { id: 'connector-plate', name: 'connectorPlate', center: { x: 0, y: 0, z: 19.5 }, dimensions: { x: 360, y: 260, z: 3 }, }, holeCenters.flatMap(([x, y]) => [ circularEdge(x, y, 18, 4), circularEdge(x, y, 21, 4), ]) ), } const bolts = holeCenters.map( ([x, y], index): BodyMeasurement => ({ id: `connector-bolt-${index + 1}`, name: `connectorBolt${index + 1}`, center: { x, y, z: 11 }, dimensions: { x: 8, y: 8, z: 30 }, }) ) const washers = holeCenters.map( ([x, y], index): BodyMeasurement => ({ id: `washer-${index + 1}`, name: `washer${index + 1}`, center: { x, y, z: 22 }, dimensions: { x: 18, y: 18, z: 2 }, holes: [ { center: { x, y, z: 22 }, diameter: 8, axis: 'z', type: 'through-hole', depth: 2, }, ], }) ) return [panel, plate, ...bolts, ...washers] } test.describe('HiCAD manufacturing extension', () => { test('keeps the extension isolated behind one official registration point', () => { const projectRoot = process.cwd() const vendorScript = fs.readFileSync( path.join(projectRoot, 'scripts/vendor-zoo-modeling-app.mjs'), 'utf8' ) expect(vendorScript).toContain( 'process.env.HICAD_MANUFACTURING_EXTENSION === "1"' ) expect(vendorScript).toContain( 'vendor/zoo-modeling-app/HicadManufacturingLauncher.tsx' .split('/') .at(-1) ) expect(vendorScript).toContain( "import { HicadManufacturingSlot } from '@src/components/HicadManufacturingSlot'" ) expect(vendorScript).toContain( "" ) const slot = fs.readFileSync( path.join( projectRoot, 'vendor/zoo-modeling-app/HicadManufacturingSlot.tsx.template' ), 'utf8' ) expect(slot).toContain('lazy(() =>') expect(slot).toContain('ManufacturingSlotBoundary') const launcher = fs.readFileSync( path.join( projectRoot, 'vendor/zoo-modeling-app/HicadManufacturingLauncher.tsx.template' ), 'utf8' ) expect(launcher).toContain("document.querySelectorAll('canvas')") expect(launcher).toContain('left: Math.round(canvas.rect.left + 16)') expect(launcher).toContain('className="fixed z-20') expect(launcher).toContain("lazy(() =>") expect(launcher).toContain('ManufacturingBoundary') const panel = fs.readFileSync( path.join( projectRoot, 'vendor/zoo-modeling-app/HicadManufacturingPanel.tsx.template' ), 'utf8' ) expect(panel).toContain("type: 'scene_get_entity_ids'") expect(panel).toContain("filter: ['solid3d']") expect(panel).toContain("readBoundingBox([])") expect(panel).toContain('engineFailureMessage(response)') }) test('creates non-zero reversible offsets for every independent body', () => { const offsets = buildExplodeOffsets(bodies) expect(offsets).toHaveLength(3) expect(new Set(offsets.map((offset) => offset.id)).size).toBe(3) expect( offsets.every( ({ offset }) => Math.hypot(offset.x, offset.y, offset.z) > 0 ) ).toBe(true) expect(buildExplodeOffsets([bodies[0]])).toEqual([]) }) test('converts evaluated KCL length units to millimeters', () => { expect(parseManufacturingLengthInMillimeters('120 cm')).toBe(1200) expect(parseManufacturingLengthInMillimeters('1.2m')).toBe(1200) expect(parseManufacturingLengthInMillimeters('47.24409449 in')).toBeCloseTo( 1200, 5 ) expect(parseManufacturingLengthInMillimeters('1200')).toBeUndefined() expect(parseManufacturingLengthInMillimeters('90 deg')).toBeUndefined() }) test('groups matching parts and exports a UTF-8 BOM', () => { const rows = groupBodiesIntoBom(bodies) const legRow = rows.find((row) => row.name === '支腿') expect(rows).toHaveLength(2) expect(legRow?.quantity).toBe(2) expect(legRow?.bodyIds).toEqual(['left-leg', 'right-leg']) const csv = createBomCsv('工作台', rows) expect(csv.startsWith('\uFEFF')).toBe(true) expect(csv).toContain('"工作台"') expect(csv).toContain('"生产工艺"') }) test('filters helper solids and groups directional leg names', () => { const stoolBodies: BodyMeasurement[] = [ { id: 'template', name: 'unitBox', center: { x: 0, y: 0, z: 0 }, dimensions: { x: 1, y: 1, z: 1 }, }, { id: 'seat', name: 'seatBody', center: { x: 0, y: 0, z: 310 }, dimensions: { x: 300, y: 220, z: 20 }, }, ...['frontLeftLeg', 'frontRightLeg', 'backLeftLeg', 'backRightLeg'].map( (name, index): BodyMeasurement => ({ id: `leg-${index}`, name, center: { x: index % 2 ? 100 : -100, y: index < 2 ? -70 : 70, z: 150 }, dimensions: { x: 30, y: 30, z: 300 }, }) ), ] const filtered = filterManufacturingBodies(stoolBodies) const rows = groupBodiesIntoBom(filtered) expect(filtered).toHaveLength(5) expect(rows).toHaveLength(2) expect(rows.find((row) => row.name === '支腿')?.quantity).toBe(4) expect(rows.find((row) => row.name === '座板')?.quantity).toBe(1) expect(rows.some((row) => row.name === 'unitBox')).toBe(false) }) test('reconciles transformed Safari measurements and groups source instances', async ({ page, }, testInfo) => { const transformedBodies: BodyMeasurement[] = [ ...[ ['rearRightStudSource', 202.52], ['rearLeftStudSource', 188.56], ['frontRightStudSource', 165.4], ['frontLeftStud', 178.53], ].map( ([name, volume], index): BodyMeasurement => ({ id: `stud-${index}`, name: String(name), center: { x: index % 2 ? 500 : -500, y: index < 2 ? -200 : 200, z: 760 }, dimensions: { x: 6, y: 6, z: 10 }, volume: Number(volume), }) ), ...[ 'rearRightLegSource', 'rearLeftLegSource', 'frontRightLegSource', 'frontLeftLeg', ].map( (name, index): BodyMeasurement => ({ id: `leg-${index}`, name, center: { x: index % 2 ? 500 : -500, y: index < 2 ? -200 : 200, z: 375 }, dimensions: { x: 60, y: 60, z: 750 }, volume: 1_463_300 + index * 12, }) ), { id: 'tabletop', name: 'tableTop', center: { x: 0, y: 0, z: 762.5 }, dimensions: { x: 1440, y: 720, z: 25.1 }, volume: 17_976_779.67, }, ] const reconciled = reconcileManufacturingDimensions(transformedBodies) const rows = groupBodiesIntoBom(reconciled) expect(rows).toHaveLength(3) expect(rows.find((row) => row.name === '连接螺柱')).toMatchObject({ quantity: 4, dimensions: { x: 5, y: 5, z: 10 }, }) expect(rows.find((row) => row.name === '支腿')).toMatchObject({ quantity: 4, dimensions: { x: 50, y: 50, z: 750 }, }) expect(rows.find((row) => row.name === '台面板')).toMatchObject({ quantity: 1, dimensions: { x: 1200, y: 600, z: 25 }, }) const html = createEngineeringSheetHtml({ projectName: '圆腿桌', generatedAt: '2026-07-28 10:00', overallDimensions: { x: 1200, y: 600, z: 775 }, bodies: reconciled, bomRows: rows, }) expect(html).toContain('data-cross-section="circular"') expect(html).toContain('class="circular-part-profile"') expect(html).toContain('data-profile="circular"') expect(html).toContain('直径 Ø50 mm') expect(html).toContain('Ø50 mm') await page.setViewportSize({ width: 1440, height: 1000 }) await page.setContent(html) await expect(page.locator('.circular-part-profile')).toHaveCount(2) const legProfile = page .locator('.part-detail[data-cross-section="circular"]') .filter({ hasText: '支腿' }) .locator('.circular-part-profile') const profileBounds = await legProfile.boundingBox() expect(profileBounds).not.toBeNull() expect(Math.abs((profileBounds?.width ?? 0) - (profileBounds?.height ?? 0))).toBeLessThan( 1 ) await page.screenshot({ fullPage: true, path: testInfo.outputPath('safari-round-leg-engineering-sheet.png'), }) }) test('reconciles mixed assembly bounds without requiring one shared scale', () => { const mixedBrowserBodies: BodyMeasurement[] = [ ...[ 'rearRightStudSource', 'rearLeftStudSource', 'frontRightStudSource', 'frontLeftStudSource', ].map( (name, index): BodyMeasurement => ({ id: `mixed-stud-${index}`, name, center: { x: index % 2 ? 500 : -500, y: index < 2 ? -200 : 200, z: 760 }, dimensions: { x: 5, y: 5, z: 10 }, volume: 196.35, }) ), ...[ 'rearRightLegSource', 'rearLeftLegSource', 'frontRightLegSource', 'frontLeftLegSource', ].map( (name, index): BodyMeasurement => ({ id: `mixed-leg-${index}`, name, center: { x: index % 2 ? 500 : -500, y: index < 2 ? -200 : 200, z: 375 }, dimensions: { x: 49, y: 49, z: 750 }, volume: 1_463_167.94, }) ), { id: 'mixed-tabletop', name: 'tableTopAssembly', center: { x: 0, y: 0, z: 762.5 }, dimensions: { x: 1440, y: 720, z: 25.2 }, volume: 17_976_787.12, }, ] const reconciled = reconcileManufacturingDimensions(mixedBrowserBodies) const rows = groupBodiesIntoBom(reconciled) expect(rows).toHaveLength(3) expect(rows.find((row) => row.name === '连接螺柱')).toMatchObject({ quantity: 4, dimensions: { x: 5, y: 5, z: 10 }, }) expect(rows.find((row) => row.name === '支腿')).toMatchObject({ quantity: 4, dimensions: { x: 50, y: 50, z: 750 }, }) expect(rows.find((row) => row.name === '台面板')).toMatchObject({ quantity: 1, dimensions: { x: 1200, y: 600, z: 25 }, }) expect( reconciled.find((body) => body.id === 'mixed-tabletop')?.holes ).toMatchObject([ { type: 'blind-hole', diameter: 5, depth: 10 }, { type: 'blind-hole', diameter: 5, depth: 10 }, { type: 'blind-hole', diameter: 5, depth: 10 }, { type: 'blind-hole', diameter: 5, depth: 10 }, ]) expect(rows.some((row) => row.name === 'tableTopAssembly')).toBe(false) }) test('does not reinterpret a solid square leg as a round rod', () => { const [leg] = reconcileManufacturingDimensions([ { id: 'square-leg', name: 'leg', center: { x: 0, y: 0, z: 375 }, dimensions: { x: 50, y: 50, z: 750 }, volume: 1_875_000, }, ]) expect(leg.dimensions).toEqual({ x: 50, y: 50, z: 750 }) expect(leg.dimensionSource).toBe('engine') }) test('uses evaluated KCL parameters when a cold browser has no physical measurements', () => { const coldBrowserBodies: BodyMeasurement[] = [ ...[ 'rearRightStudSource', 'rearLeftStudSource', 'frontRightStudSource', 'frontLeftStud', ].map( (name, index): BodyMeasurement => ({ id: `cold-stud-${index}`, name, center: { x: index % 2 ? 500 : -500, y: index < 2 ? -200 : 200, z: 760 }, dimensions: { x: 6, y: 6, z: 10 }, }) ), ...[ 'rearRightLegSource', 'rearLeftLegSource', 'frontRightLegSource', 'frontLeftLeg', ].map( (name, index): BodyMeasurement => ({ id: `cold-leg-${index}`, name, center: { x: index % 2 ? 500 : -500, y: index < 2 ? -200 : 200, z: 375 }, dimensions: { x: 60, y: 60, z: 750 }, }) ), { id: 'cold-tabletop', name: 'tableTop', center: { x: 0, y: 0, z: 762.5 }, dimensions: { x: 1440, y: 720, z: 25.1 }, }, ] const parameters: ManufacturingLengthParameter[] = [ { name: 'tableLength', valueMm: 1200 }, { name: 'tableWidth', valueMm: 600 }, { name: 'tableTopThickness', valueMm: 25 }, { name: 'legDiameter', valueMm: 50 }, { name: 'legHeight', valueMm: 750 }, { name: 'studDiameter', valueMm: 5 }, { name: 'studHeight', valueMm: 10 }, { name: 'holeDiameter', valueMm: 5 }, { name: 'holeDepth', valueMm: 10 }, { name: 'holeInset', valueMm: 50 }, { name: 'unrelatedCabinetWidth', valueMm: 900 }, ] const reconciled = reconcileManufacturingDimensions( coldBrowserBodies, parameters ) const rows = groupBodiesIntoBom(reconciled) expect(rows.find((row) => row.name === '连接螺柱')).toMatchObject({ quantity: 4, dimensions: { x: 5, y: 5, z: 10 }, }) expect(rows.find((row) => row.name === '支腿')).toMatchObject({ quantity: 4, dimensions: { x: 50, y: 50, z: 750 }, }) expect(rows.find((row) => row.name === '台面板')).toMatchObject({ quantity: 1, dimensions: { x: 1200, y: 600, z: 25 }, }) const tabletop = reconciled.find((body) => body.id === 'cold-tabletop') expect(tabletop?.holes).toHaveLength(4) expect( tabletop?.holes?.every( (hole) => hole.type === 'blind-hole' && hole.diameter === 5 && hole.depth === 10 ) ).toBe(true) expect( tabletop?.holes ?.map((hole) => [ Math.abs(hole.center.x - (tabletop?.center.x ?? 0)), Math.abs(hole.center.y - (tabletop?.center.y ?? 0)), ]) .every(([fromCenterX, fromCenterY]) => fromCenterX === 550 && fromCenterY === 250) ).toBe(true) expect( reconciled.every( (body) => body.dimensionSource === 'kcl-parameters' && body.engineDimensions !== undefined && body.dimensionReferences && body.dimensionReferences.length >= 2 ) ).toBe(true) const html = createEngineeringSheetHtml({ projectName: '冷启动孔位桌', generatedAt: '2026-07-28 12:00', overallDimensions: { x: 1200, y: 600, z: 775 }, bodies: reconciled, bomRows: rows, }) expect(html).toContain('4 × Ø5') expect(html).toContain('Ø5 盲孔,深 10 mm') }) test('keeps a square leg end view rectangular', () => { const squareLeg: BodyMeasurement = { id: 'square-leg', name: 'leg', center: { x: 0, y: 0, z: 375 }, dimensions: { x: 50, y: 50, z: 750 }, volume: 1_875_000, } const html = createEngineeringSheetHtml({ projectName: '方腿桌', generatedAt: '2026-07-28 10:00', overallDimensions: { x: 50, y: 50, z: 750 }, bodies: [squareLeg], bomRows: groupBodiesIntoBom([squareLeg]), }) expect(html).toContain('data-cross-section="rectangular"') expect(html).not.toContain('class="circular-part-profile"') }) test('localizes the hardware validation assembly for a Chinese BOM', () => { expect(localizePartName('seatBody')).toBe('座板') expect(localizePartName('tableBody')).toBe('桌体') expect(localizePartName('mountingPanel')).toBe('安装基板') expect(localizePartName('connectorPlate')).toBe('连接片') expect(localizePartName('connectorBolt4')).toBe('连接螺栓') expect(localizePartName('washer3')).toBe('垫圈') }) test('builds a production-ready hardware BOM and detailed hole schedule', async ({ page, }, testInfo) => { const assembly = hardwareAssemblyFixture() const rows = groupBodiesIntoBom(assembly) expect(assembly).toHaveLength(10) expect(rows).toHaveLength(4) expect(rows.find((row) => row.name === '安装基板')?.holes).toHaveLength(4) expect(rows.find((row) => row.name === '连接片')?.holes).toHaveLength(4) expect(rows.find((row) => row.name === '连接螺栓')?.quantity).toBe(4) expect(rows.find((row) => row.name === '垫圈')?.quantity).toBe(4) const html = createEngineeringSheetHtml({ projectName: '五金连接安装板', generatedAt: '2026-07-27 22:30', overallDimensions: { x: 400, y: 300, z: 30 }, bodies: assembly, bomRows: rows, }) expect(html).toContain('安装基板') expect(html).toContain('连接片') expect(html).toContain('连接螺栓') expect(html).toContain('垫圈') expect(html).toContain('H1:Ø8 通孔') expect(html).toContain('横向中心孔距 320 mm') expect(html).toContain('纵向中心孔距 200 mm') expect(html.match(/class="part-detail"/g)).toHaveLength(4) expect(html).toContain('data-generated-exploded-view="true"') expect(html.match(/class="explode-part"/g)).toHaveLength(10) expect(html).not.toContain('暂无爆炸图快照') expect(html.match(/class="part-front-view"/g)).toHaveLength(4) expect(html.match(/class="part-side-view"/g)).toHaveLength(4) expect(html).toContain('正面图') expect(html).toContain('侧面图') await page.setViewportSize({ width: 1600, height: 900 }) await page.setContent(html) expect( await page.locator('.part-detail').evaluateAll((parts) => parts.map((part) => part.textContent) ) ).toEqual( expect.arrayContaining([ expect.stringContaining('安装基板'), expect.stringContaining('连接片'), expect.stringContaining('连接螺栓'), expect.stringContaining('垫圈'), ]) ) await page.screenshot({ path: testInfo.outputPath('hardware-engineering-sheet.png'), fullPage: true, }) }) test('renders every BOM part without limiting the number of process drawings', () => { const manyParts: BodyMeasurement[] = Array.from( { length: 7 }, (_, index) => ({ id: `custom-part-${index + 1}`, name: `自定义零件${index + 1}`, center: { x: index * 30, y: 0, z: 0 }, dimensions: { x: 20 + index, y: 12 + index, z: 4 + index }, }) ) const rows = groupBodiesIntoBom(manyParts) const html = createEngineeringSheetHtml({ projectName: '多配件生产图', generatedAt: '2026-07-27 23:00', overallDimensions: getOverallBounds(manyParts).dimensions, bodies: manyParts, bomRows: rows, }) expect(rows).toHaveLength(7) expect(html.match(/class="part-detail"/g)).toHaveLength(7) expect(html.match(/class="part-front-view"/g)).toHaveLength(7) expect(html.match(/class="part-side-view"/g)).toHaveLength(7) }) test('normalizes URL-safe engine snapshots for browser image rendering', () => { expect(createSnapshotDataUrl('iVBORw0KGgo_8A')).toBe( 'data:image/png;base64,iVBORw0KGgo/8A==' ) expect( createSnapshotDataUrl('data:image/png;base64,iVBORw0KGgo_8A') ).toBe('data:image/png;base64,iVBORw0KGgo/8A==') }) test('detects through holes on a panel and deduplicates both face edges', () => { const panel: BodyMeasurement = { id: 'side-panel', name: 'sidePanel', center: { x: 0, y: 0, z: 9 }, dimensions: { x: 400, y: 300, z: 18 }, } const holeCenters = [ [-160, -100], [160, -100], [-160, 100], [160, 100], ] const edgeSamples = holeCenters.flatMap(([x, y]) => [ circularEdge(x, y, 0, 4), circularEdge(x, y, 18, 4), ]) const holes = detectPanelHoles(panel, edgeSamples) expect(holes).toHaveLength(4) expect(holes.every((hole) => hole.axis === 'z')).toBe(true) expect(holes.every((hole) => hole.diameter === 8)).toBe(true) expect(holes.every((hole) => hole.depth === 18)).toBe(true) const rows = groupBodiesIntoBom([{ ...panel, holes }]) const html = createEngineeringSheetHtml({ projectName: '五金连接侧板', generatedAt: '2026-07-27 22:00', overallDimensions: panel.dimensions, bodies: [{ ...panel, holes }], bomRows: rows, }) expect(html).toContain('零件加工详图、孔径与孔位尺寸') expect(html).toContain('4 × Ø8') expect(html).toContain('H1:Ø8 通孔') expect(html).toContain('距左 40') expect(html).toContain('距下 50') expect(html).toContain('横向中心孔距 320 mm') expect(html).toContain('纵向中心孔距 200 mm') }) test('builds a printable dimensioned engineering sheet safely', () => { const rows = groupBodiesIntoBom(bodies) const html = createEngineeringSheetHtml({ projectName: '<工作台>', generatedAt: '2026-07-27 20:00', overallDimensions: { x: 120, y: 60, z: 92 }, bodies, brandName: '由壹点AI', brandLogoUrl: '/api/favicon?surface=hicad', snapshotDataUrl: 'data:image/png;base64,abc', bomRows: rows, }) expect(html).toContain('<工作台>') expect(html).not.toContain('<工作台>') expect(html).toContain('正视图') expect(html).toContain('俯视图') expect(html).toContain('右视图') expect(html.match(/class="projection-part"/g)).toHaveLength(9) expect(html).toContain('data-part-name="支腿"') expect(html).toContain('data-part-name="台面板"') expect(html).toContain('120 mm') expect(html).toContain('配件明细表(BOM)') expect(html).toContain('零件加工详图、孔径与孔位尺寸') expect(html.match(/class="part-detail"/g)).toHaveLength(2) expect(html).toContain('由壹点AI') expect(html).toContain('/api/favicon?surface=hicad') expect(html).toContain('@page { size: A3 landscape;') expect(html).toContain( 'grid-template-columns: minmax(180px, 1fr) minmax(0, 2fr) minmax(180px, 1fr)' ) expect(html).toContain('h1 { overflow: hidden; margin: 0; font-size: 18px;') }) test('renders the engineering sheet without horizontal clipping', async ({ page, }, testInfo) => { const rows = groupBodiesIntoBom(bodies) const html = createEngineeringSheetHtml({ projectName: '工作台生产图', generatedAt: '2026-07-27 20:00', overallDimensions: { x: 120, y: 60, z: 92 }, bodies, brandName: '由壹点AI', brandLogoUrl: 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="80" height="80"%3E%3Crect width="80" height="80" fill="%23111827"/%3E%3Cpath d="M20 20h40v40H20z" fill="white"/%3E%3C/svg%3E', bomRows: rows, notes: '材料、表面处理、公差、连接方式和检验标准须在投产前由工程人员复核。', }) await page.setViewportSize({ width: 1600, height: 900 }) await page.setContent(html) const layout = await page.evaluate(() => ({ scrollWidth: document.documentElement.scrollWidth, clientWidth: document.documentElement.clientWidth, tableCount: document.querySelectorAll('table').length, dimensionViewCount: document.querySelectorAll('.dimension-view').length, partDetailCount: document.querySelectorAll('.part-detail').length, projectionPartCount: document.querySelectorAll('.projection-part').length, brandCount: document.querySelectorAll('.brand-name').length, })) expect(layout.scrollWidth).toBeLessThanOrEqual(layout.clientWidth) expect(layout.tableCount).toBe(1) expect(layout.dimensionViewCount).toBe(3) expect(layout.partDetailCount).toBe(rows.length) expect(layout.projectionPartCount).toBe(bodies.length * 3) expect(layout.brandCount).toBe(1) await page.screenshot({ path: testInfo.outputPath('engineering-sheet.png'), fullPage: true, }) }) })