Files
hyprcrd/public/plugins/tests/loader.js
T
thandClaude Opus 4.6 88108ecc1b focus: restore body focus on overlay close, test shield, ESC closes results
Focus restoration:
* editor.js closeModal(): blur activeElement after the close so board
  shortcuts (arrows, bare keys) work immediately without clicking.
* detail.js closeDetail(): same blur, skipped when openedFromSearch
  (search input should keep focus in that case).
* search.js closeSearch(): blur after removing .open.
* calendar.js closeCalendar(): blur after closeDayPopup.
All four check activeElement !== body before blurring to avoid no-ops.

Test shield (loader.js):
* ensureProgressBar() now also creates #test-shield — a transparent
  fixed overlay (z-index one below the progress bar) covering the
  viewport below the bar. Blocks all real mouse interaction while
  tests run; synthetic events from the test suite still work because
  they bypass pointer-events. cursor:not-allowed as visual feedback.
  Removed by hideProgress() alongside the progress bar.

ESC closes test results (runner.js):
* The existing keydown listener now also handles Escape when the
  test-panel is visible. stopPropagation prevents the app's Escape
  chain from firing a second close underneath.

Tests (board.test.js):
* New describe "Board: Focus restoration after overlay close":
  - closing the editor returns focus to body
  - closing the detail view returns focus to body
  - closing search returns focus to body
  - arrow keys work after editor round-trip (enter → enter → escape)
    — the exact flow the user reported as broken. Dispatches
    ArrowDown → Enter (detail) → Enter (editor) → Escape (close) →
    asserts body focus → ArrowDown → asserts .card-focused exists.
* Fixed .not.toBe() → (x !== y).toBe(true) since the custom runner
  doesn't have a .not chain.

chore: bump version to 0.9.14

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

101 lines
4.5 KiB
JavaScript

// ── Tests Plugin: conditional loader ──
//
// The test runner is heavy (40+ test files), so we only load it when:
// - URL contains ?test query param → auto-run
// - Ctrl+Shift+Alt+T pressed → manual open
//
// Tests are completely optional — without this plugin the app has no
// test runner code loaded at all.
const hyprcrd = window.hyprcrd;
// Cache-bust the test modules so edits to test files are picked up without
// a hard refresh. The browser treats each unique URL as a distinct module,
// so a fresh query string on each page load forces re-import.
const V = '?v=' + Date.now();
// Auto-load if ?test in query string
if (new URLSearchParams(location.search).has('test')) {
ensureProgressBar();
import('./tests.js' + V);
}
// Keyboard shortcut: Ctrl+Shift+Alt+T → open runner.
//
// runner.js is imported with the SAME `?v=2` query string the test
// files use (see RUNNER_V below). Same URL → same module instance, so
// suite registration in test files and runAll in the loader run against
// the same `suites` array. Bump RUNNER_V whenever runner.js changes
// in a way that needs cache invalidation across users.
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.shiftKey && e.altKey && (e.key === 'T' || e.key === 't')) {
e.preventDefault();
ensureProgressBar();
import('./tests.js' + V).then(() =>
import('./runner.js' + RUNNER_V).then(m => m.open?.())
);
}
});
// Must match the `?v=…` suffix used in test files' `from './runner.js?v=…'`
// imports — bump both in lock-step to invalidate the runner.js cache.
const RUNNER_V = '?v=3';
function ensureProgressBar() {
if (document.getElementById('test-progress')) return;
// Push the whole document down by 30px AND shrink the body by the
// same amount so the footer doesn't get clipped (body is h-screen +
// overflow-hidden, so without the height shrink the bottom 30px get
// pushed off-viewport). hideProgress() reverts both.
document.body.style.marginTop = '30px';
document.body.style.height = 'calc(100vh - 30px)';
// Transparent overlay that blocks all mouse interaction while tests
// run. Tests dispatch synthetic events so they still work; only real
// user clicks/hovers are absorbed. Removed by hideProgress().
let shield = document.getElementById('test-shield');
if (!shield) {
shield = document.createElement('div');
shield.id = 'test-shield';
const ss = shield.style;
ss.setProperty('position', 'fixed', 'important');
ss.setProperty('inset', '30px 0 0 0', 'important');
ss.setProperty('z-index', '2147483646', 'important');
ss.setProperty('background', 'transparent', 'important');
ss.setProperty('cursor', 'not-allowed', 'important');
document.documentElement.appendChild(shield);
}
const bar = document.createElement('div');
bar.id = 'test-progress';
const s = bar.style;
s.setProperty('position', 'fixed', 'important');
s.setProperty('top', '0', 'important');
s.setProperty('left', '0', 'important');
s.setProperty('right', '0', 'important');
s.setProperty('height', '30px', 'important');
s.setProperty('z-index', '2147483647', 'important');
s.setProperty('background', '#000', 'important');
s.setProperty('color', '#cbd5e1', 'important');
s.setProperty('font-family', 'monospace', 'important');
s.setProperty('font-size', '11px', 'important');
s.setProperty('display', 'flex', 'important');
s.setProperty('align-items', 'center', 'important');
s.setProperty('gap', '10px', 'important');
s.setProperty('padding', '0 12px', 'important');
s.setProperty('border-bottom', '1px solid #334155', 'important');
s.setProperty('pointer-events', 'none', 'important');
bar.innerHTML = `
<span id="test-progress-label" style="white-space:nowrap;color:#38bdf8;font-weight:bold;flex-shrink:0;">tests starting…</span>
<span id="test-progress-name" style="flex:1 1 0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#94a3b8;"></span>
<div style="position:relative;flex:0 0 50%;height:10px;background:#1e293b;border:1px solid #334155;">
<div id="test-progress-fill" style="position:absolute;left:0;top:0;bottom:0;width:0%;background:#38bdf8;transition:width 0.15s linear;"></div>
</div>
`;
document.documentElement.appendChild(bar);
}
hyprcrd?.registerPlugin?.({
name: 'tests',
version: '1.0.0',
description: 'UI + state test runner',
});