@@ -7,11 +7,14 @@ import {
77 createDockEntryOrderSnapshot ,
88 DOCK_ORDER_SCOPE_BY_POSITION ,
99 DOCK_ORDER_SCOPES ,
10+ getCurrentDockEntryOrderSnapshot ,
1011 getDockEntryOrderSnapshot ,
1112 getDockOrderContainer ,
1213 isDockOrderScope ,
1314 mergeCurrentDockEntryOrders ,
1415 mergeDockEntryOrderSnapshot ,
16+ moveDockEntryOrderSnapshot ,
17+ type TDockEntryMover ,
1518 type IDockOrderLayout ,
1619 type TDockOrderSnapshot ,
1720} from "./dockOrder" ;
@@ -36,6 +39,18 @@ class FakeDockItem {
3639 }
3740 return null ;
3841 }
42+
43+ public before ( item : FakeDockItem ) {
44+ this . parent ?. insertBefore ( item , this ) ;
45+ }
46+
47+ public after ( item : FakeDockItem ) {
48+ this . parent ?. insertBefore ( item , this . parent . children [ this . parent . children . indexOf ( this ) + 1 ] ) ;
49+ }
50+
51+ public get parentElement ( ) {
52+ return this . parent ;
53+ }
3954}
4055
4156class FakeDockContainer {
@@ -46,13 +61,21 @@ class FakeDockContainer {
4661 }
4762
4863 public append ( item : FakeDockItem ) {
64+ this . insertBefore ( item ) ;
65+ }
66+
67+ public insertBefore ( item : FakeDockItem , reference ?: FakeDockItem ) {
68+ let index = reference ? this . children . indexOf ( reference ) : this . children . length ;
4969 if ( item . parent ) {
5070 const oldIndex = item . parent . children . indexOf ( item ) ;
5171 if ( oldIndex > - 1 ) {
5272 item . parent . children . splice ( oldIndex , 1 ) ;
73+ if ( item . parent === this && oldIndex < index ) {
74+ index -- ;
75+ }
5376 }
5477 }
55- this . children . push ( item ) ;
78+ this . children . splice ( index < 0 ? this . children . length : index , 0 , item ) ;
5679 item . parent = this ;
5780 }
5881}
@@ -129,6 +152,22 @@ test("runtime dock snapshot reads direct items, keeps hidden items, and uses sta
129152 assert . equal ( hiddenOutline . classList . contains ( "fn__none" ) , true ) ;
130153} ) ;
131154
155+ test ( "current dock snapshot contains only DOM entries without catalog defaults" , ( ) => {
156+ const plugin = new FakeDockItem ( "plugin-runtime-type" , { entryId : "plugin:sample:dock" } ) ;
157+ const outline = new FakeDockItem ( "outline" ) ;
158+ const containers = [
159+ new FakeDockContainer ( [ outline , plugin ] ) ,
160+ ...Array . from ( { length : 5 } , ( ) => new FakeDockContainer ( ) ) ,
161+ ] ;
162+
163+ const current = getCurrentDockEntryOrderSnapshot ( layoutWith ( containers ) ) ;
164+
165+ assert . deepEqual ( current [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] , [ "outline" , "plugin:sample:dock" ] ) ;
166+ assert . deepEqual ( DOCK_ORDER_SCOPES . flatMap ( ( scope ) => current [ scope ] ) , [ "outline" , "plugin:sample:dock" ] ) ;
167+ assert . equal ( getDockEntryOrderSnapshot ( layoutWith ( containers ) ) [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ]
168+ . includes ( "file" ) , true ) ;
169+ } ) ;
170+
132171test ( "merging a dock snapshot preserves an unavailable plugin key at its saved slot" , ( ) => {
133172 const defaults = emptySnapshot ( ) ;
134173 defaults [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] = [ "file" , "outline" ] ;
@@ -151,6 +190,36 @@ test("synchronizing a dock snapshot keeps the current loaded order and preserves
151190 [ "outline" , "plugin:disabled:dock" , "file" ] ) ;
152191} ) ;
153192
193+ test ( "synchronizing uses current placement while preserving globally unique unavailable plugins" , ( ) => {
194+ const current = emptySnapshot ( ) ;
195+ current [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] = [ "outline" ] ;
196+ current [ DOCK_ORDER_SCOPE_BY_POSITION . RightBottom ] = [ "file" ] ;
197+ const merged = mergeCurrentDockEntryOrders ( current , {
198+ [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] : [ "file" , "plugin:disabled:dock" , "outline" ] ,
199+ [ DOCK_ORDER_SCOPE_BY_POSITION . RightBottom ] : [ "plugin:disabled:dock" , "file" ] ,
200+ } ) ;
201+
202+ assert . deepEqual ( merged [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] , [ "plugin:disabled:dock" , "outline" ] ) ;
203+ assert . deepEqual ( merged [ DOCK_ORDER_SCOPE_BY_POSITION . RightBottom ] , [ "file" ] ) ;
204+ assert . equal ( DOCK_ORDER_SCOPES . flatMap ( ( scope ) => merged [ scope ] )
205+ . filter ( ( key ) => key === "plugin:disabled:dock" ) . length , 1 ) ;
206+ assert . equal ( DOCK_ORDER_SCOPES . flatMap ( ( scope ) => merged [ scope ] )
207+ . filter ( ( key ) => key === "file" ) . length , 1 ) ;
208+ } ) ;
209+
210+ test ( "synchronizing filters stale loaded placements before preserving unknown slots" , ( ) => {
211+ const current = emptySnapshot ( ) ;
212+ current [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] = [ "loaded" ] ;
213+ const merged = mergeCurrentDockEntryOrders ( current , {
214+ [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] : [ "loaded" ] ,
215+ [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] : [ "unknown-before" , "loaded" , "unknown-after" ] ,
216+ } ) ;
217+
218+ assert . deepEqual ( merged [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] , [ ] ) ;
219+ assert . deepEqual ( merged [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] ,
220+ [ "unknown-before" , "loaded" , "unknown-after" ] ) ;
221+ } ) ;
222+
154223test ( "profiles without dock orders preserve the current six-slot layout" , ( ) => {
155224 const current = emptySnapshot ( ) ;
156225 current [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] = [ "outline" , "file" ] ;
@@ -159,17 +228,119 @@ test("profiles without dock orders preserve the current six-slot layout", () =>
159228 assert . deepEqual ( mergeDockEntryOrderSnapshot ( current ) , current ) ;
160229} ) ;
161230
162- test ( "merging a dock snapshot removes a loaded entry from its stale previous slot " , ( ) => {
231+ test ( "merging a dock snapshot applies saved placement and the first saved scope owns duplicate keys " , ( ) => {
163232 const current = emptySnapshot ( ) ;
164- current [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] = [ "outline" ] ;
233+ current [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] = [ "file" , " outline"] ;
165234 current [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] = [ "file" , "graph" ] ;
166235 const merged = mergeDockEntryOrderSnapshot ( current , {
167- [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] : [ "file" , "outline" , "plugin:disabled:dock" ] ,
168- [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] : [ "graph" , "file" ] ,
236+ [ DOCK_ORDER_SCOPE_BY_POSITION . LeftBottom ] : [ "file" , "plugin:disabled:dock" ] ,
237+ [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] : [ "graph" , "file" , "plugin:disabled:dock" ] ,
238+ } ) ;
239+
240+ assert . deepEqual ( merged [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] , [ "outline" ] ) ;
241+ assert . deepEqual ( merged [ DOCK_ORDER_SCOPE_BY_POSITION . LeftBottom ] , [ "file" , "plugin:disabled:dock" ] ) ;
242+ assert . deepEqual ( merged [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] , [ "graph" ] ) ;
243+ assert . equal ( DOCK_ORDER_SCOPES . flatMap ( ( scope ) => merged [ scope ] ) . filter ( ( key ) => key === "file" ) . length , 1 ) ;
244+ assert . equal ( DOCK_ORDER_SCOPES . flatMap ( ( scope ) => merged [ scope ] )
245+ . filter ( ( key ) => key === "plugin:disabled:dock" ) . length , 1 ) ;
246+ } ) ;
247+
248+ test ( "merging a partial saved snapshot moves declared entries and keeps undeclared current entries" , ( ) => {
249+ const current = emptySnapshot ( ) ;
250+ current [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] = [ "file" , "outline" ] ;
251+ current [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] = [ "graph" ] ;
252+
253+ const merged = mergeDockEntryOrderSnapshot ( current , {
254+ [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] : [ "file" , "graph" ] ,
255+ } ) ;
256+
257+ assert . deepEqual ( merged [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] , [ "outline" ] ) ;
258+ assert . deepEqual ( merged [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] , [ "file" , "graph" ] ) ;
259+ } ) ;
260+
261+ test ( "merging keeps the saved order of loaded entries moved from another scope" , ( ) => {
262+ const current = emptySnapshot ( ) ;
263+ current [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] = [ "new-entry" , "file" , "outline" ] ;
264+ current [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] = [ "graph" ] ;
265+
266+ const merged = mergeDockEntryOrderSnapshot ( current , {
267+ [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] : [ "file" , "graph" , "outline" ] ,
169268 } ) ;
170269
171- assert . deepEqual ( merged [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] , [ "outline" , "plugin:disabled:dock" ] ) ;
172- assert . deepEqual ( merged [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] , [ "graph" , "file" ] ) ;
270+ assert . deepEqual ( merged [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] ,
271+ [ "new-entry" , "file" , "graph" , "outline" ] ) ;
272+ assert . deepEqual ( merged [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] , [ ] ) ;
273+ } ) ;
274+
275+ test ( "merging saved placement is idempotent and keeps new current entries" , ( ) => {
276+ const current = emptySnapshot ( ) ;
277+ current [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] = [ "outline" , "new-left" ] ;
278+ current [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] = [ "file" , "graph" ] ;
279+ const saved = {
280+ [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] : [ "file" , "plugin:disabled:dock" , "outline" ] ,
281+ [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] : [ "graph" , "file" ] ,
282+ } ;
283+
284+ const merged = mergeDockEntryOrderSnapshot ( current , saved ) ;
285+
286+ assert . deepEqual ( merged [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] ,
287+ [ "file" , "plugin:disabled:dock" , "outline" , "new-left" ] ) ;
288+ assert . deepEqual ( merged [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] , [ "graph" ] ) ;
289+ assert . deepEqual ( mergeDockEntryOrderSnapshot ( merged , saved ) , merged ) ;
290+ } ) ;
291+
292+ test ( "moving a dock entry snapshot is atomic across scopes and supports empty targets" , ( ) => {
293+ const snapshot = emptySnapshot ( ) ;
294+ snapshot [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] = [ "file" , "outline" ] ;
295+ snapshot [ DOCK_ORDER_SCOPE_BY_POSITION . RightBottom ] = [ "backlink" ] ;
296+
297+ const crossed = moveDockEntryOrderSnapshot (
298+ snapshot ,
299+ "file" ,
300+ DOCK_ORDER_SCOPE_BY_POSITION . RightBottom ,
301+ "backlink" ,
302+ ) ;
303+ assert . deepEqual ( crossed ?. [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] , [ "outline" ] ) ;
304+ assert . deepEqual ( crossed ?. [ DOCK_ORDER_SCOPE_BY_POSITION . RightBottom ] , [ "file" , "backlink" ] ) ;
305+ assert . equal ( DOCK_ORDER_SCOPES . flatMap ( ( scope ) => crossed ?. [ scope ] || [ ] )
306+ . filter ( ( key ) => key === "file" ) . length , 1 ) ;
307+
308+ const emptied = moveDockEntryOrderSnapshot (
309+ crossed ,
310+ "file" ,
311+ DOCK_ORDER_SCOPE_BY_POSITION . BottomLeft ,
312+ ) ;
313+ assert . deepEqual ( emptied ?. [ DOCK_ORDER_SCOPE_BY_POSITION . RightBottom ] , [ "backlink" ] ) ;
314+ assert . deepEqual ( emptied ?. [ DOCK_ORDER_SCOPE_BY_POSITION . BottomLeft ] , [ "file" ] ) ;
315+ } ) ;
316+
317+ test ( "moving within one dock scope detects invalid and no-op drops" , ( ) => {
318+ const snapshot = emptySnapshot ( ) ;
319+ snapshot [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] = [ "file" , "outline" , "inbox" ] ;
320+
321+ assert . deepEqual ( moveDockEntryOrderSnapshot (
322+ snapshot ,
323+ "inbox" ,
324+ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ,
325+ "file" ,
326+ ) ?. [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] , [ "inbox" , "file" , "outline" ] ) ;
327+ assert . equal ( moveDockEntryOrderSnapshot (
328+ snapshot ,
329+ "file" ,
330+ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ,
331+ "outline" ,
332+ ) , undefined ) ;
333+ assert . equal ( moveDockEntryOrderSnapshot (
334+ snapshot ,
335+ "file" ,
336+ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ,
337+ "file" ,
338+ ) , undefined ) ;
339+ assert . equal ( moveDockEntryOrderSnapshot (
340+ snapshot ,
341+ "missing" ,
342+ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ,
343+ ) , undefined ) ;
173344} ) ;
174345
175346test ( "applying dock order sorts hidden items and keeps an unknown plugin in its current slot" , ( ) => {
@@ -190,11 +361,100 @@ test("applying dock order sorts hidden items and keeps an unknown plugin in its
190361 assert . equal ( asElement ( file ) . dataset . type , "file" ) ;
191362} ) ;
192363
364+ test ( "applying dock order moves entries across containers and is idempotent" , ( ) => {
365+ const file = new FakeDockItem ( "file" ) ;
366+ const outline = new FakeDockItem ( "outline" , { hidden : true } ) ;
367+ const graph = new FakeDockItem ( "graph" ) ;
368+ const leftTop = new FakeDockContainer ( [ file , outline ] ) ;
369+ const rightTop = new FakeDockContainer ( [ graph ] ) ;
370+ const containers = [
371+ leftTop ,
372+ new FakeDockContainer ( ) ,
373+ rightTop ,
374+ ...Array . from ( { length : 3 } , ( ) => new FakeDockContainer ( ) ) ,
375+ ] ;
376+ const layout = layoutWith ( containers ) ;
377+ const snapshot = emptySnapshot ( ) ;
378+ snapshot [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] = [ "outline" ] ;
379+ snapshot [ DOCK_ORDER_SCOPE_BY_POSITION . RightTop ] = [ "graph" , "file" ] ;
380+
381+ assert . equal ( applyDockEntryOrderSnapshot ( snapshot , layout ) , true ) ;
382+ assert . deepEqual ( leftTop . children , [ outline ] ) ;
383+ assert . deepEqual ( rightTop . children , [ graph , file ] ) ;
384+ assert . equal ( file . parent , rightTop ) ;
385+ assert . equal ( outline . classList . contains ( "fn__none" ) , true ) ;
386+ assert . equal ( applyDockEntryOrderSnapshot ( snapshot , layout ) , false ) ;
387+ } ) ;
388+
389+ test ( "applying dock order invokes the mover for cross-scope and same-scope changes" , ( ) => {
390+ const first = new FakeDockItem ( "first" ) ;
391+ const second = new FakeDockItem ( "second" ) ;
392+ const third = new FakeDockItem ( "third" ) ;
393+ const fourth = new FakeDockItem ( "fourth" ) ;
394+ const leftTop = new FakeDockContainer ( [ first , second ] ) ;
395+ const rightTop = new FakeDockContainer ( [ third , fourth ] ) ;
396+ const containers = [
397+ leftTop ,
398+ new FakeDockContainer ( ) ,
399+ rightTop ,
400+ ...Array . from ( { length : 3 } , ( ) => new FakeDockContainer ( ) ) ,
401+ ] ;
402+ const layout = layoutWith ( containers ) ;
403+ const snapshot = emptySnapshot ( ) ;
404+ snapshot [ DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ] = [
405+ "fourth" ,
406+ "plugin:disabled:dock" ,
407+ "second" ,
408+ "first" ,
409+ "third" ,
410+ ] ;
411+ const calls : Array < { scope : string ; type ?: string ; previousType ?: string } > = [ ] ;
412+ const mover : TDockEntryMover = ( scope , item , previousItem ) => {
413+ calls . push ( {
414+ scope,
415+ type : item . dataset . type ,
416+ previousType : previousItem ?. dataset . type ,
417+ } ) ;
418+ const target = getDockOrderContainer ( scope , layout ) as unknown as FakeDockContainer ;
419+ if ( previousItem ) {
420+ ( previousItem as unknown as FakeDockItem ) . after ( item as unknown as FakeDockItem ) ;
421+ } else {
422+ target . insertBefore ( item as unknown as FakeDockItem , target . children [ 0 ] ) ;
423+ }
424+ } ;
425+
426+ applyDockEntryOrderSnapshot ( snapshot , layout , mover ) ;
427+
428+ assert . deepEqual ( calls , [ {
429+ scope : DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ,
430+ type : "fourth" ,
431+ previousType : undefined ,
432+ } , {
433+ scope : DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ,
434+ type : "third" ,
435+ previousType : "first" ,
436+ } , {
437+ scope : DOCK_ORDER_SCOPE_BY_POSITION . LeftTop ,
438+ type : "second" ,
439+ previousType : "fourth" ,
440+ } ] ) ;
441+ assert . deepEqual ( leftTop . children , [ fourth , second , first , third ] ) ;
442+ assert . deepEqual ( rightTop . children , [ ] ) ;
443+ calls . length = 0 ;
444+ assert . equal ( applyDockEntryOrderSnapshot ( snapshot , layout , mover ) , false ) ;
445+ assert . deepEqual ( calls , [ ] ) ;
446+ } ) ;
447+
193448test ( "dock ordering is applied at runtime and synchronized after direct dock moves" , ( ) => {
194449 const runtimeSource = readFileSync ( resolve ( process . cwd ( ) , "src/config/entryVisibility/runtime.ts" ) , "utf8" ) ;
195450 const dockSource = readFileSync ( resolve ( process . cwd ( ) , "src/layout/dock/index.ts" ) , "utf8" ) ;
451+ const uiSource = readFileSync ( resolve ( process . cwd ( ) , "src/config/entryVisibility/ui.ts" ) , "utf8" ) ;
196452
197453 assert . match ( runtimeSource , / a p p l y D o c k E n t r y O r d e r S n a p s h o t \( m e r g e D o c k E n t r y O r d e r S n a p s h o t \( / ) ;
454+ assert . match ( runtimeSource , / c o n s t c u r r e n t O r d e r s = g e t C u r r e n t D o c k E n t r y O r d e r S n a p s h o t \( \) ; / ) ;
455+ assert . match ( runtimeSource , / \{ s y n c E n t r y O r d e r s : f a l s e \} / ) ;
198456 assert . match ( runtimeSource , / c o n s t k e y = g e t D o c k E n t r y K e y \( i t e m \) ; / ) ;
199- assert . match ( dockSource , / s y n c D o c k E n t r y O r d e r s \( \) ; / ) ;
457+ assert . match ( dockSource , / o p t i o n s \. s y n c E n t r y O r d e r s ! = = f a l s e / ) ;
458+ assert . match ( uiSource , / d a t a - e n t r y - d r o p - s c o p e / ) ;
459+ assert . match ( uiSource , / m o v e D o c k E n t r y O r d e r S n a p s h o t \( / ) ;
200460} ) ;
0 commit comments