mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
310 lines
8.9 KiB
TypeScript
310 lines
8.9 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import type { PasswordFieldDescriptor } from './browser-password-field-rules'
|
|
import { parsePasswordForm } from './browser-password-form-parser'
|
|
|
|
type TestField = PasswordFieldDescriptor & { key: string }
|
|
|
|
function input(key: string, overrides: Partial<PasswordFieldDescriptor> = {}): TestField {
|
|
return {
|
|
key,
|
|
type: 'text',
|
|
wasPassword: false,
|
|
name: key,
|
|
id: '',
|
|
autocomplete: '',
|
|
placeholder: '',
|
|
ariaLabel: '',
|
|
labelText: '',
|
|
value: '',
|
|
visible: true,
|
|
disabled: false,
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
function password(key: string, overrides: Partial<PasswordFieldDescriptor> = {}): TestField {
|
|
return input(key, { type: 'password', ...overrides })
|
|
}
|
|
|
|
type Expected = {
|
|
kind: string
|
|
username?: string | null
|
|
password?: string | null
|
|
newPassword?: string | null
|
|
confirmPassword?: string | null
|
|
} | null
|
|
|
|
function parse(fields: TestField[], text = ''): Expected {
|
|
const parsed = parsePasswordForm({ fields, text })
|
|
if (!parsed) {
|
|
return null
|
|
}
|
|
return {
|
|
kind: parsed.kind,
|
|
username: parsed.username?.key ?? null,
|
|
password: parsed.password?.key ?? null,
|
|
newPassword: parsed.newPassword?.key ?? null,
|
|
confirmPassword: parsed.confirmPassword?.key ?? null
|
|
}
|
|
}
|
|
|
|
const roles = (
|
|
kind: string,
|
|
username: string | null,
|
|
current: string | null,
|
|
fresh: string | null = null,
|
|
confirm: string | null = null
|
|
): Expected => ({
|
|
kind,
|
|
username,
|
|
password: current,
|
|
newPassword: fresh,
|
|
confirmPassword: confirm
|
|
})
|
|
|
|
describe('parsePasswordForm', () => {
|
|
it.each<[string, TestField[], string, Expected]>([
|
|
[
|
|
'plain login',
|
|
[input('email', { type: 'email' }), password('pass')],
|
|
'sign in',
|
|
roles('login', 'email', 'pass')
|
|
],
|
|
[
|
|
'login by autocomplete, even with the username after the password',
|
|
[
|
|
password('pw', { autocomplete: 'current-password' }),
|
|
input('who', { autocomplete: 'username' })
|
|
],
|
|
'',
|
|
roles('login', 'who', 'pw')
|
|
],
|
|
[
|
|
'autocomplete webauthn suffix is dropped',
|
|
[
|
|
input('u', { autocomplete: 'section-a username webauthn' }),
|
|
password('p', { autocomplete: 'current-password webauthn' })
|
|
],
|
|
'',
|
|
roles('login', 'u', 'p')
|
|
],
|
|
[
|
|
'sign-up with confirm (typed equal values)',
|
|
[
|
|
input('email'),
|
|
password('pw1', { value: 'hunter2' }),
|
|
password('pw2', { value: 'hunter2' })
|
|
],
|
|
'create account',
|
|
roles('signup', 'email', null, 'pw1', 'pw2')
|
|
],
|
|
[
|
|
'sign-up by autocomplete before anything is typed',
|
|
[
|
|
input('email', { autocomplete: 'email' }),
|
|
password('a', { autocomplete: 'new-password' }),
|
|
password('b', { autocomplete: 'new-password' })
|
|
],
|
|
'',
|
|
roles('signup', 'email', null, 'a', 'b')
|
|
],
|
|
[
|
|
'change password with three typed fields',
|
|
[
|
|
password('old', { value: 'x' }),
|
|
password('new', { value: 'y' }),
|
|
password('again', { value: 'y' })
|
|
],
|
|
'',
|
|
roles('change', null, 'old', 'new', 'again')
|
|
],
|
|
[
|
|
'two different passwords are old + new',
|
|
[password('a', { value: 'x' }), password('b', { value: 'y' })],
|
|
'',
|
|
roles('change', null, 'a', 'b')
|
|
],
|
|
[
|
|
'two empty passwords are old + new',
|
|
[password('a'), password('b')],
|
|
'',
|
|
roles('change', null, 'a', 'b')
|
|
],
|
|
[
|
|
'three equal passwords: the first is current',
|
|
[password('a', { value: 'x' }), password('b', { value: 'x' }), password('c', { value: 'x' })],
|
|
'',
|
|
roles('login', null, 'a')
|
|
],
|
|
[
|
|
'three passwords, first two equal: new + confirm',
|
|
[password('a', { value: 'x' }), password('b', { value: 'x' }), password('c', { value: 'z' })],
|
|
'',
|
|
roles('signup', null, null, 'a', 'b')
|
|
],
|
|
[
|
|
'three different passwords: the first is current',
|
|
[password('a', { value: 'x' }), password('b', { value: 'y' }), password('c', { value: 'z' })],
|
|
'',
|
|
roles('login', null, 'a')
|
|
],
|
|
[
|
|
'three empty passwords are the change layout',
|
|
[password('a'), password('b'), password('c')],
|
|
'',
|
|
roles('change', null, 'a', 'b', 'c')
|
|
],
|
|
[
|
|
'show-password toggle keeps a field that was a password',
|
|
[input('user'), input('pass', { type: 'text', wasPassword: true })],
|
|
'',
|
|
roles('login', 'user', 'pass')
|
|
],
|
|
[
|
|
'only visible passwords count when any are visible',
|
|
[input('user'), password('hidden', { visible: false }), password('shown')],
|
|
'',
|
|
roles('login', 'user', 'shown')
|
|
],
|
|
[
|
|
'falls back to hidden passwords when none are visible',
|
|
[input('user'), password('hidden', { visible: false })],
|
|
'',
|
|
roles('login', 'user', 'hidden')
|
|
],
|
|
[
|
|
'prefers the username-looking field over the last one before the password',
|
|
[input('first_name'), input('login_email'), input('phone', { type: 'tel' }), password('pw')],
|
|
'',
|
|
roles('login', 'login_email', 'pw')
|
|
],
|
|
[
|
|
'takes the last text field when none looks like a username',
|
|
[input('alpha'), input('beta'), password('pw')],
|
|
'',
|
|
roles('login', 'beta', 'pw')
|
|
],
|
|
[
|
|
'matches username words in label, placeholder and aria-label',
|
|
[input('f1', { labelText: 'Email address' }), input('f2'), password('pw')],
|
|
'',
|
|
roles('login', 'f1', 'pw')
|
|
],
|
|
[
|
|
'prefers a focusable username over a hidden one',
|
|
[input('shown'), input('hidden_user', { visible: false }), password('pw')],
|
|
'',
|
|
roles('login', 'shown', 'pw')
|
|
],
|
|
[
|
|
'a search box next to a formless login is not the username',
|
|
[
|
|
input('email', { type: 'email' }),
|
|
input('q', { placeholder: 'Search docs' }),
|
|
password('pw')
|
|
],
|
|
'',
|
|
roles('login', 'email', 'pw')
|
|
],
|
|
[
|
|
'a type=search box is never the username',
|
|
[input('site', { type: 'search' }), password('pw')],
|
|
'',
|
|
roles('login', null, 'pw')
|
|
],
|
|
[
|
|
'a camelCase search name is a search box',
|
|
[input('searchInput'), password('pw')],
|
|
'',
|
|
roles('login', null, 'pw')
|
|
],
|
|
[
|
|
'OTP field after a password is ignored',
|
|
[input('user'), password('pw'), input('otp_code', { type: 'tel' })],
|
|
'',
|
|
roles('login', 'user', 'pw')
|
|
],
|
|
[
|
|
'one-time-code password box is not a password',
|
|
[password('code', { autocomplete: 'one-time-code' })],
|
|
'verify',
|
|
null
|
|
],
|
|
['camelCase otp name is excluded', [password('otpCode')], '', null],
|
|
['SSN and CVC fields are excluded', [input('ssn'), password('card_cvc')], '', null],
|
|
[
|
|
'card fields by autocomplete are excluded',
|
|
[input('num', { autocomplete: 'cc-number' }), password('pin', { autocomplete: 'cc-csc' })],
|
|
'',
|
|
null
|
|
],
|
|
[
|
|
'disabled password fields are ignored',
|
|
[input('user'), password('pw', { disabled: true })],
|
|
'',
|
|
null
|
|
],
|
|
[
|
|
'non-text inputs are ignored',
|
|
[input('remember', { type: 'checkbox' }), input('token', { type: 'hidden' })],
|
|
'',
|
|
null
|
|
]
|
|
])('%s', (_name, fields, text, expected) => {
|
|
expect(parse(fields, text)).toEqual(expected)
|
|
})
|
|
|
|
describe('username-first steps', () => {
|
|
it.each<[string, TestField[], string, string | null]>([
|
|
[
|
|
'Google: autocomplete=username identifier box',
|
|
[input('identifier', { type: 'email', id: 'identifierId', autocomplete: 'username' })],
|
|
'next',
|
|
'identifier'
|
|
],
|
|
[
|
|
'Microsoft: lone email box in a form that says Next',
|
|
[input('loginfmt', { type: 'email' })],
|
|
'sign in next',
|
|
'loginfmt'
|
|
],
|
|
[
|
|
'Okta: autocomplete=username with a remember-me checkbox',
|
|
[
|
|
input('identifier', { autocomplete: 'username' }),
|
|
input('rememberMe', { type: 'checkbox' })
|
|
],
|
|
'sign in',
|
|
'identifier'
|
|
],
|
|
['lone autocomplete=email box', [input('mail', { autocomplete: 'email' })], 'go', 'mail'],
|
|
['lone field without any sign-in wording', [input('city')], 'submit', null],
|
|
[
|
|
'contact form: autocomplete=email beside other fields',
|
|
[input('name'), input('mail', { autocomplete: 'email' }), input('subject')],
|
|
'send',
|
|
null
|
|
],
|
|
[
|
|
'newsletter email form',
|
|
[input('email', { type: 'email', autocomplete: 'email' })],
|
|
'subscribe to our newsletter',
|
|
null
|
|
],
|
|
[
|
|
'newsletter by field name',
|
|
[input('newsletter_email', { type: 'email' })],
|
|
'continue',
|
|
null
|
|
],
|
|
['lone search box that says next', [input('q', { type: 'search' })], 'next', null]
|
|
])('%s', (_name, fields, text, username) => {
|
|
const parsed = parse(fields, text)
|
|
expect(parsed ? parsed.username : null).toBe(username)
|
|
if (username) {
|
|
expect(parsed?.kind).toBe('username-only')
|
|
}
|
|
})
|
|
})
|
|
})
|