From 0875143d987007f2c5d1d90defccfd85508c7ae4 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Sun, 26 Jul 2026 02:47:35 +0800 Subject: [PATCH] fix: show optional affiliate code on registration --- frontend/src/views/auth/RegisterView.vue | 25 +++- .../views/auth/__tests__/RegisterView.spec.ts | 112 ++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 frontend/src/views/auth/__tests__/RegisterView.spec.ts diff --git a/frontend/src/views/auth/RegisterView.vue b/frontend/src/views/auth/RegisterView.vue index b4a7a60096..4b89779142 100644 --- a/frontend/src/views/auth/RegisterView.vue +++ b/frontend/src/views/auth/RegisterView.vue @@ -134,6 +134,27 @@ + +
+ +
+
+ +
+ +
+
+
-
+
(true) const emailVerifyEnabled = ref(false) const promoCodeEnabled = ref(true) const invitationCodeEnabled = ref(false) +const affiliateEnabled = ref(false) const turnstileEnabled = ref(false) const turnstileSiteKey = ref('') const siteName = ref('Sub2API') @@ -459,6 +481,7 @@ onMounted(async () => { emailVerifyEnabled.value = settings.email_verify_enabled promoCodeEnabled.value = settings.promo_code_enabled invitationCodeEnabled.value = settings.invitation_code_enabled + affiliateEnabled.value = settings.affiliate_enabled turnstileEnabled.value = settings.turnstile_enabled turnstileSiteKey.value = settings.turnstile_site_key || '' siteName.value = settings.site_name || 'Sub2API' diff --git a/frontend/src/views/auth/__tests__/RegisterView.spec.ts b/frontend/src/views/auth/__tests__/RegisterView.spec.ts new file mode 100644 index 0000000000..20f63018a6 --- /dev/null +++ b/frontend/src/views/auth/__tests__/RegisterView.spec.ts @@ -0,0 +1,112 @@ +import { flushPromises, mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import RegisterView from '@/views/auth/RegisterView.vue' + +const { getPublicSettingsMock } = vi.hoisted(() => ({ + getPublicSettingsMock: vi.fn() +})) + +const publicSettings = { + registration_enabled: true, + email_verify_enabled: false, + promo_code_enabled: false, + invitation_code_enabled: false, + affiliate_enabled: true, + turnstile_enabled: true, + turnstile_site_key: 'site-key', + site_name: 'Sub2API', + registration_email_suffix_whitelist: [], + linuxdo_oauth_enabled: false, + wechat_oauth_enabled: false, + oidc_oauth_enabled: false, + github_oauth_enabled: false, + google_oauth_enabled: false +} + +vi.mock('vue-router', () => ({ + useRouter: () => ({ push: vi.fn() }), + useRoute: () => ({ query: {} }) +})) + +vi.mock('vue-i18n', () => ({ + createI18n: () => ({ + global: { + t: (key: string) => key + } + }), + useI18n: () => ({ + t: (key: string) => key, + locale: { value: 'en' } + }) +})) + +vi.mock('@/stores', () => ({ + useAuthStore: () => ({ register: vi.fn() }), + useAppStore: () => ({ + showError: vi.fn(), + showSuccess: vi.fn(), + showWarning: vi.fn() + }) +})) + +vi.mock('@/api/auth', async () => { + const actual = await vi.importActual('@/api/auth') + return { + ...actual, + getPublicSettings: (...args: unknown[]) => getPublicSettingsMock(...args) + } +}) + +function mountRegister() { + return mount(RegisterView, { + global: { + stubs: { + AuthLayout: { template: '
' }, + Icon: true, + TurnstileWidget: { template: '
' }, + LoginAgreementPrompt: true, + EmailOAuthButtons: true, + LinuxDoOAuthSection: true, + WechatOAuthSection: true, + OidcOAuthSection: true, + RouterLink: true, + transition: false + } + } + }) +} + +describe('RegisterView invitation layout', () => { + beforeEach(() => { + getPublicSettingsMock.mockReset() + getPublicSettingsMock.mockResolvedValue(publicSettings) + }) + + it('keeps the optional affiliate invitation field before Turnstile', async () => { + const wrapper = mountRegister() + await flushPromises() + + const invitationField = wrapper.get('[data-testid="affiliate-invitation-field"]') + const turnstile = wrapper.get('[data-testid="registration-turnstile"]') + + expect(invitationField.get('input').attributes('id')).toBe('affiliate_code') + expect(invitationField.text()).toContain('common.optional') + expect( + invitationField.element.compareDocumentPosition(turnstile.element) & + Node.DOCUMENT_POSITION_FOLLOWING + ).toBeTruthy() + }) + + it('uses the mandatory invitation field without duplicating the affiliate field', async () => { + getPublicSettingsMock.mockResolvedValueOnce({ + ...publicSettings, + invitation_code_enabled: true + }) + + const wrapper = mountRegister() + await flushPromises() + + expect(wrapper.find('[data-testid="affiliate-invitation-field"]').exists()).toBe(false) + expect(wrapper.get('#invitation_code').exists()).toBe(true) + }) +})