Skip to main content

Terminal View Region

The terminal-view region shows the progress and result of a multi-step run: a summary card with a headline and failures first, a list of steps with their state and duration, and a strip of colour blocks per test suite that you can hover or focus for a tooltip. It reads a stream of small, versioned events from an endpoint, or from events your own code hands to it, and reduces them to one model on the client.

You can show one in three ways:

WhereHow
A console page card<@card type="terminal-view" id="run">{ contractVersion: '1', terminalView: {...} }</@card>
A views page regionTerminalViewDef.forMixin(...).toRegion() in a RegionTable
A table row detailA datatables catalog detail.region with populate: 'terminal-view' and params

The event types and the report readers live in their own small module, juneau-terminal-report (package org.apache.juneau.terminal.report: TerminalEvent, TerminalViewChecks, ReportReaders and the readers), which has no dependency on the REST server. A producer such as a build tool can depend on it alone. The rest of the server half ships in juneau-console-views (org.apache.juneau.console.views), which depends on juneau-terminal-report; the client half is juneau-terminal-view.js, part of the views toolkit pack. It pairs naturally with the Terminal Region: the terminal view summarizes, the terminal holds the raw output, and a test block can link to the terminal offset it came from.

The event contract​

The contract version is "1". The client accepts one event, an array of events, or a page:

{
"contractVersion": "1",
"events": [ { "ev": "step", "seq": 1, "id": "build", "title": "mvn package" } ],
"next": "k3x9ab.1",
"more": false,
"terminal": false
}
Page memberRules
contractVersionRequired. Exactly "1"; anything else is fatal (E-RV-2).
eventsRequired, may be empty. Ascending seq. At most 2,000 per page from the bundled endpoint.
nextRequired. An opaque token (^[A-Za-z0-9._~-]{1,128}$) the client sends back as ?after=.
moreRequired. true when the source holds more events right now; the client re-fetches at once.
terminalOptional, default false. true means the source will never produce another event. The client stops polling when it sees terminal: true with more: false.

Every event carries ev and, in a page, a strictly increasing seq (gaps are fine). An unknown ev and unknown members are ignored silently, so the contract can grow. A malformed known event is dropped and reported once per batch (E-RV-3). Strings over their limit are truncated with a trailing … on the client and rejected by the Java factories.

evMembersMeaning
stepid (^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$), title (1-200), optional n, status (running or waiting), rawOffsetA step starts, or an open step is updated.
endid, status (ok, fail, skip), optional ms, exitThe step finishes. exit is shown in the step tooltip only.
suitestep, fw, suite (1-512), counts {pass, fail, skip}, optional rawOffsetA live placeholder result for a whole suite, before per-test detail exists.
teststep, fw, suite, name (1-512), status (pass, fail, skip, error), optional ms, msg (2,000), trace (8,000), rawOffsetOne test result.
replacestepDiscard the step's suites and tests so a fresh, authoritative set can follow.
notelevel (info, warn, error), text (1-1,000), optional href, stepA notable event, shown under its step or under the run.
donestatus (ok, fail, cancelled)The run finished.

fw is the test framework key (^[a-z0-9][a-z0-9._-]{0,31}$). The counts line labels the built-in keys surefire (Surefire), junit-xml (JUnit), pytest (pytest), jest (Jest) and playwright (Playwright); any other key is displayed as itself.

Reducer rules​

The client applies events in seq order and skips any event at or below the last applied seq, so a replayed page is harmless.

  • A step with a new id appends a step. With a known, open id it updates the title, ordinal, status and rawOffset (a waiting step becoming running is a second step event). For an ended step it is dropped.
  • An end for an unknown step is dropped; a second end replaces the first.
  • Suites are keyed by (step, fw, suite). A suite event creates a placeholder with counts; the first test for the key converts it to a detailed suite and discards the placeholder counts.
  • replace removes all suites and tests of a step. Which suites the user expanded survives it.
  • A step still open when a done exists is shown as fail if the run failed, otherwise skip.

Re-run steps​

A step cannot reopen once it has ended, so a producer that runs a step again sends it as a new step: the first run keeps the plain id and later runs take <id>.2, <id>.3 and so on, titled <title> (attempt 2), <title> (attempt 3). The client recognizes this naming, and shows only the latest attempt in the step list. The earlier attempts sit under it behind an N earlier attempts toggle (1 earlier attempt for a single one), collapsed by default. A step id counts as an attempt only when the step it extends (build for build.2) exists; a title ending in (attempt N) is recognized the same way when the ids do not follow the pattern. Expanding the toggle lists the earlier attempts, oldest first, each as an ordinary step. The toggle stays open as later attempts arrive, and picking a failure from the summary list opens it when that failure belongs to an earlier attempt.

[
{ "ev": "step", "id": "build", "title": "mvn package" },
{ "ev": "end", "id": "build", "status": "fail" },
{ "ev": "step", "id": "build.2", "title": "mvn package (attempt 2)" }
]

