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
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"generate:schema": "tsx scripts/generate-schema.ts",
"type-check": "pnpm run paraglide:compile && tsc --noEmit",
"type-check:watch": "tsc --noEmit --watch",
"test": "pnpm run test:mocha && pnpm run test:playwright",
"test": "pnpm run test:mocha",
"test:mocha": "mocha",
"test:playwright": "pnpm exec playwright test",
"test:playwright:ui": "pnpm exec playwright test --ui",
Expand Down
109 changes: 109 additions & 0 deletions apps/web/tests/mocha/console-bus-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as chai from 'chai';
import 'mocha';
import { ConsoleBusService } from '../../src/console/services/console-bus-service';

const expect = chai.expect;

describe('ConsoleBusService', () => {
it('appends an entry to a channel and notifies subscribers', () => {
const bus = new ConsoleBusService();
const received: unknown[] = [];
bus.subscribe('test', (entries) => received.push(entries));

bus.append('test', 'out', 'Hello');

expect(received).to.have.lengthOf(2);
expect(received[1]).to.have.lengthOf(1);
expect((received[1] as Array<{ content: string; type: string }>)[0].content).to.equal('Hello');
expect((received[1] as Array<{ content: string; type: string }>)[0].type).to.equal('out');
});

it('subscribe callback receives the current state immediately', () => {
const bus = new ConsoleBusService();
bus.append('ch', 'out', 'first');

const received: unknown[] = [];
bus.subscribe('ch', (entries) => received.push(entries));

expect(received).to.have.lengthOf(1);
expect(received[0]).to.have.lengthOf(1);
expect((received[0] as Array<{ content: string }>)[0].content).to.equal('first');
});

it('clear empties a channel and notifies subscribers', () => {
const bus = new ConsoleBusService();
bus.append('ch', 'out', 'one');

const received: unknown[] = [];
bus.subscribe('ch', (entries) => received.push(entries));

bus.clear('ch');

expect(received[1]).to.have.lengthOf(0);
});

it('removeLastEntry removes the final entry', () => {
const bus = new ConsoleBusService();
bus.append('ch', 'out', 'one');
bus.append('ch', 'out', 'two');
bus.append('ch', 'out', 'three');

bus.removeLastEntry('ch');

const received: unknown[] = [];
bus.subscribe('ch', (entries) => received.push(entries));

expect(received[0]).to.have.lengthOf(2);
expect((received[0] as Array<{ content: string }>)[0].content).to.equal('one');
expect((received[0] as Array<{ content: string }>)[1].content).to.equal('two');
});

it('removeLastEntry does nothing on an empty channel', () => {
const bus = new ConsoleBusService();
bus.removeLastEntry('ch');

const received: unknown[] = [];
bus.subscribe('ch', (entries) => received.push(entries));

expect(received[0]).to.have.lengthOf(0);
});

it('caps entries at maxEntriesPerChannel', () => {
const bus = new ConsoleBusService(3);
bus.append('ch', 'out', 'a');
bus.append('ch', 'out', 'b');
bus.append('ch', 'out', 'c');
bus.append('ch', 'out', 'd');

const received: unknown[] = [];
bus.subscribe('ch', (entries) => received.push(entries));

expect(received[0]).to.have.lengthOf(3);
expect((received[0] as Array<{ content: string }>)[0].content).to.equal('b');
expect((received[0] as Array<{ content: string }>)[2].content).to.equal('d');
});

it('unsubscribes from channel updates', () => {
const bus = new ConsoleBusService();
const received: unknown[] = [];
const unsubscribe = bus.subscribe('ch', (entries) => received.push(entries));

unsubscribe();
bus.append('ch', 'out', 'one');

expect(received).to.have.lengthOf(1);
});

it('isolates channels from each other', () => {
const bus = new ConsoleBusService();
const ch1: unknown[] = [];
const ch2: unknown[] = [];
bus.subscribe('ch1', (e) => ch1.push(e));
bus.subscribe('ch2', (e) => ch2.push(e));

bus.append('ch1', 'out', 'hello');

expect(ch1).to.have.lengthOf(2);
expect(ch2).to.have.lengthOf(1);
});
});
72 changes: 72 additions & 0 deletions apps/web/tests/mocha/console-telemetry-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as chai from 'chai';
import 'mocha';
import { ConsoleTelemetryService } from '../../src/console/services/console-telemetry-service';
import type { ConsoleEntry } from '../../src/console/types/types';

