diff --git a/src/components/dialogs/UserDialog/__tests__/UserDialogInfoTab.test.js b/src/components/dialogs/UserDialog/__tests__/UserDialogInfoTab.test.js
index 9965bd63..d69f9f3e 100644
--- a/src/components/dialogs/UserDialog/__tests__/UserDialogInfoTab.test.js
+++ b/src/components/dialogs/UserDialog/__tests__/UserDialogInfoTab.test.js
@@ -226,7 +226,7 @@ describe('UserDialogInfoTab.vue', () => {
wrapper.vm.onTabActivated();
await flushPromises();
- expect(creditsSpy).toHaveBeenCalledTimes(1);
+ expect(creditsSpy).toHaveBeenCalledTimes(0);
});
});
diff --git a/src/queries/__tests__/policies.test.js b/src/queries/__tests__/policies.test.js
index 003f8716..5c6696a1 100644
--- a/src/queries/__tests__/policies.test.js
+++ b/src/queries/__tests__/policies.test.js
@@ -89,8 +89,8 @@ describe('query policy configuration', () => {
test('file-related policies', () => {
expect(entityQueryPolicies.fileAnalysis).toMatchObject({
- staleTime: 600000,
- gcTime: 3600000,
+ staleTime: 3600000,
+ gcTime: 14400000,
retry: 1,
refetchOnWindowFocus: false
});
@@ -105,8 +105,8 @@ describe('query policy configuration', () => {
test('world persist data policy', () => {
expect(entityQueryPolicies.worldPersistData).toMatchObject({
- staleTime: 120000,
- gcTime: 600000,
+ staleTime: 1800000,
+ gcTime: 7200000,
retry: 1,
refetchOnWindowFocus: false
});
@@ -114,8 +114,8 @@ describe('query policy configuration', () => {
test('user relation policies (mutualCounts, representedGroup)', () => {
expect(entityQueryPolicies.mutualCounts).toMatchObject({
- staleTime: 120000,
- gcTime: 600000,
+ staleTime: 900000,
+ gcTime: 3600000,
retry: 1,
refetchOnWindowFocus: false
});
@@ -130,8 +130,8 @@ describe('query policy configuration', () => {
test('visits policy has longer staleTime for slow-changing data', () => {
expect(entityQueryPolicies.visits).toMatchObject({
- staleTime: 300000,
- gcTime: 900000,
+ staleTime: 1800000,
+ gcTime: 7200000,
retry: 1,
refetchOnWindowFocus: false
});
@@ -139,8 +139,8 @@ describe('query policy configuration', () => {
test('avatarStyles policy has very long staleTime for static config data', () => {
expect(entityQueryPolicies.avatarStyles).toMatchObject({
- staleTime: 600000,
- gcTime: 3600000,
+ staleTime: 3600000,
+ gcTime: 14400000,
retry: 1,
refetchOnWindowFocus: false
});
diff --git a/src/views/Tools/Tools.vue b/src/views/Tools/Tools.vue
index 3f8df395..3ac19e79 100644
--- a/src/views/Tools/Tools.vue
+++ b/src/views/Tools/Tools.vue
@@ -12,32 +12,16 @@
{{ t('view.tools.pictures.header') }}
@@ -49,73 +33,31 @@
{{ t('view.tools.shortcuts.header') }}
@@ -127,64 +69,26 @@
{{ t('view.tools.system_tools.header') }}
@@ -196,17 +100,11 @@
{{ t('view.tools.group.header') }}
@@ -217,59 +115,26 @@
@@ -281,19 +146,11 @@
{{ t('view.tools.other.header') }}
@@ -336,7 +193,7 @@
} from 'lucide-vue-next';
import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
- import { Card } from '@/components/ui/card';
+ import ToolItem from './components/ToolItem.vue';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';
@@ -529,9 +386,6 @@
margin-bottom: 12px;
transition: all 0.2s ease;
- &:hover {
- }
-
.rotation-transition {
font-size: 14px;
margin-right: 8px;
@@ -553,48 +407,7 @@
margin-left: 16px;
}
- .tool-card {
- position: relative;
- overflow: visible;
- border-radius: var(--radius-lg);
- cursor: pointer;
- width: 100%;
- .tool-content {
- display: flex;
- align-items: center;
- padding: 20px 16px;
-
- .tool-icon {
- width: 56px;
- height: 56px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: var(--radius-xl);
- margin-right: 20px;
-
- i {
- font-size: 28px;
- }
- }
-
- .tool-info {
- flex: 1;
-
- .tool-name {
- font-size: 18px;
- font-weight: 600;
- margin-bottom: 4px;
- }
-
- .tool-description {
- font-size: 14px;
- opacity: 0.8;
- }
- }
- }
- }
.is-rotated {
transform: rotate(-90deg);
diff --git a/src/views/Tools/__tests__/Tools.test.js b/src/views/Tools/__tests__/Tools.test.js
new file mode 100644
index 00000000..f0b57160
--- /dev/null
+++ b/src/views/Tools/__tests__/Tools.test.js
@@ -0,0 +1,99 @@
+import { beforeEach, describe, expect, test, vi } from 'vitest';
+import { ref } from 'vue';
+import { flushPromises, mount } from '@vue/test-utils';
+
+const push = vi.fn();
+const showGalleryPage = vi.fn();
+const showVRChatConfig = vi.fn();
+const showLaunchOptions = vi.fn();
+const showRegistryBackupDialog = vi.fn();
+const getString = vi.fn();
+const setString = vi.fn();
+const friends = ref([]);
+
+vi.mock('vue-router', () => ({
+ useRouter: () => ({ push }),
+ useRoute: () => ({ name: 'not-tools' })
+}));
+
+vi.mock('vue-i18n', () => ({
+ useI18n: () => ({ t: (key) => key })
+}));
+
+vi.mock('pinia', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ storeToRefs: (store) => store
+ };
+});
+
+vi.mock('../../../stores', () => ({
+ useFriendStore: () => ({ friends }),
+ useGalleryStore: () => ({ showGalleryPage })
+}));
+
+vi.mock('../../../stores/settings/advanced', () => ({
+ useAdvancedSettingsStore: () => ({ showVRChatConfig })
+}));
+
+vi.mock('../../../stores/launch', () => ({
+ useLaunchStore: () => ({ showLaunchOptions })
+}));
+
+vi.mock('../../../stores/vrcx', () => ({
+ useVrcxStore: () => ({ showRegistryBackupDialog })
+}));
+
+vi.mock('../../../services/config.js', () => ({
+ default: {
+ getString: (...args) => getString(...args),
+ setString: (...args) => setString(...args)
+ }
+}));
+
+vi.mock('../dialogs/AutoChangeStatusDialog.vue', () => ({
+ default: { template: '' }
+}));
+
+import Tools from '../Tools.vue';
+
+describe('Tools.vue', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ getString.mockResolvedValue('{}');
+ });
+
+ test('clicking screenshot tool navigates to screenshot metadata', async () => {
+ const wrapper = mount(Tools);
+ await flushPromises();
+
+ const screenshotItem = wrapper.findAllComponents({ name: 'ToolItem' })[0];
+ await screenshotItem.trigger('click');
+
+ expect(push).toHaveBeenCalledWith({ name: 'screenshot-metadata' });
+ });
+
+ test('clicking inventory tool calls showGalleryPage', async () => {
+ const wrapper = mount(Tools);
+ await flushPromises();
+
+ const inventoryItem = wrapper.findAllComponents({ name: 'ToolItem' })[1];
+ await inventoryItem.trigger('click');
+
+ expect(showGalleryPage).toHaveBeenCalled();
+ });
+
+ test('toggle category persists collapsed state', async () => {
+ const wrapper = mount(Tools);
+ await flushPromises();
+
+ const firstCategoryHeader = wrapper.find('.category-header');
+ await firstCategoryHeader.trigger('click');
+
+ expect(setString).toHaveBeenCalledWith(
+ 'VRCX_toolsCategoryCollapsed',
+ expect.any(String)
+ );
+ });
+});
diff --git a/src/views/Tools/components/ToolItem.vue b/src/views/Tools/components/ToolItem.vue
new file mode 100644
index 00000000..bd4ed18e
--- /dev/null
+++ b/src/views/Tools/components/ToolItem.vue
@@ -0,0 +1,21 @@
+
+
+
+ -
+
+
+
+
+ {{ title }}
+ {{ description }}
+
+
+
diff --git a/src/views/Tools/components/__tests__/ToolItem.test.js b/src/views/Tools/components/__tests__/ToolItem.test.js
new file mode 100644
index 00000000..396bb99d
--- /dev/null
+++ b/src/views/Tools/components/__tests__/ToolItem.test.js
@@ -0,0 +1,25 @@
+import { describe, expect, test } from 'vitest';
+import { defineComponent, markRaw } from 'vue';
+import { mount } from '@vue/test-utils';
+
+import ToolItem from '../ToolItem.vue';
+
+describe('ToolItem.vue', () => {
+ test('renders icon, title and description', () => {
+ const MockIcon = defineComponent({
+ template: ''
+ });
+
+ const wrapper = mount(ToolItem, {
+ props: {
+ icon: markRaw(MockIcon),
+ title: 'Test title',
+ description: 'Test description'
+ }
+ });
+
+ expect(wrapper.find('[data-test="mock-icon"]').exists()).toBe(true);
+ expect(wrapper.text()).toContain('Test title');
+ expect(wrapper.text()).toContain('Test description');
+ });
+});