This shows one step, mvn package (attempt 2), with a collapsed 1 earlier attempt toggle under it. Counts and the failures list still include every attempt.

The run status is derived, not sent:

ConditionStatusHeadline
No events yetemptyWaiting for events…
Page terminal with no donestoppedStopped
No done, a step waiting and none runningwaitingWaiting at title
No donerunningRunning
done is okokPassed
done is failfailFailed at title
done is cancelledcancelledCancelled

Serving events: TerminalViewMixin​

TerminalViewMixin serves GET /juneau-terminal-view/{runId}/events?after=<token>. Implement terminalViewSource to resolve the run id, and return an empty Optional for an id the caller may not read (the endpoint answers 404, never a distinguishable 403):

@Rest
public class Runs extends BasicRestServlet implements TerminalViewMixin {
static final Map<String,TerminalViewSource> SOURCES = new ConcurrentHashMap<>();

@Override
public Optional<TerminalViewSource> terminalViewSource(String runId, RestRequest req) {
return Optional.ofNullable(SOURCES.get(runId));
}
}

TerminalViewEndpoints gives the same logic for custom paths. An unknown or stale after token answers 410 Gone, and the client then reloads the stream from the start with no banner.

Sources​

TerminalViewLog is a thread-safe in-memory source that the producer appends to:

TerminalViewLog log = TerminalViewLog.create();
Runs.SOURCES.put("42", log);

log.append(TerminalEvent.step("build", "mvn package"));
log.append(TerminalEvent.suite("build", "surefire", "FooTest", 12, 0, 0));
log.append(TerminalEvent.end("build", TerminalEvent.EndStatus.OK));
log.append(TerminalEvent.done(TerminalEvent.DoneStatus.OK));
log.close(); // the final page carries terminal: true

append numbers the event and returns it; appendAll takes a batch. The log holds at most TerminalViewLog.DEFAULT_MAX_EVENTS (200,000) events. reset() empties it and starts a new epoch, so clients holding an old token get 410.

FileTerminalViewSource pages one growing JSON Lines file, one event per line. The line number is the seq, so write events without one. Only complete lines are read, and a truncated or replaced file answers 410:

TerminalViewSource src = FileTerminalViewSource.create(root.resolve("run-42.jsonl"))
.terminal(() -> run.isFinished())
.build();

The application supplies the Path; nothing in a request ever becomes a path. Keep one instance per file. Implement TerminalViewSource for anything else.

Report readers​

The readers in juneau-terminal-report turn test-report files into events, and work without the region. A reader never throws for bad input: a missing, unreadable, oversize, malformed or truncated file yields the tests it could recover plus a warning.

Kind passed to ReportReaders.forKindReaderfw
surefireSurefireReportReader (a directory of TEST-*.xml, or one file)surefire
junitxmlJUnitXmlReportReaderjunit-xml
pytestJUnitXmlReportReader("pytest")pytest
jest-jsonJestJsonReportReaderjest
playwright-jsonPlaywrightJsonReportReaderplaywright

forKind returns null for an unknown kind. The XML readers stream the file with external entities and DTDs disabled. ReportLimits.DEFAULT caps a file at 32 MiB and a report at 100,000 tests, with names clipped to 512 characters, messages to 2,000 and traces to 8,000.

ReportResult result = ReportReaders.forKind("surefire").read(reportsDir, ReportLimits.DEFAULT);
for (TerminalEvent e : result.toEvents("build"))
log.append(e);

toEvents(step) returns replace, one test per test, then one warn note per warning (such as report truncated: unexpected end of file after 4120 tests). A result with warnings and no recovered tests returns only the notes, so the step keeps its live suite placeholders. Warning text names files, never absolute paths.

Declaring the region​

TerminalViewDef def = TerminalViewDef.forMixin("run", "", "42")   // id, mount path, run id
.title("Run 42")
.refreshMs(2000)
.rawHref("#raw-O{offset}");
RegionTable.of(def.validate().toRegion());
OptionMeaning
eventsUrlSame-origin events URL; {id} is replaced by the row id in a row detail. Required unless poll is false.
refreshMsPoll interval, at least 1000.
pollfalse skips polling; you feed events through the JavaScript API.
compactSmaller layout for a row detail: a three-failure list and a one-line summary when clean.
titleAccessible name of the region; default "Run".
rawHrefTemplate for block links, containing {offset} once, such as #raw-O{offset}.
onBlockClickFunction called when a test block is clicked, or activated with Enter or Space. JavaScript API only; see Block clicks.

As a console card​

<@card type="terminal-view" id="run" title="Run">{
contractVersion: '1',
terminalView: { eventsUrl: '/juneau-terminal-view/42/events', rawHref: '#raw-O{offset}' }
}</@card>

