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

Commit

Permalink
feat(warning-page): Implement new design and action on opt-in button …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
divyanshiGupta authored and christianvogt committed Jan 14, 2019
1 parent b94c4a0 commit 8b5f018
Show file tree
Hide file tree
Showing 22 changed files with 819 additions and 314 deletions.
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"arrowParens": "always",
"singleQuote": true,
"bracketSpacing": true,
"printWidth": 100
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<ng-container *ngIf="(feature$ | async) as feature">
<ng-container
[ngTemplateOutlet]="
isFeatureUserEnabled ? userLevel : showFeatureOptIn ? featureOptInTemplate : defaultLevel
"
></ng-container>

<ng-template #featureOptInTemplate>
<f8-feature-warning-page
[level]="feature.level"
(onOptInButtonClick)="setUserLevelTemplate()"
></f8-feature-warning-page>
</ng-template>
</ng-container>
Original file line number Diff line number Diff line change
@@ -1,17 +1,46 @@
import { HttpClientModule } from '@angular/common/http';
import { Component } from '@angular/core';
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { of } from 'rxjs';
import { Feature } from '../models/feature';
import { FeatureTogglesService } from '../service/feature-toggles.service';
import { FeatureToggleComponent } from './feature-toggle.component';
import { RouterModule } from '@angular/router';

@Component({
selector: `f8-host-component`,
template: `
<f8-feature-toggle featureName="Planner" [userLevel]="user"></f8-feature-toggle>
<ng-template #user><div>My content here</div></ng-template>
`
})
class TestHostComponent1 {}

@Component({
selector: `f8-host-component`,
template: `
<f8-feature-toggle featureName="Planner" [userLevel]="user" [showFeatureOptIn]="true">
</f8-feature-toggle>
<ng-template #user><div>My content here</div></ng-template>
`
})
class TestHostComponent2 {}

@Component({
selector: `f8-feature-warning-page`,
template: `
<div>Warning Page!</div>
`
})
class TestWarningComponent {}

