Merge pull request #2726 from alecmerdler/fix-cor-tabs-back-button

Fix UI Tabs To Play Nicely with Browser Back Button
This commit is contained in:
Alec Merdler 2017-06-26 16:55:14 -07:00 committed by GitHub
commit e45ffb39d1
9 changed files with 235 additions and 29 deletions

View file

@ -12,11 +12,12 @@ describe("CorTabPaneComponent", () => {
beforeEach(() => { beforeEach(() => {
activeTab = new BehaviorSubject<string>(null); activeTab = new BehaviorSubject<string>(null);
spyOn(activeTab, "subscribe").and.returnValue(null); spyOn(activeTab, "subscribe").and.callThrough();
panelMock = new Mock<CorTabPanelComponent>(); panelMock = new Mock<CorTabPanelComponent>();
panelMock.setup(mock => mock.activeTab).is(activeTab); panelMock.setup(mock => mock.activeTab).is(activeTab);
component = new CorTabPaneComponent(panelMock.Object); component = new CorTabPaneComponent(panelMock.Object);
component.id = 'description';
}); });
describe("ngOnInit", () => { describe("ngOnInit", () => {
@ -36,5 +37,27 @@ describe("CorTabPaneComponent", () => {
expect((<Spy>panelMock.Object.activeTab.subscribe)).toHaveBeenCalled(); expect((<Spy>panelMock.Object.activeTab.subscribe)).toHaveBeenCalled();
}); });
it("does nothing if active tab ID is undefined", () => {
component.ngOnInit();
component.isActiveTab = true;
panelMock.Object.activeTab.next(null);
expect(component.isActiveTab).toEqual(true);
});
it("sets self as active if active tab ID matches tab ID", () => {
component.ngOnInit();
panelMock.Object.activeTab.next(component.id);
expect(component.isActiveTab).toEqual(true);
});
it("sets self as inactive if active tab ID does not match tab ID", () => {
component.ngOnInit();
panelMock.Object.activeTab.next(component.id.split('').reverse().join(''));
expect(component.isActiveTab).toEqual(false);
});
}); });
}); });

View file

