import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=2'; 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); }); });