mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
fix(auth): show a loading placeholder for Turnstile
This commit is contained in:
@@ -1,11 +1,19 @@
|
||||
<template>
|
||||
<div v-if="siteKey" class="turnstile-wrapper">
|
||||
<div v-if="siteKey" class="turnstile-wrapper relative">
|
||||
<div
|
||||
v-if="loading"
|
||||
role="status"
|
||||
class="absolute inset-0 flex items-center justify-center rounded-lg border border-gray-200 bg-gray-50 text-sm text-gray-500 dark:border-dark-600 dark:bg-dark-700 dark:text-gray-400"
|
||||
>
|
||||
{{ t('auth.captchaLoading') }}
|
||||
</div>
|
||||
<div ref="containerRef" class="turnstile-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
interface TurnstileRenderOptions {
|
||||
sitekey: string
|
||||
@@ -47,6 +55,8 @@ const emit = defineEmits<{
|
||||
(e: 'error'): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const loading = ref(true)
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const widgetId = ref<string | null>(null)
|
||||
const scriptLoaded = ref(false)
|
||||
@@ -141,6 +151,8 @@ onMounted(async () => {
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Turnstile:', error)
|
||||
emit('error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import TurnstileWidget from '../TurnstileWidget.vue'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({ t: (key: string) => key })
|
||||
}))
|
||||
|
||||
const scriptSelector = 'script[src*="challenges.cloudflare.com/turnstile"]'
|
||||
let wrapper: VueWrapper | undefined
|
||||
|
||||
function installSDK() {
|
||||
const render = vi.fn<NonNullable<typeof window.turnstile>['render']>(() => 'widget-1')
|
||||
window.turnstile = { render, reset: vi.fn(), remove: vi.fn() }
|
||||
return render
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
wrapper?.unmount()
|
||||
wrapper = undefined
|
||||
delete window.turnstile
|
||||
delete window.onTurnstileLoad
|
||||
document.querySelectorAll(scriptSelector).forEach((script) => script.remove())
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('TurnstileWidget', () => {
|
||||
it('shows a loading placeholder until the SDK initializes the widget', async () => {
|
||||
wrapper = mount(TurnstileWidget, { props: { siteKey: 'site-key' } })
|
||||
|
||||
expect(wrapper.get('[role="status"]').text()).toBe('auth.captchaLoading')
|
||||
expect(document.querySelector(scriptSelector)).not.toBeNull()
|
||||
const container = wrapper.get('.turnstile-container').element
|
||||
|
||||
const render = installSDK()
|
||||
window.onTurnstileLoad?.()
|
||||
await flushPromises()
|
||||
|
||||
expect(render).toHaveBeenCalledWith(container, expect.objectContaining({ sitekey: 'site-key' }))
|
||||
expect(wrapper.find('[role="status"]').exists()).toBe(false)
|
||||
expect(wrapper.get('.turnstile-container').element).toBe(container)
|
||||
})
|
||||
|
||||
it('initializes an already loaded SDK and still forwards verification', async () => {
|
||||
const render = installSDK()
|
||||
wrapper = mount(TurnstileWidget, { props: { siteKey: 'site-key' } })
|
||||
await flushPromises()
|
||||
|
||||
expect(render).toHaveBeenCalledOnce()
|
||||
expect(document.querySelector(scriptSelector)).toBeNull()
|
||||
expect(wrapper.find('[role="status"]').exists()).toBe(false)
|
||||
|
||||
render.mock.calls[0]![1].callback('verified-token')
|
||||
expect(wrapper.emitted('verify')).toEqual([['verified-token']])
|
||||
})
|
||||
|
||||
it('ends loading and reports a script load failure', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
wrapper = mount(TurnstileWidget, { props: { siteKey: 'site-key' } })
|
||||
|
||||
document.querySelector(scriptSelector)!.dispatchEvent(new Event('error'))
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('[role="status"]').exists()).toBe(false)
|
||||
expect(wrapper.emitted('error')).toEqual([[]])
|
||||
})
|
||||
|
||||
it('does not show a placeholder or load the SDK without a site key', async () => {
|
||||
wrapper = mount(TurnstileWidget, { props: { siteKey: '' } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('.turnstile-wrapper').exists()).toBe(false)
|
||||
expect(wrapper.find('[role="status"]').exists()).toBe(false)
|
||||
expect(document.querySelector(scriptSelector)).toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user