Three follow-ups:
- Accept #shopping as an alias for #einkauf. Either tag now makes
a card show up in the picker; the help blurb in the dialog says
so explicitly.
- Add a proper help-overlay section covering the trigger tags,
how to pick an active list, the phone URL, sync cadence, and
why phone is check-only.
- Add a client-side proxy in public/index.php for /api/shopping
and /api/shopping/check — shopping.js was calling api('api/...')
which went to hyprcrd, but only the relay had handlers. Without
this forwarder, push/pull just 404'd. Proxy mirrors the pattern
used by /api/relay/push.
- New shopping.test.js: asserts the proxy returns a JSON shape
with `ok` (works whether or not a relay is configured), the
hamburger menu toggles as expected, and the picker lists both
#einkauf + #shopping cards while excluding untagged ones.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
86 lines
3.9 KiB
JavaScript
86 lines
3.9 KiB
JavaScript
import { describe, it, expect, $, click, sleep, csrf } from './runner.js?v=4';
|
|
|
|
// Shopping-list tests. The proxy route is always present; without a
|
|
// configured relay it just returns {ok:false,error:"relay not
|
|
// configured"} rather than 404, so we can still assert shape. The UI
|
|
// tests create cards with either trigger tag and verify the picker
|
|
// finds them + skips non-tagged cards.
|
|
|
|
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 an ok field', 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(typeof r.ok).toBe('boolean');
|
|
});
|
|
});
|
|
|
|
describe('Shopping: header menu', () => {
|
|
it('hamburger button + menu exist; clicking toggles visibility', async () => {
|
|
const btn = $('#header-menu-btn');
|
|
const menu = $('#header-menu');
|
|
expect(btn).toExist();
|
|
expect(menu).toExist();
|
|
// Make sure we start closed.
|
|
menu.classList.add('hidden');
|
|
click(btn);
|
|
await sleep(40);
|
|
expect(menu.classList.contains('hidden')).toBe(false);
|
|
expect($('#menu-shopping')).toExist();
|
|
// Outside-click closer — dispatch a click elsewhere.
|
|
document.body.click();
|
|
await sleep(40);
|
|
expect(menu.classList.contains('hidden')).toBe(true);
|
|
});
|
|
});
|
|
|
|
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();
|
|
$('#header-menu')?.classList.add('hidden');
|
|
click('#header-menu-btn');
|
|
await sleep(40);
|
|
click('#menu-shopping');
|
|
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);
|
|
|
|
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();
|
|
}
|
|
});
|
|
});
|