351 lines
13 KiB
TypeScript
351 lines
13 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import { existsSync, readFileSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
import { describe, it } from 'node:test'
|
|
import {
|
|
agreeTermsOfUse,
|
|
applyCreateAccountServerError,
|
|
applyOnlineCharactersToFlowState,
|
|
backspaceCreateAccountChar,
|
|
cancelCreateAccount,
|
|
cancelPleaseRead,
|
|
createInitialFlowState,
|
|
cycleCreateAccountFocus,
|
|
disagreeTermsOfUse,
|
|
dismissPopupModal,
|
|
filterAccountName,
|
|
isValidAccountChar,
|
|
mapCreateAccountServerStatus,
|
|
openTermsOfUse,
|
|
scrollTermsOfUse,
|
|
submitCreateAccountForm,
|
|
typeCreateAccountChar,
|
|
validateNewAccountInputs,
|
|
} from '../src/client/frontend/flow-state.ts'
|
|
import { BncsCreateAccountStatus } from '../src/netproto/index.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')
|
|
|
|
function readPngDimensions(filePath: string): { readonly width: number; readonly height: number } {
|
|
const buf = readFileSync(filePath)
|
|
assert.equal(buf[0], 0x89, 'PNG signature byte 0')
|
|
assert.equal(buf[1], 0x50, 'PNG signature byte 1')
|
|
assert.equal(buf[2], 0x4e, 'PNG signature byte 2')
|
|
assert.equal(buf[3], 0x47, 'PNG signature byte 3')
|
|
return {
|
|
width: buf.readUInt32BE(16),
|
|
height: buf.readUInt32BE(20),
|
|
}
|
|
}
|
|
|
|
describe('Battle.net Create New Account Frontend Assets & Flow State (R1, R2, R4)', () => {
|
|
const manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf8')) as {
|
|
readonly strings: Record<string, unknown>
|
|
}
|
|
const strings = manifest.strings
|
|
|
|
it('R1: packs terms-of-use-bg.png (593x451), scrollbar.png (72x14), btn-popup-small.png (192x32), and canonical .tbl strings', () => {
|
|
const touBgPath = resolve(FRONTEND_DIR, 'terms-of-use-bg.png')
|
|
const scrollbarPath = resolve(FRONTEND_DIR, 'scrollbar.png')
|
|
const btnPopupPath = resolve(FRONTEND_DIR, 'btn-popup-small.png')
|
|
|
|
assert.equal(existsSync(touBgPath), true, 'terms-of-use-bg.png should exist')
|
|
assert.equal(existsSync(scrollbarPath), true, 'scrollbar.png should exist')
|
|
assert.equal(existsSync(btnPopupPath), true, 'btn-popup-small.png should exist')
|
|
|
|
const touPng = readPngDimensions(touBgPath)
|
|
assert.equal(touPng.width, 593)
|
|
assert.equal(touPng.height, 451)
|
|
|
|
const sbPng = readPngDimensions(scrollbarPath)
|
|
assert.equal(sbPng.width, 72)
|
|
assert.equal(sbPng.height, 14)
|
|
|
|
const btnPng = readPngDimensions(btnPopupPath)
|
|
assert.equal(btnPng.width, 192)
|
|
assert.equal(btnPng.height, 32)
|
|
|
|
assert.equal(strings['5180'], 'Terms of Use')
|
|
assert.equal(strings['5181'], 'AGREE')
|
|
assert.equal(strings['5182'], 'DISAGREE')
|
|
assert.equal(strings['5216'], 'Account name must be at least 2 characters.')
|
|
assert.equal(strings['5217'], "Account name can't be more than 15 characters.")
|
|
assert.equal(strings['5218'], "Passwords don't match.")
|
|
assert.equal(strings['5219'], 'Password must be at least 2 characters.')
|
|
assert.equal(strings['5220'], "Password can't be more than 15 characters.")
|
|
assert.equal(strings['5221'], 'CREATE NEW ACCOUNT')
|
|
assert.equal(strings['5222'], 'Create Battle.net Account')
|
|
assert.equal(
|
|
strings['5223'],
|
|
'Enter the Name and Password for your new battle.net account.',
|
|
)
|
|
assert.equal(strings['5224'], 'Account Name')
|
|
assert.equal(strings['5225'], 'Password')
|
|
assert.equal(strings['5226'], 'Verify Password')
|
|
assert.equal(strings['10834'], 'Verify Password')
|
|
assert.equal(strings['5227'], 'PLEASE READ')
|
|
assert.equal(strings['11127'], 'PLEASE WRITE DOWN YOUR PASSWORD.')
|
|
assert.equal(strings['5249'], 'Unable to Create Account')
|
|
assert.equal(
|
|
typeof strings.termsOfUseText === 'string' && strings.termsOfUseText.length > 500,
|
|
true,
|
|
'termsOfUseText from license.txt must be non-empty',
|
|
)
|
|
})
|
|
|
|
it('R2: transitions bnet_login -> bnet_terms_of_use, supports scrolling, DISAGREE -> bnet_login, and AGREE -> bnet_create_account', () => {
|
|
const state = createInitialFlowState()
|
|
state.stage = 'bnet_login'
|
|
state.accountName = 'HeroAcct'
|
|
state.accountPassword = 'oldpassword'
|
|
state.accountVerifyPassword = 'oldpassword'
|
|
|
|
openTermsOfUse(state)
|
|
assert.equal(state.stage, 'bnet_terms_of_use')
|
|
assert.equal(state.termsScrollLine, 0)
|
|
assert.equal(state.popupModal, null)
|
|
assert.equal(state.activeInput, null)
|
|
|
|
scrollTermsOfUse(state, 5, 20)
|
|
assert.equal(state.termsScrollLine, 5)
|
|
assert.equal(state.termsScrollOffset, 5)
|
|
|
|
scrollTermsOfUse(state, 50, 20)
|
|
assert.equal(state.termsScrollLine, 20)
|
|
|
|
scrollTermsOfUse(state, -100, 20)
|
|
assert.equal(state.termsScrollLine, 0)
|
|
|
|
disagreeTermsOfUse(state)
|
|
assert.equal(state.stage, 'bnet_login')
|
|
assert.equal(state.activeInput, 'accountPassword')
|
|
|
|
openTermsOfUse(state)
|
|
agreeTermsOfUse(state)
|
|
assert.equal(state.stage, 'bnet_create_account')
|
|
assert.equal(state.activeInput, 'accountName')
|
|
assert.equal(state.accountPassword, '')
|
|
assert.equal(state.accountVerifyPassword, '')
|
|
assert.equal(state.accountName, 'HeroAcct')
|
|
})
|
|
|
|
it('R2: cycles focus across the 3 fields with Tab / Shift+Tab and filters characters per 0x6fa52630', () => {
|
|
const state = createInitialFlowState()
|
|
openTermsOfUse(state)
|
|
agreeTermsOfUse(state)
|
|
state.accountName = ''
|
|
|
|
assert.equal(state.activeInput, 'accountName')
|
|
cycleCreateAccountFocus(state)
|
|
assert.equal(state.activeInput, 'accountPassword')
|
|
cycleCreateAccountFocus(state)
|
|
assert.equal(state.activeInput, 'accountVerifyPassword')
|
|
cycleCreateAccountFocus(state)
|
|
assert.equal(state.activeInput, 'accountName')
|
|
|
|
cycleCreateAccountFocus(state, true)
|
|
assert.equal(state.activeInput, 'accountVerifyPassword')
|
|
cycleCreateAccountFocus(state, true)
|
|
assert.equal(state.activeInput, 'accountPassword')
|
|
cycleCreateAccountFocus(state, true)
|
|
assert.equal(state.activeInput, 'accountName')
|
|
|
|
// Verify 0x6fa52630 character filter (rejects <= 0x20, >= 0x7f, and " # % * , / < > ? \)
|
|
for (const allowed of ['A', 'z', '0', '9', '-', '_', '.', '[', ']', ';', '(', ')']) {
|
|
assert.equal(isValidAccountChar(allowed), true, `Expected '${allowed}' to be valid`)
|
|
}
|
|
for (const forbidden of [
|
|
' ',
|
|
'\t',
|
|
'\n',
|
|
'"',
|
|
'#',
|
|
'%',
|
|
'*',
|
|
',',
|
|
'/',
|
|
'<',
|
|
'>',
|
|
'?',
|
|
'\\',
|
|
'\x7f',
|
|
'é',
|
|
]) {
|
|
assert.equal(isValidAccountChar(forbidden), false, `Expected '${forbidden}' to be rejected`)
|
|
}
|
|
|
|
assert.equal(filterAccountName('Hero_1#bad/name*234567890'), 'Hero_1badname23')
|
|
|
|
// Typing into accountName ignores invalid chars and caps at 15 chars
|
|
for (const ch of 'My_Hero#*.[1]23456789') {
|
|
typeCreateAccountChar(state, ch)
|
|
}
|
|
assert.equal(state.accountName, 'My_Hero.[1]2345')
|
|
assert.equal(state.accountName.length, 15)
|
|
|
|
backspaceCreateAccountChar(state)
|
|
assert.equal(state.accountName, 'My_Hero.[1]234')
|
|
})
|
|
|
|
it('R2: enforces all 5 client-side validation error popups (5216..5220) in exact 0x6fa59880 priority order and dismisses modal', () => {
|
|
const state = createInitialFlowState()
|
|
openTermsOfUse(state)
|
|
agreeTermsOfUse(state)
|
|
|
|
// 1. Account name < 2 chars -> 5216
|
|
state.accountName = 'a'
|
|
state.accountPassword = 'secret'
|
|
state.accountVerifyPassword = 'secret'
|
|
assert.equal(submitCreateAccountForm(state, strings), false)
|
|
assert.equal(state.stage, 'bnet_create_account')
|
|
assert.equal(state.popupModal?.message, 'Account name must be at least 2 characters.')
|
|
|
|
dismissPopupModal(state)
|
|
assert.equal(Boolean(state.popupModal), false)
|
|
|
|
// 2. Account name > 15 chars -> 5217
|
|
state.accountName = '1234567890123456'
|
|
assert.equal(submitCreateAccountForm(state, strings), false)
|
|
assert.equal(state.popupModal?.message, "Account name can't be more than 15 characters.")
|
|
dismissPopupModal(state)
|
|
assert.equal(Boolean(state.popupModal), false)
|
|
|
|
// 3. Passwords don't match (case-insensitive _stricmp) -> 5218
|
|
// Note: checked BEFORE password length < 2 per 0x6fa59880!
|
|
state.accountName = 'ValidHero'
|
|
state.accountPassword = 'a'
|
|
state.accountVerifyPassword = 'b'
|
|
assert.equal(submitCreateAccountForm(state, strings), false)
|
|
assert.equal(state.popupModal?.message, "Passwords don't match.")
|
|
dismissPopupModal(state)
|
|
assert.equal(Boolean(state.popupModal), false)
|
|
|
|
// 4. Password < 2 chars (when matching) -> 5219
|
|
state.accountPassword = 'a'
|
|
state.accountVerifyPassword = 'A' // matches case-insensitively!
|
|
assert.equal(submitCreateAccountForm(state, strings), false)
|
|
assert.equal(state.popupModal?.message, 'Password must be at least 2 characters.')
|
|
dismissPopupModal(state)
|
|
assert.equal(Boolean(state.popupModal), false)
|
|
|
|
// 5. Password > 15 chars (when matching) -> 5220
|
|
state.accountPassword = '1234567890123456'
|
|
state.accountVerifyPassword = '1234567890123456'
|
|
const check5220 = validateNewAccountInputs(state, strings)
|
|
assert.equal(check5220.valid, false)
|
|
if (!check5220.valid) {
|
|
assert.equal(check5220.tblId, 5220)
|
|
assert.equal(check5220.message, "Password can't be more than 15 characters.")
|
|
}
|
|
assert.equal(submitCreateAccountForm(state, strings), false)
|
|
assert.equal(state.popupModal?.message, "Password can't be more than 15 characters.")
|
|
dismissPopupModal(state)
|
|
assert.equal(Boolean(state.popupModal), false)
|
|
|
|
// 6. Valid inputs (including case-insensitive password match) -> advances to bnet_please_read
|
|
state.accountName = 'ValidHero'
|
|
state.accountPassword = 'Secret123'
|
|
state.accountVerifyPassword = 'secret123'
|
|
assert.equal(submitCreateAccountForm(state, strings), true)
|
|
assert.equal(state.stage, 'bnet_please_read')
|
|
assert.equal(Boolean(state.popupModal), false)
|
|
|
|
// Cancel on PLEASE READ returns to bnet_login
|
|
cancelPleaseRead(state)
|
|
assert.equal(state.stage, 'bnet_login')
|
|
|
|
// Exit on Create Account also returns to bnet_login
|
|
openTermsOfUse(state)
|
|
agreeTermsOfUse(state)
|
|
cancelCreateAccount(state)
|
|
assert.equal(state.stage, 'bnet_login')
|
|
})
|
|
|
|
it('R4: maps all server-side BncsCreateAccountStatus codes (0x02..0x08 + default) to 5231..5239 with title 5249 and returns to bnet_create_account', () => {
|
|
const cases: Array<{
|
|
readonly status: number
|
|
readonly expectedStrId: number
|
|
readonly expectedSubstr: string
|
|
}> = [
|
|
{
|
|
status: BncsCreateAccountStatus.INVALID_CHARS, // 0x02
|
|
expectedStrId: 5232,
|
|
expectedSubstr: 'illegal characters',
|
|
},
|
|
{
|
|
status: BncsCreateAccountStatus.BANNED_WORD, // 0x03
|
|
expectedStrId: 5233,
|
|
expectedSubstr: 'disallowed words',
|
|
},
|
|
{
|
|
status: BncsCreateAccountStatus.ALREADY_EXISTS, // 0x04
|
|
expectedStrId: 5239,
|
|
expectedSubstr: 'already exists',
|
|
},
|
|
{
|
|
status: BncsCreateAccountStatus.STILL_PROCESSING, // 0x05
|
|
expectedStrId: 5234,
|
|
expectedSubstr: 'still being processed',
|
|
},
|
|
{
|
|
status: BncsCreateAccountStatus.NOT_ENOUGH_ALPHANUM, // 0x06
|
|
expectedStrId: 5235,
|
|
expectedSubstr: 'alpha-numeric characters',
|
|
},
|
|
{
|
|
status: BncsCreateAccountStatus.ADJACENT_PUNCT, // 0x07
|
|
expectedStrId: 5236,
|
|
expectedSubstr: 'adjacent punctuation',
|
|
},
|
|
{
|
|
status: BncsCreateAccountStatus.TOO_MANY_PUNCT, // 0x08
|
|
expectedStrId: 5237,
|
|
expectedSubstr: 'too many punctuation',
|
|
},
|
|
{
|
|
status: BncsCreateAccountStatus.TOO_SHORT, // 0x01 (default branch in 0x6fa58e40)
|
|
expectedStrId: 5231,
|
|
expectedSubstr: 'at least two characters',
|
|
},
|
|
{
|
|
status: 0x99, // unknown non-zero status -> default branch 5231
|
|
expectedStrId: 5231,
|
|
expectedSubstr: 'at least two characters',
|
|
},
|
|
]
|
|
|
|
for (const c of cases) {
|
|
const state = createInitialFlowState()
|
|
state.stage = 'bnet_please_read'
|
|
state.accountName = 'TakenHero'
|
|
state.accountPassword = 'secret'
|
|
state.accountVerifyPassword = 'secret'
|
|
|
|
const mapped = mapCreateAccountServerStatus(c.status, strings)
|
|
assert.equal(mapped.tblId, c.expectedStrId)
|
|
assert.equal(mapped.title, 'Unable to Create Account')
|
|
assert.equal(
|
|
mapped.message.includes(c.expectedSubstr),
|
|
true,
|
|
`Status 0x${c.status.toString(16)} message "${mapped.message}" should include "${c.expectedSubstr}"`,
|
|
)
|
|
|
|
applyCreateAccountServerError(state, c.status, strings)
|
|
assert.equal(state.stage, 'bnet_create_account')
|
|
assert.equal(state.accountName, 'TakenHero', 'accountName must be preserved on server error')
|
|
assert.equal(state.popupModal?.title, 'Unable to Create Account')
|
|
assert.equal(state.popupModal?.message, mapped.message)
|
|
}
|
|
})
|
|
|
|
it('R4: applyOnlineCharactersToFlowState with stayOnCharSelect keeps empty new account on char_select', () => {
|
|
const state = createInitialFlowState()
|
|
state.stage = 'bnet_please_read'
|
|
applyOnlineCharactersToFlowState(state, [], { stayOnCharSelect: true })
|
|
assert.equal(state.stage, 'char_select')
|
|
assert.equal(state.roster.length, 0)
|
|
})
|
|
})
|