/* @vitest-environment jsdom */ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, expect, test, vi } from 'vitest'; import { disableProfileRedeemCode, listProfileRedeemCodes, upsertProfileRedeemCode, } from '../api/adminApiClient'; import type { ProfileRedeemCodeAdminResponse } from '../api/adminApiTypes'; import { AdminRedeemCodePage } from './AdminRedeemCodePage'; vi.mock('../api/adminApiClient', () => ({ disableProfileRedeemCode: vi.fn(), formatAdminApiError: vi.fn((error: unknown) => error instanceof Error ? error.message : '请求失败', ), isAdminApiError: vi.fn(() => false), listProfileRedeemCodes: vi.fn(), upsertProfileRedeemCode: vi.fn(), })); const baseEntry: ProfileRedeemCodeAdminResponse = { code: 'LONG-LIVED', mode: 'public', rewardPoints: 100, maxUses: 1, globalUsedCount: 0, enabled: true, allowedUserIds: [], startsAt: null, expiresAt: null, createdBy: 'admin', createdAt: '2026-07-13T01:00:00Z', updatedAt: '2026-07-13T01:00:00Z', }; const entries: ProfileRedeemCodeAdminResponse[] = [ baseEntry, { ...baseEntry, code: 'PENDING', startsAt: '2999-01-01T00:00:00Z', }, { ...baseEntry, code: 'EXPIRED', expiresAt: '2000-01-01T00:00:00Z', }, { ...baseEntry, code: 'ACTIVE', startsAt: '2000-01-01T00:00:00Z', expiresAt: '2999-01-01T00:00:00Z', }, { ...baseEntry, code: 'DISABLED', enabled: false, startsAt: '2000-01-01T00:00:00Z', expiresAt: '2999-01-01T00:00:00Z', }, ]; beforeEach(() => { vi.clearAllMocks(); vi.mocked(listProfileRedeemCodes).mockResolvedValue({ entries, operations: [ { operationId: 'operation-1', codeKind: 'redeem', code: 'LONG-LIVED', action: 'create', operatorUserId: 'admin-account-internal-1', operatorDisplayName: '兑换码运营', createdAt: '2026-07-13T01:00:00Z', }, ], }); vi.mocked(upsertProfileRedeemCode).mockResolvedValue(baseEntry); vi.mocked(disableProfileRedeemCode).mockResolvedValue({ ...baseEntry, enabled: false, }); }); test('操作记录只展示管理员显示名称', async () => { render(); expect(await screen.findByText('兑换码运营')).toBeTruthy(); expect(screen.queryByText('admin-account-internal-1')).toBeNull(); }); test('兑换码列表展示生效状态与日期范围', async () => { render(); expect((await rowForCode('LONG-LIVED')).textContent).toContain('长期有效'); expect((await rowForCode('LONG-LIVED')).textContent).toContain('立即 / 长期'); expect((await rowForCode('PENDING')).textContent).toContain('未生效'); expect((await rowForCode('EXPIRED')).textContent).toContain('已过期'); expect((await rowForCode('ACTIVE')).textContent).toContain('有效'); expect((await rowForCode('DISABLED')).textContent).toContain('停用'); }); test('点击兑换码回填本地日期输入', async () => { const user = userEvent.setup(); render(); await user.click(await screen.findByRole('button', { name: 'ACTIVE' })); expect((screen.getByLabelText('开始时间') as HTMLInputElement).value).toBe( toLocalInputValue('2000-01-01T00:00:00Z'), ); expect((screen.getByLabelText('截止时间') as HTMLInputElement).value).toBe( toLocalInputValue('2999-01-01T00:00:00Z'), ); }); test('兑换码拒绝截止时间不晚于开始时间的配置', async () => { render(); await screen.findByRole('button', { name: 'LONG-LIVED' }); fireEvent.change(screen.getByLabelText('Code'), { target: { value: 'INVALID' }, }); fireEvent.change(screen.getByLabelText('开始时间'), { target: { value: '2026-07-13T10:00' }, }); fireEvent.change(screen.getByLabelText('截止时间'), { target: { value: '2026-07-13T10:00' }, }); expect(screen.getByText('截止时间必须晚于开始时间')).toBeTruthy(); expect( (screen.getByRole('button', { name: '保存' }) as HTMLButtonElement) .disabled, ).toBe(true); expect(upsertProfileRedeemCode).not.toHaveBeenCalled(); }); test('兑换码保存时把本地时间转换为 ISO 并保留空边界', async () => { const user = userEvent.setup(); render(); await screen.findByRole('button', { name: 'LONG-LIVED' }); fireEvent.change(screen.getByLabelText('Code'), { target: { value: 'WINDOWED' }, }); fireEvent.change(screen.getByLabelText('开始时间'), { target: { value: '2026-07-13T10:30' }, }); await user.click(screen.getByRole('button', { name: '保存' })); await user.click(screen.getByRole('button', { name: '确认' })); await waitFor(() => { expect(upsertProfileRedeemCode).toHaveBeenCalledWith( 'admin-token', expect.objectContaining({ code: 'WINDOWED', startsAt: new Date('2026-07-13T10:30').toISOString(), expiresAt: null, }), ); }); }); async function rowForCode(code: string) { const button = await screen.findByRole('button', { name: code }); const row = button.closest('tr'); if (!row) { throw new Error(`未找到兑换码 ${code} 所在行`); } return row; } function toLocalInputValue(value: string) { const date = new Date(value); const offsetMs = date.getTimezoneOffset() * 60 * 1000; return new Date(date.getTime() - offsetMs).toISOString().slice(0, 16); }