mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Three things, all on the same surface. **Tighter.** The shell drops to 760x620 (was 840x700) and the density comes down with it: rail 176px, doc padding 15px, row padding 5px 10px, group gaps 3px, section head 0.88rem, row label 0.76rem, description 0.645rem. The model cards were the biggest block in the document and shrink the most (6px 8px padding, 0.72rem name). The toggle switches keep their size on purpose - only the space around them was the problem. **Checkboxes stay checkboxes.** The respawn cycle steps go back to real checkboxes in a row card (`.set-checks` / `.set-check`) rather than the chips they briefly became: they are numbered steps of one sequence, not a set of independent tags, and chips read as the latter. **Add Case joins the surface.** Same shell, rail and sections; its rail switches panels like Session Options'. The six panels keep their legacy `.form-row` markup - every id in them is read back by session-ui.js, so restructuring the forms would be a lot of risk for no visual gain. Instead an adapter block scoped to `#createCaseModal .set-doc` maps the old primitives onto the look: a form row paints as a row card, its label as a row label, its `.form-hint` as a row description, `<details class="advanced-options">` as a collapsed group head. `.form-row` everywhere else is untouched. With that, `.modal-tabs` / `.modal-tab-btn` / `.modal-tab-content` have no users left, so their CSS is deleted from both stylesheets and the guard in test/app-settings-structure.test.ts flips from "the settings modal must not steal these shared classes" to "nothing uses them any more" - a reappearance now means a modal drifted back off the shared surface.
98 lines
4.7 KiB
TypeScript
98 lines
4.7 KiB
TypeScript
/**
|
|
* Session Options structural guard.
|
|
*
|
|
* The modal shares the `set-*` settings surface with App Settings, but its rail
|
|
* is a real switcher: switchOptionsTab shows one `.set-section` and hides the
|
|
* rest. Like App Settings, its load/save path is `getElementById` by a fixed set
|
|
* of ids, so dropping or renaming an element in the markup fails silently — the
|
|
* option just stops loading, or stops being written back.
|
|
*
|
|
* These tests read the REAL session-ui.js and index.html and pin that contract.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
|
|
const publicDir = resolve(import.meta.dirname, '../src/web/public');
|
|
const html = readFileSync(resolve(publicDir, 'index.html'), 'utf8');
|
|
const sessionUi = readFileSync(resolve(publicDir, 'session-ui.js'), 'utf8');
|
|
|
|
/** The Session Options markup, so assertions can't be satisfied elsewhere. */
|
|
function optionsModal(): string {
|
|
const start = html.indexOf('<div class="modal" id="sessionOptionsModal">');
|
|
expect(start).toBeGreaterThan(-1);
|
|
const end = html.indexOf('<!-- Close Session Confirmation Modal -->', start);
|
|
expect(end).toBeGreaterThan(start);
|
|
return html.slice(start, end);
|
|
}
|
|
|
|
/** Body of a session-ui.js method, by name. */
|
|
function methodBody(signature: string): string {
|
|
const start = sessionUi.indexOf(`\n ${signature} {`);
|
|
expect(start, `${signature} not found in session-ui.js`).toBeGreaterThan(-1);
|
|
return sessionUi.slice(start, sessionUi.indexOf('\n },', start));
|
|
}
|
|
|
|
const TABS = ['respawn', 'context', 'ralph', 'summary'];
|
|
|
|
describe('Session Options modal structure', () => {
|
|
it('keeps every element openSessionOptions and switchOptionsTab touch by id', () => {
|
|
const modal = optionsModal();
|
|
const ids = new Set<string>();
|
|
for (const sig of ['openSessionOptions(sessionId)', 'switchOptionsTab(tabName)', 'getRalphConfig()']) {
|
|
for (const m of methodBody(sig).matchAll(/getElementById\('([A-Za-z0-9_-]+)'\)/g)) ids.add(m[1]);
|
|
}
|
|
// openSessionOptions also drives elements outside this modal (tabs, toasts);
|
|
// only the ones it expects to find in here are this file's contract.
|
|
const outside = new Set(['sessionOptionsDoc']);
|
|
const missing = [...ids].filter((id) => !outside.has(id) && !modal.includes(`id="${id}"`));
|
|
expect(missing).toEqual([]);
|
|
expect(modal).toContain('id="sessionOptionsDoc"');
|
|
});
|
|
|
|
it('pairs each rail entry with exactly one section, in the same order', () => {
|
|
const modal = optionsModal();
|
|
const rail = [...modal.matchAll(/class="set-rail-item[^"]*" data-tab="([a-z]+)"/g)].map((m) => m[1]);
|
|
expect(rail).toEqual(TABS);
|
|
for (const tab of TABS) {
|
|
const hits = modal.split(`id="${tab}-tab"`).length - 1;
|
|
expect(hits, `section ${tab}-tab should exist exactly once`).toBe(1);
|
|
}
|
|
// switchOptionsTab queries the rail by THIS class; `.modal-tab-btn` here
|
|
// would silently stop the active marker from moving.
|
|
expect(methodBody('switchOptionsTab(tabName)')).toContain("'#sessionOptionsModal .set-rail-item'");
|
|
expect(methodBody('openSessionOptions(sessionId)')).toContain('.set-rail-item[data-tab="ralph"]');
|
|
});
|
|
|
|
it('opens with exactly one section visible, the rest hidden', () => {
|
|
const modal = optionsModal();
|
|
const visible = TABS.filter((t) => modal.includes(`<section class="set-section" id="${t}-tab"`));
|
|
expect(visible).toEqual(['respawn']);
|
|
for (const t of TABS.filter((t) => t !== 'respawn')) {
|
|
expect(modal).toContain(`<section class="set-section hidden" id="${t}-tab"`);
|
|
}
|
|
});
|
|
|
|
it('keeps the Claude-only rail entries marked, so external CLIs lose them', () => {
|
|
const modal = optionsModal();
|
|
for (const tab of ['respawn', 'ralph']) {
|
|
const entry = modal.match(new RegExp(`<button[^>]*data-tab="${tab}"[^>]*>`))?.[0] ?? '';
|
|
expect(entry, `${tab} rail entry`).toContain('data-claude-only');
|
|
}
|
|
expect(modal.match(/<button[^>]*data-tab="context"[^>]*>/)?.[0]).not.toContain('data-claude-only');
|
|
});
|
|
|
|
it('uses the shared settings surface rather than the modal-tab chrome', () => {
|
|
const modal = optionsModal();
|
|
expect(modal).toContain('class="modal-content modal-lg set-shell"');
|
|
expect(modal).toContain('class="set-body"');
|
|
expect(modal).not.toContain('class="modal-tabs"');
|
|
expect(modal).not.toContain('modal-tab-btn');
|
|
expect(modal).not.toContain('modal-tab-content');
|
|
// The `set-*` rules are shared by both modals through one :is() scope.
|
|
const css = readFileSync(resolve(publicDir, 'styles.css'), 'utf8');
|
|
expect(css).toContain(':is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-row {');
|
|
expect(css).toContain(':is(#sessionOptionsModal, #createCaseModal) .set-section.hidden {');
|
|
});
|
|
});
|