Files
Codeman/test/scheduled-layout-registration.test.ts
Aamer Akhter 5fb56b0889 fix(tabs): put child follow behind a setting, stop unpinning children on order PUTs
Review fixes for "place new, child and web tabs into groups".

- Spawned tabs follow their parent only with the new synced setting
  spawnedTabsFollowParent (App Settings > Appearance > Tabs, off by default).
  TabLayoutService.sessionCreated() reads it fresh through a
  childrenFollowParent dep: off, a spawned session lands after its parent and
  is marked manual in the same write; on, it follows until moved by hand. The
  row menu shows the placement note and "Follow parent again" only when on.
  "New session" in a group menu is unaffected. zh-CN label and a
  Settings-Reference row.
- applyLegacySessionRank marks a child manual only when the requested order
  splits it from its parent's block. A PUT of the current order, or one that
  moves a family together, leaves every stored placement alone.
- The browser never echoes a create or delete through PUT /api/session-order,
  with or without groups: the layout service already re-projects the order and
  broadcasts it, and syncSessionOrder adopts it.
- The placement note in the row menu is plain text described by the menu, not
  a focusable menu item.
- A tab that first appears inside a collapsed group briefly marks that group's
  header.
- i18n skips .tab-name, so tab names are never translated.
- Tests: legacy-order cases for an unchanged order, a family reorder and a
  split; service cases for the setting and for order PUTs; the scheduled
  registration test now exercises register/undo; the grouped-create test binds
  port 0; one route case runs through the real layout service.
2026-10-09 20:34:37 -04:00

51 lines
2.3 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { readFileSync } from 'node:fs';
import { WebServer } from '../src/web/server.js';
describe('scheduled session layout registration', () => {
it('registers the layout before lifecycle work and rolls back a rejected tentative session', async () => {
const sessions = new Map<string, { id: string; owner?: string }>();
const session = { id: 'scheduled-session', owner: 'alice' };
let registeredBeforeFailure = false;
// The layout service registers the session inside its owner lock, then the
// write fails and it calls the undo it was handed.
const sessionCreated = vi.fn(
async (_owner: string, _id: string, _placement: unknown, register: () => void | (() => void)) => {
const undo = register();
registeredBeforeFailure = sessions.get(session.id) === session;
if (typeof undo === 'function') undo();
throw new Error('layout unavailable');
}
);
const server = Object.create(WebServer.prototype) as {
sessions: typeof sessions;
tabLayouts: { sessionCreated: typeof sessionCreated };
registerSessionWithLayout(session: typeof session): Promise<void>;
};
server.sessions = sessions;
server.tabLayouts = { sessionCreated };
await expect(server.registerSessionWithLayout(session)).rejects.toThrow('layout unavailable');
expect(sessionCreated).toHaveBeenCalledWith('alice', 'scheduled-session', undefined, expect.any(Function));
expect(registeredBeforeFailure).toBe(true);
expect(sessions.has(session.id)).toBe(false);
});
it('uses the shared registration helper in the scheduled loop before persistence and listeners', () => {
const source = readFileSync(new URL('../src/web/server.ts', import.meta.url), 'utf8');
const loop = source.slice(
source.indexOf('private async runScheduledLoop'),
source.indexOf('private async stopScheduledRun')
);
expect(loop).toContain('await this.registerSessionWithLayout(session);');
expect(loop.indexOf('await this.registerSessionWithLayout(session);')).toBeLessThan(
loop.indexOf('this.store.incrementSessionsCreated();')
);
expect(loop.indexOf('await this.registerSessionWithLayout(session);')).toBeLessThan(
loop.indexOf('await this.setupSessionListeners(session);')
);
});
});