Skip to content
This repository was archived by the owner on May 7, 2021. It is now read-only.

Commit 8b5f018

Browse files
divyanshiGuptachristianvogt
authored andcommitted
feat(warning-page): Implement new design and action on opt-in button click (#22)
* fix(feature-warning): add new design for warning-page * fix(feature-warning): add service for enabling feature * fix(feature-warning): add feature enable action on opt-in button click * fix(feature-toggle): add option to display default/customised warning * fix(feature-toggle): fix tests * fix(feature-toggle): add option to display default/customised warning * fix(feature-warning): add new design for warning page * fix(demo-app): up and running demo app with new changes * fix(demo-app): fix feature toggle mock service
1 parent b94c4a0 commit 8b5f018

22 files changed

+819
-314
lines changed

.prettierrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"arrowParens": "always",
3+
"singleQuote": true,
4+
"bracketSpacing": true,
5+
"printWidth": 100
6+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<ng-container *ngIf="(feature$ | async) as feature">
2+
<ng-container
3+
[ngTemplateOutlet]="
4+
isFeatureUserEnabled ? userLevel : showFeatureOptIn ? featureOptInTemplate : defaultLevel
5+
"
6+
></ng-container>
7+
8+
<ng-template #featureOptInTemplate>
9+
<f8-feature-warning-page
10+
[level]="feature.level"
11+
(onOptInButtonClick)="setUserLevelTemplate()"
12+
></f8-feature-warning-page>
13+
</ng-template>
14+
</ng-container>
Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,46 @@
11
import { HttpClientModule } from '@angular/common/http';
2-
import { Component } from '@angular/core';
2+
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
33
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
44
import { FormsModule } from '@angular/forms';
55
import { of } from 'rxjs';
66
import { Feature } from '../models/feature';
77
import { FeatureTogglesService } from '../service/feature-toggles.service';
88
import { FeatureToggleComponent } from './feature-toggle.component';
9+
import { RouterModule } from '@angular/router';
10+
11+
@Component({
12+
selector: `f8-host-component`,
13+
template: `
14+
<f8-feature-toggle featureName="Planner" [userLevel]="user"></f8-feature-toggle>
15+
<ng-template #user><div>My content here</div></ng-template>
16+
`
17+
})
18+
class TestHostComponent1 {}
19+
20+
@Component({
21+
selector: `f8-host-component`,
22+
template: `
23+
<f8-feature-toggle featureName="Planner" [userLevel]="user" [showFeatureOptIn]="true">
24+
</f8-feature-toggle>
25+
<ng-template #user><div>My content here</div></ng-template>
26+
`
27+
})
28+
class TestHostComponent2 {}
29+
30+
@Component({
31+
selector: `f8-feature-warning-page`,
32+
template: `
33+
<div>Warning Page!</div>
34+
`
35+
})
36+
class TestWarningComponent {}
937

1038
describe('FeatureToggleComponent', () => {
11-
let featureServiceMock: jasmine.SpyObj<FeatureTogglesService>;
12-
let hostFixture: ComponentFixture<TestHostComponent>;
39+
let mockFeatureService: jasmine.SpyObj<FeatureTogglesService>;
40+
let hostFixture1: ComponentFixture<TestHostComponent1>;
41+
let hostFixture2: ComponentFixture<TestHostComponent2>;
1342

14-
const feature: Feature = {
43+
const feature1: Feature = {
1544
attributes: {
1645
name: 'Planner',
1746
description: 'Description',
@@ -22,48 +51,57 @@ describe('FeatureToggleComponent', () => {
2251
id: 'Planner'
2352
};
2453

25-
@Component({
26-
selector: `f8-host-component`,
27-
template: `<f8-feature-toggle featureName="Planner" [userLevel]="user"></f8-feature-toggle><ng-template #user><div>My content here</div></ng-template>`
28-
})
29-
class TestHostComponent {
30-
}
54+
const feature2: Feature = {
55+
attributes: {
56+
name: 'Planner Query',
57+
description: 'Description',
58+
enabled: true,
59+
'enablement-level': 'internal',
60+
'user-enabled': false
61+
},
62+
id: 'PlannerQuery'
63+
};
64+
3165
beforeEach(() => {
32-
featureServiceMock = jasmine.createSpyObj('FeatureTogglesService', ['isFeatureUserEnabled']);
66+
mockFeatureService = jasmine.createSpyObj('FeatureTogglesService', ['getFeature']);
3367

3468
TestBed.configureTestingModule({
35-
imports: [FormsModule, HttpClientModule],
36-
declarations: [FeatureToggleComponent, TestHostComponent],
37-
providers: [
38-
{
39-
provide: FeatureTogglesService, useValue: featureServiceMock
40-
}
41-
]
69+
imports: [FormsModule, HttpClientModule, RouterModule],
70+
declarations: [
71+
FeatureToggleComponent,
72+
TestHostComponent1,
73+
TestHostComponent2,
74+
TestWarningComponent
75+
],
76+
providers: [{ provide: FeatureTogglesService, useValue: mockFeatureService }],
77+
schemas: [NO_ERRORS_SCHEMA]
4278
});
4379

44-
hostFixture = TestBed.createComponent(TestHostComponent);
80+
hostFixture1 = TestBed.createComponent(TestHostComponent1);
81+
hostFixture2 = TestBed.createComponent(TestHostComponent2);
4582
});
4683

4784
it('should render content if feature is user enabled', async(() => {
48-
// given
49-
50-
featureServiceMock.isFeatureUserEnabled.and.returnValue(of(true));
51-
hostFixture.detectChanges();
52-
hostFixture.whenStable().then(() => {
53-
expect(hostFixture.nativeElement.querySelector('div').innerText).toEqual('My content here');
85+
mockFeatureService.getFeature.and.returnValue(of(feature1));
86+
hostFixture1.detectChanges();
87+
hostFixture1.whenStable().then(() => {
88+
expect(hostFixture1.nativeElement.querySelector('div').innerText).toEqual('My content here');
5489
});
5590
}));
5691

5792
it('should not render content if feature is not user enabled', async(() => {
58-
// given
59-
featureServiceMock.isFeatureUserEnabled.and.returnValue(of(false));
93+
mockFeatureService.getFeature.and.returnValue(of(feature2));
94+
hostFixture1.detectChanges();
95+
hostFixture1.whenStable().then(() => {
96+
expect(hostFixture1.nativeElement.querySelector('div')).toBeNull();
97+
});
98+
}));
6099

61-
// given
62-
feature.attributes.enabled = false;
63-
feature.attributes['user-enabled'] = true;
64-
hostFixture.detectChanges();
65-
hostFixture.whenStable().then(() => {
66-
expect(hostFixture.nativeElement.querySelector('div')).toBeNull();
100+
it('should render default warning template when showFeatureOptIn is true', async(() => {
101+
mockFeatureService.getFeature.and.returnValue(of(feature2));
102+
hostFixture2.detectChanges();
103+
hostFixture2.whenStable().then(() => {
104+
expect(hostFixture2.nativeElement.querySelector('div').innerText).toEqual('Warning Page!');
67105
});
68106
}));
69107
});
Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,57 @@
1-
import {
2-
Component,
3-
Input,
4-
OnInit,
5-
TemplateRef
6-
} from '@angular/core';
7-
import { Observable } from 'rxjs';
1+
import { Component, Input, OnInit, TemplateRef } from '@angular/core';
2+
import { Observable, of } from 'rxjs';
83
import { FeatureTogglesService } from '../service/feature-toggles.service';
4+
import { Feature } from '../models/feature';
5+
import { map, catchError, tap } from 'rxjs/operators';
6+
7+
interface FeatureEnablementData {
8+
enabled: boolean;
9+
level: string;
10+
}
911

1012
@Component({
1113
selector: 'f8-feature-toggle',
12-
template: `<ng-container [ngTemplateOutlet]="(enabled | async) ? userLevel : defaultLevel"></ng-container>`
14+
templateUrl: './feature-toggle.component.html'
1315
})
1416
export class FeatureToggleComponent implements OnInit {
15-
1617
@Input() featureName: string;
1718
@Input() userLevel: TemplateRef<any>;
1819
@Input() defaultLevel: TemplateRef<any>;
20+
@Input() showFeatureOptIn: boolean = false;
1921

20-
enabled: Observable<{} | boolean>;
22+
isFeatureUserEnabled: boolean = false;
23+
feature$: Observable<{} | FeatureEnablementData>;
2124

2225
constructor(private featureService: FeatureTogglesService) {}
2326

2427
ngOnInit(): void {
2528
if (!this.featureName) {
2629
throw new Error('Attribute `featureName` should not be null or empty');
2730
}
28-
29-
this.enabled = this.featureService.isFeatureUserEnabled(this.featureName);
31+
this.feature$ = this.featureService.getFeature(this.featureName).pipe(
32+
map((feature: Feature) => {
33+
if (feature.attributes) {
34+
let featureEnablementData: FeatureEnablementData = {
35+
enabled: feature.attributes.enabled && feature.attributes['user-enabled'],
36+
level: feature.attributes['enablement-level']
37+
};
38+
return featureEnablementData;
39+
} else {
40+
return {};
41+
}
42+
}),
43+
tap((feature: FeatureEnablementData) => {
44+
if (feature && feature.enabled) {
45+
this.isFeatureUserEnabled = true;
46+
} else {
47+
this.isFeatureUserEnabled = false;
48+
}
49+
}),
50+
catchError(() => of({}))
51+
);
3052
}
3153

54+
setUserLevelTemplate() {
55+
this.isFeatureUserEnabled = true;
56+
}
3257
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import {
2+
HttpClientTestingModule,
3+
HttpTestingController,
4+
TestRequest
5+
} from '@angular/common/http/testing';
6+
import { async, getTestBed, TestBed } from '@angular/core/testing';
7+
import { AuthenticationService } from 'ngx-login-client';
8+
import { first } from 'rxjs/operators';
9+
import { FABRIC8_FEATURE_TOGGLES_API_URL } from './feature-toggles.service';
10+
import { EnableFeatureService, ExtProfile, ExtUser } from './enable-feature.service';
11+
import { Logger } from 'ngx-base';
12+
import { UserService } from 'ngx-login-client';
13+
14+
describe('EnableFeature service:', () => {
15+
let mockAuthService: jasmine.SpyObj<AuthenticationService>;
16+
let mockUserService: jasmine.SpyObj<UserService>;
17+
let enableFeatureService: EnableFeatureService;
18+
let httpTestingController: HttpTestingController;
19+
20+
let url: string;
21+
22+
beforeEach(() => {
23+
mockAuthService = jasmine.createSpyObj('AuthenticationService', ['getToken']);
24+
mockAuthService.getToken.and.returnValue('mock-auth-token');
25+
mockUserService = jasmine.createSpyObj('UserService', ['loggedInUser']);
26+
TestBed.configureTestingModule({
27+
imports: [HttpClientTestingModule],
28+
providers: [
29+
{
30+
provide: AuthenticationService,
31+
useValue: mockAuthService
32+
},
33+
{
34+
provide: FABRIC8_FEATURE_TOGGLES_API_URL,
35+
useValue: 'http://example.com/api/'
36+
},
37+
{
38+
provide: UserService,
39+
useValue: mockUserService
40+
},
41+
EnableFeatureService,
42+
Logger
43+
]
44+
});
45+
enableFeatureService = getTestBed().get(EnableFeatureService);
46+
httpTestingController = getTestBed().get(HttpTestingController);
47+
48+
url = getTestBed().get(FABRIC8_FEATURE_TOGGLES_API_URL);
49+
});
50+
51+
afterEach(() => {
52+
httpTestingController.verify();
53+
});
54+
55+
it('should be instantiable', async((): void => {
56+
expect(enableFeatureService).toBeDefined();
57+
}));
58+
59+
describe('#getUpdate', () => {
60+
it('should send a PATCH', (done: DoneFn): void => {
61+
enableFeatureService
62+
.update({} as ExtProfile)
63+
.pipe(first())
64+
.subscribe(
65+
(): void => {
66+
done();
67+
}
68+
);
69+
const req: TestRequest = httpTestingController.expectOne('http://example.com/api/users');
70+
expect(req.request.method).toEqual('PATCH');
71+
req.flush({});
72+
});
73+
74+
it('should send correct headers', (done: DoneFn): void => {
75+
enableFeatureService
76+
.update({} as ExtProfile)
77+
.pipe(first())
78+
.subscribe(
79+
(): void => {
80+
done();
81+
}
82+
);
83+
const req: TestRequest = httpTestingController.expectOne('http://example.com/api/users');
84+
expect(req.request.headers.get('Authorization')).toEqual('Bearer mock-auth-token');
85+
req.flush({});
86+
});
87+
88+
it('should send correct payload', (done: DoneFn): void => {
89+
const attributes: ExtProfile = { featureLevel: 'beta' } as ExtProfile;
90+
enableFeatureService
91+
.update(attributes)
92+
.pipe(first())
93+
.subscribe(
94+
(): void => {
95+
done();
96+
}
97+
);
98+
const req: TestRequest = httpTestingController.expectOne('http://example.com/api/users');
99+
expect(req.request.body).toEqual(
100+
JSON.stringify({
101+
data: {
102+
attributes,
103+
type: 'identities'
104+
}
105+
})
106+
);
107+
req.flush({});
108+
});
109+
110+
it('should return expected data', (done: DoneFn): void => {
111+
const data: ExtUser = {
112+
attributes: {
113+
featureLevel: 'beta'
114+
}
115+
} as ExtUser;
116+
enableFeatureService
117+
.update(data.attributes)
118+
.pipe(first())
119+
.subscribe(
120+
(user: ExtUser): void => {
121+
expect(user).toEqual(data);
122+
done();
123+
}
124+
);
125+
httpTestingController.expectOne('http://example.com/api/users').flush({ data });
126+
});
127+
});
128+
});

0 commit comments

Comments
 (0)