Skip to content

Commit 935fc26

Browse files
committed
add tests
1 parent a9ceb15 commit 935fc26

File tree

7 files changed

+405
-21
lines changed

7 files changed

+405
-21
lines changed

tensorboard/webapp/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ tf_ng_web_test_suite(
267267
"//tensorboard/webapp/customization:customization_test_lib",
268268
"//tensorboard/webapp/deeplink:deeplink_test_lib",
269269
"//tensorboard/webapp/feature_flag/effects:effects_test_lib",
270+
"//tensorboard/webapp/feature_flag/views:views_test",
270271
"//tensorboard/webapp/header:test_lib",
271272
"//tensorboard/webapp/metrics:integration_test",
272273
"//tensorboard/webapp/metrics:test_lib",

tensorboard/webapp/feature_flag/views/BUILD

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

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

@@ -55,3 +55,30 @@ tf_ng_module(
5555
"@npm//rxjs",
5656
],
5757
)
58+
59+
tf_ts_library(
60+
name = "views_test",
61+
testonly = True,
62+
srcs = [
63+
"feature_flag_modal_trigger_container_test.ts",
64+
"feature_flag_page_component_test.ts",
65+
"feature_flag_page_container_test.ts",
66+
],
67+
deps = [
68+
":feature_flag_modal_trigger_module",
69+
":views",
70+
"//tensorboard/webapp:app_state",
71+
"//tensorboard/webapp/angular:expect_angular_core_testing",
72+
"//tensorboard/webapp/angular:expect_angular_material_checkbox",
73+
"//tensorboard/webapp/angular:expect_angular_platform_browser_animations",
74+
"//tensorboard/webapp/feature_flag:types",
75+
"//tensorboard/webapp/feature_flag/actions",
76+
"//tensorboard/webapp/feature_flag/store",
77+
"@npm//@angular/common",
78+
"@npm//@angular/core",
79+
"@npm//@angular/platform-browser",
80+
"@npm//@ngrx/store",
81+
"@npm//@types/jasmine",
82+
"@npm//rxjs",
83+
],
84+
)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/* Copyright 2022 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
import {NO_ERRORS_SCHEMA} from '@angular/core';
16+
import {TestBed} from '@angular/core/testing';
17+
import {
18+
MatDialogModule,
19+
MatDialogRef,
20+
MAT_DIALOG_DATA,
21+
} from '@angular/material/dialog';
22+
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
23+
import {Action, Store} from '@ngrx/store';
24+
import {MockStore, provideMockStore} from '@ngrx/store/testing';
25+
import {Observable, Subscriber} from 'rxjs';
26+
import {State} from '../../app_state';
27+
import {
28+
getDefaultFeatureFlags,
29+
getOverriddenFeatureFlags,
30+
getShowFlagsEnabled,
31+
} from '../store/feature_flag_selectors';
32+
import {FeatureFlags} from '../types';
33+
import {FeatureFlagModalTriggerContainer} from './feature_flag_modal_trigger_container';
34+
import {FeatureFlagPageContainer} from './feature_flag_page_container';
35+
36+
class MatDialogMock {
37+
private onCloseSubscriptions: Subscriber<void>[] = [];
38+
private afterCloseObserable = new Observable<void>((subscriber) => {
39+
this.onCloseSubscriptions.push(subscriber);
40+
});
41+
42+
isOpen = false;
43+
open(_component: any) {
44+
this.isOpen = true;
45+
return this;
46+
}
47+
48+
afterClosed(): Observable<void> {
49+
return this.afterCloseObserable;
50+
}
51+
52+
close() {
53+
this.isOpen = false;
54+
this.onCloseSubscriptions.forEach((subscriber) => {
55+
subscriber.next();
56+
});
57+
}
58+
}
59+
60+
describe('feature_flag_modal_trigger_container', () => {
61+
let actualActions: Action[];
62+
let dispatchSpy: jasmine.Spy;
63+
let store: MockStore<State>;
64+
65+
beforeEach(async () => {
66+
await TestBed.configureTestingModule({
67+
imports: [MatDialogModule, NoopAnimationsModule],
68+
declarations: [FeatureFlagPageContainer],
69+
providers: [
70+
provideMockStore(),
71+
{provide: MatDialogRef, useValue: MatDialogMock},
72+
],
73+
schemas: [NO_ERRORS_SCHEMA],
74+
}).compileComponents();
75+
76+
TestBed.overrideProvider(MAT_DIALOG_DATA, {useValue: {}});
77+
store = TestBed.inject<Store<State>>(Store) as MockStore<State>;
78+
79+
actualActions = [];
80+
dispatchSpy = spyOn(store, 'dispatch').and.callFake((action: Action) => {
81+
actualActions.push(action);
82+
});
83+
});
84+
85+
it('creates modal when enableShowFlags is true', () => {
86+
store.overrideSelector(getDefaultFeatureFlags, {} as FeatureFlags);
87+
store.overrideSelector(getOverriddenFeatureFlags, {});
88+
store.overrideSelector(getShowFlagsEnabled, true);
89+
const dialogMock = new MatDialogMock();
90+
new FeatureFlagModalTriggerContainer(store, dialogMock as any);
91+
expect(dialogMock.isOpen).toBeTrue();
92+
});
93+
94+
it('does not create modal when enableShowFlags is false', () => {
95+
store.overrideSelector(getDefaultFeatureFlags, {} as FeatureFlags);
96+
store.overrideSelector(getOverriddenFeatureFlags, {});
97+
store.overrideSelector(getShowFlagsEnabled, false);
98+
const dialogMock = new MatDialogMock();
99+
new FeatureFlagModalTriggerContainer(store, dialogMock as any);
100+
expect(dialogMock.isOpen).toBeFalse();
101+
});
102+
});

tensorboard/webapp/feature_flag/views/feature_flag_page_component.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@ limitations under the License.
1515
import {Component, EventEmitter, Input, Output} from '@angular/core';
1616
import {FeatureFlagType} from '../store/feature_flag_metadata';
1717
import {FeatureFlags} from '../types';
18-
import {
19-
FeatureFlagOverrideStatus,
20-
FeatureFlagStatus,
21-
FeatureFlagStatusEvent,
22-
} from './types';
18+
import {FeatureFlagStatus, FeatureFlagStatusEvent} from './types';
2319

2420
@Component({
2521
selector: 'feature-flag-page-component',
@@ -33,10 +29,6 @@ export class FeatureFlagPageComponent {
3329

3430
@Output() allFlagsReset = new EventEmitter();
3531

36-
setFlag(flag: keyof FeatureFlags, status: FeatureFlagOverrideStatus) {
37-
this.flagChanged.emit({flag, status});
38-
}
39-
4032
private getFormattedFlagValue(value: FeatureFlagType): string {
4133
if (value === true) {
4234
return 'Enabled';
@@ -50,6 +42,10 @@ export class FeatureFlagPageComponent {
5042
return 'null';
5143
}
5244

45+
if (Array.isArray(value)) {
46+
return JSON.stringify(value);
47+
}
48+
5349
return value.toString();
5450
}
5551

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/* Copyright 2022 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
import {CommonModule} from '@angular/common';
16+
import {Component, NO_ERRORS_SCHEMA} from '@angular/core';
17+
import {TestBed} from '@angular/core/testing';
18+
import {FeatureFlags} from '../types';
19+
import {FeatureFlagPageComponent} from './feature_flag_page_component';
20+
import {FeatureFlagOverrideStatus, FeatureFlagStatus} from './types';
21+
22+
describe('feature_flag_page_component', () => {
23+
async function createComponent(
24+
featureFlagStatuses: FeatureFlagStatus<keyof FeatureFlags>[]
25+
) {
26+
@Component({
27+
selector: 'testable-component',
28+
template: `<feature-flag-page-component
29+
[featureFlagStatuses]="featureFlagStatuses"
30+
>
31+
</feature-flag-page-component>`,
32+
})
33+
class TestableComponent {
34+
get featureFlagStatuses(): FeatureFlagStatus<keyof FeatureFlags>[] {
35+
return featureFlagStatuses;
36+
}
37+
}
38+
39+
await TestBed.configureTestingModule({
40+
declarations: [TestableComponent, FeatureFlagPageComponent],
41+
imports: [CommonModule],
42+
schemas: [NO_ERRORS_SCHEMA],
43+
}).compileComponents();
44+
45+
const fixture = TestBed.createComponent(TestableComponent);
46+
fixture.detectChanges();
47+
return fixture.nativeElement.querySelector('feature-flag-page-component');
48+
}
49+
50+
it('creates rows for each feature flag', async () => {
51+
const component = await createComponent([
52+
{
53+
flag: 'inColab',
54+
defaultValue: false,
55+
status: FeatureFlagOverrideStatus.ENABLED,
56+
},
57+
{
58+
flag: 'enabledExperimentalPlugins',
59+
defaultValue: [],
60+
status: FeatureFlagOverrideStatus.DEFAULT,
61+
},
62+
]);
63+
64+
const rows = component.querySelectorAll('.feature-flag-table tr');
65+
expect(rows.length).toEqual(2);
66+
});
67+
68+
it('creates table data for non editable flags and mat-selects for editable flags', async () => {
69+
const component = await createComponent([
70+
{
71+
flag: 'inColab',
72+
defaultValue: false,
73+
status: FeatureFlagOverrideStatus.ENABLED,
74+
},
75+
{
76+
flag: 'enabledExperimentalPlugins',
77+
defaultValue: [],
78+
status: FeatureFlagOverrideStatus.DEFAULT,
79+
},
80+
]);
81+
82+
const dataCells = component.querySelectorAll('td');
83+
expect(dataCells.length).toEqual(3);
84+
const selectors = component.querySelectorAll('mat-select');
85+
expect(selectors.length).toEqual(1);
86+
});
87+
88+
describe('formatFlagValue', () => {
89+
it('converts true to "Enabled"', () => {
90+
const component = new FeatureFlagPageComponent();
91+
expect(component.formatFlagValue(true)).toEqual('- Enabled');
92+
});
93+
94+
it('converts false to "Disabled"', () => {
95+
const component = new FeatureFlagPageComponent();
96+
expect(component.formatFlagValue(false)).toEqual('- Disabled');
97+
});
98+
99+
it('converts null and undefined to "null"', () => {
100+
const component = new FeatureFlagPageComponent();
101+
expect(component.formatFlagValue(null)).toEqual('- null');
102+
expect(component.formatFlagValue(undefined)).toEqual('- null');
103+
});
104+
105+
it('serializes arrays', () => {
106+
const component = new FeatureFlagPageComponent();
107+
expect(component.formatFlagValue([])).toEqual('- []');
108+
});
109+
110+
it('serializes numbers and strings', () => {
111+
const component = new FeatureFlagPageComponent();
112+
expect(component.formatFlagValue(1)).toEqual('- 1');
113+
expect(component.formatFlagValue('foo')).toEqual('- foo');
114+
});
115+
116+
it('does not include hyphen when value has length 0', () => {
117+
const component = new FeatureFlagPageComponent();
118+
expect(component.formatFlagValue('')).toEqual('');
119+
});
120+
});
121+
});

tensorboard/webapp/feature_flag/views/feature_flag_page_container.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class FeatureFlagPageContainer {
5050
map(([defaultFeatureFlags, overriddenFeatureFlags]) => {
5151
return Object.entries(defaultFeatureFlags).map(
5252
([flagName, defaultValue]) => {
53-
const status = this.getFlagStatus(
53+
const status = getFlagStatus(
5454
flagName as keyof FeatureFlags,
5555
overriddenFeatureFlags
5656
);
@@ -87,16 +87,20 @@ export class FeatureFlagPageContainer {
8787
onAllFlagsReset() {
8888
this.store.dispatch(allFeatureFlagOverridesReset());
8989
}
90+
}
9091

91-
private getFlagStatus(
92-
flagName: keyof FeatureFlags,
93-
overriddenFeatureFlags: Partial<FeatureFlags>
94-
): FeatureFlagOverrideStatus {
95-
if (overriddenFeatureFlags[flagName] === undefined) {
96-
return FeatureFlagOverrideStatus.DEFAULT;
97-
}
98-
return overriddenFeatureFlags[flagName]
99-
? FeatureFlagOverrideStatus.ENABLED
100-
: FeatureFlagOverrideStatus.DISABLED;
92+
function getFlagStatus(
93+
flagName: keyof FeatureFlags,
94+
overriddenFeatureFlags: Partial<FeatureFlags>
95+
): FeatureFlagOverrideStatus {
96+
if (overriddenFeatureFlags[flagName] === undefined) {
97+
return FeatureFlagOverrideStatus.DEFAULT;
10198
}
99+
return overriddenFeatureFlags[flagName]
100+
? FeatureFlagOverrideStatus.ENABLED
101+
: FeatureFlagOverrideStatus.DISABLED;
102102
}
103+
104+
export const TEST_ONLY = {
105+
getFlagStatus,
106+
};

0 commit comments

Comments
 (0)