import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import vm from 'node:vm'; const source = readFileSync(new URL('../apps/gnome-extension/jarvis@qvac.local/extension.js', import.meta.url), 'utf8'); function harness() { const timers = new Map(); class Actor { constructor(props = {}) { if ('hexpand' in props) throw new Error('No property hexpand on StWidget'); Object.assign(this, props); this.children = []; this.visible = props.visible !== false; this.clutter_text = { connect() {}, ellipsize: null }; } add_child(child) { this.children.push(child); } destroy_all_children() { this.children = []; } get_n_children() { return this.children.length; } contains() { return false; } get_last_child() { return this.children[this.children.length - 1] || null; } connect() { return 1; } hide() { this.visible = false; } show() { this.visible = true; } destroy() { this.destroyed = true; } set_position() {} grab_key_focus() {} set_style() {} add_style_class_name() {} } const context = vm.createContext({ Extension: class {}, global: { stage: { get_key_focus() { return null; } } }, St: { BoxLayout: Actor, Label: Actor, Widget: Actor, Button: Actor, Entry: Actor, ScrollView: Actor, PolicyType: { NEVER: 0, AUTOMATIC: 1 } }, Clutter: { ActorAlign: { CENTER: 0, START: 1 }, EVENT_STOP: 1, EVENT_PROPAGATE: 0 }, Pango: { WrapMode: { WORD_CHAR: 2 }, EllipsizeMode: { NONE: 0, END: 3 } }, Main: { layoutManager: { addChrome() {} } }, GLib: { PRIORITY_DEFAULT: 0, SOURCE_REMOVE: false, timeout_add(_priority, _delay, callback) { const id = timers.size + 1; timers.set(id, callback); return id; }, Source: { remove(id) { timers.delete(id); } }, }, log() {}, }); vm.runInContext(source.replace(/^import .*;\n/gm, '').replace('export default class', 'class') + '\nglobalThis.classes = { ArcOverlay, JarvisExtension, JarvisProxy };', context); return { ...context.classes, timers }; } test('overlay constructs and destroys with pending halo animation', () => { const { ArcOverlay, timers } = harness(); const overlay = new ArcOverlay(); assert.equal(overlay.header.children[1].x_expand, true); overlay.attach(); overlay.showHalo(); overlay.showHalo(); assert.equal(timers.size, 1); overlay.destroy(); assert.equal(timers.size, 0); }); test('empty chrome widgets start hidden', () => { const { ArcOverlay } = harness(); const overlay = new ArcOverlay(); assert.equal(overlay.job.visible, false); assert.equal(overlay.target.visible, false); assert.equal(overlay.cursor.visible, false); assert.equal(overlay.wave.visible, false); }); test('Reply finalizes a streaming row instead of duplicating [object Object]', () => { const { ArcOverlay } = harness(); const overlay = new ArcOverlay(); overlay.token('Hello! I am ready.'); overlay.finalizeReply('[object Object]'); overlay.finalizeReply({ text: 'ignored duplicate' }); assert.equal(overlay.transcript.children.length, 1); assert.match(overlay.transcript.children[0].text, /Hello! I am ready/); assert.doesNotMatch(overlay.transcript.children[0].text, /object Object/); }); test('addRow and token coerce objects to readable text', () => { const { ArcOverlay } = harness(); const overlay = new ArcOverlay(); overlay.addRow('U', { text: 'Hi there' }); overlay.token({ text: 'Hello from Jarvis' }); assert.equal(overlay.transcript.children.length, 2); assert.match(overlay.transcript.children[0].text, /Hi there/); assert.match(overlay.transcript.children[1].text, /Hello from Jarvis/); assert.doesNotMatch(overlay.transcript.children.map((row) => row.text).join('\n'), /object Object/); }); for (const fails of [false, true]) { test('daemon connection finishing after disable is ignored: ' + (fails ? 'failure' : 'success'), async () => { const { JarvisExtension } = harness(); const extension = new JarvisExtension(); let finish; extension.proxy = { connect: () => new Promise((resolve, reject) => { finish = fails ? reject : resolve; }) }; const pending = extension._connectDaemon(); extension.proxy = null; extension.overlay = null; finish(fails ? new Error('Disconnected') : undefined); await pending; }); } test('GNOME GI imports expose default namespaces and panel has a real menu', () => { assert.match(source, /import Shell from 'gi:\/\/Shell'/); assert.match(source, /import Meta from 'gi:\/\/Meta'/); assert.match(source, /import Pango from 'gi:\/\/Pango'/); assert.match(source, /new PanelMenu.Button\(0.0, 'Jarvis QVAC', false\)/); assert.match(source, /PushToTalk/); assert.match(source, /finalizeReply/); });