@@ -6,7 +6,7 @@ import { renderToStaticMarkup } from 'react-dom/server'
66import { describe , expect , it , vi } from 'vitest'
77
88import { DesktopShell } from './DesktopShell'
9- import type { DesktopLayoutSnapshot , ShellAppDefinition } from './types'
9+ import type { DesktopItemDefinition , DesktopLayoutSnapshot , ShellAppDefinition } from './types'
1010
1111; ( globalThis as { IS_REACT_ACT_ENVIRONMENT ?: boolean } ) . IS_REACT_ACT_ENVIRONMENT = true
1212
@@ -20,6 +20,144 @@ const dockedApp: ShellAppDefinition = {
2020
2121const turkishShellProps = { locale : 'tr' as const }
2222
23+ const floatingDockApps : ShellAppDefinition [ ] = [ 'notes' , 'files' , 'browser' ] . map ( ( id , index ) => ( {
24+ id,
25+ title : id ,
26+ icon : { type : 'glyph' , value : `${ index + 1 } ` } ,
27+ content : < p > { id } </ p > ,
28+ placement : 'floating' ,
29+ } ) )
30+
31+ const desktopFiles : DesktopItemDefinition [ ] = [
32+ {
33+ id : 'lobby-camera' ,
34+ title : 'Lobby camera' ,
35+ kind : 'image' ,
36+ previewUrl : '/assets/lobby-camera.jpg' ,
37+ status : 'new' ,
38+ } ,
39+ {
40+ id : 'witness-statement' ,
41+ title : 'Witness statement' ,
42+ kind : 'document' ,
43+ status : 'reviewed' ,
44+ } ,
45+ ]
46+
47+ describe ( 'DesktopShell dock presentation' , ( ) => {
48+ it ( 'keeps applications in the dock and renders supplied files on the desktop' , ( ) => {
49+ const html = renderToStaticMarkup (
50+ < DesktopShell { ...turkishShellProps } apps = { floatingDockApps } desktopItems = { desktopFiles } /> ,
51+ )
52+
53+ expect ( html . match ( / d e t e c t i v e - d o c k _ _ i c o n / g) ) . toHaveLength ( floatingDockApps . length )
54+ expect ( html . match ( / d a t a - d e s k t o p - i t e m - i d = / g) ) . toHaveLength ( desktopFiles . length )
55+ expect ( html ) . toContain ( '/assets/lobby-camera.jpg' )
56+ expect ( html ) . toContain ( 'detective-desktop__file-icon--document' )
57+ expect ( html ) . not . toContain ( 'detective-desktop__alias-badge' )
58+ } )
59+
60+ it ( 'selects desktop files on one click and opens them on double click or Enter' , async ( ) => {
61+ const onOpenDesktopItem = vi . fn ( )
62+ const host = document . createElement ( 'div' )
63+ document . body . append ( host )
64+ const root = createRoot ( host )
65+
66+ try {
67+ await act ( async ( ) => {
68+ root . render (
69+ < DesktopShell
70+ { ...turkishShellProps }
71+ apps = { floatingDockApps }
72+ desktopItems = { desktopFiles }
73+ onOpenDesktopItem = { onOpenDesktopItem }
74+ /> ,
75+ )
76+ } )
77+ const file = host . querySelector < HTMLButtonElement > ( '[data-desktop-item-id="lobby-camera"]' ) !
78+
79+ await act ( async ( ) => file . click ( ) )
80+ expect ( file . classList . contains ( 'is-selected' ) ) . toBe ( true )
81+ expect ( onOpenDesktopItem ) . not . toHaveBeenCalled ( )
82+
83+ await act ( async ( ) => {
84+ file . dispatchEvent ( new MouseEvent ( 'dblclick' , { bubbles : true } ) )
85+ } )
86+ expect ( onOpenDesktopItem ) . toHaveBeenLastCalledWith ( 'lobby-camera' )
87+
88+ await act ( async ( ) => {
89+ file . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'Enter' , bubbles : true } ) )
90+ } )
91+ expect ( onOpenDesktopItem ) . toHaveBeenCalledTimes ( 2 )
92+ } finally {
93+ await act ( async ( ) => root . unmount ( ) )
94+ host . remove ( )
95+ }
96+ } )
97+
98+ it ( 'magnifies and spreads nearby dock icons continuously, then resets on leave' , async ( ) => {
99+ const originalRect = HTMLElement . prototype . getBoundingClientRect
100+ HTMLElement . prototype . getBoundingClientRect = function getBoundingClientRect ( ) {
101+ if ( this . matches ( '.detective-dock button' ) ) {
102+ const buttons = [ ...this . parentElement ! . querySelectorAll ( 'button' ) ]
103+ const index = buttons . indexOf ( this as HTMLButtonElement )
104+ return new DOMRect ( 100 + index * 54 , 700 , 48 , 53 )
105+ }
106+ return originalRect . call ( this )
107+ }
108+ const pointer = (
109+ type : string ,
110+ clientX : number ,
111+ pointerType = 'mouse' ,
112+ relatedTarget ?: EventTarget ,
113+ ) => {
114+ const event = new MouseEvent ( type , {
115+ bubbles : true ,
116+ clientX,
117+ relatedTarget,
118+ } )
119+ Object . defineProperty ( event , 'pointerType' , { value : pointerType } )
120+ return event
121+ }
122+ const host = document . createElement ( 'div' )
123+ document . body . append ( host )
124+ const root = createRoot ( host )
125+
126+ try {
127+ await act ( async ( ) => {
128+ root . render ( < DesktopShell { ...turkishShellProps } apps = { floatingDockApps } /> )
129+ } )
130+ const dock = host . querySelector < HTMLElement > ( '.detective-dock' ) !
131+ const shelf = host . querySelector < HTMLElement > ( '.detective-dock__apps' ) !
132+ const buttons = [ ...host . querySelectorAll < HTMLButtonElement > ( '.detective-dock button' ) ]
133+
134+ await act ( async ( ) => dock . dispatchEvent ( pointer ( 'pointermove' , 178 ) ) )
135+
136+ const scales = buttons . map ( ( button ) => Number ( button . style . getPropertyValue ( '--dock-scale' ) ) )
137+ expect ( scales [ 1 ] ) . toBeCloseTo ( 1.52 , 2 )
138+ expect ( scales [ 0 ] ) . toBeGreaterThan ( 1 )
139+ expect ( scales [ 0 ] ) . toBeLessThan ( scales [ 1 ] ! )
140+ expect ( scales [ 2 ] ) . toBeCloseTo ( scales [ 0 ] ! , 2 )
141+ expect ( Number . parseFloat ( buttons [ 0 ] ! . style . getPropertyValue ( '--dock-shift' ) ) ) . toBeLessThan ( 0 )
142+ expect ( Number . parseFloat ( buttons [ 2 ] ! . style . getPropertyValue ( '--dock-shift' ) ) ) . toBeGreaterThan ( 0 )
143+ expect ( Number . parseFloat ( shelf . style . getPropertyValue ( '--dock-side-expansion' ) ) ) . toBeGreaterThan ( 0 )
144+ expect ( shelf . classList . contains ( 'is-magnifying' ) ) . toBe ( true )
145+
146+ await act ( async ( ) => dock . dispatchEvent ( pointer ( 'pointerout' , 400 , 'mouse' , document . body ) ) )
147+ expect ( buttons [ 1 ] ! . style . getPropertyValue ( '--dock-scale' ) ) . toBe ( '' )
148+ expect ( shelf . style . getPropertyValue ( '--dock-side-expansion' ) ) . toBe ( '' )
149+ expect ( shelf . classList . contains ( 'is-magnifying' ) ) . toBe ( false )
150+
151+ await act ( async ( ) => dock . dispatchEvent ( pointer ( 'pointermove' , 178 , 'touch' ) ) )
152+ expect ( buttons [ 1 ] ! . style . getPropertyValue ( '--dock-scale' ) ) . toBe ( '' )
153+ } finally {
154+ await act ( async ( ) => root . unmount ( ) )
155+ host . remove ( )
156+ HTMLElement . prototype . getBoundingClientRect = originalRect
157+ }
158+ } )
159+ } )
160+
23161describe ( 'DesktopShell fixed docks' , ( ) => {
24162 it ( 'keeps a non-closable right dock open and omits all window controls' , ( ) => {
25163 const persistedClosedLayout : DesktopLayoutSnapshot = {
@@ -320,6 +458,7 @@ describe('DesktopShell fixed docks', () => {
320458 { ...dockedApp , id : 'floating-app' , placement : 'floating' , defaultOpen : true } ,
321459 { ...dockedApp , defaultOpen : true } ,
322460 ] }
461+ desktopItems = { desktopFiles }
323462 layoutPersistence = { { load : ( ) => persistedOverlayLayout } }
324463 settingsSlot = { < button type = "button" > Setting action</ button > }
325464 /> ,
@@ -370,8 +509,8 @@ describe('DesktopShell fixed docks', () => {
370509 } )
371510 expect ( panel ?. style . left ) . toBe ( '558px' )
372511
373- const desktopShortcut = host . querySelector < HTMLButtonElement > ( '.detective-desktop__shortcut ' )
374- await act ( async ( ) => desktopShortcut ?. click ( ) )
512+ const desktopItem = host . querySelector < HTMLButtonElement > ( '.detective-desktop__item ' )
513+ await act ( async ( ) => desktopItem ?. click ( ) )
375514 expect ( host . querySelector ( '.detective-settings-panel' ) ) . not . toBeNull ( )
376515 expect ( panel ?. style . left ) . toBe ( '558px' )
377516
0 commit comments