import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest'
import { clearFixture, getFixture } from '../helpers/fixture'
import Strength from '../../src/strength'
const METER = `
`
describe('Strength', () => {
let fixtureEl: HTMLElement
beforeAll(() => {
fixtureEl = getFixture()
})
beforeEach(() => {
fixtureEl.innerHTML = METER
})
afterEach(() => {
clearFixture()
})
const meter = (): HTMLElement => fixtureEl.querySelector('.strength')!
const input = (): HTMLInputElement => fixtureEl.querySelector('input')!
const text = (): HTMLElement => fixtureEl.querySelector('.strength-text')!
const filled = (): number => fixtureEl.querySelectorAll('.strength-segment.active').length
const type = (value: string): void => {
input().value = value
input().dispatchEvent(new Event('input'))
}
describe('NAME', () => {
it('should return plugin name', () => {
expect(Strength.NAME).toBe('strength')
})
})
describe('constructor', () => {
it('should bind the password field in its own parent and start empty', () => {
const instance = new Strength(meter())
expect(instance._input).toBe(input())
expect(instance.level).toBeNull()
expect(filled()).toBe(0)
expect(text().textContent).toBe('')
})
it('should read an existing value right away', () => {
input().value = 'Sup3rSecret!Pass'
new Strength(meter())
expect(meter().dataset.bsStrength).toBe('strong')
})
it('should take the field from the input option', () => {
fixtureEl.innerHTML = `
`
const instance = new Strength(meter())
expect(instance._input).toBe(fixtureEl.querySelector('#remote'))
})
it('should bind the password field before it, not a confirm field after it', () => {
fixtureEl.innerHTML = `
`
const instance = new Strength(meter())
expect(instance._input).toBe(fixtureEl.querySelector('#pw'))
})
it('should bind the last of several preceding fields, ignoring one after it', () => {
fixtureEl.innerHTML = `
`
const instance = new Strength(meter())
expect(instance._input).toBe(fixtureEl.querySelector('#new'))
})
it('should bind the field after it when none precedes it', () => {
fixtureEl.innerHTML = `
`
const instance = new Strength(meter())
expect(instance._input).toBe(fixtureEl.querySelector('#confirm'))
})
})
describe('levels', () => {
it('should climb from weak to strong as the password improves', () => {
new Strength(meter())
type('abc')
expect(meter().dataset.bsStrength).toBe('weak')
expect(text().textContent).toBe('Weak')
expect(filled()).toBe(1)
type('abcdefghijkl')
expect(meter().dataset.bsStrength).toBe('fair')
expect(filled()).toBe(2)
type('Abcdefgh1!')
expect(meter().dataset.bsStrength).toBe('good')
expect(filled()).toBe(3)
type('Abcdefgh1!?x')
expect(meter().dataset.bsStrength).toBe('strong')
expect(filled()).toBe(4)
})
it('should put the markup text back when the field is emptied', () => {
fixtureEl.innerHTML = METER.replace('', 'At least 8 characters')
new Strength(meter())
expect(text().textContent).toBe('At least 8 characters')
type('abc')
expect(text().textContent).toBe('Weak')
type('')
expect(text().textContent).toBe('At least 8 characters')
})
it('should keep the trigger attribute when the field is emptied', () => {
new Strength(meter())
type('Abcdefgh1!?x')
type('')
expect(meter().dataset.bsStrength).toBe('')
expect(text().textContent).toBe('')
expect(filled()).toBe(0)
})
it('should fire change.bs.strength once per level, without the password', () => {
const spy = vi.fn()
meter().addEventListener('change.bs.strength', spy)
new Strength(meter())
type('abc')
type('abcd')
expect(spy).toHaveBeenCalledTimes(1)
const event = spy.mock.calls[0][0] as CustomEvent & { strength: string; score: number; password?: string }
expect(event.strength).toBe('weak')
expect(event.score).toBe(1)
expect(event.password).toBeUndefined()
})
})
describe('config', () => {
it('should use a custom scorer and messages', () => {
new Strength(meter(), { scorer: () => 9, messages: { weak: 'Słabe', fair: 'Średnie', good: 'Dobre', strong: 'Mocne' } })
type('x')
expect(meter().dataset.bsStrength).toBe('strong')
expect(text().textContent).toBe('Mocne')
})
it('should respect custom thresholds', () => {
new Strength(meter(), { thresholds: [10, 20, 30] })
type('Abcdefgh1!?x')
expect(meter().dataset.bsStrength).toBe('weak')
})
it('should fill a meter with a different number of segments', () => {
fixtureEl.innerHTML = `
`
new Strength(meter())
type('abc')
expect(filled()).toBe(1)
})
})
describe('accessibility', () => {
it('should describe the meter and announce the label politely', () => {
new Strength(meter())
expect(meter().getAttribute('role')).toBe('progressbar')
expect(meter().getAttribute('aria-valuemax')).toBe('4')
expect(meter().getAttribute('aria-label')).toBe('Password strength')
expect(text().getAttribute('aria-live')).toBe('polite')
expect(fixtureEl.querySelector('.strength-segment')!.getAttribute('aria-hidden')).toBe('true')
type('Abcdefgh1!?x')
expect(meter().getAttribute('aria-valuenow')).toBe('4')
expect(meter().getAttribute('aria-valuetext')).toBe('Strong')
type('')
expect(meter().getAttribute('aria-valuenow')).toBe('0')
expect(meter().hasAttribute('aria-valuetext')).toBe(false)
})
it('should keep an aria-label from the markup', () => {
meter().setAttribute('aria-label', 'Siła hasła')
new Strength(meter())
expect(meter().getAttribute('aria-label')).toBe('Siła hasła')
})
})
describe('dispose', () => {
it('should stop listening and remove the instance', () => {
const instance = new Strength(meter())
instance.dispose()
type('Abcdefgh1!?x')
expect(meter().dataset.bsStrength).toBe('')
expect(Strength.getInstance(meter())).toBeNull()
})
})
describe('partial config', () => {
it('merges a partial messages map with the defaults', () => {
new Strength(meter(), { messages: { weak: 'Słabe' } })
type('a')
expect(text().textContent).toBe('Słabe')
type('Abcdefgh1!xyz')
expect(text().textContent).not.toBe('')
expect(text().textContent).not.toBe('Słabe')
})
it('merges a partial weights map with the defaults', () => {
const instance = new Strength(meter(), { weights: { special: 3 } })
expect(instance._config.weights.lowercase).toBe(1)
expect(instance._config.weights.special).toBe(3)
})
})
})