mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-23 00:33:50 +02:00
refactor
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
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()
|
||||
}));
|
||||
vi.mock('../../../../api', () => ({
|
||||
groupRequest: {
|
||||
getCachedGroupGallery: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ json: [], params: {} })
|
||||
},
|
||||
userRequest: {}
|
||||
}));
|
||||
|
||||
import GroupDialogPhotosTab from '../GroupDialogPhotosTab.vue';
|
||||
import { useGroupStore } from '../../../../stores';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
const MOCK_GALLERIES = [
|
||||
{
|
||||
id: 'g1',
|
||||
name: 'Photos',
|
||||
description: 'General photos',
|
||||
membersOnly: false
|
||||
},
|
||||
{
|
||||
id: 'g2',
|
||||
name: 'Screenshots',
|
||||
description: 'Game screenshots',
|
||||
membersOnly: true,
|
||||
roleIdsToView: null
|
||||
}
|
||||
];
|
||||
|
||||
const MOCK_GALLERY_IMAGES = {
|
||||
g1: [
|
||||
{
|
||||
id: 'img1',
|
||||
imageUrl: 'https://img/photo1.png',
|
||||
groupId: 'grp_1',
|
||||
galleryId: 'g1'
|
||||
},
|
||||
{
|
||||
id: 'img2',
|
||||
imageUrl: 'https://img/photo2.png',
|
||||
groupId: 'grp_1',
|
||||
galleryId: 'g1'
|
||||
}
|
||||
],
|
||||
g2: [
|
||||
{
|
||||
id: 'img3',
|
||||
imageUrl: 'https://img/screen1.png',
|
||||
groupId: 'grp_1',
|
||||
galleryId: 'g2'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {object} overrides
|
||||
*/
|
||||
function mountComponent(overrides = {}) {
|
||||
const pinia = createTestingPinia({
|
||||
stubActions: false
|
||||
});
|
||||
|
||||
const groupStore = useGroupStore(pinia);
|
||||
groupStore.groupDialog = {
|
||||
id: 'grp_1',
|
||||
visible: true,
|
||||
ref: {
|
||||
galleries: [...MOCK_GALLERIES]
|
||||
},
|
||||
galleries: { ...MOCK_GALLERY_IMAGES },
|
||||
...overrides
|
||||
};
|
||||
|
||||
return mount(GroupDialogPhotosTab, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
stubs: {
|
||||
RefreshCw: { template: '<svg class="refresh-icon" />' }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('GroupDialogPhotosTab.vue', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
test('renders gallery names', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('Photos');
|
||||
expect(wrapper.text()).toContain('Screenshots');
|
||||
});
|
||||
|
||||
test('renders gallery image counts', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('2');
|
||||
expect(wrapper.text()).toContain('1');
|
||||
});
|
||||
|
||||
test('renders gallery descriptions', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('General photos');
|
||||
});
|
||||
|
||||
test('renders gallery images', () => {
|
||||
const wrapper = mountComponent();
|
||||
const images = wrapper.findAll('img');
|
||||
expect(images.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('renders refresh button', () => {
|
||||
const wrapper = mountComponent();
|
||||
const button = wrapper.find('button');
|
||||
expect(button.exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('renders zero count for empty gallery', () => {
|
||||
const wrapper = mountComponent({
|
||||
galleries: { g1: [], g2: [] }
|
||||
});
|
||||
expect(wrapper.text()).toContain('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loading state', () => {
|
||||
test('refresh button is enabled initially', () => {
|
||||
const wrapper = mountComponent();
|
||||
const button = wrapper.find('button');
|
||||
expect(button.attributes('disabled')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with no galleries', () => {
|
||||
test('renders without gallery tabs when ref.galleries is empty', () => {
|
||||
const wrapper = mountComponent({
|
||||
ref: { galleries: [] },
|
||||
galleries: {}
|
||||
});
|
||||
// No gallery tabs should be rendered
|
||||
expect(wrapper.text()).not.toContain('Photos');
|
||||
expect(wrapper.text()).not.toContain('Screenshots');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,245 @@
|
||||
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 GroupDialogPostsTab from '../GroupDialogPostsTab.vue';
|
||||
import { useGroupStore } from '../../../../stores';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
const MOCK_POSTS = [
|
||||
{
|
||||
id: 'post_1',
|
||||
title: 'Welcome Post',
|
||||
text: 'Hello everyone!',
|
||||
imageUrl: 'https://img/post1.png',
|
||||
authorId: 'usr_author1',
|
||||
editorId: null,
|
||||
roleIds: [],
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 'post_2',
|
||||
title: 'Rules Update',
|
||||
text: 'Updated rules here.',
|
||||
imageUrl: null,
|
||||
authorId: 'usr_author2',
|
||||
editorId: 'usr_editor',
|
||||
roleIds: ['role_1'],
|
||||
createdAt: '2024-02-01T00:00:00Z',
|
||||
updatedAt: '2024-02-15T00:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 'post_3',
|
||||
title: 'Event Announcement',
|
||||
text: '',
|
||||
imageUrl: null,
|
||||
authorId: 'usr_author1',
|
||||
editorId: null,
|
||||
roleIds: [],
|
||||
createdAt: '2024-03-01T00:00:00Z',
|
||||
updatedAt: '2024-03-01T00:00:00Z'
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {Object} overrides
|
||||
*/
|
||||
function mountComponent(overrides = {}) {
|
||||
const pinia = createTestingPinia({
|
||||
stubActions: false
|
||||
});
|
||||
|
||||
const groupStore = useGroupStore(pinia);
|
||||
groupStore.groupDialog = {
|
||||
id: 'grp_1',
|
||||
visible: true,
|
||||
posts: [...MOCK_POSTS],
|
||||
postsFiltered: [...MOCK_POSTS],
|
||||
postsSearch: '',
|
||||
ref: {
|
||||
roles: [
|
||||
{ id: 'role_1', name: 'Admin' },
|
||||
{ id: 'role_2', name: 'Member' }
|
||||
],
|
||||
permissions: []
|
||||
},
|
||||
...overrides
|
||||
};
|
||||
|
||||
return mount(GroupDialogPostsTab, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
stubs: {
|
||||
Eye: { template: '<svg class="eye-icon" />' },
|
||||
Pencil: { template: '<svg class="pencil-icon" />' },
|
||||
Trash2: { template: '<svg class="trash-icon" />' }
|
||||
}
|
||||
},
|
||||
props: {
|
||||
showGroupPostEditDialog: vi.fn(),
|
||||
confirmDeleteGroupPost: vi.fn()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('GroupDialogPostsTab.vue', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
test('renders post count', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('3');
|
||||
});
|
||||
|
||||
test('renders all post titles', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('Welcome Post');
|
||||
expect(wrapper.text()).toContain('Rules Update');
|
||||
expect(wrapper.text()).toContain('Event Announcement');
|
||||
});
|
||||
|
||||
test('renders post text', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('Hello everyone!');
|
||||
expect(wrapper.text()).toContain('Updated rules here.');
|
||||
});
|
||||
|
||||
test('renders dash for empty post text', () => {
|
||||
const wrapper = mountComponent({
|
||||
posts: [MOCK_POSTS[2]],
|
||||
postsFiltered: [MOCK_POSTS[2]]
|
||||
});
|
||||
const preElements = wrapper.findAll('pre');
|
||||
expect(preElements.some((pre) => pre.text() === '-')).toBe(true);
|
||||
});
|
||||
|
||||
test('renders post image when imageUrl exists', () => {
|
||||
const wrapper = mountComponent();
|
||||
const images = wrapper.findAll('img');
|
||||
expect(
|
||||
images.some(
|
||||
(img) => img.attributes('src') === 'https://img/post1.png'
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('does not render image for posts without imageUrl', () => {
|
||||
const wrapper = mountComponent({
|
||||
posts: [MOCK_POSTS[1]],
|
||||
postsFiltered: [MOCK_POSTS[1]]
|
||||
});
|
||||
expect(wrapper.findAll('img')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('renders search input', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'InputGroupField' }).exists()
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('renders empty state when no posts', () => {
|
||||
const wrapper = mountComponent({ posts: [], postsFiltered: [] });
|
||||
expect(wrapper.text()).toContain('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('filtered posts', () => {
|
||||
test('renders only filtered posts', () => {
|
||||
const wrapper = mountComponent({
|
||||
postsFiltered: [MOCK_POSTS[0]]
|
||||
});
|
||||
const postItems = wrapper.findAll('.cursor-default');
|
||||
// should only render 1 filtered post
|
||||
expect(postItems).toHaveLength(1);
|
||||
expect(wrapper.text()).toContain('Welcome Post');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,268 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key },
|
||||
install: vi.fn()
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../shared/utils', () => ({
|
||||
hasGroupPermission: vi.fn((_group, permission) => {
|
||||
if (_group?._mockPermissions) {
|
||||
return _group._mockPermissions.includes(permission);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
}));
|
||||
|
||||
import GroupModerationBulkActions from '../GroupModerationBulkActions.vue';
|
||||
|
||||
function mountComponent(props = {}) {
|
||||
return mount(GroupModerationBulkActions, {
|
||||
props: {
|
||||
selectUserId: '',
|
||||
selectedUsersArray: [],
|
||||
selectedRoles: [],
|
||||
note: '',
|
||||
progressCurrent: 0,
|
||||
progressTotal: 0,
|
||||
groupRef: {
|
||||
roles: [
|
||||
{ id: 'role_1', name: 'Admin' },
|
||||
{ id: 'role_2', name: 'Moderator' }
|
||||
],
|
||||
_mockPermissions: [
|
||||
'group-roles-assign',
|
||||
'group-members-manage',
|
||||
'group-members-remove',
|
||||
'group-bans-manage'
|
||||
]
|
||||
},
|
||||
...props
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
AlertTriangle: { template: '<svg class="alert-icon" />' },
|
||||
Trash2: { template: '<svg class="trash-icon" />' },
|
||||
X: { template: '<svg class="x-icon" />' },
|
||||
TooltipWrapper: {
|
||||
template: '<div class="tooltip-stub"><slot /><slot name="content" /></div>'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('GroupModerationBulkActions.vue', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
test('renders user ID input field', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('dialog.group_member_moderation.user_id');
|
||||
});
|
||||
|
||||
test('renders selected users section', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('dialog.group_member_moderation.selected_users');
|
||||
});
|
||||
|
||||
test('renders roles dropdown with available roles', () => {
|
||||
const wrapper = mountComponent();
|
||||
expect(wrapper.text()).toContain('dialog.group_member_moderation.selected_roles');
|
||||
});
|
||||
|
||||
test('renders action buttons', () => {
|
||||
const wrapper = mountComponent();
|
||||
const text = wrapper.text();
|
||||
expect(text).toContain('dialog.group_member_moderation.add_roles');
|
||||
expect(text).toContain('dialog.group_member_moderation.remove_roles');
|
||||
expect(text).toContain('dialog.group_member_moderation.save_note');
|
||||
expect(text).toContain('dialog.group_member_moderation.kick');
|
||||
expect(text).toContain('dialog.group_member_moderation.ban');
|
||||
expect(text).toContain('dialog.group_member_moderation.unban');
|
||||
});
|
||||
|
||||
test('renders selected user badges', () => {
|
||||
const wrapper = mountComponent({
|
||||
selectedUsersArray: [
|
||||
{ id: 'usr_1', userId: 'usr_1', membershipStatus: 'member', user: { displayName: 'Alice' } },
|
||||
{ id: 'usr_2', userId: 'usr_2', membershipStatus: 'member', user: { displayName: 'Bob' } }
|
||||
]
|
||||
});
|
||||
expect(wrapper.text()).toContain('Alice');
|
||||
expect(wrapper.text()).toContain('Bob');
|
||||
});
|
||||
|
||||
test('shows warning tooltip for non-member users', () => {
|
||||
const wrapper = mountComponent({
|
||||
selectedUsersArray: [
|
||||
{ id: 'usr_1', userId: 'usr_1', membershipStatus: 'banned', user: { displayName: 'Charlie' } }
|
||||
]
|
||||
});
|
||||
expect(wrapper.text()).toContain('dialog.group_member_moderation.user_isnt_in_group');
|
||||
});
|
||||
|
||||
test('does not show warning for member users', () => {
|
||||
const wrapper = mountComponent({
|
||||
selectedUsersArray: [
|
||||
{ id: 'usr_1', userId: 'usr_1', membershipStatus: 'member', user: { displayName: 'Alice' } }
|
||||
]
|
||||
});
|
||||
expect(wrapper.text()).not.toContain('dialog.group_member_moderation.user_isnt_in_group');
|
||||
});
|
||||
});
|
||||
|
||||
describe('progress indicator', () => {
|
||||
test('shows progress when progressCurrent > 0', () => {
|
||||
const wrapper = mountComponent({ progressCurrent: 3, progressTotal: 10 });
|
||||
expect(wrapper.text()).toContain('dialog.group_member_moderation.progress');
|
||||
expect(wrapper.text()).toContain('3/10');
|
||||
});
|
||||
|
||||
test('shows cancel button during progress', () => {
|
||||
const wrapper = mountComponent({ progressCurrent: 3, progressTotal: 10 });
|
||||
expect(wrapper.text()).toContain('dialog.group_member_moderation.cancel');
|
||||
});
|
||||
|
||||
test('hides progress when not in progress', () => {
|
||||
const wrapper = mountComponent({ progressCurrent: 0 });
|
||||
expect(wrapper.text()).not.toContain('dialog.group_member_moderation.progress');
|
||||
});
|
||||
});
|
||||
|
||||
describe('button disabled states', () => {
|
||||
test('add/remove roles disabled when no roles selected', () => {
|
||||
const wrapper = mountComponent({ selectedRoles: [] });
|
||||
const addBtn = wrapper.findAll('button').find((b) =>
|
||||
b.text().includes('dialog.group_member_moderation.add_roles')
|
||||
);
|
||||
expect(addBtn.attributes('disabled')).toBeDefined();
|
||||
});
|
||||
|
||||
test('add/remove roles enabled when roles are selected', () => {
|
||||
const wrapper = mountComponent({ selectedRoles: ['role_1'] });
|
||||
const addBtn = wrapper.findAll('button').find((b) =>
|
||||
b.text().includes('dialog.group_member_moderation.add_roles')
|
||||
);
|
||||
expect(addBtn.attributes('disabled')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('action buttons disabled during progress', () => {
|
||||
const wrapper = mountComponent({
|
||||
selectedRoles: ['role_1'],
|
||||
progressCurrent: 5,
|
||||
progressTotal: 10
|
||||
});
|
||||
const kickBtn = wrapper.findAll('button').find((b) =>
|
||||
b.text().includes('dialog.group_member_moderation.kick')
|
||||
);
|
||||
expect(kickBtn.attributes('disabled')).toBeDefined();
|
||||
});
|
||||
|
||||
test('select user button disabled when no user ID entered', () => {
|
||||
const wrapper = mountComponent({ selectUserId: '' });
|
||||
const selectBtn = wrapper.findAll('button').find((b) =>
|
||||
b.text().includes('dialog.group_member_moderation.select_user')
|
||||
);
|
||||
expect(selectBtn.attributes('disabled')).toBeDefined();
|
||||
});
|
||||
|
||||
test('select user button enabled when user ID is entered', () => {
|
||||
const wrapper = mountComponent({ selectUserId: 'usr_test' });
|
||||
const selectBtn = wrapper.findAll('button').find((b) =>
|
||||
b.text().includes('dialog.group_member_moderation.select_user')
|
||||
);
|
||||
expect(selectBtn.attributes('disabled')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('permissions', () => {
|
||||
test('disables kick when missing group-members-remove permission', () => {
|
||||
const wrapper = mountComponent({
|
||||
groupRef: {
|
||||
roles: [],
|
||||
_mockPermissions: ['group-bans-manage']
|
||||
}
|
||||
});
|
||||
const kickBtn = wrapper.findAll('button').find((b) =>
|
||||
b.text().includes('dialog.group_member_moderation.kick')
|
||||
);
|
||||
expect(kickBtn.attributes('disabled')).toBeDefined();
|
||||
});
|
||||
|
||||
test('disables ban/unban when missing group-bans-manage permission', () => {
|
||||
const wrapper = mountComponent({
|
||||
groupRef: {
|
||||
roles: [],
|
||||
_mockPermissions: ['group-members-remove']
|
||||
}
|
||||
});
|
||||
const banBtn = wrapper.findAll('button').find((b) =>
|
||||
b.text().includes('dialog.group_member_moderation.ban')
|
||||
);
|
||||
const unbanBtn = wrapper.findAll('button').find((b) =>
|
||||
b.text().includes('dialog.group_member_moderation.unban')
|
||||
);
|
||||
expect(banBtn.attributes('disabled')).toBeDefined();
|
||||
expect(unbanBtn.attributes('disabled')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
test('emits select-user on select button click', async () => {
|
||||
const wrapper = mountComponent({ selectUserId: 'usr_test' });
|
||||
const selectBtn = wrapper.findAll('button').find((b) =>
|
||||
b.text().includes('dialog.group_member_moderation.select_user')
|
||||
);
|
||||
await selectBtn.trigger('click');
|
||||
expect(wrapper.emitted('select-user')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('emits clear-all on trash button click', async () => {
|
||||
const wrapper = mountComponent();
|
||||
// The trash button is the rounded-full icon-sm button after "selected_users" label
|
||||
const buttons = wrapper.findAll('button');
|
||||
const trashBtn = buttons.find((b) => {
|
||||
const classes = b.classes();
|
||||
return classes.includes('rounded-full');
|
||||
});
|
||||
await trashBtn.trigger('click');
|
||||
expect(wrapper.emitted('clear-all')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('emits delete-user when removing a selected user', async () => {
|
||||
const user = { id: 'usr_1', userId: 'usr_1', membershipStatus: 'member', user: { displayName: 'Alice' } };
|
||||
const wrapper = mountComponent({ selectedUsersArray: [user] });
|
||||
// The X button is a native <button type="button"> inside each Badge
|
||||
const deleteBtn = wrapper.find('button[type="button"]');
|
||||
await deleteBtn.trigger('click');
|
||||
expect(wrapper.emitted('delete-user')?.[0]?.[0]).toEqual(user);
|
||||
});
|
||||
|
||||
test('emits ban on ban button click', async () => {
|
||||
const wrapper = mountComponent();
|
||||
const banBtn = wrapper.findAll('button').find((b) =>
|
||||
b.text().includes('dialog.group_member_moderation.ban')
|
||||
);
|
||||
await banBtn.trigger('click');
|
||||
expect(wrapper.emitted('ban')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('emits cancel-progress on cancel click', async () => {
|
||||
const wrapper = mountComponent({ progressCurrent: 3, progressTotal: 10 });
|
||||
const cancelBtn = wrapper.findAll('button').find((b) =>
|
||||
b.text().includes('dialog.group_member_moderation.cancel')
|
||||
);
|
||||
await cancelBtn.trigger('click');
|
||||
expect(wrapper.emitted('cancel-progress')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { ref } from 'vue';
|
||||
import { useGroupCalendarEvents } from '../useGroupCalendarEvents';
|
||||
|
||||
function createGroupDialog(calendar = []) {
|
||||
return ref({
|
||||
calendar
|
||||
});
|
||||
}
|
||||
|
||||
const PAST_DATE = '2020-01-01T00:00:00Z';
|
||||
const FUTURE_DATE = '2099-12-31T23:59:59Z';
|
||||
|
||||
describe('useGroupCalendarEvents', () => {
|
||||
describe('pastCalenderEvents', () => {
|
||||
test('returns empty array when calendar is null', () => {
|
||||
const groupDialog = ref({ calendar: null });
|
||||
const { pastCalenderEvents } = useGroupCalendarEvents(groupDialog);
|
||||
expect(pastCalenderEvents.value).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns empty array when calendar is undefined', () => {
|
||||
const groupDialog = ref({});
|
||||
const { pastCalenderEvents } = useGroupCalendarEvents(groupDialog);
|
||||
expect(pastCalenderEvents.value).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns empty array when no past events exist', () => {
|
||||
const groupDialog = createGroupDialog([
|
||||
{ id: '1', endsAt: FUTURE_DATE }
|
||||
]);
|
||||
const { pastCalenderEvents } = useGroupCalendarEvents(groupDialog);
|
||||
expect(pastCalenderEvents.value).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns only past events', () => {
|
||||
const groupDialog = createGroupDialog([
|
||||
{ id: '1', endsAt: PAST_DATE },
|
||||
{ id: '2', endsAt: FUTURE_DATE },
|
||||
{ id: '3', endsAt: PAST_DATE }
|
||||
]);
|
||||
const { pastCalenderEvents } = useGroupCalendarEvents(groupDialog);
|
||||
expect(pastCalenderEvents.value).toHaveLength(2);
|
||||
expect(pastCalenderEvents.value.map((e) => e.id)).toEqual([
|
||||
'1',
|
||||
'3'
|
||||
]);
|
||||
});
|
||||
|
||||
test('is reactive to calendar changes', () => {
|
||||
const groupDialog = createGroupDialog([]);
|
||||
const { pastCalenderEvents } = useGroupCalendarEvents(groupDialog);
|
||||
expect(pastCalenderEvents.value).toHaveLength(0);
|
||||
|
||||
groupDialog.value.calendar = [{ id: '1', endsAt: PAST_DATE }];
|
||||
expect(pastCalenderEvents.value).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upcomingCalenderEvents', () => {
|
||||
test('returns empty array when calendar is null', () => {
|
||||
const groupDialog = ref({ calendar: null });
|
||||
const { upcomingCalenderEvents } =
|
||||
useGroupCalendarEvents(groupDialog);
|
||||
expect(upcomingCalenderEvents.value).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns empty array when no upcoming events exist', () => {
|
||||
const groupDialog = createGroupDialog([
|
||||
{ id: '1', endsAt: PAST_DATE }
|
||||
]);
|
||||
const { upcomingCalenderEvents } =
|
||||
useGroupCalendarEvents(groupDialog);
|
||||
expect(upcomingCalenderEvents.value).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns only upcoming events', () => {
|
||||
const groupDialog = createGroupDialog([
|
||||
{ id: '1', endsAt: PAST_DATE },
|
||||
{ id: '2', endsAt: FUTURE_DATE },
|
||||
{ id: '3', endsAt: FUTURE_DATE }
|
||||
]);
|
||||
const { upcomingCalenderEvents } =
|
||||
useGroupCalendarEvents(groupDialog);
|
||||
expect(upcomingCalenderEvents.value).toHaveLength(2);
|
||||
expect(upcomingCalenderEvents.value.map((e) => e.id)).toEqual([
|
||||
'2',
|
||||
'3'
|
||||
]);
|
||||
});
|
||||
|
||||
test('past and upcoming are mutually exclusive', () => {
|
||||
const events = [
|
||||
{ id: '1', endsAt: PAST_DATE },
|
||||
{ id: '2', endsAt: FUTURE_DATE }
|
||||
];
|
||||
const groupDialog = createGroupDialog(events);
|
||||
const { pastCalenderEvents, upcomingCalenderEvents } =
|
||||
useGroupCalendarEvents(groupDialog);
|
||||
|
||||
const allIds = [
|
||||
...pastCalenderEvents.value.map((e) => e.id),
|
||||
...upcomingCalenderEvents.value.map((e) => e.id)
|
||||
];
|
||||
expect(allIds).toHaveLength(2);
|
||||
expect(new Set(allIds).size).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateFollowingCalendarData', () => {
|
||||
test('updates an existing event by id', () => {
|
||||
const groupDialog = createGroupDialog([
|
||||
{ id: '1', title: 'Old Title', endsAt: FUTURE_DATE },
|
||||
{ id: '2', title: 'Other', endsAt: FUTURE_DATE }
|
||||
]);
|
||||
const { updateFollowingCalendarData } =
|
||||
useGroupCalendarEvents(groupDialog);
|
||||
|
||||
updateFollowingCalendarData({ id: '1', title: 'New Title' });
|
||||
|
||||
expect(groupDialog.value.calendar[0].title).toBe('New Title');
|
||||
expect(groupDialog.value.calendar[0].endsAt).toBe(FUTURE_DATE);
|
||||
});
|
||||
|
||||
test('does not modify other events', () => {
|
||||
const groupDialog = createGroupDialog([
|
||||
{ id: '1', title: 'Event 1', endsAt: FUTURE_DATE },
|
||||
{ id: '2', title: 'Event 2', endsAt: FUTURE_DATE }
|
||||
]);
|
||||
const { updateFollowingCalendarData } =
|
||||
useGroupCalendarEvents(groupDialog);
|
||||
|
||||
updateFollowingCalendarData({ id: '1', title: 'Updated' });
|
||||
|
||||
expect(groupDialog.value.calendar[1].title).toBe('Event 2');
|
||||
});
|
||||
|
||||
test('does nothing when event id is not found', () => {
|
||||
const events = [{ id: '1', title: 'Event 1', endsAt: FUTURE_DATE }];
|
||||
const groupDialog = createGroupDialog([...events]);
|
||||
const { updateFollowingCalendarData } =
|
||||
useGroupCalendarEvents(groupDialog);
|
||||
|
||||
updateFollowingCalendarData({
|
||||
id: 'nonexistent',
|
||||
title: 'Updated'
|
||||
});
|
||||
|
||||
expect(groupDialog.value.calendar[0].title).toBe('Event 1');
|
||||
});
|
||||
|
||||
test('merges new properties into the event', () => {
|
||||
const groupDialog = createGroupDialog([
|
||||
{ id: '1', title: 'Event', endsAt: FUTURE_DATE }
|
||||
]);
|
||||
const { updateFollowingCalendarData } =
|
||||
useGroupCalendarEvents(groupDialog);
|
||||
|
||||
updateFollowingCalendarData({
|
||||
id: '1',
|
||||
userInterest: { isFollowing: true }
|
||||
});
|
||||
|
||||
expect(groupDialog.value.calendar[0].title).toBe('Event');
|
||||
expect(groupDialog.value.calendar[0].userInterest.isFollowing).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,248 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { ref } from 'vue';
|
||||
|
||||
vi.mock('../../../../api', () => ({
|
||||
groupRequest: {
|
||||
getCachedGroupGallery: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
import { useGroupGalleries } from '../useGroupGalleries';
|
||||
import { groupRequest } from '../../../../api';
|
||||
|
||||
function createGroupDialog(overrides = {}) {
|
||||
return ref({
|
||||
id: 'grp_1',
|
||||
ref: {
|
||||
galleries: []
|
||||
},
|
||||
galleries: {},
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
|
||||
describe('useGroupGalleries', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('groupGalleryTabs', () => {
|
||||
test('returns empty array when no galleries', () => {
|
||||
const groupDialog = createGroupDialog();
|
||||
const { groupGalleryTabs } = useGroupGalleries(groupDialog);
|
||||
expect(groupGalleryTabs.value).toEqual([]);
|
||||
});
|
||||
|
||||
test('maps galleries to tabs with index values', () => {
|
||||
const groupDialog = createGroupDialog({
|
||||
ref: {
|
||||
galleries: [
|
||||
{ id: 'g1', name: 'Photos' },
|
||||
{ id: 'g2', name: 'Screenshots' }
|
||||
]
|
||||
}
|
||||
});
|
||||
const { groupGalleryTabs } = useGroupGalleries(groupDialog);
|
||||
expect(groupGalleryTabs.value).toEqual([
|
||||
{ value: '0', label: 'Photos' },
|
||||
{ value: '1', label: 'Screenshots' }
|
||||
]);
|
||||
});
|
||||
|
||||
test('handles galleries with null name', () => {
|
||||
const groupDialog = createGroupDialog({
|
||||
ref: {
|
||||
galleries: [{ id: 'g1', name: null }]
|
||||
}
|
||||
});
|
||||
const { groupGalleryTabs } = useGroupGalleries(groupDialog);
|
||||
expect(groupGalleryTabs.value).toEqual([{ value: '0', label: '' }]);
|
||||
});
|
||||
|
||||
test('is reactive to gallery changes', () => {
|
||||
const groupDialog = createGroupDialog();
|
||||
const { groupGalleryTabs } = useGroupGalleries(groupDialog);
|
||||
expect(groupGalleryTabs.value).toHaveLength(0);
|
||||
|
||||
groupDialog.value.ref.galleries = [
|
||||
{ id: 'g1', name: 'New Gallery' }
|
||||
];
|
||||
expect(groupGalleryTabs.value).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupGalleryStatus', () => {
|
||||
test('returns blue for non-members-only gallery', () => {
|
||||
const groupDialog = createGroupDialog();
|
||||
const { groupGalleryStatus } = useGroupGalleries(groupDialog);
|
||||
expect(groupGalleryStatus({ membersOnly: false })).toEqual({
|
||||
blue: true
|
||||
});
|
||||
});
|
||||
|
||||
test('returns green for members-only without role restriction', () => {
|
||||
const groupDialog = createGroupDialog();
|
||||
const { groupGalleryStatus } = useGroupGalleries(groupDialog);
|
||||
expect(
|
||||
groupGalleryStatus({ membersOnly: true, roleIdsToView: null })
|
||||
).toEqual({ green: true });
|
||||
});
|
||||
|
||||
test('returns red for role-restricted gallery', () => {
|
||||
const groupDialog = createGroupDialog();
|
||||
const { groupGalleryStatus } = useGroupGalleries(groupDialog);
|
||||
expect(
|
||||
groupGalleryStatus({
|
||||
membersOnly: true,
|
||||
roleIdsToView: ['role1']
|
||||
})
|
||||
).toEqual({ red: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGroupGalleries', () => {
|
||||
test('resets galleries and tab before loading', async () => {
|
||||
const groupDialog = createGroupDialog({
|
||||
galleries: { old: [1, 2, 3] }
|
||||
});
|
||||
const { getGroupGalleries, groupDialogGalleryCurrentName } =
|
||||
useGroupGalleries(groupDialog);
|
||||
groupDialogGalleryCurrentName.value = '2';
|
||||
|
||||
await getGroupGalleries();
|
||||
|
||||
expect(groupDialogGalleryCurrentName.value).toBe('0');
|
||||
});
|
||||
|
||||
test('sets loading state correctly during fetch', async () => {
|
||||
const groupDialog = createGroupDialog({
|
||||
ref: {
|
||||
galleries: [{ id: 'g1', name: 'Gallery' }]
|
||||
}
|
||||
});
|
||||
groupRequest.getCachedGroupGallery.mockResolvedValue({
|
||||
json: [],
|
||||
params: { groupId: 'grp_1' }
|
||||
});
|
||||
|
||||
const { getGroupGalleries, isGroupGalleryLoading } =
|
||||
useGroupGalleries(groupDialog);
|
||||
expect(isGroupGalleryLoading.value).toBe(false);
|
||||
|
||||
const promise = getGroupGalleries();
|
||||
expect(isGroupGalleryLoading.value).toBe(true);
|
||||
|
||||
await promise;
|
||||
expect(isGroupGalleryLoading.value).toBe(false);
|
||||
});
|
||||
|
||||
test('calls getCachedGroupGallery for each gallery', async () => {
|
||||
const groupDialog = createGroupDialog({
|
||||
ref: {
|
||||
galleries: [
|
||||
{ id: 'g1', name: 'A' },
|
||||
{ id: 'g2', name: 'B' }
|
||||
]
|
||||
}
|
||||
});
|
||||
groupRequest.getCachedGroupGallery.mockResolvedValue({
|
||||
json: [],
|
||||
params: { groupId: 'grp_1' }
|
||||
});
|
||||
|
||||
const { getGroupGalleries } = useGroupGalleries(groupDialog);
|
||||
await getGroupGalleries();
|
||||
|
||||
expect(groupRequest.getCachedGroupGallery).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGroupGallery', () => {
|
||||
test('populates gallery images from API response', async () => {
|
||||
const groupDialog = createGroupDialog();
|
||||
const { getGroupGallery } = useGroupGalleries(groupDialog);
|
||||
|
||||
groupRequest.getCachedGroupGallery.mockResolvedValueOnce({
|
||||
json: [
|
||||
{
|
||||
groupId: 'grp_1',
|
||||
galleryId: 'g1',
|
||||
id: 'img1',
|
||||
imageUrl: 'url1'
|
||||
},
|
||||
{
|
||||
groupId: 'grp_1',
|
||||
galleryId: 'g1',
|
||||
id: 'img2',
|
||||
imageUrl: 'url2'
|
||||
}
|
||||
],
|
||||
params: { groupId: 'grp_1' }
|
||||
});
|
||||
|
||||
await getGroupGallery('grp_1', 'g1');
|
||||
|
||||
expect(groupDialog.value.galleries['g1']).toHaveLength(2);
|
||||
expect(groupDialog.value.galleries['g1'][0].id).toBe('img1');
|
||||
});
|
||||
|
||||
test('ignores images from different groups', async () => {
|
||||
const groupDialog = createGroupDialog();
|
||||
const { getGroupGallery } = useGroupGalleries(groupDialog);
|
||||
|
||||
groupRequest.getCachedGroupGallery.mockResolvedValueOnce({
|
||||
json: [
|
||||
{
|
||||
groupId: 'grp_other',
|
||||
galleryId: 'g1',
|
||||
id: 'img1',
|
||||
imageUrl: 'url1'
|
||||
}
|
||||
],
|
||||
params: { groupId: 'grp_other' }
|
||||
});
|
||||
|
||||
await getGroupGallery('grp_1', 'g1');
|
||||
|
||||
expect(groupDialog.value.galleries['g1']).toBeUndefined();
|
||||
});
|
||||
|
||||
test('stops pagination when fewer than 100 results returned', async () => {
|
||||
const groupDialog = createGroupDialog();
|
||||
const { getGroupGallery } = useGroupGalleries(groupDialog);
|
||||
|
||||
groupRequest.getCachedGroupGallery.mockResolvedValueOnce({
|
||||
json: Array.from({ length: 50 }, (_, i) => ({
|
||||
groupId: 'grp_1',
|
||||
galleryId: 'g1',
|
||||
id: `img${i}`,
|
||||
imageUrl: `url${i}`
|
||||
})),
|
||||
params: { groupId: 'grp_1' }
|
||||
});
|
||||
|
||||
await getGroupGallery('grp_1', 'g1');
|
||||
|
||||
expect(groupRequest.getCachedGroupGallery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('handles API errors gracefully', async () => {
|
||||
const groupDialog = createGroupDialog();
|
||||
const { getGroupGallery } = useGroupGalleries(groupDialog);
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
groupRequest.getCachedGroupGallery.mockRejectedValueOnce(
|
||||
new Error('API Error')
|
||||
);
|
||||
|
||||
await expect(
|
||||
getGroupGallery('grp_1', 'g1')
|
||||
).resolves.toBeUndefined();
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,464 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { ref } from 'vue';
|
||||
|
||||
vi.mock('../../../../api', () => ({
|
||||
groupRequest: {
|
||||
getGroupMembersSearch: vi.fn(),
|
||||
getCachedGroupMember: vi.fn(),
|
||||
getCachedGroupMembers: vi.fn()
|
||||
},
|
||||
userRequest: {}
|
||||
}));
|
||||
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()
|
||||
}));
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key },
|
||||
install: vi.fn()
|
||||
})
|
||||
}));
|
||||
vi.mock('worker-timers', () => ({
|
||||
setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
|
||||
clearTimeout: (id) => globalThis.clearTimeout(id)
|
||||
}));
|
||||
|
||||
import { useGroupMembers } from '../useGroupMembers';
|
||||
import { groupRequest } from '../../../../api';
|
||||
import { groupDialogFilterOptions } from '../../../../shared/constants';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param overrides
|
||||
*/
|
||||
function createGroupDialog(overrides = {}) {
|
||||
return ref({
|
||||
id: 'grp_1',
|
||||
visible: true,
|
||||
inGroup: false,
|
||||
members: [],
|
||||
memberSearch: '',
|
||||
memberSearchResults: [],
|
||||
memberSortOrder: { value: '' },
|
||||
memberFilter: { id: null, name: 'Everyone' },
|
||||
ref: { roles: [], memberCount: 0 },
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param overrides
|
||||
*/
|
||||
function createDeps(overrides = {}) {
|
||||
return {
|
||||
currentUser: ref({ id: 'usr_me' }),
|
||||
applyGroupMember: vi.fn((json) => json),
|
||||
handleGroupMember: vi.fn(),
|
||||
t: (key) => key,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
describe('useGroupMembers', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
groupRequest.getCachedGroupMembers.mockReset();
|
||||
});
|
||||
|
||||
describe('groupDialogMemberSortValue', () => {
|
||||
test('returns current sort order value', () => {
|
||||
const groupDialog = createGroupDialog({
|
||||
memberSortOrder: { value: 'joinedAt:desc', name: 'sort.joined' }
|
||||
});
|
||||
const { groupDialogMemberSortValue } = useGroupMembers(
|
||||
groupDialog,
|
||||
createDeps()
|
||||
);
|
||||
expect(groupDialogMemberSortValue.value).toBe('joinedAt:desc');
|
||||
});
|
||||
|
||||
test('returns empty string when no sort order', () => {
|
||||
const groupDialog = createGroupDialog({
|
||||
memberSortOrder: {}
|
||||
});
|
||||
const { groupDialogMemberSortValue } = useGroupMembers(
|
||||
groupDialog,
|
||||
createDeps()
|
||||
);
|
||||
expect(groupDialogMemberSortValue.value).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupDialogMemberFilterKey', () => {
|
||||
test('returns everyone when filter id is null', () => {
|
||||
const groupDialog = createGroupDialog({
|
||||
memberFilter: { id: null }
|
||||
});
|
||||
const { groupDialogMemberFilterKey } = useGroupMembers(
|
||||
groupDialog,
|
||||
createDeps()
|
||||
);
|
||||
expect(groupDialogMemberFilterKey.value).toBe('everyone');
|
||||
});
|
||||
|
||||
test('returns usersWithNoRole when filter id is empty string', () => {
|
||||
const groupDialog = createGroupDialog({ memberFilter: { id: '' } });
|
||||
const { groupDialogMemberFilterKey } = useGroupMembers(
|
||||
groupDialog,
|
||||
createDeps()
|
||||
);
|
||||
expect(groupDialogMemberFilterKey.value).toBe('usersWithNoRole');
|
||||
});
|
||||
|
||||
test('returns role:id for role-based filters', () => {
|
||||
const groupDialog = createGroupDialog({
|
||||
memberFilter: { id: 'role_123' }
|
||||
});
|
||||
const { groupDialogMemberFilterKey } = useGroupMembers(
|
||||
groupDialog,
|
||||
createDeps()
|
||||
);
|
||||
expect(groupDialogMemberFilterKey.value).toBe('role:role_123');
|
||||
});
|
||||
|
||||
test('returns null when no filter', () => {
|
||||
const groupDialog = createGroupDialog({ memberFilter: null });
|
||||
const { groupDialogMemberFilterKey } = useGroupMembers(
|
||||
groupDialog,
|
||||
createDeps()
|
||||
);
|
||||
expect(groupDialogMemberFilterKey.value).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupDialogMemberFilterGroups', () => {
|
||||
test('includes filter options and role groups', () => {
|
||||
const groupDialog = createGroupDialog({
|
||||
ref: {
|
||||
roles: [
|
||||
{ id: 'role_1', name: 'Admin', defaultRole: false },
|
||||
{ id: 'role_2', name: 'Member', defaultRole: true }
|
||||
],
|
||||
memberCount: 10
|
||||
}
|
||||
});
|
||||
const { groupDialogMemberFilterGroups } = useGroupMembers(
|
||||
groupDialog,
|
||||
createDeps()
|
||||
);
|
||||
const groups = groupDialogMemberFilterGroups.value;
|
||||
|
||||
expect(groups.length).toBeGreaterThanOrEqual(1);
|
||||
// should have a filters group
|
||||
const filtersGroup = groups.find((g) => g.key === 'filters');
|
||||
expect(filtersGroup).toBeDefined();
|
||||
expect(filtersGroup.items.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('excludes default roles from role items', () => {
|
||||
const groupDialog = createGroupDialog({
|
||||
ref: {
|
||||
roles: [
|
||||
{ id: 'role_1', name: 'Admin', defaultRole: false },
|
||||
{ id: 'role_2', name: 'Default', defaultRole: true }
|
||||
],
|
||||
memberCount: 10
|
||||
}
|
||||
});
|
||||
const { groupDialogMemberFilterGroups } = useGroupMembers(
|
||||
groupDialog,
|
||||
createDeps()
|
||||
);
|
||||
const rolesGroup = groupDialogMemberFilterGroups.value.find(
|
||||
(g) => g.key === 'roles'
|
||||
);
|
||||
|
||||
if (rolesGroup) {
|
||||
expect(rolesGroup.items).toHaveLength(1);
|
||||
expect(rolesGroup.items[0].label).toBe('Admin');
|
||||
}
|
||||
});
|
||||
|
||||
test('omits roles group when no non-default roles exist', () => {
|
||||
const groupDialog = createGroupDialog({
|
||||
ref: {
|
||||
roles: [
|
||||
{ id: 'role_1', name: 'Default', defaultRole: true }
|
||||
],
|
||||
memberCount: 10
|
||||
}
|
||||
});
|
||||
const { groupDialogMemberFilterGroups } = useGroupMembers(
|
||||
groupDialog,
|
||||
createDeps()
|
||||
);
|
||||
const rolesGroup = groupDialogMemberFilterGroups.value.find(
|
||||
(g) => g.key === 'roles'
|
||||
);
|
||||
expect(rolesGroup).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupMembersSearch', () => {
|
||||
test('clears results when search is less than 3 characters', () => {
|
||||
const groupDialog = createGroupDialog({ memberSearch: 'ab' });
|
||||
const { groupMembersSearch, isGroupMembersLoading } =
|
||||
useGroupMembers(groupDialog, createDeps());
|
||||
|
||||
groupMembersSearch();
|
||||
|
||||
expect(groupDialog.value.memberSearchResults).toEqual([]);
|
||||
expect(isGroupMembersLoading.value).toBe(false);
|
||||
});
|
||||
|
||||
test('calls API when search is 3 or more characters', async () => {
|
||||
const groupDialog = createGroupDialog({ memberSearch: 'abc' });
|
||||
groupRequest.getGroupMembersSearch.mockResolvedValue({
|
||||
json: { results: [{ userId: 'usr_1' }] },
|
||||
params: { groupId: 'grp_1' }
|
||||
});
|
||||
|
||||
const deps = createDeps();
|
||||
const { groupMembersSearch } = useGroupMembers(groupDialog, deps);
|
||||
groupMembersSearch();
|
||||
|
||||
// wait for the debounced call
|
||||
await vi.waitFor(() => {
|
||||
expect(groupRequest.getGroupMembersSearch).toHaveBeenCalledWith(
|
||||
{
|
||||
groupId: 'grp_1',
|
||||
query: 'abc',
|
||||
n: 100,
|
||||
offset: 0
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadMoreGroupMembers', () => {
|
||||
test('does not load when already done', async () => {
|
||||
const groupDialog = createGroupDialog();
|
||||
const { loadMoreGroupMembers, isGroupMembersDone } =
|
||||
useGroupMembers(groupDialog, createDeps());
|
||||
isGroupMembersDone.value = true;
|
||||
|
||||
await loadMoreGroupMembers();
|
||||
|
||||
expect(groupRequest.getCachedGroupMembers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('does not load when already loading', async () => {
|
||||
const groupDialog = createGroupDialog();
|
||||
const { loadMoreGroupMembers, isGroupMembersLoading } =
|
||||
useGroupMembers(groupDialog, createDeps());
|
||||
isGroupMembersLoading.value = true;
|
||||
|
||||
await loadMoreGroupMembers();
|
||||
|
||||
expect(groupRequest.getCachedGroupMembers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('marks done when fewer than n results returned', async () => {
|
||||
const groupDialog = createGroupDialog();
|
||||
groupRequest.getCachedGroupMembers.mockResolvedValue({
|
||||
json: [{ userId: 'usr_1' }],
|
||||
params: { groupId: 'grp_1', n: 100, offset: 0 }
|
||||
});
|
||||
|
||||
const {
|
||||
loadMoreGroupMembers,
|
||||
isGroupMembersDone,
|
||||
loadMoreGroupMembersParams
|
||||
} = useGroupMembers(groupDialog, createDeps());
|
||||
|
||||
loadMoreGroupMembersParams.value = {
|
||||
n: 100,
|
||||
offset: 0,
|
||||
groupId: 'grp_1',
|
||||
sort: 'joinedAt:desc'
|
||||
};
|
||||
|
||||
await loadMoreGroupMembers();
|
||||
|
||||
expect(isGroupMembersDone.value).toBe(true);
|
||||
});
|
||||
|
||||
test('appends members to groupDialog.members', async () => {
|
||||
const groupDialog = createGroupDialog({
|
||||
members: [{ userId: 'existing' }]
|
||||
});
|
||||
groupRequest.getCachedGroupMembers.mockResolvedValue({
|
||||
json: [{ userId: 'usr_new' }],
|
||||
params: { groupId: 'grp_1', n: 100, offset: 0 }
|
||||
});
|
||||
|
||||
const { loadMoreGroupMembers, loadMoreGroupMembersParams } =
|
||||
useGroupMembers(groupDialog, createDeps());
|
||||
|
||||
loadMoreGroupMembersParams.value = {
|
||||
n: 100,
|
||||
offset: 0,
|
||||
groupId: 'grp_1',
|
||||
sort: 'joinedAt:desc'
|
||||
};
|
||||
|
||||
await loadMoreGroupMembers();
|
||||
|
||||
expect(groupDialog.value.members).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('removes duplicate current user from first position', async () => {
|
||||
const deps = createDeps();
|
||||
const groupDialog = createGroupDialog({
|
||||
members: [{ userId: 'usr_me' }]
|
||||
});
|
||||
groupRequest.getCachedGroupMembers.mockResolvedValue({
|
||||
json: [{ userId: 'usr_me' }, { userId: 'usr_2' }],
|
||||
params: { groupId: 'grp_1', n: 100, offset: 0 }
|
||||
});
|
||||
|
||||
const { loadMoreGroupMembers, loadMoreGroupMembersParams } =
|
||||
useGroupMembers(groupDialog, deps);
|
||||
|
||||
loadMoreGroupMembersParams.value = {
|
||||
n: 100,
|
||||
offset: 0,
|
||||
groupId: 'grp_1',
|
||||
sort: 'joinedAt:desc'
|
||||
};
|
||||
|
||||
await loadMoreGroupMembers();
|
||||
|
||||
// duplicate at position 0 should be removed
|
||||
const userIds = groupDialog.value.members.map((m) => m.userId);
|
||||
expect(userIds).toEqual(['usr_me', 'usr_2']);
|
||||
});
|
||||
|
||||
test('marks done on error', async () => {
|
||||
const groupDialog = createGroupDialog();
|
||||
groupRequest.getCachedGroupMembers.mockRejectedValue(
|
||||
new Error('fail')
|
||||
);
|
||||
|
||||
const {
|
||||
loadMoreGroupMembers,
|
||||
isGroupMembersDone,
|
||||
loadMoreGroupMembersParams
|
||||
} = useGroupMembers(groupDialog, createDeps());
|
||||
|
||||
loadMoreGroupMembersParams.value = {
|
||||
n: 100,
|
||||
offset: 0,
|
||||
groupId: 'grp_1',
|
||||
sort: 'joinedAt:desc'
|
||||
};
|
||||
|
||||
await expect(loadMoreGroupMembers()).rejects.toThrow('fail');
|
||||
expect(isGroupMembersDone.value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setGroupMemberSortOrder', () => {
|
||||
test('does not reload when sort order unchanged', async () => {
|
||||
const groupDialog = createGroupDialog({
|
||||
memberSortOrder: { value: 'joinedAt:desc' }
|
||||
});
|
||||
const { setGroupMemberSortOrder } = useGroupMembers(
|
||||
groupDialog,
|
||||
createDeps()
|
||||
);
|
||||
|
||||
await setGroupMemberSortOrder({ value: 'joinedAt:desc' });
|
||||
|
||||
expect(groupRequest.getCachedGroupMembers).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setGroupMemberFilter', () => {
|
||||
test('does not reload when filter unchanged', async () => {
|
||||
const { markRaw } = require('vue');
|
||||
const filter = markRaw(groupDialogFilterOptions.everyone);
|
||||
const groupDialog = createGroupDialog();
|
||||
// Use markRaw to prevent Vue from wrapping the filter in a Proxy
|
||||
groupDialog.value.memberFilter = filter;
|
||||
const { setGroupMemberFilter } = useGroupMembers(
|
||||
groupDialog,
|
||||
createDeps()
|
||||
);
|
||||
|
||||
await setGroupMemberFilter(filter);
|
||||
|
||||
expect(groupRequest.getCachedGroupMembers).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,436 @@
|
||||
import { reactive, ref } from 'vue';
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('vue-sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key },
|
||||
install: vi.fn()
|
||||
})
|
||||
}));
|
||||
vi.mock('../../../../plugin/router', () => {
|
||||
const { ref: vRef } = require('vue');
|
||||
return {
|
||||
router: {
|
||||
beforeEach: vi.fn(),
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
currentRoute: vRef({ path: '/', name: '', meta: {} }),
|
||||
isReady: vi.fn().mockResolvedValue(true)
|
||||
},
|
||||
initRouter: vi.fn()
|
||||
};
|
||||
});
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
const { ref: vRef } = require('vue');
|
||||
return {
|
||||
...actual,
|
||||
useRouter: vi.fn(() => ({
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
currentRoute: vRef({ 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()
|
||||
}));
|
||||
vi.mock('../../../../api', () => ({
|
||||
groupRequest: {},
|
||||
userRequest: {}
|
||||
}));
|
||||
|
||||
import { useGroupModerationData } from '../useGroupModerationData';
|
||||
|
||||
function createTables() {
|
||||
return {
|
||||
members: reactive({ data: [], pageSize: 15 }),
|
||||
bans: reactive({ data: [], filters: [{ prop: ['$displayName'], value: '' }], pageSize: 15 }),
|
||||
invites: reactive({ data: [], pageSize: 15 }),
|
||||
joinRequests: reactive({ data: [], pageSize: 15 }),
|
||||
blocked: reactive({ data: [], pageSize: 15 }),
|
||||
logs: reactive({ data: [], filters: [{ prop: ['description'], value: '' }], pageSize: 15 })
|
||||
};
|
||||
}
|
||||
|
||||
function createDeps(overrides = {}) {
|
||||
const tables = createTables();
|
||||
return {
|
||||
groupMemberModeration: ref({
|
||||
id: 'grp_test',
|
||||
visible: true,
|
||||
groupRef: { memberCount: 10, roles: [] }
|
||||
}),
|
||||
currentUser: ref({ id: 'usr_self' }),
|
||||
applyGroupMember: vi.fn((json) => json),
|
||||
handleGroupMember: vi.fn(),
|
||||
tables,
|
||||
selection: {
|
||||
selectedUsers: {},
|
||||
setSelectedUsers: vi.fn()
|
||||
},
|
||||
groupRequest: {
|
||||
getGroupBans: vi.fn(),
|
||||
getGroupLogs: vi.fn(),
|
||||
getGroupInvites: vi.fn(),
|
||||
getGroupJoinRequests: vi.fn(),
|
||||
getGroupMember: vi.fn(),
|
||||
getGroupMembers: vi.fn(),
|
||||
getGroupMembersSearch: vi.fn()
|
||||
},
|
||||
userRequest: {
|
||||
getCachedUser: vi.fn()
|
||||
},
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
describe('useGroupModerationData', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getAllGroupBans', () => {
|
||||
test('populates bans table with fetched data', async () => {
|
||||
const deps = createDeps();
|
||||
const bans = [
|
||||
{ userId: 'usr_1', user: { displayName: 'Alice' } },
|
||||
{ userId: 'usr_2', user: { displayName: 'Bob' } }
|
||||
];
|
||||
deps.groupRequest.getGroupBans.mockResolvedValue({
|
||||
json: bans,
|
||||
params: { groupId: 'grp_test' }
|
||||
});
|
||||
|
||||
const { getAllGroupBans } = useGroupModerationData(deps);
|
||||
await getAllGroupBans('grp_test');
|
||||
|
||||
expect(deps.tables.bans.data).toHaveLength(2);
|
||||
expect(deps.groupRequest.getGroupBans).toHaveBeenCalledWith({
|
||||
groupId: 'grp_test',
|
||||
n: 100,
|
||||
offset: 0
|
||||
});
|
||||
});
|
||||
|
||||
test('paginates through multiple pages', async () => {
|
||||
const deps = createDeps();
|
||||
const page1 = Array.from({ length: 100 }, (_, i) => ({
|
||||
userId: `usr_${i}`,
|
||||
user: { displayName: `User${i}` }
|
||||
}));
|
||||
const page2 = [{ userId: 'usr_100', user: { displayName: 'User100' } }];
|
||||
|
||||
deps.groupRequest.getGroupBans
|
||||
.mockResolvedValueOnce({ json: page1, params: { groupId: 'grp_test' } })
|
||||
.mockResolvedValueOnce({ json: page2, params: { groupId: 'grp_test' } });
|
||||
|
||||
const { getAllGroupBans } = useGroupModerationData(deps);
|
||||
await getAllGroupBans('grp_test');
|
||||
|
||||
expect(deps.tables.bans.data).toHaveLength(101);
|
||||
expect(deps.groupRequest.getGroupBans).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('skips data from wrong group', async () => {
|
||||
const deps = createDeps();
|
||||
deps.groupRequest.getGroupBans.mockResolvedValue({
|
||||
json: [{ userId: 'usr_1' }],
|
||||
params: { groupId: 'grp_other' }
|
||||
});
|
||||
|
||||
const { getAllGroupBans } = useGroupModerationData(deps);
|
||||
await getAllGroupBans('grp_test');
|
||||
|
||||
// Should have continued past the mismatched group and eventually exhausted pages
|
||||
// The data won't contain the mismatched entry because it was skipped
|
||||
expect(deps.tables.bans.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('handles API error gracefully', async () => {
|
||||
const { toast } = await import('vue-sonner');
|
||||
const deps = createDeps();
|
||||
deps.groupRequest.getGroupBans.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const { getAllGroupBans, isGroupMembersLoading } = useGroupModerationData(deps);
|
||||
await getAllGroupBans('grp_test');
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to get group bans');
|
||||
expect(isGroupMembersLoading.value).toBe(false);
|
||||
});
|
||||
|
||||
test('stops when dialog is no longer visible', async () => {
|
||||
const deps = createDeps();
|
||||
const page1 = Array.from({ length: 100 }, (_, i) => ({
|
||||
userId: `usr_${i}`
|
||||
}));
|
||||
deps.groupRequest.getGroupBans
|
||||
.mockResolvedValueOnce({ json: page1, params: { groupId: 'grp_test' } })
|
||||
.mockImplementation(() => {
|
||||
deps.groupMemberModeration.value.visible = false;
|
||||
return Promise.resolve({ json: [{ userId: 'usr_extra' }], params: { groupId: 'grp_test' } });
|
||||
});
|
||||
|
||||
const { getAllGroupBans } = useGroupModerationData(deps);
|
||||
await getAllGroupBans('grp_test');
|
||||
|
||||
// Should stop after detecting visible=false
|
||||
expect(deps.groupRequest.getGroupBans).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllGroupLogs', () => {
|
||||
test('populates logs table and deduplicates', async () => {
|
||||
const deps = createDeps();
|
||||
const logs = [
|
||||
{ id: 'log_1', description: 'event 1' },
|
||||
{ id: 'log_2', description: 'event 2' },
|
||||
{ id: 'log_1', description: 'event 1 dup' }
|
||||
];
|
||||
deps.groupRequest.getGroupLogs.mockResolvedValue({
|
||||
json: { results: logs, hasNext: false },
|
||||
params: { groupId: 'grp_test' }
|
||||
});
|
||||
|
||||
const { getAllGroupLogs } = useGroupModerationData(deps);
|
||||
await getAllGroupLogs('grp_test');
|
||||
|
||||
expect(deps.tables.logs.data).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('passes eventTypes filter when provided', async () => {
|
||||
const deps = createDeps();
|
||||
deps.groupRequest.getGroupLogs.mockResolvedValue({
|
||||
json: { results: [], hasNext: false },
|
||||
params: { groupId: 'grp_test' }
|
||||
});
|
||||
|
||||
const { getAllGroupLogs } = useGroupModerationData(deps);
|
||||
await getAllGroupLogs('grp_test', ['group.member.ban', 'group.member.kick']);
|
||||
|
||||
expect(deps.groupRequest.getGroupLogs).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
eventTypes: ['group.member.ban', 'group.member.kick']
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllGroupInvitesAndJoinRequests', () => {
|
||||
test('fetches invites, join requests, and blocked in parallel', async () => {
|
||||
const deps = createDeps();
|
||||
deps.groupRequest.getGroupInvites.mockResolvedValue({
|
||||
json: [{ userId: 'usr_inv' }],
|
||||
params: { groupId: 'grp_test' }
|
||||
});
|
||||
deps.groupRequest.getGroupJoinRequests
|
||||
.mockResolvedValueOnce({
|
||||
json: [{ userId: 'usr_join' }],
|
||||
params: { groupId: 'grp_test' }
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
json: [{ userId: 'usr_block' }],
|
||||
params: { groupId: 'grp_test' }
|
||||
});
|
||||
|
||||
const { getAllGroupInvitesAndJoinRequests } = useGroupModerationData(deps);
|
||||
await getAllGroupInvitesAndJoinRequests('grp_test');
|
||||
|
||||
expect(deps.tables.invites.data).toHaveLength(1);
|
||||
expect(deps.tables.joinRequests.data).toHaveLength(1);
|
||||
expect(deps.tables.blocked.data).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectGroupMemberUserId', () => {
|
||||
test('parses multiple user IDs from input', async () => {
|
||||
const deps = createDeps();
|
||||
deps.groupRequest.getGroupMember.mockResolvedValue({
|
||||
json: { userId: 'usr_aaaa1111-2222-3333-4444-555566667777', user: { displayName: 'A' } },
|
||||
params: {}
|
||||
});
|
||||
|
||||
const { selectGroupMemberUserId } = useGroupModerationData(deps);
|
||||
await selectGroupMemberUserId(
|
||||
'usr_aaaa1111-2222-3333-4444-555566667777 usr_bbbb1111-2222-3333-4444-555566667777'
|
||||
);
|
||||
|
||||
expect(deps.groupRequest.getGroupMember).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('falls back to raw input when no usr_ pattern found', async () => {
|
||||
const deps = createDeps();
|
||||
deps.groupRequest.getGroupMember.mockResolvedValue({
|
||||
json: { userId: 'some_input', user: { displayName: 'Test' } },
|
||||
params: {}
|
||||
});
|
||||
|
||||
const { selectGroupMemberUserId } = useGroupModerationData(deps);
|
||||
await selectGroupMemberUserId('some_input');
|
||||
|
||||
expect(deps.groupRequest.getGroupMember).toHaveBeenCalledWith({
|
||||
groupId: 'grp_test',
|
||||
userId: 'some_input'
|
||||
});
|
||||
});
|
||||
|
||||
test('does nothing with empty input', async () => {
|
||||
const deps = createDeps();
|
||||
const { selectGroupMemberUserId } = useGroupModerationData(deps);
|
||||
await selectGroupMemberUserId('');
|
||||
|
||||
expect(deps.groupRequest.getGroupMember).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addGroupMemberToSelection', () => {
|
||||
test('uses group member data when available', async () => {
|
||||
const deps = createDeps();
|
||||
const member = { userId: 'usr_1', user: { displayName: 'Alice' } };
|
||||
deps.groupRequest.getGroupMember.mockResolvedValue({
|
||||
json: member,
|
||||
params: {}
|
||||
});
|
||||
deps.applyGroupMember.mockReturnValue(member);
|
||||
|
||||
const { addGroupMemberToSelection } = useGroupModerationData(deps);
|
||||
await addGroupMemberToSelection('usr_1');
|
||||
|
||||
expect(deps.selection.setSelectedUsers).toHaveBeenCalledWith('usr_1', member);
|
||||
expect(deps.userRequest.getCachedUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('falls back to user API when member has no user object', async () => {
|
||||
const deps = createDeps();
|
||||
deps.groupRequest.getGroupMember.mockResolvedValue({
|
||||
json: { userId: 'usr_1' },
|
||||
params: {}
|
||||
});
|
||||
deps.applyGroupMember.mockReturnValue({ userId: 'usr_1' });
|
||||
deps.userRequest.getCachedUser.mockResolvedValue({
|
||||
json: { id: 'usr_1', displayName: 'Alice' }
|
||||
});
|
||||
|
||||
const { addGroupMemberToSelection } = useGroupModerationData(deps);
|
||||
await addGroupMemberToSelection('usr_1');
|
||||
|
||||
expect(deps.userRequest.getCachedUser).toHaveBeenCalledWith({ userId: 'usr_1' });
|
||||
expect(deps.selection.setSelectedUsers).toHaveBeenCalledWith('usr_1', expect.objectContaining({
|
||||
userId: 'usr_1',
|
||||
displayName: 'Alice'
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetData', () => {
|
||||
test('clears all table data and search state', () => {
|
||||
const deps = createDeps();
|
||||
deps.tables.members.data = [{ userId: 'usr_1' }];
|
||||
deps.tables.bans.data = [{ userId: 'usr_2' }];
|
||||
|
||||
const { resetData, memberSearch } = useGroupModerationData(deps);
|
||||
memberSearch.value = 'test';
|
||||
resetData();
|
||||
|
||||
expect(deps.tables.members.data).toHaveLength(0);
|
||||
expect(deps.tables.bans.data).toHaveLength(0);
|
||||
expect(deps.tables.invites.data).toHaveLength(0);
|
||||
expect(deps.tables.joinRequests.data).toHaveLength(0);
|
||||
expect(deps.tables.blocked.data).toHaveLength(0);
|
||||
expect(deps.tables.logs.data).toHaveLength(0);
|
||||
expect(memberSearch.value).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('member search / sort / filter', () => {
|
||||
test('groupMembersSearch clears table when search is too short', () => {
|
||||
const deps = createDeps();
|
||||
deps.tables.members.data = [{ userId: 'usr_1' }];
|
||||
|
||||
const { groupMembersSearch, memberSearch, isGroupMembersLoading } = useGroupModerationData(deps);
|
||||
memberSearch.value = 'ab';
|
||||
groupMembersSearch();
|
||||
|
||||
expect(deps.tables.members.data).toHaveLength(0);
|
||||
expect(isGroupMembersLoading.value).toBe(false);
|
||||
});
|
||||
|
||||
test('setGroupMemberSortOrder does nothing when sort is the same', async () => {
|
||||
const deps = createDeps();
|
||||
deps.groupRequest.getGroupMember.mockResolvedValue({ json: null, params: {} });
|
||||
|
||||
const { setGroupMemberSortOrder, memberSortOrder } = useGroupModerationData(deps);
|
||||
const currentSort = memberSortOrder.value;
|
||||
await setGroupMemberSortOrder(currentSort);
|
||||
|
||||
// getGroupMember should not have been called since sort didn't change
|
||||
expect(deps.groupRequest.getGroupMember).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('setGroupMemberFilter does nothing when filter is the same', async () => {
|
||||
const deps = createDeps();
|
||||
|
||||
const { setGroupMemberFilter, memberFilter } = useGroupModerationData(deps);
|
||||
const currentFilter = memberFilter.value;
|
||||
await setGroupMemberFilter(currentFilter);
|
||||
|
||||
expect(deps.groupRequest.getGroupMember).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadAllGroupMembers', () => {
|
||||
test('does nothing when already loading', async () => {
|
||||
const deps = createDeps();
|
||||
const { loadAllGroupMembers, isGroupMembersLoading } = useGroupModerationData(deps);
|
||||
isGroupMembersLoading.value = true;
|
||||
|
||||
await loadAllGroupMembers();
|
||||
|
||||
expect(deps.groupRequest.getGroupMember).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user