mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-25 01:33:51 +02:00
add test
This commit is contained in:
263
src/views/Moderation/__tests__/Moderation.test.js
Normal file
263
src/views/Moderation/__tests__/Moderation.test.js
Normal file
@@ -0,0 +1,263 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
makeRef: (value) => ({ value, __v_isRef: true }),
|
||||
playerModerationTable: null,
|
||||
refreshPlayerModerations: vi.fn(),
|
||||
handlePlayerModerationDelete: vi.fn(),
|
||||
modalConfirm: vi.fn(),
|
||||
configGetString: vi.fn(),
|
||||
configSetString: vi.fn(),
|
||||
deletePlayerModeration: vi.fn(),
|
||||
pagination: null,
|
||||
columnHandlers: null
|
||||
}));
|
||||
|
||||
mocks.playerModerationTable = mocks.makeRef({
|
||||
loading: false,
|
||||
data: [],
|
||||
filters: [{ value: [] }, { value: '' }]
|
||||
});
|
||||
mocks.pagination = mocks.makeRef({
|
||||
pageIndex: 2,
|
||||
pageSize: 10
|
||||
});
|
||||
|
||||
vi.mock('pinia', () => ({
|
||||
storeToRefs: (store) => store
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../stores', () => ({
|
||||
useModerationStore: () => ({
|
||||
playerModerationTable: mocks.playerModerationTable,
|
||||
refreshPlayerModerations: (...args) => mocks.refreshPlayerModerations(...args),
|
||||
handlePlayerModerationDelete: (...args) => mocks.handlePlayerModerationDelete(...args)
|
||||
}),
|
||||
useAppearanceSettingsStore: () => ({
|
||||
tablePageSizes: [10, 25, 50],
|
||||
tablePageSize: 25
|
||||
}),
|
||||
useModalStore: () => ({
|
||||
confirm: (...args) => mocks.modalConfirm(...args)
|
||||
}),
|
||||
useVrcxStore: () => ({
|
||||
maxTableSize: 100
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../service/config.js', () => ({
|
||||
default: {
|
||||
getString: (...args) => mocks.configGetString(...args),
|
||||
setString: (...args) => mocks.configSetString(...args)
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../../../api', () => ({
|
||||
playerModerationRequest: {
|
||||
deletePlayerModeration: (...args) => mocks.deletePlayerModeration(...args)
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../../../shared/constants', () => ({
|
||||
moderationTypes: ['block', 'mute', 'unmute']
|
||||
}));
|
||||
|
||||
vi.mock('../columns.jsx', () => ({
|
||||
createColumns: (handlers) => {
|
||||
mocks.columnHandlers = handlers;
|
||||
return [];
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../../../lib/table/useVrcxVueTable', () => ({
|
||||
useVrcxVueTable: (options) => ({
|
||||
table: {
|
||||
getFilteredRowModel: () => ({ rows: options.data })
|
||||
},
|
||||
pagination: mocks.pagination
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../composables/useDataTableScrollHeight', () => ({
|
||||
useDataTableScrollHeight: () => ({
|
||||
tableStyle: {}
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/select', () => ({
|
||||
Select: {
|
||||
template: '<div><slot /></div>'
|
||||
},
|
||||
SelectContent: { template: '<div><slot /></div>' },
|
||||
SelectGroup: { template: '<div><slot /></div>' },
|
||||
SelectItem: { template: '<div><slot /></div>' },
|
||||
SelectTrigger: { template: '<div><slot /></div>' },
|
||||
SelectValue: { template: '<div><slot /></div>' }
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/button', () => ({
|
||||
Button: {
|
||||
emits: ['click'],
|
||||
template: '<button data-testid="moderation-button" @click="$emit(\'click\')"><slot /></button>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/input-group', () => ({
|
||||
InputGroupField: {
|
||||
template: '<input />'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/spinner', () => ({
|
||||
Spinner: { template: '<span />' }
|
||||
}));
|
||||
|
||||
vi.mock('lucide-vue-next', () => ({
|
||||
RefreshCw: { template: '<span />' }
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/data-table', () => ({
|
||||
DataTableLayout: {
|
||||
props: ['totalItems'],
|
||||
template:
|
||||
'<div data-testid="moderation-layout">' +
|
||||
'<span data-testid="total-items">{{ totalItems }}</span>' +
|
||||
'</div>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/tooltip', () => ({
|
||||
TooltipWrapper: { template: '<div><slot /></div>' }
|
||||
}));
|
||||
|
||||
import Moderation from '../Moderation.vue';
|
||||
|
||||
function mountModeration() {
|
||||
return mount(Moderation, {
|
||||
global: {
|
||||
stubs: {
|
||||
TooltipWrapper: { template: '<div><slot /></div>' }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function flushAsync() {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
describe('Moderation.vue', () => {
|
||||
beforeEach(() => {
|
||||
mocks.playerModerationTable.value = {
|
||||
loading: false,
|
||||
data: [],
|
||||
filters: [{ value: [] }, { value: '' }]
|
||||
};
|
||||
mocks.pagination.value = { pageIndex: 2, pageSize: 10 };
|
||||
mocks.columnHandlers = null;
|
||||
|
||||
mocks.refreshPlayerModerations.mockReset();
|
||||
mocks.handlePlayerModerationDelete.mockReset();
|
||||
mocks.modalConfirm.mockReset();
|
||||
mocks.configGetString.mockReset();
|
||||
mocks.configSetString.mockReset();
|
||||
mocks.deletePlayerModeration.mockReset();
|
||||
|
||||
mocks.configGetString.mockResolvedValue('["block"]');
|
||||
mocks.modalConfirm.mockResolvedValue({ ok: true });
|
||||
mocks.deletePlayerModeration.mockResolvedValue({ ok: true, id: 'pm_1' });
|
||||
});
|
||||
|
||||
test('loads persisted moderation filter on init', async () => {
|
||||
mountModeration();
|
||||
await flushAsync();
|
||||
|
||||
expect(mocks.configGetString).toHaveBeenCalledWith('VRCX_playerModerationTableFilters', '[]');
|
||||
expect(mocks.playerModerationTable.value.filters[0].value).toEqual(['block']);
|
||||
});
|
||||
|
||||
test('updates moderation filter and persists value', async () => {
|
||||
mocks.configGetString.mockResolvedValueOnce('[]');
|
||||
const wrapper = mountModeration();
|
||||
await flushAsync();
|
||||
wrapper.vm.handleModerationFilterChange(['mute']);
|
||||
await nextTick();
|
||||
|
||||
expect(mocks.playerModerationTable.value.filters[0].value).toEqual(['mute']);
|
||||
expect(mocks.configSetString).toHaveBeenCalledWith(
|
||||
'VRCX_playerModerationTableFilters',
|
||||
JSON.stringify(['mute'])
|
||||
);
|
||||
});
|
||||
|
||||
test('filters table rows by type and search text', async () => {
|
||||
mocks.playerModerationTable.value.data = [
|
||||
{ type: 'block', sourceDisplayName: 'Alpha', targetDisplayName: 'Beta' },
|
||||
{ type: 'mute', sourceDisplayName: 'Gamma', targetDisplayName: 'Delta' },
|
||||
{ type: 'mute', sourceDisplayName: 'X', targetDisplayName: 'Alice' }
|
||||
];
|
||||
mocks.playerModerationTable.value.filters = [{ value: ['mute'] }, { value: 'ali' }];
|
||||
|
||||
const wrapper = mountModeration();
|
||||
await flushAsync();
|
||||
|
||||
expect(wrapper.get('[data-testid="total-items"]').text()).toBe('1');
|
||||
});
|
||||
|
||||
test('refresh button triggers moderation refresh', async () => {
|
||||
const wrapper = mountModeration();
|
||||
|
||||
await wrapper.get('[data-testid="moderation-button"]').trigger('click');
|
||||
|
||||
expect(mocks.refreshPlayerModerations).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('delete handler calls API then store handler', async () => {
|
||||
mountModeration();
|
||||
const row = { targetUserId: 'usr_1', type: 'mute' };
|
||||
|
||||
await mocks.columnHandlers.onDelete(row);
|
||||
|
||||
expect(mocks.deletePlayerModeration).toHaveBeenCalledWith({
|
||||
moderated: 'usr_1',
|
||||
type: 'mute'
|
||||
});
|
||||
expect(mocks.handlePlayerModerationDelete).toHaveBeenCalledWith({ ok: true, id: 'pm_1' });
|
||||
});
|
||||
|
||||
test('delete prompt confirms before deletion', async () => {
|
||||
mountModeration();
|
||||
const row = { targetUserId: 'usr_2', type: 'block' };
|
||||
|
||||
mocks.modalConfirm.mockResolvedValueOnce({ ok: true });
|
||||
await mocks.columnHandlers.onDeletePrompt(row);
|
||||
await flushAsync();
|
||||
|
||||
expect(mocks.modalConfirm).toHaveBeenCalled();
|
||||
expect(mocks.deletePlayerModeration).toHaveBeenCalledWith({
|
||||
moderated: 'usr_2',
|
||||
type: 'block'
|
||||
});
|
||||
});
|
||||
|
||||
test('resets page index when page size changes', async () => {
|
||||
const wrapper = mountModeration();
|
||||
wrapper.vm.handlePageSizeChange(50);
|
||||
await nextTick();
|
||||
|
||||
expect(mocks.pagination.value).toEqual({
|
||||
pageIndex: 0,
|
||||
pageSize: 50
|
||||
});
|
||||
});
|
||||
});
|
||||
144
src/views/Moderation/__tests__/columns.test.js
Normal file
144
src/views/Moderation/__tests__/columns.test.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
shiftHeld: { value: false, __v_isRef: true },
|
||||
currentUser: { value: { id: 'usr_me' }, __v_isRef: true },
|
||||
showUserDialog: vi.fn(),
|
||||
t: vi.fn((key) => key),
|
||||
te: vi.fn((key) => key === 'view.moderation.filters.block')
|
||||
}));
|
||||
|
||||
vi.mock('pinia', () => ({
|
||||
storeToRefs: (store) => store
|
||||
}));
|
||||
|
||||
vi.mock('../../../plugin', () => ({
|
||||
i18n: {
|
||||
global: {
|
||||
t: (...args) => mocks.t(...args),
|
||||
te: (...args) => mocks.te(...args)
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../../../stores', () => ({
|
||||
useUiStore: () => ({
|
||||
shiftHeld: mocks.shiftHeld
|
||||
}),
|
||||
useUserStore: () => ({
|
||||
currentUser: mocks.currentUser,
|
||||
showUserDialog: (...args) => mocks.showUserDialog(...args)
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../shared/utils', () => ({
|
||||
formatDateFilter: (value, format) => `${format}:${value}`
|
||||
}));
|
||||
|
||||
vi.mock('../../../components/ui/badge', () => ({ Badge: 'Badge' }));
|
||||
vi.mock('../../../components/ui/button', () => ({ Button: 'Button' }));
|
||||
vi.mock('../../../components/ui/tooltip', () => ({
|
||||
Tooltip: 'Tooltip',
|
||||
TooltipContent: 'TooltipContent',
|
||||
TooltipTrigger: 'TooltipTrigger'
|
||||
}));
|
||||
vi.mock('lucide-vue-next', () => ({
|
||||
ArrowUpDown: 'ArrowUpDown',
|
||||
Trash2: 'Trash2',
|
||||
X: 'X'
|
||||
}));
|
||||
|
||||
import { createColumns } from '../columns.jsx';
|
||||
|
||||
function createElement(type, props, ...children) {
|
||||
return {
|
||||
type,
|
||||
props: props ?? {},
|
||||
children: children.flat()
|
||||
};
|
||||
}
|
||||
|
||||
function findNode(node, predicate) {
|
||||
if (!node) return null;
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) {
|
||||
const result = findNode(item, predicate);
|
||||
if (result) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (predicate(node)) return node;
|
||||
if (!node.children) return null;
|
||||
return findNode(node.children, predicate);
|
||||
}
|
||||
|
||||
describe('views/Moderation/columns.jsx', () => {
|
||||
beforeEach(() => {
|
||||
globalThis.React = { createElement };
|
||||
mocks.shiftHeld.value = false;
|
||||
mocks.currentUser.value = { id: 'usr_me' };
|
||||
mocks.showUserDialog.mockReset();
|
||||
});
|
||||
|
||||
test('source and target cells open corresponding user dialog', () => {
|
||||
const cols = createColumns({ onDelete: vi.fn(), onDeletePrompt: vi.fn() });
|
||||
const sourceCol = cols.find((c) => c.accessorKey === 'sourceDisplayName');
|
||||
const targetCol = cols.find((c) => c.accessorKey === 'targetDisplayName');
|
||||
const row = {
|
||||
original: {
|
||||
sourceUserId: 'usr_source',
|
||||
sourceDisplayName: 'Source',
|
||||
targetUserId: 'usr_target',
|
||||
targetDisplayName: 'Target'
|
||||
},
|
||||
getValue: vi.fn()
|
||||
};
|
||||
|
||||
const sourceCell = sourceCol.cell({ row });
|
||||
const targetCell = targetCol.cell({ row });
|
||||
findNode(sourceCell, (n) => n.type === 'span' && typeof n.props?.onClick === 'function').props.onClick();
|
||||
findNode(targetCell, (n) => n.type === 'span' && typeof n.props?.onClick === 'function').props.onClick();
|
||||
|
||||
expect(mocks.showUserDialog).toHaveBeenNthCalledWith(1, 'usr_source');
|
||||
expect(mocks.showUserDialog).toHaveBeenNthCalledWith(2, 'usr_target');
|
||||
});
|
||||
|
||||
test('action cell hidden when source user is not current user', () => {
|
||||
const cols = createColumns({ onDelete: vi.fn(), onDeletePrompt: vi.fn() });
|
||||
const actionCol = cols.find((c) => c.id === 'action');
|
||||
const vnode = actionCol.cell({
|
||||
row: {
|
||||
original: {
|
||||
sourceUserId: 'usr_other'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(vnode).toBeNull();
|
||||
});
|
||||
|
||||
test('action cell routes to onDeletePrompt or onDelete based on shift', () => {
|
||||
const onDelete = vi.fn();
|
||||
const onDeletePrompt = vi.fn();
|
||||
const cols = createColumns({ onDelete, onDeletePrompt });
|
||||
const actionCol = cols.find((c) => c.id === 'action');
|
||||
const row = {
|
||||
original: {
|
||||
sourceUserId: 'usr_me',
|
||||
targetUserId: 'usr_target',
|
||||
type: 'block'
|
||||
}
|
||||
};
|
||||
|
||||
const promptCell = actionCol.cell({ row });
|
||||
findNode(promptCell, (n) => n.type === 'button').props.onClick();
|
||||
expect(onDeletePrompt).toHaveBeenCalledWith(row.original);
|
||||
expect(onDelete).not.toHaveBeenCalled();
|
||||
|
||||
mocks.shiftHeld.value = true;
|
||||
const directCell = actionCol.cell({ row });
|
||||
findNode(directCell, (n) => n.type === 'button').props.onClick();
|
||||
expect(onDelete).toHaveBeenCalledWith(row.original);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user