22 * Unit tests for tab visibility behavior in admin.js.
33 */
44
5- import { afterAll , beforeAll , beforeEach , describe , expect , test , vi } from "vitest" ;
5+ import {
6+ afterAll ,
7+ afterEach ,
8+ beforeAll ,
9+ beforeEach ,
10+ describe ,
11+ expect ,
12+ test ,
13+ vi ,
14+ } from "vitest" ;
615import { cleanupAdminJs , loadAdminJs } from "./helpers/admin-env.js" ;
716
817let win ;
@@ -96,7 +105,8 @@ describe("showTab hidden tab fallback", () => {
96105
97106describe ( "showTab idempotency" , ( ) => {
98107 test ( "does not re-process a tab that is already visible" , ( ) => {
99- const { panel : overviewPanel , link : overviewLink } = createTab ( "overview" ) ;
108+ const { panel : overviewPanel , link : overviewLink } =
109+ createTab ( "overview" ) ;
100110 createTab ( "gateways" ) ;
101111
102112 win . showTab ( "overview" ) ;
@@ -274,8 +284,14 @@ describe("renderGlobalSearchResults hidden section filtering", () => {
274284 win . renderGlobalSearchResults ( {
275285 groups : [
276286 { entity_type : "tools" , items : [ { id : "t1" , name : "Tool 1" } ] } ,
277- { entity_type : "gateways" , items : [ { id : "g1" , name : "GW 1" } ] } ,
278- { entity_type : "prompts" , items : [ { id : "p1" , name : "Prompt 1" } ] } ,
287+ {
288+ entity_type : "gateways" ,
289+ items : [ { id : "g1" , name : "GW 1" } ] ,
290+ } ,
291+ {
292+ entity_type : "prompts" ,
293+ items : [ { id : "p1" , name : "Prompt 1" } ] ,
294+ } ,
279295 ] ,
280296 } ) ;
281297
@@ -311,17 +327,18 @@ describe("runGlobalSearch visible entity filtering", () => {
311327 win . ROOT_PATH = "" ;
312328 win . IS_ADMIN = true ;
313329 win . UI_HIDDEN_SECTIONS = [ "tools" , "prompts" , "teams" ] ;
314- const fetchSpy = vi
315- . spyOn ( win , "fetchWithAuth" )
316- . mockResolvedValue ( {
317- ok : true ,
318- json : async ( ) => ( { groups : [ ] } ) ,
319- } ) ;
330+ const fetchSpy = vi . spyOn ( win , "fetchWithAuth" ) . mockResolvedValue ( {
331+ ok : true ,
332+ json : async ( ) => ( { groups : [ ] } ) ,
333+ } ) ;
320334
321335 await win . runGlobalSearch ( "gateway" ) ;
322336
323337 expect ( fetchSpy ) . toHaveBeenCalledTimes ( 1 ) ;
324- const requestUrl = new URL ( fetchSpy . mock . calls [ 0 ] [ 0 ] , "http://localhost" ) ;
338+ const requestUrl = new URL (
339+ fetchSpy . mock . calls [ 0 ] [ 0 ] ,
340+ "http://localhost" ,
341+ ) ;
325342 expect ( requestUrl . searchParams . get ( "entity_types" ) ) . toBe (
326343 "servers,gateways,resources,agents,users" ,
327344 ) ;
@@ -349,7 +366,9 @@ describe("runGlobalSearch visible entity filtering", () => {
349366 await win . runGlobalSearch ( "anything" ) ;
350367
351368 expect ( fetchSpy ) . not . toHaveBeenCalled ( ) ;
352- expect ( container . innerHTML ) . toContain ( "No searchable sections are visible" ) ;
369+ expect ( container . innerHTML ) . toContain (
370+ "No searchable sections are visible" ,
371+ ) ;
353372 } ) ;
354373} ) ;
355374
@@ -421,8 +440,10 @@ describe("resolveTabForNavigation", () => {
421440
422441describe ( "showTab normal navigation" , ( ) => {
423442 test ( "shows the requested visible tab and hides others" , ( ) => {
424- const { panel : overviewPanel , link : overviewLink } = createTab ( "overview" ) ;
425- const { panel : gatewaysPanel , link : gatewaysLink } = createTab ( "gateways" ) ;
443+ const { panel : overviewPanel , link : overviewLink } =
444+ createTab ( "overview" ) ;
445+ const { panel : gatewaysPanel , link : gatewaysLink } =
446+ createTab ( "gateways" ) ;
426447
427448 win . showTab ( "gateways" ) ;
428449
@@ -433,6 +454,61 @@ describe("showTab normal navigation", () => {
433454 } ) ;
434455} ) ;
435456
457+ describe ( "showTab scroll reset (#3748)" , ( ) => {
458+ let originalRAF ;
459+
460+ beforeEach ( ( ) => {
461+ // JSDOM does not provide requestAnimationFrame; install a synchronous
462+ // shim on the JSDOM window so the production code path executes.
463+ originalRAF = win . requestAnimationFrame ;
464+ win . requestAnimationFrame = ( cb ) => {
465+ cb ( ) ;
466+ return 0 ;
467+ } ;
468+ } ) ;
469+ afterEach ( ( ) => {
470+ win . requestAnimationFrame = originalRAF ;
471+ } ) ;
472+
473+ test ( "resets scrollTop on the data-scroll-container element when switching tabs" , ( ) => {
474+ createTab ( "overview" ) ;
475+ createTab ( "gateways" ) ;
476+
477+ const scrollContainer = doc . createElement ( "main" ) ;
478+ scrollContainer . setAttribute ( "data-scroll-container" , "" ) ;
479+ scrollContainer . className = "overflow-y-auto" ;
480+ scrollContainer . scrollTop = 500 ;
481+ doc . body . appendChild ( scrollContainer ) ;
482+
483+ win . showTab ( "gateways" ) ;
484+
485+ expect ( scrollContainer . scrollTop ) . toBe ( 0 ) ;
486+ } ) ;
487+
488+ test ( "falls back to main.overflow-y-auto when data-scroll-container is absent" , ( ) => {
489+ createTab ( "overview" ) ;
490+ createTab ( "gateways" ) ;
491+
492+ const mainEl = doc . createElement ( "main" ) ;
493+ mainEl . className = "overflow-y-auto" ;
494+ mainEl . scrollTop = 300 ;
495+ doc . body . appendChild ( mainEl ) ;
496+
497+ win . showTab ( "gateways" ) ;
498+
499+ expect ( mainEl . scrollTop ) . toBe ( 0 ) ;
500+ } ) ;
501+
502+ test ( "does not throw when no scroll container exists" , ( ) => {
503+ createTab ( "overview" ) ;
504+ createTab ( "gateways" ) ;
505+
506+ expect ( ( ) => {
507+ win . showTab ( "gateways" ) ;
508+ } ) . not . toThrow ( ) ;
509+ } ) ;
510+ } ) ;
511+
436512describe ( "getDefaultTabName edge cases" , ( ) => {
437513 test ( "returns gateways fallback when all tabs are hidden and no panels exist" , ( ) => {
438514 // No tabs created, nothing available
0 commit comments