test(web): add comprehensive frontend tests for Phase 3 (#129)
- Add component tests: EntityRow, RelationRow, StatItem, StatusBar, StatusWidget, TagList - Add store tests: setting - Enhance existing tests: socket (toast, alert handling), useGameInit (polling, initialization) - Update vitest.config.ts to exclude config files from coverage - Coverage improved from 13.39% to 32.49% (tested files achieve 95%+ coverage)
This commit is contained in:
175
web/src/__tests__/components/EntityRow.test.ts
Normal file
175
web/src/__tests__/components/EntityRow.test.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
// Mock getEntityColor.
|
||||
const mockGetEntityColor = vi.fn()
|
||||
|
||||
vi.mock('@/utils/theme', () => ({
|
||||
getEntityColor: (entity: any) => mockGetEntityColor(entity),
|
||||
}))
|
||||
|
||||
import EntityRow from '@/components/game/panels/info/components/EntityRow.vue'
|
||||
|
||||
describe('EntityRow', () => {
|
||||
const defaultItem = {
|
||||
id: '1',
|
||||
name: 'Test Entity',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetEntityColor.mockReturnValue('#ff0000')
|
||||
})
|
||||
|
||||
it('should render item name', () => {
|
||||
const wrapper = mount(EntityRow, {
|
||||
props: {
|
||||
item: defaultItem,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.name').text()).toBe('Test Entity')
|
||||
})
|
||||
|
||||
it('should apply color from getEntityColor', () => {
|
||||
mockGetEntityColor.mockReturnValue('#00ff00')
|
||||
|
||||
const wrapper = mount(EntityRow, {
|
||||
props: {
|
||||
item: defaultItem,
|
||||
},
|
||||
})
|
||||
|
||||
expect(mockGetEntityColor).toHaveBeenCalledWith(defaultItem)
|
||||
expect(wrapper.find('.name').attributes('style')).toContain('color: rgb(0, 255, 0)')
|
||||
})
|
||||
|
||||
it('should render meta when provided', () => {
|
||||
const wrapper = mount(EntityRow, {
|
||||
props: {
|
||||
item: defaultItem,
|
||||
meta: 'Proficiency 50%',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.meta').exists()).toBe(true)
|
||||
expect(wrapper.find('.meta').text()).toBe('Proficiency 50%')
|
||||
})
|
||||
|
||||
it('should hide meta when not provided', () => {
|
||||
const wrapper = mount(EntityRow, {
|
||||
props: {
|
||||
item: defaultItem,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.meta').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should render grade when item has grade', () => {
|
||||
const itemWithGrade = {
|
||||
...defaultItem,
|
||||
grade: 'SSR',
|
||||
}
|
||||
|
||||
const wrapper = mount(EntityRow, {
|
||||
props: {
|
||||
item: itemWithGrade,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.grade').exists()).toBe(true)
|
||||
expect(wrapper.find('.grade').text()).toBe('SSR')
|
||||
})
|
||||
|
||||
it('should hide grade when item has no grade', () => {
|
||||
const wrapper = mount(EntityRow, {
|
||||
props: {
|
||||
item: defaultItem,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.grade').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should have compact class when compact is true', () => {
|
||||
const wrapper = mount(EntityRow, {
|
||||
props: {
|
||||
item: defaultItem,
|
||||
compact: true,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.entity-row').classes()).toContain('compact')
|
||||
})
|
||||
|
||||
it('should not have compact class when compact is false', () => {
|
||||
const wrapper = mount(EntityRow, {
|
||||
props: {
|
||||
item: defaultItem,
|
||||
compact: false,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.entity-row').classes()).not.toContain('compact')
|
||||
})
|
||||
|
||||
it('should not have compact class when compact not provided', () => {
|
||||
const wrapper = mount(EntityRow, {
|
||||
props: {
|
||||
item: defaultItem,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.entity-row').classes()).not.toContain('compact')
|
||||
})
|
||||
|
||||
it('should emit click on click', async () => {
|
||||
const wrapper = mount(EntityRow, {
|
||||
props: {
|
||||
item: defaultItem,
|
||||
},
|
||||
})
|
||||
|
||||
await wrapper.find('.entity-row').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('click')).toBeTruthy()
|
||||
expect(wrapper.emitted('click')?.length).toBe(1)
|
||||
})
|
||||
|
||||
it('should render all props together', () => {
|
||||
mockGetEntityColor.mockReturnValue('#0000ff')
|
||||
|
||||
const itemWithGrade = {
|
||||
id: '2',
|
||||
name: 'Full Entity',
|
||||
grade: 'SR',
|
||||
}
|
||||
|
||||
const wrapper = mount(EntityRow, {
|
||||
props: {
|
||||
item: itemWithGrade,
|
||||
meta: 'Meta Info',
|
||||
compact: true,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.name').text()).toBe('Full Entity')
|
||||
expect(wrapper.find('.meta').text()).toBe('Meta Info')
|
||||
expect(wrapper.find('.grade').text()).toBe('SR')
|
||||
expect(wrapper.find('.entity-row').classes()).toContain('compact')
|
||||
})
|
||||
|
||||
it('should handle undefined color from getEntityColor', () => {
|
||||
mockGetEntityColor.mockReturnValue(undefined)
|
||||
|
||||
const wrapper = mount(EntityRow, {
|
||||
props: {
|
||||
item: defaultItem,
|
||||
},
|
||||
})
|
||||
|
||||
// Should not throw, just render without color.
|
||||
expect(wrapper.find('.name').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
95
web/src/__tests__/components/RelationRow.test.ts
Normal file
95
web/src/__tests__/components/RelationRow.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import RelationRow from '@/components/game/panels/info/components/RelationRow.vue'
|
||||
|
||||
describe('RelationRow', () => {
|
||||
const defaultProps = {
|
||||
name: 'Test Name',
|
||||
}
|
||||
|
||||
it('should render name prop', () => {
|
||||
const wrapper = mount(RelationRow, {
|
||||
props: defaultProps,
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('Test Name')
|
||||
expect(wrapper.find('.rel-name').text()).toBe('Test Name')
|
||||
})
|
||||
|
||||
it('should render meta when provided', () => {
|
||||
const wrapper = mount(RelationRow, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
meta: 'Test Meta',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.rel-type').exists()).toBe(true)
|
||||
expect(wrapper.find('.rel-type').text()).toBe('Test Meta')
|
||||
})
|
||||
|
||||
it('should hide meta when not provided', () => {
|
||||
const wrapper = mount(RelationRow, {
|
||||
props: defaultProps,
|
||||
})
|
||||
|
||||
expect(wrapper.find('.rel-type').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should render sub when provided', () => {
|
||||
const wrapper = mount(RelationRow, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
sub: 'Subtitle text',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.rel-sub').exists()).toBe(true)
|
||||
expect(wrapper.find('.rel-sub').text()).toBe('Subtitle text')
|
||||
})
|
||||
|
||||
it('should hide sub when not provided', () => {
|
||||
const wrapper = mount(RelationRow, {
|
||||
props: defaultProps,
|
||||
})
|
||||
|
||||
expect(wrapper.find('.rel-sub').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should emit click on click', async () => {
|
||||
const wrapper = mount(RelationRow, {
|
||||
props: defaultProps,
|
||||
})
|
||||
|
||||
await wrapper.find('.relation-row').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('click')).toBeTruthy()
|
||||
expect(wrapper.emitted('click')?.length).toBe(1)
|
||||
})
|
||||
|
||||
it('should emit multiple clicks', async () => {
|
||||
const wrapper = mount(RelationRow, {
|
||||
props: defaultProps,
|
||||
})
|
||||
|
||||
await wrapper.find('.relation-row').trigger('click')
|
||||
await wrapper.find('.relation-row').trigger('click')
|
||||
await wrapper.find('.relation-row').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('click')?.length).toBe(3)
|
||||
})
|
||||
|
||||
it('should render all props together', () => {
|
||||
const wrapper = mount(RelationRow, {
|
||||
props: {
|
||||
name: 'Full Name',
|
||||
meta: 'Meta Info',
|
||||
sub: 'Sub Info',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.rel-name').text()).toBe('Full Name')
|
||||
expect(wrapper.find('.rel-type').text()).toBe('Meta Info')
|
||||
expect(wrapper.find('.rel-sub').text()).toBe('Sub Info')
|
||||
})
|
||||
})
|
||||
160
web/src/__tests__/components/StatItem.test.ts
Normal file
160
web/src/__tests__/components/StatItem.test.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import StatItem from '@/components/game/panels/info/components/StatItem.vue'
|
||||
|
||||
describe('StatItem', () => {
|
||||
const defaultProps = {
|
||||
label: 'Test Label',
|
||||
value: 'Test Value',
|
||||
}
|
||||
|
||||
it('should render label', () => {
|
||||
const wrapper = mount(StatItem, {
|
||||
props: defaultProps,
|
||||
})
|
||||
|
||||
expect(wrapper.find('label').text()).toBe('Test Label')
|
||||
})
|
||||
|
||||
it('should render string value', () => {
|
||||
const wrapper = mount(StatItem, {
|
||||
props: defaultProps,
|
||||
})
|
||||
|
||||
expect(wrapper.find('span').text()).toContain('Test Value')
|
||||
})
|
||||
|
||||
it('should render numeric value', () => {
|
||||
const wrapper = mount(StatItem, {
|
||||
props: {
|
||||
label: 'Count',
|
||||
value: 42,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('span').text()).toContain('42')
|
||||
})
|
||||
|
||||
it('should render subValue when provided', () => {
|
||||
const wrapper = mount(StatItem, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
subValue: 'Sub Info',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.sub-value').exists()).toBe(true)
|
||||
expect(wrapper.find('.sub-value').text()).toBe('(Sub Info)')
|
||||
})
|
||||
|
||||
it('should render numeric subValue', () => {
|
||||
const wrapper = mount(StatItem, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
subValue: 100,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.sub-value').text()).toBe('(100)')
|
||||
})
|
||||
|
||||
it('should hide subValue when not provided', () => {
|
||||
const wrapper = mount(StatItem, {
|
||||
props: defaultProps,
|
||||
})
|
||||
|
||||
expect(wrapper.find('.sub-value').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('should have clickable class when onClick provided', () => {
|
||||
const onClick = vi.fn()
|
||||
const wrapper = mount(StatItem, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
onClick,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.stat-item').classes()).toContain('clickable')
|
||||
})
|
||||
|
||||
it('should not have clickable class when onClick not provided', () => {
|
||||
const wrapper = mount(StatItem, {
|
||||
props: defaultProps,
|
||||
})
|
||||
|
||||
expect(wrapper.find('.stat-item').classes()).not.toContain('clickable')
|
||||
})
|
||||
|
||||
it('should call onClick when clicked', async () => {
|
||||
const onClick = vi.fn()
|
||||
const wrapper = mount(StatItem, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
onClick,
|
||||
},
|
||||
})
|
||||
|
||||
await wrapper.find('.stat-item').trigger('click')
|
||||
|
||||
expect(onClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not throw when clicked without onClick', async () => {
|
||||
const wrapper = mount(StatItem, {
|
||||
props: defaultProps,
|
||||
})
|
||||
|
||||
// Should not throw.
|
||||
await wrapper.find('.stat-item').trigger('click')
|
||||
})
|
||||
|
||||
it('should have full class when fullWidth is true', () => {
|
||||
const wrapper = mount(StatItem, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
fullWidth: true,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.stat-item').classes()).toContain('full')
|
||||
})
|
||||
|
||||
it('should not have full class when fullWidth is false', () => {
|
||||
const wrapper = mount(StatItem, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
fullWidth: false,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.stat-item').classes()).not.toContain('full')
|
||||
})
|
||||
|
||||
it('should not have full class when fullWidth not provided', () => {
|
||||
const wrapper = mount(StatItem, {
|
||||
props: defaultProps,
|
||||
})
|
||||
|
||||
expect(wrapper.find('.stat-item').classes()).not.toContain('full')
|
||||
})
|
||||
|
||||
it('should render all props together', () => {
|
||||
const onClick = vi.fn()
|
||||
const wrapper = mount(StatItem, {
|
||||
props: {
|
||||
label: 'Full Label',
|
||||
value: 'Full Value',
|
||||
subValue: 'Full Sub',
|
||||
onClick,
|
||||
fullWidth: true,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('label').text()).toBe('Full Label')
|
||||
expect(wrapper.find('span').text()).toContain('Full Value')
|
||||
expect(wrapper.find('.sub-value').text()).toBe('(Full Sub)')
|
||||
expect(wrapper.find('.stat-item').classes()).toContain('clickable')
|
||||
expect(wrapper.find('.stat-item').classes()).toContain('full')
|
||||
})
|
||||
})
|
||||
388
web/src/__tests__/components/StatusBar.test.ts
Normal file
388
web/src/__tests__/components/StatusBar.test.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { h, defineComponent, nextTick } from 'vue'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
|
||||
// Use vi.hoisted to define mock functions that will be used by vi.mock.
|
||||
const { mockGetPhenomenaList, mockChangePhenomenon, mockSuccess, mockError } = vi.hoisted(() => ({
|
||||
mockGetPhenomenaList: vi.fn(),
|
||||
mockChangePhenomenon: vi.fn(),
|
||||
mockSuccess: vi.fn(),
|
||||
mockError: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mutable store state that can be modified in tests.
|
||||
let mockYear = 100
|
||||
let mockMonth = 5
|
||||
let mockCurrentPhenomenon: any = { id: 1, name: 'Test Phenomenon', rarity: 'R' }
|
||||
let mockActiveDomains: any[] = []
|
||||
let mockAvatarList: any[] = [{ id: '1' }, { id: '2' }]
|
||||
let mockPhenomenaList: any[] = [
|
||||
{ id: 1, name: 'Phenomenon 1', rarity: 'N', desc: 'Desc 1', effect_desc: 'Effect 1' },
|
||||
{ id: 2, name: 'Phenomenon 2', rarity: 'R', desc: 'Desc 2', effect_desc: 'Effect 2' },
|
||||
{ id: 3, name: 'Phenomenon 3', rarity: 'SSR', desc: 'Desc 3', effect_desc: 'Effect 3' },
|
||||
]
|
||||
let mockIsConnected = true
|
||||
|
||||
// Mock vue-i18n.
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string, params?: any) => {
|
||||
if (params) return `${key}:${JSON.stringify(params)}`
|
||||
return key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock stores.
|
||||
vi.mock('@/stores/world', () => ({
|
||||
useWorldStore: () => ({
|
||||
get year() { return mockYear },
|
||||
get month() { return mockMonth },
|
||||
get currentPhenomenon() { return mockCurrentPhenomenon },
|
||||
get activeDomains() { return mockActiveDomains },
|
||||
get avatarList() { return mockAvatarList },
|
||||
get phenomenaList() { return mockPhenomenaList },
|
||||
getPhenomenaList: mockGetPhenomenaList,
|
||||
changePhenomenon: mockChangePhenomenon,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/socket', () => ({
|
||||
useSocketStore: () => ({
|
||||
get isConnected() { return mockIsConnected },
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock naive-ui.
|
||||
vi.mock('naive-ui', () => ({
|
||||
NModal: defineComponent({
|
||||
name: 'NModal',
|
||||
props: ['show', 'preset', 'title'],
|
||||
emits: ['update:show'],
|
||||
setup(props, { slots, emit }) {
|
||||
return () => props.show ? h('div', {
|
||||
class: 'n-modal-stub',
|
||||
onClick: () => emit('update:show', false),
|
||||
}, slots.default?.()) : null
|
||||
},
|
||||
}),
|
||||
NList: defineComponent({
|
||||
name: 'NList',
|
||||
props: ['hoverable', 'clickable'],
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', { class: 'n-list-stub' }, slots.default?.())
|
||||
},
|
||||
}),
|
||||
NListItem: defineComponent({
|
||||
name: 'NListItem',
|
||||
emits: ['click'],
|
||||
setup(_, { slots, emit }) {
|
||||
return () => h('div', {
|
||||
class: 'n-list-item-stub',
|
||||
onClick: () => emit('click'),
|
||||
}, slots.default?.())
|
||||
},
|
||||
}),
|
||||
NTag: defineComponent({
|
||||
name: 'NTag',
|
||||
props: ['size', 'bordered', 'color'],
|
||||
setup(_, { slots }) {
|
||||
return () => h('span', { class: 'n-tag-stub' }, slots.default?.())
|
||||
},
|
||||
}),
|
||||
NEmpty: defineComponent({
|
||||
name: 'NEmpty',
|
||||
props: ['description'],
|
||||
setup(props) {
|
||||
return () => h('div', { class: 'n-empty-stub' }, props.description)
|
||||
},
|
||||
}),
|
||||
useMessage: () => ({
|
||||
success: mockSuccess,
|
||||
error: mockError,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Stub StatusWidget.
|
||||
const StatusWidgetStub = defineComponent({
|
||||
name: 'StatusWidget',
|
||||
props: ['label', 'color', 'mode', 'disablePopover', 'title', 'items', 'emptyText'],
|
||||
emits: ['trigger-click'],
|
||||
setup(props, { emit }) {
|
||||
return () => h('div', {
|
||||
class: 'status-widget-stub',
|
||||
'data-label': props.label,
|
||||
'data-color': props.color,
|
||||
onClick: () => emit('trigger-click'),
|
||||
}, props.label)
|
||||
},
|
||||
})
|
||||
|
||||
import StatusBar from '@/components/layout/StatusBar.vue'
|
||||
|
||||
describe('StatusBar', () => {
|
||||
const globalConfig = {
|
||||
global: {
|
||||
stubs: {
|
||||
StatusWidget: StatusWidgetStub,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Reset mock values.
|
||||
mockYear = 100
|
||||
mockMonth = 5
|
||||
mockCurrentPhenomenon = { id: 1, name: 'Test Phenomenon', rarity: 'R' }
|
||||
mockActiveDomains = []
|
||||
mockAvatarList = [{ id: '1' }, { id: '2' }]
|
||||
mockIsConnected = true
|
||||
|
||||
// Setup default mock implementations.
|
||||
mockGetPhenomenaList.mockImplementation(() => Promise.resolve())
|
||||
mockChangePhenomenon.mockImplementation(() => Promise.resolve())
|
||||
})
|
||||
|
||||
it('should display year and month from worldStore', () => {
|
||||
mockYear = 200
|
||||
mockMonth = 12
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
expect(wrapper.text()).toContain('200')
|
||||
expect(wrapper.text()).toContain('12')
|
||||
})
|
||||
|
||||
it('should show connected status when socketStore.isConnected is true', () => {
|
||||
mockIsConnected = true
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
expect(wrapper.find('.status-dot.connected').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should show disconnected status when socketStore.isConnected is false', () => {
|
||||
mockIsConnected = false
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
expect(wrapper.find('.status-dot.connected').exists()).toBe(false)
|
||||
expect(wrapper.find('.status-dot').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('phenomenonColor', () => {
|
||||
it('should return #ccc for N rarity', () => {
|
||||
mockCurrentPhenomenon = { id: 1, name: 'Test', rarity: 'N' }
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
const widget = wrapper.findAll('.status-widget-stub')[0]
|
||||
expect(widget.attributes('data-color')).toBe('#ccc')
|
||||
})
|
||||
|
||||
it('should return #4dabf7 for R rarity', () => {
|
||||
mockCurrentPhenomenon = { id: 1, name: 'Test', rarity: 'R' }
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
const widget = wrapper.findAll('.status-widget-stub')[0]
|
||||
expect(widget.attributes('data-color')).toBe('#4dabf7')
|
||||
})
|
||||
|
||||
it('should return #a0d911 for SR rarity', () => {
|
||||
mockCurrentPhenomenon = { id: 1, name: 'Test', rarity: 'SR' }
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
const widget = wrapper.findAll('.status-widget-stub')[0]
|
||||
expect(widget.attributes('data-color')).toBe('#a0d911')
|
||||
})
|
||||
|
||||
it('should return #fa8c16 for SSR rarity', () => {
|
||||
mockCurrentPhenomenon = { id: 1, name: 'Test', rarity: 'SSR' }
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
const widget = wrapper.findAll('.status-widget-stub')[0]
|
||||
expect(widget.attributes('data-color')).toBe('#fa8c16')
|
||||
})
|
||||
|
||||
it('should return #ccc for unknown rarity', () => {
|
||||
mockCurrentPhenomenon = { id: 1, name: 'Test', rarity: 'UNKNOWN' }
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
const widget = wrapper.findAll('.status-widget-stub')[0]
|
||||
expect(widget.attributes('data-color')).toBe('#ccc')
|
||||
})
|
||||
|
||||
it('should hide phenomenon widget when currentPhenomenon is null', () => {
|
||||
mockCurrentPhenomenon = null
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
// When phenomenon is null, v-if hides the widget.
|
||||
// Only domain widget should exist.
|
||||
const widgets = wrapper.findAll('.status-widget-stub')
|
||||
expect(widgets.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('phenomenon selector', () => {
|
||||
it('should call getPhenomenaList when opening selector', async () => {
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
// Trigger click on phenomenon widget.
|
||||
await wrapper.findAll('.status-widget-stub')[0].trigger('click')
|
||||
|
||||
// Run all pending timers and promises.
|
||||
await vi.runAllTimersAsync()
|
||||
await nextTick()
|
||||
|
||||
expect(mockGetPhenomenaList).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show selector modal after getPhenomenaList', async () => {
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
await wrapper.findAll('.status-widget-stub')[0].trigger('click')
|
||||
await vi.runAllTimersAsync()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.n-modal-stub').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('changePhenomenon', () => {
|
||||
it('should call changePhenomenon on selection', async () => {
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
// Open selector.
|
||||
await wrapper.findAll('.status-widget-stub')[0].trigger('click')
|
||||
await vi.runAllTimersAsync()
|
||||
await nextTick()
|
||||
|
||||
// Find and click a list item.
|
||||
const listItems = wrapper.findAll('.n-list-item-stub')
|
||||
expect(listItems.length).toBeGreaterThan(0)
|
||||
|
||||
await listItems[0].trigger('click')
|
||||
await vi.runAllTimersAsync()
|
||||
await nextTick()
|
||||
|
||||
expect(mockChangePhenomenon).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show success message on successful change', async () => {
|
||||
mockChangePhenomenon.mockImplementation(() => Promise.resolve())
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
await wrapper.findAll('.status-widget-stub')[0].trigger('click')
|
||||
await vi.runAllTimersAsync()
|
||||
await nextTick()
|
||||
|
||||
const listItems = wrapper.findAll('.n-list-item-stub')
|
||||
expect(listItems.length).toBeGreaterThan(0)
|
||||
|
||||
await listItems[0].trigger('click')
|
||||
await vi.runAllTimersAsync()
|
||||
await nextTick()
|
||||
|
||||
expect(mockSuccess).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show error message on failed change', async () => {
|
||||
mockChangePhenomenon.mockImplementation(() => Promise.reject(new Error('Failed')))
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
await wrapper.findAll('.status-widget-stub')[0].trigger('click')
|
||||
await vi.runAllTimersAsync()
|
||||
await nextTick()
|
||||
|
||||
const listItems = wrapper.findAll('.n-list-item-stub')
|
||||
expect(listItems.length).toBeGreaterThan(0)
|
||||
|
||||
await listItems[0].trigger('click')
|
||||
await vi.runAllTimersAsync()
|
||||
await nextTick()
|
||||
|
||||
expect(mockError).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('domain color', () => {
|
||||
it('should return #fa8c16 when any domain is open', () => {
|
||||
mockActiveDomains = [
|
||||
{ id: 1, name: 'D1', is_open: false },
|
||||
{ id: 2, name: 'D2', is_open: true },
|
||||
]
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
const domainWidget = wrapper.findAll('.status-widget-stub')[1]
|
||||
expect(domainWidget.attributes('data-color')).toBe('#fa8c16')
|
||||
})
|
||||
|
||||
it('should return #666 when all domains are closed', () => {
|
||||
mockActiveDomains = [
|
||||
{ id: 1, name: 'D1', is_open: false },
|
||||
{ id: 2, name: 'D2', is_open: false },
|
||||
]
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
const domainWidget = wrapper.findAll('.status-widget-stub')[1]
|
||||
expect(domainWidget.attributes('data-color')).toBe('#666')
|
||||
})
|
||||
|
||||
it('should return #666 when no domains', () => {
|
||||
mockActiveDomains = []
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
const domainWidget = wrapper.findAll('.status-widget-stub')[1]
|
||||
expect(domainWidget.attributes('data-color')).toBe('#666')
|
||||
})
|
||||
})
|
||||
|
||||
it('should display cultivator count', () => {
|
||||
mockAvatarList = [{ id: '1' }, { id: '2' }, { id: '3' }]
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
// The t function returns key:params format.
|
||||
expect(wrapper.text()).toContain('game.status_bar.cultivators')
|
||||
})
|
||||
|
||||
it('should render external links', () => {
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
const links = wrapper.findAll('a.author-link')
|
||||
expect(links.length).toBe(2)
|
||||
expect(links[0].attributes('href')).toContain('bilibili')
|
||||
expect(links[1].attributes('href')).toContain('github')
|
||||
})
|
||||
|
||||
it('should pass correct props to phenomenon StatusWidget', () => {
|
||||
mockCurrentPhenomenon = { id: 1, name: 'TestPhenomenon', rarity: 'SR' }
|
||||
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
const phenomenonWidget = wrapper.findAll('.status-widget-stub')[0]
|
||||
expect(phenomenonWidget.attributes('data-label')).toBe('[TestPhenomenon]')
|
||||
expect(phenomenonWidget.attributes('data-color')).toBe('#a0d911')
|
||||
})
|
||||
|
||||
it('should pass correct props to domain StatusWidget', () => {
|
||||
const wrapper = mount(StatusBar, globalConfig)
|
||||
|
||||
const domainWidget = wrapper.findAll('.status-widget-stub')[1]
|
||||
expect(domainWidget.attributes('data-label')).toBe('game.status_bar.hidden_domain.label')
|
||||
})
|
||||
})
|
||||
290
web/src/__tests__/components/StatusWidget.test.ts
Normal file
290
web/src/__tests__/components/StatusWidget.test.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { h, defineComponent } from 'vue'
|
||||
|
||||
// Mock vue-i18n.
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock naive-ui with stub components.
|
||||
vi.mock('naive-ui', () => ({
|
||||
NPopover: defineComponent({
|
||||
name: 'NPopover',
|
||||
props: ['trigger', 'placement'],
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', { class: 'n-popover-stub' }, [
|
||||
slots.trigger?.(),
|
||||
slots.default?.(),
|
||||
])
|
||||
},
|
||||
}),
|
||||
NList: defineComponent({
|
||||
name: 'NList',
|
||||
props: ['hoverable', 'clickable'],
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', { class: 'n-list-stub' }, slots.default?.())
|
||||
},
|
||||
}),
|
||||
NListItem: defineComponent({
|
||||
name: 'NListItem',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', { class: 'n-list-item-stub' }, slots.default?.())
|
||||
},
|
||||
}),
|
||||
NTag: defineComponent({
|
||||
name: 'NTag',
|
||||
props: ['size', 'bordered', 'type'],
|
||||
setup(_, { slots }) {
|
||||
return () => h('span', { class: 'n-tag-stub' }, slots.default?.())
|
||||
},
|
||||
}),
|
||||
NEmpty: defineComponent({
|
||||
name: 'NEmpty',
|
||||
props: ['description'],
|
||||
setup(props) {
|
||||
return () => h('div', { class: 'n-empty-stub' }, props.description)
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
import StatusWidget from '@/components/layout/StatusWidget.vue'
|
||||
|
||||
describe('StatusWidget', () => {
|
||||
const defaultProps = {
|
||||
label: 'Test Label',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render label', () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: defaultProps,
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('Test Label')
|
||||
})
|
||||
|
||||
it('should render label with custom color', () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
color: '#ff0000',
|
||||
},
|
||||
})
|
||||
|
||||
const trigger = wrapper.find('.widget-trigger')
|
||||
expect(trigger.attributes('style')).toContain('color: rgb(255, 0, 0)')
|
||||
})
|
||||
|
||||
it('should use default color when not provided', () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: defaultProps,
|
||||
})
|
||||
|
||||
const trigger = wrapper.find('.widget-trigger')
|
||||
expect(trigger.attributes('style')).toContain('color: rgb(204, 204, 204)')
|
||||
})
|
||||
|
||||
it('should emit trigger-click when clicked', async () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: defaultProps,
|
||||
})
|
||||
|
||||
await wrapper.find('.widget-trigger').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('trigger-click')).toBeTruthy()
|
||||
expect(wrapper.emitted('trigger-click')?.length).toBe(1)
|
||||
})
|
||||
|
||||
describe('with disablePopover', () => {
|
||||
it('should skip popover when disablePopover is true', () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
disablePopover: true,
|
||||
},
|
||||
})
|
||||
|
||||
// Should not have popover wrapper.
|
||||
expect(wrapper.find('.n-popover-stub').exists()).toBe(false)
|
||||
// But should still have trigger.
|
||||
expect(wrapper.find('.widget-trigger').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should emit trigger-click when disablePopover is true', async () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
disablePopover: true,
|
||||
},
|
||||
})
|
||||
|
||||
await wrapper.find('.widget-trigger').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('trigger-click')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('list mode', () => {
|
||||
const domainItems = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Domain One',
|
||||
desc: 'Description one',
|
||||
is_open: true,
|
||||
max_realm: 'Level 5',
|
||||
danger_prob: 0.3,
|
||||
drop_prob: 0.5,
|
||||
cd_years: 10,
|
||||
open_prob: 0.1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Domain Two',
|
||||
desc: 'Description two',
|
||||
is_open: false,
|
||||
max_realm: 'Level 3',
|
||||
danger_prob: 0.2,
|
||||
drop_prob: 0.4,
|
||||
cd_years: 5,
|
||||
open_prob: 0.2,
|
||||
},
|
||||
]
|
||||
|
||||
it('should render list mode with items', () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
mode: 'list',
|
||||
items: domainItems,
|
||||
title: 'Domain List',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.list-header').text()).toBe('Domain List')
|
||||
expect(wrapper.findAll('.n-list-item-stub').length).toBe(2)
|
||||
})
|
||||
|
||||
it('should show empty state when no items', () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
mode: 'list',
|
||||
items: [],
|
||||
emptyText: 'No data available',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.n-empty-stub').exists()).toBe(true)
|
||||
expect(wrapper.find('.n-empty-stub').text()).toBe('No data available')
|
||||
})
|
||||
|
||||
it('should use default emptyText when not provided', () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
mode: 'list',
|
||||
items: [],
|
||||
},
|
||||
})
|
||||
|
||||
// Default is Chinese text from props defaults.
|
||||
expect(wrapper.find('.n-empty-stub').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should render domain item details', () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
mode: 'list',
|
||||
items: [domainItems[0]],
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('Domain One')
|
||||
expect(wrapper.text()).toContain('Description one')
|
||||
expect(wrapper.text()).toContain('30%') // danger_prob formatted.
|
||||
expect(wrapper.text()).toContain('50%') // drop_prob formatted.
|
||||
})
|
||||
|
||||
it('should apply is-closed class for closed domains', () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
mode: 'list',
|
||||
items: [domainItems[1]], // is_open: false.
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.domain-item.is-closed').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should not apply is-closed class for open domains', () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
mode: 'list',
|
||||
items: [domainItems[0]], // is_open: true.
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.domain-item.is-closed').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('single mode', () => {
|
||||
it('should render single mode slot', () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
mode: 'single',
|
||||
},
|
||||
slots: {
|
||||
single: '<div class="custom-single">Custom Content</div>',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.custom-single').exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('Custom Content')
|
||||
})
|
||||
})
|
||||
|
||||
it('should render divider', () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: defaultProps,
|
||||
})
|
||||
|
||||
expect(wrapper.find('.divider').exists()).toBe(true)
|
||||
expect(wrapper.find('.divider').text()).toBe('|')
|
||||
})
|
||||
|
||||
it('should handle title in list mode', () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
mode: 'list',
|
||||
title: 'Custom Title',
|
||||
items: [{ id: 1, name: 'Test', desc: '', is_open: true, max_realm: '', danger_prob: 0, drop_prob: 0, cd_years: 0, open_prob: 0 }],
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.list-header').text()).toBe('Custom Title')
|
||||
})
|
||||
|
||||
it('should hide title when not provided', () => {
|
||||
const wrapper = mount(StatusWidget, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
mode: 'list',
|
||||
items: [],
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.list-header').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
163
web/src/__tests__/components/TagList.test.ts
Normal file
163
web/src/__tests__/components/TagList.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
// Mock getEntityColor.
|
||||
const mockGetEntityColor = vi.fn()
|
||||
|
||||
vi.mock('@/utils/theme', () => ({
|
||||
getEntityColor: (entity: any) => mockGetEntityColor(entity),
|
||||
}))
|
||||
|
||||
import TagList from '@/components/game/panels/info/components/TagList.vue'
|
||||
|
||||
describe('TagList', () => {
|
||||
const defaultTags = [
|
||||
{ id: '1', name: 'Tag One' },
|
||||
{ id: '2', name: 'Tag Two' },
|
||||
{ id: '3', name: 'Tag Three' },
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetEntityColor.mockReturnValue('#ff0000')
|
||||
})
|
||||
|
||||
it('should render all tags', () => {
|
||||
const wrapper = mount(TagList, {
|
||||
props: {
|
||||
tags: defaultTags,
|
||||
},
|
||||
})
|
||||
|
||||
const tags = wrapper.findAll('.tag')
|
||||
expect(tags.length).toBe(3)
|
||||
expect(tags[0].text()).toBe('Tag One')
|
||||
expect(tags[1].text()).toBe('Tag Two')
|
||||
expect(tags[2].text()).toBe('Tag Three')
|
||||
})
|
||||
|
||||
it('should apply border color from getEntityColor', () => {
|
||||
mockGetEntityColor.mockImplementation((tag) => {
|
||||
if (tag.id === '1') return '#ff0000'
|
||||
if (tag.id === '2') return '#00ff00'
|
||||
return '#0000ff'
|
||||
})
|
||||
|
||||
const wrapper = mount(TagList, {
|
||||
props: {
|
||||
tags: defaultTags,
|
||||
},
|
||||
})
|
||||
|
||||
const tags = wrapper.findAll('.tag')
|
||||
|
||||
expect(mockGetEntityColor).toHaveBeenCalledTimes(3)
|
||||
expect(tags[0].attributes('style')).toContain('border-color: rgb(255, 0, 0)')
|
||||
expect(tags[1].attributes('style')).toContain('border-color: rgb(0, 255, 0)')
|
||||
expect(tags[2].attributes('style')).toContain('border-color: rgb(0, 0, 255)')
|
||||
})
|
||||
|
||||
it('should emit click with tag item when tag is clicked', async () => {
|
||||
const wrapper = mount(TagList, {
|
||||
props: {
|
||||
tags: defaultTags,
|
||||
},
|
||||
})
|
||||
|
||||
const tags = wrapper.findAll('.tag')
|
||||
await tags[1].trigger('click')
|
||||
|
||||
expect(wrapper.emitted('click')).toBeTruthy()
|
||||
expect(wrapper.emitted('click')?.length).toBe(1)
|
||||
expect(wrapper.emitted('click')?.[0]).toEqual([defaultTags[1]])
|
||||
})
|
||||
|
||||
it('should emit click with correct tag on each click', async () => {
|
||||
const wrapper = mount(TagList, {
|
||||
props: {
|
||||
tags: defaultTags,
|
||||
},
|
||||
})
|
||||
|
||||
const tags = wrapper.findAll('.tag')
|
||||
await tags[0].trigger('click')
|
||||
await tags[2].trigger('click')
|
||||
|
||||
expect(wrapper.emitted('click')?.length).toBe(2)
|
||||
expect(wrapper.emitted('click')?.[0]).toEqual([defaultTags[0]])
|
||||
expect(wrapper.emitted('click')?.[1]).toEqual([defaultTags[2]])
|
||||
})
|
||||
|
||||
it('should handle empty tags array', () => {
|
||||
const wrapper = mount(TagList, {
|
||||
props: {
|
||||
tags: [],
|
||||
},
|
||||
})
|
||||
|
||||
const tags = wrapper.findAll('.tag')
|
||||
expect(tags.length).toBe(0)
|
||||
expect(wrapper.find('.tags-container').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should use tag name as key when id is not available', () => {
|
||||
const tagsWithoutId = [
|
||||
{ name: 'No ID Tag 1' },
|
||||
{ name: 'No ID Tag 2' },
|
||||
]
|
||||
|
||||
const wrapper = mount(TagList, {
|
||||
props: {
|
||||
tags: tagsWithoutId,
|
||||
},
|
||||
})
|
||||
|
||||
const tags = wrapper.findAll('.tag')
|
||||
expect(tags.length).toBe(2)
|
||||
expect(tags[0].text()).toBe('No ID Tag 1')
|
||||
expect(tags[1].text()).toBe('No ID Tag 2')
|
||||
})
|
||||
|
||||
it('should handle single tag', () => {
|
||||
const singleTag = [{ id: '1', name: 'Only Tag' }]
|
||||
|
||||
const wrapper = mount(TagList, {
|
||||
props: {
|
||||
tags: singleTag,
|
||||
},
|
||||
})
|
||||
|
||||
const tags = wrapper.findAll('.tag')
|
||||
expect(tags.length).toBe(1)
|
||||
expect(tags[0].text()).toBe('Only Tag')
|
||||
})
|
||||
|
||||
it('should handle undefined color gracefully', () => {
|
||||
mockGetEntityColor.mockReturnValue(undefined)
|
||||
|
||||
const wrapper = mount(TagList, {
|
||||
props: {
|
||||
tags: defaultTags,
|
||||
},
|
||||
})
|
||||
|
||||
// Should not throw, tags should still render.
|
||||
const tags = wrapper.findAll('.tag')
|
||||
expect(tags.length).toBe(3)
|
||||
})
|
||||
|
||||
it('should handle tags with additional properties', () => {
|
||||
const tagsWithExtra = [
|
||||
{ id: '1', name: 'Tag', grade: 'SSR', rarity: 'epic' },
|
||||
]
|
||||
|
||||
const wrapper = mount(TagList, {
|
||||
props: {
|
||||
tags: tagsWithExtra,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.tag').text()).toBe('Tag')
|
||||
expect(mockGetEntityColor).toHaveBeenCalledWith(tagsWithExtra[0])
|
||||
})
|
||||
})
|
||||
@@ -144,4 +144,254 @@ describe('useGameInit', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('pollInitStatus', () => {
|
||||
it('should call onIdle callback when status changes to idle', async () => {
|
||||
const onIdleMock = vi.fn()
|
||||
|
||||
// First call returns non-idle, second call returns idle.
|
||||
vi.spyOn(systemStore, 'fetchInitStatus')
|
||||
.mockResolvedValueOnce(createMockStatus({ status: 'initializing' }))
|
||||
.mockResolvedValueOnce(createMockStatus({ status: 'idle' }))
|
||||
|
||||
const TestComponent = createTestComponent({ onIdle: onIdleMock })
|
||||
const wrapper = mount(TestComponent)
|
||||
|
||||
// First poll - initializing.
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
await nextTick()
|
||||
|
||||
// Second poll - idle.
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
await nextTick()
|
||||
|
||||
expect(onIdleMock).toHaveBeenCalled()
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('should reset world and system when returning to idle from initialized state', async () => {
|
||||
const resetSpy = vi.spyOn(worldStore, 'reset')
|
||||
const setInitializedSpy = vi.spyOn(systemStore, 'setInitialized')
|
||||
|
||||
// Simulate already initialized.
|
||||
systemStore.setInitialized(true)
|
||||
|
||||
vi.spyOn(systemStore, 'fetchInitStatus')
|
||||
.mockResolvedValue(createMockStatus({ status: 'idle' }))
|
||||
|
||||
const TestComponent = createTestComponent()
|
||||
const wrapper = mount(TestComponent)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
await nextTick()
|
||||
|
||||
expect(setInitializedSpy).toHaveBeenCalledWith(false)
|
||||
expect(resetSpy).toHaveBeenCalled()
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('should preload map when phase is in MAP_READY', async () => {
|
||||
const preloadMapSpy = vi.spyOn(worldStore, 'preloadMap')
|
||||
|
||||
vi.spyOn(systemStore, 'fetchInitStatus')
|
||||
.mockResolvedValue(createMockStatus({
|
||||
status: 'initializing',
|
||||
phase_name: 'initializing_sects' // This is in MAP_READY.
|
||||
}))
|
||||
|
||||
const TestComponent = createTestComponent()
|
||||
const wrapper = mount(TestComponent)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
await nextTick()
|
||||
|
||||
expect(preloadMapSpy).toHaveBeenCalled()
|
||||
expect(wrapper.vm.mapPreloaded).toBe(true)
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('should not preload map twice', async () => {
|
||||
const preloadMapSpy = vi.spyOn(worldStore, 'preloadMap')
|
||||
|
||||
vi.spyOn(systemStore, 'fetchInitStatus')
|
||||
.mockResolvedValue(createMockStatus({
|
||||
status: 'initializing',
|
||||
phase_name: 'initializing_sects'
|
||||
}))
|
||||
|
||||
const TestComponent = createTestComponent()
|
||||
const wrapper = mount(TestComponent)
|
||||
|
||||
// First poll.
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
await nextTick()
|
||||
|
||||
// Second poll.
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
await nextTick()
|
||||
|
||||
// Should only be called once.
|
||||
expect(preloadMapSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('should preload avatars when phase is in AVATAR_READY', async () => {
|
||||
const preloadAvatarsSpy = vi.spyOn(worldStore, 'preloadAvatars')
|
||||
|
||||
vi.spyOn(systemStore, 'fetchInitStatus')
|
||||
.mockResolvedValue(createMockStatus({
|
||||
status: 'initializing',
|
||||
phase_name: 'checking_llm' // This is in AVATAR_READY.
|
||||
}))
|
||||
|
||||
const TestComponent = createTestComponent()
|
||||
const wrapper = mount(TestComponent)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
await nextTick()
|
||||
|
||||
expect(preloadAvatarsSpy).toHaveBeenCalled()
|
||||
expect(wrapper.vm.avatarsPreloaded).toBe(true)
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('should initialize game when status transitions to ready', async () => {
|
||||
const initializeSpy = vi.spyOn(worldStore, 'initialize').mockResolvedValue(undefined)
|
||||
const setInitializedSpy = vi.spyOn(systemStore, 'setInitialized')
|
||||
|
||||
// First call returns initializing, second call returns ready.
|
||||
vi.spyOn(systemStore, 'fetchInitStatus')
|
||||
.mockResolvedValueOnce(createMockStatus({ status: 'initializing' }))
|
||||
.mockResolvedValueOnce(createMockStatus({ status: 'ready' }))
|
||||
|
||||
const TestComponent = createTestComponent()
|
||||
const wrapper = mount(TestComponent)
|
||||
|
||||
// First poll - initializing.
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
await nextTick()
|
||||
|
||||
// Second poll - ready.
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
await nextTick()
|
||||
|
||||
expect(initializeSpy).toHaveBeenCalled()
|
||||
expect(setInitializedSpy).toHaveBeenCalledWith(true)
|
||||
expect(mockLoadBaseTextures).toHaveBeenCalled()
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('should handle fetch error gracefully', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
vi.spyOn(systemStore, 'fetchInitStatus')
|
||||
.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const TestComponent = createTestComponent()
|
||||
const wrapper = mount(TestComponent)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
await nextTick()
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Failed to fetch init status:', expect.any(Error))
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('should return early if fetchInitStatus returns null', async () => {
|
||||
const preloadMapSpy = vi.spyOn(worldStore, 'preloadMap')
|
||||
|
||||
vi.spyOn(systemStore, 'fetchInitStatus')
|
||||
.mockResolvedValue(null as any)
|
||||
|
||||
const TestComponent = createTestComponent()
|
||||
const wrapper = mount(TestComponent)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
await nextTick()
|
||||
|
||||
// Nothing should be called because res is null.
|
||||
expect(preloadMapSpy).not.toHaveBeenCalled()
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('initializeGame', () => {
|
||||
it('should reset world when already initialized', async () => {
|
||||
const resetSpy = vi.spyOn(worldStore, 'reset')
|
||||
const initializeSpy = vi.spyOn(worldStore, 'initialize').mockResolvedValue(undefined)
|
||||
|
||||
// Mark as initialized.
|
||||
systemStore.setInitialized(true)
|
||||
|
||||
vi.spyOn(systemStore, 'fetchInitStatus')
|
||||
.mockResolvedValue(createMockStatus({ status: 'idle' }))
|
||||
|
||||
const TestComponent = createTestComponent()
|
||||
const wrapper = mount(TestComponent)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
await nextTick()
|
||||
|
||||
// Manually call initializeGame.
|
||||
await wrapper.vm.initializeGame()
|
||||
|
||||
expect(resetSpy).toHaveBeenCalled()
|
||||
expect(initializeSpy).toHaveBeenCalled()
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('should init socket if not connected', async () => {
|
||||
const initSpy = vi.spyOn(socketStore, 'init')
|
||||
const initializeSpy = vi.spyOn(worldStore, 'initialize').mockResolvedValue(undefined)
|
||||
|
||||
vi.spyOn(systemStore, 'fetchInitStatus')
|
||||
.mockResolvedValue(createMockStatus({ status: 'idle' }))
|
||||
|
||||
const TestComponent = createTestComponent()
|
||||
const wrapper = mount(TestComponent)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
await nextTick()
|
||||
|
||||
// Ensure socket is not connected.
|
||||
expect(socketStore.isConnected).toBe(false)
|
||||
|
||||
await wrapper.vm.initializeGame()
|
||||
|
||||
expect(initSpy).toHaveBeenCalled()
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('stopPolling', () => {
|
||||
it('should clear interval when called', async () => {
|
||||
const TestComponent = createTestComponent()
|
||||
const wrapper = mount(TestComponent)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
await nextTick()
|
||||
|
||||
// Stop polling.
|
||||
wrapper.vm.stopPolling()
|
||||
|
||||
// Verify no more polls happen.
|
||||
vi.clearAllMocks()
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
|
||||
expect(systemStore.fetchInitStatus).not.toHaveBeenCalled()
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
161
web/src/__tests__/stores/setting.test.ts
Normal file
161
web/src/__tests__/stores/setting.test.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
|
||||
// Use vi.hoisted to define mock functions that will be used by vi.mock.
|
||||
const { mockSetLanguage } = vi.hoisted(() => ({
|
||||
mockSetLanguage: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
// Create a fresh mock i18n object for each test.
|
||||
let mockI18nLocale: { value: string } | string = { value: 'zh-CN' }
|
||||
let mockI18nMode = 'composition'
|
||||
|
||||
// Mock i18n.
|
||||
vi.mock('@/locales', () => ({
|
||||
default: {
|
||||
get mode() { return mockI18nMode },
|
||||
global: {
|
||||
get locale() { return mockI18nLocale },
|
||||
set locale(val) { mockI18nLocale = val },
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock systemApi.
|
||||
vi.mock('@/api/modules/system', () => ({
|
||||
systemApi: {
|
||||
setLanguage: mockSetLanguage,
|
||||
},
|
||||
}))
|
||||
|
||||
import { useSettingStore } from '@/stores/setting'
|
||||
|
||||
describe('useSettingStore', () => {
|
||||
let store: ReturnType<typeof useSettingStore>
|
||||
let localStorageMock: Record<string, string>
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset localStorage mock.
|
||||
localStorageMock = {}
|
||||
vi.spyOn(Storage.prototype, 'getItem').mockImplementation((key: string) => {
|
||||
return localStorageMock[key] || null
|
||||
})
|
||||
vi.spyOn(Storage.prototype, 'setItem').mockImplementation((key: string, value: string) => {
|
||||
localStorageMock[key] = value
|
||||
})
|
||||
|
||||
// Reset mocks.
|
||||
vi.clearAllMocks()
|
||||
mockI18nLocale = { value: 'zh-CN' }
|
||||
mockI18nMode = 'composition'
|
||||
|
||||
setActivePinia(createPinia())
|
||||
store = useSettingStore()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('initial state', () => {
|
||||
it('should read locale from localStorage if available', () => {
|
||||
// Reset with new localStorage value.
|
||||
localStorageMock = { app_locale: 'en-US' }
|
||||
setActivePinia(createPinia())
|
||||
const newStore = useSettingStore()
|
||||
|
||||
expect(newStore.locale).toBe('en-US')
|
||||
})
|
||||
|
||||
it('should default to zh-CN if localStorage is empty', () => {
|
||||
expect(store.locale).toBe('zh-CN')
|
||||
})
|
||||
})
|
||||
|
||||
describe('setLocale', () => {
|
||||
it('should update locale value', async () => {
|
||||
await store.setLocale('en-US')
|
||||
|
||||
expect(store.locale).toBe('en-US')
|
||||
})
|
||||
|
||||
it('should save locale to localStorage', async () => {
|
||||
await store.setLocale('zh-TW')
|
||||
|
||||
expect(localStorageMock.app_locale).toBe('zh-TW')
|
||||
})
|
||||
|
||||
it('should update i18n.global.locale in composition mode', async () => {
|
||||
mockI18nMode = 'composition'
|
||||
mockI18nLocale = { value: 'zh-CN' }
|
||||
|
||||
await store.setLocale('en-US')
|
||||
|
||||
expect((mockI18nLocale as { value: string }).value).toBe('en-US')
|
||||
})
|
||||
|
||||
it('should update i18n.global.locale in legacy mode', async () => {
|
||||
mockI18nMode = 'legacy'
|
||||
mockI18nLocale = 'zh-CN'
|
||||
|
||||
await store.setLocale('en-US')
|
||||
|
||||
expect(mockI18nLocale).toBe('en-US')
|
||||
})
|
||||
|
||||
it('should update document.documentElement.lang', async () => {
|
||||
await store.setLocale('en-US')
|
||||
|
||||
expect(document.documentElement.lang).toBe('en')
|
||||
})
|
||||
|
||||
it('should set correct HTML lang for zh-CN', async () => {
|
||||
await store.setLocale('zh-CN')
|
||||
|
||||
expect(document.documentElement.lang).toBe('zh-CN')
|
||||
})
|
||||
|
||||
it('should set correct HTML lang for zh-TW', async () => {
|
||||
await store.setLocale('zh-TW')
|
||||
|
||||
expect(document.documentElement.lang).toBe('zh-TW')
|
||||
})
|
||||
|
||||
it('should call syncBackend after updating locale', async () => {
|
||||
await store.setLocale('en-US')
|
||||
|
||||
expect(mockSetLanguage).toHaveBeenCalledWith('en-US')
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncBackend', () => {
|
||||
it('should call systemApi.setLanguage with current locale', async () => {
|
||||
store.locale = 'zh-TW'
|
||||
|
||||
await store.syncBackend()
|
||||
|
||||
expect(mockSetLanguage).toHaveBeenCalledWith('zh-TW')
|
||||
})
|
||||
|
||||
it('should catch errors and log warning', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
mockSetLanguage.mockRejectedValueOnce(new Error('Network error'))
|
||||
|
||||
await store.syncBackend()
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Failed to sync language with backend:',
|
||||
expect.any(Error)
|
||||
)
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should not throw when API fails', async () => {
|
||||
mockSetLanguage.mockRejectedValueOnce(new Error('API error'))
|
||||
|
||||
// Should not throw.
|
||||
await expect(store.syncBackend()).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,35 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
|
||||
// Create mock functions at module level.
|
||||
const mockOn = vi.fn(() => vi.fn())
|
||||
const mockOnStatusChange = vi.fn(() => vi.fn())
|
||||
const mockConnect = vi.fn()
|
||||
const mockDisconnect = vi.fn()
|
||||
// Use vi.hoisted to define mocks before vi.mock is hoisted.
|
||||
const {
|
||||
mockOn,
|
||||
mockOnStatusChange,
|
||||
mockConnect,
|
||||
mockDisconnect,
|
||||
mockWorldStore,
|
||||
mockUiStore,
|
||||
mockMessage,
|
||||
} = vi.hoisted(() => ({
|
||||
mockOn: vi.fn(() => vi.fn()),
|
||||
mockOnStatusChange: vi.fn(() => vi.fn()),
|
||||
mockConnect: vi.fn(),
|
||||
mockDisconnect: vi.fn(),
|
||||
mockWorldStore: {
|
||||
handleTick: vi.fn(),
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
mockUiStore: {
|
||||
selectedTarget: null as { type: string; id: string } | null,
|
||||
refreshDetail: vi.fn(),
|
||||
},
|
||||
mockMessage: {
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
success: vi.fn(),
|
||||
info: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Store callbacks for testing message handling.
|
||||
let messageCallback: ((data: any) => void) | null = null
|
||||
@@ -27,16 +51,23 @@ vi.mock('@/api/socket', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock world and ui stores.
|
||||
const mockWorldStore = {
|
||||
handleTick: vi.fn(),
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
vi.mock('@/utils/discreteApi', () => ({
|
||||
message: mockMessage,
|
||||
}))
|
||||
|
||||
const mockUiStore = {
|
||||
selectedTarget: null as { type: string; id: string } | null,
|
||||
refreshDetail: vi.fn(),
|
||||
}
|
||||
// Mock i18n with mutable module-level variables.
|
||||
let mockI18nMode = 'composition'
|
||||
let mockI18nLocale: any = { value: 'zh-CN' }
|
||||
|
||||
vi.mock('@/locales', () => ({
|
||||
default: {
|
||||
get mode() { return mockI18nMode },
|
||||
global: {
|
||||
get locale() { return mockI18nLocale },
|
||||
set locale(val) { mockI18nLocale = val },
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/world', () => ({
|
||||
useWorldStore: () => mockWorldStore,
|
||||
@@ -54,7 +85,7 @@ describe('useSocketStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
store = useSocketStore()
|
||||
|
||||
|
||||
// Reset mocks and callbacks.
|
||||
vi.clearAllMocks()
|
||||
mockUiStore.selectedTarget = null
|
||||
@@ -62,6 +93,10 @@ describe('useSocketStore', () => {
|
||||
mockOnStatusChange.mockReturnValue(vi.fn())
|
||||
messageCallback = null
|
||||
statusCallback = null
|
||||
|
||||
// Reset i18n mock.
|
||||
mockI18nMode = 'composition'
|
||||
mockI18nLocale = { value: 'zh-CN' }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -215,6 +250,186 @@ describe('useSocketStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('toast message handling', () => {
|
||||
it('should show error toast for error level', () => {
|
||||
store.init()
|
||||
|
||||
messageCallback?.({ type: 'toast', level: 'error', message: 'Error message' })
|
||||
|
||||
expect(mockMessage.error).toHaveBeenCalledWith('Error message')
|
||||
})
|
||||
|
||||
it('should show warning toast for warning level', () => {
|
||||
store.init()
|
||||
|
||||
messageCallback?.({ type: 'toast', level: 'warning', message: 'Warning message' })
|
||||
|
||||
expect(mockMessage.warning).toHaveBeenCalledWith('Warning message')
|
||||
})
|
||||
|
||||
it('should show success toast for success level', () => {
|
||||
store.init()
|
||||
|
||||
messageCallback?.({ type: 'toast', level: 'success', message: 'Success message' })
|
||||
|
||||
expect(mockMessage.success).toHaveBeenCalledWith('Success message')
|
||||
})
|
||||
|
||||
it('should show info toast for info level', () => {
|
||||
store.init()
|
||||
|
||||
messageCallback?.({ type: 'toast', level: 'info', message: 'Info message' })
|
||||
|
||||
expect(mockMessage.info).toHaveBeenCalledWith('Info message')
|
||||
})
|
||||
|
||||
it('should show info toast for unknown level', () => {
|
||||
store.init()
|
||||
|
||||
messageCallback?.({ type: 'toast', level: 'unknown', message: 'Unknown level message' })
|
||||
|
||||
expect(mockMessage.info).toHaveBeenCalledWith('Unknown level message')
|
||||
})
|
||||
|
||||
it('should switch language in composition mode when language field is present', () => {
|
||||
mockI18nMode = 'composition'
|
||||
mockI18nLocale = { value: 'zh-CN' }
|
||||
const localStorageSpy = vi.spyOn(Storage.prototype, 'setItem')
|
||||
|
||||
store.init()
|
||||
messageCallback?.({ type: 'toast', level: 'info', message: 'Test', language: 'en-US' })
|
||||
|
||||
expect(mockI18nLocale.value).toBe('en-US')
|
||||
expect(localStorageSpy).toHaveBeenCalledWith('app_locale', 'en-US')
|
||||
localStorageSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should switch language in legacy mode when language field is present', () => {
|
||||
mockI18nMode = 'legacy'
|
||||
mockI18nLocale = 'zh-CN'
|
||||
const localStorageSpy = vi.spyOn(Storage.prototype, 'setItem')
|
||||
|
||||
store.init()
|
||||
messageCallback?.({ type: 'toast', level: 'info', message: 'Test', language: 'en-US' })
|
||||
|
||||
expect(mockI18nLocale).toBe('en-US')
|
||||
expect(localStorageSpy).toHaveBeenCalledWith('app_locale', 'en-US')
|
||||
localStorageSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should not switch language if same as current', () => {
|
||||
mockI18nMode = 'composition'
|
||||
mockI18nLocale = { value: 'en-US' }
|
||||
const localStorageSpy = vi.spyOn(Storage.prototype, 'setItem')
|
||||
|
||||
store.init()
|
||||
messageCallback?.({ type: 'toast', level: 'info', message: 'Test', language: 'en-US' })
|
||||
|
||||
// Should not call setItem if language is same.
|
||||
expect(localStorageSpy).not.toHaveBeenCalled()
|
||||
localStorageSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should update document.documentElement.lang for zh-CN', () => {
|
||||
mockI18nMode = 'composition'
|
||||
mockI18nLocale = { value: 'en-US' }
|
||||
|
||||
store.init()
|
||||
messageCallback?.({ type: 'toast', level: 'info', message: 'Test', language: 'zh-CN' })
|
||||
|
||||
expect(document.documentElement.lang).toBe('zh-CN')
|
||||
})
|
||||
|
||||
it('should update document.documentElement.lang for en-US', () => {
|
||||
mockI18nMode = 'composition'
|
||||
mockI18nLocale = { value: 'zh-CN' }
|
||||
|
||||
store.init()
|
||||
messageCallback?.({ type: 'toast', level: 'info', message: 'Test', language: 'en-US' })
|
||||
|
||||
expect(document.documentElement.lang).toBe('en')
|
||||
})
|
||||
|
||||
it('should handle language switch errors gracefully', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
// Create a getter that throws.
|
||||
mockI18nMode = 'composition'
|
||||
const errorObj = {
|
||||
get value() { throw new Error('Test error') },
|
||||
set value(_v) { throw new Error('Test error') },
|
||||
}
|
||||
mockI18nLocale = errorObj
|
||||
|
||||
store.init()
|
||||
// Should not throw.
|
||||
messageCallback?.({ type: 'toast', level: 'info', message: 'Test', language: 'en-US' })
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('[Socket] Failed to switch language:', expect.any(Error))
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('llm_config_required alert handling', () => {
|
||||
it('should call alert after timeout for llm_config_required', async () => {
|
||||
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {})
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
store.init()
|
||||
messageCallback?.({ type: 'llm_config_required', error: 'LLM error' })
|
||||
|
||||
// Run the setTimeout.
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
|
||||
expect(alertSpy).toHaveBeenCalled()
|
||||
alertSpy.mockRestore()
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should use default message when error is not provided', async () => {
|
||||
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {})
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
store.init()
|
||||
messageCallback?.({ type: 'llm_config_required' })
|
||||
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
|
||||
expect(alertSpy).toHaveBeenCalledWith(expect.stringContaining('LLM 连接失败'))
|
||||
alertSpy.mockRestore()
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('game_reinitialized alert handling', () => {
|
||||
it('should call alert after timeout for game_reinitialized', async () => {
|
||||
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {})
|
||||
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
store.init()
|
||||
messageCallback?.({ type: 'game_reinitialized', message: 'Game restarted' })
|
||||
|
||||
await vi.advanceTimersByTimeAsync(300)
|
||||
|
||||
expect(alertSpy).toHaveBeenCalledWith('Game restarted')
|
||||
alertSpy.mockRestore()
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should use default message when message is not provided', async () => {
|
||||
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {})
|
||||
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
store.init()
|
||||
messageCallback?.({ type: 'game_reinitialized' })
|
||||
|
||||
await vi.advanceTimersByTimeAsync(300)
|
||||
|
||||
expect(alertSpy).toHaveBeenCalledWith('LLM 配置成功,游戏已重新初始化')
|
||||
alertSpy.mockRestore()
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('status change handling', () => {
|
||||
it('should update isConnected when status changes to connected', () => {
|
||||
store.init()
|
||||
|
||||
@@ -16,6 +16,8 @@ export default defineConfig({
|
||||
'node_modules/',
|
||||
'src/__tests__/',
|
||||
'**/*.d.ts',
|
||||
'vite.config.ts',
|
||||
'vitest.config.ts',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user