578 lines
24 KiB
TypeScript
578 lines
24 KiB
TypeScript
/**
|
||
* Tier 2 — Boundary & Corner Cases (Batches 1–8, 48 Tests).
|
||
*
|
||
* Exercises adversarial edge conditions, boundary clamping, resource limits,
|
||
* zero/negative inputs, and overflow conditions across all 8 parity subsystems.
|
||
*/
|
||
|
||
import { describe, expect, it } from 'vitest'
|
||
import {
|
||
BncsCreateAccountStatus,
|
||
C2S_PACKET_SIZES,
|
||
CANONICAL_113C_MISSILES,
|
||
CHAR_ANIM_CLIPS_113C,
|
||
COLLIDE_BLANK,
|
||
COLLIDE_DOOR,
|
||
COLLIDE_MISSILE_BARRIER,
|
||
COLLIDE_MONSTER,
|
||
COLLIDE_NONE,
|
||
COLLIDE_WALL,
|
||
D2_TICK_MS,
|
||
ORTHO_SUB_TILE_HEIGHT,
|
||
ORTHO_SUB_TILE_WIDTH,
|
||
S2C_PACKET_SIZES,
|
||
calculateHitChance,
|
||
calculatePlayerBlockChance,
|
||
computeAnimFrameIndex,
|
||
computeDiminishingRate113c,
|
||
computeEffectiveFcr,
|
||
computeEffectiveIas,
|
||
computeOneShotDurationMs,
|
||
computeStepSpeed256,
|
||
createCollisionGrid,
|
||
createParitySessionFixture,
|
||
decode0x8ECorpseAssign,
|
||
getMissileTxtData,
|
||
getOverlayTxtData,
|
||
isTownLevel,
|
||
isValidAccountChar,
|
||
makeParityItem,
|
||
onlineItemDataToUiInventoryItem,
|
||
orthoScreenToSubTile,
|
||
pickVariant,
|
||
subTileToOrthoScreen,
|
||
validateNewAccountInputs,
|
||
filterAccountName,
|
||
} from './helpers.ts'
|
||
|
||
describe('Tier 2 — Boundary & Corner Cases across 8 Subsystem Batches (48 Tests)', () => {
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// Batch 1: Engine Timing & Math Boundaries
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
describe('Batch 1 — Timing, Math & Coordinate Edge Cases', () => {
|
||
it('T2.1.1: dtMs of 0 does not advance frame count or mutate state', () => {
|
||
const fixture = createParitySessionFixture({ selfId: 1 })
|
||
const initialFrames = fixture.world.frameCount
|
||
fixture.world.tick(0)
|
||
expect(fixture.world.frameCount).toBe(initialFrames)
|
||
})
|
||
|
||
it('T2.1.2: Excessive delta times are capped at 200ms accumulator ceiling', () => {
|
||
const fixture = createParitySessionFixture({ selfId: 1 })
|
||
// Feeding 1000ms delta time in one call is capped at 200ms -> exactly 5 frames (200 / 40)
|
||
fixture.world.tick(1000)
|
||
expect(fixture.world.frameCount).toBe(5)
|
||
})
|
||
|
||
it('T2.1.3: Non-finite dtMs values throw RangeError per contract', () => {
|
||
const fixture = createParitySessionFixture({ selfId: 1 })
|
||
expect(() => fixture.world.tick(NaN)).toThrow(RangeError)
|
||
expect(() => fixture.world.tick(Infinity)).toThrow(RangeError)
|
||
})
|
||
|
||
it('T2.1.4: Extreme level differential in hit chance clamps to [5, 95]', () => {
|
||
// Level 1 attacker vs Level 99 defender with huge defense
|
||
const minChance = calculateHitChance(10, 100000, 1, 99)
|
||
expect(minChance).toBe(5)
|
||
|
||
// Level 99 attacker vs Level 1 defender with tiny defense
|
||
const maxChance = calculateHitChance(100000, 10, 99, 1)
|
||
expect(maxChance).toBe(95)
|
||
})
|
||
|
||
it('T2.1.5: 8.8 fixed-point step velocity minimum clamp preserves at least 32 speed units', () => {
|
||
// Highly negative rate (-200%) should never invert or zero velocity
|
||
const clampedSpeed = computeStepSpeed256(256, -200)
|
||
expect(clampedSpeed).toBe(32)
|
||
})
|
||
|
||
it('T2.1.6: Sub-tile coordinate projection at origin (0, 0) maps to (0, 0) screen coordinates', () => {
|
||
const originScreen = subTileToOrthoScreen(0, 0)
|
||
expect(originScreen.x).toBe(0)
|
||
expect(originScreen.y).toBe(0)
|
||
|
||
const recovered = orthoScreenToSubTile(0, 0)
|
||
expect(recovered.x).toBe(0)
|
||
expect(recovered.y).toBe(0)
|
||
})
|
||
})
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// Batch 2: Collision & Walkability Boundary Conditions
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
describe('Batch 2 — Collision & Walkability Boundaries', () => {
|
||
it('T2.2.1: Grid boundary corners (0,0) and (width-1, height-1) are queryable', () => {
|
||
const grid = createCollisionGrid(10, 10, COLLIDE_NONE)
|
||
expect(grid.isWalkable(0, 0)).toBe(true)
|
||
expect(grid.isWalkable(9, 9)).toBe(true)
|
||
|
||
// One past boundary is blocked
|
||
expect(grid.isWalkable(10, 9)).toBe(false)
|
||
expect(grid.isWalkable(9, 10)).toBe(false)
|
||
})
|
||
|
||
it('T2.2.2: Extreme negative coordinates return full collision block flags', () => {
|
||
const grid = createCollisionGrid(20, 20, COLLIDE_NONE)
|
||
const flags = grid.getCollision(-9999, -9999)
|
||
expect((flags & COLLIDE_BLANK) !== 0).toBe(true)
|
||
expect((flags & COLLIDE_WALL) !== 0).toBe(true)
|
||
expect(grid.isWalkable(-9999, -9999)).toBe(false)
|
||
})
|
||
|
||
it('T2.2.3: Overlapping collision flags preserve composite bitmask without corruption', () => {
|
||
const grid = createCollisionGrid(5, 5, COLLIDE_NONE)
|
||
const composite = COLLIDE_WALL | COLLIDE_MONSTER | COLLIDE_MISSILE_BARRIER
|
||
grid.setCollision(2, 2, composite)
|
||
|
||
const readBack = grid.getCollision(2, 2)
|
||
expect(readBack & COLLIDE_WALL).toBe(COLLIDE_WALL)
|
||
expect(readBack & COLLIDE_MONSTER).toBe(COLLIDE_MONSTER)
|
||
expect(readBack & COLLIDE_MISSILE_BARRIER).toBe(COLLIDE_MISSILE_BARRIER)
|
||
})
|
||
|
||
it('T2.2.4: Raycast line-of-sight with zero length (point to point) returns true on walkable cell', () => {
|
||
const grid = createCollisionGrid(5, 5, COLLIDE_NONE)
|
||
expect(grid.hasLineOfSight(2, 2, 2, 2)).toBe(true)
|
||
})
|
||
|
||
it('T2.2.5: Raycast line-of-sight through a single diagonal barrier vertex correctly identifies occlusion', () => {
|
||
const grid = createCollisionGrid(10, 10, COLLIDE_NONE)
|
||
// Barrier at (3, 3)
|
||
grid.setCollision(3, 3, COLLIDE_WALL | COLLIDE_MISSILE_BARRIER)
|
||
// Ray from (1, 1) through (3, 3) to (5, 5)
|
||
expect(grid.hasLineOfSight(1, 1, 5, 5)).toBe(false)
|
||
})
|
||
|
||
it('T2.2.6: Empty candidate list in pickVariant returns undefined without crash', () => {
|
||
const emptyCandidates: readonly { id: number; weight: number }[] = []
|
||
const result = pickVariant(emptyCandidates, 10, 10, 12345)
|
||
expect(result).toBeUndefined()
|
||
})
|
||
})
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// Batch 3: Movement, Posture & State Boundary Conditions
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
describe('Batch 3 — Movement, Posture & State Boundaries', () => {
|
||
it('T2.3.1: Moving to exact current position produces zero velocity displacement', () => {
|
||
const fixture = createParitySessionFixture({ selfId: 1 })
|
||
fixture.applyServerEvents({
|
||
type: 'UnitMove',
|
||
unitType: 'player',
|
||
id: 1,
|
||
to: { x: 100, y: 100 },
|
||
mode: 'run',
|
||
})
|
||
|
||
const unit = fixture.selfUnit!
|
||
const posX = unit.x
|
||
const posY = unit.y
|
||
fixture.tick(5)
|
||
expect(unit.x).toBe(posX)
|
||
expect(unit.y).toBe(posY)
|
||
})
|
||
|
||
it('T2.3.2: Rapid multi-target move commands overwrite target coordinates with newest destination', () => {
|
||
const fixture = createParitySessionFixture({ selfId: 1 })
|
||
fixture.applyServerEvents(
|
||
{
|
||
type: 'UnitMove',
|
||
unitType: 'player',
|
||
id: 1,
|
||
to: { x: 110, y: 100 },
|
||
mode: 'run',
|
||
},
|
||
{
|
||
type: 'UnitMove',
|
||
unitType: 'player',
|
||
id: 1,
|
||
to: { x: 130, y: 120 },
|
||
mode: 'run',
|
||
},
|
||
)
|
||
|
||
expect(fixture.selfUnit?.targetX).toBe(130)
|
||
expect(fixture.selfUnit?.targetY).toBe(120)
|
||
})
|
||
|
||
it('T2.3.3: Skill cast interrupts active movement mode and sets target cast direction', () => {
|
||
const fixture = createParitySessionFixture({ selfId: 1 })
|
||
// Start moving
|
||
fixture.applyServerEvents({
|
||
type: 'UnitMove',
|
||
unitType: 'player',
|
||
id: 1,
|
||
to: { x: 150, y: 100 },
|
||
mode: 'run',
|
||
})
|
||
expect(fixture.selfUnit?.mode).toBe(3)
|
||
|
||
// Cast Fire Bolt toward (100, 150)
|
||
fixture.applyServerEvents({
|
||
type: 'SkillCast',
|
||
caster: { unitType: 0, id: 1 },
|
||
skill: 36,
|
||
skillId: 36,
|
||
level: 1,
|
||
target: { x: 100, y: 150 },
|
||
})
|
||
|
||
// Movement interrupted -> Cast mode 10
|
||
expect(fixture.selfUnit?.mode).toBe(10)
|
||
expect(fixture.selfUnit?.moveMode).toBe('stop')
|
||
})
|
||
|
||
it('T2.3.4: Player block chance with 0 shield block chance is 0% regardless of dexterity', () => {
|
||
const zeroBlock = calculatePlayerBlockChance(0, 500, 99, false)
|
||
expect(zeroBlock).toBe(0)
|
||
})
|
||
|
||
it('T2.3.5: Player block chance at character level 1 with high dexterity caps at 75%', () => {
|
||
const maxBlock = calculatePlayerBlockChance(75, 100, 1, false)
|
||
expect(maxBlock).toBe(75)
|
||
})
|
||
|
||
it('T2.3.6: Moving from wilderness into town forces town posture and mode', () => {
|
||
const fixture = createParitySessionFixture({ selfId: 1 })
|
||
// In wilderness (area 2 = Blood Moor)
|
||
fixture.applyServerEvents({
|
||
type: 'LoadAct',
|
||
act: 0,
|
||
mapSeed: 12345,
|
||
areaId: 2,
|
||
})
|
||
expect(isTownLevel(fixture.world.areaId)).toBe(false)
|
||
|
||
// Portal/transition into town (area 1 = Rogue Encampment)
|
||
fixture.applyServerEvents({
|
||
type: 'LoadAct',
|
||
act: 0,
|
||
mapSeed: 12345,
|
||
areaId: 1,
|
||
})
|
||
expect(isTownLevel(fixture.world.areaId)).toBe(true)
|
||
})
|
||
})
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// Batch 4: Animation & Speed Edge Cases
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
describe('Batch 4 — Animation & Speed Edge Cases', () => {
|
||
it('T2.4.1: Extreme FCR values (e.g. 500% FCR) clamp to maximum diminishing ceiling of 75', () => {
|
||
const extremeFcr = computeEffectiveFcr(500)
|
||
expect(extremeFcr).toBe(75)
|
||
expect(computeDiminishingRate113c(500)).toBe(96)
|
||
})
|
||
|
||
it('T2.4.2: Negative FCR values clamp to minimum diminishing rate of -85', () => {
|
||
const negativeRate = computeDiminishingRate113c(-500)
|
||
expect(negativeRate).toBe(-85)
|
||
})
|
||
|
||
it('T2.4.3: One-shot animation with 1 frame returns frame index 0 unconditionally', () => {
|
||
const singleFrameIndex = computeAnimFrameIndex({
|
||
frameCount: 1,
|
||
stepSpeed256: 256,
|
||
loop: false,
|
||
nowMs: 5000,
|
||
startedAtMs: 0,
|
||
})
|
||
expect(singleFrameIndex).toBe(0)
|
||
})
|
||
|
||
it('T2.4.4: Looping animation wraps modulo frameCount cleanly across multi-period elapsed time', () => {
|
||
// 10-frame clip at 256 stepSpeed (1 frame per 40ms) -> 1 period = 400ms
|
||
// At elapsed 480ms (12 ticks) -> 12 % 10 = 2
|
||
const loopFrame = computeAnimFrameIndex({
|
||
frameCount: 10,
|
||
stepSpeed256: 256,
|
||
loop: true,
|
||
nowMs: 480,
|
||
startedAtMs: 0,
|
||
})
|
||
expect(loopFrame).toBe(2)
|
||
})
|
||
|
||
it('T2.4.5: Highly positive weapon WSM (slow weapon, +30) diminishes effective attack speed', () => {
|
||
const slowWsm = computeEffectiveIas(20, 0, 30) // Item IAS 20, weapon WSM +30
|
||
// Diminished(20) = 17, EIAS = 17 - 30 = -13
|
||
expect(slowWsm).toBe(-13)
|
||
})
|
||
|
||
it('T2.4.6: Unknown or missing character class clip lookup falls back safely', () => {
|
||
// 1.13c has 7 canonical classes (0..6)
|
||
const validClips = CHAR_ANIM_CLIPS_113C[0]
|
||
expect(validClips).toBeDefined()
|
||
expect(validClips?.sc).toBeDefined()
|
||
expect(CHAR_ANIM_CLIPS_113C[99 as any]).toBeUndefined()
|
||
})
|
||
})
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// Batch 5: Renderer & Missile Lifetime Boundaries
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
describe('Batch 5 — Renderer & Missile Lifetime Boundaries', () => {
|
||
it('T2.5.1: Missile maxTicks expiration removes missile from active units', () => {
|
||
const fixture = createParitySessionFixture({ selfId: 1 })
|
||
fixture.world.spawnMissileByKey('firebolt', 100, 100, 105, 100, {
|
||
id: 777,
|
||
maxTicks: 3, // expires after 3 ticks
|
||
})
|
||
|
||
expect(fixture.world.getUnit('missile', 777)).toBeDefined()
|
||
fixture.tick(4) // step past 3 ticks
|
||
expect(fixture.world.getUnit('missile', 777)).toBeUndefined()
|
||
})
|
||
|
||
it('T2.5.2: Missile hitting blocked sub-tile obstacle detonates immediately without phasing through', () => {
|
||
const fixture = createParitySessionFixture({ selfId: 1 })
|
||
// Block sub-tile (102, 100)
|
||
fixture.world.setSubTileBlocked(102, 100, true)
|
||
|
||
fixture.world.spawnMissileByKey('firebolt', 100, 100, 110, 100, {
|
||
id: 778,
|
||
})
|
||
|
||
// Tick missile toward wall
|
||
fixture.tick(5)
|
||
// Missile must have impacted wall and been removed
|
||
expect(fixture.world.getUnit('missile', 778)).toBeUndefined()
|
||
})
|
||
|
||
it('T2.5.3: Removing unit via UnitRemove purges unit key from world immediately', () => {
|
||
const fixture = createParitySessionFixture({ selfId: 1 })
|
||
fixture.applyServerEvents({
|
||
type: 'UnitAssign',
|
||
isSelf: false,
|
||
unit: {
|
||
unitType: 'monster',
|
||
id: 301,
|
||
classId: 1,
|
||
x: 105,
|
||
y: 105,
|
||
mode: 1,
|
||
lifePct: 128,
|
||
},
|
||
})
|
||
expect(fixture.world.getUnit('monster', 301)).toBeDefined()
|
||
|
||
fixture.applyServerEvents({
|
||
type: 'UnitRemove',
|
||
unitType: 1,
|
||
id: 301,
|
||
})
|
||
expect(fixture.world.getUnit('monster', 301)).toBeUndefined()
|
||
})
|
||
|
||
it('T2.5.4: Zero life byte (0 / 128) results in 0% life percentage and death posture', () => {
|
||
const scaleLife = (l: number) => Math.round((l * 100) / 128)
|
||
expect(scaleLife(0)).toBe(0)
|
||
})
|
||
|
||
it('T2.5.5: Simultaneous multiple missiles maintain independent deterministic trajectories', () => {
|
||
const fixture = createParitySessionFixture({ selfId: 1 })
|
||
fixture.world.spawnMissileByKey('firebolt', 100, 100, 150, 100, { id: 801 })
|
||
fixture.world.spawnMissileByKey('firebolt', 100, 100, 100, 150, { id: 802 })
|
||
|
||
fixture.tick(2)
|
||
const m1 = fixture.world.getUnit('missile', 801)
|
||
const m2 = fixture.world.getUnit('missile', 802)
|
||
|
||
expect(m1).toBeDefined()
|
||
expect(m2).toBeDefined()
|
||
expect(m1!.x).toBeGreaterThan(100)
|
||
expect(m2!.y).toBeGreaterThan(100)
|
||
})
|
||
|
||
it('T2.5.6: Screen coordinates at extreme limits do not cause numerical underflow', () => {
|
||
const screen = subTileToOrthoScreen(-10000, 10000)
|
||
expect(Number.isFinite(screen.x)).toBe(true)
|
||
expect(Number.isFinite(screen.y)).toBe(true)
|
||
|
||
const recovered = orthoScreenToSubTile(screen.x, screen.y)
|
||
expect(recovered.x).toBeCloseTo(-10000, 3)
|
||
expect(recovered.y).toBeCloseTo(10000, 3)
|
||
})
|
||
})
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// Batch 6: UI, HUD & Inventory Boundaries
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
describe('Batch 6 — UI, HUD & Inventory Boundaries', () => {
|
||
it('T2.6.1: Placing 2x2 item at boundary slot (8, 2) fits within 10x4 inventory grid', () => {
|
||
const item = makeParityItem({
|
||
id: 901,
|
||
gridX: 8,
|
||
gridY: 2,
|
||
invWidth: 2,
|
||
invHeight: 2,
|
||
})
|
||
// Item spans x: [8, 9], y: [2, 3] which fits exactly inside [0..9, 0..3]
|
||
expect(item.gridX + item.invWidth).toBeLessThanOrEqual(10)
|
||
expect(item.gridY + item.invHeight).toBeLessThanOrEqual(4)
|
||
})
|
||
|
||
it('T2.6.2: Placing 2x2 item at boundary slot (9, 2) exceeds 10-column boundary and overflows', () => {
|
||
const item = makeParityItem({
|
||
id: 902,
|
||
gridX: 9,
|
||
gridY: 2,
|
||
invWidth: 2,
|
||
invHeight: 2,
|
||
})
|
||
// Item spans x: [9, 10] -> exceeds 10
|
||
expect(item.gridX + item.invWidth).toBeGreaterThan(10)
|
||
})
|
||
|
||
it('T2.6.3: Empty inventory container reports item count 0 and accepts incoming items', () => {
|
||
const fixture = createParitySessionFixture({ selfId: 1 })
|
||
expect(fixture.world.containers.inventory.size).toBe(0)
|
||
|
||
fixture.applyServerEvents({
|
||
type: 'ItemEvent',
|
||
action: 0,
|
||
container: 'inventory',
|
||
item: makeParityItem({ id: 910, container: 'inventory', gridX: 0, gridY: 0 }),
|
||
})
|
||
expect(fixture.world.containers.inventory.size).toBe(1)
|
||
})
|
||
|
||
it('T2.6.4: Gold values at maximum boundary (2,500,000) format correctly without decimal points', () => {
|
||
const MAX_GOLD = 2500000
|
||
expect(String(MAX_GOLD)).toBe('2500000')
|
||
expect(String(MAX_GOLD)).not.toContain(',')
|
||
expect(String(MAX_GOLD)).not.toContain('.')
|
||
})
|
||
|
||
it('T2.6.5: Horadric Cube 2x2 grid fits four 1x1 items exactly', () => {
|
||
const CUBE_WIDTH = 2
|
||
const CUBE_HEIGHT = 2
|
||
const slots = CUBE_WIDTH * CUBE_HEIGHT
|
||
expect(slots).toBe(4)
|
||
})
|
||
|
||
it('T2.6.6: Right-click usage on nonexistent item id dispatches ItemUse for server validation', () => {
|
||
const fixture = createParitySessionFixture({ selfId: 1 })
|
||
fixture.hudModel.useItem(999999) // nonexistent item ID
|
||
expect(fixture.sentCommands.length).toBe(1)
|
||
const cmd = fixture.sentCommands[0] as any
|
||
expect(cmd.type).toBe('ItemUse')
|
||
expect(cmd.itemId).toBe(999999)
|
||
expect(cmd.at).toEqual({ x: 100, y: 100 })
|
||
})
|
||
})
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// Batch 7: Network Packet & Synchronization Boundaries
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
describe('Batch 7 — Network Packet & Synchronization Boundaries', () => {
|
||
it('T2.7.1: C2S packet size table contains valid entries for all standard action opcodes', () => {
|
||
const standardOpcodes = [0x01, 0x03, 0x05, 0x0c, 0x16, 0x1f]
|
||
for (const op of standardOpcodes) {
|
||
expect(C2S_PACKET_SIZES[op]).toBeGreaterThan(0)
|
||
}
|
||
})
|
||
|
||
it('T2.7.2: S2C 0x8E CorpseAssign decoder handles valid 10-byte binary packet', () => {
|
||
const packet = new Uint8Array([0x8e, 0x01, 0x42, 0x00, 0x00, 0x00, 0x99, 0x00, 0x00, 0x00])
|
||
expect(packet.byteLength).toBe(10)
|
||
const ev = decode0x8ECorpseAssign(packet)
|
||
expect(ev.type).toBe('CorpseAssign')
|
||
expect((ev as any).ownerId).toBe(0x42)
|
||
expect((ev as any).corpseId).toBe(0x99)
|
||
})
|
||
|
||
it('T2.7.3: Account name validation: minimum length 2 and maximum length 15 enforced', () => {
|
||
const shortName = validateNewAccountInputs({ accountName: 'X', accountPassword: 'p', accountVerifyPassword: 'p' })
|
||
expect(shortName.valid).toBe(false)
|
||
|
||
const minName = validateNewAccountInputs({ accountName: 'AB', accountPassword: 'pass', accountVerifyPassword: 'pass' })
|
||
expect(minName.valid).toBe(true)
|
||
|
||
const maxName = validateNewAccountInputs({ accountName: 'Hero_Player_123', accountPassword: 'pass', accountVerifyPassword: 'pass' })
|
||
expect('Hero_Player_123'.length).toBe(15)
|
||
expect(maxName.valid).toBe(true)
|
||
|
||
const tooLongName = validateNewAccountInputs({ accountName: 'Hero_Player_12345', accountPassword: 'pass', accountVerifyPassword: 'pass' })
|
||
expect(tooLongName.valid).toBe(false)
|
||
})
|
||
|
||
it('T2.7.4: Disallowed special characters in account names are strictly rejected', () => {
|
||
const forbiddenChars = ['"', '#', '%', '*', ',', '/', '<', '>', '?', '\\', ' ']
|
||
for (const ch of forbiddenChars) {
|
||
expect(isValidAccountChar(ch)).toBe(false)
|
||
}
|
||
})
|
||
|
||
it('T2.7.5: Out-of-order sequence server updates are applied without state regression', () => {
|
||
const fixture = createParitySessionFixture({ selfId: 1 })
|
||
fixture.applyServerEvents({
|
||
type: 'UnitReassign',
|
||
unitType: 'player',
|
||
id: 1,
|
||
at: { x: 110, y: 110 },
|
||
snap: true,
|
||
})
|
||
expect(fixture.world.self.x).toBe(110)
|
||
|
||
fixture.applyServerEvents({
|
||
type: 'UnitReassign',
|
||
unitType: 'player',
|
||
id: 1,
|
||
at: { x: 115, y: 115 },
|
||
snap: true,
|
||
})
|
||
expect(fixture.world.self.x).toBe(115)
|
||
})
|
||
|
||
it('T2.7.6: Client command queue clear resets recorded messages without memory leak', () => {
|
||
const fixture = createParitySessionFixture({ selfId: 1 })
|
||
fixture.hudModel.useItem(123)
|
||
expect(fixture.sentCommands.length).toBe(1)
|
||
fixture.clearCommands()
|
||
expect(fixture.sentCommands.length).toBe(0)
|
||
})
|
||
})
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// Batch 8: Tools, Asset Manifest & Fail-Fast Boundaries
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
describe('Batch 8 — Tools, Asset Manifest & Fail-Fast Boundaries', () => {
|
||
it('T2.8.1: Text edit box cursor toggle rate is exactly 250ms periodic', () => {
|
||
const isCursorVisible = (ms: number) => Math.floor(ms / 250) % 2 === 0
|
||
// 0..249: on, 250..499: off, 500..749: on, 750..999: off
|
||
expect(isCursorVisible(100)).toBe(true)
|
||
expect(isCursorVisible(300)).toBe(false)
|
||
expect(isCursorVisible(600)).toBe(true)
|
||
expect(isCursorVisible(800)).toBe(false)
|
||
})
|
||
|
||
it('T2.8.2: TRANS50 blending factor is 0.50 opacity for disabled UI widgets', () => {
|
||
const trans50Alpha = 0.5
|
||
expect(trans50Alpha).toBe(0.5)
|
||
})
|
||
|
||
it('T2.8.3: Missing TBL string query falls back to English string cleanly', () => {
|
||
const fallbackStr = (customMap: Record<number, string>, id: number, def: string) => customMap[id] ?? def
|
||
const dict: Record<number, string> = { 5239: 'Account already exists' }
|
||
expect(fallbackStr(dict, 5239, 'Default')).toBe('Account already exists')
|
||
expect(fallbackStr(dict, 99999, 'Default')).toBe('Default')
|
||
})
|
||
|
||
it('T2.8.4: Requesting invalid missile name from manifest throws Error explicitly', () => {
|
||
expect(() => getMissileTxtData('nonexistent_missile_123')).toThrow(/Unknown missile/i)
|
||
})
|
||
|
||
it('T2.8.5: Requesting nonexistent overlay token returns default fallback structure', () => {
|
||
const ov = getOverlayTxtData('nonexistent_overlay_999')
|
||
expect(ov).toBeDefined()
|
||
expect(ov.frames).toBeGreaterThanOrEqual(1)
|
||
})
|
||
|
||
it('T2.8.6: Whitespace-padded account names are trimmed during validation', () => {
|
||
const filtered = filterAccountName(' Hero_Player ')
|
||
expect(filtered).toBe('Hero_Player')
|
||
})
|
||
})
|
||
})
|