473 lines
18 KiB
TypeScript
473 lines
18 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
GEM_INFO,
|
|
RUNE_INFO,
|
|
getSocketableInfo,
|
|
getSocketableSlotLines,
|
|
getSocketableSlotText,
|
|
isGemCode,
|
|
isJewelCode,
|
|
isRuneCode,
|
|
isSocketableCode,
|
|
} from '../src/game/gems-data.ts'
|
|
import {
|
|
CANONICAL_GEMS_TABLE,
|
|
canSocketDomainItem,
|
|
getGemModifiers,
|
|
getSocketedItemModifiersForHost,
|
|
isSocketableDomainItem,
|
|
resolveSocketTargetCategory,
|
|
socketItemIntoHost,
|
|
} from '../src/game/gems.ts'
|
|
import { formatItemTooltip } from '../src/game/item-tooltip.ts'
|
|
import { ItemQuality, type Item } from '../src/game/items.ts'
|
|
import {
|
|
InventoryPanel,
|
|
applySocketToUiItem,
|
|
canSocketUiItem,
|
|
isSocketableUiItem,
|
|
itemToUiInventoryItem,
|
|
resolveUiSocketTargetCategory,
|
|
type UiInventoryItem,
|
|
} from '../src/ui/inventory.ts'
|
|
import { WorldPanelsHud } from '../src/ui/world-panels.ts'
|
|
|
|
function createDomainHostItem(
|
|
category: 'weapon' | 'helm' | 'armor' | 'shield',
|
|
sockets = 3,
|
|
quality: ItemQuality = ItemQuality.NORMAL,
|
|
): Item {
|
|
const specs = {
|
|
weapon: {
|
|
id: 'crs',
|
|
name: 'Crystal Sword',
|
|
kind: 'weapon' as const,
|
|
type: 'swor',
|
|
tags: ['weap', 'mele', 'swor'],
|
|
invWidth: 2,
|
|
invHeight: 3,
|
|
minDam: 5,
|
|
maxDam: 15,
|
|
defense: 0,
|
|
reqstr: 43,
|
|
reqdex: 0,
|
|
durability: 20,
|
|
},
|
|
helm: {
|
|
id: 'uap',
|
|
name: 'Shako',
|
|
kind: 'armor' as const,
|
|
type: 'helm',
|
|
tags: ['armo', 'helm'],
|
|
invWidth: 2,
|
|
invHeight: 2,
|
|
minDam: 0,
|
|
maxDam: 0,
|
|
defense: 141,
|
|
reqstr: 50,
|
|
reqdex: 0,
|
|
durability: 12,
|
|
},
|
|
armor: {
|
|
id: 'xtp',
|
|
name: 'Mage Plate',
|
|
kind: 'armor' as const,
|
|
type: 'tors',
|
|
tags: ['armo', 'tors'],
|
|
invWidth: 2,
|
|
invHeight: 3,
|
|
minDam: 0,
|
|
maxDam: 0,
|
|
defense: 250,
|
|
reqstr: 55,
|
|
reqdex: 0,
|
|
durability: 60,
|
|
},
|
|
shield: {
|
|
id: 'uit',
|
|
name: 'Monarch',
|
|
kind: 'armor' as const,
|
|
type: 'shld',
|
|
tags: ['armo', 'shld'],
|
|
invWidth: 2,
|
|
invHeight: 3,
|
|
minDam: 0,
|
|
maxDam: 0,
|
|
defense: 148,
|
|
reqstr: 156,
|
|
reqdex: 0,
|
|
durability: 86,
|
|
},
|
|
}[category]
|
|
|
|
return {
|
|
id: 1,
|
|
code: specs.id,
|
|
name: specs.name,
|
|
quality,
|
|
level: 80,
|
|
invWidth: specs.invWidth,
|
|
invHeight: specs.invHeight,
|
|
identified: true,
|
|
sockets,
|
|
totalSockets: sockets,
|
|
socketedItems: [],
|
|
defense: specs.defense > 0 ? specs.defense : undefined,
|
|
durability: specs.durability,
|
|
maxDurability: specs.durability,
|
|
base: {
|
|
id: specs.id,
|
|
name: specs.name,
|
|
kind: specs.kind,
|
|
type: specs.type,
|
|
invWidth: specs.invWidth,
|
|
invHeight: specs.invHeight,
|
|
maxStack: 1,
|
|
value: 1000,
|
|
damage: specs.maxDam,
|
|
mindam: specs.minDam > 0 ? specs.minDam : undefined,
|
|
maxdam: specs.maxDam > 0 ? specs.maxDam : undefined,
|
|
minDam: specs.minDam > 0 ? specs.minDam : undefined,
|
|
maxDam: specs.maxDam > 0 ? specs.maxDam : undefined,
|
|
defense: specs.defense,
|
|
reqstr: specs.reqstr,
|
|
reqdex: specs.reqdex,
|
|
tags: specs.tags,
|
|
level: 40,
|
|
} as any,
|
|
rolledProps: [],
|
|
stats: {},
|
|
} as unknown as Item
|
|
}
|
|
|
|
function createDomainJewel(): Item {
|
|
return {
|
|
id: 100,
|
|
code: 'jew',
|
|
name: 'Ruby Jewel of Fervor',
|
|
quality: ItemQuality.MAGIC,
|
|
level: 58,
|
|
invWidth: 1,
|
|
invHeight: 1,
|
|
identified: true,
|
|
reqLevel: 58,
|
|
base: {
|
|
id: 'jew',
|
|
name: 'Jewel',
|
|
kind: 'misc',
|
|
type: 'jewl',
|
|
invWidth: 1,
|
|
invHeight: 1,
|
|
maxStack: 1,
|
|
value: 500,
|
|
damage: 0,
|
|
defense: 0,
|
|
tags: ['jewl'],
|
|
level: 1,
|
|
} as any,
|
|
rolledProps: [
|
|
{ code: 'dmg%', min: 40, max: 40, value: 40 },
|
|
{ code: 'swing2', min: 15, max: 15, value: 15 },
|
|
],
|
|
stats: {},
|
|
} as unknown as Item
|
|
}
|
|
|
|
function createDomainGemOrRune(code: string, name: string): Item {
|
|
const isRune = isRuneCode(code)
|
|
return {
|
|
id: 200,
|
|
code,
|
|
name,
|
|
quality: ItemQuality.NORMAL,
|
|
level: 1,
|
|
invWidth: 1,
|
|
invHeight: 1,
|
|
identified: true,
|
|
base: {
|
|
id: code,
|
|
name,
|
|
kind: 'misc',
|
|
type: isRune ? 'rune' : 'gem',
|
|
invWidth: 1,
|
|
invHeight: 1,
|
|
maxStack: 1,
|
|
value: 500,
|
|
damage: 0,
|
|
defense: 0,
|
|
tags: [isRune ? 'rune' : 'gem'],
|
|
level: 1,
|
|
} as any,
|
|
rolledProps: [],
|
|
stats: {},
|
|
} as unknown as Item
|
|
}
|
|
|
|
describe('Diablo II v1.13c Socketing System (Issue #507)', () => {
|
|
describe('1. Jewels: Free socketing with slot-independent modifiers', () => {
|
|
it('identifies jewel codes and renders socket instruction on un-socketed jewels', () => {
|
|
expect(isJewelCode('jew')).toBe(true)
|
|
expect(isJewelCode('jewl')).toBe(true)
|
|
expect(isSocketableCode('jew')).toBe(true)
|
|
|
|
const jewel = createDomainJewel()
|
|
expect(isSocketableDomainItem(jewel)).toBe(true)
|
|
|
|
const ttZh = formatItemTooltip(jewel, 'zh')
|
|
const ttEn = formatItemTooltip(jewel, 'en')
|
|
expect(ttZh.isSocketable).toBe(true)
|
|
expect(ttEn.isSocketable).toBe(true)
|
|
expect(ttZh.lines[0]?.text).toBe('可镶嵌进有凹槽的装备')
|
|
expect(ttEn.lines[0]?.text).toBe('Can be Inserted into Socketed Items')
|
|
expect(ttZh.lines.some(l => l.text.includes('+40% 增强伤害'))).toBe(true)
|
|
expect(ttEn.lines.some(l => l.text.includes('+40% Enhanced Damage'))).toBe(true)
|
|
})
|
|
|
|
it('applies identical jewel modifiers across Weapon, Helm, Armor, and Shield', () => {
|
|
const jewel = createDomainJewel()
|
|
const categories = ['weapon', 'helm', 'armor', 'shield'] as const
|
|
|
|
for (const cat of categories) {
|
|
const mods = getSocketedItemModifiersForHost(jewel, cat)
|
|
expect(mods).toEqual([
|
|
{ code: 'dmg%', min: 40, max: 40, value: 40 },
|
|
{ code: 'swing2', min: 15, max: 15, value: 15 },
|
|
])
|
|
|
|
const host = createDomainHostItem(cat, 2)
|
|
expect(canSocketDomainItem(jewel, host)).toBe(true)
|
|
const socketedHost = socketItemIntoHost(host, jewel)
|
|
expect(socketedHost.socketedItems?.length).toBe(1)
|
|
|
|
const ttZh = formatItemTooltip(socketedHost, 'zh')
|
|
const ttEn = formatItemTooltip(socketedHost, 'en')
|
|
expect(ttZh.lines.some(l => l.text.includes('+40% 增强伤害'))).toBe(true)
|
|
expect(ttZh.lines.some(l => l.text.includes('+15% 增加攻击速度'))).toBe(true)
|
|
expect(ttEn.lines.some(l => l.text.includes('+40% Enhanced Damage'))).toBe(true)
|
|
expect(ttEn.lines.some(l => l.text.includes('+15% Increased Attack Speed'))).toBe(true)
|
|
expect(ttZh.reqLevel).toBe(58)
|
|
}
|
|
})
|
|
|
|
it('applies UI jewel socketing identically to Weapon, Helm, Armor, and Shield and updates base stats', () => {
|
|
const jewelUi: UiInventoryItem = {
|
|
id: 'jew-40-15',
|
|
code: 'jew',
|
|
invFile: 'invjw1',
|
|
name: 'Ruby Jewel of Fervor',
|
|
nameZh: '红宝石之急速珠宝',
|
|
baseNameZh: '珠宝',
|
|
baseNameEn: 'Jewel',
|
|
quality: 'magic',
|
|
invWidth: 1,
|
|
invHeight: 1,
|
|
allowedSlots: [],
|
|
reqLevel: 58,
|
|
stats: [
|
|
{ text: '可镶嵌进有凹槽的装备', color: 'white' },
|
|
{ text: '+40% 增强伤害', color: 'blue' },
|
|
{ text: '+15% 增加攻击速度', color: 'blue' },
|
|
{ text: '需求 -15%', color: 'blue' },
|
|
],
|
|
statsEn: [
|
|
{ text: 'Can be Inserted into Socketed Items', color: 'white' },
|
|
{ text: '+40% Enhanced Damage', color: 'blue' },
|
|
{ text: '+15% Increased Attack Speed', color: 'blue' },
|
|
{ text: 'Requirements -15%', color: 'blue' },
|
|
],
|
|
}
|
|
|
|
expect(isSocketableUiItem(jewelUi)).toBe(true)
|
|
|
|
const categories = ['weapon', 'helm', 'armor', 'shield'] as const
|
|
for (const cat of categories) {
|
|
const hostUi = itemToUiInventoryItem(createDomainHostItem(cat, 2))
|
|
expect(resolveUiSocketTargetCategory(hostUi)).toBe(cat)
|
|
expect(canSocketUiItem(jewelUi, hostUi)).toBe(true)
|
|
|
|
const origReqStr = hostUi.reqStr!
|
|
applySocketToUiItem(hostUi, jewelUi)
|
|
|
|
expect(hostUi.socketedItems?.length).toBe(1)
|
|
expect(hostUi.reqLevel).toBe(58)
|
|
expect(hostUi.reqStr).toBe(Math.floor((origReqStr * 85) / 100))
|
|
expect(hostUi.stats.some(s => s.text === '+40% 增强伤害')).toBe(true)
|
|
expect(hostUi.stats.some(s => s.text === '+15% 增加攻击速度')).toBe(true)
|
|
expect(hostUi.stats.some(s => s.text.includes('可镶嵌进有凹槽的装备'))).toBe(false)
|
|
expect(hostUi.statsEn?.some(s => s.text === '+40% Enhanced Damage')).toBe(true)
|
|
expect(hostUi.statsEn?.some(s => s.text === '+15% Increased Attack Speed')).toBe(true)
|
|
if (cat === 'weapon') {
|
|
// 5-15 with +40% ED -> 7 - 21
|
|
expect(hostUi.damage).toBe('7 - 21')
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('2. Gems & Runes: Slot-dependent modifiers across Weapon, Helm, Armor, and Shield', () => {
|
|
it('loads all 68 canonical Gems.txt entries (35 Gems + 33 Runes) with valid slot modifiers', () => {
|
|
const allCodes = [...Object.keys(GEM_INFO), ...Object.keys(RUNE_INFO)]
|
|
expect(allCodes.length).toBe(68)
|
|
expect(CANONICAL_GEMS_TABLE.length).toBe(68)
|
|
|
|
for (const code of allCodes) {
|
|
expect(isSocketableCode(code)).toBe(true)
|
|
expect(isGemCode(code) || isRuneCode(code)).toBe(true)
|
|
for (const cat of ['weapon', 'helm', 'armor', 'shield'] as const) {
|
|
const mods = getGemModifiers(code, cat)
|
|
expect(mods.length).toBeGreaterThan(0)
|
|
const zhLines = getSocketableSlotLines(code, cat, 'zh')
|
|
const enLines = getSocketableSlotLines(code, cat, 'en')
|
|
expect(zhLines.length).toBeGreaterThan(0)
|
|
expect(enLines.length).toBeGreaterThan(0)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('applies distinct slot-specific modifiers for Perfect Gems across Weapon, Helm, Armor, and Shield', () => {
|
|
// Perfect Topaz (gpy): Weapon = 1-40 Lightning Dmg, Helm/Armor = +24% MF, Shield = +40% Lightning Res
|
|
expect(getSocketableSlotText('gpy', 'weapon', 'zh')).toContain('1-40 闪电伤害')
|
|
expect(getSocketableSlotText('gpy', 'helm', 'zh')).toContain('24% 获得魔法装备几率')
|
|
expect(getSocketableSlotText('gpy', 'armor', 'zh')).toContain('24% 获得魔法装备几率')
|
|
expect(getSocketableSlotText('gpy', 'shield', 'zh')).toContain('抗电 +40%')
|
|
|
|
// Perfect Diamond (gpw): Weapon = +68% Undead Dmg, Helm/Armor = +100 AR, Shield = All Res +19
|
|
expect(getSocketableSlotText('gpw', 'weapon', 'en')).toContain('+68% Damage to Undead')
|
|
expect(getSocketableSlotText('gpw', 'helm', 'en')).toContain('+100 to Attack Rating')
|
|
expect(getSocketableSlotText('gpw', 'armor', 'en')).toContain('+100 to Attack Rating')
|
|
expect(getSocketableSlotText('gpw', 'shield', 'en')).toContain('All Resistances +19')
|
|
|
|
// Perfect Ruby (gpr): Weapon = 15-20 Fire Dmg, Helm/Armor = +38 Life, Shield = +40% Fire Res
|
|
expect(getSocketableSlotText('gpr', 'weapon', 'zh')).toContain('15-20 火焰伤害')
|
|
expect(getSocketableSlotText('gpr', 'helm', 'zh')).toContain('+38 生命')
|
|
expect(getSocketableSlotText('gpr', 'shield', 'zh')).toContain('抗火 +40%')
|
|
|
|
// Perfect Amethyst (gpv): Weapon = +150 AR, Helm/Armor = +10 Str, Shield = +30 Defense
|
|
const shieldWithAmethyst = socketItemIntoHost(
|
|
createDomainHostItem('shield', 2),
|
|
createDomainGemOrRune('gpv', 'Perfect Amethyst'),
|
|
)
|
|
const shieldTt = formatItemTooltip(shieldWithAmethyst, 'zh')
|
|
expect(shieldTt.defense).toBe(148 + 30)
|
|
})
|
|
|
|
it('applies distinct slot-specific modifiers for Runes (Um, Ber, Jah, Cham, Hel, Ohm, Zod)', () => {
|
|
// Um Rune (r22): Weapon = 25% Open Wounds, Helm/Armor = All Res +15, Shield = All Res +22
|
|
expect(getSocketableSlotText('r22', 'weapon', 'zh')).toContain('25% 几率撕开伤口')
|
|
expect(getSocketableSlotText('r22', 'helm', 'zh')).toContain('所有抗性 +15')
|
|
expect(getSocketableSlotText('r22', 'armor', 'zh')).toContain('所有抗性 +15')
|
|
expect(getSocketableSlotText('r22', 'shield', 'zh')).toContain('所有抗性 +22')
|
|
|
|
// Ber Rune (r30): Weapon = 20% Crushing Blow, Helm/Armor/Shield = Damage Reduced by 8%
|
|
expect(getSocketableSlotText('r30', 'weapon', 'en')).toContain('20% Chance of Crushing Blow')
|
|
expect(getSocketableSlotText('r30', 'helm', 'en')).toContain('Damage Reduced by 8%')
|
|
expect(getSocketableSlotText('r30', 'shield', 'en')).toContain('Damage Reduced by 8%')
|
|
|
|
// Jah Rune (r31): Weapon = Ignore Target's Defense, Helm/Armor = +5% Max Life, Shield = +50 Life
|
|
expect(getSocketableSlotText('r31', 'weapon', 'en')).toContain("Ignore Target's Defense")
|
|
expect(getSocketableSlotText('r31', 'helm', 'en')).toContain('Increase Maximum Life 5%')
|
|
expect(getSocketableSlotText('r31', 'shield', 'en')).toContain('+50 to Life')
|
|
|
|
// Hel Rune (r15): Weapon = Requirements -20%, Helm/Armor/Shield = Requirements -15%
|
|
const weaponUi = itemToUiInventoryItem(createDomainHostItem('weapon', 2))
|
|
const shieldUi = itemToUiInventoryItem(createDomainHostItem('shield', 2))
|
|
const helUi = itemToUiInventoryItem(createDomainGemOrRune('r15', 'Hel Rune'))
|
|
|
|
applySocketToUiItem(weaponUi, helUi)
|
|
applySocketToUiItem(shieldUi, helUi)
|
|
expect(weaponUi.reqStr).toBe(Math.floor((43 * 80) / 100))
|
|
expect(shieldUi.reqStr).toBe(Math.floor((156 * 85) / 100))
|
|
|
|
// Zod Rune (r33): Indestructible removes durability
|
|
const helmUi = itemToUiInventoryItem(createDomainHostItem('helm', 1))
|
|
const zodUi = itemToUiInventoryItem(createDomainGemOrRune('r33', 'Zod Rune'))
|
|
expect(helmUi.durability).toBeDefined()
|
|
applySocketToUiItem(helmUi, zodUi)
|
|
expect(helmUi.durability).toBeUndefined()
|
|
expect(helmUi.reqLevel).toBe(69)
|
|
expect(helmUi.stats.some(s => s.text.includes('无法破坏'))).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('3. Interactive UI Socketing (Inventory, Equipped Slots, Stash, Cube) & Runeword Activation', () => {
|
|
it('sockets Gems, Runes, and Jewels via Left-Click in Inventory Grid and Equipped Paperdoll Slots', () => {
|
|
const inv = new InventoryPanel()
|
|
inv.visible = true
|
|
inv.gridItems = []
|
|
|
|
// 1. Place a 2-socket Shako in the 10x4 Inventory grid at (0, 0)
|
|
const shako = itemToUiInventoryItem(createDomainHostItem('helm', 2, ItemQuality.UNIQUE))
|
|
inv.gridItems.push({ item: shako, col: 0, row: 0 })
|
|
|
|
// Hold a Perfect Topaz on cursor and left-click (0, 0)
|
|
const pTopaz = itemToUiInventoryItem(createDomainGemOrRune('gpy', 'Perfect Topaz'))
|
|
inv.cursorItem = pTopaz
|
|
expect(inv.clickGridCell(0, 0)).toBe(true)
|
|
expect(inv.cursorItem).toBeNull()
|
|
expect(shako.socketedItems?.length).toBe(1)
|
|
expect(shako.stats.some(s => s.text.includes('24% 获得魔法装备几率'))).toBe(true)
|
|
|
|
// 2. Equip Shako into 'helm' slot, hold an Um Rune, and left-click the 'helm' slot
|
|
inv.equipped.helm = shako
|
|
const umRune = itemToUiInventoryItem(createDomainGemOrRune('r22', 'Um Rune'))
|
|
inv.cursorItem = umRune
|
|
expect(inv.clickEquipSlot('helm')).toBe(true)
|
|
expect(inv.cursorItem).toBeNull()
|
|
expect(shako.socketedItems?.length).toBe(2)
|
|
expect(shako.stats.some(s => s.text.includes('所有抗性 +15'))).toBe(true)
|
|
|
|
// 3. When all 2 sockets are full, clicking the grid cell with another gem swaps instead of socketing
|
|
const extraGem = itemToUiInventoryItem(createDomainGemOrRune('gpr', 'Perfect Ruby'))
|
|
inv.cursorItem = extraGem
|
|
expect(canSocketUiItem(extraGem, shako)).toBe(false)
|
|
expect(inv.clickGridCell(0, 0)).toBe(true)
|
|
expect(inv.cursorItem).toBe(shako)
|
|
expect(inv.gridItems[0]?.item).toBe(extraGem)
|
|
})
|
|
|
|
it('sockets items via Left-Click inside Private Stash (6x8) and Horadric Cube (3x4)', () => {
|
|
const worldPanels = new WorldPanelsHud()
|
|
const shieldInStash = itemToUiInventoryItem(createDomainHostItem('shield', 1))
|
|
const weaponInCube = itemToUiInventoryItem(createDomainHostItem('weapon', 1))
|
|
worldPanels.stashItems = [{ item: shieldInStash, col: 0, row: 0 }]
|
|
worldPanels.cubeItems = [{ item: weaponInCube, col: 0, row: 0 }]
|
|
|
|
// Socket Perfect Diamond into Shield in Stash -> All Resistances +19
|
|
const pDiamond = itemToUiInventoryItem(createDomainGemOrRune('gpw', 'Perfect Diamond'))
|
|
const stashRes = worldPanels.clickStashCell(0, 0, pDiamond)
|
|
expect(stashRes.handled).toBe(true)
|
|
expect(stashRes.newCursorItem).toBeNull()
|
|
expect(shieldInStash.socketedItems?.length).toBe(1)
|
|
expect(shieldInStash.stats.some(s => s.text.includes('所有抗性 +19'))).toBe(true)
|
|
|
|
// Socket the same Perfect Diamond code into Weapon in Cube -> +68% Damage to Undead
|
|
const pDiamond2 = itemToUiInventoryItem(createDomainGemOrRune('gpw', 'Perfect Diamond'))
|
|
const cubeRes = worldPanels.clickCubeCell(0, 0, pDiamond2)
|
|
expect(cubeRes.handled).toBe(true)
|
|
expect(cubeRes.newCursorItem).toBeNull()
|
|
expect(weaponInCube.socketedItems?.length).toBe(1)
|
|
expect(weaponInCube.stats.some(s => s.text.includes('+68% 对不死生物的伤害'))).toBe(true)
|
|
})
|
|
|
|
it('activates Runeword when all sockets of a normal item are filled with a valid rune sequence', () => {
|
|
// 4-socket Monarch + Tal (r07) + Thul (r10) + Ort (r09) + Amn (r11) -> Spirit
|
|
const monarch = itemToUiInventoryItem(createDomainHostItem('shield', 4, ItemQuality.NORMAL))
|
|
const runeCodes = ['r07', 'r10', 'r09', 'r11'] as const
|
|
|
|
for (const code of runeCodes) {
|
|
const info = getSocketableInfo(code)!
|
|
const runeUi = itemToUiInventoryItem(createDomainGemOrRune(code, info.nameEn))
|
|
applySocketToUiItem(monarch, runeUi)
|
|
}
|
|
|
|
expect(monarch.socketedItems?.length).toBe(4)
|
|
expect(monarch.quality).toBe('rune')
|
|
expect(monarch.runewordRunes).toBe("'TalThulOrtAmn'")
|
|
expect(monarch.name).toBe('Spirit')
|
|
expect(monarch.nameZh).toContain('精神')
|
|
expect(monarch.stats.some(s => s.text.includes('+2 所有技能'))).toBe(true)
|
|
expect(monarch.statsEn?.some(s => s.text.includes('+2 to All Skills'))).toBe(true)
|
|
})
|
|
})
|
|
})
|