Files
hyprcrd/public/plugins/tests/shopping.test.js
T
thandClaude Opus 4.6 d925070a36 fix(tests): shopping proxy-check test tolerates session-auth 401
/api/shopping/check is session-auth on the relay (phone-only), so
hitting it via the client proxy with client-auth credentials returns
{error: "unauthorized"} instead of a response with an ok field. The
test was over-specific — relax to either `ok:boolean` (relay not
configured, proxy's own fallback) or `error:string` (relay rejected).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 00:43:14 +02:00

91 lines
4.3 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 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 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();
}
});
});