add unit test

This commit is contained in:
pa
2026-02-12 17:21:48 +09:00
parent b12c3d679b
commit fbeb02fb7d
6 changed files with 935 additions and 66 deletions

View 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();
});
});
});

View File

@@ -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
};
}

View 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');
});
});

View 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();
});
});
});

View 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);
});
});
});

View File

@@ -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;