mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
Merge pull request #7717 from ouchihao/codex/bug-payment-countdown-drift
fix(payment): calculate QR countdowns from elapsed time
This commit is contained in:
@@ -234,12 +234,13 @@ async function tryRecoverPendingOrder(order: PaymentOrder): Promise<PaymentOrder
|
||||
|
||||
function startCountdown(seconds: number) {
|
||||
remainingSeconds.value = Math.max(0, seconds)
|
||||
const deadline = Date.now() + remainingSeconds.value * 1000
|
||||
if (remainingSeconds.value <= 0) {
|
||||
expired.value = true
|
||||
return
|
||||
}
|
||||
countdownTimer = setInterval(() => {
|
||||
remainingSeconds.value--
|
||||
remainingSeconds.value = Math.max(0, Math.floor((deadline - Date.now()) / 1000))
|
||||
if (remainingSeconds.value <= 0) {
|
||||
expired.value = true
|
||||
cleanup()
|
||||
|
||||
@@ -450,9 +450,10 @@ async function pollStatus() {
|
||||
|
||||
function startCountdown(seconds: number) {
|
||||
remainingSeconds.value = Math.max(0, seconds)
|
||||
const deadline = Date.now() + remainingSeconds.value * 1000
|
||||
if (remainingSeconds.value <= 0) { setOutcome('expired'); return }
|
||||
countdownTimer = setInterval(() => {
|
||||
remainingSeconds.value--
|
||||
remainingSeconds.value = Math.max(0, Math.floor((deadline - Date.now()) / 1000))
|
||||
if (remainingSeconds.value <= 0) { setOutcome('expired'); cleanup() }
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { enableAutoUnmount, mount } from '@vue/test-utils'
|
||||
import PaymentQRDialog from '../PaymentQRDialog.vue'
|
||||
import PaymentStatusPanel from '../PaymentStatusPanel.vue'
|
||||
import PaymentQRCodeView from '@/views/user/PaymentQRCodeView.vue'
|
||||
|
||||
vi.mock('vue-i18n', async (importOriginal) => ({
|
||||
...await importOriginal<typeof import('vue-i18n')>(),
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
}))
|
||||
vi.mock('@/components/layout/AppLayout.vue', () => ({
|
||||
default: { template: '<div><slot /></div>' },
|
||||
}))
|
||||
vi.mock('@/stores/payment', () => ({
|
||||
usePaymentStore: () => ({ pollOrderStatus: vi.fn().mockResolvedValue(null) }),
|
||||
}))
|
||||
vi.mock('@/stores', () => ({ useAppStore: () => ({ showError: vi.fn() }) }))
|
||||
vi.mock('@/api/payment', () => ({ paymentAPI: {} }))
|
||||
vi.mock('qrcode', () => ({ default: { toCanvas: vi.fn() } }))
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => ({ query: { order_id: '42', expires_at: new Date(Date.now() + 120_000).toISOString() } }),
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
}))
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date('2026-09-30T12:00:00Z'))
|
||||
})
|
||||
afterEach(() => vi.useRealTimers())
|
||||
|
||||
async function open(kind: 'dialog' | 'panel' | 'page') {
|
||||
const props = { orderId: 42, qrCode: '', expiresAt: new Date(Date.now() + 120_000).toISOString(), paymentType: 'custom' }
|
||||
const global = { stubs: {
|
||||
Icon: true,
|
||||
AppLayout: { template: '<div><slot /></div>' },
|
||||
BaseDialog: { props: ['show'], template: '<div v-if="show"><slot /><slot name="footer" /></div>' },
|
||||
} }
|
||||
if (kind === 'page') return mount(PaymentQRCodeView, { global })
|
||||
if (kind === 'panel') return mount(PaymentStatusPanel, { props, global })
|
||||
const wrapper = mount(PaymentQRDialog, { props: { ...props, show: false }, global })
|
||||
await wrapper.setProps({ show: true })
|
||||
return wrapper
|
||||
}
|
||||
|
||||
describe.each(['dialog', 'panel', 'page'] as const)('payment countdown: %s', (kind) => {
|
||||
it('catches up after timers were suspended', async () => {
|
||||
const wrapper = await open(kind)
|
||||
expect(wrapper.text()).toContain('02:00')
|
||||
vi.setSystemTime(Date.now() + 65_000)
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
expect(wrapper.text()).toContain('00:54')
|
||||
})
|
||||
|
||||
it('expires and stops polling on the first overdue tick', async () => {
|
||||
const wrapper = await open(kind)
|
||||
vi.setSystemTime(Date.now() + 121_000)
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
expect(wrapper.text()).toContain('payment.qr.expired')
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
})
|
||||
|
||||
it('still counts down normally', async () => {
|
||||
const wrapper = await open(kind)
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
expect(wrapper.text()).toContain('01:59')
|
||||
})
|
||||
})
|
||||
@@ -157,12 +157,13 @@ async function pollStatus() {
|
||||
|
||||
function startCountdown(seconds: number) {
|
||||
remainingSeconds.value = Math.max(0, seconds)
|
||||
const deadline = Date.now() + remainingSeconds.value * 1000
|
||||
if (remainingSeconds.value <= 0) {
|
||||
expired.value = true
|
||||
return
|
||||
}
|
||||
countdownTimer = setInterval(() => {
|
||||
remainingSeconds.value--
|
||||
remainingSeconds.value = Math.max(0, Math.floor((deadline - Date.now()) / 1000))
|
||||
if (remainingSeconds.value <= 0) {
|
||||
expired.value = true
|
||||
cleanup()
|
||||
|
||||
Reference in New Issue
Block a user