v0.15.0 8a6cfec

log.test.mjs RawHistoryFolder

1// Tests of the live job log of static/js/log.js (window.ermiteLog) with a stub
2// EventSource on the tiny hand-rolled DOM. Run with
3//   node --test 'internal/adapters/web/jstest/*.test.mjs'
4import assert from 'node:assert/strict';
5import { test } from 'node:test';
6import { el, installDom } from './dom.mjs';
7
8// A line is "text" or "+text" for a piece that continues the line before it.
9function line(text) {
10  const cont = text.startsWith('+');
11  const li = el('li', { cls: 'lg', attrs: cont ? { 'data-cont': '' } : {} }, el('span', { cls: 't', text: cont ? text.slice(1) : text }));
12  return li;
13}
14
15installDom({ parseHTML: (data) => data.split('|').filter(Boolean).map(line) });
16globalThis.document.addEventListener = () => {}; // log.js starts itself on DOMContentLoaded
17
18class StubEventSource {
19  static CLOSED = 2;
20  static last = null;
21  constructor(url) {
22    this.url = url;
23    this.readyState = 1;
24    this.handlers = new Map();
25    StubEventSource.last = this;
26  }
27  addEventListener(name, fn) { this.handlers.set(name, fn); }
28  close() { this.readyState = StubEventSource.CLOSED; }
29  emit(name, data) { this.handlers.get(name)?.({ data }); }
30}
31globalThis.EventSource = StubEventSource;
32
33let scrolls = [];
34globalThis.scrollTo = (x, y) => scrolls.push(y);
35Object.assign(globalThis, { innerHeight: 800, scrollY: 0 });
36
37await import('../static/js/log.js');
38const { follow } = window.ermiteLog;
39
40// page makes a job page body: the box with its list and the lines already there.
41function page(attrs, ...lines) {
42  const list = el('ol', { cls: 'log' }, ...lines.map(line));
43  const box = el('div', { id: 'job-log', attrs }, list);
44  const body = document.body;
45  body.replaceChildren(box);
46  Object.defineProperty(globalThis.document.body, 'scrollHeight', { value: 1000, configurable: true });
47  return { box, list };
48}
49const running = { 'data-follow': '', 'data-stream': '/r/job/stream', 'data-next': '3' };
50const texts = (list) => list.children.map((li) => li.querySelector('.t').textContent);
51
52test('it does nothing for a job that is over, or a page without a feed', () => {
53  assert.equal(follow(null), null);
54  assert.equal(follow(page({ 'data-stream': '/s' }).box), null, 'no data-follow: the job ended');
55  assert.equal(follow(page({ 'data-follow': '' }).box), null, 'no stream URL');
56});
57
58test('it opens the feed after the last line the page holds', () => {
59  const { box } = page(running, 'a', 'b', 'c');
60  const es = follow(box);
61  assert.equal(es.url, '/r/job/stream?from=3');
62});
63
64test('it appends the lines of a batch', () => {
65  const { box, list } = page(running, 'a');
66  const es = follow(box);
67  es.emit('ci-log', 'b|c');
68  es.emit('ci-log', 'd');
69  assert.deepEqual(texts(list), ['a', 'b', 'c', 'd']);
70});
71
72test('a piece that continues a line of an earlier batch joins that line', () => {
73  const { box, list } = page(running, 'start of a long');
74  const es = follow(box);
75  es.emit('ci-log', '+ line|next');
76  assert.deepEqual(texts(list), ['start of a long line', 'next']);
77});
78
79test('it scrolls with the log only while the reader is at its end', () => {
80  const { box } = page(running, 'a');
81  const es = follow(box);
82  scrolls = [];
83  Object.assign(globalThis, { scrollY: 190 }); // 800 + 190 >= 1000 - 80: at the end
84  es.emit('ci-log', 'b');
85  assert.deepEqual(scrolls, [1000]);
86  scrolls = [];
87  Object.assign(globalThis, { scrollY: 0 }); // reading above
88  es.emit('ci-log', 'c');
89  assert.deepEqual(scrolls, []);
90});
91
92test('ci-end closes the feed, and a closed connection stays closed', () => {
93  const { box } = page(running, 'a');
94  const es = follow(box);
95  assert.equal(es.readyState, 1);
96  es.emit('ci-end', 'end');
97  assert.equal(es.readyState, StubEventSource.CLOSED);
98
99  const again = follow(page(running, 'a').box);
100  again.readyState = StubEventSource.CLOSED;
101  again.onerror();
102  assert.equal(again.readyState, StubEventSource.CLOSED);
103});

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close