diff --git a/README.md b/README.md index 8b88e1d..063e596 100644 --- a/README.md +++ b/README.md @@ -209,9 +209,14 @@ gnome-extensions enable claude-code-status@git.vakhrushev.me ```sh gjs -m tests/test-sessions.js # чтение состояния, порядок, живость, слежение gjs -m tests/test-abbrev.js # метки чипов: сжатие, коллизии, липкость +gjs -m tests/test-prefs.js # окно настроек строится и находит хуки tests/test-hook.sh # события -> состояния, блокировка, снос файлов ``` +`prefs.js` живёт в отдельном процессе, а не в композиторе, поэтому проверка во +вложенном шелле до него не достаёт — без этого теста он единственный файл, +который впервые исполняется у пользователя. + `lib/sessions.js` намеренно ничего не импортирует из `resource:///org/gnome/shell`, а `lib/abbrev.js` — вообще ничего. Именно это позволяет гонять их вне композитора — в `gjs`, а `abbrev` и в `node`. diff --git a/tests/test-prefs.js b/tests/test-prefs.js new file mode 100644 index 0000000..fdd8023 --- /dev/null +++ b/tests/test-prefs.js @@ -0,0 +1,87 @@ +#!/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 === '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 switch per settings key, so a key added without a row is caught. +const keys = schemas.lookup('org.gnome.shell.extensions.claude-code-status', true) + .list_keys().length; +const switches = rows.filter(r => r.type === 'AdwSwitchRow').length; +check('a switch for every settings key', switches === keys, `${switches} 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);