mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-11-09 00:22:38 -06:00
fix(auth): validate password before Enter submission
This commit is contained in:
@@ -12,8 +12,8 @@
|
||||
:autocomplete="autocomplete"
|
||||
:aria-invalid="isValid !== true ? true : undefined"
|
||||
:aria-describedby="errorId"
|
||||
@keyup.enter="e => $emit('submit', e)"
|
||||
@focusout="() => {validate(); validateAfterFirst = true}"
|
||||
@keyup.enter="handleSubmit"
|
||||
@focusout="e => {handleInput(e); validate(); validateAfterFirst = true}"
|
||||
@keyup="() => {validateAfterFirst ? validate() : null}"
|
||||
@input="handleInput"
|
||||
>
|
||||
@@ -71,12 +71,19 @@ const isValid = ref<true | string>(true)
|
||||
const validateAfterFirst = ref(false)
|
||||
const errorId = computed(() => isValid.value !== true ? `${props.id}-error` : undefined)
|
||||
|
||||
const validate = useDebounceFn(() => {
|
||||
const valid = validatePassword(props.modelValue, props.validateMinLength)
|
||||
function validateValue(value: string) {
|
||||
const valid = validatePassword(value, props.validateMinLength)
|
||||
isValid.value = valid === true ? true : t(valid)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
watchEffect(() => props.validateInitially && validate())
|
||||
const validate = useDebounceFn(() => validateValue(props.modelValue), 100)
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.validateInitially) {
|
||||
validateAfterFirst.value = true
|
||||
validate()
|
||||
}
|
||||
})
|
||||
|
||||
function togglePasswordFieldType() {
|
||||
passwordFieldType.value = passwordFieldType.value === 'password'
|
||||
@@ -87,6 +94,14 @@ function togglePasswordFieldType() {
|
||||
function handleInput(e: Event) {
|
||||
emit('update:modelValue', (e.target as HTMLInputElement).value)
|
||||
}
|
||||
|
||||
function handleSubmit(e: Event) {
|
||||
// Autofill may not fire an input event.
|
||||
handleInput(e)
|
||||
validateValue((e.target as HTMLInputElement).value)
|
||||
validateAfterFirst.value = true
|
||||
emit('submit', e)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -50,6 +50,51 @@ test.describe('Login', () => {
|
||||
await expect(page.locator('main h1')).toContainText(credentials.username)
|
||||
})
|
||||
|
||||
for (const scenario of ['typed', 'blurred', 'retried', 'autofilled'] as const) {
|
||||
test(`Should log in with Enter without flashing a password validation error (${scenario})`, async ({page}) => {
|
||||
await page.goto('/login')
|
||||
await page.locator('#username').fill(credentials.username)
|
||||
if (scenario === 'retried') {
|
||||
await page.getByRole('button', {name: 'Login', exact: true}).click()
|
||||
await expect(page.locator('#password-error')).toBeVisible()
|
||||
}
|
||||
if (scenario === 'autofilled') {
|
||||
await page.locator('#password').evaluate((input: HTMLInputElement, password) => {
|
||||
input.value = password
|
||||
}, credentials.password)
|
||||
} else {
|
||||
await page.locator('#password').pressSequentially(credentials.password)
|
||||
}
|
||||
if (scenario === 'blurred') {
|
||||
await page.locator('#password').press('Tab')
|
||||
await page.locator('#password').focus()
|
||||
}
|
||||
// Keep the form mounted while debounced validation runs.
|
||||
await page.route('**/api/v2/login', async route => {
|
||||
const response = await route.fetch()
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
await route.fulfill({response})
|
||||
})
|
||||
const errors = await page.evaluateHandle(() => {
|
||||
const errors: string[] = []
|
||||
const observer = new MutationObserver(() => {
|
||||
const error = document.querySelector('#password-error')
|
||||
if (error) errors.push(error.textContent ?? '')
|
||||
})
|
||||
observer.observe(document.body, {childList: true, subtree: true, characterData: true})
|
||||
return errors
|
||||
})
|
||||
|
||||
await page.locator('#password').press('Enter')
|
||||
await expect(page).toHaveURL('/')
|
||||
await expect(page.locator('main h1')).toContainText(credentials.username)
|
||||
expect(await errors.jsonValue()).toEqual([])
|
||||
await errors.dispose()
|
||||
await page.reload()
|
||||
await expect(page.locator('main h1')).toContainText(credentials.username)
|
||||
})
|
||||
}
|
||||
|
||||
test('Should fail with a bad password', async ({page}) => {
|
||||
const fixture = {
|
||||
username: 'test',
|
||||
|
||||
Reference in New Issue
Block a user