split user dialog

This commit is contained in:
pa
2026-03-09 01:11:21 +09:00
parent 6f94ee9aab
commit 64b27ce7f1
11 changed files with 2499 additions and 1503 deletions

View File

@@ -0,0 +1,262 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { createTestingPinia } from '@pinia/testing';
import { mount } from '@vue/test-utils';
// ─── Mocks (must be before any imports that use them) ────────────────
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key)
}),
createI18n: () => ({
global: { t: (key) => key },
install: vi.fn()
})
}));
vi.mock('../../../../plugin/router', () => {
const { ref } = require('vue');
return {
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();
const { ref } = require('vue');
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 }
}));
import UserDialogAvatarsTab from '../UserDialogAvatarsTab.vue';
import { useUserStore } from '../../../../stores';
// ─── Helpers ─────────────────────────────────────────────────────────
const MOCK_AVATARS = [
{
id: 'avtr_1',
name: 'Alpha',
thumbnailImageUrl: 'https://img/1.png',
releaseStatus: 'public',
authorId: 'usr_me'
},
{
id: 'avtr_2',
name: 'Beta',
thumbnailImageUrl: 'https://img/2.png',
releaseStatus: 'private',
authorId: 'usr_me'
},
{
id: 'avtr_3',
name: 'Gamma',
thumbnailImageUrl: 'https://img/3.png',
releaseStatus: 'public',
authorId: 'usr_me'
}
];
/**
*
* @param overrides
*/
function mountComponent(overrides = {}) {
const pinia = createTestingPinia({
stubActions: false
});
const userStore = useUserStore(pinia);
userStore.userDialog = {
id: 'usr_me',
ref: { id: 'usr_me' },
avatars: [...MOCK_AVATARS],
avatarSorting: 'name',
avatarReleaseStatus: 'all',
isAvatarsLoading: false,
isWorldsLoading: false,
...overrides
};
userStore.currentUser = {
id: 'usr_me',
...overrides.currentUser
};
return mount(UserDialogAvatarsTab, {
global: {
plugins: [pinia],
stubs: {
RefreshCw: { template: '<svg class="refresh-icon" />' },
DeprecationAlert: {
template: '<div class="deprecation-stub" />'
}
}
}
});
}
// ─── Tests ───────────────────────────────────────────────────────────
describe('UserDialogAvatarsTab.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('rendering', () => {
test('renders avatar count', () => {
const wrapper = mountComponent();
expect(wrapper.text()).toContain('3');
});
test('renders all avatars when releaseStatus is "all"', () => {
const wrapper = mountComponent();
const items = wrapper.findAll('.cursor-pointer');
expect(items).toHaveLength(3);
});
test('renders avatar names', () => {
const wrapper = mountComponent();
expect(wrapper.text()).toContain('Alpha');
expect(wrapper.text()).toContain('Beta');
expect(wrapper.text()).toContain('Gamma');
});
test('renders avatar thumbnails', () => {
const wrapper = mountComponent();
const images = wrapper.findAll('img');
expect(images).toHaveLength(3);
expect(images[0].attributes('src')).toBe('https://img/1.png');
});
test('shows deprecation alert for current user', () => {
const wrapper = mountComponent();
expect(wrapper.find('.deprecation-stub').exists()).toBe(true);
});
test('hides deprecation alert for other users', () => {
const wrapper = mountComponent({
id: 'usr_other',
ref: { id: 'usr_other' }
});
expect(wrapper.find('.deprecation-stub').exists()).toBe(false);
});
test('shows empty state when no avatars and not loading', () => {
const wrapper = mountComponent({ avatars: [] });
expect(wrapper.text()).toContain('0');
});
});
describe('filtering by releaseStatus', () => {
test('shows only public avatars when releaseStatus is "public"', () => {
const wrapper = mountComponent({ avatarReleaseStatus: 'public' });
expect(wrapper.text()).toContain('Alpha');
expect(wrapper.text()).toContain('Gamma');
expect(wrapper.text()).not.toContain('Beta');
});
test('shows only private avatars when releaseStatus is "private"', () => {
const wrapper = mountComponent({ avatarReleaseStatus: 'private' });
expect(wrapper.text()).toContain('Beta');
expect(wrapper.text()).not.toContain('Alpha');
expect(wrapper.text()).not.toContain('Gamma');
});
});
describe('search', () => {
test('renders search input for current user', () => {
const wrapper = mountComponent();
const input = wrapper.find('input');
expect(input.exists()).toBe(true);
});
test('does not render search input for other users', () => {
const wrapper = mountComponent({
id: 'usr_other',
ref: { id: 'usr_other' }
});
const input = wrapper.find('input');
expect(input.exists()).toBe(false);
});
test('filters avatars by search query', async () => {
const wrapper = mountComponent();
const input = wrapper.find('input');
await input.setValue('alpha');
expect(wrapper.text()).toContain('Alpha');
expect(wrapper.text()).not.toContain('Beta');
expect(wrapper.text()).not.toContain('Gamma');
});
test('search is case-insensitive', async () => {
const wrapper = mountComponent();
const input = wrapper.find('input');
await input.setValue('BETA');
expect(wrapper.text()).toContain('Beta');
});
test('shows all avatars when search query is cleared', async () => {
const wrapper = mountComponent();
const input = wrapper.find('input');
await input.setValue('alpha');
await input.setValue('');
expect(wrapper.text()).toContain('Alpha');
expect(wrapper.text()).toContain('Beta');
expect(wrapper.text()).toContain('Gamma');
});
});
describe('loading state', () => {
test('disables refresh button when loading', () => {
const wrapper = mountComponent({ isAvatarsLoading: true });
const button = wrapper.find('button');
expect(button.attributes('disabled')).toBeDefined();
});
});
});