The keys of terminalView are exactly those above. id= is required; src= and template= are rejected. The card needs the views toolkit on the page. If JuneauViews.terminalView is missing at load time, the shell shows E-JS-14. The other card types are listed in Admin-Console Chrome.

Linking to the raw console​

A host page that shows a terminal card beside the terminal view gives the terminal view rawHref: '#<terminal id>-O{offset}'. A block or step whose event carries rawOffset then links to that byte offset in the terminal output. Which tab shows the terminal on hashchange is the host page's job.

JavaScript API​

JuneauViews.terminalView publishes:

MemberPurpose
create(el, options)Build an instance on an element without polling and return its handle.
poll(handle, opts)Start polling the handle's eventsUrl; resolves when polling ends.
mount(el, options, ctx)create plus poll (unless poll: false); returns a cleanup function.
of(elOrId)The handle mounted on an element or id.
validateEvent, formatDuration, isSafeNoteHref, isSafeRawHref, CONTRACT_VERSIONHelpers.

A handle has append(events), which takes one event or an array and returns {applied, skipped, dropped}, and destroy(). To drive a region from your own transport, such as server-sent events, declare it with poll: false and append what arrives:

const rv = JuneauViews.terminalView.of('run');
const es = new EventSource('/runs/42/stream');
es.onmessage = m => rv.append(JSON.parse(m.data));

An event passed without a seq gets the next one. The region publishes {kind: 'terminal-view.state', id, state, terminal} on the region bus whenever the derived status changes, so a host can enable or disable a Cancel button without reading the DOM.

Block clicks​

Every test block carries data-step-id, the id of the step it belongs to (a re-run step has its own id, such as build.2). A host can map a block to its step without counting positions.

Pass onBlockClick to create() or mount() to be told when a block is used:

JuneauViews.terminalView.create(el, {
rawHref: '#term-O{offset}',
onBlockClick: info => console.log(info.stepId, info.offset, info.kind)
});

info is {stepId, offset, kind}:

MemberValue
stepIdThe step's id, the same value as the block's data-step-id.
offsetThe test's rawOffset. Omitted when the test has none.
kindThe test status: pass, fail, skip or error.

The hook fires once per click, and once per Enter or Space on a focused block that is not a link. Link blocks still navigate. A hook that throws is logged and ignored. A function can't be written in a declarative region or a card, so onBlockClick works only in JavaScript-configured mounts; a value that is not a function is rejected with E-RV-1.

Limits​

Counts are always exact. Individual records beyond the caps are counted but not kept, and a cap hit is reported once (E-RV-9).

CapValue
Steps1,000; further new steps are dropped (existing steps still update)
Suites per step5,000
Retained passing or skipped tests20,000 per instance
Retained failing or errored tests5,000 per instance, so passes cannot crowd out failures
Notes1,000
Serialized event65,536 characters

The DOM is bounded separately: collapsed suites build no blocks, an expanded strip shows at most 300 blocks, at most 2,000 suite rows render, and the failures list shows 50 failures (3 in compact mode).

Security​

  • All event text is assigned with textContent; nothing is parsed as HTML.
  • A block link is built from rawHref and the digits of rawOffset; an event cannot inject a URL.
  • note.href must be an absolute http: or https: URL with a host, or a same-origin path. Anything else is dropped (E-RV-7) and the note still renders. Absolute links open with rel="noopener noreferrer".
  • runId is checked against ^[A-Za-z0-9_-]{1,128}$ before your resolver sees it, and after against the token grammar. A token never names a path.

Accessibility​

  • The region is role="region" with an accessible name; the headline is its only live region.
  • Steps are an ordered list, and each step's state is spoken as text, not conveyed by the glyph.
  • Suite toggles are buttons with aria-expanded. Blocks are focusable and carry the test name and status in aria-label; tooltips show on focus and close with Escape.
  • The failures list gives keyboard access to every failure without tabbing through the blocks, and a failure's trace opens from its own "details" button.
  • Nothing is animated, and focus is never moved by an update.

Errors​

Errors carry data-juneau-error="E-RV-n". A fatal one shows a banner inside the region, stops polling and keeps the rendered content.

CodeMeaning
E-RV-1Bad option: missing or cross-origin eventsUrl, bad refreshMs, bad rawHref. Thrown before any DOM.
E-RV-2A response's contractVersion is not "1". Fatal.
E-RV-3Malformed events were dropped (a warning once per batch).
E-RV-4Fetching events failed. Fatal for HTTP 4xx other than 408 and 429; otherwise "Reconnecting…" with backoff.
E-RV-5Invalid next token, or seq not strictly increasing within a page. Fatal.
E-RV-6An event named an unknown or ended step (warning, once per step).
E-RV-7An unsafe note.href was ignored.
E-RV-8The server reset the stream (410 with a token) and the view reloaded. Console only, no banner.
E-RV-9A model cap was hit; some records are counted but not shown.

The console shell adds E-JS-14 for a terminal-view card on a page without the views toolkit.

See also​