feat(ext/popup): field-row + concealed-row + signature-block helpers
This commit is contained in:
117
extension/src/popup/components/__tests__/fields.test.ts
Normal file
117
extension/src/popup/components/__tests__/fields.test.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
renderRow,
|
||||
renderConcealedRow,
|
||||
renderSignatureBlock,
|
||||
wireFieldHandlers,
|
||||
} from '../fields';
|
||||
|
||||
describe('renderRow', () => {
|
||||
it('plain row contains label + value', () => {
|
||||
const html = renderRow({ label: 'username', value: 'alice' });
|
||||
expect(html).toContain('username');
|
||||
expect(html).toContain('alice');
|
||||
expect(html).toContain('field-row');
|
||||
});
|
||||
|
||||
it('copyable row exposes a copy action', () => {
|
||||
const html = renderRow({ label: 'email', value: 'alice@example.com', copyable: true });
|
||||
expect(html).toContain('data-field-action="copy"');
|
||||
});
|
||||
|
||||
it('href row wraps value in an external anchor', () => {
|
||||
const html = renderRow({ label: 'url', value: 'https://example.com', href: 'https://example.com' });
|
||||
expect(html).toContain('href="https://example.com"');
|
||||
expect(html).toContain('target="_blank"');
|
||||
expect(html).toContain('rel="noopener noreferrer"');
|
||||
});
|
||||
|
||||
it('monospace flag toggles the monospace class', () => {
|
||||
const html = renderRow({ label: 'fingerprint', value: 'AB:CD', monospace: true });
|
||||
expect(html).toContain('monospace');
|
||||
});
|
||||
|
||||
it('multiline value renders inside a <pre>', () => {
|
||||
const html = renderRow({ label: 'address', value: '1 Main\n2 Main', multiline: true });
|
||||
expect(html).toContain('<pre');
|
||||
});
|
||||
|
||||
it('escapes HTML in value and label', () => {
|
||||
const html = renderRow({ label: '<script>x</script>', value: '"&<>' });
|
||||
expect(html).not.toContain('<script>');
|
||||
expect(html).toContain('&');
|
||||
expect(html).toContain('<');
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderConcealedRow', () => {
|
||||
it('initial state hides the value behind a placeholder', () => {
|
||||
const html = renderConcealedRow({ id: 'pw1', label: 'password', value: 'hunter2' });
|
||||
expect(html).toContain('data-field-id="pw1"');
|
||||
expect(html).toContain('data-revealed="false"');
|
||||
expect(html).toContain('••••');
|
||||
// Plaintext is in a data attribute on the row, NOT in the visible textContent.
|
||||
expect(html).not.toMatch(/>hunter2</);
|
||||
});
|
||||
|
||||
it('exposes show + copy actions', () => {
|
||||
const html = renderConcealedRow({ id: 'pw1', label: 'password', value: 'hunter2' });
|
||||
expect(html).toContain('data-field-action="reveal"');
|
||||
expect(html).toContain('data-field-action="copy"');
|
||||
});
|
||||
|
||||
it('multiline concealed shows char count when hidden', () => {
|
||||
const html = renderConcealedRow({ id: 'k1', label: 'key', value: 'abcdefghij', multiline: true });
|
||||
expect(html).toContain('•••• (10 chars)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderSignatureBlock', () => {
|
||||
it('default accent is blue', () => {
|
||||
const html = renderSignatureBlock({ children: '<p>hi</p>' });
|
||||
expect(html).toContain('sig-block--blue');
|
||||
expect(html).toContain('<p>hi</p>');
|
||||
});
|
||||
|
||||
it('honors accent prop', () => {
|
||||
expect(renderSignatureBlock({ accent: 'green', children: '' })).toContain('sig-block--green');
|
||||
expect(renderSignatureBlock({ accent: 'amber', children: '' })).toContain('sig-block--amber');
|
||||
expect(renderSignatureBlock({ accent: 'red', children: '' })).toContain('sig-block--red');
|
||||
});
|
||||
});
|
||||
|
||||
describe('wireFieldHandlers', () => {
|
||||
it('reveal toggle flips data-revealed and swaps placeholder for plaintext', () => {
|
||||
document.body.innerHTML = renderConcealedRow({
|
||||
id: 'pw1',
|
||||
label: 'password',
|
||||
value: 'hunter2',
|
||||
});
|
||||
wireFieldHandlers(document.body);
|
||||
const row = document.querySelector('[data-field-id="pw1"]') as HTMLElement;
|
||||
const revealBtn = row.querySelector('[data-field-action="reveal"]') as HTMLButtonElement;
|
||||
const valueEl = row.querySelector('[data-field-role="value"]') as HTMLElement;
|
||||
expect(row.getAttribute('data-revealed')).toBe('false');
|
||||
expect(valueEl.textContent).toContain('••••');
|
||||
revealBtn.click();
|
||||
expect(row.getAttribute('data-revealed')).toBe('true');
|
||||
expect(valueEl.textContent).toBe('hunter2');
|
||||
});
|
||||
|
||||
it('copy button writes the row value to the clipboard', async () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: { writeText },
|
||||
});
|
||||
document.body.innerHTML = renderRow({
|
||||
label: 'email',
|
||||
value: 'alice@example.com',
|
||||
copyable: true,
|
||||
});
|
||||
wireFieldHandlers(document.body);
|
||||
const copyBtn = document.querySelector('[data-field-action="copy"]') as HTMLButtonElement;
|
||||
copyBtn.click();
|
||||
expect(writeText).toHaveBeenCalledWith('alice@example.com');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user