#!/usr/bin/gjs -m // Builds the preferences window for real, under real Adw. // // prefs.js runs in its own process, not in the compositor, so a nested-shell // check cannot reach it -- without this it is the one file in the extension // that never executes until a user opens it and finds it broken. // // The shell's own base class and gettext are stubbed rather than loaded: they // resolve an extension by walking the caller's URL up to a registered UUID, // which needs the whole extension manager. Stubbing them keeps the test about // this extension's code, which is the part that can be wrong. // // Run: gjs -m tests/test-prefs.js import Gio from 'gi://Gio'; import GLib from 'gi://GLib'; import Adw from 'gi://Adw'; Adw.init(); const HERE = GLib.path_get_dirname( GLib.filename_from_uri(import.meta.url)[0]); const EXT = GLib.path_get_dirname(HERE); const SHELL_PREFS = 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; const tmp = GLib.dir_make_tmp('ccs-prefs-XXXXXX'); const [, source] = GLib.file_get_contents(`${EXT}/prefs.js`); GLib.file_set_contents(`${tmp}/stub.js`, 'export class ExtensionPreferences {}\nexport const gettext = s => s;\n'); GLib.file_set_contents(`${tmp}/prefs.js`, new TextDecoder().decode(source).replace(SHELL_PREFS, './stub.js')); const schemas = Gio.SettingsSchemaSource.new_from_directory( `${EXT}/schemas`, Gio.SettingsSchemaSource.get_default(), false); const { default: Prefs } = await import(`file://${tmp}/prefs.js`); const prefs = new Prefs(); Object.defineProperty(prefs, 'path', { value: EXT }); // a getter upstream prefs.getSettings = () => new Gio.Settings({ settings_schema: schemas.lookup( 'org.gnome.shell.extensions.claude-code-status', true), }); const window = new Adw.PreferencesWindow(); prefs.fillPreferencesWindow(window); const rows = []; const walk = widget => { for (let c = widget.get_first_child?.(); c; c = c.get_next_sibling()) { const type = c.constructor.$gtype.name; if (type === 'AdwSwitchRow' || type === 'AdwSpinRow' || type === 'AdwActionRow') rows.push({ type, title: c.title, subtitle: c.subtitle }); walk(c); } }; walk(window); let failures = 0; function check(name, condition, detail = '') { if (!condition) failures++; print(`${condition ? 'ok ' : 'FAIL'} ${name}${detail ? ` (${detail})` : ''}`); } for (const row of rows) print(` ${row.type.replace('Adw', '').padEnd(10)} ${row.title}`); // One control per settings key, so a key added without a row to change it is // caught here rather than by a user wondering why nothing happens. const keys = schemas.lookup('org.gnome.shell.extensions.claude-code-status', true) .list_keys().length; const controls = rows.filter( r => r.type === 'AdwSwitchRow' || r.type === 'AdwSpinRow').length; check('a control for every settings key', controls === keys, `${controls} of ${keys}`); // The hook status line is the reason this page is worth opening at all: a // silent panel looks the same whether nothing runs or nothing is installed. const status = rows.find(r => r.title === 'Status'); check('hook status reported', !!status?.subtitle, status?.subtitle); check('status names the events it found', /SessionStart|Not installed/.test(status?.subtitle ?? ''), status?.subtitle); const dir = rows.find(r => r.title === 'State directory'); check('state directory shown', (dir?.subtitle ?? '').endsWith('claude-code-status'), dir?.subtitle); GLib.spawn_command_line_sync(`rm -rf ${tmp}`); print(failures ? `\n${failures} failure(s)` : '\nall passed'); imports.system.exit(failures ? 1 : 0);