mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-30 17:59:02 +00:00
36b859cabb
* FIX - layer dynamic island above phone camera The phone screen stacking context kept the Dynamic Island below the physical frame and its camera lens. Render the island at device-shell level, position it around the lens, and keep expanded notification spacing in sync. * ADD - preview all dynamic island states Reuse the runtime component for a development-only gallery and size expanded previews to the actual phone display so they cannot be clipped. * FIX - match dynamic island live activity behavior Show each activity only outside its owning app, use reference-sized activity layouts, and remove the separate expand glyph in favor of direct island taps. * FIX - refine dynamic island live controls Keep background activity state separate from foreground visibility so the island remains visible in the compact closed-phone peek. Match the timer, stopwatch, music, collapse, and spacing behavior to the supplied references. * FIX - keep business call availability after closing Stop treating the closed phone UI as the end of business-call readiness. Route background service-line calls only after revalidating the employee, owned device, and registered SIM. * FIX - remove dock apps from home grid The stored Home layout treated dock and grid shortcuts independently, so default dock apps appeared twice. Let the dock own those default placements and migrate saved layouts by removing matching grid shortcuts. * FIX - keep memo island active after closing Memo recording was cancelled as soon as the UI closed, while uploads required an active UI session and reopening a locked phone forced the home route. Keep the recorder bound to the physical device, revalidate device ownership server-side for uploads, preserve the last route behind the lock screen, and render only a compact frame, display, and live Dynamic Island while closed. * ENH - align live activity controls with phone hardware Report compact live-activity state to the client so Space opens Home or the enabled lock screen contextually. Apply the phone hardware volume as a shared output level for app media, games, YouTube playback, radio, and auxiliary sounds, and render the volume speaker icon in grey. * FIX - hide paused music live activity The music island previously depended only on the selected track, which remains set after playback pauses. Require active playback as well so the island closes when music stops while preserving the track for later resume. --------- Co-authored-by: DerEchteAlec <bycraky@gmail.com>
696 lines
20 KiB
TypeScript
696 lines
20 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import type { LaunchablePhoneAppId } from '@/types/apps'
|
|
import {
|
|
addHomeAppToFolder,
|
|
addHomePage,
|
|
createDefaultHomeLayout,
|
|
createHomeFolder,
|
|
deleteHomePage,
|
|
extractHomeFolderApp,
|
|
getHomeFolder,
|
|
HOME_GRID_COLUMNS,
|
|
HOME_GRID_PAGE_SIZE,
|
|
HOME_GRID_ROWS,
|
|
HOME_LAYOUT_VERSION,
|
|
homeKeyboardTarget,
|
|
isHomeFolder,
|
|
MAX_HOME_GRID_PAGES,
|
|
moveHomeApp,
|
|
moveHomeAppToGridPage,
|
|
moveHomeFolderApp,
|
|
parseHomeLayout,
|
|
reflowHomeGridForWidgetChange,
|
|
removeDockGridDuplicates,
|
|
removeHomeApp,
|
|
renameHomeFolder,
|
|
restoreHomeApp,
|
|
type HomeLayout,
|
|
} from '@/utils/homeLayout'
|
|
|
|
const installed = ['phone', 'messages', 'mail', 'clock', 'notes'] as const
|
|
const defaults = createDefaultHomeLayout(
|
|
[...installed],
|
|
['phone', 'messages', 'mail', 'clock', 'notes'],
|
|
['phone', 'messages', 'clock'],
|
|
)
|
|
|
|
function generatedApp(index: number): LaunchablePhoneAppId {
|
|
return `generated-app-${index}` as LaunchablePhoneAppId
|
|
}
|
|
|
|
function pageLayout(pageCounts: readonly number[]): HomeLayout {
|
|
const grid: HomeLayout['grid'] = Array.from(
|
|
{ length: pageCounts.length * HOME_GRID_PAGE_SIZE },
|
|
() => null,
|
|
)
|
|
let appIndex = 0
|
|
for (const [pageIndex, count] of pageCounts.entries()) {
|
|
for (let offset = 0; offset < count; offset += 1) {
|
|
grid[pageIndex * HOME_GRID_PAGE_SIZE + offset] = generatedApp(appIndex)
|
|
appIndex += 1
|
|
}
|
|
}
|
|
return {
|
|
dock: [generatedApp(999), null, null, null],
|
|
grid,
|
|
hidden: [],
|
|
pageCount: pageCounts.length,
|
|
version: HOME_LAYOUT_VERSION,
|
|
}
|
|
}
|
|
|
|
describe('home layout', () => {
|
|
it('uses six-row grid pages and fixed dock slots', () => {
|
|
const layout = parseHomeLayout(undefined, defaults, [...installed])
|
|
|
|
expect(HOME_GRID_PAGE_SIZE).toBe(HOME_GRID_COLUMNS * HOME_GRID_ROWS)
|
|
expect(HOME_GRID_ROWS).toBe(6)
|
|
expect(layout.grid).toHaveLength(HOME_GRID_PAGE_SIZE)
|
|
expect(layout.grid.slice(0, 6)).toEqual([
|
|
'phone',
|
|
'messages',
|
|
'mail',
|
|
'clock',
|
|
'notes',
|
|
null,
|
|
])
|
|
expect(layout.dock).toEqual(['phone', 'messages', 'clock', null])
|
|
expect(layout.version).toBe(HOME_LAYOUT_VERSION)
|
|
})
|
|
|
|
it('removes dock apps from the grid and normalizes affected folders', () => {
|
|
const layout: HomeLayout = {
|
|
...defaults,
|
|
dock: [
|
|
'phone',
|
|
{
|
|
apps: ['messages', 'mail'],
|
|
id: 'folder-abcdef',
|
|
name: 'Dock',
|
|
type: 'folder',
|
|
},
|
|
null,
|
|
null,
|
|
],
|
|
grid: [
|
|
'phone',
|
|
{
|
|
apps: ['messages', 'notes'],
|
|
id: 'folder-ghijkl',
|
|
name: 'Grid',
|
|
type: 'folder',
|
|
},
|
|
'mail',
|
|
'clock',
|
|
...Array.from({ length: HOME_GRID_PAGE_SIZE - 4 }, () => null),
|
|
],
|
|
}
|
|
|
|
const normalized = removeDockGridDuplicates(layout)
|
|
|
|
expect(normalized.grid.slice(0, 3)).toEqual(['notes', 'clock', null])
|
|
expect(normalized.dock).toEqual(layout.dock)
|
|
})
|
|
|
|
it('migrates compact persisted arrays and appends newly installed apps', () => {
|
|
const layout = parseHomeLayout(
|
|
{
|
|
dock: ['messages', 'invalid', 'messages'],
|
|
grid: ['mail'],
|
|
hidden: ['phone'],
|
|
},
|
|
defaults,
|
|
[...installed],
|
|
)
|
|
|
|
expect(layout.dock).toEqual(['messages', null, null, null])
|
|
expect(layout.grid.slice(0, 4)).toEqual(['mail', 'clock', 'notes', null])
|
|
expect(layout.hidden).toEqual(['phone'])
|
|
expect(layout.version).toBe(HOME_LAYOUT_VERSION)
|
|
})
|
|
|
|
it('closes gaps within a versioned page without moving apps between pages', () => {
|
|
const grid: HomeLayout['grid'] = Array.from({ length: 20 }, () => null)
|
|
grid[0] = 'phone'
|
|
grid[7] = 'mail'
|
|
|
|
const layout = parseHomeLayout(
|
|
{
|
|
dock: ['messages', null, 'clock', null],
|
|
grid,
|
|
hidden: ['notes'],
|
|
version: 2,
|
|
},
|
|
defaults,
|
|
[...installed],
|
|
)
|
|
|
|
expect(layout.grid.slice(0, 3)).toEqual(['phone', 'mail', null])
|
|
expect(layout.dock).toEqual(['messages', null, 'clock', null])
|
|
})
|
|
|
|
it('keeps valid custom-app tombstones while migrating version 3 layouts', () => {
|
|
const grid: HomeLayout['grid'] = Array.from({ length: 20 }, () => null)
|
|
grid[6] = 'temporarily-missing' as HomeLayout['hidden'][number]
|
|
|
|
const layout = parseHomeLayout(
|
|
{ dock: ['phone'], grid, hidden: [], version: 3 },
|
|
defaults,
|
|
[...installed],
|
|
)
|
|
|
|
expect(layout.grid).toContain('temporarily-missing')
|
|
const pageItemCount = layout.grid
|
|
.slice(0, HOME_GRID_PAGE_SIZE)
|
|
.filter((item) => item !== null).length
|
|
expect(layout.grid.slice(0, pageItemCount)).not.toContain(null)
|
|
expect(layout.version).toBe(HOME_LAYOUT_VERSION)
|
|
})
|
|
|
|
it('preserves page membership while expanding version 3 pages', () => {
|
|
const grid: HomeLayout['grid'] = Array.from({ length: 40 }, () => null)
|
|
grid[0] = 'phone'
|
|
grid[19] = 'messages'
|
|
grid[20] = 'mail'
|
|
grid[39] = 'clock'
|
|
|
|
const layout = parseHomeLayout(
|
|
{ dock: [], grid, hidden: [], version: 3 },
|
|
defaults,
|
|
[...installed],
|
|
)
|
|
|
|
expect(layout.grid).toHaveLength(HOME_GRID_PAGE_SIZE * 2)
|
|
expect(layout.grid.slice(0, 3)).toEqual(['phone', 'messages', 'notes'])
|
|
expect(layout.grid[HOME_GRID_PAGE_SIZE]).toBe('mail')
|
|
expect(layout.grid[HOME_GRID_PAGE_SIZE + 1]).toBe('clock')
|
|
expect(layout.version).toBe(HOME_LAYOUT_VERSION)
|
|
})
|
|
|
|
it('reads version 4 pages directly before migrating the schema', () => {
|
|
const grid: HomeLayout['grid'] = Array.from(
|
|
{ length: HOME_GRID_PAGE_SIZE * 2 },
|
|
() => null,
|
|
)
|
|
grid[20] = 'phone'
|
|
grid[HOME_GRID_PAGE_SIZE] = 'messages'
|
|
|
|
const layout = parseHomeLayout(
|
|
{ dock: [], grid, hidden: [], version: 4 },
|
|
defaults,
|
|
[...installed],
|
|
)
|
|
|
|
expect(layout.grid).toHaveLength(HOME_GRID_PAGE_SIZE * 2)
|
|
expect(layout.grid[0]).toBe('phone')
|
|
expect(layout.grid[HOME_GRID_PAGE_SIZE]).toBe('messages')
|
|
expect(layout.version).toBe(HOME_LAYOUT_VERSION)
|
|
})
|
|
|
|
it('preserves folders and page membership in version 5 layouts', () => {
|
|
const grid: HomeLayout['grid'] = Array.from(
|
|
{ length: HOME_GRID_PAGE_SIZE * 2 },
|
|
() => null,
|
|
)
|
|
grid[8] = {
|
|
apps: ['mail', 'notes'],
|
|
id: 'folder-work-123456',
|
|
name: 'Work',
|
|
type: 'folder',
|
|
}
|
|
grid[HOME_GRID_PAGE_SIZE + 7] = 'clock'
|
|
|
|
const layout = parseHomeLayout(
|
|
{ dock: ['phone'], grid, hidden: [], version: 5 },
|
|
defaults,
|
|
[...installed],
|
|
)
|
|
|
|
expect(getHomeFolder(layout, 'folder-work-123456')?.apps).toEqual([
|
|
'mail',
|
|
'notes',
|
|
])
|
|
expect(layout.grid[HOME_GRID_PAGE_SIZE]).toBe('clock')
|
|
expect(layout.version).toBe(HOME_LAYOUT_VERSION)
|
|
})
|
|
|
|
it('preserves independently positioned shortcuts for the same app', () => {
|
|
const grid: HomeLayout['grid'] = Array.from({ length: 20 }, () => null)
|
|
grid[0] = 'phone'
|
|
grid[5] = 'phone'
|
|
|
|
const layout = parseHomeLayout(
|
|
{ dock: ['phone'], grid, hidden: [], version: 2 },
|
|
defaults,
|
|
[...installed],
|
|
)
|
|
|
|
expect(layout.grid[0]).toBe('phone')
|
|
expect(layout.grid[1]).toBe('phone')
|
|
expect(layout.dock[0]).toBe('phone')
|
|
})
|
|
|
|
it('inserts into an empty area and closes the source gap', () => {
|
|
const moved = moveHomeApp(defaults, 'grid', 2, 'grid', 12)
|
|
|
|
expect(moved.grid.slice(0, 6)).toEqual([
|
|
'phone',
|
|
'messages',
|
|
'clock',
|
|
'notes',
|
|
'mail',
|
|
null,
|
|
])
|
|
})
|
|
|
|
it('extends the layout when moving an item onto an empty new page', () => {
|
|
const targetIndex = HOME_GRID_PAGE_SIZE + 20
|
|
const moved = moveHomeApp(defaults, 'grid', 0, 'grid', targetIndex)
|
|
|
|
expect(moved.grid).toHaveLength(HOME_GRID_PAGE_SIZE * 2)
|
|
expect(moved.grid.slice(0, 5)).toEqual([
|
|
'messages',
|
|
'mail',
|
|
'clock',
|
|
'notes',
|
|
null,
|
|
])
|
|
expect(moved.grid[HOME_GRID_PAGE_SIZE]).toBe('phone')
|
|
})
|
|
|
|
it.each([20, 21, 23])(
|
|
'fills a target page containing %s apps without swapping one backwards',
|
|
(targetCount) => {
|
|
const layout = pageLayout([2, targetCount])
|
|
const dragged = layout.grid[0]
|
|
const originalTarget = layout.grid
|
|
.slice(HOME_GRID_PAGE_SIZE, HOME_GRID_PAGE_SIZE * 2)
|
|
.filter((item) => item !== null)
|
|
const moved = moveHomeAppToGridPage(layout, 'grid', 0, 2, targetCount)
|
|
|
|
expect(moved.grid[0]).toBe(layout.grid[1])
|
|
expect(
|
|
moved.grid
|
|
.slice(HOME_GRID_PAGE_SIZE, HOME_GRID_PAGE_SIZE * 2)
|
|
.filter((item) => item !== null),
|
|
).toEqual([...originalTarget, dragged])
|
|
},
|
|
)
|
|
|
|
it('cascades a full target page forward into a newly created page', () => {
|
|
const layout = pageLayout([2, HOME_GRID_PAGE_SIZE])
|
|
const dragged = layout.grid[0]
|
|
const displaced = layout.grid[HOME_GRID_PAGE_SIZE * 2 - 1]
|
|
const moved = moveHomeAppToGridPage(layout, 'grid', 0, 2, 0)
|
|
|
|
expect(moved.grid).toHaveLength(HOME_GRID_PAGE_SIZE * 3)
|
|
expect(moved.grid.slice(0, 2)).toEqual([layout.grid[1], null])
|
|
expect(moved.grid[HOME_GRID_PAGE_SIZE]).toBe(dragged)
|
|
expect(moved.grid[HOME_GRID_PAGE_SIZE * 2]).toBe(displaced)
|
|
})
|
|
|
|
it('uses a later source-page gap for forward overflow', () => {
|
|
const layout = pageLayout([HOME_GRID_PAGE_SIZE, 2])
|
|
const draggedIndex = HOME_GRID_PAGE_SIZE
|
|
const dragged = layout.grid[draggedIndex]
|
|
const displaced = layout.grid[HOME_GRID_PAGE_SIZE - 1]
|
|
const remainingSecondPageItem = layout.grid[draggedIndex + 1]
|
|
const moved = moveHomeAppToGridPage(layout, 'grid', draggedIndex, 1, 4)
|
|
|
|
expect(moved.grid).toHaveLength(HOME_GRID_PAGE_SIZE * 2)
|
|
expect(moved.grid[4]).toBe(dragged)
|
|
expect(moved.grid[HOME_GRID_PAGE_SIZE]).toBe(displaced)
|
|
expect(moved.grid[HOME_GRID_PAGE_SIZE + 1]).toBe(remainingSecondPageItem)
|
|
})
|
|
|
|
it('rejects a dock insertion atomically when all pages are full', () => {
|
|
const layout = pageLayout(
|
|
Array.from({ length: MAX_HOME_GRID_PAGES }, () => HOME_GRID_PAGE_SIZE),
|
|
)
|
|
|
|
expect(moveHomeAppToGridPage(layout, 'dock', 0, 1, 0)).toBe(layout)
|
|
})
|
|
|
|
it('moves a dock app onto another page and preserves every other item', () => {
|
|
const layout = pageLayout([3, 2])
|
|
const dockApp = layout.dock[0]
|
|
const before = [...layout.grid.filter(Boolean), dockApp].sort()
|
|
const moved = moveHomeAppToGridPage(layout, 'dock', 0, 2, 1)
|
|
|
|
expect(moved.dock[0]).toBeNull()
|
|
expect(moved.grid[HOME_GRID_PAGE_SIZE + 1]).toBe(dockApp)
|
|
expect(moved.grid.filter(Boolean).sort()).toEqual(before)
|
|
})
|
|
|
|
it('respects widget-reduced capacities while cascading forward', () => {
|
|
const layout = pageLayout([HOME_GRID_PAGE_SIZE])
|
|
const moved = moveHomeAppToGridPage(layout, 'grid', 0, 1, 0, [
|
|
20,
|
|
HOME_GRID_PAGE_SIZE,
|
|
])
|
|
|
|
expect(moved.grid.slice(0, 20).filter(Boolean)).toHaveLength(20)
|
|
expect(
|
|
moved.grid
|
|
.slice(HOME_GRID_PAGE_SIZE, HOME_GRID_PAGE_SIZE * 2)
|
|
.filter(Boolean),
|
|
).toHaveLength(4)
|
|
expect(moved.grid.filter(Boolean).sort()).toEqual(
|
|
layout.grid.filter(Boolean).sort(),
|
|
)
|
|
})
|
|
|
|
it('materializes the visible pages before moving a widget forward', () => {
|
|
const layout = pageLayout([0, HOME_GRID_PAGE_SIZE, 7])
|
|
const pageTwoApps = layout.grid
|
|
.slice(HOME_GRID_PAGE_SIZE, HOME_GRID_PAGE_SIZE * 2)
|
|
.filter((item) => item !== null)
|
|
const pageThreeApps = layout.grid
|
|
.slice(HOME_GRID_PAGE_SIZE * 2, HOME_GRID_PAGE_SIZE * 3)
|
|
.filter((item) => item !== null)
|
|
const reflowed = reflowHomeGridForWidgetChange(
|
|
layout,
|
|
[HOME_GRID_PAGE_SIZE, 20, HOME_GRID_PAGE_SIZE],
|
|
[HOME_GRID_PAGE_SIZE, HOME_GRID_PAGE_SIZE, 20],
|
|
)
|
|
|
|
expect(reflowed).not.toBeNull()
|
|
expect(
|
|
reflowed?.grid
|
|
.slice(HOME_GRID_PAGE_SIZE, HOME_GRID_PAGE_SIZE * 2)
|
|
.filter(Boolean),
|
|
).toEqual(pageTwoApps.slice(0, 20))
|
|
expect(
|
|
reflowed?.grid
|
|
.slice(HOME_GRID_PAGE_SIZE * 2, HOME_GRID_PAGE_SIZE * 3)
|
|
.filter(Boolean),
|
|
).toEqual([...pageTwoApps.slice(20), ...pageThreeApps])
|
|
})
|
|
|
|
it('moves widget overflow only to later pages', () => {
|
|
const layout = pageLayout([0, HOME_GRID_PAGE_SIZE, 20])
|
|
const allApps = layout.grid.filter(Boolean).sort()
|
|
const reflowed = reflowHomeGridForWidgetChange(
|
|
layout,
|
|
[HOME_GRID_PAGE_SIZE, 20, HOME_GRID_PAGE_SIZE],
|
|
[HOME_GRID_PAGE_SIZE, HOME_GRID_PAGE_SIZE, 20],
|
|
)
|
|
|
|
expect(reflowed).not.toBeNull()
|
|
expect(
|
|
reflowed?.grid
|
|
.slice(HOME_GRID_PAGE_SIZE, HOME_GRID_PAGE_SIZE * 2)
|
|
.filter(Boolean),
|
|
).toHaveLength(20)
|
|
expect(
|
|
reflowed?.grid
|
|
.slice(HOME_GRID_PAGE_SIZE * 2, HOME_GRID_PAGE_SIZE * 3)
|
|
.filter(Boolean),
|
|
).toHaveLength(20)
|
|
expect(
|
|
reflowed?.grid
|
|
.slice(HOME_GRID_PAGE_SIZE * 3, HOME_GRID_PAGE_SIZE * 4)
|
|
.filter(Boolean),
|
|
).toHaveLength(4)
|
|
expect(reflowed?.grid.filter(Boolean).sort()).toEqual(allApps)
|
|
})
|
|
|
|
it('provides bounded keyboard reorder targets without wrapping rows', () => {
|
|
expect(homeKeyboardTarget(defaults, 'grid', 1, 'right')).toBe(2)
|
|
expect(homeKeyboardTarget(defaults, 'grid', 3, 'right')).toBeNull()
|
|
expect(homeKeyboardTarget(defaults, 'grid', 0, 'up')).toBeNull()
|
|
expect(homeKeyboardTarget(defaults, 'grid', 0, 'down')).toBe(4)
|
|
expect(homeKeyboardTarget(defaults, 'dock', 1, 'left')).toBe(0)
|
|
expect(homeKeyboardTarget(defaults, 'dock', 1, 'down')).toBeNull()
|
|
})
|
|
|
|
it('shifts occupied grid slots instead of replacing their items', () => {
|
|
const reordered = moveHomeApp(defaults, 'grid', 2, 'grid', 0)
|
|
|
|
expect(reordered.grid.slice(0, 5)).toEqual([
|
|
'mail',
|
|
'phone',
|
|
'messages',
|
|
'clock',
|
|
'notes',
|
|
])
|
|
})
|
|
|
|
it('shifts an occupied dock slot toward its gap', () => {
|
|
const docked = moveHomeApp(defaults, 'grid', 2, 'dock', 2)
|
|
|
|
expect(docked.dock).toEqual(['phone', 'messages', 'mail', 'clock'])
|
|
expect(docked.grid.slice(0, 5)).toEqual([
|
|
'phone',
|
|
'messages',
|
|
'clock',
|
|
'notes',
|
|
null,
|
|
])
|
|
})
|
|
|
|
it('moves a dock item displaced from a full dock into the source slot', () => {
|
|
const layout: HomeLayout = {
|
|
...defaults,
|
|
dock: ['phone', 'messages', 'clock', 'notes'],
|
|
}
|
|
const docked = moveHomeApp(layout, 'grid', 2, 'dock', 1)
|
|
|
|
expect(docked.dock).toEqual(['phone', 'mail', 'messages', 'clock'])
|
|
expect(docked.grid[2]).toBe('notes')
|
|
})
|
|
|
|
it('moves shortcuts between the dock and grid independently', () => {
|
|
const movedToGrid = moveHomeApp(defaults, 'dock', 0, 'grid', 5)
|
|
|
|
expect(movedToGrid.dock[0]).toBeNull()
|
|
expect(movedToGrid.grid[0]).toBe('phone')
|
|
expect(movedToGrid.grid[5]).toBe('phone')
|
|
|
|
const movedToDock = moveHomeApp(movedToGrid, 'grid', 1, 'dock', 3)
|
|
expect(movedToDock.grid.slice(0, 6)).toEqual([
|
|
'phone',
|
|
'mail',
|
|
'clock',
|
|
'notes',
|
|
'phone',
|
|
null,
|
|
])
|
|
expect(movedToDock.dock[1]).toBe('messages')
|
|
expect(movedToDock.dock[3]).toBe('messages')
|
|
})
|
|
|
|
it('removes and restores shortcuts without leaving a page gap', () => {
|
|
const layout = moveHomeApp(defaults, 'grid', 0, 'grid', 10)
|
|
const removed = removeHomeApp(layout, 'phone')
|
|
|
|
expect(removed.grid).not.toContain('phone')
|
|
expect(removed.dock[0]).toBeNull()
|
|
expect(removed.hidden).toContain('phone')
|
|
|
|
const restored = restoreHomeApp(removed, 'phone')
|
|
expect(restored.grid[4]).toBe('phone')
|
|
expect(restored.grid.slice(0, 5)).not.toContain(null)
|
|
expect(restored.hidden).not.toContain('phone')
|
|
})
|
|
|
|
it('reveals an existing shortcut when restoring a hidden app', () => {
|
|
const hidden = {
|
|
...defaults,
|
|
grid: [...defaults.grid],
|
|
hidden: [...defaults.hidden, 'health' as const],
|
|
}
|
|
hidden.grid[5] = 'health'
|
|
|
|
const restored = restoreHomeApp(hidden, 'health')
|
|
|
|
expect(restored.grid[5]).toBe('health')
|
|
expect(restored.hidden).not.toContain('health')
|
|
})
|
|
|
|
it('adds persistent empty pages up to the home screen limit', () => {
|
|
let layout = defaults
|
|
for (let page = 1; page < MAX_HOME_GRID_PAGES; page += 1) {
|
|
layout = addHomePage(layout)
|
|
}
|
|
|
|
expect(layout.grid).toHaveLength(HOME_GRID_PAGE_SIZE * MAX_HOME_GRID_PAGES)
|
|
expect(layout.pageCount).toBe(MAX_HOME_GRID_PAGES)
|
|
expect(addHomePage(layout)).toBe(layout)
|
|
})
|
|
|
|
it('restores an explicitly persisted empty trailing page', () => {
|
|
const layout = parseHomeLayout(
|
|
{
|
|
dock: defaults.dock,
|
|
grid: defaults.grid.slice(0, 5),
|
|
hidden: [],
|
|
pageCount: 2,
|
|
version: HOME_LAYOUT_VERSION,
|
|
},
|
|
defaults,
|
|
[...installed],
|
|
)
|
|
|
|
expect(layout.pageCount).toBe(2)
|
|
expect(layout.grid).toHaveLength(HOME_GRID_PAGE_SIZE * 2)
|
|
expect(
|
|
layout.grid.slice(HOME_GRID_PAGE_SIZE).every((item) => item === null),
|
|
).toBe(true)
|
|
})
|
|
|
|
it('deletes a page and moves its items into remaining empty slots', () => {
|
|
let layout = addHomePage(defaults)
|
|
layout = moveHomeApp(layout, 'grid', 0, 'grid', HOME_GRID_PAGE_SIZE)
|
|
const deleted = deleteHomePage(layout, 2)
|
|
|
|
expect(deleted.grid).toHaveLength(HOME_GRID_PAGE_SIZE)
|
|
expect(deleted.pageCount).toBe(1)
|
|
expect(deleted.grid).toContain('phone')
|
|
expect(deleteHomePage(deleted, 1)).toBe(deleted)
|
|
})
|
|
|
|
it('creates, renames, and parses a folder with compact pages', () => {
|
|
const folderLayout = createHomeFolder(
|
|
defaults,
|
|
'grid',
|
|
2,
|
|
'grid',
|
|
4,
|
|
'folder-work-123456',
|
|
'Work',
|
|
)
|
|
const folder = getHomeFolder(folderLayout, 'folder-work-123456')
|
|
|
|
expect(folder?.apps).toEqual(['notes', 'mail'])
|
|
expect(folderLayout.grid.slice(0, 4)).toEqual([
|
|
'phone',
|
|
'messages',
|
|
'clock',
|
|
folder,
|
|
])
|
|
|
|
const renamed = renameHomeFolder(
|
|
folderLayout,
|
|
'folder-work-123456',
|
|
' Dienstprogramme ',
|
|
)
|
|
const parsed = parseHomeLayout(renamed, defaults, [...installed])
|
|
expect(parsed.version).toBe(HOME_LAYOUT_VERSION)
|
|
expect(getHomeFolder(parsed, 'folder-work-123456')).toEqual({
|
|
apps: ['notes', 'mail'],
|
|
id: 'folder-work-123456',
|
|
name: 'Dienstprogramme',
|
|
type: 'folder',
|
|
})
|
|
})
|
|
|
|
it('adds apps to a folder and swaps apps inside it', () => {
|
|
const folderLayout = createHomeFolder(
|
|
defaults,
|
|
'grid',
|
|
2,
|
|
'grid',
|
|
4,
|
|
'folder-tools-123456',
|
|
'Tools',
|
|
)
|
|
const clockIndex = folderLayout.grid.indexOf('clock')
|
|
const expanded = addHomeAppToFolder(
|
|
folderLayout,
|
|
'grid',
|
|
clockIndex,
|
|
'folder-tools-123456',
|
|
)
|
|
|
|
expect(getHomeFolder(expanded, 'folder-tools-123456')?.apps).toEqual([
|
|
'notes',
|
|
'mail',
|
|
'clock',
|
|
])
|
|
const reordered = moveHomeFolderApp(expanded, 'folder-tools-123456', 2, 0)
|
|
expect(getHomeFolder(reordered, 'folder-tools-123456')?.apps).toEqual([
|
|
'clock',
|
|
'mail',
|
|
'notes',
|
|
])
|
|
})
|
|
|
|
it('extracts a folder app and dissolves one-app folders', () => {
|
|
const folderLayout = createHomeFolder(
|
|
defaults,
|
|
'grid',
|
|
2,
|
|
'grid',
|
|
4,
|
|
'folder-social-123456',
|
|
'Social',
|
|
)
|
|
const extracted = extractHomeFolderApp(
|
|
folderLayout,
|
|
'folder-social-123456',
|
|
1,
|
|
'grid',
|
|
12,
|
|
)
|
|
|
|
expect(extracted.grid.slice(0, 5)).toEqual([
|
|
'phone',
|
|
'messages',
|
|
'clock',
|
|
'notes',
|
|
'mail',
|
|
])
|
|
expect(getHomeFolder(extracted, 'folder-social-123456')).toBeNull()
|
|
})
|
|
|
|
it('removes hidden apps from folders and dissolves the folder', () => {
|
|
const folderLayout = createHomeFolder(
|
|
defaults,
|
|
'grid',
|
|
2,
|
|
'grid',
|
|
4,
|
|
'folder-mixed-123456',
|
|
'Mixed',
|
|
)
|
|
const removed = removeHomeApp(folderLayout, 'mail')
|
|
|
|
expect(removed.grid.slice(0, 4)).toEqual([
|
|
'phone',
|
|
'messages',
|
|
'clock',
|
|
'notes',
|
|
])
|
|
expect(removed.hidden).toContain('mail')
|
|
})
|
|
|
|
it('moves folders as one item without crossing page boundaries', () => {
|
|
const folderLayout = createHomeFolder(
|
|
defaults,
|
|
'grid',
|
|
2,
|
|
'grid',
|
|
4,
|
|
'folder-page-123456',
|
|
'Page two',
|
|
)
|
|
const folderIndex = folderLayout.grid.findIndex(isHomeFolder)
|
|
const moved = moveHomeApp(
|
|
folderLayout,
|
|
'grid',
|
|
folderIndex,
|
|
'grid',
|
|
HOME_GRID_PAGE_SIZE + 8,
|
|
)
|
|
|
|
expect(moved.grid.slice(0, 4)).toEqual(['phone', 'messages', 'clock', null])
|
|
const movedFolder = moved.grid[HOME_GRID_PAGE_SIZE]
|
|
expect(isHomeFolder(movedFolder)).toBe(true)
|
|
expect(isHomeFolder(movedFolder) && movedFolder.id).toBe(
|
|
'folder-page-123456',
|
|
)
|
|
})
|
|
})
|