493 lines
21 KiB
TypeScript
493 lines
21 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
parseItemStats,
|
|
parseItem,
|
|
parseItems,
|
|
parseItemDescription,
|
|
parseItemFull
|
|
} from '../src/item.js'
|
|
|
|
describe('parseItemStats', () => {
|
|
describe('basic stats', () => {
|
|
it('should parse Move Speed', () => {
|
|
// Boots (id: 1001)
|
|
const description =
|
|
'<mainText><stats><attention> 25</attention> Move Speed</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.moveSpeed).toBe(25)
|
|
})
|
|
|
|
it('should parse Attack Damage', () => {
|
|
// Long Sword (id: 1036)
|
|
const description =
|
|
'<mainText><stats><attention> 10</attention> Attack Damage</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.attackDamage).toBe(10)
|
|
})
|
|
|
|
it('should parse Ability Power', () => {
|
|
// Amplifying Tome (id: 1052)
|
|
const description =
|
|
'<mainText><stats><attention> 20</attention> Ability Power</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.abilityPower).toBe(20)
|
|
})
|
|
|
|
it('should parse Health', () => {
|
|
// Ruby Crystal (id: 1028)
|
|
const description =
|
|
'<mainText><stats><attention> 150</attention> Health</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.health).toBe(150)
|
|
})
|
|
|
|
it('should parse Armor', () => {
|
|
// Cloth Armor (id: 1029)
|
|
const description =
|
|
'<mainText><stats><attention> 15</attention> Armor</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.armor).toBe(15)
|
|
})
|
|
|
|
it('should parse Magic Resist', () => {
|
|
// Null-Magic Mantle (id: 1033)
|
|
const description =
|
|
'<mainText><stats><attention> 20</attention> Magic Resist</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.magicResist).toBe(20)
|
|
})
|
|
|
|
it('should parse Mana', () => {
|
|
// Sapphire Crystal (id: 1027)
|
|
const description =
|
|
'<mainText><stats><attention> 300</attention> Mana</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.mana).toBe(300)
|
|
})
|
|
})
|
|
|
|
describe('percentage stats', () => {
|
|
it('should parse Attack Speed percentage', () => {
|
|
// Dagger (id: 1042)
|
|
const description =
|
|
'<mainText><stats><attention> 10%</attention> Attack Speed</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.attackSpeed).toBe(10)
|
|
})
|
|
|
|
it('should parse Critical Strike Chance', () => {
|
|
// Cloak of Agility (id: 1018)
|
|
const description =
|
|
'<mainText><stats><attention> 15%</attention> Critical Strike Chance</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.criticalStrikeChance).toBe(15)
|
|
})
|
|
|
|
it('should parse Life Steal', () => {
|
|
// Vampiric Scepter (id: 1053)
|
|
const description =
|
|
'<mainText><stats><attention> 15</attention> Attack Damage<br><attention> 7%</attention> Life Steal</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.attackDamage).toBe(15)
|
|
expect(stats.lifeSteal).toBe(7)
|
|
})
|
|
|
|
it('should parse Base Mana Regen percentage', () => {
|
|
// Faerie Charm (id: 1004)
|
|
const description =
|
|
'<mainText><stats><attention> 50%</attention> Base Mana Regen</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.baseManaRegen).toBe(50)
|
|
})
|
|
|
|
it('should parse Base Health Regen percentage', () => {
|
|
// Rejuvenation Bead (id: 1006)
|
|
const description =
|
|
'<mainText><stats><attention> 100%</attention> Base Health Regen</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.baseHealthRegen).toBe(100)
|
|
})
|
|
})
|
|
|
|
describe('multiple stats', () => {
|
|
it("should parse multiple stats from Doran's Blade", () => {
|
|
// Doran's Blade (id: 1055)
|
|
const description =
|
|
'<mainText><stats><attention> 10</attention> Attack Damage<br><attention> 80</attention> Health<br><attention> 2.5%</attention> Omnivamp</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.attackDamage).toBe(10)
|
|
expect(stats.health).toBe(80)
|
|
expect(stats.omnivamp).toBe(2.5)
|
|
})
|
|
|
|
it("should parse multiple stats from Doran's Ring", () => {
|
|
// Doran's Ring (id: 1056)
|
|
const description =
|
|
'<mainText><stats><attention> 18</attention> Ability Power<br><attention> 90</attention> Health</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.abilityPower).toBe(18)
|
|
expect(stats.health).toBe(90)
|
|
})
|
|
|
|
it("should parse multiple stats from Seeker's Armguard", () => {
|
|
// Seeker's Armguard (id: 2420)
|
|
const description =
|
|
'<mainText><stats><attention> 40</attention> Ability Power<br><attention> 25</attention> Armor</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.abilityPower).toBe(40)
|
|
expect(stats.armor).toBe(25)
|
|
})
|
|
|
|
it('should parse complex item with many stats', () => {
|
|
// Overlord's Bloodmail (id: 2501)
|
|
const description =
|
|
'<mainText><stats><attention> 30</attention> Attack Damage<br><attention> 550</attention> Health</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.attackDamage).toBe(30)
|
|
expect(stats.health).toBe(550)
|
|
})
|
|
|
|
it('should parse item with Ability Haste', () => {
|
|
// Unending Despair (id: 2502)
|
|
const description =
|
|
'<mainText><stats><attention> 400</attention> Health<br><attention> 50</attention> Armor<br><attention> 15</attention> Ability Haste</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.health).toBe(400)
|
|
expect(stats.armor).toBe(50)
|
|
expect(stats.abilityHaste).toBe(15)
|
|
})
|
|
|
|
it('should parse item with Mana and Ability Haste', () => {
|
|
// Blackfire Torch (id: 2503)
|
|
const description =
|
|
'<mainText><stats><attention> 80</attention> Ability Power<br><attention> 600</attention> Mana<br><attention> 20</attention> Ability Haste</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.abilityPower).toBe(80)
|
|
expect(stats.mana).toBe(600)
|
|
expect(stats.abilityHaste).toBe(20)
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it('should return empty object for empty description', () => {
|
|
const stats = parseItemStats('')
|
|
expect(stats).toEqual({})
|
|
})
|
|
|
|
it('should return empty object for description without stats', () => {
|
|
const description = '<mainText><stats></stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats).toEqual({})
|
|
})
|
|
|
|
it('should handle description with only passive text', () => {
|
|
// Emberknife (id: 1035) - no stats, only passives
|
|
const description =
|
|
'<mainText><stats></stats><br><br> <passive>7%</passive> Omnivamp against jungle monsters<br><li><passive>Sear:</passive> Damaging jungle monsters burns them for <magicDamage> magic damage</magicDamage> over 5 seconds.</mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats).toEqual({})
|
|
})
|
|
|
|
it('should handle decimal values', () => {
|
|
const description =
|
|
'<mainText><stats><attention> 2.5%</attention> Omnivamp</stats><br><br></mainText>'
|
|
const stats = parseItemStats(description)
|
|
expect(stats.omnivamp).toBe(2.5)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('parseItemDescription', () => {
|
|
describe('stats parsing', () => {
|
|
it('should parse stats from description', () => {
|
|
const description =
|
|
'<mainText><stats><attention> 75</attention> Attack Damage<br><attention> 25%</attention> Critical Strike Chance</stats><br><br></mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
expect(parsed.stats.attackDamage).toBe(75)
|
|
expect(parsed.stats.criticalStrikeChance).toBe(25)
|
|
})
|
|
|
|
it('should parse Infinity Edge stats', () => {
|
|
// Infinity Edge (id: 3031)
|
|
const description =
|
|
'<mainText><stats><attention> 75</attention> Attack Damage<br><attention> 25%</attention> Critical Strike Chance<br><attention> 30%</attention> Critical Strike Damage</stats><br><br></mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
expect(parsed.stats.attackDamage).toBe(75)
|
|
expect(parsed.stats.criticalStrikeChance).toBe(25)
|
|
// Critical Strike Damage is not in our mappings, but should not crash
|
|
})
|
|
})
|
|
|
|
describe('effects parsing', () => {
|
|
it('should parse passive effect', () => {
|
|
// Rabadon's Deathcap (id: 3089)
|
|
const description =
|
|
'<mainText><stats><attention> 130</attention> Ability Power</stats><br><br><passive>Magical Opus</passive><br>Increases your total <scaleAP>Ability Power by 30%</scaleAP>.</mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
|
|
expect(parsed.effects).toHaveLength(1)
|
|
expect(parsed.effects[0].type).toBe('passive')
|
|
expect(parsed.effects[0].name).toBe('Magical Opus')
|
|
})
|
|
|
|
it('should parse active effect', () => {
|
|
// Zhonya's Hourglass (id: 3157)
|
|
const description =
|
|
'<mainText><stats><attention> 105</attention> Ability Power<br><attention> 50</attention> Armor</stats><br><br><br> <br><active>Time Stop</active><br>Enter <keyword>Stasis</keyword> for 2.5 seconds.</mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
|
|
expect(parsed.effects).toHaveLength(1)
|
|
expect(parsed.effects[0].type).toBe('active')
|
|
expect(parsed.effects[0].name).toBe('Time Stop')
|
|
})
|
|
|
|
it('should parse multiple passives', () => {
|
|
// Trinity Force (id: 3078)
|
|
const description =
|
|
'<mainText><stats><attention> 36</attention> Attack Damage<br><attention> 30%</attention> Attack Speed<br><attention> 333</attention> Health<br><attention> 15</attention> Ability Haste</stats><br><br><passive>Spellblade</passive><br>After using an Ability, your next Attack deals <physicalDamage>bonus physical damage</physicalDamage> <OnHit>On-Hit</OnHit>.<br> <br><passive>Quicken</passive><br>Attacking grants <speed>20 Move Speed</speed> for 2 seconds.</mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
|
|
expect(parsed.effects).toHaveLength(2)
|
|
expect(parsed.effects[0].type).toBe('passive')
|
|
expect(parsed.effects[0].name).toBe('Spellblade')
|
|
expect(parsed.effects[1].type).toBe('passive')
|
|
expect(parsed.effects[1].name).toBe('Quicken')
|
|
})
|
|
|
|
it('should parse passive with status tag', () => {
|
|
// Serylda's Grudge (id: 6694)
|
|
const description =
|
|
'<mainText><stats><attention> 45</attention> Attack Damage<br><attention> 35%</attention> Armor Penetration<br><attention> 15</attention> Ability Haste</stats><br><br><passive>Bitter Cold</passive><br>Damaging Abilities <status>Slow</status> enemies below 50% Health by 30% for 1 second.</mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
|
|
expect(parsed.effects).toHaveLength(1)
|
|
expect(parsed.effects[0].type).toBe('passive')
|
|
expect(parsed.effects[0].name).toBe('Bitter Cold')
|
|
})
|
|
|
|
it('should handle item with no effects', () => {
|
|
// Infinity Edge has no passive/active tags
|
|
const description =
|
|
'<mainText><stats><attention> 75</attention> Attack Damage<br><attention> 25%</attention> Critical Strike Chance<br><attention> 30%</attention> Critical Strike Damage</stats><br><br></mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
|
|
expect(parsed.effects).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe('text segments', () => {
|
|
it('should parse scaleAP tag', () => {
|
|
const description =
|
|
'<mainText><stats><attention> 130</attention> Ability Power</stats><br><br><passive>Magical Opus</passive><br>Increases your total <scaleAP>Ability Power by 30%</scaleAP>.</mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
|
|
// Find the scaleAP segment
|
|
const effect = parsed.effects[0]
|
|
const scaleSegment = effect.description.find(s => s.type === 'scaleAP')
|
|
expect(scaleSegment).toBeDefined()
|
|
expect(scaleSegment?.content).toContain('Ability Power by 30%')
|
|
})
|
|
|
|
it('should parse physicalDamage tag', () => {
|
|
const description =
|
|
'<mainText><stats><attention> 36</attention> Attack Damage</stats><br><br><passive>Spellblade</passive><br>Deals <physicalDamage>bonus physical damage</physicalDamage>.</mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
|
|
const effect = parsed.effects[0]
|
|
const damageSegment = effect.description.find(s => s.type === 'physicalDamage')
|
|
expect(damageSegment).toBeDefined()
|
|
expect(damageSegment?.content).toContain('bonus physical damage')
|
|
})
|
|
|
|
it('should parse keyword tag', () => {
|
|
const description =
|
|
'<mainText><stats><attention> 105</attention> Ability Power</stats><br><br><active>Time Stop</active><br>Enter <keyword>Stasis</keyword> for 2.5 seconds.</mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
|
|
const effect = parsed.effects[0]
|
|
const keywordSegment = effect.description.find(s => s.type === 'keyword')
|
|
expect(keywordSegment).toBeDefined()
|
|
expect(keywordSegment?.content).toBe('Stasis')
|
|
})
|
|
|
|
it('should parse speed tag', () => {
|
|
const description =
|
|
'<mainText><stats><attention> 36</attention> Attack Damage</stats><br><br><passive>Quicken</passive><br>Grants <speed>20 Move Speed</speed>.</mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
|
|
const effect = parsed.effects[0]
|
|
const speedSegment = effect.description.find(s => s.type === 'speed')
|
|
expect(speedSegment).toBeDefined()
|
|
expect(speedSegment?.content).toContain('20 Move Speed')
|
|
})
|
|
|
|
it('should parse status tag', () => {
|
|
const description =
|
|
'<mainText><stats><attention> 45</attention> Attack Damage</stats><br><br><passive>Bitter Cold</passive><br><status>Slow</status> enemies.</mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
|
|
const effect = parsed.effects[0]
|
|
const statusSegment = effect.description.find(s => s.type === 'status')
|
|
expect(statusSegment).toBeDefined()
|
|
expect(statusSegment?.content).toBe('Slow')
|
|
})
|
|
})
|
|
|
|
describe('rarity detection', () => {
|
|
it('should detect mythic rarity', () => {
|
|
const description =
|
|
'<mainText><stats><attention> 65</attention> Attack Damage</stats><br><br><rarityMythic>Mythic</rarityMythic></mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
|
|
expect(parsed.rarity).toBe('mythic')
|
|
})
|
|
|
|
it('should detect legendary rarity', () => {
|
|
const description =
|
|
'<mainText><stats><attention> 65</attention> Attack Damage</stats><br><br><rarityLegendary>Legendary</rarityLegendary></mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
|
|
expect(parsed.rarity).toBe('legendary')
|
|
})
|
|
|
|
it('should detect epic rarity', () => {
|
|
const description =
|
|
'<mainText><stats><attention> 65</attention> Attack Damage</stats><br><br><rarityGeneric>Epic</rarityGeneric></mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
|
|
expect(parsed.rarity).toBe('epic')
|
|
})
|
|
})
|
|
|
|
describe('rules and flavor text', () => {
|
|
it('should parse rules section', () => {
|
|
const description =
|
|
'<mainText><stats><attention> 25</attention> Move Speed</stats><br><br><rules>Ornn upgrade only</rules></mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
|
|
expect(parsed.rules).toBeDefined()
|
|
expect(parsed.rules?.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should parse flavor text section', () => {
|
|
const description =
|
|
'<mainText><stats><attention> 25</attention> Move Speed</stats><br><br><flavorText>A ancient artifact</flavorText></mainText>'
|
|
const parsed = parseItemDescription(description)
|
|
|
|
expect(parsed.flavorText).toBeDefined()
|
|
expect(parsed.flavorText?.length).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('parseItem', () => {
|
|
it('should parse item and add stats', () => {
|
|
const item = {
|
|
id: 1001,
|
|
name: 'Boots',
|
|
description:
|
|
'<mainText><stats><attention> 25</attention> Move Speed</stats><br><br></mainText>',
|
|
iconPath: '/path/to/icon.png'
|
|
}
|
|
|
|
const result = parseItem(item)
|
|
expect(result.id).toBe(1001)
|
|
expect(result.name).toBe('Boots')
|
|
expect(result.stats.moveSpeed).toBe(25)
|
|
})
|
|
})
|
|
|
|
describe('parseItems', () => {
|
|
it('should parse multiple items', () => {
|
|
const items = [
|
|
{
|
|
id: 1001,
|
|
name: 'Boots',
|
|
description:
|
|
'<mainText><stats><attention> 25</attention> Move Speed</stats><br><br></mainText>',
|
|
iconPath: '/path/to/boots.png'
|
|
},
|
|
{
|
|
id: 1036,
|
|
name: 'Long Sword',
|
|
description:
|
|
'<mainText><stats><attention> 10</attention> Attack Damage</stats><br><br></mainText>',
|
|
iconPath: '/path/to/sword.png'
|
|
}
|
|
]
|
|
|
|
const results = parseItems(items)
|
|
expect(results).toHaveLength(2)
|
|
expect(results[0].stats.moveSpeed).toBe(25)
|
|
expect(results[1].stats.attackDamage).toBe(10)
|
|
})
|
|
})
|
|
|
|
describe('parseItemFull', () => {
|
|
it('should parse item with full description', () => {
|
|
const item = {
|
|
id: 3089,
|
|
name: "Rabadon's Deathcap",
|
|
description:
|
|
'<mainText><stats><attention> 130</attention> Ability Power</stats><br><br><passive>Magical Opus</passive><br>Increases your total <scaleAP>Ability Power by 30%</scaleAP>.</mainText>',
|
|
iconPath: '/path/to/icon.png'
|
|
}
|
|
|
|
const result = parseItemFull(item)
|
|
expect(result.id).toBe(3089)
|
|
expect(result.name).toBe("Rabadon's Deathcap")
|
|
expect(result.parsedDescription.stats.abilityPower).toBe(130)
|
|
expect(result.parsedDescription.effects).toHaveLength(1)
|
|
expect(result.parsedDescription.effects[0].name).toBe('Magical Opus')
|
|
})
|
|
})
|
|
|
|
describe('edge case items', () => {
|
|
it('should parse Mercurial Scimitar with ACTIVE label before effect name', () => {
|
|
const description =
|
|
'<mainText><stats><attention> 50</attention> Attack Damage<br><attention> 35</attention> Magic Resist<br><attention> 10%</attention> Life Steal</stats><br><br><br><br> <active>ACTIVE</active><br><active>Quicksilver</active><br>Removes all crowd control debuffs (excluding <keyword>Airborne</keyword>) and grants Move Speed.</mainText>'
|
|
|
|
const result = parseItemDescription(description)
|
|
expect(result.stats.attackDamage).toBe(50)
|
|
expect(result.stats.magicResist).toBe(35)
|
|
expect(result.stats.lifeSteal).toBe(10)
|
|
expect(result.effects).toHaveLength(1)
|
|
expect(result.effects[0].type).toBe('active')
|
|
expect(result.effects[0].name).toBe('Quicksilver')
|
|
expect(result.effects[0].description.length).toBeGreaterThan(0)
|
|
expect(result.effects[0].description[0].content).toContain('Removes all crowd control')
|
|
})
|
|
|
|
it('should parse Hextech Gunblade with ACTIVE label and cooldown', () => {
|
|
const description =
|
|
'<mainText><stats><attention> 80</attention> Ability Power<br><attention> 40</attention> Attack Damage<br><attention> 10%</attention> Omnivamp</stats><br><br><br><br> <active>ACTIVE</active> (0s)<br><active>Lightning Bolt</active><br>Shocks the target enemy champion, dealing magic damage and slowing them by 25% for 1.5 seconds.</mainText>'
|
|
|
|
const result = parseItemDescription(description)
|
|
expect(result.stats.abilityPower).toBe(80)
|
|
expect(result.stats.attackDamage).toBe(40)
|
|
expect(result.stats.omnivamp).toBe(10)
|
|
expect(result.effects).toHaveLength(1)
|
|
expect(result.effects[0].type).toBe('active')
|
|
expect(result.effects[0].name).toBe('Lightning Bolt')
|
|
expect(result.effects[0].description.length).toBeGreaterThan(0)
|
|
expect(result.effects[0].description[0].content).toContain('Shocks the target enemy champion')
|
|
})
|
|
|
|
it('should parse Dark Seal with passive name appearing in description', () => {
|
|
const description =
|
|
'<mainText><stats><attention> 15</attention> Ability Power<br><attention> 50</attention> Health</stats><br><br><passive>Glory</passive><br><keyword>Takedowns</keyword> grant <passive>Glory</passive>, up to 10. 5 <passive>Glory</passive> is lost on death.<br>Gain <scaleAP>4 Ability Power</scaleAP> per <passive>Glory</passive>.</mainText>'
|
|
|
|
const result = parseItemDescription(description)
|
|
expect(result.stats.abilityPower).toBe(15)
|
|
expect(result.stats.health).toBe(50)
|
|
expect(result.effects).toHaveLength(1)
|
|
expect(result.effects[0].type).toBe('passive')
|
|
expect(result.effects[0].name).toBe('Glory')
|
|
// Description should contain the full text with Glory mentions
|
|
const descText = result.effects[0].description.map(d => d.content).join('')
|
|
expect(descText).toContain('Takedowns')
|
|
expect(descText).toContain('Glory')
|
|
expect(descText).toContain('Ability Power')
|
|
})
|
|
})
|