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

225 lines
7.2 KiB
TypeScript
Raw 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,
filterManufacturingBodies,
groupBodiesIntoBom,
type BodyMeasurement,
} 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,
},
]
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("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('groups matching parts and exports a UTF-8 BOM', () => {
const rows = groupBodiesIntoBom(bodies)
const legRow = rows.find((row) => row.name === 'leg')
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 === 'Leg')?.quantity).toBe(4)
expect(rows.some((row) => row.name === 'unitBox')).toBe(false)
})
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('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 },
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).toContain('120 mm')
expect(html).toContain('配件明细表(BOM)')
expect(html).toContain('由壹点AI')
expect(html).toContain('/api/favicon?surface=hicad')
expect(html).toContain('@page { size: A3 landscape;')
})
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 },
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,
brandCount: document.querySelectorAll('.brand-name').length,
}))
expect(layout.scrollWidth).toBeLessThanOrEqual(layout.clientWidth)
expect(layout.tableCount).toBe(1)
expect(layout.dimensionViewCount).toBe(3)
expect(layout.brandCount).toBe(1)
await page.screenshot({
path: testInfo.outputPath('engineering-sheet.png'),
fullPage: true,
})
})
})