View File

@@ -0,0 +1,238 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { createTestingPinia } from '@pinia/testing';
import { mount } from '@vue/test-utils';
// ─── Mocks ───────────────────────────────────────────────────────────
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key)
}),
createI18n: () => ({
global: { t: (key) => key },
install: vi.fn()
})
}));
vi.mock('../../../../plugin/router', () => {
const { ref } = require('vue');
return {
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();
const { ref } = require('vue');
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('../../../../service/request', () => ({
request: vi.fn().mockResolvedValue({ json: {} }),
processBulk: vi.fn(),
buildRequestInit: vi.fn(),
parseResponse: vi.fn(),
shouldIgnoreError: vi.fn(),
$throw: vi.fn(),
failedGetRequests: new Map()
}));
import UserDialogMutualFriendsTab from '../UserDialogMutualFriendsTab.vue';
import { useUserStore } from '../../../../stores';
import { userDialogMutualFriendSortingOptions } from '../../../../shared/constants';
// ─── Helpers ─────────────────────────────────────────────────────────
const MOCK_MUTUAL_FRIENDS = [
{
id: 'usr_1',
displayName: 'Charlie',
$userColour: '#ff0000',
currentAvatarThumbnailImageUrl: 'https://img/charlie.png'
},
{
id: 'usr_2',
displayName: 'Alice',
$userColour: '#00ff00',
currentAvatarThumbnailImageUrl: 'https://img/alice.png'
},
{
id: 'usr_3',
displayName: 'Bob',
$userColour: '#0000ff',
currentAvatarThumbnailImageUrl: 'https://img/bob.png'
}
];
/**
*
* @param overrides
*/
function mountComponent(overrides = {}) {
const pinia = createTestingPinia({
stubActions: false
});
const userStore = useUserStore(pinia);
userStore.userDialog = {
id: 'usr_target',
ref: { id: 'usr_target' },
mutualFriends: [...MOCK_MUTUAL_FRIENDS],
mutualFriendSorting: userDialogMutualFriendSortingOptions.alphabetical,
isMutualFriendsLoading: false,
...overrides
};
userStore.currentUser = {
id: 'usr_me',
hasSharedConnectionsOptOut: false,
...overrides.currentUser
};
return mount(UserDialogMutualFriendsTab, {
global: {
plugins: [pinia],
stubs: {
RefreshCw: { template: '<svg class="refresh-icon" />' }
}
}
});
}
// ─── Tests ───────────────────────────────────────────────────────────
describe('UserDialogMutualFriendsTab.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('rendering', () => {
test('renders mutual friend count', () => {
const wrapper = mountComponent();
expect(wrapper.text()).toContain('3');
});
test('renders all mutual friends', () => {
const wrapper = mountComponent();
const items = wrapper.findAll('li');
expect(items).toHaveLength(3);
});
test('renders friend display names', () => {
const wrapper = mountComponent();
expect(wrapper.text()).toContain('Charlie');
expect(wrapper.text()).toContain('Alice');
expect(wrapper.text()).toContain('Bob');
});
test('renders friend avatar images', () => {
const wrapper = mountComponent();
const images = wrapper.findAll('img');
expect(images).toHaveLength(3);
});
test('applies user colour to display name', () => {
const wrapper = mountComponent();
const nameSpan = wrapper.find('span[style*="color"]');
expect(nameSpan.exists()).toBe(true);
});
test('renders empty list when no mutual friends', () => {
const wrapper = mountComponent({ mutualFriends: [] });
const items = wrapper.findAll('li');
expect(items).toHaveLength(0);
expect(wrapper.text()).toContain('0');
});
});
describe('loading state', () => {
test('disables refresh button when loading', () => {
const wrapper = mountComponent({ isMutualFriendsLoading: true });
const button = wrapper.find('button');
expect(button.attributes('disabled')).toBeDefined();
});
test('refresh button is enabled when not loading', () => {
const wrapper = mountComponent({ isMutualFriendsLoading: false });
const button = wrapper.find('button');
expect(button.attributes('disabled')).toBeUndefined();
});
});
describe('click interactions', () => {
test('calls showUserDialog when a friend is clicked', async () => {
const pinia = createTestingPinia({ stubActions: false });
const userStore = useUserStore(pinia);
userStore.userDialog = {
id: 'usr_target',
ref: { id: 'usr_target' },
mutualFriends: [...MOCK_MUTUAL_FRIENDS],
mutualFriendSorting:
userDialogMutualFriendSortingOptions.alphabetical,
isMutualFriendsLoading: false
};
userStore.currentUser = { id: 'usr_me' };
const showUserDialogSpy = vi
.spyOn(userStore, 'showUserDialog')
.mockImplementation(() => {});
const wrapper = mount(UserDialogMutualFriendsTab, {
global: {
plugins: [pinia],
stubs: {
RefreshCw: { template: '<svg class="refresh-icon" />' }
}
}
});
const firstItem = wrapper.findAll('li')[0];
await firstItem.trigger('click');
expect(showUserDialogSpy).toHaveBeenCalledWith('usr_1');
});
});
});

