mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-24 01:03:50 +02:00
add test
This commit is contained in:
167
src/views/Settings/dialogs/__tests__/ChangelogDialog.test.js
Normal file
167
src/views/Settings/dialogs/__tests__/ChangelogDialog.test.js
Normal file
@@ -0,0 +1,167 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { ref } from 'vue';
|
||||
|
||||
// ─── Mocks ───────────────────────────────────────────────────────────
|
||||
|
||||
const changeLogDialog = ref({
|
||||
visible: true,
|
||||
buildName: 'VRCX 2025.1.0',
|
||||
changeLog: '## New Features\n- Feature A\n- Feature B'
|
||||
});
|
||||
|
||||
const openExternalLinkFn = vi.fn();
|
||||
|
||||
vi.mock('pinia', () => ({
|
||||
storeToRefs: () => ({ changeLogDialog }),
|
||||
defineStore: (id, fn) => fn
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../stores', () => ({
|
||||
useVRCXUpdaterStore: () => ({})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../shared/utils', () => ({
|
||||
openExternalLink: (...args) => openExternalLinkFn(...args)
|
||||
}));
|
||||
|
||||
// Stub VueShowdown since it's async and we don't need real markdown rendering
|
||||
vi.mock('vue-showdown', () => ({
|
||||
VueShowdown: {
|
||||
props: ['markdown', 'flavor', 'options'],
|
||||
template:
|
||||
'<div class="changelog-markdown" data-testid="showdown">{{ markdown }}</div>'
|
||||
}
|
||||
}));
|
||||
|
||||
import ChangelogDialog from '../ChangelogDialog.vue';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function mountComponent() {
|
||||
return mount(ChangelogDialog, {
|
||||
global: {
|
||||
stubs: {
|
||||
Dialog: {
|
||||
props: ['open'],
|
||||
emits: ['update:open'],
|
||||
template:
|
||||
'<div data-testid="dialog" v-if="open"><slot /></div>'
|
||||
},
|
||||
DialogContent: { template: '<div><slot /></div>' },
|
||||
DialogHeader: { template: '<div><slot /></div>' },
|
||||
DialogTitle: { template: '<h2><slot /></h2>' },
|
||||
DialogFooter: {
|
||||
template: '<div data-testid="footer"><slot /></div>'
|
||||
},
|
||||
Button: {
|
||||
emits: ['click'],
|
||||
props: ['variant'],
|
||||
template:
|
||||
'<button @click="$emit(\'click\')"><slot /></button>'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('ChangelogDialog.vue', () => {
|
||||
beforeEach(() => {
|
||||
changeLogDialog.value = {
|
||||
visible: true,
|
||||
buildName: 'VRCX 2025.1.0',
|
||||
changeLog: '## New Features\n- Feature A\n- Feature B'
|
||||
};
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
test('renders dialog title', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('dialog.change_log.header');
|
||||
});
|
||||
|
||||
test('renders build name', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('VRCX 2025.1.0');
|
||||
});
|
||||
|
||||
test('renders description text', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('dialog.change_log.description');
|
||||
});
|
||||
|
||||
test('renders donation links', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('Ko-fi');
|
||||
expect(wrapper.text()).toContain('Patreon');
|
||||
});
|
||||
|
||||
test('renders GitHub button', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('dialog.change_log.github');
|
||||
});
|
||||
|
||||
test('renders Close button', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('dialog.change_log.close');
|
||||
});
|
||||
|
||||
test('does not render when visible is false', () => {
|
||||
changeLogDialog.value.visible = false;
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.find('[data-testid="dialog"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interactions', () => {
|
||||
test('clicking Close button sets visible to false', async () => {
|
||||
const wrapper = mountComponent();
|
||||
const buttons = wrapper.findAll('button');
|
||||
const closeBtn = buttons.find((b) =>
|
||||
b.text().includes('dialog.change_log.close')
|
||||
);
|
||||
expect(closeBtn).toBeTruthy();
|
||||
|
||||
await closeBtn.trigger('click');
|
||||
expect(changeLogDialog.value.visible).toBe(false);
|
||||
});
|
||||
|
||||
test('clicking GitHub button opens external link', async () => {
|
||||
const wrapper = mountComponent();
|
||||
const buttons = wrapper.findAll('button');
|
||||
const githubBtn = buttons.find((b) =>
|
||||
b.text().includes('dialog.change_log.github')
|
||||
);
|
||||
expect(githubBtn).toBeTruthy();
|
||||
|
||||
await githubBtn.trigger('click');
|
||||
expect(openExternalLinkFn).toHaveBeenCalledWith(
|
||||
'https://github.com/vrcx-team/VRCX/releases'
|
||||
);
|
||||
});
|
||||
|
||||
test('clicking Ko-fi link opens external link', async () => {
|
||||
const wrapper = mountComponent();
|
||||
const links = wrapper.findAll('a');
|
||||
const kofiLink = links.find((l) => l.text().includes('Ko-fi'));
|
||||
expect(kofiLink).toBeTruthy();
|
||||
|
||||
await kofiLink.trigger('click');
|
||||
expect(openExternalLinkFn).toHaveBeenCalledWith(
|
||||
'https://ko-fi.com/map1en_'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
320
src/views/Settings/dialogs/__tests__/LaunchOptionsDialog.test.js
Normal file
320
src/views/Settings/dialogs/__tests__/LaunchOptionsDialog.test.js
Normal file
@@ -0,0 +1,320 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { ref } from 'vue';
|
||||
|
||||
// ─── Hoisted mocks (accessible inside vi.mock factories) ─────────────
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
configRepository: {
|
||||
getString: vi.fn().mockResolvedValue(''),
|
||||
setString: vi.fn()
|
||||
},
|
||||
openExternalLink: vi.fn(),
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
const isLaunchOptionsDialogVisible = ref(true);
|
||||
|
||||
vi.mock('pinia', () => ({
|
||||
storeToRefs: () => ({ isLaunchOptionsDialogVisible }),
|
||||
defineStore: (id, fn) => fn
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../stores', () => ({
|
||||
useLaunchStore: () => ({})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../service/config', () => ({
|
||||
default: mocks.configRepository
|
||||
}));
|
||||
|
||||
vi.mock('../../../../shared/utils', () => ({
|
||||
openExternalLink: (...args) => mocks.openExternalLink(...args)
|
||||
}));
|
||||
|
||||
vi.mock('vue-sonner', () => ({
|
||||
toast: mocks.toast
|
||||
}));
|
||||
|
||||
import LaunchOptionsDialog from '../LaunchOptionsDialog.vue';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function flushPromises() {
|
||||
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function mountComponent() {
|
||||
return mount(LaunchOptionsDialog, {
|
||||
global: {
|
||||
stubs: {
|
||||
Dialog: {
|
||||
props: ['open'],
|
||||
emits: ['update:open'],
|
||||
template:
|
||||
'<div data-testid="dialog" v-if="open"><slot /></div>'
|
||||
},
|
||||
DialogContent: { template: '<div><slot /></div>' },
|
||||
DialogHeader: { template: '<div><slot /></div>' },
|
||||
DialogTitle: { template: '<h2><slot /></h2>' },
|
||||
DialogFooter: {
|
||||
template: '<div data-testid="footer"><slot /></div>'
|
||||
},
|
||||
Button: {
|
||||
emits: ['click'],
|
||||
props: ['variant', 'disabled'],
|
||||
template:
|
||||
'<button @click="$emit(\'click\')" :disabled="disabled"><slot /></button>'
|
||||
},
|
||||
InputGroupTextareaField: {
|
||||
props: [
|
||||
'modelValue',
|
||||
'placeholder',
|
||||
'rows',
|
||||
'autosize',
|
||||
'inputClass',
|
||||
'spellcheck'
|
||||
],
|
||||
emits: ['update:modelValue'],
|
||||
template:
|
||||
'<textarea data-testid="textarea" :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)"></textarea>'
|
||||
},
|
||||
Badge: { template: '<span><slot /></span>' }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('LaunchOptionsDialog.vue', () => {
|
||||
beforeEach(() => {
|
||||
isLaunchOptionsDialogVisible.value = true;
|
||||
mocks.configRepository.getString.mockResolvedValue('');
|
||||
vi.clearAllMocks();
|
||||
globalThis.LINUX = false;
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
test('renders dialog title', async () => {
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
expect(wrapper.text()).toContain('dialog.launch_options.header');
|
||||
});
|
||||
|
||||
test('renders description and example args', async () => {
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.launch_options.description'
|
||||
);
|
||||
expect(wrapper.text()).toContain('--fps=144');
|
||||
expect(wrapper.text()).toContain('--enable-debug-gui');
|
||||
});
|
||||
|
||||
test('renders save button', async () => {
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
expect(wrapper.text()).toContain('dialog.launch_options.save');
|
||||
});
|
||||
|
||||
test('renders VRChat docs and Unity manual buttons', async () => {
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.launch_options.vrchat_docs'
|
||||
);
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.launch_options.unity_manual'
|
||||
);
|
||||
});
|
||||
|
||||
test('renders path override section when not Linux', async () => {
|
||||
globalThis.LINUX = false;
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.launch_options.path_override'
|
||||
);
|
||||
});
|
||||
|
||||
test('hides path override section on Linux', async () => {
|
||||
globalThis.LINUX = true;
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
expect(wrapper.text()).not.toContain(
|
||||
'dialog.launch_options.path_override'
|
||||
);
|
||||
});
|
||||
|
||||
test('does not render when not visible', () => {
|
||||
isLaunchOptionsDialogVisible.value = false;
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.find('[data-testid="dialog"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
test('loads launch arguments from configRepository on mount', async () => {
|
||||
mocks.configRepository.getString.mockImplementation((key) => {
|
||||
if (key === 'launchArguments')
|
||||
return Promise.resolve('--fps=90');
|
||||
if (key === 'vrcLaunchPathOverride')
|
||||
return Promise.resolve('C:\\VRChat');
|
||||
return Promise.resolve('');
|
||||
});
|
||||
|
||||
mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.configRepository.getString).toHaveBeenCalledWith(
|
||||
'launchArguments'
|
||||
);
|
||||
expect(mocks.configRepository.getString).toHaveBeenCalledWith(
|
||||
'vrcLaunchPathOverride'
|
||||
);
|
||||
});
|
||||
|
||||
test('clears null/string-null vrcLaunchPathOverride values', async () => {
|
||||
mocks.configRepository.getString.mockImplementation((key) => {
|
||||
if (key === 'vrcLaunchPathOverride')
|
||||
return Promise.resolve('null');
|
||||
return Promise.resolve('');
|
||||
});
|
||||
|
||||
mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.configRepository.setString).toHaveBeenCalledWith(
|
||||
'vrcLaunchPathOverride',
|
||||
''
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('save logic', () => {
|
||||
test('normalizes whitespace in launch arguments on save', async () => {
|
||||
mocks.configRepository.getString.mockImplementation((key) => {
|
||||
if (key === 'launchArguments')
|
||||
return Promise.resolve('--fps=90 --debug ');
|
||||
return Promise.resolve('');
|
||||
});
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
const saveBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) => b.text().includes('dialog.launch_options.save'));
|
||||
await saveBtn.trigger('click');
|
||||
|
||||
expect(mocks.configRepository.setString).toHaveBeenCalledWith(
|
||||
'launchArguments',
|
||||
'--fps=90 --debug'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows error toast for invalid .exe path', async () => {
|
||||
mocks.configRepository.getString.mockImplementation((key) => {
|
||||
if (key === 'launchArguments') return Promise.resolve('');
|
||||
if (key === 'vrcLaunchPathOverride')
|
||||
return Promise.resolve('C:\\VRChat\\VRChat.exe');
|
||||
return Promise.resolve('');
|
||||
});
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
const saveBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) => b.text().includes('dialog.launch_options.save'));
|
||||
await saveBtn.trigger('click');
|
||||
|
||||
expect(mocks.toast.error).toHaveBeenCalledWith(
|
||||
'message.launch.invalid_path'
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts valid launch.exe path', async () => {
|
||||
mocks.configRepository.getString.mockImplementation((key) => {
|
||||
if (key === 'launchArguments') return Promise.resolve('');
|
||||
if (key === 'vrcLaunchPathOverride')
|
||||
return Promise.resolve('C:\\VRChat\\launch.exe');
|
||||
return Promise.resolve('');
|
||||
});
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
const saveBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) => b.text().includes('dialog.launch_options.save'));
|
||||
await saveBtn.trigger('click');
|
||||
|
||||
expect(mocks.toast.error).not.toHaveBeenCalled();
|
||||
expect(mocks.toast.success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('closes dialog after successful save', async () => {
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
const saveBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) => b.text().includes('dialog.launch_options.save'));
|
||||
await saveBtn.trigger('click');
|
||||
|
||||
expect(isLaunchOptionsDialogVisible.value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('external links', () => {
|
||||
test('clicking VRChat docs button opens external link', async () => {
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
const docsBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) =>
|
||||
b.text().includes('dialog.launch_options.vrchat_docs')
|
||||
);
|
||||
await docsBtn.trigger('click');
|
||||
|
||||
expect(mocks.openExternalLink).toHaveBeenCalledWith(
|
||||
'https://docs.vrchat.com/docs/launch-options'
|
||||
);
|
||||
});
|
||||
|
||||
test('clicking Unity manual button opens external link', async () => {
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
const unityBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) =>
|
||||
b.text().includes('dialog.launch_options.unity_manual')
|
||||
);
|
||||
await unityBtn.trigger('click');
|
||||
|
||||
expect(mocks.openExternalLink).toHaveBeenCalledWith(
|
||||
'https://docs.unity3d.com/Manual/CommandLineArguments.html'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,505 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
// ─── Hoisted mocks ──────────────────────────────────────────────────
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
openExternalLink: vi.fn(),
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn()
|
||||
},
|
||||
setBioLanguage: vi.fn(),
|
||||
translateText: vi.fn().mockResolvedValue('Bonjour le monde'),
|
||||
fetchAvailableModels: vi.fn().mockResolvedValue([]),
|
||||
setTranslationApiKey: vi.fn().mockResolvedValue(undefined),
|
||||
setTranslationApiType: vi.fn().mockResolvedValue(undefined),
|
||||
setTranslationApiEndpoint: vi.fn().mockResolvedValue(undefined),
|
||||
setTranslationApiModel: vi.fn().mockResolvedValue(undefined),
|
||||
setTranslationApiPrompt: vi.fn().mockResolvedValue(undefined)
|
||||
}));
|
||||
|
||||
const bioLanguage = ref('en');
|
||||
const translationApiKey = ref('');
|
||||
const translationApiType = ref('google');
|
||||
const translationApiEndpoint = ref('');
|
||||
const translationApiModel = ref('');
|
||||
const translationApiPrompt = ref('');
|
||||
|
||||
vi.mock('pinia', () => ({
|
||||
storeToRefs: () => ({
|
||||
bioLanguage,
|
||||
translationApiKey,
|
||||
translationApiType,
|
||||
translationApiEndpoint,
|
||||
translationApiModel,
|
||||
translationApiPrompt
|
||||
}),
|
||||
defineStore: (id, fn) => fn
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key)
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../stores', () => ({
|
||||
useAdvancedSettingsStore: () => ({
|
||||
setBioLanguage: mocks.setBioLanguage,
|
||||
translateText: mocks.translateText,
|
||||
fetchAvailableModels: mocks.fetchAvailableModels,
|
||||
setTranslationApiKey: mocks.setTranslationApiKey,
|
||||
setTranslationApiType: mocks.setTranslationApiType,
|
||||
setTranslationApiEndpoint: mocks.setTranslationApiEndpoint,
|
||||
setTranslationApiModel: mocks.setTranslationApiModel,
|
||||
setTranslationApiPrompt: mocks.setTranslationApiPrompt
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../shared/utils', () => ({
|
||||
openExternalLink: (...args) => mocks.openExternalLink(...args)
|
||||
}));
|
||||
|
||||
vi.mock('vue-sonner', () => ({
|
||||
toast: mocks.toast
|
||||
}));
|
||||
|
||||
vi.mock('../../../../localization', () => ({
|
||||
getLanguageName: (code) => `Language_${code}`,
|
||||
languageCodes: ['en', 'ja', 'ko', 'zh-CN', 'fr']
|
||||
}));
|
||||
|
||||
import TranslationApiDialog from '../TranslationApiDialog.vue';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function flushPromises() {
|
||||
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param propsOverrides
|
||||
*/
|
||||
function mountComponent(propsOverrides = {}) {
|
||||
return mount(TranslationApiDialog, {
|
||||
props: {
|
||||
isTranslationApiDialogVisible: true,
|
||||
...propsOverrides
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
Dialog: {
|
||||
props: ['open'],
|
||||
emits: ['update:open'],
|
||||
template:
|
||||
'<div data-testid="dialog" v-if="open"><slot /></div>'
|
||||
},
|
||||
DialogContent: { template: '<div><slot /></div>' },
|
||||
DialogHeader: { template: '<div><slot /></div>' },
|
||||
DialogTitle: { template: '<h2><slot /></h2>' },
|
||||
DialogFooter: {
|
||||
template: '<div data-testid="footer"><slot /></div>'
|
||||
},
|
||||
Button: {
|
||||
emits: ['click'],
|
||||
props: ['variant', 'disabled', 'size'],
|
||||
template:
|
||||
'<button @click="$emit(\'click\')" :disabled="disabled"><slot /></button>'
|
||||
},
|
||||
InputGroupField: {
|
||||
props: [
|
||||
'modelValue',
|
||||
'type',
|
||||
'showPassword',
|
||||
'placeholder',
|
||||
'clearable'
|
||||
],
|
||||
emits: ['update:modelValue'],
|
||||
template:
|
||||
'<input data-testid="input-field" :type="type" :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />'
|
||||
},
|
||||
InputGroupTextareaField: {
|
||||
props: ['modelValue', 'rows', 'clearable'],
|
||||
emits: ['update:modelValue'],
|
||||
template:
|
||||
'<textarea data-testid="textarea" :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)"></textarea>'
|
||||
},
|
||||
Select: {
|
||||
props: ['modelValue'],
|
||||
emits: ['update:modelValue'],
|
||||
template:
|
||||
'<select data-testid="select" :value="modelValue" @change="$emit(\'update:modelValue\', $event.target.value)"><slot /></select>'
|
||||
},
|
||||
SelectTrigger: {
|
||||
props: ['size'],
|
||||
template: '<div><slot /></div>'
|
||||
},
|
||||
SelectValue: {
|
||||
props: ['placeholder', 'textValue'],
|
||||
template: '<span>{{ placeholder }}</span>'
|
||||
},
|
||||
SelectContent: { template: '<div><slot /></div>' },
|
||||
SelectGroup: { template: '<div><slot /></div>' },
|
||||
SelectItem: {
|
||||
props: ['value', 'textValue'],
|
||||
template: '<option :value="value"><slot /></option>'
|
||||
},
|
||||
FieldGroup: {
|
||||
template: '<div data-testid="field-group"><slot /></div>'
|
||||
},
|
||||
Field: { template: '<div data-testid="field"><slot /></div>' },
|
||||
FieldLabel: { template: '<label><slot /></label>' },
|
||||
FieldContent: { template: '<div><slot /></div>' }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('TranslationApiDialog.vue', () => {
|
||||
beforeEach(() => {
|
||||
bioLanguage.value = 'en';
|
||||
translationApiKey.value = '';
|
||||
translationApiType.value = 'google';
|
||||
translationApiEndpoint.value = '';
|
||||
translationApiModel.value = '';
|
||||
translationApiPrompt.value = '';
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
test('renders dialog title', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('dialog.translation_api.header');
|
||||
});
|
||||
|
||||
test('renders bio language selector', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain(
|
||||
'view.settings.appearance.appearance.bio_language'
|
||||
);
|
||||
});
|
||||
|
||||
test('renders language options', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('Language_en');
|
||||
expect(wrapper.text()).toContain('Language_ja');
|
||||
});
|
||||
|
||||
test('renders API type selector', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('dialog.translation_api.mode');
|
||||
});
|
||||
|
||||
test('renders google and openai options', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.translation_api.mode_google'
|
||||
);
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.translation_api.mode_openai'
|
||||
);
|
||||
});
|
||||
|
||||
test('does not render when not visible', () => {
|
||||
const wrapper = mountComponent({
|
||||
isTranslationApiDialogVisible: false
|
||||
});
|
||||
expect(wrapper.find('[data-testid="dialog"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('renders save button', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('dialog.translation_api.save');
|
||||
});
|
||||
});
|
||||
|
||||
describe('google mode', () => {
|
||||
test('shows API key field in google mode', () => {
|
||||
translationApiType.value = 'google';
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.translation_api.description'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows guide button in google mode', () => {
|
||||
translationApiType.value = 'google';
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('dialog.translation_api.guide');
|
||||
});
|
||||
|
||||
test('clicking guide button opens external link', async () => {
|
||||
translationApiType.value = 'google';
|
||||
const wrapper = mountComponent();
|
||||
|
||||
const guideBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) => b.text().includes('dialog.translation_api.guide'));
|
||||
await guideBtn.trigger('click');
|
||||
|
||||
expect(mocks.openExternalLink).toHaveBeenCalledWith(
|
||||
'https://translatepress.com/docs/automatic-translation/generate-google-api-key/'
|
||||
);
|
||||
});
|
||||
|
||||
test('does not show openai-specific fields', () => {
|
||||
translationApiType.value = 'google';
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).not.toContain(
|
||||
'dialog.translation_api.openai.endpoint'
|
||||
);
|
||||
expect(wrapper.text()).not.toContain(
|
||||
'dialog.translation_api.openai.model'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openai mode', () => {
|
||||
test('shows endpoint, api key, model, and prompt fields', async () => {
|
||||
translationApiType.value = 'openai';
|
||||
const wrapper = mountComponent();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.translation_api.openai.endpoint'
|
||||
);
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.translation_api.openai.api_key'
|
||||
);
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.translation_api.openai.model'
|
||||
);
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.translation_api.openai.prompt_optional'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows test button in openai mode', async () => {
|
||||
translationApiType.value = 'openai';
|
||||
const wrapper = mountComponent();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain('dialog.translation_api.test');
|
||||
});
|
||||
|
||||
test('does not show guide button in openai mode', async () => {
|
||||
translationApiType.value = 'openai';
|
||||
const wrapper = mountComponent();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).not.toContain(
|
||||
'dialog.translation_api.guide'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows fetch models button', async () => {
|
||||
translationApiType.value = 'openai';
|
||||
const wrapper = mountComponent();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.translation_api.fetch_models'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('save logic', () => {
|
||||
test('saves all config values on save in google mode', async () => {
|
||||
translationApiType.value = 'google';
|
||||
translationApiKey.value = 'test-key';
|
||||
const wrapper = mountComponent();
|
||||
|
||||
const saveBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) => b.text().includes('dialog.translation_api.save'));
|
||||
await saveBtn.trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.setTranslationApiType).toHaveBeenCalledWith('google');
|
||||
expect(mocks.setTranslationApiKey).toHaveBeenCalled();
|
||||
expect(mocks.toast.success).toHaveBeenCalledWith(
|
||||
'dialog.translation_api.msg_settings_saved'
|
||||
);
|
||||
});
|
||||
|
||||
test('warns if openai endpoint/model are empty on save', async () => {
|
||||
translationApiType.value = 'openai';
|
||||
translationApiEndpoint.value = '';
|
||||
translationApiModel.value = '';
|
||||
const wrapper = mountComponent();
|
||||
await nextTick();
|
||||
|
||||
const saveBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) => b.text().includes('dialog.translation_api.save'));
|
||||
await saveBtn.trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.toast.warning).toHaveBeenCalledWith(
|
||||
'dialog.translation_api.msg_fill_endpoint_model'
|
||||
);
|
||||
expect(mocks.setTranslationApiType).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('emits close event after successful save', async () => {
|
||||
translationApiType.value = 'google';
|
||||
const wrapper = mountComponent();
|
||||
|
||||
const saveBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) => b.text().includes('dialog.translation_api.save'));
|
||||
await saveBtn.trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(
|
||||
wrapper.emitted('update:isTranslationApiDialogVisible')
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
wrapper.emitted('update:isTranslationApiDialogVisible')[0]
|
||||
).toEqual([false]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test translation', () => {
|
||||
test('calls translateText with test parameters in openai mode', async () => {
|
||||
translationApiType.value = 'openai';
|
||||
translationApiEndpoint.value =
|
||||
'https://api.openai.com/v1/chat/completions';
|
||||
translationApiModel.value = 'gpt-4';
|
||||
const wrapper = mountComponent();
|
||||
await nextTick();
|
||||
|
||||
const testBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) => b.text().includes('dialog.translation_api.test'));
|
||||
await testBtn.trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.translateText).toHaveBeenCalledWith(
|
||||
'Hello world',
|
||||
'fr',
|
||||
expect.objectContaining({
|
||||
type: 'openai'
|
||||
})
|
||||
);
|
||||
expect(mocks.toast.success).toHaveBeenCalledWith(
|
||||
'dialog.translation_api.msg_test_success'
|
||||
);
|
||||
});
|
||||
|
||||
test('shows error toast when test fails', async () => {
|
||||
translationApiType.value = 'openai';
|
||||
translationApiEndpoint.value =
|
||||
'https://api.openai.com/v1/chat/completions';
|
||||
translationApiModel.value = 'gpt-4';
|
||||
mocks.translateText.mockRejectedValue(new Error('fail'));
|
||||
const wrapper = mountComponent();
|
||||
await nextTick();
|
||||
|
||||
const testBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) => b.text().includes('dialog.translation_api.test'));
|
||||
await testBtn.trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.toast.error).toHaveBeenCalledWith(
|
||||
'dialog.translation_api.msg_test_failed'
|
||||
);
|
||||
});
|
||||
|
||||
test('warns when endpoint/model are missing before test', async () => {
|
||||
translationApiType.value = 'openai';
|
||||
translationApiEndpoint.value = '';
|
||||
translationApiModel.value = '';
|
||||
const wrapper = mountComponent();
|
||||
await nextTick();
|
||||
|
||||
const testBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) => b.text().includes('dialog.translation_api.test'));
|
||||
await testBtn.trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.toast.warning).toHaveBeenCalledWith(
|
||||
'dialog.translation_api.msg_fill_endpoint_model'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetch models', () => {
|
||||
test('fetches models and shows success toast', async () => {
|
||||
translationApiType.value = 'openai';
|
||||
translationApiEndpoint.value =
|
||||
'https://api.openai.com/v1/chat/completions';
|
||||
mocks.fetchAvailableModels.mockResolvedValue([
|
||||
'gpt-4',
|
||||
'gpt-3.5-turbo'
|
||||
]);
|
||||
const wrapper = mountComponent();
|
||||
await nextTick();
|
||||
|
||||
const fetchBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) =>
|
||||
b.text().includes('dialog.translation_api.fetch_models')
|
||||
);
|
||||
await fetchBtn.trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.fetchAvailableModels).toHaveBeenCalled();
|
||||
expect(mocks.toast.success).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'dialog.translation_api.msg_models_fetched'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('warns when no models found', async () => {
|
||||
translationApiType.value = 'openai';
|
||||
translationApiEndpoint.value =
|
||||
'https://api.openai.com/v1/chat/completions';
|
||||
mocks.fetchAvailableModels.mockResolvedValue([]);
|
||||
const wrapper = mountComponent();
|
||||
await nextTick();
|
||||
|
||||
const fetchBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) =>
|
||||
b.text().includes('dialog.translation_api.fetch_models')
|
||||
);
|
||||
await fetchBtn.trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.toast.warning).toHaveBeenCalledWith(
|
||||
'dialog.translation_api.msg_no_models_found'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('form loading', () => {
|
||||
test('loads form values from store when dialog opens', async () => {
|
||||
translationApiType.value = 'openai';
|
||||
translationApiEndpoint.value = 'https://custom.api/v1';
|
||||
translationApiModel.value = 'custom-model';
|
||||
translationApiKey.value = 'sk-test';
|
||||
translationApiPrompt.value = 'Translate precisely';
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await nextTick();
|
||||
|
||||
// openai mode fields should be visible
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.translation_api.openai.endpoint'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
482
src/views/Settings/dialogs/__tests__/VRChatConfigDialog.test.js
Normal file
482
src/views/Settings/dialogs/__tests__/VRChatConfigDialog.test.js
Normal file
@@ -0,0 +1,482 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
// ─── Hoisted mocks ──────────────────────────────────────────────────
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
openExternalLink: vi.fn(),
|
||||
getVRChatResolution: vi.fn((res) => res),
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn()
|
||||
},
|
||||
appApi: {
|
||||
ReadConfigFileSafe: vi.fn().mockResolvedValue(null),
|
||||
WriteConfigFile: vi.fn()
|
||||
},
|
||||
assetBundleManager: {
|
||||
DeleteAllCache: vi.fn().mockResolvedValue(undefined)
|
||||
},
|
||||
sweepVRChatCache: vi.fn(),
|
||||
getVRChatCacheSize: vi.fn(),
|
||||
folderSelectorDialog: vi.fn().mockResolvedValue(null),
|
||||
confirm: vi.fn().mockResolvedValue({ ok: false })
|
||||
}));
|
||||
|
||||
const isVRChatConfigDialogVisible = ref(false);
|
||||
const VRChatUsedCacheSize = ref('5.2');
|
||||
const VRChatTotalCacheSize = ref('30');
|
||||
const VRChatCacheSizeLoading = ref(false);
|
||||
|
||||
vi.mock('pinia', () => ({
|
||||
storeToRefs: (store) => {
|
||||
const result = {};
|
||||
for (const key in store) {
|
||||
if (
|
||||
store[key] &&
|
||||
typeof store[key] === 'object' &&
|
||||
'__v_isRef' in store[key]
|
||||
) {
|
||||
result[key] = store[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
defineStore: (id, fn) => fn
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key)
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../stores', () => ({
|
||||
useGameStore: () => ({
|
||||
VRChatUsedCacheSize,
|
||||
VRChatTotalCacheSize,
|
||||
VRChatCacheSizeLoading,
|
||||
sweepVRChatCache: mocks.sweepVRChatCache,
|
||||
getVRChatCacheSize: mocks.getVRChatCacheSize
|
||||
}),
|
||||
useAdvancedSettingsStore: () => ({
|
||||
isVRChatConfigDialogVisible,
|
||||
folderSelectorDialog: mocks.folderSelectorDialog
|
||||
}),
|
||||
useModalStore: () => ({
|
||||
confirm: mocks.confirm
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../shared/utils', () => ({
|
||||
openExternalLink: (...args) => mocks.openExternalLink(...args),
|
||||
getVRChatResolution: (...args) => mocks.getVRChatResolution(...args)
|
||||
}));
|
||||
|
||||
vi.mock('../../../../shared/constants', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
VRChatCameraResolutions: [
|
||||
{ name: '1920x1080 (1080p)', width: 1920, height: 1080 },
|
||||
{ name: '3840x2160 (4K)', width: 3840, height: 2160 },
|
||||
{ name: 'Default', width: 0, height: 0 }
|
||||
],
|
||||
VRChatScreenshotResolutions: [
|
||||
{ name: '1920x1080 (1080p)', width: 1920, height: 1080 },
|
||||
{ name: '3840x2160 (4K)', width: 3840, height: 2160 },
|
||||
{ name: 'Default', width: 0, height: 0 }
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('vue-sonner', () => ({
|
||||
toast: mocks.toast
|
||||
}));
|
||||
|
||||
// Set global mocks for CefSharp-injected APIs
|
||||
globalThis.AppApi = mocks.appApi;
|
||||
globalThis.AssetBundleManager = mocks.assetBundleManager;
|
||||
|
||||
import VRChatConfigDialog from '../VRChatConfigDialog.vue';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function flushPromises() {
|
||||
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function mountComponent() {
|
||||
return mount(VRChatConfigDialog, {
|
||||
global: {
|
||||
stubs: {
|
||||
Dialog: {
|
||||
props: ['open'],
|
||||
emits: ['update:open'],
|
||||
template:
|
||||
'<div data-testid="dialog" v-if="open"><slot /></div>'
|
||||
},
|
||||
DialogContent: { template: '<div><slot /></div>' },
|
||||
DialogHeader: { template: '<div><slot /></div>' },
|
||||
DialogTitle: { template: '<h2><slot /></h2>' },
|
||||
DialogFooter: {
|
||||
template: '<div data-testid="footer"><slot /></div>'
|
||||
},
|
||||
Button: {
|
||||
emits: ['click'],
|
||||
props: ['variant', 'disabled', 'size'],
|
||||
template:
|
||||
'<button @click="$emit(\'click\')" :disabled="disabled"><slot /></button>'
|
||||
},
|
||||
InputGroupAction: {
|
||||
props: [
|
||||
'modelValue',
|
||||
'placeholder',
|
||||
'size',
|
||||
'type',
|
||||
'min',
|
||||
'max'
|
||||
],
|
||||
emits: ['update:modelValue', 'input'],
|
||||
template:
|
||||
'<div data-testid="input-group"><input :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value); $emit(\'input\')" /><slot name="actions" /></div>'
|
||||
},
|
||||
Select: {
|
||||
props: ['modelValue'],
|
||||
emits: ['update:modelValue'],
|
||||
template: '<div data-testid="select"><slot /></div>'
|
||||
},
|
||||
SelectTrigger: {
|
||||
props: ['size'],
|
||||
template: '<div><slot /></div>'
|
||||
},
|
||||
SelectValue: {
|
||||
props: ['placeholder'],
|
||||
template: '<span>{{ placeholder }}</span>'
|
||||
},
|
||||
SelectContent: { template: '<div><slot /></div>' },
|
||||
SelectGroup: { template: '<div><slot /></div>' },
|
||||
SelectItem: {
|
||||
props: ['value'],
|
||||
template: '<option :value="value"><slot /></option>'
|
||||
},
|
||||
Checkbox: {
|
||||
props: ['modelValue'],
|
||||
emits: ['update:modelValue'],
|
||||
template:
|
||||
'<input type="checkbox" :checked="modelValue" @change="$emit(\'update:modelValue\', $event.target.checked)" />'
|
||||
},
|
||||
TooltipWrapper: {
|
||||
props: ['side', 'content'],
|
||||
template: '<div><slot /></div>'
|
||||
},
|
||||
Spinner: { template: '<span data-testid="spinner" />' },
|
||||
RefreshCw: { template: '<span />' },
|
||||
FolderOpen: { template: '<span />' }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('VRChatConfigDialog.vue', () => {
|
||||
beforeEach(() => {
|
||||
isVRChatConfigDialogVisible.value = false;
|
||||
VRChatUsedCacheSize.value = '5.2';
|
||||
VRChatTotalCacheSize.value = '30';
|
||||
VRChatCacheSizeLoading.value = false;
|
||||
mocks.appApi.ReadConfigFileSafe.mockResolvedValue(null);
|
||||
mocks.confirm.mockResolvedValue({ ok: false });
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
test('does not render when not visible', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.find('[data-testid="dialog"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('renders dialog content when visible', async () => {
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find('[data-testid="dialog"]').exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain('dialog.config_json.header');
|
||||
});
|
||||
|
||||
test('renders descriptions', async () => {
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain('dialog.config_json.description1');
|
||||
expect(wrapper.text()).toContain('dialog.config_json.description2');
|
||||
});
|
||||
|
||||
test('renders cache size info', async () => {
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain('dialog.config_json.cache_size');
|
||||
expect(wrapper.text()).toContain('5.2');
|
||||
expect(wrapper.text()).toContain('GB');
|
||||
});
|
||||
|
||||
test('renders config items', async () => {
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.config_json.max_cache_size'
|
||||
);
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.config_json.cache_expiry_delay'
|
||||
);
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.config_json.fpv_steadycam_fov'
|
||||
);
|
||||
});
|
||||
|
||||
test('renders resolution selectors', async () => {
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.config_json.camera_resolution'
|
||||
);
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.config_json.spout_resolution'
|
||||
);
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.config_json.screenshot_resolution'
|
||||
);
|
||||
});
|
||||
|
||||
test('renders checkbox options', async () => {
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.config_json.picture_sort_by_date'
|
||||
);
|
||||
expect(wrapper.text()).toContain(
|
||||
'dialog.config_json.disable_discord_presence'
|
||||
);
|
||||
});
|
||||
|
||||
test('renders footer buttons', async () => {
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain('dialog.config_json.vrchat_docs');
|
||||
expect(wrapper.text()).toContain('dialog.config_json.cancel');
|
||||
expect(wrapper.text()).toContain('dialog.config_json.save');
|
||||
});
|
||||
});
|
||||
|
||||
describe('config loading', () => {
|
||||
test('reads config file when dialog opens', async () => {
|
||||
mocks.appApi.ReadConfigFileSafe.mockResolvedValue(
|
||||
JSON.stringify({ cache_size: 50 })
|
||||
);
|
||||
|
||||
mountComponent();
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.appApi.ReadConfigFileSafe).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('save logic', () => {
|
||||
test('calls AppApi.WriteConfigFile on save', async () => {
|
||||
mocks.appApi.ReadConfigFileSafe.mockResolvedValue(
|
||||
JSON.stringify({ cache_size: 50 })
|
||||
);
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const saveBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) => b.text().includes('dialog.config_json.save'));
|
||||
await saveBtn.trigger('click');
|
||||
|
||||
expect(mocks.appApi.WriteConfigFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('removes empty string values before saving', async () => {
|
||||
mocks.appApi.ReadConfigFileSafe.mockResolvedValue(
|
||||
JSON.stringify({ cache_directory: '', cache_size: 50 })
|
||||
);
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const saveBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) => b.text().includes('dialog.config_json.save'));
|
||||
await saveBtn.trigger('click');
|
||||
|
||||
const savedJson = JSON.parse(
|
||||
mocks.appApi.WriteConfigFile.mock.calls[0][0]
|
||||
);
|
||||
expect(savedJson).not.toHaveProperty('cache_directory');
|
||||
});
|
||||
|
||||
test('closes dialog after save', async () => {
|
||||
mocks.appApi.ReadConfigFileSafe.mockResolvedValue(
|
||||
JSON.stringify({})
|
||||
);
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const saveBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) => b.text().includes('dialog.config_json.save'));
|
||||
await saveBtn.trigger('click');
|
||||
|
||||
expect(isVRChatConfigDialogVisible.value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cache operations', () => {
|
||||
test('delete cache button triggers confirm dialog', async () => {
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const deleteBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) =>
|
||||
b.text().includes('dialog.config_json.delete_cache')
|
||||
);
|
||||
expect(deleteBtn).toBeTruthy();
|
||||
await deleteBtn.trigger('click');
|
||||
|
||||
expect(mocks.confirm).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('confirming delete calls AssetBundleManager.DeleteAllCache', async () => {
|
||||
mocks.confirm.mockResolvedValue({ ok: true });
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const deleteBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) =>
|
||||
b.text().includes('dialog.config_json.delete_cache')
|
||||
);
|
||||
await deleteBtn.trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.assetBundleManager.DeleteAllCache).toHaveBeenCalled();
|
||||
expect(mocks.toast.success).toHaveBeenCalledWith(
|
||||
'message.cache.deleted'
|
||||
);
|
||||
});
|
||||
|
||||
test('sweep cache button calls sweepVRChatCache', async () => {
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const sweepBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) =>
|
||||
b.text().includes('dialog.config_json.sweep_cache')
|
||||
);
|
||||
expect(sweepBtn).toBeTruthy();
|
||||
await sweepBtn.trigger('click');
|
||||
|
||||
expect(mocks.sweepVRChatCache).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('close behavior', () => {
|
||||
test('clicking cancel closes dialog', async () => {
|
||||
mocks.appApi.ReadConfigFileSafe.mockResolvedValue(
|
||||
JSON.stringify({})
|
||||
);
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const cancelBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) => b.text().includes('dialog.config_json.cancel'));
|
||||
await cancelBtn.trigger('click');
|
||||
|
||||
expect(isVRChatConfigDialogVisible.value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('external links', () => {
|
||||
test('clicking VRChat docs opens external link', async () => {
|
||||
isVRChatConfigDialogVisible.value = true;
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
const docsBtn = wrapper
|
||||
.findAll('button')
|
||||
.find((b) =>
|
||||
b.text().includes('dialog.config_json.vrchat_docs')
|
||||
);
|
||||
await docsBtn.trigger('click');
|
||||
|
||||
expect(mocks.openExternalLink).toHaveBeenCalledWith(
|
||||
'https://docs.vrchat.com/docs/configuration-file'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user