'use strict'; const { test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { JSDOM } = require('jsdom'); const SOURCE = fs.readFileSync(path.join(__dirname, '../../static/read-more.js'), 'utf8'); // jsdom has no layout: heights come from data-h on each .text element. const measure = (el) => Number(el.dataset.h || 0); function entry(id, height, notRead = false) { return `

Article ${id}

`; } function setup(entries) { const dom = new JSDOM(`
${entries.join('')}
`, { runScripts: 'outside-only' }); dom.window.eval(SOURCE); return { window: dom.window, document: dom.window.document, ReadMore: dom.window.ReadMore }; } const OPTS = { collapsedHeight: 250, label: 'Lire plus', measure }; function setContext(window, view) { window.context = { current_view: view, extensions: { read_more: { collapsed_height: 250, label: 'Lire plus' } }, }; } test('shouldCollapse thresholds', () => { const { ReadMore } = setup([]); assert.equal(ReadMore.THRESHOLD_RATIO, 1.5); assert.equal(ReadMore.shouldCollapse(374, 250), false); assert.equal(ReadMore.shouldCollapse(375, 250), true); assert.equal(ReadMore.shouldCollapse(1000, 250), true); }); test('collapses tall entry', () => { const { document, ReadMore } = setup([entry(1, 1000)]); ReadMore.processEntries(document, OPTS); const flux = document.getElementById('flux_1'); const text = flux.querySelector('.text'); assert.ok(text.classList.contains('read-more-collapsed')); assert.equal(text.style.getPropertyValue('--read-more-height'), '250px'); const buttons = flux.querySelectorAll('.read-more-btn'); assert.equal(buttons.length, 1); assert.equal(buttons[0].textContent, 'Lire plus'); assert.equal(buttons[0].type, 'button'); assert.equal(text.nextElementSibling, buttons[0]); assert.equal(flux.dataset.readMore, 'collapsed'); }); test('skips short entry', () => { const { document, ReadMore } = setup([entry(1, 300)]); ReadMore.processEntries(document, OPTS); const flux = document.getElementById('flux_1'); assert.ok(!flux.querySelector('.text').classList.contains('read-more-collapsed')); assert.equal(flux.querySelectorAll('.read-more-btn').length, 0); assert.equal(flux.dataset.readMore, 'short'); }); test('is idempotent', () => { const { document, ReadMore } = setup([entry(1, 1000), entry(2, 1000)]); ReadMore.processEntries(document, OPTS); ReadMore.processEntries(document, OPTS); for (const flux of document.querySelectorAll('.flux')) { assert.equal(flux.querySelectorAll('.read-more-btn').length, 1); } }); test('click expands', () => { const { window, document, ReadMore } = setup([entry(1, 1000)]); ReadMore.processEntries(document, OPTS); const flux = document.getElementById('flux_1'); let parentClicks = 0; flux.addEventListener('click', () => { parentClicks++; }); const event = new window.MouseEvent('click', { bubbles: true, cancelable: true }); flux.querySelector('.read-more-btn').dispatchEvent(event); assert.ok(!flux.querySelector('.text').classList.contains('read-more-collapsed')); assert.equal(flux.querySelectorAll('.read-more-btn').length, 0); assert.equal(flux.dataset.readMore, 'expanded'); assert.equal(event.defaultPrevented, true); assert.equal(parentClicks, 0); }); test('does not re-collapse expanded entry', () => { const { document, ReadMore } = setup([entry(1, 1000)]); ReadMore.processEntries(document, OPTS); document.querySelector('.read-more-btn').click(); ReadMore.processEntries(document, OPTS); const flux = document.getElementById('flux_1'); assert.ok(!flux.querySelector('.text').classList.contains('read-more-collapsed')); assert.equal(flux.querySelectorAll('.read-more-btn').length, 0); assert.equal(flux.dataset.readMore, 'expanded'); }); test('does not touch read state', () => { const { document, ReadMore } = setup([entry(1, 1000, true)]); ReadMore.processEntries(document, OPTS); const flux = document.getElementById('flux_1'); assert.ok(flux.classList.contains('not_read')); flux.querySelector('.read-more-btn').click(); assert.ok(flux.classList.contains('not_read')); }); test('reevaluates short entry after image load', () => { const { window, document, ReadMore } = setup([entry(1, 300)]); setContext(window, 'reader'); ReadMore.init(window, measure); const flux = document.getElementById('flux_1'); assert.equal(flux.dataset.readMore, 'short'); const text = flux.querySelector('.text'); text.dataset.h = '1000'; text.querySelector('img').dispatchEvent(new window.Event('load')); assert.ok(text.classList.contains('read-more-collapsed')); assert.equal(flux.dataset.readMore, 'collapsed'); assert.equal(flux.querySelectorAll('.read-more-btn').length, 1); }); test('init ignores non-reader view', () => { const { window, document, ReadMore } = setup([entry(1, 1000)]); setContext(window, 'normal'); ReadMore.init(window, measure); assert.equal(document.querySelectorAll('.read-more-btn').length, 0); assert.equal(document.getElementById('flux_1').dataset.readMore, undefined); }); test('init handles load-more', () => { const { window, document, ReadMore } = setup([entry(1, 1000)]); setContext(window, 'reader'); ReadMore.init(window, measure); assert.equal(document.querySelectorAll('.read-more-btn').length, 1); document.getElementById('stream').insertAdjacentHTML('beforeend', entry(2, 1000)); document.body.dispatchEvent(new window.Event('freshrss:load-more', { bubbles: true })); assert.equal(document.querySelectorAll('.read-more-btn').length, 2); assert.equal(document.getElementById('flux_2').dataset.readMore, 'collapsed'); });