chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it } from 'node:test'
|
||||
|
||||
import { getAssetIntelligence, getSceneProfile, inferAssetCategory } from '../src/lib/assetIntelligence.js'
|
||||
|
||||
describe('asset intelligence', () => {
|
||||
it('uses a road scene for generated supercars', () => {
|
||||
const intelligence = getAssetIntelligence({
|
||||
name: 'hyper3d supercar test',
|
||||
sourceFileName: 'red-ferrari-supercar.png',
|
||||
})
|
||||
|
||||
assert.equal(intelligence.category.id, 'road')
|
||||
assert.equal(intelligence.scene.id, 'road')
|
||||
assert.match(intelligence.scene.summary, /road deck/i)
|
||||
})
|
||||
|
||||
it('keeps aircraft carriers in the vessel scene even when aircraft appears first', () => {
|
||||
const category = inferAssetCategory({
|
||||
name: 'Chinese aircraft carrier',
|
||||
sourceFileName: 'chinese-aircraft-carrier.png',
|
||||
})
|
||||
|
||||
assert.equal(category.id, 'vessel')
|
||||
assert.equal(getSceneProfile(category.sceneProfile).id, 'vessel')
|
||||
})
|
||||
|
||||
it('maps bronze mask artifacts to museum presentation', () => {
|
||||
const intelligence = getAssetIntelligence({
|
||||
name: '戴金面罩青铜人头像',
|
||||
sourceFileName: 'sanxingdui-bronze-mask.png',
|
||||
})
|
||||
|
||||
assert.equal(intelligence.category.id, 'artifact')
|
||||
assert.equal(intelligence.scene.label, 'Museum Turntable')
|
||||
})
|
||||
|
||||
it('trusts configured vision analysis over ambiguous filenames', () => {
|
||||
const intelligence = getAssetIntelligence({
|
||||
name: 'demo upload',
|
||||
sourceFileName: 'unknown-image.png',
|
||||
intelligence: {
|
||||
configured: true,
|
||||
categoryId: 'artifact',
|
||||
categoryLabel: 'Museum Artifact',
|
||||
description: 'Ancient bronze ritual object.',
|
||||
material: 'Aged bronze and gold foil.',
|
||||
inspectionFocus: 'face relief and patina',
|
||||
presentation: 'Use a dark museum turntable.',
|
||||
tags: ['bronze', 'mask'],
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(intelligence.category.id, 'artifact')
|
||||
assert.equal(intelligence.category.material, 'Aged bronze and gold foil.')
|
||||
assert.equal(intelligence.scene.id, 'artifact')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it } from 'node:test'
|
||||
|
||||
import { getAssetMetadata } from '../src/lib/assetMetadata.js'
|
||||
|
||||
describe('asset metadata inference', () => {
|
||||
it('describes museum artifacts from Chinese artifact keywords', () => {
|
||||
const metadata = getAssetMetadata({
|
||||
id: 'sanxingdui-mask',
|
||||
name: '戴金面罩青铜人头像',
|
||||
sourceFileName: '三星堆-戴金面罩青铜人头像.png',
|
||||
type: 'Uploaded 3D Asset',
|
||||
custom: true,
|
||||
generation: { provider: 'rodin', status: 'success', modelUrl: '/api/3d/local-model/mask.glb' },
|
||||
})
|
||||
|
||||
assert.equal(metadata.subtitle, 'Museum Artifact')
|
||||
assert.match(metadata.description, /museum-style artifact/i)
|
||||
assert.deepEqual(metadata.facts.find(([label]) => label === 'Scene'), ['Scene', 'Museum Turntable'])
|
||||
assert.ok(metadata.tags.includes('artifact'))
|
||||
})
|
||||
|
||||
it('keeps generated supercars on the road presentation path', () => {
|
||||
const metadata = getAssetMetadata({
|
||||
id: 'custom-supercar',
|
||||
name: 'hyper3d supercar test',
|
||||
fullName: 'hyper3d supercar test',
|
||||
sourceFileName: 'red-supercar.png',
|
||||
custom: true,
|
||||
template: 'animal',
|
||||
generation: { provider: 'rodin', status: 'success', modelUrl: '/api/3d/local-model/car.glb' },
|
||||
})
|
||||
|
||||
assert.equal(metadata.subtitle, 'Performance Vehicle')
|
||||
assert.match(metadata.value, /Road push-in/)
|
||||
assert.ok(metadata.tags.includes('low camera'))
|
||||
})
|
||||
|
||||
it('classifies aircraft carriers as vessels instead of aircraft', () => {
|
||||
const metadata = getAssetMetadata({
|
||||
id: 'carrier',
|
||||
name: 'chinese aircraft carrier',
|
||||
sourceFileName: 'chinese-aircraft-carrier.png',
|
||||
custom: true,
|
||||
generation: { provider: 'fal', status: 'success', modelUrl: '/api/3d/local-model/carrier.glb' },
|
||||
})
|
||||
|
||||
assert.equal(metadata.subtitle, 'Naval Vessel')
|
||||
assert.match(metadata.value, /Naval cruise/)
|
||||
})
|
||||
|
||||
it('does not label built-in starter scenes as Hyper3D assets', () => {
|
||||
const metadata = getAssetMetadata({
|
||||
id: 'plant',
|
||||
name: 'Plant Specimen',
|
||||
type: 'Starter Asset',
|
||||
template: 'plant',
|
||||
custom: false,
|
||||
})
|
||||
|
||||
assert.deepEqual(metadata.facts.find(([label]) => label === 'Provider'), ['Provider', 'Built-in'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { formatBytes, formatDuration, formatNumber, getModelQuality } from '../src/lib/modelQuality.js'
|
||||
|
||||
test('model quality scoring', async (t) => {
|
||||
await t.test('keeps built-in starter models in the usable range', () => {
|
||||
const quality = getModelQuality({ id: 'plant', custom: false }, null, [])
|
||||
|
||||
assert.equal(quality.score, 68)
|
||||
assert.equal(quality.verdict, 'Usable')
|
||||
assert.equal(quality.providerLabel, 'Built-in')
|
||||
assert.equal(quality.hasGlb, false)
|
||||
})
|
||||
|
||||
await t.test('rewards generated GLB assets with geometry and textures', () => {
|
||||
const quality = getModelQuality(
|
||||
{
|
||||
id: 'custom-1',
|
||||
custom: true,
|
||||
generation: {
|
||||
provider: 'hyper3d',
|
||||
status: 'success',
|
||||
modelUrl: '/api/3d/local-model/custom-1.glb',
|
||||
},
|
||||
},
|
||||
{
|
||||
fileBytes: 2_400_000,
|
||||
meshCount: 12,
|
||||
textureCount: 5,
|
||||
triangleCount: 72_000,
|
||||
},
|
||||
[{ cellId: 'custom-1', status: 'success', durationMs: 92_000 }],
|
||||
)
|
||||
|
||||
assert.equal(quality.score, 98)
|
||||
assert.equal(quality.verdict, 'Demo-ready')
|
||||
assert.equal(quality.hasGlb, true)
|
||||
assert.equal(quality.fileBytes, 2_400_000)
|
||||
})
|
||||
|
||||
await t.test('keeps failed generations clearly below demo quality', () => {
|
||||
const quality = getModelQuality({
|
||||
id: 'custom-failed',
|
||||
custom: true,
|
||||
generation: {
|
||||
provider: 'tripo',
|
||||
status: 'failed',
|
||||
modelUrl: '',
|
||||
},
|
||||
})
|
||||
|
||||
assert.equal(quality.score, 12)
|
||||
assert.equal(quality.verdict, 'Failed')
|
||||
})
|
||||
})
|
||||
|
||||
test('model quality formatters', () => {
|
||||
assert.equal(formatBytes(0), 'n/a')
|
||||
assert.equal(formatBytes(2_400_000), '2.4 MB')
|
||||
assert.equal(formatDuration(92_000), '1m 32s')
|
||||
assert.equal(formatNumber(72_000), '72,000')
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { inferMotionProfile } from '../src/lib/motionProfiles.js'
|
||||
|
||||
test('infers object-aware demo motion profiles', () => {
|
||||
assert.equal(inferMotionProfile({ name: 'hyper3d supercar test' }).id, 'road')
|
||||
assert.equal(inferMotionProfile({ name: 'hyper3d supercar test', generation: { modelUrl: '/generated-models/aircraft-test.glb' } }).id, 'road')
|
||||
assert.equal(inferMotionProfile({ name: 'advanced fighter jet render' }).id, 'aircraft')
|
||||
assert.equal(inferMotionProfile({ name: 'Chinese aircraft carrier' }).id, 'vessel')
|
||||
assert.equal(inferMotionProfile({ name: 'chinese aircraft car...' }).id, 'vessel')
|
||||
assert.equal(inferMotionProfile({ name: '戴金面罩青铜人头像', sourceFileName: 'sanxingdui-bronze-mask.png' }).id, 'artifact')
|
||||
assert.equal(inferMotionProfile({ name: 'Plant Cell', template: 'plant' }).id, 'specimen')
|
||||
assert.equal(inferMotionProfile({ name: 'Luxury watch model' }).id, 'product')
|
||||
})
|
||||
@@ -0,0 +1,247 @@
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { assertLocalDiagnosticsRequest, parseDataUrl, sanitizeFileName } from '../server/http-utils.mjs'
|
||||
import { getModelExtension, shouldAttachTripoAuth, validateModelBuffer } from '../server/model-store.mjs'
|
||||
import { findFirstValue, findModelUrl, isSuccessStatus } from '../server/object-utils.mjs'
|
||||
import { buildFalInput, decodeFalTaskId, encodeFalTaskId, findFalModelFile, normalizeFalModelId, normalizeFalStatus } from '../server/providers/fal.mjs'
|
||||
import { decodeRodinTaskId, encodeRodinTaskId, findRodinDownloadItem, normalizeRodinStatus } from '../server/providers/rodin.mjs'
|
||||
import { extractJsonObject, normalizeVisionInsight } from '../server/providers/vision.mjs'
|
||||
import { compactCustomCellsForStorage, persistCustomCells } from '../src/domain/cellPersistence.js'
|
||||
|
||||
describe('server utility functions', () => {
|
||||
it('sanitizes uploaded filenames without losing readable words', () => {
|
||||
assert.equal(sanitizeFileName('../plant cell ✨.png'), 'plant cell .png')
|
||||
assert.equal(sanitizeFileName(''), 'asset-reference.png')
|
||||
})
|
||||
|
||||
it('parses supported image data URLs and rejects tiny payloads', () => {
|
||||
const dataUrl = `data:image/png;base64,${Buffer.alloc(1024).toString('base64')}`
|
||||
const image = parseDataUrl(dataUrl)
|
||||
|
||||
assert.equal(image.mime, 'image/png')
|
||||
assert.equal(image.ext, 'png')
|
||||
assert.equal(image.buffer.length, 1024)
|
||||
assert.throws(() => parseDataUrl('data:text/plain;base64,abc'), /Only PNG, JPEG, or WebP/)
|
||||
assert.throws(() => parseDataUrl(`data:image/png;base64,${Buffer.alloc(8).toString('base64')}`), /too small/)
|
||||
})
|
||||
|
||||
it('restricts diagnostics logs to local callers and localhost pages', () => {
|
||||
assert.doesNotThrow(() => assertLocalDiagnosticsRequest({
|
||||
socket: { remoteAddress: '127.0.0.1' },
|
||||
headers: { origin: 'http://127.0.0.1:5174' },
|
||||
}))
|
||||
assert.doesNotThrow(() => assertLocalDiagnosticsRequest({
|
||||
socket: { remoteAddress: '::ffff:127.0.0.1' },
|
||||
headers: { referer: 'http://localhost:5174/logs' },
|
||||
}))
|
||||
|
||||
assert.throws(
|
||||
() => assertLocalDiagnosticsRequest({
|
||||
socket: { remoteAddress: '192.168.1.8' },
|
||||
headers: {},
|
||||
}),
|
||||
/only available from this machine/,
|
||||
)
|
||||
assert.throws(
|
||||
() => assertLocalDiagnosticsRequest({
|
||||
socket: { remoteAddress: '127.0.0.1' },
|
||||
headers: { origin: 'https://example.com' },
|
||||
}),
|
||||
/only available to localhost pages/,
|
||||
)
|
||||
})
|
||||
|
||||
it('detects model extensions and validates GLB headers', () => {
|
||||
assert.equal(getModelExtension('https://example.com/model.glb?download=1'), 'glb')
|
||||
assert.equal(getModelExtension('scene.gltf'), 'gltf')
|
||||
assert.throws(() => getModelExtension('model.obj'), /Only GLB/)
|
||||
|
||||
assert.doesNotThrow(() => validateModelBuffer(Buffer.concat([Buffer.from('glTF'), Buffer.alloc(28)]), 'glb'))
|
||||
assert.throws(() => validateModelBuffer(Buffer.concat([Buffer.from('nope'), Buffer.alloc(28)]), 'glb'), /GLB files/)
|
||||
})
|
||||
|
||||
it('does not attach Tripo auth to arbitrary model URLs', () => {
|
||||
assert.equal(shouldAttachTripoAuth('https://example.com/model.glb'), false)
|
||||
assert.equal(shouldAttachTripoAuth('http://127.0.0.1:8787/model.glb'), false)
|
||||
})
|
||||
|
||||
it('finds nested task ids and preferred model URLs', () => {
|
||||
const payload = {
|
||||
data: {
|
||||
task: { task_id: 'task-123' },
|
||||
assets: [
|
||||
{ url: 'https://example.com/preview.png' },
|
||||
{ result: 'https://example.com/model.obj' },
|
||||
{ result: 'https://example.com/model.glb?x=1' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
assert.equal(findFirstValue(payload, ['task_id']), 'task-123')
|
||||
assert.equal(findModelUrl(payload), 'https://example.com/model.glb?x=1')
|
||||
assert.equal(findModelUrl({ result: 'https://example.com/model.obj' }), '')
|
||||
assert.equal(isSuccessStatus('finished'), true)
|
||||
assert.equal(isSuccessStatus('running'), false)
|
||||
})
|
||||
|
||||
it('normalizes model vision output for asset intelligence', () => {
|
||||
const parsed = extractJsonObject('```json\n{"objectName":"Red Supercar","categoryId":"car","confidence":92,"tags":["Vehicle","Gloss"]}\n```')
|
||||
const insight = normalizeVisionInsight(parsed, { provider: 'openai', model: 'test-model' })
|
||||
|
||||
assert.equal(insight.objectName, 'Red Supercar')
|
||||
assert.equal(insight.categoryId, 'road')
|
||||
assert.equal(insight.categoryLabel, 'Performance Vehicle')
|
||||
assert.equal(insight.confidence, 0.92)
|
||||
assert.deepEqual(insight.tags, ['vehicle', 'gloss'])
|
||||
})
|
||||
|
||||
it('normalizes Rodin task ids, statuses, and downloads', () => {
|
||||
const task = { taskUuid: 'task-uuid-1', subscriptionKey: 'subscription-key-1' }
|
||||
const encoded = encodeRodinTaskId(task)
|
||||
|
||||
assert.deepEqual(decodeRodinTaskId(encoded), task)
|
||||
assert.deepEqual(decodeRodinTaskId('legacy-task-id'), { taskUuid: 'legacy-task-id', subscriptionKey: 'legacy-task-id' })
|
||||
assert.equal(normalizeRodinStatus(['Done', 'Done']), 'success')
|
||||
assert.equal(normalizeRodinStatus(['Waiting']), 'queued')
|
||||
assert.equal(normalizeRodinStatus(['Generating']), 'running')
|
||||
assert.equal(normalizeRodinStatus(['Done', 'Failed']), 'failed')
|
||||
assert.deepEqual(
|
||||
findRodinDownloadItem({
|
||||
list: [
|
||||
{ name: 'preview.webp', url: 'https://example.com/preview.webp' },
|
||||
{ name: 'model.glb', url: 'https://cdn.example.com/signed-download' },
|
||||
],
|
||||
}),
|
||||
{ name: 'model.glb', url: 'https://cdn.example.com/signed-download' },
|
||||
)
|
||||
})
|
||||
|
||||
it('normalizes Fal model ids, task ids, inputs, statuses, and model files', () => {
|
||||
const task = { modelId: 'tripo3d/tripo/v2.5/image-to-3d', requestId: 'fal-request-1' }
|
||||
const encoded = encodeFalTaskId(task)
|
||||
|
||||
assert.deepEqual(decodeFalTaskId(encoded), task)
|
||||
assert.equal(normalizeFalModelId('tripo3d/tripo/v2.5/image-to-3d'), 'tripo3d/tripo/v2.5/image-to-3d')
|
||||
assert.equal(normalizeFalModelId('fal-ai/tripo3d/v2.5/image-to-3d'), 'fal-ai/hunyuan3d/v2')
|
||||
assert.equal(normalizeFalStatus('IN_QUEUE'), 'queued')
|
||||
assert.equal(normalizeFalStatus('IN_PROGRESS'), 'running')
|
||||
assert.equal(normalizeFalStatus('COMPLETED'), 'success')
|
||||
assert.equal(normalizeFalStatus('ERROR'), 'failed')
|
||||
|
||||
assert.deepEqual(
|
||||
buildFalInput('fal-ai/hunyuan3d/v2', 'https://cdn.example.com/input.png', { seed: 12 }),
|
||||
{ input_image_url: 'https://cdn.example.com/input.png', seed: 12 },
|
||||
)
|
||||
assert.deepEqual(
|
||||
buildFalInput('fal-ai/hyper3d/rodin', 'https://cdn.example.com/input.png', { prompt: 'a detailed cell', seed: 7 }),
|
||||
{
|
||||
geometry_file_format: 'glb',
|
||||
input_image_urls: ['https://cdn.example.com/input.png'],
|
||||
material: 'PBR',
|
||||
prompt: 'a detailed cell',
|
||||
quality: 'medium',
|
||||
seed: 7,
|
||||
tier: 'Regular',
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
findFalModelFile({
|
||||
base_model: { url: 'https://cdn.example.com/base.glb' },
|
||||
pbr_model: { url: 'https://cdn.example.com/file', content_type: 'model/gltf-binary' },
|
||||
}),
|
||||
{ url: 'https://cdn.example.com/file', ext: 'glb' },
|
||||
)
|
||||
})
|
||||
|
||||
it('compacts generated custom cells without dropping pending retry images first', () => {
|
||||
const generated = {
|
||||
id: 'custom-ready',
|
||||
imageUrl: 'data:image/webp;base64,large',
|
||||
thumbnailUrl: 'data:image/webp;base64,thumb',
|
||||
generation: { status: 'success', modelUrl: '/api/3d/local-model/task.glb', rawModelUrl: 'https://signed.example.com/model.glb', message: 'ready' },
|
||||
}
|
||||
const pending = {
|
||||
id: 'custom-pending',
|
||||
imageUrl: 'data:image/webp;base64,source',
|
||||
generation: { status: 'failed', modelUrl: '', rawModelUrl: '', message: 'retry possible' },
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
compactCustomCellsForStorage([generated, pending], 'generated-previews').map((cell) => cell.imageUrl),
|
||||
['', pending.imageUrl],
|
||||
)
|
||||
assert.equal(compactCustomCellsForStorage([generated, pending], 'generated-previews')[0].thumbnailUrl, generated.thumbnailUrl)
|
||||
assert.deepEqual(
|
||||
compactCustomCellsForStorage([generated, pending], 'minimal').map((cell) => ({
|
||||
imageUrl: cell.imageUrl,
|
||||
thumbnailUrl: cell.thumbnailUrl,
|
||||
rawModelUrl: cell.generation.rawModelUrl,
|
||||
})),
|
||||
[
|
||||
{ imageUrl: '', thumbnailUrl: generated.thumbnailUrl, rawModelUrl: '' },
|
||||
{ imageUrl: '', thumbnailUrl: undefined, rawModelUrl: '' },
|
||||
],
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to compact custom-cell storage when localStorage quota fails', () => {
|
||||
const writes = []
|
||||
global.window = {
|
||||
localStorage: {
|
||||
setItem(key, value) {
|
||||
writes.push({ key, value: JSON.parse(value) })
|
||||
if (writes.length === 1) throw new Error('quota exceeded')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
const result = persistCustomCells([
|
||||
{
|
||||
id: 'custom-ready',
|
||||
imageUrl: 'data:image/webp;base64,large',
|
||||
thumbnailUrl: 'data:image/webp;base64,thumb',
|
||||
generation: { status: 'success', modelUrl: '/api/3d/local-model/task.glb', rawModelUrl: 'https://signed.example.com/model.glb', message: 'ready' },
|
||||
},
|
||||
])
|
||||
|
||||
assert.equal(result.stored, true)
|
||||
assert.equal(result.compacted, true)
|
||||
assert.equal(result.cells[0].imageUrl, '')
|
||||
assert.equal(result.cells[0].thumbnailUrl, 'data:image/webp;base64,thumb')
|
||||
assert.equal(result.cells[0].generation.modelUrl, '/api/3d/local-model/task.glb')
|
||||
assert.equal(writes.length, 2)
|
||||
} finally {
|
||||
delete global.window
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps compacted custom-cell array identity when storage remains unavailable', () => {
|
||||
global.window = {
|
||||
localStorage: {
|
||||
setItem() {
|
||||
throw new Error('storage unavailable')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
const compacted = [
|
||||
{
|
||||
id: 'custom-ready',
|
||||
imageUrl: '',
|
||||
previewDropped: true,
|
||||
generation: { status: 'success', modelUrl: '/api/3d/local-model/task.glb', rawModelUrl: '', message: 'ready' },
|
||||
},
|
||||
]
|
||||
const result = persistCustomCells(compacted)
|
||||
|
||||
assert.equal(result.stored, false)
|
||||
assert.equal(result.compacted, true)
|
||||
assert.equal(result.cells, compacted)
|
||||
} finally {
|
||||
delete global.window
|
||||
}
|
||||
})
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
@@ -0,0 +1,133 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
async function prepareWorkbench(page) {
|
||||
await page.addStyleTag({
|
||||
content: `
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0s !important;
|
||||
animation-delay: 0s !important;
|
||||
transition-duration: 0s !important;
|
||||
caret-color: transparent !important;
|
||||
}
|
||||
`,
|
||||
})
|
||||
await page.waitForSelector('.studio-window')
|
||||
await page.locator('.status-toast').evaluate((node) => {
|
||||
node.style.display = 'none'
|
||||
}).catch(() => {})
|
||||
await page.waitForTimeout(450)
|
||||
}
|
||||
|
||||
async function expectSeparated(page, leftSelector, centerSelector, rightSelector) {
|
||||
const left = await page.locator(leftSelector).boundingBox()
|
||||
const center = await page.locator(centerSelector).boundingBox()
|
||||
const right = await page.locator(rightSelector).boundingBox()
|
||||
|
||||
expect(left).toBeTruthy()
|
||||
expect(center).toBeTruthy()
|
||||
expect(right).toBeTruthy()
|
||||
expect(left.x + left.width).toBeLessThanOrEqual(center.x)
|
||||
expect(center.x + center.width).toBeLessThanOrEqual(right.x)
|
||||
}
|
||||
|
||||
async function expectClippedScreenshot(page, selector, name, options = {}) {
|
||||
const box = await page.locator(selector).boundingBox()
|
||||
expect(box).toBeTruthy()
|
||||
|
||||
const image = await page.screenshot({
|
||||
animations: 'disabled',
|
||||
clip: {
|
||||
x: Math.floor(box.x),
|
||||
y: Math.floor(box.y),
|
||||
width: Math.ceil(box.width),
|
||||
height: Math.ceil(box.height),
|
||||
},
|
||||
mask: options.mask || [],
|
||||
})
|
||||
|
||||
expect(image).toMatchSnapshot(name, {
|
||||
maxDiffPixelRatio: 0.025,
|
||||
threshold: 0.18,
|
||||
})
|
||||
}
|
||||
|
||||
test('workbench layout keeps library, stage, and source rail separated', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await prepareWorkbench(page)
|
||||
|
||||
await expectSeparated(page, '.selection-shelf', '.stage-zone', '.command-zone')
|
||||
await expectClippedScreenshot(page, '.studio-window', 'workbench-layout.png', {
|
||||
mask: [page.locator('.cell-viewer canvas')],
|
||||
})
|
||||
})
|
||||
|
||||
test('model library drawer renders productized asset cards', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await prepareWorkbench(page)
|
||||
|
||||
await page.getByRole('button', { name: 'Library' }).click()
|
||||
await expect(page.locator('.drawer-library')).toBeVisible()
|
||||
await expect(page.locator('.asset-library-card').first()).toBeVisible()
|
||||
await expect(page.locator('.drawer-library')).toContainText('Generated & Imported Assets')
|
||||
await expect(page.locator('.drawer-library')).not.toContainText('Organelle')
|
||||
|
||||
await expectClippedScreenshot(page, '.drawer-library', 'asset-library-drawer.png', {
|
||||
mask: [page.locator('.asset-preview-frame img')],
|
||||
})
|
||||
})
|
||||
|
||||
test('demo mode uses a clean presentation surface', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await prepareWorkbench(page)
|
||||
|
||||
await page.getByRole('button', { name: 'Demo' }).click()
|
||||
await expect(page.locator('.workbench-v2.demo-mode')).toBeVisible()
|
||||
await expect(page.locator('.demo-exit-button')).toBeVisible()
|
||||
await expect(page.locator('.selection-shelf')).toBeHidden()
|
||||
await expect(page.locator('.command-zone')).toBeHidden()
|
||||
await page.addStyleTag({
|
||||
content: '.workbench-v2.demo-mode .cell-viewer canvas { opacity: 0 !important; }',
|
||||
})
|
||||
|
||||
await expectClippedScreenshot(page, '.studio-window', 'demo-mode.png')
|
||||
})
|
||||
|
||||
test('view mode controls change the live viewer state', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await prepareWorkbench(page)
|
||||
|
||||
const solidButton = page.getByRole('button', { name: 'Solid view' })
|
||||
const xrayButton = page.getByRole('button', { name: 'X-Ray layer view' })
|
||||
const inspectButton = page.getByRole('button', { name: 'Inspect focus view' })
|
||||
|
||||
await expect(solidButton).toBeVisible()
|
||||
await expect(xrayButton).toBeVisible()
|
||||
await expect(inspectButton).toBeVisible()
|
||||
|
||||
await solidButton.click()
|
||||
await expect(page.locator('.cell-viewer.solid')).toBeVisible()
|
||||
await expect(page.locator('.stage-status')).toContainText('Solid')
|
||||
|
||||
await xrayButton.click()
|
||||
await expect(page.locator('.cell-viewer.layers')).toBeVisible()
|
||||
await expect(page.locator('.stage-status')).toContainText('X-Ray')
|
||||
|
||||
await inspectButton.click()
|
||||
await expect(page.locator('.cell-viewer.focus')).toBeVisible()
|
||||
await expect(page.locator('.stage-status')).toContainText('Inspect')
|
||||
})
|
||||
|
||||
test('inspector explains the selected object instead of generic biology parts', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await prepareWorkbench(page)
|
||||
|
||||
await page.getByRole('button', { name: 'Info' }).click()
|
||||
await expect(page.locator('.inspector-zone.open')).toBeVisible()
|
||||
await expect(page.locator('.inspector-zone.open')).toContainText('Asset Details')
|
||||
await expect(page.locator('.inspector-zone.open')).toContainText('Object Description')
|
||||
await expect(page.locator('.inspector-zone.open')).toContainText('Category')
|
||||
await expect(page.locator('.inspector-zone.open')).not.toContainText('Organelle')
|
||||
await expect(page.locator('.inspector-zone.open')).not.toContainText('Plasma Membrane')
|
||||
|
||||
await expectClippedScreenshot(page, '.inspector-zone.open', 'asset-inspector.png')
|
||||
})
|
||||
Reference in New Issue
Block a user