@@ -12,6 +12,11 @@ import {
1212} from "./graphViewLayoutDefaults" ;
1313import { persistenceStatusStore } from "./persistence" ;
1414import { createInMemorySessionStorage } from "./safeSessionStorage" ;
15+ import {
16+ defaultSchemaViewLayout ,
17+ type SchemaViewLayout ,
18+ schemaViewLayoutCodec ,
19+ } from "./schemaViewLayoutDefaults" ;
1520import {
1621 createSessionScopedAtom ,
1722 parseSessionJson ,
@@ -34,38 +39,47 @@ const counterCodec: SessionValueCodec<Counter> = {
3439const KEY = "test-counter" ;
3540
3641/**
37- * Simulates one browser tab over the counter atom: its own Jotai store and
38- * sessionStorage (per-tab), all tabs sharing the one fake-indexeddb. Mirrors
39- * the active-connection test harness.
42+ * Builds a tab-opener bound to one key/default/codec. Each opened tab has its
43+ * own Jotai store and sessionStorage (per-tab), while all tabs share the one
44+ * fake-indexeddb — exactly how same-origin tabs relate. Mirrors the
45+ * active-connection test harness, parameterized so each real codec can be
46+ * exercised through the same multi-tab sequences rather than only a toy codec.
4047 */
41- async function openTab ( ) {
42- const sessionStorage = createInMemorySessionStorage ( ) ;
43- let store = createStore ( ) ;
44- let atom = await createSessionScopedAtom < Counter > ( {
45- key : KEY ,
46- defaultValue : { count : 0 } ,
47- codec : counterCodec ,
48- sessionStorage,
49- } ) ;
50- return {
51- read : ( ) => store . get ( atom ) ,
52- write : ( value : Counter ) => {
53- store . set ( atom , value ) ;
54- return persistenceStatusStore . waitForIdle ( ) ;
55- } ,
56- session : ( ) => sessionStorage . getItem ( KEY ) ,
57- reload : async ( ) => {
58- store = createStore ( ) ;
59- atom = await createSessionScopedAtom < Counter > ( {
60- key : KEY ,
61- defaultValue : { count : 0 } ,
62- codec : counterCodec ,
63- sessionStorage,
64- } ) ;
65- } ,
48+ function tabOpener < T > (
49+ key : string ,
50+ defaultValue : T ,
51+ codec : SessionValueCodec < T > ,
52+ ) {
53+ return async function openTab ( ) {
54+ const sessionStorage = createInMemorySessionStorage ( ) ;
55+ let store = createStore ( ) ;
56+ let atom = await createSessionScopedAtom < T > ( {
57+ key,
58+ defaultValue,
59+ codec,
60+ sessionStorage,
61+ } ) ;
62+ return {
63+ read : ( ) => store . get ( atom ) ,
64+ write : ( value : T ) => {
65+ store . set ( atom , value ) ;
66+ return persistenceStatusStore . waitForIdle ( ) ;
67+ } ,
68+ reload : async ( ) => {
69+ store = createStore ( ) ;
70+ atom = await createSessionScopedAtom < T > ( {
71+ key,
72+ defaultValue,
73+ codec,
74+ sessionStorage,
75+ } ) ;
76+ } ,
77+ } ;
6678 } ;
6779}
6880
81+ const openTab = tabOpener < Counter > ( KEY , { count : 0 } , counterCodec ) ;
82+
6983describe ( "createSessionScopedAtom" , ( ) => {
7084 beforeEach ( async ( ) => {
7185 await localForage . clear ( ) ;
@@ -278,3 +292,98 @@ describe("createSessionScopedAtom with the graph view layout codec", () => {
278292 ) . toStrictEqual ( breadcrumb ) ;
279293 } ) ;
280294} ) ;
295+
296+ // The two layout atoms ride the same primitive as the active connection, so
297+ // they get the same multi-tab assurances active-connection has — proven
298+ // through their real codecs, not the toy counter codec. The graph view codec
299+ // is the interesting one: its activeToggles Set must survive the array
300+ // serialization across a write-in-one-tab / cold-start-in-another sequence.
301+ describe ( "graph view layout across tabs" , ( ) => {
302+ const openGraphViewTab = tabOpener < GraphViewLayout > (
303+ "graph-view-layout" ,
304+ defaultGraphViewLayout ,
305+ graphViewLayoutCodec ,
306+ ) ;
307+
308+ beforeEach ( async ( ) => {
309+ await localForage . clear ( ) ;
310+ } ) ;
311+
312+ test ( "changing layout in one tab does not change an already-open tab" , async ( ) => {
313+ const tabB = await openGraphViewTab ( ) ;
314+ const tabBLayout : GraphViewLayout = {
315+ ...defaultGraphViewLayout ,
316+ activeSidebarItem : "filters" ,
317+ activeToggles : new Set ( [ "graph-viewer" ] ) ,
318+ } ;
319+ await tabB . write ( tabBLayout ) ;
320+
321+ const tabA = await openGraphViewTab ( ) ;
322+ await tabA . write ( {
323+ ...defaultGraphViewLayout ,
324+ activeSidebarItem : "edges-styling" ,
325+ } ) ;
326+
327+ expect ( tabB . read ( ) ) . toStrictEqual ( tabBLayout ) ;
328+ } ) ;
329+
330+ test ( "a later tab cold-starts to the layout an earlier tab wrote, with toggles rebuilt as a Set" , async ( ) => {
331+ const earlierTab = await openGraphViewTab ( ) ;
332+ const written : GraphViewLayout = {
333+ ...defaultGraphViewLayout ,
334+ activeSidebarItem : "expand" ,
335+ activeToggles : new Set ( [ "table-view" ] ) ,
336+ sidebar : { width : 512 } ,
337+ } ;
338+ await earlierTab . write ( written ) ;
339+
340+ const freshTab = await openGraphViewTab ( ) ;
341+
342+ const seeded = freshTab . read ( ) ;
343+ expect ( seeded ) . toStrictEqual ( written ) ;
344+ expect ( seeded . activeToggles ) . toBeInstanceOf ( Set ) ;
345+ } ) ;
346+ } ) ;
347+
348+ describe ( "schema view layout across tabs" , ( ) => {
349+ const openSchemaViewTab = tabOpener < SchemaViewLayout > (
350+ "schema-view-layout" ,
351+ defaultSchemaViewLayout ,
352+ schemaViewLayoutCodec ,
353+ ) ;
354+
355+ beforeEach ( async ( ) => {
356+ await localForage . clear ( ) ;
357+ } ) ;
358+
359+ test ( "changing layout in one tab does not change an already-open tab" , async ( ) => {
360+ const tabB = await openSchemaViewTab ( ) ;
361+ const tabBLayout : SchemaViewLayout = {
362+ ...defaultSchemaViewLayout ,
363+ activeSidebarItem : "nodes-styling" ,
364+ } ;
365+ await tabB . write ( tabBLayout ) ;
366+
367+ const tabA = await openSchemaViewTab ( ) ;
368+ await tabA . write ( {
369+ ...defaultSchemaViewLayout ,
370+ activeSidebarItem : "edges-styling" ,
371+ } ) ;
372+
373+ expect ( tabB . read ( ) ) . toStrictEqual ( tabBLayout ) ;
374+ } ) ;
375+
376+ test ( "a later tab cold-starts to the layout an earlier tab wrote" , async ( ) => {
377+ const earlierTab = await openSchemaViewTab ( ) ;
378+ const written : SchemaViewLayout = {
379+ ...defaultSchemaViewLayout ,
380+ activeSidebarItem : "edges-styling" ,
381+ sidebar : { width : 480 } ,
382+ } ;
383+ await earlierTab . write ( written ) ;
384+
385+ const freshTab = await openSchemaViewTab ( ) ;
386+
387+ expect ( freshTab . read ( ) ) . toStrictEqual ( written ) ;
388+ } ) ;
389+ } ) ;
0 commit comments