const expect = chai.expect;

function entry(type: ConsoleEntry['type'], content: string): ConsoleEntry {
return { timestamp: new Date(), type, content };
}

describe('ConsoleTelemetryService', () => {
const service = new ConsoleTelemetryService(60_000);

it('createSnapshot returns empty state', () => {
const snapshot = service.createSnapshot();
expect(snapshot.historyEntries).to.deep.equal({});
expect(snapshot.latestEntries).to.deep.equal({});
});

it('extractKeyValuePairs parses latest values', () => {
const result = service.extractKeyValuePairs([entry('out', 'temp: 23.5')]);
expect(result.temp).to.exist;
expect(result.temp.value).to.equal(23.5);
});

it('extractKeyValueHistory returns history arrays', () => {
const result = service.extractKeyValueHistory([entry('out', 'temp: 23.5; humidity: 60')]);
expect(result.temp).to.have.lengthOf(1);
expect(result.humidity).to.have.lengthOf(1);
expect(result.temp[0].value).to.equal(23.5);
expect(result.humidity[0].value).to.equal(60);
});

it('ignores entries that are not out or err', () => {
const result = service.extractKeyValuePairs([
entry('in', 'temp: 23.5'),
entry('out', 'humidity: 60'),
]);
expect(result.temp).to.not.exist;
expect(result.humidity).to.exist;
});

it('appendTelemetry merges with existing snapshot', () => {
const snapshot = service.extractTelemetry([entry('out', 'a: 1')]);
const merged = service.appendTelemetry(snapshot, [entry('out', 'b: 2')]);

expect(merged.latestEntries.a.value).to.equal(1);
expect(merged.latestEntries.b.value).to.equal(2);
expect(merged.historyEntries.a).to.have.lengthOf(1);
expect(merged.historyEntries.b).to.have.lengthOf(1);
});

it('appendTelemetry with empty entries returns the same snapshot', () => {
const snapshot = service.extractTelemetry([entry('out', 'x: 1')]);
const result = service.appendTelemetry(snapshot, []);
expect(result).to.equal(snapshot);
});

it('handles multiple key-value pairs on one line', () => {
const result = service.extractKeyValuePairs([entry('out', 'x: 1; y: 2; z: 3')]);
expect(result.x.value).to.equal(1);
expect(result.y.value).to.equal(2);
expect(result.z.value).to.equal(3);
});

it('handles float and negative values', () => {
const result = service.extractKeyValuePairs([entry('out', 'temp: -5.5; ratio: 0.333')]);
expect(result.temp.value).to.equal(-5.5);
expect(result.ratio.value).to.equal(0.333);
});
});
10 changes: 0 additions & 10 deletions apps/web/tests/mocha/demo.test.ts

This file was deleted.

34 changes: 34 additions & 0 deletions apps/web/tests/mocha/key-value-history.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as chai from 'chai';
import 'mocha';
import { cloneHistoryMap } from '../../src/console/key-value/key-value-history';

const expect = chai.expect;

describe('cloneHistoryMap', () => {
it('creates a deep copy of the history map', () => {
const original = {
temp: [{ value: 23.5, timestamp: 1000 }],
humidity: [{ value: 60, timestamp: 2000 }],
};

const cloned = cloneHistoryMap(original);

expect(cloned).to.deep.equal(original);
expect(cloned).to.not.equal(original);
});

it('does not share array references with the original', () => {
const original = { temp: [{ value: 23.5, timestamp: 1000 }] };
const cloned = cloneHistoryMap(original);

cloned.temp.push({ value: 24, timestamp: 2000 });

expect(original.temp).to.have.lengthOf(1);
expect(cloned.temp).to.have.lengthOf(2);
});

it('handles empty history maps', () => {
const result = cloneHistoryMap({});
expect(result).to.deep.equal({});
});
});
66 changes: 66 additions & 0 deletions apps/web/tests/mocha/key-value-parser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as chai from 'chai';
import 'mocha';
import { parseKeyValue } from '../../src/console/key-value/key-value-parser';

