diff --git a/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.spec.ts b/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.spec.ts index 46441848a..a86296d93 100644 --- a/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.spec.ts +++ b/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.spec.ts @@ -12,11 +12,12 @@ describe("CorTabPaneComponent", () => { beforeEach(() => { activeTab = new BehaviorSubject(null); - spyOn(activeTab, "subscribe").and.returnValue(null); + spyOn(activeTab, "subscribe").and.callThrough(); panelMock = new Mock(); panelMock.setup(mock => mock.activeTab).is(activeTab); component = new CorTabPaneComponent(panelMock.Object); + component.id = 'description'; }); describe("ngOnInit", () => { @@ -36,5 +37,27 @@ describe("CorTabPaneComponent", () => { expect((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); + }); }); }); diff --git a/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.ts b/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.ts index 95867aa3b..5f64c7add 100644 --- a/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.ts +++ b/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.ts @@ -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); + }); } } diff --git a/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.spec.ts b/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.spec.ts index c26634472..e0a616d92 100644 --- a/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.spec.ts +++ b/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.spec.ts @@ -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(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((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((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"; - (component.activeTab.subscribe).calls.argsFor(0)[0](tabId); + component.activeTab.next(tabId); - expect((component.tabChange.emit).calls.argsFor(0)[0]).toEqual(tabId); + expect((component.tabChange.emit).calls.argsFor(1)[0]).toEqual(tabId); }); }); diff --git a/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.ts b/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.ts index 3d3321054..01a1740a6 100644 --- a/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.ts +++ b/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.ts @@ -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); + } }); } diff --git a/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.spec.ts b/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.spec.ts index ead20c980..6f471beb1 100644 --- a/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.spec.ts +++ b/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.spec.ts @@ -12,7 +12,7 @@ describe("CorTabComponent", () => { beforeEach(() => { activeTab = new BehaviorSubject(null); - spyOn(activeTab, "subscribe").and.returnValue(null); + spyOn(activeTab, "subscribe").and.callThrough(); panelMock = new Mock(); panelMock.setup(mock => mock.activeTab).is(activeTab); @@ -35,16 +35,25 @@ describe("CorTabComponent", () => { expect((panelMock.Object.activeTab.subscribe)).toHaveBeenCalled(); }); + it("does nothing if active tab ID is undefined", () => { + component.ngOnInit(); + panelMock.Object.activeTab.next(null); + + expect(component.tabInit.emit).not.toHaveBeenCalled(); + expect(component.tabShow.emit).not.toHaveBeenCalled(); + expect(component.tabHide.emit).not.toHaveBeenCalled(); + }); + it("emits output event for tab init if it is new active tab", () => { component.ngOnInit(); - (panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](component.tabId); + panelMock.Object.activeTab.next(component.tabId); expect(component.tabInit.emit).toHaveBeenCalled(); }); it("emits output event for tab show if it is new active tab", () => { component.ngOnInit(); - (panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](component.tabId); + panelMock.Object.activeTab.next(component.tabId); expect(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 - (panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](component.tabId); - (panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](newTabId); + panelMock.Object.activeTab.next(component.tabId); + panelMock.Object.activeTab.next(newTabId); expect(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(); - (panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](newTabId); + panelMock.Object.activeTab.next(newTabId); expect(component.tabHide.emit).not.toHaveBeenCalled(); }); diff --git a/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.ts b/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.ts index 2bb832194..0fc76f7fd 100644 --- a/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.ts +++ b/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.ts @@ -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); } diff --git a/static/js/directives/ui/cor-tabs/cor-tabs.view-object.ts b/static/js/directives/ui/cor-tabs/cor-tabs.view-object.ts new file mode 100644 index 000000000..c07ea281f --- /dev/null +++ b/static/js/directives/ui/cor-tabs/cor-tabs.view-object.ts @@ -0,0 +1,13 @@ +import { element, by, browser, $, ElementFinder, ExpectedConditions as until } from 'protractor'; + + +export class CorTabsViewObject { + + public selectTabByTitle(title: string): Promise { + return Promise.resolve($(`cor-tab[tab-title="${title}"] a`).click()); + } + + public isActiveTab(title: string): Promise { + return Promise.resolve($(`cor-tab[tab-title="${title}"] .cor-tab-itself.active`).isPresent()); + } +} diff --git a/static/test/e2e/image-repo.scenario.ts b/static/test/e2e/image-repo.scenario.ts new file mode 100644 index 000000000..ff000e3e0 --- /dev/null +++ b/static/test/e2e/image-repo.scenario.ts @@ -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); + }); + }); +}); diff --git a/static/test/protractor.conf.ts b/static/test/protractor.conf.ts index 9016c1743..e69f9753d 100644 --- a/static/test/protractor.conf.ts +++ b/static/test/protractor.conf.ts @@ -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', ], };