| import { describe, expect, it, vi } from 'vitest' |
| import { mount } from '@vue/test-utils' |
| import { nextTick } from 'vue' |
| import PaymentProviderDialog from '@/components/payment/PaymentProviderDialog.vue' |
| import { STRIPE_SDK_API_VERSION } from '@/components/payment/providerConfig' |
| import type { ProviderInstance } from '@/types/payment' |
|
|
| const messages: Record<string, string> = { |
| 'admin.settings.payment.providerConfig': 'Credentials', |
| 'admin.settings.payment.paymentGuideTrigger': 'View payment guide', |
| 'admin.settings.payment.alipayGuideSummary': 'Desktop prefers QR precreate and falls back to cashier; mobile prefers WAP checkout.', |
| 'admin.settings.payment.wxpayGuideSummary': 'Desktop prefers Native QR; mobile routes to JSAPI or H5 based on browser context.', |
| 'admin.settings.payment.airwallexGuideSummary': 'Use Payment Acceptance read/write only.', |
| 'admin.settings.payment.stripeWebhookHint': 'Configure Stripe webhook.', |
| 'admin.settings.payment.stripeWebhookApiVersionHint': 'Use Stripe API version {version}.', |
| 'admin.settings.payment.airwallexWebhookHint': 'Select payment_intent.succeeded and use the latest stable API version.', |
| } |
|
|
| vi.mock('vue-i18n', () => ({ |
| useI18n: () => ({ |
| t: (key: string, params?: Record<string, string>) => { |
| const message = messages[key] ?? key |
| if (!params) return message |
| return Object.entries(params).reduce( |
| (value, [name, replacement]) => value.replaceAll(`{${name}}`, replacement), |
| message, |
| ) |
| }, |
| }), |
| })) |
|
|
| function providerFactory(overrides: Partial<ProviderInstance> = {}): ProviderInstance { |
| return { |
| id: 1, |
| provider_key: 'airwallex', |
| name: 'Airwallex', |
| config: {}, |
| supported_types: ['airwallex'], |
| enabled: true, |
| payment_mode: '', |
| refund_enabled: false, |
| allow_user_refund: false, |
| limits: '', |
| sort_order: 0, |
| ...overrides, |
| } |
| } |
|
|
| function mountDialog(options: { editing?: ProviderInstance | null } = {}) { |
| return mount(PaymentProviderDialog, { |
| props: { |
| show: true, |
| saving: false, |
| editing: options.editing ?? null, |
| allKeyOptions: [ |
| { value: 'alipay', label: 'Alipay' }, |
| { value: 'wxpay', label: 'WeChat Pay' }, |
| { value: 'stripe', label: 'Stripe' }, |
| { value: 'airwallex', label: 'Airwallex' }, |
| ], |
| enabledKeyOptions: [ |
| { value: 'alipay', label: 'Alipay' }, |
| { value: 'wxpay', label: 'WeChat Pay' }, |
| { value: 'airwallex', label: 'Airwallex' }, |
| ], |
| allPaymentTypes: [ |
| { value: 'alipay', label: 'Alipay' }, |
| { value: 'wxpay', label: 'WeChat Pay' }, |
| ], |
| redirectLabel: 'Redirect', |
| }, |
| global: { |
| stubs: { |
| BaseDialog: { |
| template: '<div><slot /><slot name="footer" /></div>', |
| }, |
| Select: { |
| props: ['modelValue', 'options', 'disabled'], |
| template: '<div />', |
| }, |
| ToggleSwitch: { |
| template: '<div />', |
| }, |
| }, |
| }, |
| }) |
| } |
|
|
| describe('PaymentProviderDialog payment guide', () => { |
| it('shows no payment guide for providers without a flow guide', () => { |
| const wrapper = mountDialog() |
|
|
| expect(wrapper.text()).not.toContain(messages['admin.settings.payment.alipayGuideSummary']) |
| expect(wrapper.text()).not.toContain(messages['admin.settings.payment.wxpayGuideSummary']) |
| expect(wrapper.find('button[title="View payment guide"]').exists()).toBe(false) |
| }) |
|
|
| it.each([ |
| ['alipay', 'admin.settings.payment.alipayGuideSummary'], |
| ['wxpay', 'admin.settings.payment.wxpayGuideSummary'], |
| ['airwallex', 'admin.settings.payment.airwallexGuideSummary'], |
| ])('shows the payment guide summary for %s', async (providerKey, summaryKey) => { |
| const wrapper = mountDialog() |
|
|
| ;(wrapper.vm as unknown as { reset: (key: string) => void }).reset(providerKey) |
| await nextTick() |
|
|
| expect(wrapper.text()).toContain(messages[summaryKey]) |
| expect(wrapper.find('button[title="View payment guide"]').exists()).toBe(true) |
| }) |
|
|
| it('shows Airwallex webhook event and API version guidance with the webhook URL', async () => { |
| const wrapper = mountDialog() |
|
|
| ;(wrapper.vm as unknown as { reset: (key: string) => void }).reset('airwallex') |
| await nextTick() |
|
|
| expect(wrapper.text()).toContain(messages['admin.settings.payment.airwallexWebhookHint']) |
| expect(wrapper.text()).toContain('/api/v1/payment/webhook/airwallex') |
| }) |
|
|
| it('shows Stripe webhook API version guidance with the integrated SDK version', async () => { |
| const wrapper = mountDialog() |
|
|
| ;(wrapper.vm as unknown as { reset: (key: string) => void }).reset('stripe') |
| await nextTick() |
|
|
| expect(wrapper.text()).toContain(messages['admin.settings.payment.stripeWebhookHint']) |
| expect(wrapper.text()).toContain(`Use Stripe API version ${STRIPE_SDK_API_VERSION}.`) |
| expect(wrapper.text()).toContain('/api/v1/payment/webhook/stripe') |
| }) |
|
|
| it('emits an empty Airwallex accountId when the admin clears it', async () => { |
| const provider = providerFactory({ |
| config: { |
| clientId: 'cid_123', |
| apiBase: 'https://api.airwallex.com/api/v1', |
| countryCode: 'CN', |
| currency: 'CNY', |
| accountId: 'acct_123', |
| }, |
| }) |
| const wrapper = mountDialog({ editing: provider }) |
|
|
| ;(wrapper.vm as unknown as { loadProvider: (provider: ProviderInstance) => void }).loadProvider(provider) |
| await nextTick() |
|
|
| const accountIdInput = wrapper |
| .findAll('input[type="text"]') |
| .find(input => (input.element as HTMLInputElement).value === 'acct_123') |
| if (!accountIdInput) throw new Error('accountId input not found') |
|
|
| await accountIdInput.setValue('') |
| await wrapper.find('form').trigger('submit.prevent') |
|
|
| const payload = wrapper.emitted('save')?.[0]?.[0] as { config: Record<string, string> } |
| expect(payload.config.accountId).toBe('') |
| }) |
| }) |
|
|