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) }) }) })