From 157475cb2e126ceb7363686337f15ac22ac7a301 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Thu, 23 Jan 2025 14:44:06 -0500 Subject: [PATCH] Add `validateUrlFn` props on UrlInput component (#2330) --- src/components/common/UrlInput.vue | 22 +++-- .../common/__tests__/UrlInput.test.ts | 83 +++++++++++++++++-- 2 files changed, 88 insertions(+), 17 deletions(-) diff --git a/src/components/common/UrlInput.vue b/src/components/common/UrlInput.vue index 327cac1d2..2dbe23c59 100644 --- a/src/components/common/UrlInput.vue +++ b/src/components/common/UrlInput.vue @@ -32,6 +32,7 @@ import { checkUrlReachable } from '@/utils/networkUtil' const props = defineProps<{ modelValue: string + validateUrlFn?: (url: string) => Promise }>() const emit = defineEmits<{ @@ -53,6 +54,16 @@ const handleInput = (value: string) => { validationState.value = UrlValidationState.IDLE } +// Default validation implementation +const defaultValidateUrl = async (url: string): Promise => { + if (!isValidUrl(url)) return false + try { + return await checkUrlReachable(url) + } catch { + return false + } +} + const validateUrl = async () => { const url = props.modelValue.trim() @@ -62,17 +73,10 @@ const validateUrl = async () => { // Skip validation if empty if (!url) return - // First check if it's a valid URL format - if (!isValidUrl(url)) { - validationState.value = UrlValidationState.INVALID - return - } - - // Then check if URL is reachable validationState.value = UrlValidationState.LOADING try { - const reachable = await checkUrlReachable(url) - validationState.value = reachable + const isValid = await (props.validateUrlFn ?? defaultValidateUrl)(url) + validationState.value = isValid ? UrlValidationState.VALID : UrlValidationState.INVALID } catch { diff --git a/src/components/common/__tests__/UrlInput.test.ts b/src/components/common/__tests__/UrlInput.test.ts index 18499475a..e9665198c 100644 --- a/src/components/common/__tests__/UrlInput.test.ts +++ b/src/components/common/__tests__/UrlInput.test.ts @@ -4,7 +4,7 @@ import IconField from 'primevue/iconfield' import InputIcon from 'primevue/inputicon' import InputText from 'primevue/inputtext' import { beforeEach, describe, expect, it } from 'vitest' -import { createApp } from 'vue' +import { createApp, nextTick } from 'vue' import UrlInput from '../UrlInput.vue' @@ -14,19 +14,86 @@ describe('UrlInput', () => { app.use(PrimeVue) }) - it('passes through additional attributes to input element', () => { - const wrapper = mount(UrlInput, { + const mountComponent = (props: any, options = {}) => { + return mount(UrlInput, { global: { plugins: [PrimeVue], components: { IconField, InputIcon, InputText } }, - props: { - modelValue: '', - placeholder: 'Enter URL', - disabled: true - } + props, + ...options + }) + } + + it('passes through additional attributes to input element', () => { + const wrapper = mountComponent({ + modelValue: '', + placeholder: 'Enter URL', + disabled: true }) expect(wrapper.find('input').attributes('disabled')).toBe('') }) + + it('emits update:modelValue on input', async () => { + const wrapper = mountComponent({ + modelValue: '', + placeholder: 'Enter URL' + }) + + const input = wrapper.find('input') + await input.setValue('https://test.com') + + expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([ + 'https://test.com' + ]) + }) + + it('renders spinner when validation is loading', async () => { + const wrapper = mountComponent({ + modelValue: 'https://test.com', + placeholder: 'Enter URL', + validateUrlFn: () => + new Promise(() => { + // Never resolves, simulating perpetual loading state + }) + }) + + const input = wrapper.findComponent(InputText) + await input.trigger('blur') + + await nextTick() + + expect(wrapper.find('.pi-spinner').exists()).toBe(true) + }) + + it('renders check icon when validation is valid', async () => { + const wrapper = mountComponent({ + modelValue: 'https://test.com', + placeholder: 'Enter URL', + validateUrlFn: () => Promise.resolve(true) + }) + + const input = wrapper.findComponent(InputText) + await input.trigger('blur') + + await nextTick() + + expect(wrapper.find('.pi-check').exists()).toBe(true) + }) + + it('renders cross icon when validation is invalid', async () => { + const wrapper = mountComponent({ + modelValue: 'https://test.com', + placeholder: 'Enter URL', + validateUrlFn: () => Promise.resolve(false) + }) + + const input = wrapper.findComponent(InputText) + await input.trigger('blur') + + await nextTick() + + expect(wrapper.find('.pi-times').exists()).toBe(true) + }) })