@ -1,5 +1,6 @@
import { Component, Input, Inject, Host, OnInit } from 'ng-metadata/core'; import { Component, Input, Inject, Host, OnInit } from 'ng-metadata/core';
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component'; import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
import 'rxjs/add/operator/filter';
/** /**
@ -16,7 +17,7 @@ export class CorTabPaneComponent implements OnInit {
@Input('@') public id: string; @Input('@') public id: string;
private isActiveTab: boolean = false; public isActiveTab: boolean = false;
constructor(@Host() @Inject(CorTabPanelComponent) private panel: CorTabPanelComponent) { constructor(@Host() @Inject(CorTabPanelComponent) private panel: CorTabPanelComponent) {
@ -25,8 +26,10 @@ export class CorTabPaneComponent implements OnInit {
public ngOnInit(): void { public ngOnInit(): void {
this.panel.addTabPane(this); this.panel.addTabPane(this);
this.panel.activeTab.subscribe((tabId: string) => { this.panel.activeTab
this.isActiveTab = (this.id === tabId); .filter(tabId => tabId != undefined)
}); .subscribe((tabId: string) => {
this.isActiveTab = (this.id === tabId);
});
} }
} }

View file

@ -12,9 +12,16 @@ describe("CorTabPanelComponent", () => {
}); });
describe("ngOnInit", () => { describe("ngOnInit", () => {
var tabs: CorTabComponent[] = [];
beforeEach(() => { beforeEach(() => {
spyOn(component.activeTab, "subscribe").and.returnValue(null); // Add tabs to panel
tabs.push(new CorTabComponent(component));
tabs[0].tabId = "info";
tabs.forEach((tab) => component.addTab(tab));
spyOn(component.activeTab, "subscribe").and.callThrough();
spyOn(component.activeTab, "next").and.callThrough();
spyOn(component.tabChange, "emit").and.returnValue(null); spyOn(component.tabChange, "emit").and.returnValue(null);
}); });
@ -24,12 +31,26 @@ describe("CorTabPanelComponent", () => {
expect(<Spy>component.activeTab.subscribe).toHaveBeenCalled(); expect(<Spy>component.activeTab.subscribe).toHaveBeenCalled();
}); });
it("emits output event for tab change when ", () => { it("emits next active tab with tab ID of first registered tab if given tab ID is null", () => {
component.ngOnInit();
component.activeTab.next(null);
expect((<Spy>component.activeTab.next).calls.argsFor(1)[0]).toEqual(tabs[0].tabId);
});
it("does not emit output event for tab change if tab ID is null", () => {
component.ngOnInit();
component.activeTab.next(null);
expect((<Spy>component.tabChange.emit).calls.allArgs).not.toContain(null);
});
it("emits output event for tab change when tab ID is not null", () => {
component.ngOnInit(); component.ngOnInit();
const tabId: string = "description"; const tabId: string = "description";
(<Spy>component.activeTab.subscribe).calls.argsFor(0)[0](tabId); component.activeTab.next(tabId);
expect((<Spy>component.tabChange.emit).calls.argsFor(0)[0]).toEqual(tabId); expect((<Spy>component.tabChange.emit).calls.argsFor(1)[0]).toEqual(tabId);
}); });
}); });

View file

@ -28,7 +28,12 @@ export class CorTabPanelComponent implements OnInit, OnChanges {
public ngOnInit(): void { public ngOnInit(): void {
this.activeTab.subscribe((tabId: string) => { this.activeTab.subscribe((tabId: string) => {
this.tabChange.emit(tabId); // Catch null values and replace with tabId of first tab
if (!tabId && this.tabs[0]) {
this.activeTab.next(this.tabs[0].tabId);
} else {
this.tabChange.emit(tabId);
}
}); });
} }

View file

@ -12,7 +12,7 @@ describe("CorTabComponent", () => {
beforeEach(() => { beforeEach(() => {
activeTab = new BehaviorSubject<string>(null); activeTab = new BehaviorSubject<string>(null);
spyOn(activeTab, "subscribe").and.returnValue(null); spyOn(activeTab, "subscribe").and.callThrough();
panelMock = new Mock<CorTabPanelComponent>(); panelMock = new Mock<CorTabPanelComponent>();
panelMock.setup(mock => mock.activeTab).is(activeTab); panelMock.setup(mock => mock.activeTab).is(activeTab);
@ -35,16 +35,25 @@ describe("CorTabComponent", () => {
expect((<Spy>panelMock.Object.activeTab.subscribe)).toHaveBeenCalled(); expect((<Spy>panelMock.Object.activeTab.subscribe)).toHaveBeenCalled();
}); });
it("does nothing if active tab ID is undefined", () => {
component.ngOnInit();
panelMock.Object.activeTab.next(null);
expect(<Spy>component.tabInit.emit).not.toHaveBeenCalled();
expect(<Spy>component.tabShow.emit).not.toHaveBeenCalled();
expect(<Spy>component.tabHide.emit).not.toHaveBeenCalled();
});
it("emits output event for tab init if it is new active tab", () => { it("emits output event for tab init if it is new active tab", () => {
component.ngOnInit(); component.ngOnInit();
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](component.tabId); panelMock.Object.activeTab.next(component.tabId);
expect(<Spy>component.tabInit.emit).toHaveBeenCalled(); expect(<Spy>component.tabInit.emit).toHaveBeenCalled();
}); });
it("emits output event for tab show if it is new active tab", () => { it("emits output event for tab show if it is new active tab", () => {
component.ngOnInit(); component.ngOnInit();
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](component.tabId); panelMock.Object.activeTab.next(component.tabId);
expect(<Spy>component.tabShow.emit).toHaveBeenCalled(); expect(<Spy>component.tabShow.emit).toHaveBeenCalled();
}); });
@ -53,8 +62,8 @@ describe("CorTabComponent", () => {
const newTabId: string = component.tabId.split('').reverse().join(''); const newTabId: string = component.tabId.split('').reverse().join('');
component.ngOnInit(); component.ngOnInit();
// Call twice, first time to set 'isActive' to true // Call twice, first time to set 'isActive' to true
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](component.tabId); panelMock.Object.activeTab.next(component.tabId);
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](newTabId); panelMock.Object.activeTab.next(newTabId);
expect(<Spy>component.tabHide.emit).toHaveBeenCalled(); expect(<Spy>component.tabHide.emit).toHaveBeenCalled();
}); });
@ -62,7 +71,7 @@ describe("CorTabComponent", () => {
it("does not emit output event for tab hide if was not previously active tab", () => { it("does not emit output event for tab hide if was not previously active tab", () => {
const newTabId: string = component.tabId.split('').reverse().join(''); const newTabId: string = component.tabId.split('').reverse().join('');
component.ngOnInit(); component.ngOnInit();
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](newTabId); panelMock.Object.activeTab.next(newTabId);
expect(<Spy>component.tabHide.emit).not.toHaveBeenCalled(); expect(<Spy>component.tabHide.emit).not.toHaveBeenCalled();
}); });

View file

@ -1,5 +1,6 @@
import { Component, Input, Output, Inject, EventEmitter, Host, OnInit } from 'ng-metadata/core'; import { Component, Input, Output, Inject, EventEmitter, Host, OnInit } from 'ng-metadata/core';
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component'; import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
import 'rxjs/add/operator/filter';
/** /**
@ -28,16 +29,18 @@ export class CorTabComponent implements OnInit {
} }
public ngOnInit(): void { public ngOnInit(): void {
this.panel.activeTab.subscribe((tabId: string) => { this.panel.activeTab
if (!this.isActive && this.tabId === tabId) { .filter(tabId => tabId != undefined)
this.isActive = true; .subscribe((tabId: string) => {
this.tabInit.emit({}); if (!this.isActive && this.tabId === tabId) {
this.tabShow.emit({}); this.isActive = true;
} else if (this.isActive && this.tabId !== tabId) { this.tabInit.emit({});
this.isActive = false; this.tabShow.emit({});
this.tabHide.emit({}); } else if (this.isActive && this.tabId !== tabId) {
} this.isActive = false;
}); this.tabHide.emit({});
}
});
this.panel.addTab(this); this.panel.addTab(this);
} }

View file

@ -0,0 +1,13 @@
import { element, by, browser, $, ElementFinder, ExpectedConditions as until } from 'protractor';
export class CorTabsViewObject {
public selectTabByTitle(title: string): Promise<void> {
return Promise.resolve($(`cor-tab[tab-title="${title}"] a`).click());
}
public isActiveTab(title: string): Promise<boolean> {
return Promise.resolve($(`cor-tab[tab-title="${title}"] .cor-tab-itself.active`).isPresent());
}
}

View file

@ -0,0 +1,128 @@
import { browser, element, by, $, $$ } from 'protractor';
import { appHost } from '../protractor.conf';
import { CorTabsViewObject } from '../../js/directives/ui/cor-tabs/cor-tabs.view-object';
describe("Image Repository", () => {
const username = 'devtable';
const password = 'password';
const repoTabs: CorTabsViewObject = new CorTabsViewObject();
beforeAll((done) => {
browser.waitForAngularEnabled(false);
// Sign in
browser.get(appHost);
$$('a[href="/signin/"]').get(1).click();
$('#signin-username').sendKeys(username);
$('#signin-password').sendKeys(password);
element(by.partialButtonText('Sign in')).click();
browser.sleep(4000);
// Navigate to image repository
browser.get(`${appHost}/repository/devtable/simple`).then(() => done());
});
afterAll(() => {
browser.waitForAngularEnabled(true);
});
describe("information tab", () => {
const tabTitle: string = 'Information';
beforeAll((done) => {
repoTabs.selectTabByTitle(tabTitle).then(() => done());
});
it("displays repository description", () => {
expect(repoTabs.isActiveTab(tabTitle)).toBe(true);
expect(element(by.cssContainingText('h4', 'Description')).isDisplayed()).toBe(true);
});
});
describe("tags tab", () => {
const tabTitle: string = 'Tags';
beforeAll((done) => {
repoTabs.selectTabByTitle(tabTitle).then(() => done());
});
it("displays repository tags", () => {
expect(repoTabs.isActiveTab(tabTitle)).toBe(true);
expect(element(by.cssContainingText('.tab-header', 'Repository Tags')).isDisplayed()).toBe(true);
});
});
describe("tag history tab", () => {
const tabTitle: string = 'Tag History';
beforeAll((done) => {
repoTabs.selectTabByTitle(tabTitle).then(() => done());
});
it("displays repository tags", () => {
expect(repoTabs.isActiveTab(tabTitle)).toBe(true);
expect(element(by.cssContainingText('.tab-header', 'Tag History')).isDisplayed()).toBe(true);
});
});
describe("builds tab", () => {
const tabTitle: string = 'Builds';
beforeAll((done) => {
repoTabs.selectTabByTitle(tabTitle).then(() => done());
});
it("displays repository tags", () => {
expect(repoTabs.isActiveTab(tabTitle)).toBe(true);
expect(element(by.cssContainingText('.tab-header', 'Repository Builds')).isDisplayed()).toBe(true);
});
});
describe("usage logs tab", () => {
const tabTitle: string = 'Usage Logs';
beforeAll((done) => {
repoTabs.selectTabByTitle(tabTitle).then(() => done());
});
it("displays repository tags", () => {
expect(repoTabs.isActiveTab(tabTitle)).toBe(true);
expect(element(by.cssContainingText('h3', 'Usage Logs')).isDisplayed()).toBe(true);
});
});
describe("settings tab", () => {
const tabTitle: string = 'Settings';
beforeAll((done) => {
repoTabs.selectTabByTitle(tabTitle).then(() => done());
});
it("displays repository tags", () => {
expect(repoTabs.isActiveTab(tabTitle)).toBe(true);
expect(element(by.cssContainingText('.tab-header', 'Settings')).isDisplayed()).toBe(true);
});
});
describe("tabs navigation", () => {
beforeAll((done) => {
repoTabs.selectTabByTitle('Information');
repoTabs.selectTabByTitle('Tags');
done();
});
it("back button returns to previous tab", () => {
browser.navigate().back();
expect(repoTabs.isActiveTab('Information')).toBe(true);
});
it("forward button returns to next tab", () => {
browser.navigate().forward();
expect(repoTabs.isActiveTab('Tags')).toBe(true);
});
});
});

View file

@ -20,7 +20,7 @@ export const config: Config = {
framework: 'jasmine', framework: 'jasmine',
seleniumAddress: 'http://localhost:4444/wd/hub', seleniumAddress: 'http://localhost:4444/wd/hub',
// Uncomment to run tests against local Chrome instance // Uncomment to run tests against local Chrome instance
// directConnect: true, directConnect: true,
capabilities: { capabilities: {
browserName: 'chrome', browserName: 'chrome',
chromeOptions: { chromeOptions: {
@ -60,7 +60,8 @@ export const config: Config = {
browser.close(); browser.close();
}, },
specs: [ specs: [
'./e2e/sanity.scenario.ts', // './e2e/sanity.scenario.ts',
'./e2e/trigger-creation.scenario.ts' // './e2e/trigger-creation.scenario.ts',
'./e2e/image-repo.scenario.ts',
], ],
}; };