feat: repli des articles en mode lecture avec bouton Lire plus
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d8b334767d
commit
dcacf1acb1
5 files changed
+872
No files matched your search
@@ -0,0 +1,145 @@
|
||||
'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 `<div class="flux${notRead ? ' not_read' : ''}" id="flux_${id}">
|
||||
<article class="flux_content"><div class="content">
|
||||
<div class="text" data-h="${height}"><p>Article ${id}</p><img src="x.png"></div>
|
||||
</div></article>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function setup(entries) {
|
||||
const dom = new JSDOM(`<!DOCTYPE html><body><main id="stream">${entries.join('')}</main></body>`,
|
||||
{ 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');
|
||||
});
|
||||
Reference in new issue
Block a user