// @vitest-environment jsdom /** * test/grid-presets.test.ts — saved split-grid layout presets + dropdown. * * Covers persistence (round-trip, garbage tolerance, malformed drop) and the * dropdown behavior: empty state, save current, apply (calls back), delete. */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { loadPresets, savePresets, mountGridPresets, type GridPreset } from '../public/grid-presets.js' const KEY = 'web-terminal:grid-presets' const CURRENT: { layout: GridPreset['layout']; split: GridPreset['split'] } = { layout: 'grid-4', split: { cols: [1.5, 0.5], rows: [1, 1] }, } beforeEach(() => localStorage.clear()) describe('grid-presets: persistence', () => { it('round-trips presets through localStorage', () => { const list: GridPreset[] = [{ name: 'wide-left', layout: 'grid-4', split: CURRENT.split }] savePresets(list) expect(loadPresets()).toEqual(list) }) it('returns [] for missing or garbage storage', () => { expect(loadPresets()).toEqual([]) localStorage.setItem(KEY, 'not json') expect(loadPresets()).toEqual([]) }) it('drops malformed entries', () => { localStorage.setItem( KEY, JSON.stringify([ { name: 'ok', layout: 'grid-4', split: { cols: [1, 1], rows: [1, 1] } }, { name: 'bad-layout', layout: 'nope', split: {} }, { nope: true }, ]), ) const out = loadPresets() expect(out).toHaveLength(1) expect(out[0]!.name).toBe('ok') }) }) describe('grid-presets: dropdown', () => { let toolbar: HTMLElement let apply: ReturnType const mount = () => { apply = vi.fn() return mountGridPresets(toolbar, { current: () => CURRENT, apply }) } beforeEach(() => { toolbar = document.createElement('div') document.body.replaceChildren(toolbar) }) const openMenu = (): HTMLElement => { toolbar.querySelector('.grid-presets-btn')!.click() return toolbar.querySelector('.grid-presets-menu')! } it('shows an empty state when there are no presets', () => { mount() const menu = openMenu() expect(menu.hidden).toBe(false) expect(menu.querySelector('.gp-empty')).not.toBeNull() }) it('saves the current arrangement under a typed name', () => { mount() openMenu() const input = toolbar.querySelector('.gp-name')! input.value = 'wide-left' toolbar.querySelector('.gp-save-btn')!.click() expect(loadPresets()).toEqual([{ name: 'wide-left', layout: 'grid-4', split: CURRENT.split }]) expect(toolbar.querySelector('.gp-apply-name')?.textContent).toBe('wide-left') }) it('does not save a blank name', () => { mount() openMenu() toolbar.querySelector('.gp-save-btn')!.click() expect(loadPresets()).toEqual([]) }) it('applies a preset (calls back) and closes the menu', () => { savePresets([{ name: 'p1', layout: 'grid-4', split: CURRENT.split }]) mount() const menu = openMenu() toolbar.querySelector('.gp-apply')!.click() expect(apply).toHaveBeenCalledWith({ name: 'p1', layout: 'grid-4', split: CURRENT.split }) expect(menu.hidden).toBe(true) }) it('deletes a preset', () => { savePresets([{ name: 'p1', layout: 'grid-4', split: CURRENT.split }]) mount() openMenu() toolbar.querySelector('.gp-del')!.click() expect(loadPresets()).toEqual([]) expect(toolbar.querySelector('.gp-empty')).not.toBeNull() }) it('closes on an outside pointerdown', () => { mount() const menu = openMenu() expect(menu.hidden).toBe(false) document.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true })) expect(menu.hidden).toBe(true) }) it('closes on Escape', () => { mount() const menu = openMenu() document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })) expect(menu.hidden).toBe(true) }) it('replaces a same-named preset instead of duplicating', () => { savePresets([{ name: 'dup', layout: 'split-2', split: { cols: [1, 1], rows: [1] } }]) mount() openMenu() const input = toolbar.querySelector('.gp-name')! input.value = 'dup' toolbar.querySelector('.gp-save-btn')!.click() const out = loadPresets() expect(out).toHaveLength(1) expect(out[0]!.layout).toBe('grid-4') // overwritten with the current arrangement }) })