diablo2-web/tests/issue-503-bilingual-parity....

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)
}
})
})