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:
commit
e45ffb39d1
9 changed files with 235 additions and 29 deletions
|
@ -12,11 +12,12 @@ describe("CorTabPaneComponent", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
activeTab = new BehaviorSubject<string>(null);
|
||||
spyOn(activeTab, "subscribe").and.returnValue(null);
|
||||
spyOn(activeTab, "subscribe").and.callThrough();
|
||||
panelMock = new Mock<CorTabPanelComponent>();
|
||||
panelMock.setup(mock => mock.activeTab).is(activeTab);
|
||||
|
||||
component = new CorTabPaneComponent(panelMock.Object);
|
||||
component.id = 'description';
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
|
@ -36,5 +37,27 @@ describe("CorTabPaneComponent", () => {
|
|||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Component, Input, Inject, Host, OnInit } from 'ng-metadata/core';
|
||||
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;
|
||||
|
||||
private isActiveTab: boolean = false;
|
||||
public isActiveTab: boolean = false;
|
||||
|
||||
constructor(@Host() @Inject(CorTabPanelComponent) private panel: CorTabPanelComponent) {
|
||||
|
||||
|
@ -25,8 +26,10 @@ export class CorTabPaneComponent implements OnInit {
|
|||
public ngOnInit(): void {
|
||||
this.panel.addTabPane(this);
|
||||
|
||||
this.panel.activeTab.subscribe((tabId: string) => {
|
||||
this.isActiveTab = (this.id === tabId);
|
||||
});
|
||||
this.panel.activeTab
|
||||
.filter(tabId => tabId != undefined)
|
||||
.subscribe((tabId: string) => {
|
||||
this.isActiveTab = (this.id === tabId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,16 @@ describe("CorTabPanelComponent", () => {
|
|||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
var tabs: CorTabComponent[] = [];
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
|
@ -24,12 +31,26 @@ describe("CorTabPanelComponent", () => {
|
|||
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();
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -28,7 +28,12 @@ export class CorTabPanelComponent implements OnInit, OnChanges {
|
|||
|
||||
public ngOnInit(): void {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ describe("CorTabComponent", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
activeTab = new BehaviorSubject<string>(null);
|
||||
spyOn(activeTab, "subscribe").and.returnValue(null);
|
||||
spyOn(activeTab, "subscribe").and.callThrough();
|
||||
panelMock = new Mock<CorTabPanelComponent>();
|
||||
panelMock.setup(mock => mock.activeTab).is(activeTab);
|
||||
|
||||
|
@ -35,16 +35,25 @@ describe("CorTabComponent", () => {
|
|||
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", () => {
|
||||
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();
|
||||
});
|
||||
|
||||
it("emits output event for tab show if it is new active tab", () => {
|
||||
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();
|
||||
});
|
||||
|
@ -53,8 +62,8 @@ describe("CorTabComponent", () => {
|
|||
const newTabId: string = component.tabId.split('').reverse().join('');
|
||||
component.ngOnInit();
|
||||
// Call twice, first time to set 'isActive' to true
|
||||
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](component.tabId);
|
||||
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](newTabId);
|
||||
panelMock.Object.activeTab.next(component.tabId);
|
||||
panelMock.Object.activeTab.next(newTabId);
|
||||
|
||||
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", () => {
|
||||
const newTabId: string = component.tabId.split('').reverse().join('');
|
||||
component.ngOnInit();
|
||||
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](newTabId);
|
||||
panelMock.Object.activeTab.next(newTabId);
|
||||
|
||||
expect(<Spy>component.tabHide.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Component, Input, Output, Inject, EventEmitter, Host, OnInit } from 'ng-metadata/core';
|
||||
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 {
|
||||
this.panel.activeTab.subscribe((tabId: string) => {
|
||||
if (!this.isActive && this.tabId === tabId) {
|
||||
this.isActive = true;
|
||||
this.tabInit.emit({});
|
||||
this.tabShow.emit({});
|
||||
} else if (this.isActive && this.tabId !== tabId) {
|
||||
this.isActive = false;
|
||||
this.tabHide.emit({});
|
||||
}
|
||||
});
|
||||
this.panel.activeTab
|
||||
.filter(tabId => tabId != undefined)
|
||||
.subscribe((tabId: string) => {
|
||||
if (!this.isActive && this.tabId === tabId) {
|
||||
this.isActive = true;
|
||||
this.tabInit.emit({});
|
||||
this.tabShow.emit({});
|
||||
} else if (this.isActive && this.tabId !== tabId) {
|
||||
this.isActive = false;
|
||||
this.tabHide.emit({});
|
||||
}
|
||||
});
|
||||
|
||||
this.panel.addTab(this);
|
||||
}
|
||||
|
|
13
static/js/directives/ui/cor-tabs/cor-tabs.view-object.ts
Normal file
13
static/js/directives/ui/cor-tabs/cor-tabs.view-object.ts
Normal 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());
|
||||
}
|
||||
}
|
128
static/test/e2e/image-repo.scenario.ts
Normal file
128
static/test/e2e/image-repo.scenario.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -20,7 +20,7 @@ export const config: Config = {
|
|||
framework: 'jasmine',
|
||||
seleniumAddress: 'http://localhost:4444/wd/hub',
|
||||
// Uncomment to run tests against local Chrome instance
|
||||
// directConnect: true,
|
||||
directConnect: true,
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
chromeOptions: {
|
||||
|
@ -60,7 +60,8 @@ export const config: Config = {
|
|||
browser.close();
|
||||
},
|
||||
specs: [
|
||||
'./e2e/sanity.scenario.ts',
|
||||
'./e2e/trigger-creation.scenario.ts'
|
||||
// './e2e/sanity.scenario.ts',
|
||||
// './e2e/trigger-creation.scenario.ts',
|
||||
'./e2e/image-repo.scenario.ts',
|
||||
],
|
||||
};
|
||||
|
|
Reference in a new issue