Skip to content

Commit e5d1771

Browse files
authored
Fix flaky feature flag test. (#6266)
Feature flag tests have been flaky since the Angular 13/14 upgrade. The problematic change was actually #6056, where we started using resetSelectors everywhere. There is one test where a dialog with FeatureFlagDialogContainer is opened. Sometimes FeatureFlagDialogContainer would not complete initialization until after the test was torn down and the selectors reset, meaning NgRx selector calls would fail since state is undefined at that point. To workaround this problem, I allow FeatureFlagDialogContainer to be replaced by a simpler TestableFeatureFlagDialogComponent that has no dependencies on NgRx. This is a strategy we've used elsewhere in the code: https://github.com/tensorflow/tensorboard/blob/dca18190a18ac151437bb6fcd128bc49af617ae5/tensorboard/webapp/metrics/views/card_renderer/scalar_card_container.ts#L206 While I was in this code, I did some other cleanup: * Stop running the feature flag view tests twice. Only run them as part of the feature flag test suite. * When FeatureFlagModalTriggerContainer is destroyed, unsubscribe from some subjects created in ngOnInit. * Remove some unnecessary setup from FeatureFlagModalTriggerContainer tests. Alternative solution: * I thought I could solve the problem by calling dialog.close() in the tests but observed that closing the dialog is not enough to prevent the child component from instantiating after the test.
1 parent e659943 commit e5d1771

File tree

5 files changed

+60
-48
lines changed

5 files changed

+60
-48
lines changed

tensorboard/webapp/BUILD

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,6 @@ tf_ng_web_test_suite(
264264
"//tensorboard/webapp/core/views:test_lib",
265265
"//tensorboard/webapp/customization:customization_test_lib",
266266
"//tensorboard/webapp/deeplink:deeplink_test_lib",
267-
"//tensorboard/webapp/feature_flag/views:views_test",
268267
"//tensorboard/webapp/header:test_lib",
269268
"//tensorboard/webapp/metrics:integration_test",
270269
"//tensorboard/webapp/metrics:test_lib",
@@ -288,7 +287,6 @@ tf_ng_web_test_suite(
288287
"//tensorboard/webapp/runs/views/runs_table:runs_table_test",
289288
"//tensorboard/webapp/tbdev_upload:test_lib",
290289
"//tensorboard/webapp/util:util_tests",
291-
"//tensorboard/webapp/webapp_data_source:feature_flag_test_lib",
292290
"//tensorboard/webapp/webapp_data_source:http_client_test",
293291
"//tensorboard/webapp/webapp_data_source:webapp_data_source_test_lib",
294292
"//tensorboard/webapp/widgets:resize_detector_test",

tensorboard/webapp/feature_flag/BUILD

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,9 @@ tf_ts_library(
4343
tf_ng_web_test_suite(
4444
name = "karma_test",
4545
deps = [
46-
":test_lib",
4746
"//tensorboard/webapp/feature_flag/effects:effects_test_lib",
4847
"//tensorboard/webapp/feature_flag/http:http_test_lib",
4948
"//tensorboard/webapp/feature_flag/store:store_test_lib",
50-
"//tensorboard/webapp/feature_flag/views:views_test",
49+
"//tensorboard/webapp/feature_flag/views:views_test_lib",
5150
],
5251
)

tensorboard/webapp/feature_flag/views/BUILD

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("//tensorboard/defs:defs.bzl", "tf_ng_module", "tf_sass_binary", "tf_ts_library")
1+
load("//tensorboard/defs:defs.bzl", "tf_ng_module", "tf_sass_binary")
22

33
package(default_visibility = ["//tensorboard:internal"])
44

@@ -23,6 +23,7 @@ tf_ng_module(
2323
deps = [
2424
":feature_flag_dialog",
2525
"//tensorboard/webapp:app_state",
26+
"//tensorboard/webapp/angular:expect_angular_cdk_overlay",
2627
"//tensorboard/webapp/angular:expect_angular_material_dialog",
2728
"//tensorboard/webapp/feature_flag/actions",
2829
"//tensorboard/webapp/feature_flag/store",
@@ -61,8 +62,8 @@ tf_ng_module(
6162
],
6263
)
6364

64-
tf_ts_library(
65-
name = "views_test",
65+
tf_ng_module(
66+
name = "views_test_lib",
6667
testonly = True,
6768
srcs = [
6869
"feature_flag_dialog_test.ts",

tensorboard/webapp/feature_flag/views/feature_flag_modal_trigger_container.ts

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
See the License for the specific language governing permissions and
1313
limitations under the License.
1414
==============================================================================*/
15-
import {Component, OnInit} from '@angular/core';
15+
import {ComponentType} from '@angular/cdk/overlay';
16+
import {Component, OnDestroy, OnInit} from '@angular/core';
1617
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
1718
import {Store} from '@ngrx/store';
19+
import {Subject, takeUntil} from 'rxjs';
1820
import {State} from '../../app_state';
1921
import {featureFlagOverridesReset} from '../actions/feature_flag_actions';
2022
import {getShowFlagsEnabled} from '../store/feature_flag_selectors';
@@ -31,38 +33,54 @@ const util = {
3133
template: ``,
3234
styles: [],
3335
})
34-
export class FeatureFlagModalTriggerContainer implements OnInit {
36+
export class FeatureFlagModalTriggerContainer implements OnInit, OnDestroy {
37+
// Allow the dialog component type to be overriden for testing purposes.
38+
featureFlagDialogType: ComponentType<any> = FeatureFlagDialogContainer;
39+
3540
readonly showFeatureFlags$ = this.store.select(getShowFlagsEnabled);
3641
private featureFlagsDialog?: MatDialogRef<FeatureFlagDialogContainer>;
42+
private ngUnsubscribe = new Subject<void>();
3743

3844
constructor(
3945
private readonly store: Store<State>,
4046
private dialog: MatDialog
4147
) {}
4248

4349
ngOnInit() {
44-
this.showFeatureFlags$.subscribe((showFeatureFlags: boolean) => {
45-
if (showFeatureFlags) {
46-
this.featureFlagsDialog = this.dialog.open(FeatureFlagDialogContainer);
47-
this.featureFlagsDialog.afterClosed().subscribe(() => {
48-
// Reset the 'showFlags' flag when the dialog is closed to prevent the
49-
// dialog from appearing again after the page is refreshed.
50-
this.store.dispatch(
51-
featureFlagOverridesReset({
52-
flags: ['showFlags'],
53-
})
50+
this.showFeatureFlags$
51+
.pipe(takeUntil(this.ngUnsubscribe))
52+
.subscribe((showFeatureFlags: boolean) => {
53+
if (showFeatureFlags) {
54+
this.featureFlagsDialog = this.dialog.open(
55+
this.featureFlagDialogType
5456
);
55-
// Reload the page so that the application restarts with stable
56-
// feature flag values.
57-
// Wait one tick before reloading the page so the 'showFlags'
58-
// reset has a chance to be reflected in the URL before page reload.
59-
setTimeout(() => {
60-
util.reloadWindow();
61-
}, 1);
62-
});
63-
return;
64-
}
65-
});
57+
this.featureFlagsDialog
58+
.afterClosed()
59+
.pipe(takeUntil(this.ngUnsubscribe))
60+
.subscribe(() => {
61+
// Reset the 'showFlags' flag when the dialog is closed to prevent the
62+
// dialog from appearing again after the page is refreshed.
63+
this.store.dispatch(
64+
featureFlagOverridesReset({
65+
flags: ['showFlags'],
66+
})
67+
);
68+
// Reload the page so that the application restarts with stable
69+
// feature flag values.
70+
// Wait one tick before reloading the page so the 'showFlags'
71+
// reset has a chance to be reflected in the URL before page reload.
72+
setTimeout(() => {
73+
util.reloadWindow();
74+
}, 1);
75+
});
76+
return;
77+
}
78+
});
79+
}
80+
81+
ngOnDestroy() {
82+
this.ngUnsubscribe.next();
83+
this.ngUnsubscribe.complete();
6684
}
6785
}
6886

tensorboard/webapp/feature_flag/views/feature_flag_modal_trigger_container_test.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,9 @@ limitations under the License.
1414
==============================================================================*/
1515
import {HarnessLoader} from '@angular/cdk/testing';
1616
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
17-
import {NO_ERRORS_SCHEMA} from '@angular/core';
17+
import {Component} from '@angular/core';
1818
import {ComponentFixture, TestBed} from '@angular/core/testing';
19-
import {
20-
MatDialog,
21-
MatDialogModule,
22-
MatDialogRef,
23-
MAT_DIALOG_DATA,
24-
} from '@angular/material/dialog';
19+
import {MatDialogModule} from '@angular/material/dialog';
2520
import {MatDialogHarness} from '@angular/material/dialog/testing';
2621
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
2722
import {Store} from '@ngrx/store';
@@ -38,27 +33,24 @@ import {
3833
FeatureFlagModalTriggerContainer,
3934
TEST_ONLY,
4035
} from './feature_flag_modal_trigger_container';
41-
import {FeatureFlagDialogContainer} from './feature_flag_dialog_container';
36+
37+
@Component({
38+
selector: 'testable-feature-flag-dialog-container',
39+
template: '<div>Test</div>',
40+
})
41+
class TestableFeatureFlagDialogContainer {}
4242

4343
describe('feature_flag_modal_trigger_container', () => {
4444
let store: MockStore<State>;
45-
4645
let fixture: ComponentFixture<FeatureFlagModalTriggerContainer>;
47-
let loader: HarnessLoader;
4846
let rootLoader: HarnessLoader;
4947

5048
beforeEach(async () => {
5149
await TestBed.configureTestingModule({
5250
imports: [MatDialogModule, NoopAnimationsModule],
53-
declarations: [FeatureFlagDialogContainer],
54-
providers: [
55-
provideMockTbStore(),
56-
{provide: MatDialogRef, useValue: MatDialog},
57-
],
58-
schemas: [NO_ERRORS_SCHEMA],
51+
providers: [provideMockTbStore()],
5952
}).compileComponents();
6053

61-
TestBed.overrideProvider(MAT_DIALOG_DATA, {useValue: {}});
6254
store = TestBed.inject<Store<State>>(Store) as MockStore<State>;
6355

6456
spyOn(store, 'dispatch').and.stub();
@@ -73,7 +65,11 @@ describe('feature_flag_modal_trigger_container', () => {
7365

7466
function createComponent() {
7567
fixture = TestBed.createComponent(FeatureFlagModalTriggerContainer);
76-
loader = TestbedHarnessEnvironment.loader(fixture);
68+
// Override the dialog component to something that does not require a
69+
// working store. The dialog component sometimes completes initialization
70+
// after the test is torn down and the selectors have been reset.
71+
fixture.componentInstance.featureFlagDialogType =
72+
TestableFeatureFlagDialogContainer;
7773
rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture);
7874
}
7975

0 commit comments

Comments
 (0)