2 changed files with 92 additions and 0 deletions
@ -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> |
||||
@ -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…
Reference in new issue