Files
hyprcrd/public/plugins/tests/detail.test.js
T
thandClaude Opus 4.7 f986a41ccc v2.0.4 — re-auth on 401, robust bucket-switch test
- 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>
2026-04-18 12:50:44 +02:00

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