Small bookmark icon in the search header bar opens a popup listing localStorage-persisted search strings. Clicking an entry fills the input and runs the search; the x button deletes it; "save current search" at the bottom saves the active input (no dupes). List persists across sessions, no server round-trip. Tests: 6 cases covering button/popup toggle, save + recall, delete, save-current-button, and duplicate rejection. All snapshot/restore localStorage in try/finally so they don't leak into user data or cross-test state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
169 lines
5.6 KiB
JavaScript
169 lines
5.6 KiB
JavaScript
import { describe, it, afterEach, expect, $, click, type, pressKey, sleep, closeAll } from './runner.js?v=4';
|
|
|
|
describe('Search', () => {
|
|
afterEach(() => closeAll());
|
|
|
|
it('opens with Ctrl+K', async () => {
|
|
pressKey('k', { ctrlKey: true });
|
|
await sleep(150);
|
|
expect($('#search')).toBeVisible();
|
|
});
|
|
|
|
it('opens with search button', async () => {
|
|
click('#search-btn');
|
|
await sleep(150);
|
|
expect($('#search')).toBeVisible();
|
|
});
|
|
|
|
it('input is focused on open', async () => {
|
|
click('#search-btn');
|
|
await sleep(150);
|
|
expect(document.activeElement).toBe($('#search-input'));
|
|
});
|
|
|
|
it('has input and results area', async () => {
|
|
click('#search-btn');
|
|
await sleep(100);
|
|
expect($('#search-input')).toExist();
|
|
expect($('#search-results')).toExist();
|
|
});
|
|
|
|
it('typing triggers results render', async () => {
|
|
click('#search-btn');
|
|
await sleep(100);
|
|
type('#search-input', 'a');
|
|
await sleep(250);
|
|
expect($('#search-results')).toExist();
|
|
});
|
|
|
|
it('closes with ESC', async () => {
|
|
click('#search-btn');
|
|
await sleep(100);
|
|
pressKey('Escape');
|
|
await sleep(150);
|
|
expect($('#search')).toBeHidden();
|
|
});
|
|
});
|
|
|
|
describe('Search: saved searches', () => {
|
|
const SAVED_KEY = 'hyprcrd-saved-searches';
|
|
|
|
afterEach(async () => {
|
|
// Close overlays and restore localStorage to avoid cross-test pollution.
|
|
document.querySelector('#search-saved-popup')?.classList.add('hidden');
|
|
await closeAll();
|
|
});
|
|
|
|
it('bookmark button and popup exist', () => {
|
|
expect($('#search-saved-btn')).toExist();
|
|
expect($('#search-saved-popup')).toExist();
|
|
expect($('#search-saved-popup').classList.contains('hidden')).toBe(true);
|
|
});
|
|
|
|
it('clicking bookmark opens the popup', async () => {
|
|
click('#search-btn');
|
|
await sleep(100);
|
|
click('#search-saved-btn');
|
|
await sleep(40);
|
|
expect($('#search-saved-popup').classList.contains('hidden')).toBe(false);
|
|
});
|
|
|
|
it('saves and recalls a search', async () => {
|
|
// Seed localStorage directly to avoid UI coupling for the save path.
|
|
const before = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]');
|
|
const probe = '__test_saved_search_probe__';
|
|
try {
|
|
localStorage.setItem(SAVED_KEY, JSON.stringify([probe, ...before]));
|
|
|
|
click('#search-btn');
|
|
await sleep(100);
|
|
click('#search-saved-btn');
|
|
await sleep(60);
|
|
|
|
const popup = $('#search-saved-popup');
|
|
const rows = popup.querySelectorAll('.search-saved-row');
|
|
expect(rows.length).toBeGreaterThan(0);
|
|
// First row should be our probe.
|
|
expect(rows[0].textContent).toContain(probe);
|
|
|
|
// Click the row → fills input.
|
|
click(rows[0]);
|
|
await sleep(40);
|
|
expect($('#search-input').value).toBe(probe);
|
|
} finally {
|
|
// Restore original saved searches.
|
|
localStorage.setItem(SAVED_KEY, JSON.stringify(before));
|
|
}
|
|
});
|
|
|
|
it('deletes a saved search', async () => {
|
|
const before = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]');
|
|
const probe = '__test_saved_del__';
|
|
try {
|
|
localStorage.setItem(SAVED_KEY, JSON.stringify([probe]));
|
|
|
|
click('#search-btn');
|
|
await sleep(100);
|
|
click('#search-saved-btn');
|
|
await sleep(60);
|
|
|
|
const delBtn = $('#search-saved-popup').querySelector('.search-saved-del');
|
|
expect(delBtn).toExist();
|
|
click(delBtn);
|
|
await sleep(40);
|
|
|
|
const after = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]');
|
|
expect(after.includes(probe)).toBe(false);
|
|
} finally {
|
|
localStorage.setItem(SAVED_KEY, JSON.stringify(before));
|
|
}
|
|
});
|
|
|
|
it('save-current button adds input value', async () => {
|
|
const before = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]');
|
|
const probe = '__test_saved_add_' + Date.now() + '__';
|
|
try {
|
|
click('#search-btn');
|
|
await sleep(100);
|
|
const inp = $('#search-input');
|
|
inp.value = probe;
|
|
inp.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
click('#search-saved-btn');
|
|
await sleep(40);
|
|
click('#search-saved-add');
|
|
await sleep(40);
|
|
|
|
const after = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]');
|
|
expect(after.includes(probe)).toBe(true);
|
|
} finally {
|
|
localStorage.setItem(SAVED_KEY, JSON.stringify(before));
|
|
}
|
|
});
|
|
|
|
it('does not add duplicates', async () => {
|
|
const before = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]');
|
|
const probe = '__test_saved_dupe__';
|
|
try {
|
|
localStorage.setItem(SAVED_KEY, JSON.stringify([probe]));
|
|
|
|
click('#search-btn');
|
|
await sleep(100);
|
|
const inp = $('#search-input');
|
|
inp.value = probe;
|
|
inp.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
click('#search-saved-btn');
|
|
await sleep(40);
|
|
click('#search-saved-add');
|
|
await sleep(40);
|
|
|
|
const after = JSON.parse(localStorage.getItem(SAVED_KEY) || '[]');
|
|
const count = after.filter(q => q === probe).length;
|
|
expect(count).toBe(1);
|
|
} finally {
|
|
localStorage.setItem(SAVED_KEY, JSON.stringify(before));
|
|
}
|
|
});
|
|
});
|