Something went wrong. Try again.
A local-first note taking app
Something went wrong. Try again.
14 kB · 415 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416import { describe, expect, it } from 'vitest';
import type { OpenDocRequest } from '../workspace';import { 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) }, })), 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' } }], 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, }); });});