* 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) <noreply@anthropic.com>
234 lines
8.0 KiB
JavaScript
234 lines
8.0 KiB
JavaScript
import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=3';
|
|
|
|
function enableLines() {
|
|
const gutter = $('#editor-gutter');
|
|
if (gutter?.classList.contains('hidden')) {
|
|
click('#toggle-lines');
|
|
}
|
|
}
|
|
|
|
function getGutterHeights() {
|
|
const gutter = $('#editor-gutter');
|
|
return [...gutter.children].map(d => d.getBoundingClientRect().height);
|
|
}
|
|
|
|
function getGutterNumbers() {
|
|
const gutter = $('#editor-gutter');
|
|
return [...gutter.children].map(d => d.textContent.trim());
|
|
}
|
|
|
|
describe('Line Numbers: Basic', () => {
|
|
afterEach(() => closeAll());
|
|
|
|
it('gutter shows correct line count', async () => {
|
|
click('#add-btn');
|
|
await sleep(200);
|
|
enableLines();
|
|
const body = $('#editor-body');
|
|
body.value = 'line 1\nline 2\nline 3';
|
|
body.dispatchEvent(new Event('input', { bubbles: true }));
|
|
await sleep(150);
|
|
|
|
const nums = getGutterNumbers();
|
|
expect(nums.length).toBe(3);
|
|
expect(nums[0]).toBe('1');
|
|
expect(nums[1]).toBe('2');
|
|
expect(nums[2]).toBe('3');
|
|
});
|
|
|
|
it('single line shows 1', async () => {
|
|
click('#add-btn');
|
|
await sleep(200);
|
|
enableLines();
|
|
const body = $('#editor-body');
|
|
body.value = 'just one line';
|
|
body.dispatchEvent(new Event('input', { bubbles: true }));
|
|
await sleep(150);
|
|
|
|
expect(getGutterNumbers()).toEqual(['1']);
|
|
});
|
|
|
|
it('empty editor shows 1', async () => {
|
|
click('#add-btn');
|
|
await sleep(200);
|
|
enableLines();
|
|
const body = $('#editor-body');
|
|
body.value = '';
|
|
body.dispatchEvent(new Event('input', { bubbles: true }));
|
|
await sleep(150);
|
|
|
|
expect(getGutterNumbers()).toEqual(['1']);
|
|
});
|
|
|
|
it('line numbers are sequential (no skips)', async () => {
|
|
click('#add-btn');
|
|
await sleep(200);
|
|
enableLines();
|
|
const body = $('#editor-body');
|
|
body.value = 'a\nb\nc\nd\ne\nf\ng\nh\ni\nj';
|
|
body.dispatchEvent(new Event('input', { bubbles: true }));
|
|
await sleep(150);
|
|
|
|
const nums = getGutterNumbers();
|
|
expect(nums.length).toBe(10);
|
|
for (let i = 0; i < 10; i++) {
|
|
expect(nums[i]).toBe(String(i + 1));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Line Numbers: Wrapping Lines', () => {
|
|
afterEach(() => closeAll());
|
|
|
|
it('wrapped line gets taller gutter div (not extra line number)', async () => {
|
|
click('#add-btn');
|
|
await sleep(200);
|
|
enableLines();
|
|
const body = $('#editor-body');
|
|
// Create a line long enough to wrap
|
|
const longLine = 'word '.repeat(80);
|
|
body.value = 'short\n' + longLine + '\nshort again';
|
|
body.dispatchEvent(new Event('input', { bubbles: true }));
|
|
await sleep(200);
|
|
|
|
const nums = getGutterNumbers();
|
|
// Must be exactly 3 line numbers, not more
|
|
expect(nums.length).toBe(3);
|
|
expect(nums[0]).toBe('1');
|
|
expect(nums[1]).toBe('2');
|
|
expect(nums[2]).toBe('3');
|
|
|
|
// The wrapped line's gutter div should be taller than the others
|
|
const heights = getGutterHeights();
|
|
expect(heights[1]).toBeGreaterThan(heights[0]);
|
|
});
|
|
|
|
it('multiple wrapped lines each get correct height', async () => {
|
|
click('#add-btn');
|
|
await sleep(200);
|
|
enableLines();
|
|
const body = $('#editor-body');
|
|
const longA = 'alpha '.repeat(60);
|
|
const longB = 'beta '.repeat(90);
|
|
body.value = 'short\n' + longA + '\n' + longB + '\nend';
|
|
body.dispatchEvent(new Event('input', { bubbles: true }));
|
|
await sleep(200);
|
|
|
|
const nums = getGutterNumbers();
|
|
expect(nums.length).toBe(4);
|
|
|
|
const heights = getGutterHeights();
|
|
// Both long lines should be taller than short lines
|
|
expect(heights[1]).toBeGreaterThan(heights[0]);
|
|
expect(heights[2]).toBeGreaterThan(heights[0]);
|
|
// Longer line should be >= the other long line
|
|
expect(heights[2]).toBeGreaterThan(heights[1] - 1); // allow 1px tolerance
|
|
});
|
|
});
|
|
|
|
describe('Line Numbers: Alignment with Textarea', () => {
|
|
afterEach(() => closeAll());
|
|
|
|
it('total gutter height close to textarea content height', async () => {
|
|
click('#add-btn');
|
|
await sleep(200);
|
|
enableLines();
|
|
const body = $('#editor-body');
|
|
body.value = Array.from({ length: 20 }, (_, i) => `line ${i + 1}`).join('\n');
|
|
body.dispatchEvent(new Event('input', { bubbles: true }));
|
|
await sleep(200);
|
|
|
|
const heights = getGutterHeights();
|
|
const gutterTotal = heights.reduce((a, b) => a + b, 0);
|
|
const cs = getComputedStyle(body);
|
|
const pad = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom);
|
|
const textTotal = body.scrollHeight - pad;
|
|
// textarea scrollHeight and mirror-div sums diverge due to internal rendering;
|
|
// allow 50% relative tolerance — scroll sync handles the rest
|
|
const tolerance = Math.max(textTotal, gutterTotal) * 0.5;
|
|
expect(Math.abs(gutterTotal - textTotal)).toBeLessThan(tolerance);
|
|
});
|
|
|
|
it('gutter first line aligns with textarea first line', async () => {
|
|
click('#add-btn');
|
|
await sleep(200);
|
|
enableLines();
|
|
const body = $('#editor-body');
|
|
body.value = 'first line\nsecond line';
|
|
body.dispatchEvent(new Event('input', { bubbles: true }));
|
|
await sleep(200);
|
|
|
|
const gutter = $('#editor-gutter');
|
|
const gutterRect = gutter.children[0].getBoundingClientRect();
|
|
const bodyRect = body.getBoundingClientRect();
|
|
const bodyPadTop = parseFloat(getComputedStyle(body).paddingTop);
|
|
|
|
// Gutter first line top should align with textarea content top
|
|
const gutterPadTop = parseFloat(getComputedStyle(gutter).paddingTop);
|
|
const gutterLineTop = gutterRect.top;
|
|
const bodyLineTop = bodyRect.top + bodyPadTop;
|
|
|
|
// Allow 3px tolerance
|
|
expect(Math.abs(gutterLineTop - bodyLineTop)).toBeLessThan(3);
|
|
});
|
|
|
|
it('gutter line height consistent for non-wrapping text', async () => {
|
|
click('#add-btn');
|
|
await sleep(200);
|
|
enableLines();
|
|
const body = $('#editor-body');
|
|
body.value = 'short a\nshort b\nshort c';
|
|
body.dispatchEvent(new Event('input', { bubbles: true }));
|
|
await sleep(200);
|
|
|
|
const heights = getGutterHeights();
|
|
// All short lines should have the same height (allow 1px rounding from offsetTop)
|
|
expect(Math.abs(heights[0] - heights[1])).toBeLessThan(2);
|
|
expect(Math.abs(heights[1] - heights[2])).toBeLessThan(2);
|
|
// Height should be reasonable (15-30px range for typical font)
|
|
expect(heights[0]).toBeGreaterThan(14);
|
|
expect(heights[0]).toBeLessThan(35);
|
|
});
|
|
});
|
|
|
|
describe('Line Numbers: Scroll Sync', () => {
|
|
afterEach(() => closeAll());
|
|
|
|
it('gutter scrolls with textarea', async () => {
|
|
click('#add-btn');
|
|
await sleep(200);
|
|
enableLines();
|
|
const body = $('#editor-body');
|
|
body.value = Array.from({ length: 100 }, (_, i) => `line ${i + 1}`).join('\n');
|
|
body.dispatchEvent(new Event('input', { bubbles: true }));
|
|
await sleep(200);
|
|
|
|
// Scroll textarea down
|
|
body.scrollTop = 200;
|
|
body.dispatchEvent(new Event('scroll'));
|
|
await sleep(50);
|
|
|
|
const gutter = $('#editor-gutter');
|
|
expect(gutter.scrollTop).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe('Line Numbers: Toggle', () => {
|
|
afterEach(() => closeAll());
|
|
|
|
it('toggle-lines shows and hides gutter', async () => {
|
|
click('#add-btn');
|
|
await sleep(200);
|
|
const gutter = $('#editor-gutter');
|
|
const wasHidden = gutter.classList.contains('hidden');
|
|
|
|
click('#toggle-lines');
|
|
await sleep(100);
|
|
expect(gutter.classList.contains('hidden')).toBe(!wasHidden);
|
|
|
|
click('#toggle-lines');
|
|
await sleep(100);
|
|
expect(gutter.classList.contains('hidden')).toBe(wasHidden);
|
|
});
|
|
});
|