- jumpToMessage() rendered the APDU list through the unqualified renderMessages(), whose tail pins the view to the bottom (the live-capture stick), and only then let focusMessage() smooth-scroll back up to the clicked record — the visible jump-to-end-then-back. It now renders with skipScroll, so the only scroll is the focus itself - the content-follow became conditional: shouldStickToBottom() pins the view to the newest messages only while the reader is already at the bottom (40 px slack), so the 500 ms live poll no longer pulls away a view the reader scrolled to — a Files/map jump is no longer yanked down by the next batch. A freshly opened session or a new capture still lands at the bottom (renderMessages(false, true)) - tests (node): the jump's paging/render(true)/focus sequence with stubs, the no-session no-op, the stick decision (at bottom / within slack / scrolled up / explicit jump / forced / first render) and source pins for renderMessages, jumpToMessage, loadSession and startCapture; 527 Python + 53 Node tests - README/README_RUS note the follow behaviour; PWA_VERSION/pyproject/ version.py 2.6.1, SW cache simtrace-analyser-v73
84 lines
3.2 KiB
JavaScript
84 lines
3.2 KiB
JavaScript
// The Files-view row jump and the APDU list's content-follow decision:
|
|
// clicking a timeline row must scroll straight to its APDU — it used to
|
|
// render the list with the stick-to-bottom (jump to the end of the trace)
|
|
// and then smooth-scroll back up to the record.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const { loadSrc, extractFunc } = require('./helpers.js');
|
|
|
|
const src = loadSrc();
|
|
|
|
let STATE = {};
|
|
let API = {};
|
|
let renders = [];
|
|
let focused = [];
|
|
function renderMessages(skipScroll, forceStick) {
|
|
renders.push([skipScroll, forceStick]);
|
|
}
|
|
function focusMessage(idx) {
|
|
focused.push(idx);
|
|
}
|
|
eval(
|
|
extractFunc(src, 'jumpToMessage') + '\n' +
|
|
extractFunc(src, 'shouldStickToBottom')
|
|
);
|
|
|
|
test('jumpToMessage pages, renders without stick-to-bottom, then focuses', async () => {
|
|
renders = [];
|
|
focused = [];
|
|
STATE = { sessionViewId: 7, messages: [{}, {}] };
|
|
API = {
|
|
getSession: async (sid, from) => ({
|
|
messages: from === 2 ? [{}, {}, {}] : [],
|
|
}),
|
|
};
|
|
await jumpToMessage(3);
|
|
assert.equal(STATE.messages.length, 5);
|
|
assert.deepEqual(renders, [[true, undefined]]);
|
|
assert.deepEqual(focused, [3]);
|
|
});
|
|
|
|
test('jumpToMessage no-ops without a session view', async () => {
|
|
renders = [];
|
|
focused = [];
|
|
STATE = { sessionViewId: null, messages: [] };
|
|
API = { getSession: async () => { throw new Error('must not fetch'); } };
|
|
await jumpToMessage(3);
|
|
assert.deepEqual(renders, []);
|
|
assert.deepEqual(focused, []);
|
|
});
|
|
|
|
test('shouldStickToBottom: only a reader at the bottom follows new content', () => {
|
|
const list = (scrollTop, clientHeight, scrollHeight) => ({ scrollTop, clientHeight, scrollHeight });
|
|
// At the bottom / within the slack → follow.
|
|
assert.equal(shouldStickToBottom(list(500, 100, 600), false, false), true);
|
|
assert.equal(shouldStickToBottom(list(560, 100, 600), false, false), true);
|
|
// Scrolled up (e.g. after a Files/map jump) → leave the view alone.
|
|
assert.equal(shouldStickToBottom(list(200, 100, 600), false, false), false);
|
|
// An explicit jump never sticks; a new session/capture always does.
|
|
assert.equal(shouldStickToBottom(list(200, 100, 600), true, false), false);
|
|
assert.equal(shouldStickToBottom(list(200, 100, 600), false, true), true);
|
|
// First render: the empty list has no geometry yet.
|
|
assert.equal(shouldStickToBottom(list(0, 0, 0), false, false), true);
|
|
});
|
|
|
|
test('renderMessages scrolls through shouldStickToBottom', () => {
|
|
const body = extractFunc(src, 'renderMessages');
|
|
assert.match(body, /function renderMessages\(skipScroll, forceStick\)/);
|
|
assert.match(body, /const stick = shouldStickToBottom\(list, skipScroll, forceStick\);/);
|
|
assert.match(body, /if \(stick\) list\.scrollTop = list\.scrollHeight;/);
|
|
assert.doesNotMatch(body, /if \(!skipScroll\) list\.scrollTop/);
|
|
});
|
|
|
|
test('jumpToMessage renders with skipScroll (source pin)', () => {
|
|
const body = extractFunc(src, 'jumpToMessage');
|
|
assert.match(body, /renderMessages\(true\);/);
|
|
assert.match(body, /focusMessage\(idx\);/);
|
|
});
|
|
|
|
test('a new session/capture lands at the bottom (source pins)', () => {
|
|
assert.match(extractFunc(src, 'loadSession'), /renderMessages\(false, true\);/);
|
|
assert.match(extractFunc(src, 'startCapture'), /renderMessages\(false, true\);/);
|
|
});
|