- auth: when a background poll or save returns 401 (session expired while the UI is still open), the api() helpers now prompt for the password instead of letting the save fail silently. Concurrent 401s collapse onto a single dialog via a shared reauthPromise, and each caller retries its original request once after login. /api/auth/* is excluded from the retry path. Extracts promptLogin() in app.js and exposes it as window._reauth so both the core util.js api() and the plugin-SDK hyprcrd.js api() can share the flow. - tests: the "open-switches-bucket" detail test assumed buckets[0] was a plain top-level bucket, but the bar renders super-bucket parents with data-bucket-super and hides child buckets entirely, so the .bucket-tab.active[data-bucket] selector found nothing on setups with nested layouts. Test now picks two plain top-level buckets or skips. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
161 lines
6.1 KiB
JavaScript
161 lines
6.1 KiB
JavaScript
import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll, csrf } from './runner.js?v=4';
|
|
|
|
describe('Detail View', () => {
|
|
afterEach(() => closeAll());
|
|
|
|
it('has structural elements', () => {
|
|
for (const id of ['detail', 'detail-title', 'detail-body', 'detail-meta', 'detail-tags']) {
|
|
expect($(`#${id}`)).toExist();
|
|
}
|
|
});
|
|
|
|
it('has control buttons', () => {
|
|
for (const id of ['detail-edit', 'detail-delete', 'detail-done', 'detail-zen', 'detail-close']) {
|
|
expect($(`#${id}`)).toExist();
|
|
}
|
|
});
|
|
|
|
it('opens on card click (non-split)', async () => {
|
|
if (!$('#board-preview')?.classList.contains('hidden')) {
|
|
click('#board-split-toggle');
|
|
await sleep(100);
|
|
}
|
|
const card = $('#board [data-id]');
|
|
if (!card) return;
|
|
click(card);
|
|
await sleep(300);
|
|
expect($('#detail')).toBeVisible();
|
|
});
|
|
|
|
it('shows card title', async () => {
|
|
if (!$('#board-preview')?.classList.contains('hidden')) {
|
|
click('#board-split-toggle');
|
|
await sleep(100);
|
|
}
|
|
const card = $('#board [data-id]');
|
|
if (!card) return;
|
|
click(card);
|
|
await sleep(300);
|
|
expect($('#detail-title').textContent.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('closes on ESC', async () => {
|
|
if (!$('#board-preview')?.classList.contains('hidden')) {
|
|
click('#board-split-toggle');
|
|
await sleep(100);
|
|
}
|
|
const card = $('#board [data-id]');
|
|
if (!card) return;
|
|
click(card);
|
|
await sleep(300);
|
|
if (!$('#detail')?.classList.contains('open')) return;
|
|
pressKey('Escape');
|
|
for (let i = 0; i < 20; i++) {
|
|
if (!$('#detail')?.classList.contains('open')) break;
|
|
await sleep(50);
|
|
}
|
|
expect($('#detail')).toBeHidden();
|
|
});
|
|
|
|
it('closes on close button', async () => {
|
|
if (!$('#board-preview')?.classList.contains('hidden')) {
|
|
click('#board-split-toggle');
|
|
await sleep(100);
|
|
}
|
|
const card = $('#board [data-id]');
|
|
if (!card) return;
|
|
click(card);
|
|
await sleep(300);
|
|
click('#detail-close');
|
|
await sleep(150);
|
|
expect($('#detail')).toBeHidden();
|
|
});
|
|
});
|
|
|
|
describe('Detail: open-switches-bucket', () => {
|
|
const SETTING_KEY = 'hyprcrd-open-switches-bucket';
|
|
const BUCKET_KEY = 'hyprcrd-active-bucket';
|
|
|
|
afterEach(async () => {
|
|
await closeAll();
|
|
// Restore runner baseline.
|
|
window.state.activeBucket = 'all';
|
|
localStorage.setItem(BUCKET_KEY, 'all');
|
|
});
|
|
|
|
it('switches to the card bucket when the setting is on', async () => {
|
|
const buckets = await fetch('api/buckets').then(r => r.json());
|
|
if (!Array.isArray(buckets) || !buckets.length) return;
|
|
// The bucket bar renders top-level non-super buckets with
|
|
// `data-bucket=<id>`, super-bucket parents with `data-bucket-super`,
|
|
// and hides child buckets behind the parent's dropdown. To keep
|
|
// this test robust across different user bucket layouts, pick two
|
|
// plain top-level buckets (no children, no parent). Skip when the
|
|
// fixture doesn't give us two.
|
|
const childParentIds = new Set(buckets.filter(x => x.parent_id).map(x => x.parent_id));
|
|
const plainTop = buckets.filter(x => !x.parent_id && !childParentIds.has(x.id));
|
|
if (plainTop.length < 2) return;
|
|
const b = plainTop[0];
|
|
const other = plainTop[1];
|
|
const token = csrf();
|
|
const h = { 'Content-Type': 'application/json', 'X-CSRF-Token': token };
|
|
let card;
|
|
const prev = localStorage.getItem(SETTING_KEY);
|
|
try {
|
|
card = await fetch('api/cards', { method: 'POST', headers: h,
|
|
body: JSON.stringify({ title: '__test_switch_bucket__', body: '', bucket_id: b.id })
|
|
}).then(r => r.json());
|
|
await window._reloadAll();
|
|
await sleep(100);
|
|
|
|
// Start on a DIFFERENT bucket so the switch is visible.
|
|
window.state.activeBucket = String(other.id);
|
|
|
|
// Enable the setting.
|
|
localStorage.setItem(SETTING_KEY, '1');
|
|
|
|
window._openDetail(card.id);
|
|
await sleep(150);
|
|
|
|
expect(String(window.state.activeBucket)).toBe(String(b.id));
|
|
// Bucket bar must highlight the switched-to bucket.
|
|
const activeTab = document.querySelector('#bucket-bar .bucket-tab.active[data-bucket]');
|
|
expect(activeTab).toExist();
|
|
expect(activeTab.dataset.bucket).toBe(String(b.id));
|
|
} finally {
|
|
localStorage.setItem(SETTING_KEY, prev ?? '0');
|
|
if (card?.id) await fetch(`api/cards/${card.id}`, { method: 'DELETE', headers: { 'X-CSRF-Token': token } });
|
|
await window._reloadAll();
|
|
}
|
|
});
|
|
|
|
it('does NOT switch when the setting is off', async () => {
|
|
const buckets = await fetch('api/buckets').then(r => r.json());
|
|
if (!Array.isArray(buckets) || buckets.length < 2) return;
|
|
const token = csrf();
|
|
const h = { 'Content-Type': 'application/json', 'X-CSRF-Token': token };
|
|
let card;
|
|
const prev = localStorage.getItem(SETTING_KEY);
|
|
try {
|
|
card = await fetch('api/cards', { method: 'POST', headers: h,
|
|
body: JSON.stringify({ title: '__test_no_switch__', body: '', bucket_id: buckets[0].id })
|
|
}).then(r => r.json());
|
|
await window._reloadAll();
|
|
await sleep(100);
|
|
|
|
window.state.activeBucket = String(buckets[1].id);
|
|
localStorage.setItem(SETTING_KEY, '0');
|
|
|
|
window._openDetail(card.id);
|
|
await sleep(100);
|
|
|
|
// Should stay on the other bucket.
|
|
expect(String(window.state.activeBucket)).toBe(String(buckets[1].id));
|
|
} finally {
|
|
localStorage.setItem(SETTING_KEY, prev ?? '0');
|
|
if (card?.id) await fetch(`api/cards/${card.id}`, { method: 'DELETE', headers: { 'X-CSRF-Token': token } });
|
|
await window._reloadAll();
|
|
}
|
|
});
|
|
});
|