Browse Source

feat: add PasswordStrength component with real-time strength scoring

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
feat/registration-page
npmrun 1 month ago
parent
commit
4c511e404f
  1. 53
      app/components/register/PasswordStrength.vue
  2. 39
      app/components/register/__tests__/PasswordStrength.test.ts

53
app/components/register/PasswordStrength.vue

@ -0,0 +1,53 @@
<script setup lang="ts">
const props = defineProps<{ password: string }>()
const score = computed(() => {
const p = props.password
if (!p) return 0
let s = 0
const hasLower = /[a-z]/.test(p)
const hasUpper = /[A-Z]/.test(p)
const hasDigit = /\d/.test(p)
const hasSpecial = /[^a-zA-Z\d]/.test(p)
if (p.length >= 8 && hasLower && hasDigit) s = 2
if (p.length >= 10 && hasLower && hasUpper && hasDigit) s = 3
if (p.length >= 12 && hasLower && hasUpper && hasDigit && hasSpecial) s = 4
// Fallback: any input < 8 or single-type = weak
if (s === 0 && p.length > 0) s = 1
return s
})
const label = computed(() => {
const labels = ['', '弱', '中', '强', '很强']
return labels[score.value]
})
const barWidth = computed(() => `${(score.value / 4) * 100}%`)
const barColor = computed(() => {
const colors = ['', 'bg-red-500', 'bg-amber-500', 'bg-green-500', 'bg-emerald-500']
return colors[score.value]
})
</script>
<template>
<div v-if="score > 0" class="mt-1 space-y-1">
<div class="h-1 w-full rounded-full bg-(--ui-bg-accented)">
<div
data-testid="strength-bar"
:data-score="score"
:class="[barColor, 'h-1 rounded-full transition-all duration-300']"
:style="{ width: barWidth }"
/>
</div>
<p class="text-xs" :class="{
'text-red-500': score === 1,
'text-amber-500': score === 2,
'text-green-500': score === 3,
'text-emerald-500': score === 4,
}">{{ label }}</p>
</div>
</template>

39
app/components/register/__tests__/PasswordStrength.test.ts

@ -0,0 +1,39 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import PasswordStrength from '../PasswordStrength.vue'
describe('PasswordStrength', () => {
it('renders nothing for empty password', () => {
const wrapper = mount(PasswordStrength, { props: { password: '' } })
expect(wrapper.find('[data-testid="strength-bar"]').exists()).toBe(false)
})
it('score 1 for weak password (only letters, < 8)', () => {
const wrapper = mount(PasswordStrength, { props: { password: 'abc' } })
expect(wrapper.find('[data-testid="strength-bar"]').attributes('data-score')).toBe('1')
expect(wrapper.text()).toContain('弱')
})
it('score 1 for only digits', () => {
const wrapper = mount(PasswordStrength, { props: { password: '1234567' } })
expect(wrapper.find('[data-testid="strength-bar"]').attributes('data-score')).toBe('1')
})
it('score 2 for letters+digits >= 8 chars', () => {
const wrapper = mount(PasswordStrength, { props: { password: 'abc12345' } })
expect(wrapper.find('[data-testid="strength-bar"]').attributes('data-score')).toBe('2')
expect(wrapper.text()).toContain('中')
})
it('score 3 for upper+lower+digits >= 10 chars', () => {
const wrapper = mount(PasswordStrength, { props: { password: 'Abcdef1234' } })
expect(wrapper.find('[data-testid="strength-bar"]').attributes('data-score')).toBe('3')
expect(wrapper.text()).toContain('强')
})
it('score 4 for upper+lower+digits+special >= 12 chars', () => {
const wrapper = mount(PasswordStrength, { props: { password: 'Abcdef1234!@' } })
expect(wrapper.find('[data-testid="strength-bar"]').attributes('data-score')).toBe('4')
expect(wrapper.text()).toContain('很强')
})
})
Loading…
Cancel
Save