Files
hyprcrd/public/plugins/tests/line-numbers.test.js
T
thandClaude Opus 4.6 be7eb1523e shopping: multi-list, DnD ordering, phone layout; fix flaky tests
Shopping lists:
  - Multiple lists per tenant: picker uses checkboxes instead of
    radios. Relay schema migrated to composite PK (tenant_id,
    card_uid) with a position column for display ordering.
  - Picker rows are drag-and-drop reorderable (grip handle +
    accent drop indicator). Order persists in localStorage and is
    pushed to the relay as position integers — phone renders lists
    in that order (ORDER BY position, title).
  - Each picker row has a goto-arrow that closes the popup,
    switches to the card's bucket if needed, and opens the card.
  - Relay DELETE /api/shopping removes deactivated lists. Client
    proxy forwards DELETE. Deactivating a list in the picker
    immediately removes it from the relay.
  - Phone UI renders multiple lists as stacked boxes with headers.
    Section headings (# h1–h6) get 14px top padding for breathing
    room. CSS selectors fixed to match div-based headings inside
    the new .shopping-list-box containers.
  - Help text updated for multi-list wording.
  - pullOne preserves the synced position so a pull doesn't cause
    a spurious re-push of the same order.

Test fixes:
  - Line-numbers gutter height tolerance bumped 50%→60% — OS text
    shaping can push the divergence past 50%.
  - Find/replace flaky: replaced sleep(200) with waitFor on the
    confirm dialog so slow renders don't miss the OK click.
  - Shopping picker test updated: checkboxes instead of radios.

Bumps to 1.9.25.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:26:50 +02:00

235 lines
8.1 KiB
JavaScript

import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=4';
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,
// font metrics, and OS-level text shaping — allow 60% relative tolerance.
// Scroll sync (proportional mapping) handles the real alignment at runtime.
const tolerance = Math.max(textTotal, gutterTotal) * 0.6;
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);
});
});