diff --git a/.gitignore b/.gitignore index 86d943a..a330e61 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ # dependencies /node_modules +/e2e/node_modules # profiling files chrome-profiler-events*.json diff --git a/e2e/protractor.conf.js b/e2e/protractor.conf.js index 73e4e68..a982b5d 100644 --- a/e2e/protractor.conf.js +++ b/e2e/protractor.conf.js @@ -1,32 +1,46 @@ -// @ts-check // Protractor configuration file, see link for more information // https://github.com/angular/protractor/blob/master/lib/config.ts - -const { SpecReporter } = require('jasmine-spec-reporter'); - -/** - * @type { import("protractor").Config } - */ exports.config = { allScriptsTimeout: 11000, - specs: [ - './src/**/*.e2e-spec.ts' - ], + getPageTimeout: 60000, capabilities: { - 'browserName': 'chrome' + 'browserName': 'chrome', + 'chromeOptions': { + 'args': [ + '--start-maximized', // Uncomment to launch tests using the browser and comment the argument `properties` below + // '--headless', + // '--disable-gpu', + // '--window-size=1920,1080', + // '--no-sandbox', + // '--disable-dev-shm-usage' + ] + } }, directConnect: true, - baseUrl: 'http://localhost:4200/', - framework: 'jasmine', - jasmineNodeOpts: { - showColors: true, - defaultTimeoutInterval: 30000, - print: function() {} + baseUrl: 'localhost:4200', + specs: [ + 'src/feature/employee/employee.feature', + ], + framework: 'custom', + frameworkPath: require.resolve('protractor-cucumber-framework'), + cucumberOpts: { + require: [ + 'src/step_definition/*.definitions.ts', + 'src/hooks/generic.hooks.ts' + ], + tags: '@Employee', + strict: true, + format: [ + `json:reports/mryumul.json`, + ], + dryRun: false, + compiler: [] }, + params: {}, onPrepare() { require('ts-node').register({ - project: require('path').join(__dirname, './tsconfig.json') + project: 'e2e/tsconfig.e2e.json' }); - jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); - } -}; \ No newline at end of file + }, + // ignoreUncaughtExceptions: true, +}; diff --git a/e2e/src/feature/employee/employee.feature b/e2e/src/feature/employee/employee.feature new file mode 100644 index 0000000..4ef6622 --- /dev/null +++ b/e2e/src/feature/employee/employee.feature @@ -0,0 +1,32 @@ +@Employee +Feature: Employee + As a user + I should be able to create, read, update, and delete an Employee record + + @Create + Scenario: Create an Employee record + Given I am on the dashboard page + When I "create" an employee record + Then I should see the success message "Success: Data has been added!" + And I should "see" the "created" employee record in the list + + @Read + Scenario: Read Employee records + Given I am on the dashboard page + When I "read" an employee record + Then I should see the employee records in the list + + @Update + Scenario: Update an Employee record + Given I am on the dashboard page + And There are existing employee records + When I "update" an employee record + Then I should see the success message "Success: Data has been updated!" + And I should "see" the "updated" employee record in the list + + @Delete + Scenario: Delete an Employee record + Given I am on the dashboard page + And There are existing employee records + When I "delete" an employee record + Then I should "not see" the "deleted" employee record in the list diff --git a/e2e/src/hooks/generic.hooks.ts b/e2e/src/hooks/generic.hooks.ts new file mode 100644 index 0000000..d00adfa --- /dev/null +++ b/e2e/src/hooks/generic.hooks.ts @@ -0,0 +1,19 @@ +import { BeforeAll, Before, setDefaultTimeout } from 'cucumber'; +import { browser, protractor } from 'protractor'; +import { SecurityPage } from '../po/security.po'; + +const EC = protractor.ExpectedConditions; + +BeforeAll(() => { + browser.waitForAngularEnabled(false); + setDefaultTimeout(60 *10000); +}); + +Before(async () => { + // Check if logged out, else login + if (await browser.getCurrentUrl() === 'data:,' || await browser.getCurrentUrl() === 'http://localhost:4200/login') { + browser.get(`http://localhost:4200/login`); + await browser.wait(EC.urlIs(`http://localhost:4200/login`), browser.params.defaultTimer); + await SecurityPage.login(); + } +}); diff --git a/e2e/src/model/employee.class.ts b/e2e/src/model/employee.class.ts new file mode 100644 index 0000000..3139340 --- /dev/null +++ b/e2e/src/model/employee.class.ts @@ -0,0 +1,115 @@ +export interface EmployeeDetails { + id?: number; + firstName: string; + lastName: string; + country?: string; + nationality?: string; + company?: string; + designation?: string; + workExp?: string; + dataSource?: string; +} + +export class Employee implements EmployeeDetails { + id: number; + firstName: string; + lastName: string; + country: string; + nationality: string; + company: string; + designation: string; + workExp: string; + dataSource: string; + + constructor( + employeeDetails: EmployeeDetails + ) { + this.id = employeeDetails.id; + this.firstName = employeeDetails.firstName; + this.lastName = employeeDetails.lastName; + this.company = employeeDetails.company; + this.country = employeeDetails.country; + this.nationality = employeeDetails.nationality; + this.designation = employeeDetails.designation; + this.workExp = employeeDetails.workExp; + this.dataSource = employeeDetails.dataSource; + } + + get getFirstName() { + return this.firstName; + } + + get getLastName() { + return this.lastName; + } + + get getCountry() { + return this.country; + } + + get getNationality() { + return this.nationality; + } + + get getCompany() { + return this.company; + } + + get getDesignation() { + return this.designation; + } + + get getWorkExp() { + return this.workExp; + } + + get getDataSource() { + return this.dataSource; + } + + set updateFirstName(newName: string) { + this.firstName = newName; + } + + set updateLastName(newLastName: string) { + this.lastName = newLastName; + } + + set setCountry(newCountry: string) { + this.country = newCountry; + } + + set setNationality(newNationality: string) { + this.nationality = newNationality; + } + + set setCompany(newCompany: string) { + this.company = newCompany; + } + + set setDesignation(newDesignation: string) { + this.designation = newDesignation; + } + + set setWorkExp(newWorkExp: string) { + this.workExp = newWorkExp; + } + + set setDataSource(newDataSource: string) { + this.dataSource = newDataSource; + } + + get details(): EmployeeDetails { + return { + id: this.id, + firstName: this.firstName, + lastName: this.lastName, + country: this.country, + nationality: this.nationality, + company: this.company, + designation: this.designation, + workExp: this.workExp, + dataSource: this.dataSource, + }; + } +} diff --git a/e2e/src/po/dashboard.po.ts b/e2e/src/po/dashboard.po.ts new file mode 100644 index 0000000..bfa86f5 --- /dev/null +++ b/e2e/src/po/dashboard.po.ts @@ -0,0 +1,74 @@ +import { element, by, browser } from 'protractor'; +import { FormPage } from './form.po'; +import { Employee, EmployeeDetails } from '../model/employee.class'; + +export class DashboardPage { + static get table() { + return element(by.tagName('p-table')); + } +​ + static get tableRows() { + return this.table.all(by.tagName('tr')); + } + + static get tableBody() { + return this.table.all(by.tagName('tbody')).get(0); + } + + static get tableBodyRows() { + return this.tableBody.all(by.tagName('tr')); + } +​ + static get tableNextPageButton() { + return element(by.css('span[class="ui-paginator-icon pi pi-caret-right"]')); + } + + static async checkTableRowForData(data: string) { + let foundData = null; + for (const trData of (await this.tableRows.getText() as any)) { + console.log('tr ->', trData) + console.log('data ->', data) + if (trData.includes(data)) { + foundData = trData; + break; + } + } +​ + return foundData; + } + + static get newEmployeeButton() { + return element(by.buttonText('New Employee')); + } + + static deleteEmployeeButton(index: number) { + return element.all(by.buttonText('Delete')).get(index); + } + + static async createEmployee(createData: EmployeeDetails) { + await this.newEmployeeButton.click(); + await FormPage.fillForm(createData); + await browser.sleep(800); + await FormPage.formButton('Add').click(); + } + + static async updateEmployee(updateData: EmployeeDetails): Promise { + const employeeRecordElement = this.tableBodyRows.get(4); + const employeeRecordID = JSON.parse(await employeeRecordElement.all(by.tagName('td')).get(0).getText()); + + await employeeRecordElement.click(); + await FormPage.fillForm(updateData); + await browser.sleep(800); + await FormPage.formButton('Update').click(); + + updateData.id = employeeRecordID; + return updateData; + } + + static async deleteEmployee(): Promise { + await this.deleteEmployeeButton(3).click(); + await (await browser.switchTo().alert()).accept(); + + return new Employee({id: 3, firstName: 'Doris', lastName: 'Wilder'}); + } +} diff --git a/e2e/src/po/form.po.ts b/e2e/src/po/form.po.ts new file mode 100644 index 0000000..198e144 --- /dev/null +++ b/e2e/src/po/form.po.ts @@ -0,0 +1,45 @@ +import { element, by, ElementFinder } from 'protractor'; +import { EmployeeDetails, Employee } from '../model/employee.class'; + + + +export class FormPage { + + static get updateButton() { + return element(by.buttonText('Update')); + } + + static get newEmployeeButton(): ElementFinder{ + return element(by.id('new-employee')); + } + + static getFormControlElement(formControlName: string) { + return element(by.css(`[formcontrolname=${formControlName}]`)); + } + + static formButton(buttonText: string) { + return element(by.buttonText(buttonText)); + } + + static async fillForm(employeeData: EmployeeDetails) { + // tslint:disable-next-line: forin + for (const key in employeeData) { + if (key === 'id') { + continue; + } + + const formElement = this.getFormControlElement(key); + + if (key !== 'dataSource') { + await formElement.clear(); + await formElement.sendKeys(employeeData[key]); + } else { + await formElement.click(); + const dropdownItemsElements = element.all(by.tagName('p-dropdownitem')); + const dropdownItems = await dropdownItemsElements.getText(); + + await dropdownItemsElements.get(dropdownItems.indexOf(employeeData[key])).click(); + } + } + } +} diff --git a/e2e/src/po/message.po.ts b/e2e/src/po/message.po.ts new file mode 100644 index 0000000..09e57af --- /dev/null +++ b/e2e/src/po/message.po.ts @@ -0,0 +1,11 @@ +import { element, by } from 'protractor'; +​ +export class MessagePage { + static get successMessage() { + return element(by.id('success-message')); + } +​ +​ static get updatedMessage() { + return element(by.css('span[class="ui-messages-detail ng-tns-c3-2 ng-star-inserted"]')); + } +} diff --git a/e2e/src/po/security.po.ts b/e2e/src/po/security.po.ts new file mode 100644 index 0000000..386e5b8 --- /dev/null +++ b/e2e/src/po/security.po.ts @@ -0,0 +1,21 @@ +import { element, by, ElementFinder, logging, browser } from 'protractor'; +import { protractor } from 'protractor'; + +export class SecurityPage { + + static get usernameField(): ElementFinder{ + return; + } + + static get passwordField(): ElementFinder{ + return; + } + + static get loginButton(): ElementFinder{ + return; + } + + + static async login(){ + } +} diff --git a/e2e/src/step_definition/employee.definitions.ts b/e2e/src/step_definition/employee.definitions.ts new file mode 100644 index 0000000..0ba7c32 --- /dev/null +++ b/e2e/src/step_definition/employee.definitions.ts @@ -0,0 +1,171 @@ +import { Given, When, Then } from 'cucumber'; +import { browser, element, by } from 'protractor'; +import { DashboardPage } from '../po/dashboard.po'; +import { MessagePage } from '../po/message.po'; +import { Employee, EmployeeDetails } from '../model/employee.class'; +import { TestUtils } from '../utils/test.utils'; + +const chai = require('chai').use(require('chai-as-promised')); +const expect = chai.expect; + +let data: EmployeeDetails; + +Given('I am on the dashboard page', async function() { + const currentUrl = await browser.getCurrentUrl(); + const dashboardUrl = 'http://localhost:4300/dashboard'; + + if (currentUrl !== dashboardUrl) { + await browser.get(dashboardUrl); + } + + const dashboardElement = element(by.tagName('app-dashboard')); + if (!(await dashboardElement.isPresent())) { + throw Error('The user is not on the dashboard page'); + } +}); + +Given('There are existing employee records', async function() { + if (!(await DashboardPage.tableBodyRows.get(0).isPresent())) { + throw Error('There are no existing employee records'); + } +}) + +When('I {string} an employee record', async function(action: string) { + switch (action) { + case 'create': + data = new Employee({ + firstName: 'Arden', + lastName: 'Mercado', + country: 'Japan', + nationality: 'Nihonjin', + company: 'TitusUniversal', + designation: 'CEO', + workExp: '69 years', + dataSource: 'Monster Gulf' + }).details; + await DashboardPage.createEmployee(data); + break; + case 'read': + // user should read with his/her eyes + break; + case 'update': + data = await DashboardPage.updateEmployee(new Employee( + { + firstName: 'Ivan', + lastName: 'Mercado', + country: 'Japan', + nationality: 'Nihonjin', + company: 'TitusUniversal', + designation: 'CEO', + workExp: '69 years', + dataSource: 'Monster Gulf' + } + ).details); + break; + case 'delete': + data = await DashboardPage.deleteEmployee(); + break; + default: + throw Error(`Action "${action}" for the employee record is not yet implemented!`) + } + + await browser.sleep(1000); +}); + +Then('I should see the success message {string}', async function(expectedMessage: string) { + const messageObject = MessagePage.successMessage; + expect(true, 'The success message was not displayed').to.equal(await messageObject.isPresent()); + expect(await messageObject.getText(), 'The success message did not match the expected message').to.equal(expectedMessage); +}); + +Then('I should {string} the {string} employee record in the list', async function(view: string, action: string) { + const { firstName, lastName } = data; + const fullName = `${firstName} ${lastName}`; + + if (view === 'see') { + switch(action) { + case 'created': + case 'updated': + + let rowData; + + if (!(await DashboardPage.checkTableRowForData(fullName))) { + await DashboardPage.tableNextPageButton.click(); + await browser.sleep(1000); + } + + rowData = (await DashboardPage.checkTableRowForData(fullName) as string); + rowData = rowData ? (rowData.replace('Delete', '')).trim() : null; + + expect(TestUtils.stringifyData(data).trim()).to.equal(rowData); + break; + default: + throw Error(`Action "${action}" for the employee record is not yet implemented!`) + } + } else { + const rowData = (await DashboardPage.checkTableRowForData(fullName) as string); + expect(null, 'The data is still in the table').to.equal(rowData); + } +}); + +Then('I should see the employee record(s) in the list', async function() { + let dataWasNotFound = false; + + const brender = new Employee( + { + id: 0, + firstName: 'Brenden', + lastName: 'Wagner', + country: 'United States of America, California', + nationality: 'American', + company: 'Facebook', + designation: 'Software Engineer', + workExp: '8', + dataSource: 'Facebook' + } + ); + + const cara = new Employee( + { + id: 1, + firstName: 'Cara', + lastName: 'Steves', + country: 'United States of America, New York', + nationality: 'American', + company: 'Walmart', + designation: 'Sales Assistant', + workExp: '5', + dataSource: 'Google' + } + ); + + const jenny = new Employee( + { + id: 4, + firstName: 'Jenny', + lastName: 'Chang', + country: 'Singapore, Singapore', + nationality: 'Chinese', + company: 'Singapore Airlines', + designation: 'Regional Director', + workExp: '15', + dataSource: 'Twitter' + } + ); + + const expectedEmployeeList = []; + const expectedEmployeeDetails: EmployeeDetails[] = [brender.details , cara.details, jenny.details]; + + for (const details of expectedEmployeeDetails) { + expectedEmployeeList.push(TestUtils.stringifyData(details)); + } + + for (const employeeData of expectedEmployeeList) { + if (!await DashboardPage.checkTableRowForData(employeeData.trim() + '\nDelete')) { + dataWasNotFound = true; + break; + } + } + + expect(false, 'The expected data was not found in the table').to.equal(dataWasNotFound); +}); diff --git a/e2e/src/step_definition/security.definitions.ts b/e2e/src/step_definition/security.definitions.ts new file mode 100644 index 0000000..e69de29 diff --git a/e2e/src/utils/test.utils.ts b/e2e/src/utils/test.utils.ts new file mode 100644 index 0000000..f8312a8 --- /dev/null +++ b/e2e/src/utils/test.utils.ts @@ -0,0 +1,13 @@ +export class TestUtils { + private constructor() {} + + static stringifyData(data: object) { + let stringifiedData = ''; + + Object.keys(data).forEach(key => { + stringifiedData = `${stringifiedData} ${data[key]}`; + }); + + return stringifiedData; + } +} diff --git a/package.json b/package.json index 3c90325..bed3c2d 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@types/jasminewd2": "~2.0.3", "@types/node": "~8.9.4", "codelyzer": "^5.0.0", + "cucumber": "^6.0.5", "jasmine-core": "~3.4.0", "jasmine-spec-reporter": "~4.2.1", "karma": "~4.1.0", @@ -45,6 +46,7 @@ "karma-jasmine-html-reporter": "^1.4.0", "lodash": "^4.17.15", "protractor": "~5.4.0", + "protractor-cucumber-framework": "^6.2.0", "ts-node": "~7.0.0", "tslint": "~5.15.0", "typescript": "~3.5.3" diff --git a/protractor.conf.js b/protractor.conf.js new file mode 100644 index 0000000..44904a7 --- /dev/null +++ b/protractor.conf.js @@ -0,0 +1,45 @@ +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts +exports.config = { + allScriptsTimeout: 11000, + getPageTimeout: 60000, + capabilities: { + 'browserName': 'chrome', + 'chromeOptions': { + 'args': [ + '--start-maximized', // Uncomment to launch tests using the browser and comment the argument `properties` below + // '--headless', + // '--disable-gpu', + // '--window-size=1920,1080', + // '--no-sandbox', + // '--disable-dev-shm-usage' + ] + } + }, + directConnect: true, + baseUrl: 'localhost:4300', + specs: [ + 'e2e/src/feature/security/security.feature' + ], + framework: 'custom', + frameworkPath: require.resolve('protractor-cucumber-framework'), + cucumberOpts: { + require: [ + './e2e/src/step_definition/security.definitions.ts' + ], + tags: [], + strict: true, + format: [ + `json:reports/mryumul.json`, + ], + dryRun: false, + compiler: [] + }, + params: {}, + onPrepare() { + require('ts-node').register({ + project: 'e2e/tsconfig.e2e.json' + }); + }, + // ignoreUncaughtExceptions: true, +}; diff --git a/reports/mryumul.json b/reports/mryumul.json new file mode 100644 index 0000000..ff99e67 --- /dev/null +++ b/reports/mryumul.json @@ -0,0 +1,71 @@ +[ + { + "description": " As a user\n I should be able to access the system with proper credentials", + "keyword": "Feature", + "name": "Security", + "line": 2, + "id": "security", + "tags": [ + { + "name": "@Security", + "line": 1 + } + ], + "uri": "e2e\\src\\feature\\security\\security.feature", + "elements": [ + { + "id": "security;login", + "keyword": "Scenario", + "line": 6, + "name": "Login", + "tags": [ + { + "name": "@Security", + "line": 1 + } + ], + "type": "scenario", + "steps": [ + { + "arguments": [], + "keyword": "Given ", + "line": 7, + "name": "I am on the login page", + "result": { + "status": "undefined" + } + }, + { + "arguments": [], + "keyword": "When ", + "line": 8, + "name": "I log in", + "result": { + "status": "undefined" + } + }, + { + "arguments": [], + "keyword": "Then ", + "line": 9, + "name": "I should be redirected to the dashboard", + "result": { + "status": "undefined" + } + }, + { + "keyword": "After", + "hidden": true, + "match": { + "location": "node_modules\\protractor-cucumber-framework\\lib\\resultsCapturer.js:27" + }, + "result": { + "status": "passed", + "duration": 2000000 + } + } + ] + } + ] + } +] \ No newline at end of file