From c8835825e7050460dbf4190e566eeadbeba11608 Mon Sep 17 00:00:00 2001 From: th Date: Sun, 12 Apr 2026 10:05:38 +0200 Subject: [PATCH] test fixes: dialog selector, closeAll scope, copy filter, runner ?v=3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * card-interactions.test.js: group delete tests used `.dialog-ok` (class selector) but the button has `id="dialog-ok"`. querySelector returned null, the click was skipped, the dialog stayed open and blocked every downstream test. Fixed to getElementById. * runner.js closeAll(): added #dialog, #bucket-super-popover (remove), and #gitea-overlay to the cleanup list. A failed test that leaves any of these open no longer poisons the rest of the suite. * runner.js copy handler: when there are failures or flaky tests, the "copy" button now builds the clipboard text from lastResults — only failed + flaky lines (suite name, test name, timing, error, original error for retried). When everything passed, copies the full output as before. lastResults is set in renderResults(). * bucket-context.test.js: move-left test used `[data-bucket]` to find user bucket tabs, which misses super-bucket tiles (they carry `data-bucket-super`). Changed to `[data-bucket-drag]` which is on both regular and super-bucket tiles but not on fixed filters. * context-menu.test.js: make-ticket pre-fill assertion now polls up to 2s for the title input to be populated (async openCreateForm repo-fetch was racing against the immediate assertion). * card-join.test.js: clickJoin sleep bumped from 400ms → 800ms. joinSelectedCards does 3+ serial API calls (fetch bodies, PATCH winner, N×DELETE losers, _reloadAll) which sometimes exceeded the old 400ms window on slow runs. * Runner cache: RUNNER_V bumped from ?v=2 → ?v=3 in loader.js + all 42 test files so the browser picks up the new runner.js content (copy handler, closeAll, retry progress bar) without a hard reload. super-buckets: remember last selection per group * buckets.js: module-scope lastSuperSelection Map tracks the last active member per super-bucket parent. Updated in renderBar() whenever activeBucket matches a group member. tileFor() uses it as a fallback when the user is on a different bucket — the tile shows the last-picked child's name and color instead of reverting to the parent. The dropdown popover also highlights the last selection when no member is currently active. chore: bump version to 0.9.11 Co-Authored-By: Claude Opus 4.6 (1M context) --- config.php | 2 +- public/js/modules/buckets.js | 33 +++++++++++++++-- public/plugins/tests/api-edge.test.js | 2 +- public/plugins/tests/api.test.js | 2 +- public/plugins/tests/board.test.js | 2 +- public/plugins/tests/bucket-context.test.js | 8 +++-- public/plugins/tests/buckets.test.js | 2 +- public/plugins/tests/calendar-popup.test.js | 2 +- public/plugins/tests/calendar.test.js | 2 +- public/plugins/tests/canvas.test.js | 2 +- .../plugins/tests/card-interactions.test.js | 6 ++-- public/plugins/tests/card-join.test.js | 7 ++-- public/plugins/tests/context-menu.test.js | 10 ++++-- public/plugins/tests/datepicker.test.js | 2 +- .../plugins/tests/detail-interactions.test.js | 2 +- public/plugins/tests/detail.test.js | 2 +- public/plugins/tests/editor-features.test.js | 2 +- public/plugins/tests/editor-save.test.js | 2 +- public/plugins/tests/editor-toolbar.test.js | 2 +- public/plugins/tests/editor.test.js | 2 +- public/plugins/tests/export.test.js | 2 +- public/plugins/tests/favorites.test.js | 2 +- public/plugins/tests/filtering.test.js | 2 +- public/plugins/tests/gallery.test.js | 2 +- public/plugins/tests/help.test.js | 2 +- public/plugins/tests/image-fullscreen.test.js | 2 +- public/plugins/tests/line-numbers.test.js | 2 +- public/plugins/tests/loader.js | 2 +- public/plugins/tests/markdown-edge.test.js | 2 +- public/plugins/tests/markdown.test.js | 2 +- public/plugins/tests/multiselect.test.js | 2 +- public/plugins/tests/notifications.test.js | 2 +- public/plugins/tests/quick-capture.test.js | 2 +- public/plugins/tests/relay.test.js | 2 +- public/plugins/tests/rrule.test.js | 2 +- public/plugins/tests/runner.js | 36 +++++++++++++++++-- public/plugins/tests/search-logic.test.js | 2 +- public/plugins/tests/search.test.js | 2 +- public/plugins/tests/settings.test.js | 2 +- public/plugins/tests/shell.test.js | 2 +- public/plugins/tests/snippets.test.js | 2 +- public/plugins/tests/sorting.test.js | 2 +- public/plugins/tests/state-functions.test.js | 2 +- public/plugins/tests/super-buckets.test.js | 2 +- public/plugins/tests/theme.test.js | 2 +- public/plugins/tests/toolbar.test.js | 2 +- public/plugins/tests/undo.test.js | 2 +- 47 files changed, 126 insertions(+), 56 deletions(-) diff --git a/config.php b/config.php index c9613fc..79377c1 100644 --- a/config.php +++ b/config.php @@ -1,7 +1,7 @@ `bucket-tab ${active ? 'active' : ''} flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors shrink-0`; @@ -253,14 +259,31 @@ function renderBar() { .filter(b => b.parent_id == parentId) .sort((a, b) => (a.position || 0) - (b.position || 0)); - // Resolve which child / parent to *show* on a super-bucket tile. The - // active bucket wins; otherwise fall back to the parent itself. + // Track which super-bucket member is active NOW so we can recall it + // later when the user comes back from a different bucket. + for (const b of topLevel) { + const kids = childrenOf(b.id); + if (!kids.length) continue; + if (state.activeBucket == b.id) lastSuperSelection.set(b.id, b.id); + const activeKid = kids.find(c => c.id == state.activeBucket); + if (activeKid) lastSuperSelection.set(b.id, activeKid.id); + } + + // Resolve which child / parent to *show* on a super-bucket tile. + // Priority: currently active member → last selected member → parent. const tileFor = (parent) => { const kids = childrenOf(parent.id); if (!kids.length) return null; // not a super-bucket const activeChild = kids.find(c => c.id == state.activeBucket); if (activeChild) return activeChild; if (state.activeBucket == parent.id) return parent; + // Fall back to the last selection rather than the parent + const lastId = lastSuperSelection.get(parent.id); + if (lastId) { + if (lastId == parent.id) return parent; + const lastKid = kids.find(c => c.id == lastId); + if (lastKid) return lastKid; + } return parent; }; @@ -369,9 +392,13 @@ function renderBar() { pop.style.top = (rect.bottom + 4) + 'px'; pop.style.left = rect.left + 'px'; + // Highlight the currently active member OR the last remembered + // selection (so the user sees which child was picked last time + // even when they're currently on a different bucket). + const lastId = lastSuperSelection.get(parentId); const renderEntry = (m) => { const c = m.color || '#64748b'; - const active = state.activeBucket == m.id; + const active = state.activeBucket == m.id || (!members.some(x => x.id == state.activeBucket) && lastId == m.id); const isParent = m.id === parentId; return ``; }; diff --git a/public/plugins/tests/api-edge.test.js b/public/plugins/tests/api-edge.test.js index 4f351fd..eea0733 100644 --- a/public/plugins/tests/api-edge.test.js +++ b/public/plugins/tests/api-edge.test.js @@ -1,4 +1,4 @@ -import { describe, it, expect, csrf } from './runner.js?v=2'; +import { describe, it, expect, csrf } from './runner.js?v=3'; describe('API: Edge Cases', () => { it('create card with special characters in title', async () => { diff --git a/public/plugins/tests/api.test.js b/public/plugins/tests/api.test.js index 95958ff..00814cd 100644 --- a/public/plugins/tests/api.test.js +++ b/public/plugins/tests/api.test.js @@ -1,4 +1,4 @@ -import { describe, it, expect, csrf } from './runner.js?v=2'; +import { describe, it, expect, csrf } from './runner.js?v=3'; describe('API: Cards CRUD', () => { it('GET /api/cards returns array', async () => { diff --git a/public/plugins/tests/board.test.js b/public/plugins/tests/board.test.js index 449578e..b420aef 100644 --- a/public/plugins/tests/board.test.js +++ b/public/plugins/tests/board.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=3'; describe('Board Rendering', () => { it('renders card elements', () => { diff --git a/public/plugins/tests/bucket-context.test.js b/public/plugins/tests/bucket-context.test.js index 353c912..aa3033c 100644 --- a/public/plugins/tests/bucket-context.test.js +++ b/public/plugins/tests/bucket-context.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=3'; describe('Bucket Context Menu', () => { afterEach(async () => { @@ -100,7 +100,11 @@ describe('Bucket Context Menu', () => { it('move-left disabled on first bucket', async () => { await window._reloadAll?.(); await sleep(200); - const tabs = $$('#bucket-bar [data-bucket]').filter(t => !['all', 'current', 'favorites', 'journal', 'none'].includes(t.dataset.bucket)); + // data-bucket-drag is present on BOTH regular and super-bucket + // tiles but not on the fixed filters (all/current/favorites/…). + // Using it instead of [data-bucket] avoids missing super-bucket + // tiles which carry data-bucket-super instead. + const tabs = $$('#bucket-bar [data-bucket-drag]'); if (!tabs.length) return; tabs[0].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, clientX: 100, clientY: 50 })); await sleep(150); diff --git a/public/plugins/tests/buckets.test.js b/public/plugins/tests/buckets.test.js index d035066..a613619 100644 --- a/public/plugins/tests/buckets.test.js +++ b/public/plugins/tests/buckets.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=3'; import { state } from '../../js/modules/state.js'; describe('Bucket Bar', () => { diff --git a/public/plugins/tests/calendar-popup.test.js b/public/plugins/tests/calendar-popup.test.js index dbc3324..5b0163c 100644 --- a/public/plugins/tests/calendar-popup.test.js +++ b/public/plugins/tests/calendar-popup.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll } from './runner.js?v=3'; describe('Calendar: Day Click', () => { afterEach(() => closeAll()); diff --git a/public/plugins/tests/calendar.test.js b/public/plugins/tests/calendar.test.js index eca1234..7a83731 100644 --- a/public/plugins/tests/calendar.test.js +++ b/public/plugins/tests/calendar.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=3'; describe('Calendar', () => { afterEach(() => closeAll()); diff --git a/public/plugins/tests/canvas.test.js b/public/plugins/tests/canvas.test.js index 995b546..3c8d441 100644 --- a/public/plugins/tests/canvas.test.js +++ b/public/plugins/tests/canvas.test.js @@ -1,4 +1,4 @@ -import { describe, it, beforeEach, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=2'; +import { describe, it, beforeEach, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=3'; // ── Helpers ── diff --git a/public/plugins/tests/card-interactions.test.js b/public/plugins/tests/card-interactions.test.js index 4795f47..80262a8 100644 --- a/public/plugins/tests/card-interactions.test.js +++ b/public/plugins/tests/card-interactions.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, $$, click, rightClick, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, $$, click, rightClick, sleep, closeAll, csrf } from './runner.js?v=3'; import { state } from '../../js/modules/state.js'; describe('Card: Tag Operations', () => { @@ -320,7 +320,7 @@ describe('Card: Group Delete', () => { expect(msg.textContent).toContain('__test_grpdel_child1__'); expect(msg.textContent).toContain('__test_grpdel_child2__'); // Confirm the delete - const okBtn = dialog.querySelector('.dialog-ok'); + const okBtn = document.getElementById('dialog-ok'); if (okBtn) click(okBtn); await sleep(500); } @@ -364,7 +364,7 @@ describe('Card: Group Delete', () => { await sleep(200); const dialog = document.getElementById('dialog'); if (dialog?.classList.contains('open')) { - const okBtn = dialog.querySelector('.dialog-ok'); + const okBtn = document.getElementById('dialog-ok'); if (okBtn) click(okBtn); await sleep(500); } diff --git a/public/plugins/tests/card-join.test.js b/public/plugins/tests/card-join.test.js index 76bf0fd..7f0fc45 100644 --- a/public/plugins/tests/card-join.test.js +++ b/public/plugins/tests/card-join.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, $$, click, rightClick, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, $$, click, rightClick, sleep, closeAll, csrf } from './runner.js?v=3'; import { state } from '../../js/modules/state.js'; async function makeCard(title, body = '') { @@ -26,7 +26,10 @@ async function clickJoin(rightClickTargetEl) { const joinBtn = $$('#ctx-items [data-action="join"]')[0]; if (!joinBtn) return false; click(joinBtn); - await sleep(400); + // joinSelectedCards does: fetch full bodies → PATCH winner → N × DELETE + // losers → _reloadAll. That's 3+ serial API calls and a full board + // re-render, so 400ms was sometimes too tight on slower runs. + await sleep(800); return true; } diff --git a/public/plugins/tests/context-menu.test.js b/public/plugins/tests/context-menu.test.js index f0e2d5f..9047a68 100644 --- a/public/plugins/tests/context-menu.test.js +++ b/public/plugins/tests/context-menu.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, $$, click, rightClick, sleep, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, $$, click, rightClick, sleep, csrf } from './runner.js?v=3'; describe('Context Menu', () => { afterEach(async () => { @@ -203,9 +203,15 @@ describe('Card: Make ticket (gitea)', () => { expect(form && !form.classList.contains('hidden')).toBe(true); // The form must NOT auto-submit — the title and body should be - // populated and the form still visible afterwards. + // populated and the form still visible afterwards. The pre-fill + // happens after an async openCreateForm, so poll until the title + // input has our expected value or we time out. const titleInput = document.getElementById('gitea-create-title'); const bodyInput = document.getElementById('gitea-create-body'); + for (let i = 0; i < 40; i++) { + if (titleInput?.value === title) break; + await sleep(50); + } expect(titleInput?.value).toBe(title); expect(bodyInput?.value).toBe(body); // Issues list / detail view should NOT be the foreground diff --git a/public/plugins/tests/datepicker.test.js b/public/plugins/tests/datepicker.test.js index ee8031e..950dbc5 100644 --- a/public/plugins/tests/datepicker.test.js +++ b/public/plugins/tests/datepicker.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll } from './runner.js?v=3'; import { showDialog } from '../../js/modules/dialog.js'; describe('Datepicker: Rendering', () => { diff --git a/public/plugins/tests/detail-interactions.test.js b/public/plugins/tests/detail-interactions.test.js index 7e22f2b..adc537d 100644 --- a/public/plugins/tests/detail-interactions.test.js +++ b/public/plugins/tests/detail-interactions.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll, csrf } from './runner.js?v=3'; describe('Detail: Done Toggle', () => { afterEach(() => closeAll()); diff --git a/public/plugins/tests/detail.test.js b/public/plugins/tests/detail.test.js index 44773af..f706d6e 100644 --- a/public/plugins/tests/detail.test.js +++ b/public/plugins/tests/detail.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll } from './runner.js?v=3'; describe('Detail View', () => { afterEach(() => closeAll()); diff --git a/public/plugins/tests/editor-features.test.js b/public/plugins/tests/editor-features.test.js index 2c6ac62..1659b90 100644 --- a/public/plugins/tests/editor-features.test.js +++ b/public/plugins/tests/editor-features.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, click, type, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, click, type, sleep, closeAll, csrf } from './runner.js?v=3'; describe('Editor: Color Picker', () => { afterEach(() => closeAll()); diff --git a/public/plugins/tests/editor-save.test.js b/public/plugins/tests/editor-save.test.js index 8fbd8e6..6442551 100644 --- a/public/plugins/tests/editor-save.test.js +++ b/public/plugins/tests/editor-save.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, click, type, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, click, type, sleep, closeAll, csrf } from './runner.js?v=3'; describe('Editor: Empty Title Prevention', () => { afterEach(() => closeAll()); diff --git a/public/plugins/tests/editor-toolbar.test.js b/public/plugins/tests/editor-toolbar.test.js index d01982e..2dd2d67 100644 --- a/public/plugins/tests/editor-toolbar.test.js +++ b/public/plugins/tests/editor-toolbar.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=3'; // Helper: set selection range in textarea function selectRange(textarea, start, end) { diff --git a/public/plugins/tests/editor.test.js b/public/plugins/tests/editor.test.js index aed39f6..852f2a8 100644 --- a/public/plugins/tests/editor.test.js +++ b/public/plugins/tests/editor.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll, csrf } from './runner.js?v=3'; describe('Editor: Structure', () => { it('has all elements', () => { diff --git a/public/plugins/tests/export.test.js b/public/plugins/tests/export.test.js index 8f11cfe..235f3ee 100644 --- a/public/plugins/tests/export.test.js +++ b/public/plugins/tests/export.test.js @@ -1,4 +1,4 @@ -import { describe, it, expect, csrf } from './runner.js?v=2'; +import { describe, it, expect, csrf } from './runner.js?v=3'; describe('Export: JSON ZIP', () => { it('export endpoint returns ZIP', async () => { diff --git a/public/plugins/tests/favorites.test.js b/public/plugins/tests/favorites.test.js index 2686c57..0db0a21 100644 --- a/public/plugins/tests/favorites.test.js +++ b/public/plugins/tests/favorites.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=3'; import { state } from '../../js/modules/state.js'; describe('Favorites Bucket', () => { diff --git a/public/plugins/tests/filtering.test.js b/public/plugins/tests/filtering.test.js index fd7fbc8..95416a1 100644 --- a/public/plugins/tests/filtering.test.js +++ b/public/plugins/tests/filtering.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=3'; import { state, parseTags, sortedCards } from '../../js/modules/state.js'; describe('Filtering: Tag AND Logic', () => { diff --git a/public/plugins/tests/gallery.test.js b/public/plugins/tests/gallery.test.js index e25fbe4..c256782 100644 --- a/public/plugins/tests/gallery.test.js +++ b/public/plugins/tests/gallery.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=3'; describe('Gallery', () => { afterEach(() => closeAll()); diff --git a/public/plugins/tests/help.test.js b/public/plugins/tests/help.test.js index 605e7e7..f55a990 100644 --- a/public/plugins/tests/help.test.js +++ b/public/plugins/tests/help.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, $$, click, pressKey, sleep, closeAll } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, $$, click, pressKey, sleep, closeAll } from './runner.js?v=3'; describe('Help', () => { afterEach(() => closeAll()); diff --git a/public/plugins/tests/image-fullscreen.test.js b/public/plugins/tests/image-fullscreen.test.js index 3d5f57a..87fa08d 100644 --- a/public/plugins/tests/image-fullscreen.test.js +++ b/public/plugins/tests/image-fullscreen.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll, csrf } from './runner.js?v=3'; describe('Image Fullscreen Viewer', () => { afterEach(async () => { diff --git a/public/plugins/tests/line-numbers.test.js b/public/plugins/tests/line-numbers.test.js index aa49c25..73e147d 100644 --- a/public/plugins/tests/line-numbers.test.js +++ b/public/plugins/tests/line-numbers.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=3'; function enableLines() { const gutter = $('#editor-gutter'); diff --git a/public/plugins/tests/loader.js b/public/plugins/tests/loader.js index 94c0977..6431fe6 100644 --- a/public/plugins/tests/loader.js +++ b/public/plugins/tests/loader.js @@ -39,7 +39,7 @@ document.addEventListener('keydown', e => { // 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=2'; +const RUNNER_V = '?v=3'; function ensureProgressBar() { if (document.getElementById('test-progress')) return; diff --git a/public/plugins/tests/markdown-edge.test.js b/public/plugins/tests/markdown-edge.test.js index fabead5..6fefd74 100644 --- a/public/plugins/tests/markdown-edge.test.js +++ b/public/plugins/tests/markdown-edge.test.js @@ -1,4 +1,4 @@ -import { describe, it, expect } from './runner.js?v=2'; +import { describe, it, expect } from './runner.js?v=3'; import { renderMd } from '../../js/modules/markdown.js'; describe('Markdown: Blank Lines', () => { diff --git a/public/plugins/tests/markdown.test.js b/public/plugins/tests/markdown.test.js index 5e46bce..e3fc8a8 100644 --- a/public/plugins/tests/markdown.test.js +++ b/public/plugins/tests/markdown.test.js @@ -1,4 +1,4 @@ -import { describe, it, expect } from './runner.js?v=2'; +import { describe, it, expect } from './runner.js?v=3'; import { renderMd, formatTodoItems, stripMd } from '../../js/modules/markdown.js'; import { state } from '../../js/modules/state.js'; diff --git a/public/plugins/tests/multiselect.test.js b/public/plugins/tests/multiselect.test.js index 627b2df..da9fd81 100644 --- a/public/plugins/tests/multiselect.test.js +++ b/public/plugins/tests/multiselect.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, sleep, closeAll } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, sleep, closeAll } from './runner.js?v=3'; import { state } from '../../js/modules/state.js'; describe('Multi-Select', () => { diff --git a/public/plugins/tests/notifications.test.js b/public/plugins/tests/notifications.test.js index 7bca614..060681d 100644 --- a/public/plugins/tests/notifications.test.js +++ b/public/plugins/tests/notifications.test.js @@ -1,4 +1,4 @@ -import { describe, it, expect, $ } from './runner.js?v=2'; +import { describe, it, expect, $ } from './runner.js?v=3'; describe('Notification Banner', () => { it('banner element exists', () => { diff --git a/public/plugins/tests/quick-capture.test.js b/public/plugins/tests/quick-capture.test.js index 5c9fcca..4a1177f 100644 --- a/public/plugins/tests/quick-capture.test.js +++ b/public/plugins/tests/quick-capture.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, type, pressKey, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, type, pressKey, sleep, closeAll, csrf } from './runner.js?v=3'; describe('Quick Capture: Open/Close', () => { afterEach(async () => { diff --git a/public/plugins/tests/relay.test.js b/public/plugins/tests/relay.test.js index 5889e9c..6a963e0 100644 --- a/public/plugins/tests/relay.test.js +++ b/public/plugins/tests/relay.test.js @@ -1,4 +1,4 @@ -import { describe, it, expect, csrf } from './runner.js?v=2'; +import { describe, it, expect, csrf } from './runner.js?v=3'; describe('Relay: Push API', () => { it('push without config returns error', async () => { diff --git a/public/plugins/tests/rrule.test.js b/public/plugins/tests/rrule.test.js index 1f57297..33c4cd0 100644 --- a/public/plugins/tests/rrule.test.js +++ b/public/plugins/tests/rrule.test.js @@ -1,4 +1,4 @@ -import { describe, it, expect } from './runner.js?v=2'; +import { describe, it, expect } from './runner.js?v=3'; import { state } from '../../js/modules/state.js'; // RRULE is set by CalDAV sync, not via PATCH. Test the data model and existing cards. diff --git a/public/plugins/tests/runner.js b/public/plugins/tests/runner.js index f6aef5d..0c97f48 100644 --- a/public/plugins/tests/runner.js +++ b/public/plugins/tests/runner.js @@ -171,7 +171,7 @@ export function csrf() { } export async function closeAll() { - for (const id of ['settings-overlay', 'help', 'search', 'calendar', 'gallery', 'detail', 'modal', 'tasks']) { + for (const id of ['settings-overlay', 'help', 'search', 'calendar', 'gallery', 'detail', 'modal', 'tasks', 'dialog']) { const el = document.getElementById(id); if (el?.classList.contains('open')) el.classList.remove('open'); } @@ -179,6 +179,8 @@ export async function closeAll() { document.querySelector('#select-bar')?.classList.add('hidden'); document.querySelector('.bucket-picker')?.remove(); document.querySelector('.orphan-lightbox')?.remove(); + document.getElementById('bucket-super-popover')?.remove(); + document.getElementById('gitea-overlay')?.classList.remove('open'); // Clear multi-select document.querySelectorAll('.card-selected').forEach(el => { el.classList.remove('card-selected'); @@ -289,6 +291,7 @@ async function runAll(only = null) { // ── UI Panel ── let panel = null; +let lastResults = null; function createPanel() { if (panel) { panel.remove(); panel = null; } @@ -309,9 +312,35 @@ function createPanel() { document.body.appendChild(panel); panel.querySelector('#test-close-btn').addEventListener('click', close); panel.querySelector('#test-run-btn').addEventListener('click', run); + // Copy: when failures exist, only copy the failed + flaky tests so + // the user can paste the relevant part without wading through 200+ + // lines of green. When everything passed, copy the full output. panel.querySelector('#test-copy-btn').addEventListener('click', () => { - const output = panel.querySelector('#test-output'); - const text = output.innerText; + let text; + if (lastResults) { + const hasFailures = lastResults.some(s => s.tests.some(t => t.status === 'fail' || t.retried)); + if (hasFailures) { + const lines = []; + for (const s of lastResults) { + const relevant = s.tests.filter(t => t.status === 'fail' || t.retried); + if (!relevant.length) continue; + lines.push(s.name); + for (const t of relevant) { + const icon = t.status === 'fail' ? '✗' : '⚠'; + const note = t.retried ? (t.status === 'pass' ? ' (flaky — passed on retry)' : ' (failed twice)') : ''; + lines.push(`${icon} ${t.name} ${t.time}ms${note}`); + if (t.error) lines.push(` ${t.error}`); + if (t.originalError) lines.push(` first run: ${t.originalError}`); + } + lines.push(''); + } + text = lines.join('\n').trim(); + } else { + text = panel.querySelector('#test-output').innerText; + } + } else { + text = panel.querySelector('#test-output').innerText; + } navigator.clipboard.writeText(text).then(() => { const btn = panel.querySelector('#test-copy-btn'); btn.textContent = 'copied'; @@ -322,6 +351,7 @@ function createPanel() { } function renderResults(results) { + lastResults = results; const output = panel.querySelector('#test-output'); const summary = panel.querySelector('#test-summary'); let total = 0, passed = 0, failed = 0, flaky = 0, totalTime = 0; diff --git a/public/plugins/tests/search-logic.test.js b/public/plugins/tests/search-logic.test.js index d995d4c..d5db558 100644 --- a/public/plugins/tests/search-logic.test.js +++ b/public/plugins/tests/search-logic.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, click, type, pressKey, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, click, type, pressKey, sleep, closeAll, csrf } from './runner.js?v=3'; describe('Search: Tag + Text Combined', () => { afterEach(() => closeAll()); diff --git a/public/plugins/tests/search.test.js b/public/plugins/tests/search.test.js index ee46377..3e2fca3 100644 --- a/public/plugins/tests/search.test.js +++ b/public/plugins/tests/search.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, click, type, pressKey, sleep, closeAll } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, click, type, pressKey, sleep, closeAll } from './runner.js?v=3'; describe('Search', () => { afterEach(() => closeAll()); diff --git a/public/plugins/tests/settings.test.js b/public/plugins/tests/settings.test.js index e630c2c..7552eba 100644 --- a/public/plugins/tests/settings.test.js +++ b/public/plugins/tests/settings.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, $$, click, closeAll, sleep } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, $$, click, closeAll, sleep } from './runner.js?v=3'; describe('Settings: Open/Close', () => { afterEach(() => closeAll()); diff --git a/public/plugins/tests/shell.test.js b/public/plugins/tests/shell.test.js index 482f8e1..c36f68d 100644 --- a/public/plugins/tests/shell.test.js +++ b/public/plugins/tests/shell.test.js @@ -1,4 +1,4 @@ -import { describe, it, expect, $ } from './runner.js?v=2'; +import { describe, it, expect, $ } from './runner.js?v=3'; describe('App Shell', () => { it('header with logo', () => { diff --git a/public/plugins/tests/snippets.test.js b/public/plugins/tests/snippets.test.js index 80bb520..9f36940 100644 --- a/public/plugins/tests/snippets.test.js +++ b/public/plugins/tests/snippets.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=3'; // Helper: simulate typing a snippet trigger in the editor body function typeSnippet(bodyEl, trigger) { diff --git a/public/plugins/tests/sorting.test.js b/public/plugins/tests/sorting.test.js index f48ed4d..5259c6d 100644 --- a/public/plugins/tests/sorting.test.js +++ b/public/plugins/tests/sorting.test.js @@ -1,4 +1,4 @@ -import { describe, it, expect, csrf } from './runner.js?v=2'; +import { describe, it, expect, csrf } from './runner.js?v=3'; import { state, sortedCards } from '../../js/modules/state.js'; describe('Sorting: All Modes', () => { diff --git a/public/plugins/tests/state-functions.test.js b/public/plugins/tests/state-functions.test.js index 840f50e..94ee449 100644 --- a/public/plugins/tests/state-functions.test.js +++ b/public/plugins/tests/state-functions.test.js @@ -1,4 +1,4 @@ -import { describe, it, expect } from './runner.js?v=2'; +import { describe, it, expect } from './runner.js?v=3'; import { state, parseTags, sortedCards, getChildren, getGroupCards, getRecent, markRecent, getFavorites, toggleFavorite, isFavorite } from '../../js/modules/state.js'; describe('State: parseTags', () => { diff --git a/public/plugins/tests/super-buckets.test.js b/public/plugins/tests/super-buckets.test.js index 5cba72d..bf3961a 100644 --- a/public/plugins/tests/super-buckets.test.js +++ b/public/plugins/tests/super-buckets.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=3'; import { state } from '../../js/modules/state.js'; // Helper: create a bucket with the given name and return its row. diff --git a/public/plugins/tests/theme.test.js b/public/plugins/tests/theme.test.js index 504ba28..c85ccf6 100644 --- a/public/plugins/tests/theme.test.js +++ b/public/plugins/tests/theme.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, $$, click, closeAll, sleep } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, $$, click, closeAll, sleep } from './runner.js?v=3'; describe('Theme', () => { it('CSS custom properties set', () => { diff --git a/public/plugins/tests/toolbar.test.js b/public/plugins/tests/toolbar.test.js index 4c36dbf..f6bd1f6 100644 --- a/public/plugins/tests/toolbar.test.js +++ b/public/plugins/tests/toolbar.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=3'; import { state } from '../../js/modules/state.js'; describe('Toolbar: Filter Toggles', () => { diff --git a/public/plugins/tests/undo.test.js b/public/plugins/tests/undo.test.js index cf4db8d..402fd32 100644 --- a/public/plugins/tests/undo.test.js +++ b/public/plugins/tests/undo.test.js @@ -1,4 +1,4 @@ -import { describe, it, afterEach, expect, $, $$, click, type, rightClick, pressKey, sleep, closeAll, csrf } from './runner.js?v=2'; +import { describe, it, afterEach, expect, $, $$, click, type, rightClick, pressKey, sleep, closeAll, csrf } from './runner.js?v=3'; import { state } from '../../js/modules/state.js'; function ctrlZ() {