From 02bfa9e0e0e0359054ae7150a50fba86c8a01f95 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Wed, 8 Apr 2020 15:27:46 +0300 Subject: [PATCH] test(docs-infra): add tests for `universal` docs example (#36483) Previously, there were no tests for the `universal` docs example. This meant that the project was not tested at all (not even ensuring that it can be built successfully). This commit adds e2e tests for the `universal` example (ported from `toh-pt6` and cleaned up) and also verifies that the project can be built successfully (including the server). PR Close #36483 --- .../universal/e2e/src/app.e2e-spec.ts | 300 ++++++++++++++++++ .../examples/universal/example-config.json | 6 +- 2 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 aio/content/examples/universal/e2e/src/app.e2e-spec.ts diff --git a/aio/content/examples/universal/e2e/src/app.e2e-spec.ts b/aio/content/examples/universal/e2e/src/app.e2e-spec.ts new file mode 100644 index 0000000000..92cae94e31 --- /dev/null +++ b/aio/content/examples/universal/e2e/src/app.e2e-spec.ts @@ -0,0 +1,300 @@ +import { browser, by, element, ElementArrayFinder, ElementFinder, logging } from 'protractor'; + +class Hero { + id: number; + name: string; + + // Factory methods + + // Hero from string formatted as ' '. + static fromString(s: string): Hero { + return { + id: +s.substr(0, s.indexOf(' ')), + name: s.substr(s.indexOf(' ') + 1), + }; + } + + // Hero from hero list
  • element. + static async fromLi(li: ElementFinder): Promise { + const stringsFromA = await li.all(by.css('a')).getText(); + const strings = stringsFromA[0].split(' '); + return { id: +strings[0], name: strings[1] }; + } + + // Hero id and name from the given detail element. + static async fromDetail(detail: ElementFinder): Promise { + // Get hero id from the first
    + const id = await detail.all(by.css('div')).first().getText(); + // Get name from the h2 + const name = await detail.element(by.css('h2')).getText(); + return { + id: +id.substr(id.indexOf(' ') + 1), + name: name.substr(0, name.lastIndexOf(' ')) + }; + } +} + +describe('Universal', () => { + const expectedH1 = 'Tour of Heroes'; + const expectedTitle = `${expectedH1}`; + const targetHero = { id: 15, name: 'Magneta' }; + const targetHeroDashboardIndex = 3; + const nameSuffix = 'X'; + const newHeroName = targetHero.name + nameSuffix; + + afterEach(async () => { + // Assert that there are no errors emitted from the browser + const logs = await browser.manage().logs().get(logging.Type.BROWSER); + const severeLogs = logs.filter(entry => entry.level === logging.Level.SEVERE); + expect(severeLogs).toEqual([]); + }); + + describe('Initial page', () => { + beforeAll(() => browser.get('')); + + it(`has title '${expectedTitle}'`, () => { + expect(browser.getTitle()).toEqual(expectedTitle); + }); + + it(`has h1 '${expectedH1}'`, () => { + expectHeading(1, expectedH1); + }); + + const expectedViewNames = ['Dashboard', 'Heroes']; + it(`has views ${expectedViewNames}`, () => { + const viewNames = getPageElts().navElts.map((el: ElementFinder) => el.getText()); + expect(viewNames).toEqual(expectedViewNames); + }); + + it('has dashboard as the active view', () => { + const page = getPageElts(); + expect(page.appDashboard.isPresent()).toBeTruthy(); + }); + }); + + describe('Dashboard tests', () => { + beforeAll(() => browser.get('')); + + it('has top heroes', () => { + const page = getPageElts(); + expect(page.topHeroes.count()).toEqual(4); + }); + + it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero); + + it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView); + + it(`cancels and shows ${targetHero.name} in Dashboard`, () => { + element(by.buttonText('go back')).click(); + browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6 + + const targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex); + expect(targetHeroElt.getText()).toEqual(targetHero.name); + }); + + it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero); + + it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView); + + it(`saves and shows ${newHeroName} in Dashboard`, () => { + element(by.buttonText('save')).click(); + browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6 + + const targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex); + expect(targetHeroElt.getText()).toEqual(newHeroName); + }); + }); + + describe('Heroes tests', () => { + beforeAll(() => browser.get('')); + + it('can switch to Heroes view', () => { + getPageElts().appHeroesHref.click(); + const page = getPageElts(); + expect(page.appHeroes.isPresent()).toBeTruthy(); + expect(page.allHeroes.count()).toEqual(10, 'number of heroes'); + }); + + it('can route to hero details', async () => { + getHeroLiEltById(targetHero.id).click(); + + const page = getPageElts(); + expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail'); + const hero = await Hero.fromDetail(page.heroDetail); + expect(hero.id).toEqual(targetHero.id); + expect(hero.name).toEqual(targetHero.name.toUpperCase()); + }); + + it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView); + + it(`shows ${newHeroName} in Heroes list`, () => { + element(by.buttonText('save')).click(); + browser.waitForAngular(); + const expectedText = `${targetHero.id} ${newHeroName}`; + expect(getHeroAEltById(targetHero.id).getText()).toEqual(expectedText); + }); + + it(`deletes ${newHeroName} from Heroes list`, async () => { + const heroesBefore = await toHeroArray(getPageElts().allHeroes); + const li = getHeroLiEltById(targetHero.id); + li.element(by.buttonText('x')).click(); + + const page = getPageElts(); + expect(page.appHeroes.isPresent()).toBeTruthy(); + expect(page.allHeroes.count()).toEqual(9, 'number of heroes'); + const heroesAfter = await toHeroArray(page.allHeroes); + // console.log(await Hero.fromLi(page.allHeroes[0])); + const expectedHeroes = heroesBefore.filter(h => h.name !== newHeroName); + expect(heroesAfter).toEqual(expectedHeroes); + // expect(page.selectedHeroSubview.isPresent()).toBeFalsy(); + }); + + it(`adds back ${targetHero.name}`, async () => { + const updatedHeroName = 'Alice'; + const heroesBefore = await toHeroArray(getPageElts().allHeroes); + const numHeroes = heroesBefore.length; + + element(by.css('input')).sendKeys(updatedHeroName); + element(by.buttonText('add')).click(); + + const page = getPageElts(); + const heroesAfter = await toHeroArray(page.allHeroes); + expect(heroesAfter.length).toEqual(numHeroes + 1, 'number of heroes'); + + expect(heroesAfter.slice(0, numHeroes)).toEqual(heroesBefore, 'Old heroes are still there'); + + const maxId = heroesBefore[heroesBefore.length - 1].id; + expect(heroesAfter[numHeroes]).toEqual({id: maxId + 1, name: updatedHeroName}); + }); + + it('displays correctly styled buttons', async () => { + element.all(by.buttonText('x')).then(buttons => { + for (const button of buttons) { + // Inherited styles from styles.css + expect(button.getCssValue('font-family')).toBe('Arial'); + expect(button.getCssValue('border')).toContain('none'); + expect(button.getCssValue('padding')).toBe('5px 10px'); + expect(button.getCssValue('border-radius')).toBe('4px'); + // Styles defined in heroes.component.css + expect(button.getCssValue('left')).toBe('194px'); + expect(button.getCssValue('top')).toBe('-32px'); + } + }); + + const addButton = element(by.buttonText('add')); + // Inherited styles from styles.css + expect(addButton.getCssValue('font-family')).toBe('Arial'); + expect(addButton.getCssValue('border')).toContain('none'); + expect(addButton.getCssValue('padding')).toBe('5px 10px'); + expect(addButton.getCssValue('border-radius')).toBe('4px'); + }); + }); + + describe('Progressive hero search', () => { + beforeAll(() => browser.get('')); + + it(`searches for 'Ma'`, async () => { + getPageElts().searchBox.sendKeys('Ma'); + browser.sleep(1000); + + expect(getPageElts().searchResults.count()).toBe(4); + }); + + it(`continues search with 'g'`, async () => { + getPageElts().searchBox.sendKeys('g'); + browser.sleep(1000); + expect(getPageElts().searchResults.count()).toBe(2); + }); + + it(`continues search with 'e' and gets ${targetHero.name}`, async () => { + getPageElts().searchBox.sendKeys('n'); + browser.sleep(1000); + const page = getPageElts(); + expect(page.searchResults.count()).toBe(1); + const hero = page.searchResults.get(0); + expect(hero.getText()).toEqual(targetHero.name); + }); + + it(`navigates to ${targetHero.name} details view`, async () => { + const hero = getPageElts().searchResults.get(0); + expect(hero.getText()).toEqual(targetHero.name); + hero.click(); + + const page = getPageElts(); + expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail'); + const hero2 = await Hero.fromDetail(page.heroDetail); + expect(hero2.id).toEqual(targetHero.id); + expect(hero2.name).toEqual(targetHero.name.toUpperCase()); + }); + }); + + // Helpers + function addToHeroName(text: string): Promise { + return element(by.css('input')).sendKeys(text) as Promise; + } + + async function dashboardSelectTargetHero(): Promise { + const targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex); + expect(targetHeroElt.getText()).toEqual(targetHero.name); + targetHeroElt.click(); + browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6 + + const page = getPageElts(); + expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail'); + const hero = await Hero.fromDetail(page.heroDetail); + expect(hero.id).toEqual(targetHero.id); + expect(hero.name).toEqual(targetHero.name.toUpperCase()); + } + + function expectHeading(hLevel: number, expectedText: string): void { + const hTag = `h${hLevel}`; + const hText = element(by.css(hTag)).getText(); + expect(hText).toEqual(expectedText, hTag); + } + + function getHeroAEltById(id: number): ElementFinder { + const spanForId = element(by.cssContainingText('li span.badge', id.toString())); + return spanForId.element(by.xpath('..')); + } + + function getHeroLiEltById(id: number): ElementFinder { + const spanForId = element(by.cssContainingText('li span.badge', id.toString())); + return spanForId.element(by.xpath('../..')); + } + + function getPageElts() { + const navElts = element.all(by.css('app-root nav a')); + + return { + navElts, + + appDashboardHref: navElts.get(0), + appDashboard: element(by.css('app-root app-dashboard')), + topHeroes: element.all(by.css('app-root app-dashboard > div h4')), + + appHeroesHref: navElts.get(1), + appHeroes: element(by.css('app-root app-heroes')), + allHeroes: element.all(by.css('app-root app-heroes li')), + selectedHeroSubview: element(by.css('app-root app-heroes > div:last-child')), + + heroDetail: element(by.css('app-root app-hero-detail > div')), + + searchBox: element(by.css('#search-box')), + searchResults: element.all(by.css('.search-result li')) + }; + } + + async function toHeroArray(allHeroes: ElementArrayFinder): Promise { + return await allHeroes.map(Hero.fromLi); + } + + async function updateHeroNameInDetailView(): Promise { + // Assumes that the current view is the hero details view. + addToHeroName(nameSuffix); + + const page = getPageElts(); + const hero = await Hero.fromDetail(page.heroDetail); + expect(hero.id).toEqual(targetHero.id); + expect(hero.name).toEqual(newHeroName.toUpperCase()); + } +}); diff --git a/aio/content/examples/universal/example-config.json b/aio/content/examples/universal/example-config.json index 2c13a4178b..5a915d9184 100644 --- a/aio/content/examples/universal/example-config.json +++ b/aio/content/examples/universal/example-config.json @@ -1,3 +1,7 @@ { - "projectType": "universal" + "projectType": "universal", + "e2e": [ + {"cmd": "yarn", "args": ["e2e", "--prod", "--protractor-config=e2e/protractor-puppeteer.conf.js", "--no-webdriver-update", "--port={PORT}"]}, + {"cmd": "yarn", "args": ["run", "build:ssr"]} + ] }