670 lines
22 KiB
TypeScript
670 lines
22 KiB
TypeScript
/**
|
|
* tests/issue-503-bilingual-parity.test.ts
|
|
*
|
|
* Comprehensive unit test suite for Issue #503:
|
|
* Full Chinese (`CHI`) and English (`ENG`) Bilingual Versions of Diablo II v1.13c Web Port.
|
|
*/
|
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { existsSync, readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import {
|
|
buildLangSwitchHref,
|
|
detectLangFromUrl,
|
|
getGameLang,
|
|
getTblLang,
|
|
isEn,
|
|
isEnglish,
|
|
onTblLangChange,
|
|
setTblLang,
|
|
} from '../src/i18n/lang.ts'
|
|
import {
|
|
CANONICAL_CHI_TBL_DICT,
|
|
CANONICAL_ENG_TBL_DICT,
|
|
resolveTblKey,
|
|
} from '../src/data/canonical-tbl-strings.ts'
|
|
import {
|
|
ACT_NAMES_EN,
|
|
ACT_NAMES_ZH,
|
|
LEVEL_NAMES_ZH,
|
|
levelDisplayName,
|
|
sceneDisplayName,
|
|
variantDisplayLabel,
|
|
} from '../src/game/level-names-zh.ts'
|
|
import {
|
|
CANONICAL_SUPER_UNIQUE_LANDMARKS,
|
|
formatEliteAffixLabel,
|
|
monsterDisplayName,
|
|
} from '../src/game/monsters.ts'
|
|
import { TOWN_NPC_DESCRIPTORS, getLocalizedNpcName } from '../src/game/npc-table.ts'
|
|
import { createStarterProfileForClass } from '../src/game/class-starter-profiles.ts'
|
|
import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts'
|
|
import { CharacterSheetPanel } from '../src/ui/character-sheet.ts'
|
|
import { InventoryPanel, itemToUiInventoryItem } from '../src/ui/inventory.ts'
|
|
import { SkillTreePanel } from '../src/ui/skill-tree-panel.ts'
|
|
import { ACT_QUESTS, ACT_WAYPOINTS, WorldPanelsHud } from '../src/ui/world-panels.ts'
|
|
import { ControlBarHud } from '../src/ui/control-bar.ts'
|
|
import { GlobesHud } from '../src/ui/globes.ts'
|
|
import { SkillHotkeysHud } from '../src/ui/hotkeys.ts'
|
|
import { cleanChiItemName, formatGroundItemLabelText } from '../src/ui/ground-labels.ts'
|
|
import type { D2FontRenderer } from '../src/ui/font.ts'
|
|
|
|
const CJK_REGEX = /[\u4e00-\u9fff]/
|
|
|
|
function createMockCanvasAndFont() {
|
|
const textCalls: string[] = []
|
|
const ctx = {
|
|
drawImage: vi.fn(),
|
|
fillRect: vi.fn(),
|
|
strokeRect: vi.fn(),
|
|
fillText: vi.fn((text: string) => {
|
|
textCalls.push(String(text))
|
|
}),
|
|
beginPath: vi.fn(),
|
|
moveTo: vi.fn(),
|
|
lineTo: vi.fn(),
|
|
arc: vi.fn(),
|
|
closePath: vi.fn(),
|
|
fill: vi.fn(),
|
|
stroke: vi.fn(),
|
|
save: vi.fn(),
|
|
restore: vi.fn(),
|
|
clip: vi.fn(),
|
|
rect: vi.fn(),
|
|
setLineDash: vi.fn(),
|
|
createLinearGradient: vi.fn(() => ({ addColorStop: vi.fn() })),
|
|
createRadialGradient: vi.fn(() => ({ addColorStop: vi.fn() })),
|
|
measureText: vi.fn((t: string) => ({ width: String(t).length * 8 })),
|
|
fillStyle: '',
|
|
strokeStyle: '',
|
|
lineWidth: 1,
|
|
globalAlpha: 1,
|
|
font: '',
|
|
textAlign: 'left',
|
|
textBaseline: 'alphabetic',
|
|
} as unknown as CanvasRenderingContext2D
|
|
|
|
const font = {
|
|
isLoaded: true,
|
|
measureText: vi.fn((t: string) => String(t).length * 8),
|
|
drawText: vi.fn((_ctx: CanvasRenderingContext2D, text: string) => {
|
|
textCalls.push(String(text))
|
|
}),
|
|
} as unknown as D2FontRenderer
|
|
|
|
return { ctx, font, textCalls }
|
|
}
|
|
|
|
describe('Issue #503: Bilingual Language Detection & State (src/i18n/lang.ts)', () => {
|
|
afterEach(() => {
|
|
setTblLang(null)
|
|
})
|
|
|
|
it('detects ENG vs CHI from URL paths and query parameters', () => {
|
|
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')
|
|
expect(detectLangFromUrl('http://localhost/acts.html?lang=en')).toBe('ENG')
|
|
expect(detectLangFromUrl('http://localhost/acts-zh.html?lang=en')).toBe('ENG')
|
|
expect(detectLangFromUrl('http://localhost/acts-en.html?lang=zh')).toBe('CHI')
|
|
})
|
|
|
|
it('updates runtime language state and notifies listeners on setTblLang', () => {
|
|
const observed: string[] = []
|
|
const unsub = onTblLangChange(l => observed.push(l))
|
|
|
|
setTblLang('ENG')
|
|
expect(getTblLang()).toBe('ENG')
|
|
expect(getGameLang()).toBe('en')
|
|
expect(isEn()).toBe(true)
|
|
expect(isEnglish()).toBe(true)
|
|
|
|
setTblLang('CHI')
|
|
expect(getTblLang()).toBe('CHI')
|
|
expect(getGameLang()).toBe('zh')
|
|
expect(isEn()).toBe(false)
|
|
expect(isEnglish()).toBe(false)
|
|
|
|
unsub()
|
|
expect(observed).toEqual(['ENG', 'CHI'])
|
|
})
|
|
|
|
it('builds language switch href preserving act/level/hero/lighting/density/elite query params', () => {
|
|
expect(
|
|
buildLangSwitchHref(
|
|
'ENG',
|
|
'/acts-zh.html',
|
|
'?act=2&level=40-lut-gholein&hero=pa&lighting=sunset&density=2&elite=4&lang=zh',
|
|
),
|
|
).toBe('/acts-en.html?act=2&level=40-lut-gholein&hero=pa&lighting=sunset&density=2&elite=4')
|
|
|
|
expect(
|
|
buildLangSwitchHref(
|
|
'CHI',
|
|
'/acts-en.html',
|
|
'?act=4&level=103-the-pandemonium-fortress&hero=ne',
|
|
),
|
|
).toBe('/acts-zh.html?act=4&level=103-the-pandemonium-fortress&hero=ne')
|
|
})
|
|
})
|
|
|
|
describe('Issue #503: HTML Entry Pages & Zero-CJK Invariant in acts-en.html', () => {
|
|
it('verifies index.html, acts-zh.html, acts-en.html, and acts.html exist with required bilingual controls', () => {
|
|
const root = process.cwd()
|
|
const indexHtml = readFileSync(join(root, 'index.html'), 'utf8')
|
|
const actsZhHtml = readFileSync(join(root, 'acts-zh.html'), 'utf8')
|
|
const actsEnHtml = readFileSync(join(root, 'acts-en.html'), 'utf8')
|
|
const actsHtml = readFileSync(join(root, 'acts.html'), 'utf8')
|
|
|
|
expect(indexHtml).toContain('id="enter-zh"')
|
|
expect(indexHtml).toContain('id="enter-en"')
|
|
expect(indexHtml).toContain('acts-zh.html')
|
|
expect(indexHtml).toContain('acts-en.html')
|
|
|
|
expect(actsZhHtml).toContain('lang="zh-CN"')
|
|
expect(actsZhHtml).toContain('id="lang-switch"')
|
|
expect(actsHtml).toContain('id="lang-switch"')
|
|
|
|
expect(actsEnHtml).toContain('lang="en"')
|
|
expect(actsEnHtml).toContain('id="lang-switch"')
|
|
expect(CJK_REGEX.test(actsEnHtml)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Issue #503: Canonical 1.13c .tbl Dictionary & Domain Lookups', () => {
|
|
afterEach(() => {
|
|
setTblLang(null)
|
|
})
|
|
|
|
it('provides comprehensive ENG and CHI .tbl strings for UI, monsters, and NPCs', () => {
|
|
expect(Object.keys(CANONICAL_ENG_TBL_DICT).length).toBeGreaterThan(3500)
|
|
expect(Object.keys(CANONICAL_CHI_TBL_DICT).length).toBeGreaterThan(3000)
|
|
|
|
expect(resolveTblKey('strBSArmor', 'ENG')).toBe('Armor')
|
|
expect(resolveTblKey('strBSWeapons', 'ENG')).toBe('Weapons')
|
|
expect(resolveTblKey('strBSMisc', 'ENG')).toBe('Misc')
|
|
expect(resolveTblKey('Fallen', 'ENG')).toBe('Fallen')
|
|
expect(resolveTblKey('Fallen', 'CHI')).toBe('沈淪魔')
|
|
expect(resolveTblKey('Bishibosh', 'ENG')).toBe('Bishibosh')
|
|
expect(resolveTblKey('Bishibosh', 'CHI')).toBe('畢須博須')
|
|
})
|
|
|
|
it('resolves level, scene, variant, monster, affix, and NPC names in both ENG and CHI', () => {
|
|
expect(ACT_NAMES_EN[1]).toBe('Act I')
|
|
expect(ACT_NAMES_ZH[1]).toBe('第一章')
|
|
|
|
expect(levelDisplayName(1, 'ENG')).toBe('Rogue Encampment')
|
|
expect(levelDisplayName(1, 'CHI')).toBe('罗格营地')
|
|
expect(sceneDisplayName('1-rogue-encampment', 'Rogue Encampment', 'ENG')).toBe('Rogue Encampment')
|
|
expect(sceneDisplayName('1-rogue-encampment', 'Rogue Encampment', 'CHI')).toBe('罗格营地')
|
|
expect(variantDisplayLabel('var1', 'ENG')).toBe('Variant 1')
|
|
expect(variantDisplayLabel('var1', 'CHI')).toBe('变体 1')
|
|
expect(variantDisplayLabel('towns1', 'ENG')).toBe('South Gate')
|
|
expect(variantDisplayLabel('towns1', 'CHI')).toBe('南门')
|
|
|
|
expect(monsterDisplayName('Fallen', 'ENG')).toBe('Fallen')
|
|
expect(monsterDisplayName('Fallen', 'CHI')).toBe('沈淪魔')
|
|
expect(formatEliteAffixLabel('strong', 'ENG')).toBe('Extra Strong')
|
|
expect(formatEliteAffixLabel('strong', 'CHI')).toBe('特别强壮')
|
|
|
|
expect(getLocalizedNpcName('akara', 'ENG')).toBe('Akara')
|
|
expect(getLocalizedNpcName('akara', 'CHI')).toBe('阿卡拉')
|
|
})
|
|
})
|
|
|
|
describe('Issue #503: HUD & Panel Rendering in ENG (Zero CJK) and CHI Modes', () => {
|
|
afterEach(() => {
|
|
setTblLang(null)
|
|
})
|
|
|
|
it('renders CharacterSheetPanel, InventoryPanel, SkillTreePanel, WorldPanelsHud, ControlBarHud, GlobesHud, and SkillHotkeysHud with zero CJK in ENG mode', () => {
|
|
setTblLang('ENG')
|
|
const { ctx, font, textCalls } = createMockCanvasAndFont()
|
|
const mockImg = { width: 256, height: 256, complete: true, naturalWidth: 256 } as unknown as HTMLImageElement
|
|
|
|
const charSheet = new CharacterSheetPanel()
|
|
charSheet.visible = true
|
|
charSheet.lang = 'ENG'
|
|
charSheet.draw(
|
|
ctx,
|
|
{
|
|
hp: 100,
|
|
maxHp: 100,
|
|
mana: 50,
|
|
maxMana: 50,
|
|
stamina: 80,
|
|
maxStamina: 80,
|
|
leftSkillDmg: '45-82',
|
|
rightSkillDmg: '520-585',
|
|
},
|
|
{
|
|
borderLeftImg: null,
|
|
charSheetImg: null,
|
|
buySellBtnImg: null,
|
|
levelBtnImg: null,
|
|
},
|
|
font,
|
|
)
|
|
|
|
const invPanel = new InventoryPanel()
|
|
invPanel.visible = true
|
|
invPanel.draw(
|
|
ctx,
|
|
{
|
|
borderRightImg: null,
|
|
invSheetImg: null,
|
|
invTab0Img: null,
|
|
invTab1Img: null,
|
|
buySellBtnImg: null,
|
|
goldCoinBtnImg: null,
|
|
itemsAtlasImg: null,
|
|
itemRects: {},
|
|
},
|
|
font,
|
|
)
|
|
|
|
const skillTree = new SkillTreePanel()
|
|
skillTree.visible = true
|
|
skillTree.lang = 'ENG'
|
|
skillTree.draw(
|
|
ctx,
|
|
{
|
|
borderRightImg: null,
|
|
skillPointsImg: null,
|
|
skillTabsImg: null,
|
|
buySellBtnImg: null,
|
|
},
|
|
font,
|
|
)
|
|
|
|
const worldPanels = new WorldPanelsHud()
|
|
worldPanels.lang = 'ENG'
|
|
const assets = {
|
|
borderLeftImg: null,
|
|
questBgImg: null,
|
|
waypointBgImg: null,
|
|
stashBgImg: null,
|
|
cubeBgImg: null,
|
|
vendorBgImg: null,
|
|
vendorTabsImg: null,
|
|
buySellBtnImg: null,
|
|
miniConvertImg: null,
|
|
questsAtlasImg: null,
|
|
itemsAtlasImg: null,
|
|
waypointTabsImg: null,
|
|
waypointIconsImg: null,
|
|
questTabsImg: null,
|
|
inventory: invPanel,
|
|
}
|
|
worldPanels.drawLeftDockPanel(ctx, 'quest', assets, font)
|
|
worldPanels.drawLeftDockPanel(ctx, 'waypoint', assets, font)
|
|
worldPanels.drawLeftDockPanel(ctx, 'stash', assets, font)
|
|
worldPanels.drawLeftDockPanel(ctx, 'cube', assets, font)
|
|
worldPanels.openNpcMenuByName('Akara', 400, 220)
|
|
worldPanels.drawNpcMenu(ctx, font, { boxPiecesImg: mockImg, focus16Img: mockImg })
|
|
|
|
const controlBar = new ControlBarHud()
|
|
controlBar.lang = 'ENG'
|
|
controlBar.hoveredElement = 'char'
|
|
controlBar.draw(
|
|
ctx,
|
|
{
|
|
level: 1,
|
|
xp: 0,
|
|
currentLevelXp: 0,
|
|
nextLevelXp: 500,
|
|
stamina: 80,
|
|
maxStamina: 80,
|
|
isRunning: true,
|
|
unspentStatPoints: 5,
|
|
unspentSkillPoints: 2,
|
|
},
|
|
{
|
|
ctrlPnlImg: null,
|
|
runBtnImg: null,
|
|
menuBtnImg: null,
|
|
miniPanelImg: null,
|
|
miniPanelBtnsImg: null,
|
|
levelBtnImg: null,
|
|
levelSocketImg: null,
|
|
},
|
|
font,
|
|
)
|
|
|
|
const globes = new GlobesHud()
|
|
globes.lang = 'ENG'
|
|
globes.lifeHovered = true
|
|
globes.draw(
|
|
ctx,
|
|
{
|
|
hp: 100,
|
|
maxHp: 100,
|
|
mana: 50,
|
|
maxMana: 50,
|
|
poisoned: false,
|
|
},
|
|
{
|
|
lifeImg: null,
|
|
manaImg: null,
|
|
poisonImg: null,
|
|
overlapLeftImg: null,
|
|
overlapRightImg: null,
|
|
},
|
|
font,
|
|
)
|
|
|
|
const hotkeys = new SkillHotkeysHud()
|
|
hotkeys.lang = 'ENG'
|
|
hotkeys.openPopup = 'right'
|
|
hotkeys.draw(ctx, null, font, true)
|
|
|
|
expect(textCalls.length).toBeGreaterThan(20)
|
|
const cjkLeaks = textCalls.filter(t => CJK_REGEX.test(t))
|
|
expect(cjkLeaks).toEqual([])
|
|
})
|
|
|
|
it('renders authentic Chinese strings in CHI mode without mixed English parentheticals on CharacterSheet, SkillTree, ControlBar, and WorldPanels', () => {
|
|
setTblLang('CHI')
|
|
const { ctx, font, textCalls } = createMockCanvasAndFont()
|
|
|
|
const charSheet = new CharacterSheetPanel()
|
|
charSheet.visible = true
|
|
charSheet.lang = 'CHI'
|
|
charSheet.draw(
|
|
ctx,
|
|
{
|
|
hp: 100,
|
|
maxHp: 100,
|
|
mana: 50,
|
|
maxMana: 50,
|
|
stamina: 80,
|
|
maxStamina: 80,
|
|
leftSkillDmg: '45-82',
|
|
rightSkillDmg: '520-585',
|
|
},
|
|
{
|
|
borderLeftImg: null,
|
|
charSheetImg: null,
|
|
buySellBtnImg: null,
|
|
levelBtnImg: null,
|
|
},
|
|
font,
|
|
)
|
|
|
|
const skillTree = new SkillTreePanel()
|
|
skillTree.visible = true
|
|
skillTree.lang = 'CHI'
|
|
skillTree.draw(
|
|
ctx,
|
|
{
|
|
borderRightImg: null,
|
|
skillPointsImg: null,
|
|
skillTabsImg: null,
|
|
buySellBtnImg: null,
|
|
},
|
|
font,
|
|
)
|
|
|
|
const controlBar = new ControlBarHud()
|
|
controlBar.lang = 'CHI'
|
|
controlBar.hoveredElement = 'char'
|
|
controlBar.draw(
|
|
ctx,
|
|
{
|
|
level: 1,
|
|
xp: 0,
|
|
currentLevelXp: 0,
|
|
nextLevelXp: 500,
|
|
stamina: 80,
|
|
maxStamina: 80,
|
|
isRunning: true,
|
|
unspentStatPoints: 5,
|
|
unspentSkillPoints: 2,
|
|
},
|
|
{
|
|
ctrlPnlImg: null,
|
|
runBtnImg: null,
|
|
menuBtnImg: null,
|
|
miniPanelImg: null,
|
|
miniPanelBtnsImg: null,
|
|
levelBtnImg: null,
|
|
levelSocketImg: null,
|
|
},
|
|
font,
|
|
)
|
|
|
|
const invPanel = new InventoryPanel()
|
|
const worldPanels = new WorldPanelsHud()
|
|
worldPanels.lang = 'CHI'
|
|
const assets = {
|
|
borderLeftImg: null,
|
|
questBgImg: null,
|
|
waypointBgImg: null,
|
|
stashBgImg: null,
|
|
cubeBgImg: null,
|
|
vendorBgImg: null,
|
|
vendorTabsImg: null,
|
|
buySellBtnImg: null,
|
|
miniConvertImg: null,
|
|
questsAtlasImg: null,
|
|
itemsAtlasImg: null,
|
|
waypointTabsImg: null,
|
|
waypointIconsImg: null,
|
|
questTabsImg: null,
|
|
inventory: invPanel,
|
|
}
|
|
worldPanels.drawLeftDockPanel(ctx, 'waypoint', assets, font)
|
|
worldPanels.drawLeftDockPanel(ctx, 'stash', assets, font)
|
|
worldPanels.drawLeftDockPanel(ctx, 'cube', assets, font)
|
|
|
|
expect(textCalls).toContain('力量')
|
|
expect(textCalls).toContain('剩余技能点')
|
|
expect(textCalls).toContain('角色属性 (A/C)')
|
|
expect(textCalls).toContain('罗格营地')
|
|
expect(textCalls).toContain('私人储物箱')
|
|
expect(textCalls).toContain('赫拉迪克方块 (已合成 0 次)')
|
|
|
|
expect(textCalls).not.toContain('角色属性 (Character)')
|
|
expect(textCalls).not.toContain('技能树 (Skill Tree)')
|
|
expect(textCalls).not.toContain('传送站 (Waypoints)')
|
|
expect(textCalls).not.toContain('私人储物箱 (Personal Stash)')
|
|
expect(textCalls).not.toContain('赫拉迪克方块 (Horadric Cube)')
|
|
})
|
|
|
|
it('localizes all 136 levels, 27 quests, and 39 waypoints cleanly in both ENG and CHI', () => {
|
|
const levelKeys = Object.keys(LEVEL_NAMES_ZH)
|
|
expect(levelKeys.length).toBeGreaterThanOrEqual(136)
|
|
for (let levelId = 1; levelId <= 136; levelId++) {
|
|
const zhName = LEVEL_NAMES_ZH[levelId]!
|
|
expect(zhName).toBeTruthy()
|
|
setTblLang('ENG')
|
|
const resolvedEn = levelDisplayName(levelId)
|
|
expect(resolvedEn.length).toBeGreaterThan(0)
|
|
expect(CJK_REGEX.test(resolvedEn)).toBe(false)
|
|
|
|
setTblLang('CHI')
|
|
const resolvedZh = levelDisplayName(levelId)
|
|
expect(resolvedZh).toBe(zhName)
|
|
expect(CJK_REGEX.test(resolvedZh)).toBe(true)
|
|
}
|
|
|
|
const wpHud = new WorldPanelsHud()
|
|
let totalQuests = 0
|
|
for (const act of [1, 2, 3, 4, 5] as const) {
|
|
const quests = ACT_QUESTS[act] ?? []
|
|
for (const q of quests) {
|
|
totalQuests++
|
|
for (const status of ['completed', 'active', 'locked'] as const) {
|
|
wpHud.setQuestStatus(q.id, status)
|
|
const enText = wpHud.getQuestDisplayText(q, 'ENG')
|
|
expect(CJK_REGEX.test(enText.title)).toBe(false)
|
|
expect(CJK_REGEX.test(enText.body)).toBe(false)
|
|
expect(CJK_REGEX.test(enText.statusTag)).toBe(false)
|
|
if (enText.rewardText) expect(CJK_REGEX.test(enText.rewardText)).toBe(false)
|
|
|
|
const zhText = wpHud.getQuestDisplayText(q, 'CHI')
|
|
expect(CJK_REGEX.test(zhText.title)).toBe(true)
|
|
expect(CJK_REGEX.test(zhText.body)).toBe(true)
|
|
expect(CJK_REGEX.test(zhText.statusTag)).toBe(true)
|
|
}
|
|
}
|
|
}
|
|
expect(totalQuests).toBe(27)
|
|
|
|
let totalWaypoints = 0
|
|
for (const act of [1, 2, 3, 4, 5] as const) {
|
|
const wps = ACT_WAYPOINTS[act] ?? []
|
|
for (const wp of wps) {
|
|
totalWaypoints++
|
|
expect(CJK_REGEX.test(wp.nameEn)).toBe(false)
|
|
expect(CJK_REGEX.test(wp.nameZh)).toBe(true)
|
|
}
|
|
}
|
|
expect(totalWaypoints).toBe(39)
|
|
})
|
|
|
|
it('renders pure Chinese header (no English parentheticals) in CHI mode and pure English header in ENG mode for all Town NPCs', () => {
|
|
expect(TOWN_NPC_DESCRIPTORS.length).toBeGreaterThanOrEqual(30)
|
|
const dummyBox = { complete: true, naturalWidth: 192, naturalHeight: 16 } as any
|
|
for (const desc of TOWN_NPC_DESCRIPTORS) {
|
|
const { ctx: ctxChi, font: fontChi, textCalls: callsChi } = createMockCanvasAndFont()
|
|
const wpChi = new WorldPanelsHud()
|
|
wpChi.lang = 'CHI'
|
|
wpChi.openNpcMenu(desc, 400, 220)
|
|
wpChi.drawNpcMenu(ctxChi, fontChi, { boxPiecesImg: dummyBox })
|
|
expect(callsChi.length).toBeGreaterThan(0)
|
|
const headerChi = callsChi[0]!
|
|
expect(CJK_REGEX.test(headerChi)).toBe(true)
|
|
expect(/\([A-Za-z]/.test(headerChi)).toBe(false)
|
|
|
|
const { ctx: ctxEng, font: fontEng, textCalls: callsEng } = createMockCanvasAndFont()
|
|
const wpEng = new WorldPanelsHud()
|
|
wpEng.lang = 'ENG'
|
|
wpEng.openNpcMenu(desc, 400, 220)
|
|
wpEng.drawNpcMenu(ctxEng, fontEng, { boxPiecesImg: dummyBox })
|
|
expect(callsEng.length).toBeGreaterThan(0)
|
|
for (const line of callsEng) {
|
|
expect(CJK_REGEX.test(line)).toBe(false)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('renders zero CJK in ENG tooltips and zero mixed parentheticals in CHI ground labels across all 7 class starter profiles', () => {
|
|
const classCodes = ['ama', 'sor', 'nec', 'pal', 'bar', 'dru', 'ass'] as const
|
|
expect(classCodes.length).toBe(7)
|
|
|
|
for (const code of classCodes) {
|
|
const profile = createStarterProfileForClass(code)
|
|
const allItems = [
|
|
...Object.values(profile.equipped).filter(Boolean),
|
|
...profile.bagItems.map(b => b.item),
|
|
]
|
|
for (const item of allItems) {
|
|
if (!item) continue
|
|
// 1. ENG Tooltip check
|
|
const { ctx, font, textCalls } = createMockCanvasAndFont()
|
|
const wp = new WorldPanelsHud()
|
|
wp.lang = 'ENG'
|
|
wp.drawItemTooltip(ctx, { item, x: 400, y: 300 }, font, false)
|
|
const cjkLines = textCalls.filter(line => CJK_REGEX.test(line))
|
|
expect(cjkLines, `Starter item ${item.id} (${code}) leaked CJK in ENG tooltip: ${cjkLines.join(' | ')}`).toEqual([])
|
|
|
|
// 2. CHI Ground Label & Drop Notification check
|
|
setTblLang('CHI')
|
|
const groundEntity = {
|
|
id: 1,
|
|
name: item.name,
|
|
nameZh: item.nameZh,
|
|
quality: item.quality,
|
|
x: 0,
|
|
y: 0,
|
|
item,
|
|
isGold: false,
|
|
amount: 1,
|
|
droppedAt: 0,
|
|
} as any
|
|
const labelChi = formatGroundItemLabelText(groundEntity)
|
|
expect(/\([A-Za-z]/.test(labelChi), `Ground label in CHI leaked English parens: ${labelChi}`).toBe(false)
|
|
const cleanedDropName = cleanChiItemName(item.nameZh)
|
|
expect(/\([A-Za-z]/.test(cleanedDropName), `cleanChiItemName leaked English parens: ${cleanedDropName}`).toBe(false)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('renders zero CJK and proper One-Hand/Two-Hand/Throw Damage labels in ENG tooltips across all 659 base items and vendor weapons', () => {
|
|
setTblLang('ENG')
|
|
const tables = getEmbeddedDropTables()
|
|
const allBases = [
|
|
...tables.weapons.byCode.values(),
|
|
...tables.armor.byCode.values(),
|
|
...tables.misc.byCode.values(),
|
|
]
|
|
expect(allBases.length).toBeGreaterThanOrEqual(650)
|
|
|
|
for (const base of allBases) {
|
|
const raw: any = {
|
|
id: `test-${base.id}`,
|
|
code: base.id,
|
|
name: base.name,
|
|
nameZh: (base as any).nameZh,
|
|
quality: 'normal',
|
|
identified: true,
|
|
level: 1,
|
|
base,
|
|
invWidth: base.invWidth ?? 1,
|
|
invHeight: base.invHeight ?? 1,
|
|
durability: (base as any).durability ?? 10,
|
|
maxDurability: (base as any).durability ?? 10,
|
|
defense: (base as any).defense ?? 0,
|
|
stats: {},
|
|
affixes: [],
|
|
rolledProps: [],
|
|
runewordProps: [],
|
|
}
|
|
const uiItem = itemToUiInventoryItem(raw, tables)
|
|
const { ctx, font, textCalls } = createMockCanvasAndFont()
|
|
const wp = new WorldPanelsHud()
|
|
wp.lang = 'ENG'
|
|
wp.drawItemTooltip(ctx, { item: uiItem, x: 400, y: 300 }, font, true)
|
|
for (const line of textCalls) {
|
|
expect(CJK_REGEX.test(line), `Base item ${base.id} (${base.name}) leaked CJK in ENG: ${line}`).toBe(false)
|
|
expect(line).not.toMatch(/One-Hand Damage:\s*One-Hand Damage:/i)
|
|
expect(line).not.toMatch(/\/\s*Two-Hand:/i)
|
|
}
|
|
}
|
|
|
|
// Also test Charsi vendor inventory across all 4 tabs
|
|
const wpVendor = new WorldPanelsHud()
|
|
wpVendor.lang = 'ENG'
|
|
wpVendor.openVendorForNpc('Charsi', 'trade', 85, 0)
|
|
for (const tabId of ['armor', 'weapons1', 'weapons2', 'misc'] as const) {
|
|
for (const p of wpVendor.vendorTabPlacements[tabId]) {
|
|
const { ctx, font, textCalls } = createMockCanvasAndFont()
|
|
wpVendor.drawItemTooltip(ctx, { item: p.item, x: 400, y: 300 }, font, true)
|
|
for (const line of textCalls) {
|
|
expect(CJK_REGEX.test(line), `Charsi ${tabId} item ${p.item.name} leaked CJK in ENG: ${line}`).toBe(false)
|
|
expect(line).not.toMatch(/\/\s*Two-Hand:/i)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
it('resolves all 23 SuperUnique landmarks in CANONICAL_SUPER_UNIQUE_LANDMARKS to Chinese in CHI mode and English in ENG mode', () => {
|
|
expect(CANONICAL_SUPER_UNIQUE_LANDMARKS.length).toBe(23)
|
|
for (const su of CANONICAL_SUPER_UNIQUE_LANDMARKS) {
|
|
const zhName = monsterDisplayName(
|
|
{ id: su.minionMonsterId || su.id, name: su.id, superUniqueId: su.id },
|
|
'CHI',
|
|
)
|
|
expect(CJK_REGEX.test(zhName), `SuperUnique ${su.id} did not resolve to Chinese in CHI mode: ${zhName}`).toBe(true)
|
|
|
|
const enName = monsterDisplayName(
|
|
{ id: su.minionMonsterId || su.id, name: su.id, superUniqueId: su.id },
|
|
'ENG',
|
|
)
|
|
expect(CJK_REGEX.test(enName), `SuperUnique ${su.id} leaked CJK in ENG mode: ${enName}`).toBe(false)
|
|
}
|
|
})
|
|
})
|
|
|