|
|
@@ -2,6 +2,7 @@
|
|
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
import { createLayoutStore } from '../src/client/stores.ts'
|
|
|
+import type { MainPanelId } from '../src/client/service.ts'
|
|
|
|
|
|
beforeEach(() => { vi.stubGlobal('innerWidth', 1920) })
|
|
|
afterEach(() => { vi.unstubAllGlobals(); vi.restoreAllMocks() })
|
|
|
@@ -10,14 +11,17 @@ describe('createLayoutStore', () => {
|
|
|
it('starts with the default sidebar and no right panel preference', () => {
|
|
|
const { store } = createLayoutStore().create()
|
|
|
expect(store.getSnapshot()).toEqual({
|
|
|
- sidebar: 280,
|
|
|
- viewportWidth: 1920,
|
|
|
- narrowExpanded: false,
|
|
|
- rightbar: null,
|
|
|
- rightbarShown: false,
|
|
|
- rightbarTrack: false,
|
|
|
- rightbarFullscreen: false,
|
|
|
- rightbarInstant: false,
|
|
|
+ panelInfo: { activePanelId: null },
|
|
|
+ layoutInfo: {
|
|
|
+ sidebar: 280,
|
|
|
+ viewportWidth: 1920,
|
|
|
+ narrowExpanded: false,
|
|
|
+ rightbar: null,
|
|
|
+ rightbarShown: false,
|
|
|
+ rightbarTrack: false,
|
|
|
+ rightbarFullscreen: false,
|
|
|
+ rightbarInstant: false,
|
|
|
+ },
|
|
|
})
|
|
|
})
|
|
|
|
|
|
@@ -27,26 +31,26 @@ describe('createLayoutStore', () => {
|
|
|
const b = createLayoutStore().create()
|
|
|
a.actions.setSidebar(400)
|
|
|
a.actions.openRightbar(true, false)
|
|
|
- expect(b.store.getSnapshot().sidebar).toBe(280)
|
|
|
- expect(b.store.getSnapshot().rightbar).toBeNull()
|
|
|
+ expect(b.store.getSnapshot().layoutInfo.sidebar).toBe(280)
|
|
|
+ expect(b.store.getSnapshot().layoutInfo.rightbar).toBeNull()
|
|
|
expect(write).not.toHaveBeenCalled()
|
|
|
})
|
|
|
|
|
|
it('clamps the sidebar to 264–420px', () => {
|
|
|
const { store, actions } = createLayoutStore().create()
|
|
|
actions.setSidebar(1)
|
|
|
- expect(store.getSnapshot().sidebar).toBe(264)
|
|
|
+ expect(store.getSnapshot().layoutInfo.sidebar).toBe(264)
|
|
|
actions.setSidebar(9999)
|
|
|
- expect(store.getSnapshot().sidebar).toBe(420)
|
|
|
+ expect(store.getSnapshot().layoutInfo.sidebar).toBe(420)
|
|
|
})
|
|
|
|
|
|
it('toggles the wide sidebar between closed and default width', () => {
|
|
|
const { store, actions } = createLayoutStore().create()
|
|
|
actions.setSidebar(400)
|
|
|
actions.toggleSidebar()
|
|
|
- expect(store.getSnapshot().sidebar).toBe(0)
|
|
|
+ expect(store.getSnapshot().layoutInfo.sidebar).toBe(0)
|
|
|
actions.toggleSidebar()
|
|
|
- expect(store.getSnapshot().sidebar).toBe(280)
|
|
|
+ expect(store.getSnapshot().layoutInfo.sidebar).toBe(280)
|
|
|
})
|
|
|
|
|
|
it('keeps the sidebar preference while toggling its narrow override', () => {
|
|
|
@@ -54,9 +58,9 @@ describe('createLayoutStore', () => {
|
|
|
actions.setSidebar(400)
|
|
|
actions.setViewportWidth(980)
|
|
|
actions.toggleSidebar()
|
|
|
- expect(store.getSnapshot()).toMatchObject({ sidebar: 400, viewportWidth: 980, narrowExpanded: true })
|
|
|
+ expect(store.getSnapshot().layoutInfo).toMatchObject({ sidebar: 400, viewportWidth: 980, narrowExpanded: true })
|
|
|
actions.toggleSidebar()
|
|
|
- expect(store.getSnapshot()).toMatchObject({ sidebar: 400, narrowExpanded: false })
|
|
|
+ expect(store.getSnapshot().layoutInfo).toMatchObject({ sidebar: 400, narrowExpanded: false })
|
|
|
})
|
|
|
|
|
|
it('clears the manual override only when crossing 1024px', () => {
|
|
|
@@ -65,39 +69,101 @@ describe('createLayoutStore', () => {
|
|
|
actions.toggleSidebar()
|
|
|
actions.setViewportWidth(980)
|
|
|
actions.setViewportWidth(1023)
|
|
|
- expect(store.getSnapshot().narrowExpanded).toBe(true)
|
|
|
+ expect(store.getSnapshot().layoutInfo.narrowExpanded).toBe(true)
|
|
|
actions.setViewportWidth(1024)
|
|
|
- expect(store.getSnapshot().narrowExpanded).toBe(false)
|
|
|
+ expect(store.getSnapshot().layoutInfo.narrowExpanded).toBe(false)
|
|
|
actions.setViewportWidth(980)
|
|
|
- expect(store.getSnapshot().narrowExpanded).toBe(false)
|
|
|
+ expect(store.getSnapshot().layoutInfo.narrowExpanded).toBe(false)
|
|
|
})
|
|
|
})
|
|
|
|
|
|
+describe('main panel selection', () => {
|
|
|
+ const panelA = 'panel-a' as MainPanelId
|
|
|
+ const panelB = 'panel-b' as MainPanelId
|
|
|
+
|
|
|
+ it('changes only panelInfo when switching panels and returning to the Conversation', () => {
|
|
|
+ const { store, actions } = createLayoutStore().create()
|
|
|
+ actions.setSidebar(400)
|
|
|
+ actions.openRightbar(true, true)
|
|
|
+ actions.closeRightbar()
|
|
|
+ const layoutInfo = store.getSnapshot().layoutInfo
|
|
|
+ for (const activePanelId of [panelA, panelB, null]) {
|
|
|
+ const previousPanelInfo = store.getSnapshot().panelInfo
|
|
|
+ actions.selectPanel(activePanelId)
|
|
|
+ expect(store.getSnapshot().panelInfo).toEqual({ activePanelId })
|
|
|
+ expect(store.getSnapshot().panelInfo).not.toBe(previousPanelInfo)
|
|
|
+ expect(store.getSnapshot().layoutInfo).toBe(layoutInfo)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ it('keeps the complete snapshot when selecting the current panel again', () => {
|
|
|
+ const { store, actions } = createLayoutStore().create()
|
|
|
+ actions.selectPanel(panelA)
|
|
|
+ const selected = store.getSnapshot()
|
|
|
+ actions.selectPanel(panelA)
|
|
|
+ expect(store.getSnapshot()).toBe(selected)
|
|
|
+ expect(store.getSnapshot().panelInfo.activePanelId).toBe(panelA)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('returns to the Conversation only when the selected main registration disappears', () => {
|
|
|
+ const { store, actions } = createLayoutStore().create()
|
|
|
+ const initial = store.getSnapshot()
|
|
|
+ actions.retainMainPanels([])
|
|
|
+ expect(store.getSnapshot()).toBe(initial)
|
|
|
+ actions.selectPanel(panelA)
|
|
|
+ const selected = store.getSnapshot()
|
|
|
+ actions.retainMainPanels(['conversation', panelA, panelB])
|
|
|
+ expect(store.getSnapshot()).toBe(selected)
|
|
|
+ actions.retainMainPanels(['conversation', panelB])
|
|
|
+ expect(store.getSnapshot().panelInfo).toEqual({ activePanelId: null })
|
|
|
+ expect(store.getSnapshot().layoutInfo).toBe(selected.layoutInfo)
|
|
|
+ })
|
|
|
+
|
|
|
+ it.each(['setSidebar', 'toggleSidebar', 'setViewportWidth', 'setRightbar', 'openRightbar', 'closeRightbar'] as const)(
|
|
|
+ 'preserves panelInfo identity when %s changes layoutInfo', (action) => {
|
|
|
+ const { store, actions } = createLayoutStore().create()
|
|
|
+ actions.selectPanel(panelA)
|
|
|
+ if (action === 'closeRightbar') actions.openRightbar(true, true)
|
|
|
+ const previous = store.getSnapshot()
|
|
|
+ switch (action) {
|
|
|
+ case 'setSidebar': actions.setSidebar(400); break
|
|
|
+ case 'toggleSidebar': actions.toggleSidebar(); break
|
|
|
+ case 'setViewportWidth': actions.setViewportWidth(980); break
|
|
|
+ case 'setRightbar': actions.setRightbar(500); break
|
|
|
+ case 'openRightbar': actions.openRightbar(true, true); break
|
|
|
+ case 'closeRightbar': actions.closeRightbar(); break
|
|
|
+ }
|
|
|
+ expect(store.getSnapshot().panelInfo).toBe(previous.panelInfo)
|
|
|
+ expect(store.getSnapshot().layoutInfo).not.toBe(previous.layoutInfo)
|
|
|
+ },
|
|
|
+ )
|
|
|
+})
|
|
|
+
|
|
|
describe('right panel', () => {
|
|
|
it('initializes at 45% of the latest frame only on first opening', () => {
|
|
|
const { store, actions } = createLayoutStore().create()
|
|
|
actions.setViewportWidth(1000)
|
|
|
- expect(store.getSnapshot().rightbar).toBeNull()
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbar).toBeNull()
|
|
|
actions.openRightbar(true, false)
|
|
|
- expect(store.getSnapshot().rightbar).toBe(450)
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbar).toBe(450)
|
|
|
actions.setViewportWidth(2000)
|
|
|
actions.openRightbar(true, true)
|
|
|
- expect(store.getSnapshot().rightbar).toBe(450)
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbar).toBe(450)
|
|
|
actions.closeRightbar()
|
|
|
actions.openRightbar(true, false)
|
|
|
- expect(store.getSnapshot().rightbar).toBe(450)
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbar).toBe(450)
|
|
|
})
|
|
|
|
|
|
it('keeps track and fullscreen reports independent and clears both on close', () => {
|
|
|
const { store, actions } = createLayoutStore().create()
|
|
|
actions.openRightbar(true, false)
|
|
|
- expect(store.getSnapshot()).toMatchObject({ rightbarShown: true, rightbarTrack: true, rightbarFullscreen: false })
|
|
|
+ expect(store.getSnapshot().layoutInfo).toMatchObject({ rightbarShown: true, rightbarTrack: true, rightbarFullscreen: false })
|
|
|
actions.openRightbar(true, true)
|
|
|
- expect(store.getSnapshot()).toMatchObject({ rightbarShown: true, rightbarTrack: true, rightbarFullscreen: true })
|
|
|
+ expect(store.getSnapshot().layoutInfo).toMatchObject({ rightbarShown: true, rightbarTrack: true, rightbarFullscreen: true })
|
|
|
actions.openRightbar(false, true)
|
|
|
- expect(store.getSnapshot()).toMatchObject({ rightbarShown: true, rightbarTrack: false, rightbarFullscreen: true })
|
|
|
+ expect(store.getSnapshot().layoutInfo).toMatchObject({ rightbarShown: true, rightbarTrack: false, rightbarFullscreen: true })
|
|
|
actions.closeRightbar()
|
|
|
- expect(store.getSnapshot()).toMatchObject({ rightbarShown: false, rightbarTrack: false, rightbarFullscreen: false })
|
|
|
+ expect(store.getSnapshot().layoutInfo).toMatchObject({ rightbarShown: false, rightbarTrack: false, rightbarFullscreen: false })
|
|
|
})
|
|
|
|
|
|
it('keeps dragged px preferences across resize, close, and reopen', () => {
|
|
|
@@ -105,29 +171,29 @@ describe('right panel', () => {
|
|
|
actions.openRightbar(true, false)
|
|
|
actions.setRightbar(1100)
|
|
|
actions.setViewportWidth(800)
|
|
|
- expect(store.getSnapshot().rightbar).toBe(1100)
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbar).toBe(1100)
|
|
|
actions.closeRightbar()
|
|
|
actions.openRightbar(false, true)
|
|
|
- expect(store.getSnapshot().rightbar).toBe(1100)
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbar).toBe(1100)
|
|
|
})
|
|
|
|
|
|
it('clamps drag preferences to 300px and 70% of the current frame', () => {
|
|
|
const { store, actions } = createLayoutStore().create()
|
|
|
actions.setViewportWidth(1600)
|
|
|
actions.setRightbar(9999)
|
|
|
- expect(store.getSnapshot().rightbar).toBe(1120)
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbar).toBe(1120)
|
|
|
actions.setViewportWidth(1000)
|
|
|
actions.setRightbar(9999)
|
|
|
- expect(store.getSnapshot().rightbar).toBe(700)
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbar).toBe(700)
|
|
|
actions.setRightbar(1)
|
|
|
- expect(store.getSnapshot().rightbar).toBe(300)
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbar).toBe(300)
|
|
|
})
|
|
|
|
|
|
it('retains a minimum normal preference when first opened fullscreen on a phone', () => {
|
|
|
const { store, actions } = createLayoutStore().create()
|
|
|
actions.setViewportWidth(320)
|
|
|
actions.openRightbar(false, true)
|
|
|
- expect(store.getSnapshot().rightbar).toBe(300)
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbar).toBe(300)
|
|
|
})
|
|
|
|
|
|
it('collapses a manually expanded narrow sidebar on opening, not presentation reports', () => {
|
|
|
@@ -136,23 +202,23 @@ describe('right panel', () => {
|
|
|
actions.setViewportWidth(800)
|
|
|
actions.toggleSidebar()
|
|
|
actions.openRightbar(true, false)
|
|
|
- expect(store.getSnapshot()).toMatchObject({ sidebar: 400, narrowExpanded: false })
|
|
|
+ expect(store.getSnapshot().layoutInfo).toMatchObject({ sidebar: 400, narrowExpanded: false })
|
|
|
actions.toggleSidebar()
|
|
|
actions.openRightbar(true, true)
|
|
|
- expect(store.getSnapshot().narrowExpanded).toBe(true)
|
|
|
+ expect(store.getSnapshot().layoutInfo.narrowExpanded).toBe(true)
|
|
|
actions.closeRightbar()
|
|
|
actions.openRightbar(true, false)
|
|
|
- expect(store.getSnapshot().narrowExpanded).toBe(false)
|
|
|
+ expect(store.getSnapshot().layoutInfo.narrowExpanded).toBe(false)
|
|
|
})
|
|
|
|
|
|
it('keeps the wide sidebar preference and never opens a closed right panel on resize', () => {
|
|
|
const { store, actions } = createLayoutStore().create()
|
|
|
actions.setSidebar(420)
|
|
|
actions.openRightbar(true, false)
|
|
|
- expect(store.getSnapshot().sidebar).toBe(420)
|
|
|
+ expect(store.getSnapshot().layoutInfo.sidebar).toBe(420)
|
|
|
actions.closeRightbar()
|
|
|
actions.setViewportWidth(3000)
|
|
|
- expect(store.getSnapshot()).toMatchObject({ sidebar: 420, rightbarShown: false, rightbarTrack: false })
|
|
|
+ expect(store.getSnapshot().layoutInfo).toMatchObject({ sidebar: 420, rightbarShown: false, rightbarTrack: false })
|
|
|
})
|
|
|
})
|
|
|
|
|
|
@@ -161,7 +227,7 @@ describe('right panel instant geometry', () => {
|
|
|
const { store, actions } = createLayoutStore().create()
|
|
|
actions.openRightbar(track, true)
|
|
|
actions.closeRightbar()
|
|
|
- expect(store.getSnapshot()).toMatchObject({
|
|
|
+ expect(store.getSnapshot().layoutInfo).toMatchObject({
|
|
|
rightbarShown: false, rightbarTrack: false, rightbarFullscreen: false, rightbarInstant: true,
|
|
|
})
|
|
|
const closed = store.getSnapshot()
|
|
|
@@ -173,38 +239,38 @@ describe('right panel instant geometry', () => {
|
|
|
const { store, actions } = createLayoutStore().create()
|
|
|
actions.openRightbar(true, true)
|
|
|
actions.openRightbar(true, false)
|
|
|
- expect(store.getSnapshot()).toMatchObject({
|
|
|
+ expect(store.getSnapshot().layoutInfo).toMatchObject({
|
|
|
rightbarShown: true, rightbarTrack: true, rightbarFullscreen: false, rightbarInstant: true,
|
|
|
})
|
|
|
const restored = store.getSnapshot()
|
|
|
actions.openRightbar(true, false)
|
|
|
expect(store.getSnapshot()).toBe(restored)
|
|
|
actions.openRightbar(false, false)
|
|
|
- expect(store.getSnapshot().rightbarInstant).toBe(false)
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbarInstant).toBe(false)
|
|
|
})
|
|
|
|
|
|
it('allows a normal close to animate, including after restoring from fullscreen', () => {
|
|
|
const { store, actions } = createLayoutStore().create()
|
|
|
actions.openRightbar(true, false)
|
|
|
actions.closeRightbar()
|
|
|
- expect(store.getSnapshot().rightbarInstant).toBe(false)
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbarInstant).toBe(false)
|
|
|
actions.openRightbar(true, true)
|
|
|
actions.openRightbar(true, false)
|
|
|
- expect(store.getSnapshot().rightbarInstant).toBe(true)
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbarInstant).toBe(true)
|
|
|
actions.closeRightbar()
|
|
|
- expect(store.getSnapshot()).toMatchObject({ rightbarTrack: false, rightbarInstant: false })
|
|
|
+ expect(store.getSnapshot().layoutInfo).toMatchObject({ rightbarTrack: false, rightbarInstant: false })
|
|
|
})
|
|
|
|
|
|
it.each(['setSidebar', 'toggleSidebar', 'setRightbar', 'setViewportWidth'] as const)('clears instant geometry on %s', (action) => {
|
|
|
const { store, actions } = createLayoutStore().create()
|
|
|
actions.openRightbar(true, true)
|
|
|
actions.closeRightbar()
|
|
|
- expect(store.getSnapshot().rightbarInstant).toBe(true)
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbarInstant).toBe(true)
|
|
|
if (action === 'toggleSidebar') actions.toggleSidebar()
|
|
|
else actions[action](action === 'setViewportWidth' ? 1800 : 350)
|
|
|
- expect(store.getSnapshot().rightbarInstant).toBe(false)
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbarInstant).toBe(false)
|
|
|
actions.closeRightbar()
|
|
|
- expect(store.getSnapshot().rightbarInstant).toBe(false)
|
|
|
+ expect(store.getSnapshot().layoutInfo.rightbarInstant).toBe(false)
|
|
|
})
|
|
|
|
|
|
it('does not let an unchanged frame measurement reset the fullscreen-exit marker', () => {
|
|
|
@@ -212,7 +278,7 @@ describe('right panel instant geometry', () => {
|
|
|
actions.openRightbar(true, true)
|
|
|
actions.closeRightbar()
|
|
|
const closed = store.getSnapshot()
|
|
|
- actions.setViewportWidth(closed.viewportWidth)
|
|
|
+ actions.setViewportWidth(closed.layoutInfo.viewportWidth)
|
|
|
expect(store.getSnapshot()).toBe(closed)
|
|
|
})
|
|
|
|
|
|
@@ -221,6 +287,6 @@ describe('right panel instant geometry', () => {
|
|
|
actions.openRightbar(true, true)
|
|
|
actions.closeRightbar()
|
|
|
actions.openRightbar(true, fullscreen)
|
|
|
- expect(store.getSnapshot()).toMatchObject({ rightbarShown: true, rightbarFullscreen: fullscreen, rightbarInstant: false })
|
|
|
+ expect(store.getSnapshot().layoutInfo).toMatchObject({ rightbarShown: true, rightbarFullscreen: fullscreen, rightbarInstant: false })
|
|
|
})
|
|
|
})
|