import { describe, expect, it } from 'vitest'; import type { OpenDocRequest } from '../workspace'; import { activeDocument, addTab, closeAllOfFile, closeOthers, closeTab, closeToLeft, closeToRight, createTile, cycleActiveTab, ensureTile, findEmptyTab, findTabForRequest, getActiveSlot, getActiveTabId, getTabs, moveTab, planOpen, setActiveTab, setTabSlot, type TileTabsMap, } from './tabsModel'; function request(entryId: string, providerId = 'fs'): OpenDocRequest { return { providerId, entryId, title: entryId }; } /** A tile with the given tab ids, each holding a `loading` slot for its entry. */ function tileWithDocs( tileId: string, entries: string[], activeEntry = entries[0], ): TileTabsMap { return { [tileId]: { tabs: entries.map((entryId) => ({ tabId: `tab-${entryId}`, slot: { kind: 'loading', request: request(entryId) }, mode: 'edit' as const, })), activeTabId: `tab-${activeEntry}`, }, }; } describe('createTile / ensureTile', () => { it('creates a tile with one empty active tab', () => { expect(createTile('tab-1')).toEqual({ tabs: [{ tabId: 'tab-1', slot: { kind: 'empty' }, mode: 'edit' }], activeTabId: 'tab-1', }); }); it('ensureTile adds a missing tile', () => { const next = ensureTile({}, 'tile-1', 'tab-1'); expect(getActiveTabId(next, 'tile-1')).toBe('tab-1'); expect(getActiveSlot(next, 'tile-1')).toEqual({ kind: 'empty' }); }); it('ensureTile is a no-op (same reference) when the tile exists', () => { const map = ensureTile({}, 'tile-1', 'tab-1'); expect(ensureTile(map, 'tile-1', 'tab-999')).toBe(map); }); }); describe('getActiveSlot', () => { it('returns the active tab slot', () => { const map = tileWithDocs('tile-1', ['a', 'b'], 'b'); expect(getActiveSlot(map, 'tile-1')).toEqual({ kind: 'loading', request: request('b'), }); }); it('returns empty for an unknown tile', () => { expect(getActiveSlot({}, 'nope')).toEqual({ kind: 'empty' }); }); }); describe('setActiveTab', () => { it('activates an existing tab', () => { const map = tileWithDocs('tile-1', ['a', 'b'], 'a'); const next = setActiveTab(map, 'tile-1', 'tab-b'); expect(getActiveTabId(next, 'tile-1')).toBe('tab-b'); }); it('is a no-op (same reference) for a missing tab or already-active tab', () => { const map = tileWithDocs('tile-1', ['a', 'b'], 'a'); expect(setActiveTab(map, 'tile-1', 'missing')).toBe(map); expect(setActiveTab(map, 'tile-1', 'tab-a')).toBe(map); }); }); describe('addTab', () => { it('appends an empty tab and activates it', () => { const map = tileWithDocs('tile-1', ['a'], 'a'); const next = addTab(map, 'tile-1', 'tab-new'); expect(getTabs(next, 'tile-1').map((t) => t.tabId)).toEqual([ 'tab-a', 'tab-new', ]); expect(getActiveTabId(next, 'tile-1')).toBe('tab-new'); expect(getActiveSlot(next, 'tile-1')).toEqual({ kind: 'empty' }); }); it('creates the tile when it is missing', () => { const next = addTab({}, 'tile-1', 'tab-1'); expect(getActiveTabId(next, 'tile-1')).toBe('tab-1'); }); }); describe('setTabSlot', () => { it('updates only the targeted tab', () => { const map = tileWithDocs('tile-1', ['a', 'b'], 'a'); const next = setTabSlot(map, 'tile-1', 'tab-b', { kind: 'empty' }); expect(getTabs(next, 'tile-1')[0].slot).toEqual({ kind: 'loading', request: request('a'), }); expect(getTabs(next, 'tile-1')[1].slot).toEqual({ kind: 'empty' }); }); it('is a no-op (same reference) for a missing tab', () => { const map = tileWithDocs('tile-1', ['a'], 'a'); expect(setTabSlot(map, 'tile-1', 'missing', { kind: 'empty' })).toBe(map); }); }); describe('findTabForRequest', () => { it('finds a tab already showing the requested document', () => { const map = tileWithDocs('tile-1', ['a', 'b'], 'a'); expect(findTabForRequest(map, 'tile-1', request('b'))).toBe('tab-b'); }); it('returns null when no tab matches', () => { const map = tileWithDocs('tile-1', ['a'], 'a'); expect(findTabForRequest(map, 'tile-1', request('z'))).toBeNull(); }); it('distinguishes by provider', () => { const map = tileWithDocs('tile-1', ['a'], 'a'); expect(findTabForRequest(map, 'tile-1', request('a', 'other'))).toBeNull(); }); it('ignores empty tabs', () => { const map = addTab( tileWithDocs('tile-1', ['a'], 'a'), 'tile-1', 'tab-blank', ); expect(findTabForRequest(map, 'tile-1', request('a'))).toBe('tab-a'); }); }); describe('planOpen', () => { const makeId = () => 'tab-new'; it('focuses an existing tab without creating a new one', () => { const map = tileWithDocs('tile-1', ['a', 'b'], 'a'); const plan = planOpen(map, 'tile-1', request('b'), makeId); expect(plan.alreadyOpen).toBe(true); expect(plan.tabId).toBe('tab-b'); expect(getActiveTabId(plan.map, 'tile-1')).toBe('tab-b'); expect(getTabs(plan.map, 'tile-1')).toHaveLength(2); }); it('reuses the active empty start tab', () => { const map = ensureTile({}, 'tile-1', 'tab-1'); const plan = planOpen(map, 'tile-1', request('a'), makeId); expect(plan.alreadyOpen).toBe(false); expect(plan.tabId).toBe('tab-1'); expect(getTabs(plan.map, 'tile-1')).toHaveLength(1); }); it('appends a new tab when the active tab holds a document', () => { const map = tileWithDocs('tile-1', ['a'], 'a'); const plan = planOpen(map, 'tile-1', request('b'), makeId); expect(plan.alreadyOpen).toBe(false); expect(plan.tabId).toBe('tab-new'); expect(getTabs(plan.map, 'tile-1').map((t) => t.tabId)).toEqual([ 'tab-a', 'tab-new', ]); expect(getActiveTabId(plan.map, 'tile-1')).toBe('tab-new'); }); it('creates the tile when it does not exist yet', () => { const plan = planOpen({}, 'tile-1', request('a'), makeId); expect(plan.tabId).toBe('tab-new'); expect(getActiveTabId(plan.map, 'tile-1')).toBe('tab-new'); }); }); describe('cycleActiveTab', () => { it('moves to the next tab and wraps around', () => { const map = tileWithDocs('tile-1', ['a', 'b', 'c'], 'c'); const next = cycleActiveTab(map, 'tile-1', 1); expect(getActiveTabId(next, 'tile-1')).toBe('tab-a'); }); it('moves to the previous tab and wraps around', () => { const map = tileWithDocs('tile-1', ['a', 'b', 'c'], 'a'); const prev = cycleActiveTab(map, 'tile-1', -1); expect(getActiveTabId(prev, 'tile-1')).toBe('tab-c'); }); it('is a no-op (same reference) for a single tab or missing tile', () => { const map = tileWithDocs('tile-1', ['a'], 'a'); expect(cycleActiveTab(map, 'tile-1', 1)).toBe(map); expect(cycleActiveTab(map, 'other', 1)).toBe(map); }); }); describe('closeOthers', () => { it('keeps only the anchor tab and removes the rest', () => { const map = tileWithDocs('tile-1', ['a', 'b', 'c'], 'a'); const res = closeOthers(map, 'tile-1', 'tab-b'); expect(res.removedTabIds.sort()).toEqual(['tab-a', 'tab-c']); expect(getTabs(res.map, 'tile-1').map((t) => t.tabId)).toEqual(['tab-b']); expect(getActiveTabId(res.map, 'tile-1')).toBe('tab-b'); }); it('is a no-op when the anchor is the only tab', () => { const map = tileWithDocs('tile-1', ['a'], 'a'); expect(closeOthers(map, 'tile-1', 'tab-a')).toEqual({ map, removedTabIds: [], }); }); }); describe('closeToRight / closeToLeft', () => { it('closes tabs after the anchor and keeps active valid', () => { const map = tileWithDocs('tile-1', ['a', 'b', 'c', 'd'], 'd'); const res = closeToRight(map, 'tile-1', 'tab-b'); expect(res.removedTabIds).toEqual(['tab-c', 'tab-d']); expect(getTabs(res.map, 'tile-1').map((t) => t.tabId)).toEqual([ 'tab-a', 'tab-b', ]); // active 'tab-d' was removed -> falls back to the anchor. expect(getActiveTabId(res.map, 'tile-1')).toBe('tab-b'); }); it('closes tabs before the anchor', () => { const map = tileWithDocs('tile-1', ['a', 'b', 'c'], 'b'); const res = closeToLeft(map, 'tile-1', 'tab-b'); expect(res.removedTabIds).toEqual(['tab-a']); expect(getTabs(res.map, 'tile-1').map((t) => t.tabId)).toEqual([ 'tab-b', 'tab-c', ]); expect(getActiveTabId(res.map, 'tile-1')).toBe('tab-b'); }); it('is a no-op at the edges', () => { const map = tileWithDocs('tile-1', ['a', 'b'], 'a'); expect(closeToRight(map, 'tile-1', 'tab-b').removedTabIds).toEqual([]); expect(closeToLeft(map, 'tile-1', 'tab-a').removedTabIds).toEqual([]); }); }); describe('closeAllOfFile', () => { it('removes the document from every tile and reports emptied tiles', () => { const map: TileTabsMap = { ...tileWithDocs('tile-1', ['a', 'shared'], 'shared'), ...tileWithDocs('tile-2', ['shared'], 'shared'), }; const res = closeAllOfFile(map, request('shared')); expect(res.removedTabIds.sort()).toEqual(['tab-shared', 'tab-shared']); // tile-1 keeps 'a'; tile-2 is emptied. expect(getTabs(res.map, 'tile-1').map((t) => t.tabId)).toEqual(['tab-a']); expect(getActiveTabId(res.map, 'tile-1')).toBe('tab-a'); expect(res.map['tile-2']).toBeUndefined(); expect(res.emptiedTileIds).toEqual(['tile-2']); }); it('is a no-op when no tab shows the document', () => { const map = tileWithDocs('tile-1', ['a'], 'a'); const res = closeAllOfFile(map, request('z')); expect(res.removedTabIds).toEqual([]); expect(res.emptiedTileIds).toEqual([]); }); }); describe('moveTab', () => { it('reorders within a tile, inserting before the target', () => { const map = tileWithDocs('tile-1', ['a', 'b', 'c'], 'a'); const res = moveTab(map, 'tile-1', 'tab-c', 'tile-1', 'tab-a'); expect(res.sourceEmptied).toBe(false); expect(getTabs(res.map, 'tile-1').map((t) => t.tabId)).toEqual([ 'tab-c', 'tab-a', 'tab-b', ]); expect(getActiveTabId(res.map, 'tile-1')).toBe('tab-c'); }); it('reorders to the end when beforeTabId is null', () => { const map = tileWithDocs('tile-1', ['a', 'b', 'c'], 'a'); const res = moveTab(map, 'tile-1', 'tab-a', 'tile-1', null); expect(getTabs(res.map, 'tile-1').map((t) => t.tabId)).toEqual([ 'tab-b', 'tab-c', 'tab-a', ]); }); it('is a no-op when dropping a tab on itself', () => { const map = tileWithDocs('tile-1', ['a', 'b'], 'a'); expect(moveTab(map, 'tile-1', 'tab-a', 'tile-1', 'tab-a').map).toBe(map); }); it('moves a tab to another tile and activates it there', () => { const map: TileTabsMap = { ...tileWithDocs('tile-1', ['a', 'b'], 'a'), ...tileWithDocs('tile-2', ['c'], 'c'), }; const res = moveTab(map, 'tile-1', 'tab-b', 'tile-2', 'tab-c'); expect(res.sourceEmptied).toBe(false); expect(getTabs(res.map, 'tile-1').map((t) => t.tabId)).toEqual(['tab-a']); expect(getTabs(res.map, 'tile-2').map((t) => t.tabId)).toEqual([ 'tab-b', 'tab-c', ]); expect(getActiveTabId(res.map, 'tile-2')).toBe('tab-b'); }); it('creates the destination tile when it does not exist yet', () => { const map = tileWithDocs('tile-1', ['a', 'b'], 'a'); const res = moveTab(map, 'tile-1', 'tab-b', 'tile-new', null); expect(getTabs(res.map, 'tile-new').map((t) => t.tabId)).toEqual(['tab-b']); expect(getActiveTabId(res.map, 'tile-new')).toBe('tab-b'); expect(getTabs(res.map, 'tile-1').map((t) => t.tabId)).toEqual(['tab-a']); }); it('empties the source tile when its last tab moves away', () => { const map: TileTabsMap = { ...tileWithDocs('tile-1', ['a'], 'a'), ...tileWithDocs('tile-2', ['b'], 'b'), }; const res = moveTab(map, 'tile-1', 'tab-a', 'tile-2', null); expect(res.sourceEmptied).toBe(true); expect(res.map['tile-1']).toBeUndefined(); expect(getTabs(res.map, 'tile-2').map((t) => t.tabId)).toEqual([ 'tab-b', 'tab-a', ]); }); }); describe('findEmptyTab', () => { it('finds an empty tab when one exists', () => { const map = addTab( tileWithDocs('tile-1', ['a'], 'a'), 'tile-1', 'tab-blank', ); expect(findEmptyTab(map, 'tile-1')).toBe('tab-blank'); }); it('returns null when every tab holds a document', () => { const map = tileWithDocs('tile-1', ['a', 'b'], 'a'); expect(findEmptyTab(map, 'tile-1')).toBeNull(); }); }); describe('closeTab', () => { it('removes a non-active tab without changing the active tab', () => { const map = tileWithDocs('tile-1', ['a', 'b', 'c'], 'a'); const res = closeTab(map, 'tile-1', 'tab-b'); expect(res.removedTabIds).toEqual(['tab-b']); expect(res.tileEmptied).toBe(false); expect(getTabs(res.map, 'tile-1').map((t) => t.tabId)).toEqual([ 'tab-a', 'tab-c', ]); expect(getActiveTabId(res.map, 'tile-1')).toBe('tab-a'); }); it('activates the right neighbor when closing the active tab', () => { const map = tileWithDocs('tile-1', ['a', 'b', 'c'], 'b'); const res = closeTab(map, 'tile-1', 'tab-b'); expect(getActiveTabId(res.map, 'tile-1')).toBe('tab-c'); }); it('falls back to the left neighbor when closing the last (active) tab', () => { const map = tileWithDocs('tile-1', ['a', 'b'], 'b'); const res = closeTab(map, 'tile-1', 'tab-b'); expect(getActiveTabId(res.map, 'tile-1')).toBe('tab-a'); }); it('empties the tile when closing its last tab', () => { const map = tileWithDocs('tile-1', ['a'], 'a'); const res = closeTab(map, 'tile-1', 'tab-a'); expect(res.tileEmptied).toBe(true); expect(res.removedTabIds).toEqual(['tab-a']); expect(res.map['tile-1']).toBeUndefined(); }); it('is a no-op for a missing tile or tab', () => { const map = tileWithDocs('tile-1', ['a'], 'a'); expect(closeTab(map, 'other', 'tab-a')).toEqual({ map, removedTabIds: [], tileEmptied: false, }); expect(closeTab(map, 'tile-1', 'missing')).toEqual({ map, removedTabIds: [], tileEmptied: false, }); }); }); describe('activeDocument', () => { it('reports the request shown by the active tab', () => { const map = tileWithDocs('tile-1', ['a', 'b'], 'b'); expect(activeDocument(map, 'tile-1')).toMatchObject({ entryId: 'b' }); }); it('follows a tab switch — no navigation event required', () => { // The whole point: switching tabs is not an "open", yet the shell's // notion of the current document must still move. const map = tileWithDocs('tile-1', ['a', 'b'], 'a'); const switched = setActiveTab(map, 'tile-1', 'tab-b'); expect(activeDocument(switched, 'tile-1')).toMatchObject({ entryId: 'b' }); }); it('returns null for an empty tab, a missing tile, and an emptied tile', () => { expect( activeDocument(ensureTile({}, 'tile-1', 'tab-1'), 'tile-1'), ).toBeNull(); expect(activeDocument(tileWithDocs('tile-1', ['a']), 'nope')).toBeNull(); const closed = closeTab(tileWithDocs('tile-1', ['a']), 'tile-1', 'tab-a'); expect(activeDocument(closed.map, 'tile-1')).toBeNull(); }); it('still reports the request while the document is loading or errored', () => { const loading = tileWithDocs('tile-1', ['a']); expect(activeDocument(loading, 'tile-1')).toMatchObject({ entryId: 'a' }); const errored = setTabSlot(loading, 'tile-1', 'tab-a', { kind: 'error', request: request('a'), message: 'boom', }); expect(activeDocument(errored, 'tile-1')).toMatchObject({ entryId: 'a' }); }); });