View File

@@ -0,0 +1,263 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { createTestingPinia } from '@pinia/testing';
import { mount } from '@vue/test-utils';
// ─── Mocks ───────────────────────────────────────────────────────────
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key)
}),
createI18n: () => ({
global: { t: (key) => key },
install: vi.fn()
})
}));
vi.mock('../../../../plugin/router', () => {
const { ref } = require('vue');
return {
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();
const { ref } = require('vue');
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('../../../../service/request', () => ({
request: vi.fn().mockResolvedValue({ json: {} }),
processBulk: vi.fn(),
buildRequestInit: vi.fn(),
parseResponse: vi.fn(),
shouldIgnoreError: vi.fn(),
$throw: vi.fn(),
failedGetRequests: new Map()
}));
import UserDialogWorldsTab from '../UserDialogWorldsTab.vue';
import { useUserStore } from '../../../../stores';
import {
userDialogWorldSortingOptions,
userDialogWorldOrderOptions
} from '../../../../shared/constants';
// ─── Helpers ─────────────────────────────────────────────────────────
const MOCK_WORLDS = [
{
id: 'wrld_1',
name: 'Sunset Valley',
thumbnailImageUrl: 'https://img/world1.png',
occupants: 12,
authorId: 'usr_me'
},
{
id: 'wrld_2',
name: 'Midnight Club',
thumbnailImageUrl: 'https://img/world2.png',
occupants: 5,
authorId: 'usr_me'
},
{
id: 'wrld_3',
name: 'Cozy Cottage',
thumbnailImageUrl: 'https://img/world3.png',
occupants: 0,
authorId: 'usr_me'
}
];
/**
*
* @param overrides
*/
function mountComponent(overrides = {}) {
const pinia = createTestingPinia({
stubActions: false
});
const userStore = useUserStore(pinia);
userStore.userDialog = {
id: 'usr_me',
ref: { id: 'usr_me' },
worlds: [...MOCK_WORLDS],
worldSorting: userDialogWorldSortingOptions.name,
worldOrder: userDialogWorldOrderOptions.descending,
isWorldsLoading: false,
...overrides
};
userStore.currentUser = {
id: 'usr_me',
...overrides.currentUser
};
return mount(UserDialogWorldsTab, {
global: {
plugins: [pinia],
stubs: {
RefreshCw: { template: '<svg class="refresh-icon" />' }
}
}
});
}
// ─── Tests ───────────────────────────────────────────────────────────
describe('UserDialogWorldsTab.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('rendering', () => {
test('renders world count', () => {
const wrapper = mountComponent();
expect(wrapper.text()).toContain('3');
});
test('renders all worlds', () => {
const wrapper = mountComponent();
const items = wrapper.findAll('.cursor-pointer');
expect(items).toHaveLength(3);
});
test('renders world names', () => {
const wrapper = mountComponent();
expect(wrapper.text()).toContain('Sunset Valley');
expect(wrapper.text()).toContain('Midnight Club');
expect(wrapper.text()).toContain('Cozy Cottage');
});
test('renders world thumbnail images', () => {
const wrapper = mountComponent();
const images = wrapper.findAll('img');
expect(images).toHaveLength(3);
expect(images[0].attributes('src')).toBe('https://img/world1.png');
});
test('renders occupant count for worlds with occupants', () => {
const wrapper = mountComponent();
expect(wrapper.text()).toContain('12');
expect(wrapper.text()).toContain('5');
});
test('does not render occupant count for worlds with zero occupants', () => {
const wrapper = mountComponent({
worlds: [
{
id: 'wrld_3',
name: 'Empty',
thumbnailImageUrl: '',
occupants: 0
}
]
});
// The (0) should NOT be rendered because v-if="world.occupants" is falsy for 0
const items = wrapper.findAll('.cursor-pointer');
expect(items).toHaveLength(1);
expect(wrapper.text()).not.toContain('(0)');
});
test('renders empty state when no worlds and not loading', () => {
const wrapper = mountComponent({ worlds: [] });
expect(wrapper.text()).toContain('0');
});
});
describe('loading state', () => {
test('disables refresh button when loading', () => {
const wrapper = mountComponent({ isWorldsLoading: true });
const button = wrapper.find('button');
expect(button.attributes('disabled')).toBeDefined();
});
test('refresh button is enabled when not loading', () => {
const wrapper = mountComponent({ isWorldsLoading: false });
const button = wrapper.find('button');
expect(button.attributes('disabled')).toBeUndefined();
});
});
describe('click interactions', () => {
test('calls showWorldDialog when a world is clicked', async () => {
const pinia = createTestingPinia({ stubActions: false });
const userStore = useUserStore(pinia);
const { useWorldStore } = await import('../../../../stores');
const worldStore = useWorldStore(pinia);
const showWorldDialogSpy = vi
.spyOn(worldStore, 'showWorldDialog')
.mockImplementation(() => {});
userStore.userDialog = {
id: 'usr_me',
ref: { id: 'usr_me' },
worlds: [...MOCK_WORLDS],
worldSorting: userDialogWorldSortingOptions.name,
worldOrder: userDialogWorldOrderOptions.descending,
isWorldsLoading: false
};
userStore.currentUser = { id: 'usr_me' };
const wrapper = mount(UserDialogWorldsTab, {
global: {
plugins: [pinia],
stubs: {
RefreshCw: { template: '<svg class="refresh-icon" />' }
}
}
});
const firstItem = wrapper.findAll('.cursor-pointer')[0];
await firstItem.trigger('click');
expect(showWorldDialogSpy).toHaveBeenCalledWith('wrld_1');
});
});
});