import { describe, it, expect, $, click, sleep, csrf } from './runner.js?v=4'; // Shopping-plugin tests. Everything ships through the shopping plugin // (backend.php registers /api/shopping*, shopping.js injects the // header button and the picker dialog on load). Without a configured // relay the backend still returns {ok:false,error:"relay not // configured"} so we can assert shape. describe('Shopping: plugin registration', () => { it('registers itself in window.hyprcrd.plugins', () => { const entries = window.hyprcrd?.plugins || []; const entry = entries.find(p => p?.name === 'shopping'); expect(entry).toExist(); }); }); describe('Shopping: API proxy', () => { it('GET /api/shopping returns an ok field', async () => { const r = await fetch('api/shopping').then(x => x.json()); expect(typeof r.ok).toBe('boolean'); }); it('POST /api/shopping/check returns a JSON object', async () => { const r = await fetch('api/shopping/check', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf() }, body: JSON.stringify({ line: '- [ ] nope', checked: true }), }).then(x => x.json()); expect(r).toExist(); // The check endpoint is session-auth on the relay (phone-only), // so hitting it via the client proxy returns an `error` field // from the relay's unauthorized branch. Without a configured // relay, it's `ok:false`. Either way it's a JSON object. expect(typeof r.ok === 'boolean' || typeof r.error === 'string').toBe(true); }); }); describe('Shopping: header button', () => { it('Shopping button is injected into the header', () => { const btn = $('#shopping-btn'); expect(btn).toExist(); // Lives inside the main header, not bolted onto the body. expect(btn.closest('#main-header')).toExist(); }); it('clicking opens the picker dialog with save/close controls', async () => { document.querySelector('.shopping-picker-overlay')?.remove(); click('#shopping-btn'); await sleep(60); const overlay = document.querySelector('.shopping-picker-overlay'); expect(overlay).toExist(); expect(overlay.querySelector('.shopping-picker-save')).toExist(); expect(overlay.querySelector('.shopping-picker-cancel')).toExist(); overlay.querySelector('.shopping-picker-cancel').click(); await sleep(40); expect(document.querySelector('.shopping-picker-overlay')).toBe(null); }); }); describe('Shopping: picker tag matching', () => { // Creates one card per trigger tag + one untagged control, then // opens the picker and asserts the list. Both #einkauf and // #shopping must qualify; untagged card must not. it('lists both #einkauf and #shopping cards, excludes others', async () => { const token = csrf(); const h = { 'Content-Type': 'application/json', 'X-CSRF-Token': token }; const hDel = { 'X-CSRF-Token': token }; let eink, shop, other; try { eink = await fetch('api/cards', { method: 'POST', headers: h, body: JSON.stringify({ title: '__test_shop_einkauf__', body: '- [ ] milk #einkauf' }) }).then(r => r.json()); shop = await fetch('api/cards', { method: 'POST', headers: h, body: JSON.stringify({ title: '__test_shop_shopping__', body: '- [ ] bread #shopping' }) }).then(r => r.json()); other = await fetch('api/cards', { method: 'POST', headers: h, body: JSON.stringify({ title: '__test_shop_control__', body: 'plain card' }) }).then(r => r.json()); await window._reloadAll(); // Close any leftover overlay, then open the picker fresh. document.querySelector('.shopping-picker-overlay')?.remove(); click('#shopping-btn'); await sleep(80); const overlay = document.querySelector('.shopping-picker-overlay'); expect(overlay).toExist(); const listText = overlay.querySelector('#shopping-picker-list').textContent; expect(listText).toContain('__test_shop_einkauf__'); expect(listText).toContain('__test_shop_shopping__'); // Non-tagged control must be absent. const controlShown = listText.includes('__test_shop_control__'); expect(controlShown).toBe(false); // Picker always offers a "no active list" option so the // user can stop syncing without leaving the dialog. const radios = overlay.querySelectorAll('input[name="shopping-active"]'); const hasEmpty = [...radios].some(r => r.value === ''); expect(hasEmpty).toBe(true); overlay.querySelector('.shopping-picker-cancel').click(); await sleep(40); } finally { for (const c of [eink, shop, other]) { if (c?.id) await fetch(`api/cards/${c.id}`, { method: 'DELETE', headers: hDel }); } await window._reloadAll(); } }); });