227 lines
8.8 KiB
TypeScript
227 lines
8.8 KiB
TypeScript
import { existsSync, readFileSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import {
|
|
detectLangFromUrl,
|
|
onGameLangChange,
|
|
onLangChange,
|
|
resolveLangFromPathname,
|
|
setGameLang,
|
|
setLang,
|
|
} from '../../src/client/i18n/lang.ts'
|
|
|
|
const ROOT_DIR = resolve(import.meta.dirname, '../..')
|
|
const FRONTEND_DIR = resolve(ROOT_DIR, 'public/ui/frontend')
|
|
const MANIFEST_PATH = resolve(FRONTEND_DIR, 'manifest.json')
|
|
const MAIN_TS_PATH = resolve(ROOT_DIR, 'src/client/main.ts')
|
|
|
|
function readPngHeader(filePath: string): { readonly width: number; readonly height: number } {
|
|
const buf = readFileSync(filePath)
|
|
expect(buf[0]).toBe(0x89)
|
|
expect(buf[1]).toBe(0x50)
|
|
expect(buf[2]).toBe(0x4e)
|
|
expect(buf[3]).toBe(0x47)
|
|
return {
|
|
width: buf.readUInt32BE(16),
|
|
height: buf.readUInt32BE(20),
|
|
}
|
|
}
|
|
|
|
describe('Batch 7 & 8 Phase 2 — Campfire Overlays, Manifest TBL Strings, main.ts Guards & i18n Fail-Fast (#702, #712, #697, #698, #699, #700, #704, #707, #708, #710)', () => {
|
|
const manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf8')) as {
|
|
readonly animStrips: Record<
|
|
string,
|
|
{
|
|
readonly file: string
|
|
readonly width: number
|
|
readonly height: number
|
|
readonly frameCount: number
|
|
readonly rate?: number
|
|
readonly drawMode?: number
|
|
}
|
|
>
|
|
readonly campfireClasses: Record<
|
|
string,
|
|
Record<
|
|
string,
|
|
{
|
|
readonly file: string
|
|
readonly width: number
|
|
readonly height: number
|
|
readonly frameCount: number
|
|
readonly rate?: number
|
|
readonly drawMode?: number
|
|
readonly frames: readonly unknown[]
|
|
}
|
|
>
|
|
>
|
|
readonly strings: Record<string, unknown> & {
|
|
readonly lobby: Record<string, unknown>
|
|
}
|
|
}
|
|
|
|
afterEach(() => {
|
|
setGameLang(null)
|
|
})
|
|
|
|
it('#712: lang.ts notifyListeners propagates listener errors instead of swallowing them', () => {
|
|
const unsub = onLangChange(() => {
|
|
throw new Error('broken tbl listener')
|
|
})
|
|
try {
|
|
expect(() => setLang('en')).toThrow('broken tbl listener')
|
|
} finally {
|
|
unsub()
|
|
}
|
|
|
|
const calls: string[] = []
|
|
const u1 = onGameLangChange((l) => {
|
|
calls.push(`first:${l}`)
|
|
throw new Error('first error')
|
|
})
|
|
const u2 = onGameLangChange((l) => {
|
|
calls.push(`second:${l}`)
|
|
})
|
|
try {
|
|
expect(() => setGameLang('zh')).toThrow('first error')
|
|
expect(calls).toEqual(['first:zh', 'second:zh'])
|
|
} finally {
|
|
u1()
|
|
u2()
|
|
}
|
|
|
|
expect(resolveLangFromPathname('/acts-en.html')).toBe('en')
|
|
expect(resolveLangFromPathname('/acts-zh.html')).toBe('zh')
|
|
expect(resolveLangFromPathname('/acts.html')).toBe('zh')
|
|
expect(detectLangFromUrl('http://localhost/acts-en.html')).toBe('ENG')
|
|
expect(detectLangFromUrl('http://localhost/acts-zh.html')).toBe('CHI')
|
|
expect(detectLangFromUrl('http://localhost/acts.html')).toBe('CHI')
|
|
})
|
|
|
|
it('#702: bakes Barbarian (bafws.dc6) and Paladin (pafws.dc6) forward overlays with drawMode=5 and ne/so overlays with drawMode=3', () => {
|
|
const baFwOv = manifest.campfireClasses.ba?.fw_ov
|
|
const paFwOv = manifest.campfireClasses.pa?.fw_ov
|
|
expect(baFwOv).toBeDefined()
|
|
expect(paFwOv).toBeDefined()
|
|
expect(baFwOv?.drawMode).toBe(5)
|
|
expect(paFwOv?.drawMode).toBe(5)
|
|
expect(baFwOv?.frameCount).toBe(manifest.campfireClasses.ba?.fw?.frameCount)
|
|
expect(paFwOv?.frameCount).toBe(manifest.campfireClasses.pa?.fw?.frameCount)
|
|
|
|
// Verify PNG files exist on disk under both flat (`campfire/ba-fw-ov.png`) and nested (`campfire/ba/fw_ov.png`) paths
|
|
for (const relPath of [
|
|
baFwOv!.file,
|
|
paFwOv!.file,
|
|
'campfire/ba/fw_ov.png',
|
|
'campfire/pa/fw_ov.png',
|
|
]) {
|
|
const fullPath = resolve(FRONTEND_DIR, relPath)
|
|
expect(existsSync(fullPath)).toBe(true)
|
|
const hdr = readPngHeader(fullPath)
|
|
expect(hdr.width).toBeGreaterThan(0)
|
|
expect(hdr.height).toBeGreaterThan(0)
|
|
}
|
|
|
|
const baPng = readPngHeader(resolve(FRONTEND_DIR, baFwOv!.file))
|
|
expect(baPng.width).toBe(baFwOv!.width)
|
|
expect(baPng.height).toBe(baFwOv!.height)
|
|
|
|
const paPng = readPngHeader(resolve(FRONTEND_DIR, paFwOv!.file))
|
|
expect(paPng.width).toBe(paFwOv!.width)
|
|
expect(paPng.height).toBe(paFwOv!.height)
|
|
|
|
// Necromancer and Sorceress additive overlays use drawMode 3 (DRAWMODE_MODULATE)
|
|
for (const clsId of ['ne', 'so'] as const) {
|
|
for (const ovKey of ['nu3_ov', 'fw_ov', 'bw_ov'] as const) {
|
|
const ov = manifest.campfireClasses[clsId]?.[ovKey]
|
|
expect(ov).toBeDefined()
|
|
expect(ov?.drawMode).toBe(3)
|
|
}
|
|
}
|
|
|
|
expect(manifest.animStrips.campfireFlame?.drawMode).toBe(3)
|
|
})
|
|
|
|
it('#698, #699, #700, #704, #708: manifest.json contains canonical .tbl strings without fake suffixes or hardcoded expiresIn90Days', () => {
|
|
const s = manifest.strings
|
|
|
|
// #698: Character expiration .tbl 11132, 11133, 11134; hardcoded expiresIn90Days removed
|
|
expect(s.expiresIn90Days).toBeUndefined()
|
|
expect(s['11132']).toBe('Expires Today')
|
|
expect(s['11133']).toBe('Expires in %d days')
|
|
expect(s['11134']).toBe('Character Expired')
|
|
expect(s.expiresToday).toBe('Expires Today')
|
|
expect(s.expiresInDays).toBe('Expires in %d days')
|
|
expect(s.characterExpired).toBe('Character Expired')
|
|
|
|
// #699: Canonical BNet connect status strings 5171..5177 without "..." suffixes
|
|
expect(s['5171']).toBe('Connecting to Battle.net')
|
|
expect(s['5172']).toBe('Querying gateway')
|
|
expect(s['5173']).toBe('Connecting to the fastest server')
|
|
expect(s['5174']).toBe('Accessing your account')
|
|
expect(s['5175']).toBe('Checking versions')
|
|
expect(s['5176']).toBe('Verifying CD-key')
|
|
expect(s['5177']).toBe('Getting player profile')
|
|
expect(s.connectingToFastest).toBe('Connecting to the fastest server')
|
|
expect(s.accessingAccount).toBe('Accessing your account')
|
|
expect(s.checkingVersions).toBe('Checking versions')
|
|
expect(String(s.connectingToFastest).endsWith('...')).toBe(false)
|
|
expect(String(s.accessingAccount).endsWith('...')).toBe(false)
|
|
expect(String(s.checkingVersions).endsWith('...')).toBe(false)
|
|
|
|
// #700: Login title (5204) and instruction (5205)
|
|
expect(s['5204']).toBe('Battle.net Log In')
|
|
expect(s['5205']).toBe(
|
|
"To use an existing Battle.net account, enter the information below and click 'Log In'. Otherwise, click 'Create New Account'.",
|
|
)
|
|
expect(s.loginTitle).toBe(s['5204'])
|
|
expect(s.loginInstruction).toBe(s['5205'])
|
|
|
|
// #704: Chat gem .tbl 5334, 5335, 5336
|
|
expect(s['5334']).toBe('Gem Activated')
|
|
expect(s['5335']).toBe('Gem Deactivated')
|
|
expect(s['5336']).toBe('Perfect Gem Activated')
|
|
expect(s.lobby.gemActivated).toBe('Gem Activated')
|
|
expect(s.lobby.gemDeactivated).toBe('Gem Deactivated')
|
|
expect(s.lobby.perfectGemActivated).toBe('Perfect Gem Activated')
|
|
|
|
// #707 & #708: Create Account instruction (5223), Please Read paragraphs (5228..5230), Level & class names
|
|
expect(s['5223']).toBe('Enter the Name and Password for your new battle.net account.')
|
|
expect(s.createAccountInstruction).toBe(s['5223'])
|
|
expect(typeof s.pleaseReadPara1).toBe('string')
|
|
expect(typeof s.pleaseReadPara2).toBe('string')
|
|
expect(typeof s.pleaseReadPara3).toBe('string')
|
|
expect(s['4057']).toBe('Level')
|
|
expect(s['5017']).toBe('Level %d')
|
|
expect(s['4007']).toBe('Barbarian')
|
|
expect(s['4008']).toBe('Paladin')
|
|
expect(s['4009']).toBe('Necromancer')
|
|
expect(s['4010']).toBe('Sorceress')
|
|
expect(s['4011']).toBe('Amazon')
|
|
expect(s['4012']).toBe('Druid')
|
|
expect(s['4013']).toBe('Assassin')
|
|
})
|
|
|
|
it('#697, #699, #704, #707, #710: main.ts has zero invented English validation popups, zero st.gateway hostname fallback, and zero silent difficulty downgrade', () => {
|
|
const mainSrc = readFileSync(MAIN_TS_PATH, 'utf8')
|
|
|
|
// #697 & #707: Zero invented English validation error strings
|
|
expect(mainSrc).not.toContain('PLEASE ENTER AN ACCOUNT NAME.')
|
|
expect(mainSrc).not.toContain('CHARACTER NAME MUST BE 2-15 LETTERS.')
|
|
expect(mainSrc).not.toContain('PLEASE ENTER A GAME NAME.')
|
|
expect(mainSrc).not.toContain('MAX NUMBER OF PLAYERS MUST BE 1-8.')
|
|
expect(mainSrc).not.toContain('CHARACTER DIFFERENCE MUST BE 1-99.')
|
|
|
|
// #704: Never use display gateway name (`st.gateway`) as network hostname fallback
|
|
expect(mainSrc).not.toContain('|| st.gateway')
|
|
|
|
// #710: Never silently downgrade locked difficulty to 'normal'
|
|
expect(mainSrc).not.toContain("st.difficulty = 'normal'")
|
|
expect(mainSrc).toContain('is not unlocked for character')
|
|
|
|
// #699: onConnect wires canonical BNCS status stages (5171, 5173) into bnet_login
|
|
expect(mainSrc).toContain("manifestStrings?.['5171']")
|
|
expect(mainSrc).toContain("manifestStrings?.['5173']")
|
|
})
|
|
})
|