Files
coolify/resources/js/terminal-session-start.test.js
T
Andras BacsaiandClaude Opus 5.5 0d20a14c5c fix(terminal): show an error instead of an endless connecting spinner
When the terminal started automatically, the session timeout started
only after a token arrived. If the server returned no token (container
not running, no shell, access denied, terminal disabled), the spinner
stayed on "connecting…" until the page was reloaded.

- Every early return now sends terminal-session-failed with a short
  message; denied and unknown servers get the same text.
- Auto-start starts the session timeout in init(), so a missing token
  always ends in a visible error with "Reload page".
- With more than one container, auto-start is cancelled, so the
  container picker shows no false timeout error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:27:28 +02:00

180 lines
5.8 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
TERMINAL_CONNECTION_ERRORS,
TERMINAL_SESSION_START_TIMEOUT_MS,
terminalSessionStartMethods,
} from './terminal-connection.js';
/**
* Builds the smallest host that the terminal Alpine component gives to the mixin.
*/
function createTerminalHost(overrides = {}) {
const host = {
starting: false,
terminalActive: false,
connectionError: null,
authRejected: false,
pendingCommand: null,
sessionStartTimeoutId: null,
toasts: [],
websocketStarts: 0,
$wire: {
dispatch(name, message) {
host.toasts.push([name, message]);
},
},
ensureWebSocketConnection() {
host.websocketStarts++;
},
...terminalSessionStartMethods,
...overrides,
};
return host;
}
/** Same steps as `init()` in terminal.js for `data-auto-start="true"`. */
function autoStart(host) {
host.starting = true;
host.armTerminalSessionStartTimeout();
}
test('auto-start without a token leaves connecting and shows the timeout error', (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const host = createTerminalHost();
autoStart(host);
t.mock.timers.tick(TERMINAL_SESSION_START_TIMEOUT_MS - 1);
assert.equal(host.starting, true);
assert.equal(host.connectionError, null);
t.mock.timers.tick(1);
assert.equal(host.starting, false);
assert.equal(host.connectionError, TERMINAL_CONNECTION_ERRORS.timeout);
assert.equal(host.sessionStartTimeoutId, null);
assert.deepEqual(host.toasts, [['error', TERMINAL_CONNECTION_ERRORS.timeout]]);
});
test('a token after auto-start replaces the timer and pty-ready clears it', (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const host = createTerminalHost();
autoStart(host);
t.mock.timers.tick(TERMINAL_SESSION_START_TIMEOUT_MS - 1000);
// send-terminal-token
host.beginTerminalSessionStart();
assert.equal(host.websocketStarts, 1);
// The auto-start timer was replaced, so its deadline passes without an error.
t.mock.timers.tick(2000);
assert.equal(host.starting, true);
assert.equal(host.connectionError, null);
// pty-ready
host.terminalActive = true;
host.completeTerminalSessionStart();
t.mock.timers.tick(TERMINAL_SESSION_START_TIMEOUT_MS * 2);
assert.equal(host.starting, false);
assert.equal(host.connectionError, null);
assert.equal(host.sessionStartTimeoutId, null);
assert.deepEqual(host.toasts, []);
});
test('a token that arrives after the timeout error still starts the session', (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const host = createTerminalHost();
autoStart(host);
t.mock.timers.tick(TERMINAL_SESSION_START_TIMEOUT_MS);
assert.equal(host.connectionError, TERMINAL_CONNECTION_ERRORS.timeout);
host.beginTerminalSessionStart();
assert.equal(host.starting, true);
assert.equal(host.connectionError, null);
host.terminalActive = true;
host.completeTerminalSessionStart();
assert.equal(host.starting, false);
assert.equal(host.sessionStartTimeoutId, null);
});
test('arming the timer twice keeps one timer only', (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const host = createTerminalHost();
autoStart(host);
host.armTerminalSessionStartTimeout();
// Teardown must clear the only timer; an orphaned first timer would still fire.
host.clearSessionStartTimeout();
t.mock.timers.tick(TERMINAL_SESSION_START_TIMEOUT_MS * 2);
assert.equal(host.starting, true);
assert.equal(host.connectionError, null);
assert.deepEqual(host.toasts, []);
});
test('a server-side start failure stops the spinner at once with its message', (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const host = createTerminalHost({ pendingCommand: { terminalToken: 'stale' } });
autoStart(host);
host.failTerminalSessionStart('The container is not running.');
assert.equal(host.starting, false);
assert.equal(host.connectionError, 'The container is not running.');
assert.equal(host.pendingCommand, null);
assert.equal(host.sessionStartTimeoutId, null);
t.mock.timers.tick(TERMINAL_SESSION_START_TIMEOUT_MS);
assert.deepEqual(host.toasts, [['error', 'The container is not running.']]);
});
test('a server-side start failure replaces an earlier timeout error', () => {
const host = createTerminalHost({ connectionError: TERMINAL_CONNECTION_ERRORS.timeout });
host.failTerminalSessionStart('No shell is available in this container.');
assert.equal(host.connectionError, 'No shell is available in this container.');
});
test('a server-side start failure without a message falls back to the generic error', () => {
const host = createTerminalHost();
host.failTerminalSessionStart(undefined);
assert.equal(host.connectionError, TERMINAL_CONNECTION_ERRORS.connectionFailed);
});
test('a cancelled auto-start waits for a container choice without an error', (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const host = createTerminalHost();
autoStart(host);
host.cancelTerminalAutoStart();
t.mock.timers.tick(TERMINAL_SESSION_START_TIMEOUT_MS * 2);
assert.equal(host.starting, false);
assert.equal(host.connectionError, null);
assert.equal(host.sessionStartTimeoutId, null);
assert.deepEqual(host.toasts, []);
});
test('a cancelled auto-start does not stop a session that is already starting', (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const host = createTerminalHost({ pendingCommand: { terminalToken: 'token' } });
autoStart(host);
host.cancelTerminalAutoStart();
assert.equal(host.starting, true);
assert.notEqual(host.sessionStartTimeoutId, null);
});