195 lines
7.0 KiB
TypeScript
195 lines
7.0 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import { createRequire, registerHooks, stripTypeScriptTypes } from 'node:module'
|
|
import test from 'node:test'
|
|
|
|
import { createRenderer, nextTick, ref } from 'vue'
|
|
|
|
const session = {
|
|
skinSitePlayers: ref([]),
|
|
selectedSkinSitePlayerId: ref(null),
|
|
skinSiteStatus: ref('signed-in'),
|
|
skinSitePlayersStatus: ref('ready'),
|
|
}
|
|
const testGlobal = globalThis as unknown as {
|
|
previewTestSession: typeof session
|
|
previewNormalize: (skin: { texture: string }) => Promise<string>
|
|
}
|
|
testGlobal.previewTestSession = session
|
|
const require = createRequire(import.meta.url)
|
|
const { parse, compileScript } = require('vue/compiler-sfc')
|
|
const componentUrl = new URL('./SkinSitePlayerPreview.vue', import.meta.url).href
|
|
const stubs: Record<string, string> = {
|
|
'@modrinth/assets': 'export const RefreshCwIcon={}, SpinnerIcon={}, UserIcon={}',
|
|
'@modrinth/ui':
|
|
'export const commonMessages={retryButton:{defaultMessage:"Retry"}}; export const defineMessages=x=>x; export const useVIntl=()=>({formatMessage:x=>x.defaultMessage})',
|
|
'@modrinth/ui/src/components/skin/SkinPreviewRenderer.vue': 'export default {}',
|
|
'./SkinSiteSelectionRail.vue': 'export default {}',
|
|
'@/composables/skin-site-session':
|
|
'export const {skinSitePlayers,selectedSkinSitePlayerId,skinSiteStatus,skinSitePlayersStatus}=globalThis.previewTestSession; export const requestSkinSitePlayers=async()=>[]',
|
|
'@/helpers/skins':
|
|
'export const get_normalized_skin_texture=skin=>globalThis.previewNormalize(skin)',
|
|
}
|
|
const hooks = registerHooks({
|
|
resolve(specifier, context, nextResolve) {
|
|
if (specifier in stubs)
|
|
return {
|
|
url: `data:text/javascript,${encodeURIComponent(stubs[specifier])}`,
|
|
shortCircuit: true,
|
|
}
|
|
return nextResolve(specifier, context)
|
|
},
|
|
load(url, context, nextLoad) {
|
|
if (url !== componentUrl) return nextLoad(url, context)
|
|
const { descriptor } = parse(readFileSync(new URL(url), 'utf8'))
|
|
return {
|
|
format: 'module',
|
|
source: stripTypeScriptTypes(
|
|
compileScript(descriptor, { id: 'player-preview-test' }).content,
|
|
),
|
|
shortCircuit: true,
|
|
}
|
|
},
|
|
})
|
|
const { default: Preview } = await import(componentUrl)
|
|
hooks.deregister()
|
|
Preview.render = () => null
|
|
type Node = Record<string, unknown>
|
|
const renderer = createRenderer<Node, Node>({
|
|
createElement: () => ({}),
|
|
createText: () => ({}),
|
|
createComment: () => ({}),
|
|
insert() {},
|
|
remove() {},
|
|
setText() {},
|
|
setElementText() {},
|
|
patchProp() {},
|
|
parentNode: () => null,
|
|
nextSibling: () => null,
|
|
})
|
|
const flush = async () => {
|
|
await new Promise((resolve) => setImmediate(resolve))
|
|
await nextTick()
|
|
}
|
|
|
|
function mountPreview(t: test.TestContext, card: unknown) {
|
|
const app = renderer.createApp(Preview)
|
|
app.provide('accountsCard', card)
|
|
const instance = app.mount({})
|
|
t.after(() => app.unmount())
|
|
return (instance.$ as unknown as { setupState: { texture: string; textureLoading: boolean } })
|
|
.setupState
|
|
}
|
|
|
|
test('home normalizes official textures and ignores a late texture after switching players', async (t) => {
|
|
const accounts = [
|
|
{ profile: { id: 'first', name: 'First' }, account_type: 'microsoft' },
|
|
{ profile: { id: 'second', name: 'Second' }, account_type: 'microsoft' },
|
|
]
|
|
const card = ref({
|
|
accounts,
|
|
defaultUser: 'first',
|
|
getAccountSkin: (account: (typeof accounts)[number]) => ({
|
|
texture: `https://textures.test/${account.profile.id}.png`,
|
|
variant: 'CLASSIC',
|
|
}),
|
|
getAccountAvatarUrl: () => undefined,
|
|
})
|
|
const requests: Array<{ texture: string; finish: (texture: string) => void }> = []
|
|
testGlobal.previewNormalize = (skin) =>
|
|
new Promise((resolve) => requests.push({ texture: skin.texture, finish: resolve }))
|
|
const state = mountPreview(t, card)
|
|
assert.equal(requests[0].texture, 'https://textures.test/first.png')
|
|
card.value.defaultUser = 'second'
|
|
await flush()
|
|
assert.equal(state.texture, '')
|
|
requests[1].finish('data:image/png;base64,normalized-second')
|
|
await flush()
|
|
requests[0].finish('data:image/png;base64,normalized-first')
|
|
await flush()
|
|
assert.equal(state.texture, 'data:image/png;base64,normalized-second')
|
|
assert.equal(state.textureLoading, false)
|
|
})
|
|
|
|
test('account polling, avatar updates and another player changing skin keep the current preview loaded', async (t) => {
|
|
const avatar = ref('first-head')
|
|
const skins = ref<Record<string, { texture: string; variant: 'CLASSIC' | 'SLIM' }>>({
|
|
first: { texture: 'https://textures.test/first.png', variant: 'CLASSIC' },
|
|
second: { texture: 'https://textures.test/second.png', variant: 'CLASSIC' },
|
|
})
|
|
const card = ref({
|
|
accounts: [
|
|
{ profile: { id: 'first', name: 'First' }, account_type: 'microsoft' },
|
|
{ profile: { id: 'second', name: 'Second' }, account_type: 'microsoft' },
|
|
],
|
|
defaultUser: 'first',
|
|
getAccountSkin: (account: { profile: { id: string } }) => skins.value[account.profile.id],
|
|
getAccountAvatarUrl: () => avatar.value,
|
|
})
|
|
const requests: string[] = []
|
|
testGlobal.previewNormalize = (skin) => {
|
|
requests.push(skin.texture)
|
|
return requests.length === 1
|
|
? Promise.resolve('data:image/png;base64,current-skin')
|
|
: new Promise(() => {})
|
|
}
|
|
const state = mountPreview(t, card)
|
|
await flush()
|
|
assert.equal(state.texture, 'data:image/png;base64,current-skin')
|
|
|
|
card.value.accounts = card.value.accounts.map((account) => ({
|
|
...account,
|
|
profile: { ...account.profile },
|
|
}))
|
|
await flush()
|
|
assert.equal(
|
|
state.texture,
|
|
'data:image/png;base64,current-skin',
|
|
'polling must not clear the visible skin',
|
|
)
|
|
assert.equal(requests.length, 1, 'unchanged accounts must not normalize the skin again')
|
|
|
|
avatar.value = 'updated-head'
|
|
await flush()
|
|
assert.equal(state.texture, 'data:image/png;base64,current-skin')
|
|
assert.equal(requests.length, 1, 'avatar-only changes must not reload the 3D skin')
|
|
|
|
skins.value.second = { texture: 'https://textures.test/second-new.png', variant: 'SLIM' }
|
|
await flush()
|
|
assert.equal(state.texture, 'data:image/png;base64,current-skin')
|
|
assert.equal(state.textureLoading, false)
|
|
assert.equal(
|
|
requests.length,
|
|
1,
|
|
'another player changing skin must not reload the current preview',
|
|
)
|
|
})
|
|
|
|
test('the current player changing texture or model still reloads the preview', async (t) => {
|
|
const skin = ref({ texture: 'https://textures.test/original.png', variant: 'CLASSIC' })
|
|
const card = ref({
|
|
accounts: [{ profile: { id: 'first', name: 'First' }, account_type: 'microsoft' }],
|
|
defaultUser: 'first',
|
|
getAccountSkin: () => skin.value,
|
|
getAccountAvatarUrl: () => undefined,
|
|
})
|
|
const requests: string[] = []
|
|
testGlobal.previewNormalize = async (current) => {
|
|
requests.push(current.texture)
|
|
return `data:image/png;base64,skin-${requests.length}`
|
|
}
|
|
const state = mountPreview(t, card)
|
|
await flush()
|
|
skin.value.texture = 'https://textures.test/updated.png'
|
|
await flush()
|
|
assert.equal(state.texture, 'data:image/png;base64,skin-2')
|
|
skin.value.variant = 'SLIM'
|
|
await flush()
|
|
assert.equal(state.texture, 'data:image/png;base64,skin-3')
|
|
assert.deepEqual(requests, [
|
|
'https://textures.test/original.png',
|
|
'https://textures.test/updated.png',
|
|
'https://textures.test/updated.png',
|
|
])
|
|
})
|