93 lines
5.0 KiB
JavaScript
93 lines
5.0 KiB
JavaScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import * as THREE from 'three'
|
|
import './test-support.mjs'
|
|
|
|
const { loadCardTextures, disposeCardTextures } = await import('../src/cardTextures.ts')
|
|
const { createCardMaterial, applyCardTextMask } = await import('../src/cardMaterial.ts')
|
|
const { validateCardCatalog } = await import('../src/cardCatalog.ts')
|
|
const asset = {
|
|
name: 'test', artwork: '/card-art/test.png', mask: '/card-art/test-mask.png',
|
|
revision: 'test', width: 10, height: 14,
|
|
}
|
|
|
|
test('cards without a text mask request only artwork/finish and retain the disabled shader path', async (t) => {
|
|
const requests = []
|
|
const loaded = await loadCardTextures({ async loadAsync(path) { requests.push(path); return new THREE.Texture() } }, asset, 8)
|
|
assert.deepEqual(requests, [asset.artwork, asset.mask])
|
|
assert.equal(Object.hasOwn(loaded, 'textMask'), false)
|
|
assert.equal(loaded.artwork.colorSpace, THREE.SRGBColorSpace)
|
|
assert.equal(loaded.mask.colorSpace, THREE.NoColorSpace)
|
|
assert.equal(loaded.artwork.anisotropy, 8)
|
|
const material = createCardMaterial(loaded.artwork, loaded.mask, new THREE.Texture(), new THREE.CubeTexture(), new THREE.Vector3())
|
|
t.after(() => { material.dispose(); disposeCardTextures(loaded) })
|
|
assert.equal(material.uniforms.hasTextMask.value, false)
|
|
assert.equal(material.uniforms.textMask.value, null)
|
|
})
|
|
|
|
test('optional text masks load as data and switching to an unmasked card clears protection without shader recompilation', async (t) => {
|
|
const textMask = '/card-art/test-text-mask.png'
|
|
const requests = []
|
|
const loaded = await loadCardTextures({ async loadAsync(path) { requests.push(path); return new THREE.Texture() } }, { ...asset, textMask }, 4)
|
|
assert.deepEqual(requests, [asset.artwork, asset.mask, textMask])
|
|
assert.equal(loaded.textMask.colorSpace, THREE.NoColorSpace)
|
|
assert.equal(loaded.textMask.anisotropy, 4)
|
|
const material = createCardMaterial(loaded.artwork, loaded.mask, new THREE.Texture(), new THREE.CubeTexture(), new THREE.Vector3(), loaded.textMask)
|
|
const version = material.version
|
|
assert.equal(material.uniforms.hasTextMask.value, true)
|
|
assert.equal(material.uniforms.textMask.value, loaded.textMask)
|
|
applyCardTextMask(material)
|
|
assert.equal(material.uniforms.hasTextMask.value, false)
|
|
assert.equal(material.uniforms.textMask.value, null)
|
|
applyCardTextMask(material, loaded.textMask)
|
|
assert.equal(material.uniforms.hasTextMask.value, true)
|
|
assert.equal(material.version, version)
|
|
material.dispose()
|
|
const spies = [loaded.artwork, loaded.mask, loaded.textMask].map(texture => t.mock.method(texture, 'dispose'))
|
|
disposeCardTextures(loaded)
|
|
assert.deepEqual(spies.map(spy => spy.mock.callCount()), [1, 1, 1])
|
|
})
|
|
|
|
test('advertised optional-mask failures reject loading and clean up every successful texture', async (t) => {
|
|
const paths = [asset.artwork, asset.mask, '/card-art/test-text-mask.png']
|
|
for (const failingPath of paths) {
|
|
const textures = paths.map(() => new THREE.Texture())
|
|
const spies = textures.map(texture => t.mock.method(texture, 'dispose'))
|
|
const failure = new Error(`failed ${failingPath}`)
|
|
await assert.rejects(loadCardTextures({ async loadAsync(path) {
|
|
if (path === failingPath) throw failure
|
|
return textures[paths.indexOf(path)]
|
|
} }, { ...asset, textMask: paths[2] }), error => error === failure)
|
|
assert.deepEqual(spies.map((spy, index) => spy.mock.callCount()),
|
|
paths.map(path => path === failingPath ? 0 : 1))
|
|
}
|
|
})
|
|
|
|
test('runtime catalog validation accepts complete artifact variants and rejects malformed text-mask fields', () => {
|
|
const artifactAsset = {
|
|
artwork: '/__card_asset/BP-002-david/high/normal/card.png',
|
|
mask: '/__card_asset/BP-002-david/high/normal/finish-mask.png',
|
|
revision: 'test', width: 2000, height: 2800,
|
|
}
|
|
const resolutions = ['low', 'med', 'high']
|
|
const printings = ['normal', 'borderless', 'textless', 'boundless']
|
|
const variants = Object.fromEntries(resolutions.map(resolution => [resolution,
|
|
Object.fromEntries(printings.map(printing => [printing, { ...artifactAsset,
|
|
artwork: `/__card_asset/BP-002-david/${resolution}/${printing}/card.png`,
|
|
mask: `/__card_asset/BP-002-david/${resolution}/${printing}/finish-mask.png`,
|
|
}]))
|
|
]))
|
|
const card = { cardId: 'BP-002', title: 'David', rarity: 'Blessed', folderName: 'BP-002-david',
|
|
acceptedRevision: 'accepted', variants }
|
|
const catalog = { schemaVersion: 2, cards: [card], errors: [] }
|
|
assert.deepEqual(validateCardCatalog(catalog), catalog)
|
|
const masked = structuredClone(catalog)
|
|
masked.cards[0].variants.high.normal.textMask = '/__card_asset/BP-002-david/high/normal/text-mask.png'
|
|
assert.deepEqual(validateCardCatalog(masked), masked)
|
|
for (const textMask of [null, 1, false, '', '/reference/text.png', 'https://example.com/text.png']) {
|
|
const malformed = structuredClone(catalog)
|
|
malformed.cards[0].variants.high.normal.textMask = textMask
|
|
assert.throws(() => validateCardCatalog(malformed), /invalid format/)
|
|
}
|
|
})
|