const expect = chai.expect;

describe('parseKeyValue', () => {
it('parses a single key:value pair', () => {
const result = parseKeyValue('temp: 23.5');
expect(result.temp).to.exist;
expect(result.temp.value).to.equal(23.5);
expect(result.temp.timestamp).to.be.a('number');
});

it('parses multiple key:value pairs on one line', () => {
const result = parseKeyValue('temp: 23.5; humidity: 60.2');
expect(result.temp.value).to.equal(23.5);
expect(result.humidity.value).to.equal(60.2);
});

it('returns empty map for empty string', () => {
const result = parseKeyValue('');
expect(result).to.deep.equal({});
});

it('ignores invalid formats', () => {
const result = parseKeyValue('hello world');
expect(result).to.deep.equal({});
});

it('parses integer values', () => {
const result = parseKeyValue('count: 42');
expect(result.count.value).to.equal(42);
});

it('parses negative values', () => {
const result = parseKeyValue('offset: -10');
expect(result.offset.value).to.equal(-10);
});

it('ignores keys starting with a digit', () => {
const result = parseKeyValue('123key: 10');
expect(result['123key']).to.not.exist;
});

it('parses values across multiple lines', () => {
const result = parseKeyValue('temp: 23.5\nhumidity: 60.2');
expect(result.temp.value).to.equal(23.5);
expect(result.humidity.value).to.equal(60.2);
});

it('skips empty lines between entries', () => {
const result = parseKeyValue('temp: 23.5\n\nhumidity: 60.2');
expect(result.temp.value).to.equal(23.5);
expect(result.humidity.value).to.equal(60.2);
});

it('assigns the same timestamp to all parsed entries in a call', () => {
const before = Date.now();
const result = parseKeyValue('a: 1; b: 2');
const after = Date.now();
expect(result.a.timestamp).to.be.at.least(before);
expect(result.a.timestamp).to.be.at.most(after);
expect(result.b.timestamp).to.equal(result.a.timestamp);
});
});
44 changes: 44 additions & 0 deletions apps/web/tests/mocha/language.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as chai from 'chai';
import 'mocha';
import { inferLanguageFromPath } from '../../src/editor/services/language';

const expect = chai.expect;

describe('inferLanguageFromPath', () => {
it('returns typescript for .ts files', () => {
expect(inferLanguageFromPath('src/app.ts')).to.equal('typescript');
});

it('returns javascript for .js files', () => {
expect(inferLanguageFromPath('dist/bundle.js')).to.equal('javascript');
});

it('returns javascript for .mjs files', () => {
expect(inferLanguageFromPath('lib/utils.mjs')).to.equal('javascript');
});

it('returns javascript for .cjs files', () => {
expect(inferLanguageFromPath('lib/common.cjs')).to.equal('javascript');
});

it('returns json for .json files', () => {
expect(inferLanguageFromPath('package.json')).to.equal('json');
});

it('returns json for .jacly files', () => {
expect(inferLanguageFromPath('workspace.jacly')).to.equal('json');
});

it('returns plaintext for files without extension', () => {
expect(inferLanguageFromPath('README')).to.equal('plaintext');
});

it('returns plaintext for unknown extensions', () => {
expect(inferLanguageFromPath('data.csv')).to.equal('plaintext');
expect(inferLanguageFromPath('image.png')).to.equal('plaintext');
});

it('returns plaintext for empty path', () => {
expect(inferLanguageFromPath('')).to.equal('plaintext');
});
});
Loading
Loading