ai-chatbot/tests/routes/hicad-manufacturing-extension.test.ts

779 lines
26 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(
"<HicadManufacturingSlot projectName={projectName ?? 'HiCAD 模型'} />"
)
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('&lt;工作台&gt;')
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,
})
})
})