Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 0 additions & 21 deletions docs/gallery.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,24 +128,3 @@ frame is captured, so the header reads `Rate: 0x`.

![A320 above Zermatt](screenshots/swiss-matterhorn-a320.png)
*A320 above Zermatt and the Matterhorn (light theme)*

![A350 over the Saas valley](screenshots/swiss-valais-a350.png)
*A350 over the Saas valley (light theme)*

![A380 above Macugnaga](screenshots/swiss-valais-a380.png)
*A380 above Macugnaga, with traffic over the Simplon (light theme)*

![Fleet over the Bernese Oberland](screenshots/swiss-jungfrau-fleet-light.png)
*Fleet over the Bernese Oberland (light theme)*

![A320 above Lauterbrunnen](screenshots/swiss-jungfrau-a320.png)
*A320 above Lauterbrunnen, Interlaken behind (light theme)*

![B747 above Meiringen](screenshots/swiss-oberland-b747.png)
*B747 above Meiringen and the Brienzersee (light theme)*

![Fleet over Lake Geneva](screenshots/swiss-leman-fleet-light.png)
*Fleet over Lake Geneva and the Rhône valley (light theme)*

![A350 above the Rhône valley](screenshots/swiss-leman-a350.png)
*A350 above the Rhône valley near Aigle (light theme)*
167 changes: 167 additions & 0 deletions frontend/src/ui/Console.keyboard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// @vitest-environment happy-dom
/**
* Tests for the console's two keyboard paths that go through
* setupEventListeners():
*
* - Enter must be an exact mirror of the "Send" button, i.e. route through
* the shared submitCurrent() path. Before the fix Enter duplicated the
* submit logic inline and skipped the trailing-separator trim, so a
* command finished via the map picker (which appends a comma after every
* click) was sent with a dangling `,`.
* - Tab completion must use the console's separator rules (spaces AND
* commas) and the cursor position. Before the fix it split on spaces
* only and always completed the last word, so the comma-separated form
* the app itself produces ("CRE KL123,A38") never completed.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { Console } from './Console';
import type { StateManager } from '../core/StateManager';

function setupDom(): void {
document.body.innerHTML = `
<div class="console-panel">
<button id="clear-console">Clear</button>
<div id="console-output"></div>
<div class="console-input-container">
<span class="console-prompt">BS&gt;</span>
<input type="text" id="console-input" placeholder="Enter command… (Ctrl+K to browse)" autocomplete="off">
<button id="send-command" class="console-btn">Send</button>
</div>
</div>
`;
}

function input(): HTMLInputElement {
return document.getElementById('console-input') as HTMLInputElement;
}

function press(key: string): void {
input().dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }));
}

/** Type a value and put the cursor at the given position (default: end). */
function type(value: string, cursor: number = value.length): void {
const el = input();
el.value = value;
el.setSelectionRange(cursor, cursor);
}

const CMDDICT = {
CRE: 'acid,type,lat,lon,hdg,alt,spd',
HDG: 'acid,hdg',
};

function stateManagerStub(): StateManager {
return {
getCommandDict: () => CMDDICT,
getState: () => ({ aircraftData: { id: [] } }),
} as unknown as StateManager;
}

describe('Console Enter key', () => {
const app = { sendCommand: vi.fn() };

beforeEach(() => {
localStorage.clear();
setupDom();
app.sendCommand.mockReset();
window.app = app as unknown as Window['app'];
new Console();
});

afterEach(() => {
delete window.app;
document.body.innerHTML = '';
});

it('sends the typed command and clears the input', () => {
type('MCRE 5');
press('Enter');
expect(app.sendCommand).toHaveBeenCalledWith('MCRE 5');
expect(input().value).toBe('');
});

it('trims trailing separators, matching the Send button', () => {
// The console map picker appends a comma after every click, so this
// is exactly what the input holds when a POLY is finished with Enter.
type('POLY TEST,52.3,4.2,53.0,4.2,52.6,5.2,');
press('Enter');
expect(app.sendCommand).toHaveBeenCalledWith(
'POLY TEST,52.3,4.2,53.0,4.2,52.6,5.2'
);
});

it('stores the trimmed command in the arrow-key history', () => {
type('MCRE 5, ');
press('Enter');
const saved = JSON.parse(
localStorage.getItem('webatm-console-command-history') ?? '[]'
);
expect(saved).toContain('MCRE 5');
});

it('does nothing for an empty input', () => {
type(' ');
press('Enter');
expect(app.sendCommand).not.toHaveBeenCalled();
});
});

describe('Console Tab completion', () => {
const app = { sendCommand: vi.fn() };
let konsole: Console;

beforeEach(() => {
localStorage.clear();
setupDom();
app.sendCommand.mockReset();
window.app = app as unknown as Window['app'];
konsole = new Console();
konsole.setStateManager(stateManagerStub());
});

afterEach(() => {
delete window.app;
document.body.innerHTML = '';
});

it('completes the aircraft type after a comma separator', () => {
type('CRE KL123,A38');
press('Tab');
expect(input().value).toBe('CRE KL123,A388 ');
});

it('completes the aircraft type after a space separator', () => {
type('CRE KL123 A38');
press('Tab');
expect(input().value).toBe('CRE KL123 A388 ');
});

it('completes the token under the cursor, keeping the tail', () => {
const value = 'CRE KL123,A38 52 4';
type(value, value.indexOf('A38') + 3); // cursor at end of "A38"
press('Tab');
expect(input().value).toBe('CRE KL123,A388 52 4');
});

it('completes a unique command prefix from cmddict', () => {
type('HD');
press('Tab');
expect(input().value).toBe('HDG ');
});

it('echoes the alternatives when several types match', () => {
type('CRE KL123,A32');
press('Tab');
expect(input().value).toBe('CRE KL123,A32'); // unchanged
const output = document.getElementById('console-output');
expect(output?.textContent).toContain('A320');
expect(output?.textContent).toContain('A321');
});

it('does not complete types for non-CRE commands', () => {
type('HDG KL123,A38');
press('Tab');
expect(input().value).toBe('HDG KL123,A38');
});
});
Loading
Loading