11import { Component } from '@angular/core' ;
22import { ComponentFixture , TestBed } from '@angular/core/testing' ;
33import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed' ;
4+ import { By } from '@angular/platform-browser' ;
45import { TabsDirective } from './tabs.directive' ;
56import { TabTriggerDirective } from './tab-trigger.directive' ;
67import { TabPanelDirective } from './tab-panel.directive' ;
78import { TabListDirective } from './tabs-list.directive' ;
89import { TabsHarness } from './tabs.harness' ;
10+ import { TabsState } from './tabs.state' ;
911
1012@Component ( {
1113 template : `
@@ -65,4 +67,105 @@ describe('TabsDirective Accessibility', () => {
6567 const activeTab = await harness . getActiveTab ( ) ;
6668 expect ( await activeTab ?. isActive ( ) ) . toBe ( true ) ;
6769 } ) ;
70+
71+ // -------------------------------------------------------------------------
72+ // Cleanup / ngOnDestroy
73+ // -------------------------------------------------------------------------
74+
75+ it ( 'should maintain tab state isolation across component instances' , async ( ) => {
76+ // First instance
77+ const fixture1 = TestBed . createComponent ( TestTabsComponent ) ;
78+ const harness1 = await TestbedHarnessEnvironment . harnessForFixture ( fixture1 , TabsHarness ) ;
79+
80+ await harness1 . selectTabByText ( 'Tab 2' ) ;
81+ let activeTab = await harness1 . getActiveTab ( ) ;
82+ expect ( await activeTab ?. getText ( ) ) . toBe ( 'Tab 2' ) ;
83+
84+ fixture1 . destroy ( ) ;
85+
86+ // Second instance should start fresh
87+ const fixture2 = TestBed . createComponent ( TestTabsComponent ) ;
88+ const harness2 = await TestbedHarnessEnvironment . harnessForFixture ( fixture2 , TabsHarness ) ;
89+
90+ activeTab = await harness2 . getActiveTab ( ) ;
91+ expect ( await activeTab ?. getText ( ) ) . toBe ( 'Tab 1' ) ; // Should be first tab
92+ } ) ;
93+
94+ it ( 'should clean up keyboard listeners on destroy' , async ( ) => {
95+ const fixture = TestBed . createComponent ( TestTabsComponent ) ;
96+ const harness = await TestbedHarnessEnvironment . harnessForFixture ( fixture , TabsHarness ) ;
97+
98+ const triggers = await harness . getTabTriggers ( ) ;
99+ expect ( triggers . length ) . toBeGreaterThan ( 0 ) ;
100+
101+ // Just select a tab to engage the directive
102+ await harness . selectTabByText ( 'Tab 2' ) ;
103+
104+ // Destroy component
105+ fixture . destroy ( ) ;
106+
107+ // Should not error on subsequent operations
108+ expect ( ( ) => {
109+ document . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'ArrowRight' } ) ) ;
110+ } ) . not . toThrow ( ) ;
111+ } ) ;
112+
113+ it ( 'should not leak tab state on component destruction' , async ( ) => {
114+ const fixture = TestBed . createComponent ( TestTabsComponent ) ;
115+ const harness = await TestbedHarnessEnvironment . harnessForFixture ( fixture , TabsHarness ) ;
116+
117+ await harness . selectTabByText ( 'Tab 2' ) ;
118+ let activeTab = await harness . getActiveTab ( ) ;
119+ expect ( await activeTab ?. getText ( ) ) . toBe ( 'Tab 2' ) ;
120+
121+ // Verify tabs are in DOM
122+ const panels1 = await harness . getTabPanels ( ) ;
123+ expect ( panels1 . length ) . toBe ( 2 ) ;
124+
125+ // Destroy
126+ fixture . destroy ( ) ;
127+
128+ // Create new fixture with clean state
129+ const fixture2 = TestBed . createComponent ( TestTabsComponent ) ;
130+ const harness2 = await TestbedHarnessEnvironment . harnessForFixture ( fixture2 , TabsHarness ) ;
131+
132+ // First tab should be active in new instance
133+ activeTab = await harness2 . getActiveTab ( ) ;
134+ expect ( await activeTab ?. getText ( ) ) . toBe ( 'Tab 1' ) ;
135+
136+ const panels2 = await harness2 . getTabPanels ( ) ;
137+ expect ( panels2 . length ) . toBe ( 2 ) ;
138+ } ) ;
139+
140+ it ( 'should safely destroy even if no tabs are selected' , ( ) => {
141+ const fixture = TestBed . createComponent ( TestTabsComponent ) ;
142+
143+ expect ( ( ) => {
144+ fixture . destroy ( ) ;
145+ } ) . not . toThrow ( ) ;
146+ } ) ;
147+
148+ it ( 'should unsubscribe from all effects on destroy' , async ( ) => {
149+ const fixture = TestBed . createComponent ( TestTabsComponent ) ;
150+
151+ // Get the state service injected into the directive
152+ const tabsDir = fixture . debugElement . query ( By . directive ( TabsDirective ) ) ;
153+ const tabsState = tabsDir ?. injector . get ( TabsState ) ;
154+
155+ if ( tabsState ) {
156+ // Verify initial state
157+ expect ( tabsState ) . toBeTruthy ( ) ;
158+
159+ // Destroy
160+ fixture . destroy ( ) ;
161+
162+ // Creating new fixture should not be affected by old state
163+ const fixture2 = TestBed . createComponent ( TestTabsComponent ) ;
164+ const tabsState2 = fixture2 . debugElement . query ( By . directive ( TabsDirective ) )
165+ ?. injector . get ( TabsState ) ;
166+
167+ expect ( tabsState2 ) . toBeTruthy ( ) ;
168+ expect ( tabsState ) . not . toBe ( tabsState2 ) ;
169+ }
170+ } ) ;
68171} ) ;
0 commit comments