225 lines
7.2 KiB
TypeScript
225 lines
7.2 KiB
TypeScript
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('<工作台>')
|
||
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,
|
||
})
|
||
})
|
||
})
|