import { beforeEach, describe, expect, it, vi } from 'vitest' import { flushPromises, mount } from '@vue/test-utils' import RedeemView from '../RedeemView.vue' const { listRedeemCodes, batchUpdateRedeemCodes, getAllGroups, showSuccess, showError, showInfo } = vi.hoisted(() => ({ listRedeemCodes: vi.fn(), batchUpdateRedeemCodes: vi.fn(), getAllGroups: vi.fn(), showSuccess: vi.fn(), showError: vi.fn(), showInfo: vi.fn() })) vi.mock('@/api/admin', () => ({ adminAPI: { redeem: { list: listRedeemCodes, generate: vi.fn(), delete: vi.fn(), batchDelete: vi.fn(), batchUpdate: batchUpdateRedeemCodes, exportCodes: vi.fn() }, groups: { getAll: getAllGroups } } })) vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showSuccess, showError, showInfo }) })) vi.mock('@/composables/useClipboard', () => ({ useClipboard: () => ({ copyToClipboard: vi.fn() }) })) vi.mock('vue-i18n', async () => { const actual = await vi.importActual('vue-i18n') return { ...actual, useI18n: () => ({ t: (key: string) => key }) } }) const DataTableStub = { props: ['columns', 'data'], template: `
{{ column.label }}
{{ row[column.key] }}
` } const SelectStub = { props: ['modelValue', 'options'], emits: ['update:modelValue', 'change'], setup(props: { options: Array<{ value: unknown; label: string }> }, { emit }: { emit: (event: string, ...args: unknown[]) => void }) { const onChange = (event: Event) => { const raw = (event.target as HTMLSelectElement).value const option = props.options.find((item) => String(item.value ?? '') === raw) const value = option ? option.value : raw emit('update:modelValue', value) emit('change', value, option ?? null) } return { onChange } }, template: ` ` } describe('admin RedeemView batch update', () => { beforeEach(() => { localStorage.clear() document.body.innerHTML = '' listRedeemCodes.mockReset() batchUpdateRedeemCodes.mockReset() getAllGroups.mockReset() showSuccess.mockReset() showError.mockReset() showInfo.mockReset() listRedeemCodes.mockResolvedValue({ items: [ { id: 1, code: 'CODE-1', type: 'balance', value: 10, status: 'unused', used_by: null, used_at: null, created_at: '2026-01-01T00:00:00Z', expires_at: null }, { id: 2, code: 'CODE-2', type: 'balance', value: 20, status: 'unused', used_by: null, used_at: null, created_at: '2026-01-01T00:00:00Z', expires_at: null } ], total: 2, page: 1, page_size: 20, pages: 1 }) batchUpdateRedeemCodes.mockResolvedValue({ updated: 1, message: 'ok' }) getAllGroups.mockResolvedValue([]) }) it('submits only checked fields for selected redeem codes', async () => { const wrapper = mount(RedeemView, { attachTo: document.body, global: { stubs: { AppLayout: { template: '
' }, TablePageLayout: { template: '
' }, DataTable: DataTableStub, Pagination: true, ConfirmDialog: true, Select: SelectStub, GroupBadge: true, GroupOptionItem: true, Icon: true, Teleport: true } } }) await flushPromises() await wrapper.findAll('[data-test="select-code"]')[0].setValue(true) await wrapper.get('[data-test="batch-update-open"]').trigger('click') await flushPromises() await wrapper.get('[data-test="batch-field-status"]').setValue(true) await wrapper.get('[data-test="batch-status-select"]').setValue('disabled') await wrapper.get('[data-test="batch-field-notes"]').setValue(true) await wrapper.get('[data-test="batch-notes-input"]').setValue('maintenance') await wrapper.get('[data-test="batch-update-form"]').trigger('submit') await flushPromises() expect(batchUpdateRedeemCodes).toHaveBeenCalledWith([1], { status: 'disabled', notes: 'maintenance' }) expect(showSuccess).toHaveBeenCalledWith('admin.redeem.batchUpdateSuccess') }) })