import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { flushPromises, mount } from '@vue/test-utils' import type { AdminUser } from '@/types' import UsersView from '../UsersView.vue' const { listUsers, getAllGroups, getBatchUsersUsage, listEnabledDefinitions, getBatchUserAttributes } = vi.hoisted(() => ({ listUsers: vi.fn(), getAllGroups: vi.fn(), getBatchUsersUsage: vi.fn(), listEnabledDefinitions: vi.fn(), getBatchUserAttributes: vi.fn() })) vi.mock('@/api/admin', () => ({ adminAPI: { users: { list: listUsers, toggleStatus: vi.fn(), delete: vi.fn() }, groups: { getAll: getAllGroups }, dashboard: { getBatchUsersUsage }, userAttributes: { listEnabledDefinitions, getBatchUserAttributes } } })) vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showError: vi.fn(), showSuccess: vi.fn() }) })) vi.mock('vue-i18n', async () => { const actual = await vi.importActual('vue-i18n') return { ...actual, useI18n: () => ({ t: (key: string) => key }) } }) const createAdminUser = (overrides: Partial = {}): AdminUser => ({ id: 42, username: 'scoped-user', email: 'scoped@example.com', role: 'user', balance: 0, concurrency: 1, status: 'active', allowed_groups: [], balance_notify_enabled: false, balance_notify_threshold: null, balance_notify_extra_emails: [], created_at: '2026-04-17T00:00:00Z', updated_at: '2026-04-17T00:00:00Z', notes: '', last_active_at: '2026-04-16T02:00:00Z', last_used_at: '2026-04-17T02:00:00Z', current_concurrency: 0, ...overrides }) const DataTableStub = { props: ['columns', 'data'], emits: ['sort'], template: `
{{ columns.map(col => col.key).join(',') }}
{{ data.map(row => row.email).join(',') }}
` } describe('admin UsersView', () => { beforeEach(() => { vi.useRealTimers() localStorage.clear() listUsers.mockReset() getAllGroups.mockReset() getBatchUsersUsage.mockReset() listEnabledDefinitions.mockReset() getBatchUserAttributes.mockReset() listUsers.mockResolvedValue({ items: [createAdminUser()], total: 1, page: 1, page_size: 20, pages: 1 }) getAllGroups.mockResolvedValue([]) getBatchUsersUsage.mockResolvedValue({ stats: {} }) listEnabledDefinitions.mockResolvedValue([]) getBatchUserAttributes.mockResolvedValue({ values: {} }) }) afterEach(() => { vi.useRealTimers() }) it('shows active, used, and created activity columns in order and requests last_used_at sort', async () => { const wrapper = mount(UsersView, { global: { stubs: { AppLayout: { template: '
' }, TablePageLayout: { template: '
' }, DataTable: DataTableStub, Pagination: true, ConfirmDialog: true, EmptyState: true, GroupBadge: true, Select: true, UserAttributesConfigModal: true, UserConcurrencyCell: true, UserCreateModal: true, UserEditModal: true, UserApiKeysModal: true, UserAllowedGroupsModal: true, UserBalanceModal: true, UserBalanceHistoryModal: true, GroupReplaceModal: true, Icon: true, Teleport: true } } }) await flushPromises() const columns = wrapper.get('[data-test="columns"]').text() const visibleColumns = columns.split(',') expect(visibleColumns.slice(-4, -1)).toEqual(['last_active_at', 'last_used_at', 'created_at']) expect(visibleColumns).not.toContain('last_login_at') await wrapper.get('[data-test="sort-last-used"]').trigger('click') await flushPromises() expect(listUsers).toHaveBeenLastCalledWith( 1, 20, expect.objectContaining({ sort_by: 'last_used_at', sort_order: 'desc' }), expect.any(Object) ) }) it('clears usage current-page sort when switching to last_used_at server sort', async () => { vi.useFakeTimers() localStorage.setItem('user-column-settings-version', '3') localStorage.setItem( 'user-hidden-columns', JSON.stringify([ 'notes', 'groups', 'subscriptions', 'concurrency', 'usage_anthropic', 'usage_openai', 'usage_gemini', 'usage_antigravity', 'balance_platform_quota' ]) ) listUsers.mockResolvedValue({ items: [ createAdminUser({ id: 1, email: 'last-used-first@example.com' }), createAdminUser({ id: 2, email: 'usage-first@example.com' }) ], total: 2, page: 1, page_size: 20, pages: 1 }) getBatchUsersUsage.mockResolvedValue({ stats: { 1: { user_id: 1, today_actual_cost: 1, total_actual_cost: 1, by_platform: [] }, 2: { user_id: 2, today_actual_cost: 9, total_actual_cost: 9, by_platform: [] } } }) const wrapper = mount(UsersView, { global: { stubs: { AppLayout: { template: '
' }, TablePageLayout: { template: '
' }, DataTable: DataTableStub, Pagination: true, ConfirmDialog: true, EmptyState: true, GroupBadge: true, Select: true, UserAttributesConfigModal: true, UserConcurrencyCell: true, UserCreateModal: true, UserEditModal: true, UserApiKeysModal: true, UserAllowedGroupsModal: true, UserBalanceModal: true, UserBalanceHistoryModal: true, GroupReplaceModal: true, Icon: true, Teleport: true } } }) await flushPromises() await vi.advanceTimersByTimeAsync(50) await flushPromises() expect(wrapper.get('[data-test="row-order"]').text()).toBe('last-used-first@example.com,usage-first@example.com') await wrapper.get('[data-test="usage-sort-trigger-usage"]').trigger('click') await flushPromises() await wrapper.get('[data-test="usage-sort-usage-today"]').trigger('click') await flushPromises() expect(wrapper.get('[data-test="row-order"]').text()).toBe('usage-first@example.com,last-used-first@example.com') expect(localStorage.getItem('admin-users-usage-sort')).toContain('"key":"usage"') await wrapper.get('[data-test="sort-last-used"]').trigger('click') await flushPromises() expect(localStorage.getItem('admin-users-usage-sort')).toBeNull() expect(wrapper.get('[data-test="row-order"]').text()).toBe('last-used-first@example.com,usage-first@example.com') expect(listUsers).toHaveBeenLastCalledWith( 1, 20, expect.objectContaining({ sort_by: 'last_used_at', sort_order: 'desc' }), expect.any(Object) ) }) })