describe('FeatureToggleComponent', () => {
let featureServiceMock: jasmine.SpyObj<FeatureTogglesService>;
let hostFixture: ComponentFixture<TestHostComponent>;
let mockFeatureService: jasmine.SpyObj<FeatureTogglesService>;
let hostFixture1: ComponentFixture<TestHostComponent1>;
let hostFixture2: ComponentFixture<TestHostComponent2>;

const feature: Feature = {
const feature1: Feature = {
attributes: {
name: 'Planner',
description: 'Description',
Expand All @@ -22,48 +51,57 @@ describe('FeatureToggleComponent', () => {
id: 'Planner'
};

@Component({
selector: `f8-host-component`,
template: `<f8-feature-toggle featureName="Planner" [userLevel]="user"></f8-feature-toggle><ng-template #user><div>My content here</div></ng-template>`
})
class TestHostComponent {
}
const feature2: Feature = {
attributes: {
name: 'Planner Query',
description: 'Description',
enabled: true,
'enablement-level': 'internal',
'user-enabled': false
},
id: 'PlannerQuery'
};

beforeEach(() => {
featureServiceMock = jasmine.createSpyObj('FeatureTogglesService', ['isFeatureUserEnabled']);
mockFeatureService = jasmine.createSpyObj('FeatureTogglesService', ['getFeature']);

TestBed.configureTestingModule({
imports: [FormsModule, HttpClientModule],
declarations: [FeatureToggleComponent, TestHostComponent],
providers: [
{
provide: FeatureTogglesService, useValue: featureServiceMock
}
]
imports: [FormsModule, HttpClientModule, RouterModule],
declarations: [
FeatureToggleComponent,
TestHostComponent1,
TestHostComponent2,
TestWarningComponent
],
providers: [{ provide: FeatureTogglesService, useValue: mockFeatureService }],
schemas: [NO_ERRORS_SCHEMA]
});

hostFixture = TestBed.createComponent(TestHostComponent);
hostFixture1 = TestBed.createComponent(TestHostComponent1);
hostFixture2 = TestBed.createComponent(TestHostComponent2);
});

it('should render content if feature is user enabled', async(() => {
// given

featureServiceMock.isFeatureUserEnabled.and.returnValue(of(true));
hostFixture.detectChanges();
hostFixture.whenStable().then(() => {
expect(hostFixture.nativeElement.querySelector('div').innerText).toEqual('My content here');
mockFeatureService.getFeature.and.returnValue(of(feature1));
hostFixture1.detectChanges();
hostFixture1.whenStable().then(() => {
expect(hostFixture1.nativeElement.querySelector('div').innerText).toEqual('My content here');
});
}));

it('should not render content if feature is not user enabled', async(() => {
// given
featureServiceMock.isFeatureUserEnabled.and.returnValue(of(false));
mockFeatureService.getFeature.and.returnValue(of(feature2));
hostFixture1.detectChanges();
hostFixture1.whenStable().then(() => {
expect(hostFixture1.nativeElement.querySelector('div')).toBeNull();
});
}));

// given
feature.attributes.enabled = false;
feature.attributes['user-enabled'] = true;
hostFixture.detectChanges();
hostFixture.whenStable().then(() => {
expect(hostFixture.nativeElement.querySelector('div')).toBeNull();
it('should render default warning template when showFeatureOptIn is true', async(() => {
mockFeatureService.getFeature.and.returnValue(of(feature2));
hostFixture2.detectChanges();
hostFixture2.whenStable().then(() => {
expect(hostFixture2.nativeElement.querySelector('div').innerText).toEqual('Warning Page!');
});
}));
});
Original file line number Diff line number Diff line change
@@ -1,32 +1,57 @@
import {
Component,
Input,
OnInit,
TemplateRef
} from '@angular/core';
import { Observable } from 'rxjs';
import { Component, Input, OnInit, TemplateRef } from '@angular/core';
import { Observable, of } from 'rxjs';
import { FeatureTogglesService } from '../service/feature-toggles.service';
import { Feature } from '../models/feature';
import { map, catchError, tap } from 'rxjs/operators';

interface FeatureEnablementData {
enabled: boolean;
level: string;
}

@Component({
selector: 'f8-feature-toggle',
template: `<ng-container [ngTemplateOutlet]="(enabled | async) ? userLevel : defaultLevel"></ng-container>`
templateUrl: './feature-toggle.component.html'
})
export class FeatureToggleComponent implements OnInit {

@Input() featureName: string;
@Input() userLevel: TemplateRef<any>;
@Input() defaultLevel: TemplateRef<any>;
@Input() showFeatureOptIn: boolean = false;

enabled: Observable<{} | boolean>;
isFeatureUserEnabled: boolean = false;
feature$: Observable<{} | FeatureEnablementData>;

constructor(private featureService: FeatureTogglesService) {}

ngOnInit(): void {
if (!this.featureName) {
throw new Error('Attribute `featureName` should not be null or empty');
}

this.enabled = this.featureService.isFeatureUserEnabled(this.featureName);
this.feature$ = this.featureService.getFeature(this.featureName).pipe(
map((feature: Feature) => {
if (feature.attributes) {
let featureEnablementData: FeatureEnablementData = {
enabled: feature.attributes.enabled && feature.attributes['user-enabled'],
level: feature.attributes['enablement-level']
};
return featureEnablementData;
} else {
return {};
}
}),
tap((feature: FeatureEnablementData) => {
if (feature && feature.enabled) {
this.isFeatureUserEnabled = true;
} else {
this.isFeatureUserEnabled = false;
}
}),
catchError(() => of({}))
);
}

setUserLevelTemplate() {
this.isFeatureUserEnabled = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {
HttpClientTestingModule,
HttpTestingController,
TestRequest
} from '@angular/common/http/testing';
import { async, getTestBed, TestBed } from '@angular/core/testing';
import { AuthenticationService } from 'ngx-login-client';
import { first } from 'rxjs/operators';
import { FABRIC8_FEATURE_TOGGLES_API_URL } from './feature-toggles.service';
import { EnableFeatureService, ExtProfile, ExtUser } from './enable-feature.service';
import { Logger } from 'ngx-base';
import { UserService } from 'ngx-login-client';

describe('EnableFeature service:', () => {
let mockAuthService: jasmine.SpyObj<AuthenticationService>;
let mockUserService: jasmine.SpyObj<UserService>;
let enableFeatureService: EnableFeatureService;
let httpTestingController: HttpTestingController;

let url: string;

beforeEach(() => {
mockAuthService = jasmine.createSpyObj('AuthenticationService', ['getToken']);
mockAuthService.getToken.and.returnValue('mock-auth-token');
mockUserService = jasmine.createSpyObj('UserService', ['loggedInUser']);
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{
provide: AuthenticationService,
useValue: mockAuthService
},
{
provide: FABRIC8_FEATURE_TOGGLES_API_URL,
useValue: 'http://example.com/api/'
},
{
provide: UserService,
useValue: mockUserService
},
EnableFeatureService,
Logger
]
});
enableFeatureService = getTestBed().get(EnableFeatureService);
httpTestingController = getTestBed().get(HttpTestingController);

url = getTestBed().get(FABRIC8_FEATURE_TOGGLES_API_URL);
});

afterEach(() => {
httpTestingController.verify();
});

it('should be instantiable', async((): void => {
expect(enableFeatureService).toBeDefined();
}));

describe('#getUpdate', () => {
it('should send a PATCH', (done: DoneFn): void => {
enableFeatureService
.update({} as ExtProfile)
.pipe(first())
.subscribe(
(): void => {
done();
}
);
const req: TestRequest = httpTestingController.expectOne('http://example.com/api/users');
expect(req.request.method).toEqual('PATCH');
req.flush({});
});

it('should send correct headers', (done: DoneFn): void => {
enableFeatureService
.update({} as ExtProfile)
.pipe(first())
.subscribe(
(): void => {
done();
}
);
const req: TestRequest = httpTestingController.expectOne('http://example.com/api/users');
expect(req.request.headers.get('Authorization')).toEqual('Bearer mock-auth-token');
req.flush({});
});

it('should send correct payload', (done: DoneFn): void => {
const attributes: ExtProfile = { featureLevel: 'beta' } as ExtProfile;
enableFeatureService
.update(attributes)
.pipe(first())
.subscribe(
(): void => {
done();
}
);
const req: TestRequest = httpTestingController.expectOne('http://example.com/api/users');
expect(req.request.body).toEqual(
JSON.stringify({
data: {
attributes,
type: 'identities'
}
})
);
req.flush({});
});

it('should return expected data', (done: DoneFn): void => {
const data: ExtUser = {
attributes: {
featureLevel: 'beta'
}
} as ExtUser;
enableFeatureService
.update(data.attributes)
.pipe(first())
.subscribe(
(user: ExtUser): void => {
expect(user).toEqual(data);
done();
}
);
httpTestingController.expectOne('http://example.com/api/users').flush({ data });
});
});
});
Loading

0 comments on commit 8b5f018

Please sign in to comment.