mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-17 05:43:51 +02:00
add unit test
This commit is contained in:
218
src/components/__tests__/AvatarInfo.test.js
Normal file
218
src/components/__tests__/AvatarInfo.test.js
Normal file
@@ -0,0 +1,218 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import AvatarInfo from '../AvatarInfo.vue';
|
||||
import en from '../../localization/en.json';
|
||||
|
||||
vi.mock('../../views/Feed/Feed.vue', () => ({
|
||||
default: { template: '<div />' }
|
||||
}));
|
||||
vi.mock('../../views/Feed/columns.jsx', () => ({
|
||||
columns: []
|
||||
}));
|
||||
vi.mock('../../plugin/router', () => ({
|
||||
router: {
|
||||
beforeEach: vi.fn(),
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
currentRoute: ref({ path: '/', name: '', meta: {} }),
|
||||
isReady: vi.fn().mockResolvedValue(true)
|
||||
},
|
||||
initRouter: vi.fn()
|
||||
}));
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
useRouter: vi.fn(() => ({
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
currentRoute: ref({ path: '/', name: '', meta: {} })
|
||||
}))
|
||||
};
|
||||
});
|
||||
vi.mock('../../plugin/interopApi', () => ({
|
||||
initInteropApi: vi.fn()
|
||||
}));
|
||||
vi.mock('../../service/database', () => ({
|
||||
database: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, prop) => {
|
||||
if (prop === '__esModule') return false;
|
||||
return vi.fn().mockResolvedValue(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
}));
|
||||
vi.mock('../../service/config', () => ({
|
||||
default: {
|
||||
init: vi.fn(),
|
||||
getString: vi
|
||||
.fn()
|
||||
.mockImplementation((_key, defaultValue) => defaultValue ?? '{}'),
|
||||
setString: vi.fn(),
|
||||
getBool: vi
|
||||
.fn()
|
||||
.mockImplementation((_key, defaultValue) => defaultValue ?? false),
|
||||
setBool: vi.fn(),
|
||||
getInt: vi
|
||||
.fn()
|
||||
.mockImplementation((_key, defaultValue) => defaultValue ?? 0),
|
||||
setInt: vi.fn(),
|
||||
getFloat: vi
|
||||
.fn()
|
||||
.mockImplementation((_key, defaultValue) => defaultValue ?? 0),
|
||||
setFloat: vi.fn(),
|
||||
getObject: vi.fn().mockReturnValue(null),
|
||||
setObject: vi.fn(),
|
||||
getArray: vi.fn().mockReturnValue([]),
|
||||
setArray: vi.fn(),
|
||||
remove: vi.fn()
|
||||
}
|
||||
}));
|
||||
vi.mock('../../service/jsonStorage', () => ({
|
||||
default: vi.fn()
|
||||
}));
|
||||
vi.mock('../../service/watchState', () => ({
|
||||
watchState: { isLoggedIn: false }
|
||||
}));
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
legacy: false,
|
||||
globalInjection: false,
|
||||
missingWarn: false,
|
||||
fallbackWarn: false,
|
||||
messages: { en }
|
||||
});
|
||||
|
||||
const stubs = {
|
||||
TooltipWrapper: {
|
||||
template:
|
||||
'<span class="tooltip"><slot /><slot name="content" /></span>',
|
||||
props: ['content']
|
||||
}
|
||||
};
|
||||
|
||||
function mountAvatarInfo(props = {}, storeOverrides = {}) {
|
||||
const pinia = createTestingPinia({
|
||||
stubActions: true,
|
||||
initialState: {
|
||||
Avatar: {},
|
||||
...storeOverrides
|
||||
}
|
||||
});
|
||||
return mount(AvatarInfo, {
|
||||
props,
|
||||
global: {
|
||||
plugins: [i18n, pinia],
|
||||
stubs
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('AvatarInfo.vue', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('avatar name display', () => {
|
||||
test('shows hintavatarname when hintownerid is provided', () => {
|
||||
const wrapper = mountAvatarInfo({
|
||||
imageurl: 'https://example.com/avatar.png',
|
||||
hintownerid: 'usr_owner_123',
|
||||
hintavatarname: 'Cool Avatar'
|
||||
});
|
||||
expect(wrapper.text()).toContain('Cool Avatar');
|
||||
});
|
||||
|
||||
test('shows empty when no imageurl', () => {
|
||||
const wrapper = mountAvatarInfo({});
|
||||
expect(wrapper.text().trim()).toBe('');
|
||||
});
|
||||
|
||||
test('does not show hintavatarname if it is not a string', () => {
|
||||
const wrapper = mountAvatarInfo({
|
||||
imageurl: 'https://example.com/avatar.png',
|
||||
hintownerid: 'usr_owner_123',
|
||||
hintavatarname: { notAString: true }
|
||||
});
|
||||
// avatarName stays empty since hintavatarname is not a string
|
||||
expect(wrapper.text()).not.toContain('notAString');
|
||||
});
|
||||
});
|
||||
|
||||
describe('avatar type (own vs public)', () => {
|
||||
test('shows lock icon when owner matches userid (own avatar)', () => {
|
||||
const wrapper = mountAvatarInfo({
|
||||
imageurl: 'https://example.com/avatar.png',
|
||||
userid: 'usr_owner_123',
|
||||
hintownerid: 'usr_owner_123',
|
||||
hintavatarname: 'My Avatar'
|
||||
});
|
||||
expect(wrapper.find('.lucide-lock').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('does not show lock when owner differs from userid (public)', () => {
|
||||
const wrapper = mountAvatarInfo({
|
||||
imageurl: 'https://example.com/avatar.png',
|
||||
userid: 'usr_viewer_456',
|
||||
hintownerid: 'usr_owner_123',
|
||||
hintavatarname: 'Someone Avatar'
|
||||
});
|
||||
expect(wrapper.find('.lucide-lock').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('does not show lock when userid is undefined', () => {
|
||||
const wrapper = mountAvatarInfo({
|
||||
imageurl: 'https://example.com/avatar.png',
|
||||
hintownerid: 'usr_owner_123',
|
||||
hintavatarname: 'Avatar'
|
||||
});
|
||||
expect(wrapper.find('.lucide-lock').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('avatar tags', () => {
|
||||
test('displays tags with content_ prefix stripped', () => {
|
||||
const wrapper = mountAvatarInfo({
|
||||
imageurl: 'https://example.com/avatar.png',
|
||||
hintownerid: 'usr_123',
|
||||
hintavatarname: 'Test',
|
||||
avatartags: [
|
||||
'content_horror',
|
||||
'content_gore',
|
||||
'content_adult_language'
|
||||
]
|
||||
});
|
||||
expect(wrapper.text()).toContain('horror');
|
||||
expect(wrapper.text()).toContain('gore');
|
||||
expect(wrapper.text()).toContain('adult_language');
|
||||
expect(wrapper.text()).not.toContain('content_horror');
|
||||
});
|
||||
|
||||
test('does not show tags section when avatartags is empty', () => {
|
||||
const wrapper = mountAvatarInfo({
|
||||
imageurl: 'https://example.com/avatar.png',
|
||||
hintownerid: 'usr_123',
|
||||
hintavatarname: 'Test'
|
||||
});
|
||||
expect(wrapper.find('.tooltip').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('click behavior', () => {
|
||||
test('does not call showAvatarAuthorDialog when no imageurl', async () => {
|
||||
const wrapper = mountAvatarInfo({});
|
||||
await wrapper.trigger('click');
|
||||
const { useAvatarStore } = await import('../../stores');
|
||||
const avatarStore = useAvatarStore();
|
||||
expect(avatarStore.showAvatarAuthorDialog).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
export function useTableHeight(tableRef, options = {}) {
|
||||
const containerRef = ref(null);
|
||||
const offset = options.offset ?? 127;
|
||||
const immediate = options.immediate ?? true;
|
||||
|
||||
let resizeObserver;
|
||||
|
||||
const setTableHeight = () => {
|
||||
if (!tableRef?.value || !containerRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
tableRef.value.tableProps = {
|
||||
...(tableRef.value.tableProps || {}),
|
||||
// @ts-ignore default is null
|
||||
height: containerRef.value.clientHeight - offset
|
||||
};
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (immediate) {
|
||||
setTableHeight();
|
||||
}
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
setTableHeight();
|
||||
});
|
||||
|
||||
if (containerRef.value) {
|
||||
resizeObserver.observe(containerRef.value);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
resizeObserver?.disconnect();
|
||||
});
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
setTableHeight
|
||||
};
|
||||
}
|
||||
189
src/shared/utils/__tests__/avatar.test.js
Normal file
189
src/shared/utils/__tests__/avatar.test.js
Normal file
@@ -0,0 +1,189 @@
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { getPlatformInfo, parseAvatarUrl, storeAvatarImage } from '../avatar';
|
||||
|
||||
vi.mock('../../../views/Feed/Feed.vue', () => ({
|
||||
default: { template: '<div />' }
|
||||
}));
|
||||
vi.mock('../../../views/Feed/columns.jsx', () => ({ columns: [] }));
|
||||
vi.mock('../../../plugin/router', () => ({
|
||||
router: {
|
||||
beforeEach: vi.fn(),
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
currentRoute: ref({ path: '/', name: '', meta: {} }),
|
||||
isReady: vi.fn().mockResolvedValue(true)
|
||||
},
|
||||
initRouter: vi.fn()
|
||||
}));
|
||||
|
||||
describe('storeAvatarImage', () => {
|
||||
function makeArgs(name, ownerId, createdAt = '2024-01-01T00:00:00Z') {
|
||||
return {
|
||||
params: { fileId: 'file_abc123' },
|
||||
json: {
|
||||
name,
|
||||
ownerId,
|
||||
versions: [{ created_at: createdAt }]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test('extracts avatar name from standard image file name', () => {
|
||||
const cache = new Map();
|
||||
const result = storeAvatarImage(
|
||||
makeArgs('Avatar - Cool Robot - Image - 2024.01.01', 'usr_owner1'),
|
||||
cache
|
||||
);
|
||||
expect(result.avatarName).toBe('Cool Robot');
|
||||
expect(result.ownerId).toBe('usr_owner1');
|
||||
expect(result.fileCreatedAt).toBe('2024-01-01T00:00:00Z');
|
||||
});
|
||||
|
||||
test('stores result in cachedAvatarNames map', () => {
|
||||
const cache = new Map();
|
||||
storeAvatarImage(
|
||||
makeArgs('Avatar - Test - Image - x', 'usr_123'),
|
||||
cache
|
||||
);
|
||||
expect(cache.has('file_abc123')).toBe(true);
|
||||
expect(cache.get('file_abc123').avatarName).toBe('Test');
|
||||
});
|
||||
|
||||
test('returns empty avatarName when name does not match pattern', () => {
|
||||
const cache = new Map();
|
||||
const result = storeAvatarImage(
|
||||
makeArgs('SomeOtherFileName.png', 'usr_456'),
|
||||
cache
|
||||
);
|
||||
expect(result.avatarName).toBe('');
|
||||
});
|
||||
|
||||
test('handles special characters in avatar name', () => {
|
||||
const cache = new Map();
|
||||
const result = storeAvatarImage(
|
||||
makeArgs('Avatar - ★ Fancy (Name) ★ - Image - v1', 'usr_789'),
|
||||
cache
|
||||
);
|
||||
expect(result.avatarName).toContain('Fancy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseAvatarUrl', () => {
|
||||
test('extracts avatar ID from valid avatar URL', () => {
|
||||
const result = parseAvatarUrl(
|
||||
'https://api.vrchat.cloud/file/avatar/avtr_12345-abcde'
|
||||
);
|
||||
expect(result).toBe('avtr_12345-abcde');
|
||||
});
|
||||
|
||||
test('returns null for non-avatar URL', () => {
|
||||
const result = parseAvatarUrl(
|
||||
'https://api.vrchat.cloud/api/1/worlds/wrld_12345'
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null for unrelated URL', () => {
|
||||
const result = parseAvatarUrl('https://example.com/something');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlatformInfo', () => {
|
||||
test('separates packages by platform', () => {
|
||||
const packages = [
|
||||
{
|
||||
platform: 'standalonewindows',
|
||||
performanceRating: 'Good',
|
||||
variant: 'standard'
|
||||
},
|
||||
{
|
||||
platform: 'android',
|
||||
performanceRating: 'Medium',
|
||||
variant: 'standard'
|
||||
},
|
||||
{ platform: 'ios', performanceRating: 'Poor', variant: 'standard' }
|
||||
];
|
||||
const result = getPlatformInfo(packages);
|
||||
expect(result.pc.platform).toBe('standalonewindows');
|
||||
expect(result.android.platform).toBe('android');
|
||||
expect(result.ios.platform).toBe('ios');
|
||||
});
|
||||
|
||||
test('skips non-standard/non-security variants', () => {
|
||||
const packages = [
|
||||
{
|
||||
platform: 'standalonewindows',
|
||||
performanceRating: 'Good',
|
||||
variant: 'impostor'
|
||||
}
|
||||
];
|
||||
const result = getPlatformInfo(packages);
|
||||
expect(result.pc).toEqual({});
|
||||
});
|
||||
|
||||
test('allows standard and security variants', () => {
|
||||
const packages = [
|
||||
{
|
||||
platform: 'standalonewindows',
|
||||
performanceRating: 'Good',
|
||||
variant: 'security'
|
||||
}
|
||||
];
|
||||
const result = getPlatformInfo(packages);
|
||||
expect(result.pc.platform).toBe('standalonewindows');
|
||||
});
|
||||
|
||||
test('skips None performanceRating if platform already has a rating', () => {
|
||||
const packages = [
|
||||
{
|
||||
platform: 'standalonewindows',
|
||||
performanceRating: 'Good',
|
||||
variant: 'standard'
|
||||
},
|
||||
{
|
||||
platform: 'standalonewindows',
|
||||
performanceRating: 'None',
|
||||
variant: 'standard'
|
||||
}
|
||||
];
|
||||
const result = getPlatformInfo(packages);
|
||||
expect(result.pc.performanceRating).toBe('Good');
|
||||
});
|
||||
|
||||
test('accepts None performanceRating when no prior entry exists', () => {
|
||||
const packages = [
|
||||
{
|
||||
platform: 'android',
|
||||
performanceRating: 'None',
|
||||
variant: 'standard'
|
||||
}
|
||||
];
|
||||
const result = getPlatformInfo(packages);
|
||||
expect(result.android.performanceRating).toBe('None');
|
||||
});
|
||||
|
||||
test('returns empty objects when input is undefined', () => {
|
||||
const result = getPlatformInfo(undefined);
|
||||
expect(result.pc).toEqual({});
|
||||
expect(result.android).toEqual({});
|
||||
expect(result.ios).toEqual({});
|
||||
});
|
||||
|
||||
test('returns empty objects for empty array', () => {
|
||||
const result = getPlatformInfo([]);
|
||||
expect(result.pc).toEqual({});
|
||||
expect(result.android).toEqual({});
|
||||
expect(result.ios).toEqual({});
|
||||
});
|
||||
|
||||
test('allows packages without variant (undefined)', () => {
|
||||
const packages = [
|
||||
{ platform: 'standalonewindows', performanceRating: 'Good' }
|
||||
];
|
||||
const result = getPlatformInfo(packages);
|
||||
expect(result.pc.platform).toBe('standalonewindows');
|
||||
});
|
||||
});
|
||||
196
src/stores/__tests__/launch.test.js
Normal file
196
src/stores/__tests__/launch.test.js
Normal file
@@ -0,0 +1,196 @@
|
||||
/* eslint-disable pretty-import/sort-import-groups */
|
||||
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import en from '../../localization/en.json';
|
||||
|
||||
vi.mock('../../views/Feed/Feed.vue', () => ({
|
||||
default: { template: '<div />' }
|
||||
}));
|
||||
vi.mock('../../views/Feed/columns.jsx', () => ({ columns: [] }));
|
||||
vi.mock('../../plugin/router', () => ({
|
||||
router: {
|
||||
beforeEach: vi.fn(),
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
currentRoute: ref({ path: '/', name: '', meta: {} }),
|
||||
isReady: vi.fn().mockResolvedValue(true)
|
||||
},
|
||||
initRouter: vi.fn()
|
||||
}));
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
useRouter: vi.fn(() => ({
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
currentRoute: ref({ path: '/', name: '', meta: {} })
|
||||
}))
|
||||
};
|
||||
});
|
||||
vi.mock('../../plugin/interopApi', () => ({ initInteropApi: vi.fn() }));
|
||||
vi.mock('../../service/database', () => ({
|
||||
database: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, prop) => {
|
||||
if (prop === '__esModule') return false;
|
||||
return vi.fn().mockResolvedValue(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
}));
|
||||
vi.mock('../../service/config', () => ({
|
||||
default: {
|
||||
init: vi.fn(),
|
||||
getString: vi.fn().mockResolvedValue(''),
|
||||
setString: vi.fn(),
|
||||
getBool: vi.fn().mockImplementation((_k, d) => d ?? false),
|
||||
setBool: vi.fn(),
|
||||
getInt: vi.fn().mockImplementation((_k, d) => d ?? 0),
|
||||
setInt: vi.fn(),
|
||||
getFloat: vi.fn().mockImplementation((_k, d) => d ?? 0),
|
||||
setFloat: vi.fn(),
|
||||
getObject: vi.fn().mockReturnValue(null),
|
||||
setObject: vi.fn(),
|
||||
getArray: vi.fn().mockReturnValue([]),
|
||||
setArray: vi.fn(),
|
||||
remove: vi.fn()
|
||||
}
|
||||
}));
|
||||
vi.mock('../../service/jsonStorage', () => ({ default: vi.fn() }));
|
||||
vi.mock('../../service/watchState', () => ({
|
||||
watchState: { isLoggedIn: false }
|
||||
}));
|
||||
vi.mock('vue-i18n', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
const i18n = actual.createI18n({
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
legacy: false,
|
||||
missingWarn: false,
|
||||
fallbackWarn: false,
|
||||
messages: { en }
|
||||
});
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => i18n.global
|
||||
};
|
||||
});
|
||||
|
||||
const mockGetInstanceShortName = vi.fn();
|
||||
vi.mock('../../api', () => ({
|
||||
instanceRequest: {
|
||||
getInstanceShortName: (...args) => mockGetInstanceShortName(...args),
|
||||
selfInvite: vi.fn().mockResolvedValue({})
|
||||
},
|
||||
miscRequest: {}
|
||||
}));
|
||||
vi.mock('vue-sonner', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
import { useLaunchStore } from '../launch';
|
||||
|
||||
describe('useLaunchStore', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useLaunchStore();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('showLaunchDialog', () => {
|
||||
test('sets dialog visible with tag and shortName', async () => {
|
||||
await store.showLaunchDialog(
|
||||
'wrld_123:456~friends(usr_abc)',
|
||||
'abc'
|
||||
);
|
||||
|
||||
expect(store.launchDialogData.visible).toBe(true);
|
||||
expect(store.launchDialogData.tag).toBe(
|
||||
'wrld_123:456~friends(usr_abc)'
|
||||
);
|
||||
expect(store.launchDialogData.shortName).toBe('abc');
|
||||
});
|
||||
|
||||
test('defaults shortName to null', async () => {
|
||||
await store.showLaunchDialog('wrld_123:456');
|
||||
|
||||
expect(store.launchDialogData.shortName).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('showLaunchOptions', () => {
|
||||
test('sets isLaunchOptionsDialogVisible to true', () => {
|
||||
expect(store.isLaunchOptionsDialogVisible).toBe(false);
|
||||
store.showLaunchOptions();
|
||||
expect(store.isLaunchOptionsDialogVisible).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLaunchUrl', () => {
|
||||
test('uses provided shortName for non-public instance', async () => {
|
||||
const url = await store.getLaunchUrl(
|
||||
'wrld_123:456~friends(usr_abc)',
|
||||
'myShort'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'vrchat://launch?ref=vrcx.app&id=wrld_123:456~friends(usr_abc)&shortName=myShort'
|
||||
);
|
||||
expect(mockGetInstanceShortName).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('fetches shortName from API when not provided', async () => {
|
||||
mockGetInstanceShortName.mockResolvedValue({
|
||||
json: { shortName: 'fetched123' }
|
||||
});
|
||||
|
||||
const url = await store.getLaunchUrl(
|
||||
'wrld_123:456~friends(usr_abc)',
|
||||
''
|
||||
);
|
||||
expect(url).toContain('shortName=fetched123');
|
||||
expect(mockGetInstanceShortName).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('uses secureName as fallback when shortName is empty', async () => {
|
||||
mockGetInstanceShortName.mockResolvedValue({
|
||||
json: { shortName: '', secureName: 'secure456' }
|
||||
});
|
||||
|
||||
const url = await store.getLaunchUrl(
|
||||
'wrld_123:456~friends(usr_abc)',
|
||||
''
|
||||
);
|
||||
expect(url).toContain('shortName=secure456');
|
||||
});
|
||||
|
||||
test('omits shortName when API returns nothing', async () => {
|
||||
mockGetInstanceShortName.mockResolvedValue({ json: null });
|
||||
|
||||
const url = await store.getLaunchUrl(
|
||||
'wrld_123:456~friends(usr_abc)',
|
||||
''
|
||||
);
|
||||
expect(url).toBe(
|
||||
'vrchat://launch?ref=vrcx.app&id=wrld_123:456~friends(usr_abc)'
|
||||
);
|
||||
expect(url).not.toContain('shortName');
|
||||
});
|
||||
|
||||
test('calls API for public instances without shortName', async () => {
|
||||
mockGetInstanceShortName.mockResolvedValue({ json: null });
|
||||
await store.getLaunchUrl('wrld_123:456', '');
|
||||
expect(mockGetInstanceShortName).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
323
src/stores/__tests__/modal.test.js
Normal file
323
src/stores/__tests__/modal.test.js
Normal file
@@ -0,0 +1,323 @@
|
||||
/* eslint-disable pretty-import/sort-import-groups */
|
||||
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import en from '../../localization/en.json';
|
||||
|
||||
vi.mock('../../views/Feed/Feed.vue', () => ({
|
||||
default: { template: '<div />' }
|
||||
}));
|
||||
vi.mock('../../views/Feed/columns.jsx', () => ({ columns: [] }));
|
||||
vi.mock('../../plugin/router', () => ({
|
||||
router: {
|
||||
beforeEach: vi.fn(),
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
currentRoute: ref({ path: '/', name: '', meta: {} }),
|
||||
isReady: vi.fn().mockResolvedValue(true)
|
||||
},
|
||||
initRouter: vi.fn()
|
||||
}));
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
useRouter: vi.fn(() => ({
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
currentRoute: ref({ path: '/', name: '', meta: {} })
|
||||
}))
|
||||
};
|
||||
});
|
||||
vi.mock('../../plugin/interopApi', () => ({ initInteropApi: vi.fn() }));
|
||||
vi.mock('../../service/database', () => ({
|
||||
database: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, prop) => {
|
||||
if (prop === '__esModule') return false;
|
||||
return vi.fn().mockResolvedValue(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
}));
|
||||
vi.mock('../../service/config', () => ({
|
||||
default: {
|
||||
init: vi.fn(),
|
||||
getString: vi.fn().mockImplementation((_k, d) => d ?? '{}'),
|
||||
setString: vi.fn(),
|
||||
getBool: vi.fn().mockImplementation((_k, d) => d ?? false),
|
||||
setBool: vi.fn(),
|
||||
getInt: vi.fn().mockImplementation((_k, d) => d ?? 0),
|
||||
setInt: vi.fn(),
|
||||
getFloat: vi.fn().mockImplementation((_k, d) => d ?? 0),
|
||||
setFloat: vi.fn(),
|
||||
getObject: vi.fn().mockReturnValue(null),
|
||||
setObject: vi.fn(),
|
||||
getArray: vi.fn().mockReturnValue([]),
|
||||
setArray: vi.fn(),
|
||||
remove: vi.fn()
|
||||
}
|
||||
}));
|
||||
vi.mock('../../service/jsonStorage', () => ({ default: vi.fn() }));
|
||||
vi.mock('../../service/watchState', () => ({
|
||||
watchState: { isLoggedIn: false }
|
||||
}));
|
||||
vi.mock('vue-i18n', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
const i18n = actual.createI18n({
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
legacy: false,
|
||||
missingWarn: false,
|
||||
fallbackWarn: false,
|
||||
messages: { en }
|
||||
});
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => i18n.global
|
||||
};
|
||||
});
|
||||
|
||||
import { useModalStore } from '../modal';
|
||||
|
||||
describe('useModalStore', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useModalStore();
|
||||
});
|
||||
|
||||
describe('confirm', () => {
|
||||
test('opens dialog and resolves ok:true on handleOk', async () => {
|
||||
const promise = store.confirm({
|
||||
title: 'Delete?',
|
||||
description: 'Are you sure?'
|
||||
});
|
||||
|
||||
expect(store.alertOpen).toBe(true);
|
||||
expect(store.alertMode).toBe('confirm');
|
||||
expect(store.alertTitle).toBe('Delete?');
|
||||
expect(store.alertDescription).toBe('Are you sure?');
|
||||
|
||||
store.handleOk();
|
||||
|
||||
const result = await promise;
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.reason).toBe('ok');
|
||||
expect(store.alertOpen).toBe(false);
|
||||
});
|
||||
|
||||
test('resolves ok:false on handleCancel', async () => {
|
||||
const promise = store.confirm({
|
||||
title: 'Test',
|
||||
description: 'desc'
|
||||
});
|
||||
store.handleCancel();
|
||||
|
||||
const result = await promise;
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toBe('cancel');
|
||||
expect(store.alertOpen).toBe(false);
|
||||
});
|
||||
|
||||
test('resolves ok:false with reason dismiss on handleDismiss', async () => {
|
||||
const promise = store.confirm({
|
||||
title: 'Test',
|
||||
description: 'desc'
|
||||
});
|
||||
store.handleDismiss();
|
||||
|
||||
const result = await promise;
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toBe('dismiss');
|
||||
});
|
||||
|
||||
test('does not dismiss when dismissible is false', async () => {
|
||||
const promise = store.confirm({
|
||||
title: 'Test',
|
||||
description: 'desc',
|
||||
dismissible: false
|
||||
});
|
||||
|
||||
expect(store.alertDismissible).toBe(false);
|
||||
store.handleDismiss();
|
||||
expect(store.alertOpen).toBe(true);
|
||||
|
||||
store.handleCancel();
|
||||
await promise;
|
||||
});
|
||||
|
||||
test('uses custom confirmText and cancelText', () => {
|
||||
store.confirm({
|
||||
title: 'T',
|
||||
description: 'D',
|
||||
confirmText: 'Yes',
|
||||
cancelText: 'No'
|
||||
});
|
||||
|
||||
expect(store.alertOkText).toBe('Yes');
|
||||
expect(store.alertCancelText).toBe('No');
|
||||
|
||||
store.handleCancel();
|
||||
});
|
||||
|
||||
test('replaces previous dialog with reason replaced', async () => {
|
||||
const first = store.confirm({
|
||||
title: 'First',
|
||||
description: 'first desc'
|
||||
});
|
||||
const second = store.confirm({
|
||||
title: 'Second',
|
||||
description: 'second desc'
|
||||
});
|
||||
|
||||
const firstResult = await first;
|
||||
expect(firstResult.ok).toBe(false);
|
||||
expect(firstResult.reason).toBe('replaced');
|
||||
|
||||
expect(store.alertTitle).toBe('Second');
|
||||
expect(store.alertOpen).toBe(true);
|
||||
|
||||
store.handleOk();
|
||||
const secondResult = await second;
|
||||
expect(secondResult.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('alert', () => {
|
||||
test('opens in alert mode with only ok button text', () => {
|
||||
store.alert({
|
||||
title: 'Notice',
|
||||
description: 'Something happened'
|
||||
});
|
||||
|
||||
expect(store.alertMode).toBe('alert');
|
||||
expect(store.alertCancelText).toBe('');
|
||||
expect(store.alertOpen).toBe(true);
|
||||
|
||||
store.handleOk();
|
||||
});
|
||||
|
||||
test('handleCancel resolves as ok for alert mode', async () => {
|
||||
const promise = store.alert({
|
||||
title: 'Notice',
|
||||
description: 'desc'
|
||||
});
|
||||
store.handleCancel();
|
||||
|
||||
const result = await promise;
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.reason).toBe('ok');
|
||||
});
|
||||
|
||||
test('handleDismiss resolves as ok for alert mode', async () => {
|
||||
const promise = store.alert({
|
||||
title: 'Notice',
|
||||
description: 'desc'
|
||||
});
|
||||
store.handleDismiss();
|
||||
|
||||
const result = await promise;
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.reason).toBe('ok');
|
||||
});
|
||||
});
|
||||
|
||||
describe('prompt', () => {
|
||||
test('opens prompt dialog with input value', () => {
|
||||
store.prompt({
|
||||
title: 'Enter name',
|
||||
description: 'New name',
|
||||
inputValue: 'default'
|
||||
});
|
||||
|
||||
expect(store.promptOpen).toBe(true);
|
||||
expect(store.promptTitle).toBe('Enter name');
|
||||
expect(store.promptInputValue).toBe('default');
|
||||
|
||||
store.handlePromptCancel('');
|
||||
});
|
||||
|
||||
test('resolves with value on handlePromptOk', async () => {
|
||||
const promise = store.prompt({
|
||||
title: 'T',
|
||||
description: 'D'
|
||||
});
|
||||
|
||||
store.handlePromptOk('myValue');
|
||||
|
||||
const result = await promise;
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.reason).toBe('ok');
|
||||
expect(result.value).toBe('myValue');
|
||||
expect(store.promptOpen).toBe(false);
|
||||
});
|
||||
|
||||
test('resolves with value on handlePromptCancel', async () => {
|
||||
const promise = store.prompt({
|
||||
title: 'T',
|
||||
description: 'D',
|
||||
inputValue: 'initial'
|
||||
});
|
||||
|
||||
store.handlePromptCancel('initial');
|
||||
|
||||
const result = await promise;
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toBe('cancel');
|
||||
expect(result.value).toBe('initial');
|
||||
});
|
||||
|
||||
test('sets pattern and errorMessage', () => {
|
||||
store.prompt({
|
||||
title: 'T',
|
||||
description: 'D',
|
||||
pattern: /^\d+$/,
|
||||
errorMessage: 'Numbers only'
|
||||
});
|
||||
|
||||
expect(store.promptPattern).toEqual(/^\d+$/);
|
||||
expect(store.promptErrorMessage).toBe('Numbers only');
|
||||
|
||||
store.handlePromptCancel('');
|
||||
});
|
||||
|
||||
test('replaces previous prompt with reason replaced', async () => {
|
||||
const first = store.prompt({
|
||||
title: 'First',
|
||||
description: 'D',
|
||||
inputValue: 'a'
|
||||
});
|
||||
const second = store.prompt({
|
||||
title: 'Second',
|
||||
description: 'D',
|
||||
inputValue: 'b'
|
||||
});
|
||||
|
||||
const firstResult = await first;
|
||||
expect(firstResult.ok).toBe(false);
|
||||
expect(firstResult.reason).toBe('replaced');
|
||||
expect(firstResult.value).toBe('a');
|
||||
|
||||
store.handlePromptOk('b');
|
||||
const secondResult = await second;
|
||||
expect(secondResult.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no-op when no pending', () => {
|
||||
test('handleOk does nothing without pending dialog', () => {
|
||||
store.handleOk();
|
||||
expect(store.alertOpen).toBe(false);
|
||||
});
|
||||
|
||||
test('handlePromptOk does nothing without pending prompt', () => {
|
||||
store.handlePromptOk('test');
|
||||
expect(store.promptOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,6 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { i18n } from '@/plugin';
|
||||
import { ref } from 'vue';
|
||||
|
||||
function translate(key, fallback) {
|
||||
try {
|
||||
return i18n.global.t(key);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
/**
|
||||
* @typedef {Object} ConfirmResult
|
||||
@@ -53,9 +45,9 @@ function translate(key, fallback) {
|
||||
* @property {boolean=} dismissible
|
||||
*/
|
||||
|
||||
// TODO: Method chains for confirm
|
||||
|
||||
export const useModalStore = defineStore('Modal', () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
const alertOpen = ref(false);
|
||||
const alertMode = ref('confirm'); // 'confirm' | 'alert'
|
||||
const alertTitle = ref('');
|
||||
@@ -146,15 +138,13 @@ export const useModalStore = defineStore('Modal', () => {
|
||||
|
||||
if (mode === 'alert') {
|
||||
alertOkText.value =
|
||||
options.confirmText || translate('dialog.alertdialog.ok', 'OK');
|
||||
options.confirmText || t('dialog.alertdialog.ok');
|
||||
alertCancelText.value = '';
|
||||
} else {
|
||||
alertOkText.value =
|
||||
options.confirmText ||
|
||||
translate('dialog.alertdialog.confirm', 'Confirm');
|
||||
options.confirmText || t('dialog.alertdialog.confirm');
|
||||
alertCancelText.value =
|
||||
options.cancelText ||
|
||||
translate('dialog.alertdialog.cancel', 'Cancel');
|
||||
options.cancelText || t('dialog.alertdialog.cancel');
|
||||
}
|
||||
|
||||
alertOpen.value = true;
|
||||
@@ -186,15 +176,12 @@ export const useModalStore = defineStore('Modal', () => {
|
||||
promptInputType.value = options.inputType || 'text';
|
||||
promptPattern.value = options.pattern ?? null;
|
||||
promptErrorMessage.value =
|
||||
options.errorMessage ||
|
||||
translate('dialog.prompt.input_invalid', '输入错误');
|
||||
options.errorMessage || t('dialog.prompt.input_invalid');
|
||||
|
||||
promptOkText.value =
|
||||
options.confirmText ||
|
||||
translate('dialog.alertdialog.confirm', 'Confirm');
|
||||
options.confirmText || t('dialog.alertdialog.confirm');
|
||||
promptCancelText.value =
|
||||
options.cancelText ||
|
||||
translate('dialog.alertdialog.cancel', 'Cancel');
|
||||
options.cancelText || t('dialog.alertdialog.cancel');
|
||||
|
||||
promptOpen.value = true;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user