├── livecode-rest-backend ├── .gitignore ├── framework │ ├── config │ │ ├── index.ts │ │ └── urls.ts │ ├── index.ts │ └── services │ │ ├── admin.service.ts │ │ ├── index.ts │ │ └── user.service.ts ├── lib │ ├── index.ts │ ├── report │ │ └── index.ts │ ├── request │ │ └── index.ts │ └── service.base.interface.ts ├── package.json ├── playground.test.ts ├── servers.stub.js ├── specs │ └── user.spec.ts └── tsconfig.json ├── livecode-selenium ├── .gitignore ├── lib │ ├── browser.js │ ├── element.js │ ├── index.js │ └── share.js ├── package.json ├── specs │ └── index.spec.js └── test.js ├── livecode ├── .gitignore ├── framework │ ├── config │ │ └── protractor.conf.ts │ ├── index.ts │ └── pages │ │ ├── index.ts │ │ └── main │ │ ├── fragments │ │ ├── header.fragment.ts │ │ ├── index.ts │ │ ├── login.fragment.ts │ │ └── register.fragment.ts │ │ └── page.ts ├── lib │ ├── base.fragment.interface.ts │ ├── base.page.interface.ts │ ├── element_utils │ │ ├── index.ts │ │ └── waiter.ts │ ├── elements │ │ ├── base.element.ts │ │ ├── button.ts │ │ ├── index.ts │ │ ├── input.ts │ │ └── text.ts │ ├── index.ts │ └── report │ │ ├── console.ts │ │ └── index.ts ├── package-lock.json ├── package.json ├── protractor.conf.js ├── specs │ └── noop.spec.ts └── tsconfig.json ├── livecoding-codeceptjs ├── .gitignore ├── codecept.conf.js ├── jsconfig.json ├── output │ └── Open_machine_details.failed.png ├── package.json ├── pages │ ├── main.login.fragment.js │ ├── main.register.fragment.js │ └── table.fragment.js ├── specs │ └── main │ │ ├── main.login.spec.js │ │ ├── main.register.spec.js │ │ └── table.spec.js ├── steps.d.ts └── steps_file.js ├── livecoding-playwright-part-2 ├── .gitignore ├── example.png ├── framework │ ├── index.js │ └── pages │ │ ├── index.js │ │ ├── main │ │ ├── fragments │ │ │ ├── header.js │ │ │ ├── login.js │ │ │ └── register.js │ │ └── index.js │ │ └── tables │ │ └── index.js ├── lib │ ├── base.fragment.implementation.js │ ├── base.page.implementation.js │ ├── elements │ │ ├── base.element.js │ │ └── index.js │ ├── helpers │ │ ├── index.js │ │ ├── make.singleton.js │ │ └── waits.js │ ├── index.js │ └── page.conditions.js ├── package-lock.json ├── package.json └── specs │ └── noop.spec.js ├── livecoding-playwright-part-3 ├── .gitignore ├── allure-results │ ├── 0b10885f-4e4c-4e21-abe8-b8ac751077bd-attachment.json │ ├── 4192f607-8a20-4407-886a-646ce5182edc-attachment.json │ ├── 4d8703f3-ff5f-49c6-a123-9209cc70e299-attachment.json │ ├── 566ebb7c-6e27-440b-88e1-c1f852e97d94-attachment.json │ ├── 57e46dca-d59d-4f6d-88e3-98181c6ff220-result.json │ ├── 6617f414-ac37-469f-acdc-294d245a36d0-attachment.json │ ├── 757c86b0-6dc9-45e5-992e-a7953aa902f9-result.json │ ├── 8211c1d8-b407-4edd-beb9-9a37c3c5e6d2-attachment.json │ ├── 84577fa2-31f4-486f-bb56-099fd4ae395e-attachment.json │ ├── 8f0a22b3-8e93-4387-893e-889cd31de987-result.json │ ├── b119174c-b6b1-4e5d-8102-dbce4e6fb207-attachment.json │ ├── c6752d0d-be8d-45ea-9ea5-25333ba23174-attachment.json │ ├── d221c36d-1f50-4887-880d-1cfdbd1fa1c9-attachment.json │ ├── d4f5cc31-5526-48b3-b856-274ed2784cb8-container.json │ ├── e302f7e2-343d-4485-b06d-8d8e7c8935f7-container.json │ ├── e9f2d824-5b03-4a9f-b355-b2a40882e49d-result.json │ ├── f84fa715-a170-4eb8-bb92-9c49291561ed-attachment.json │ └── fc55c526-4841-4967-aafb-be0f1e959ba2-attachment.json ├── example.png ├── framework │ ├── index.ts │ └── pages │ │ ├── index.ts │ │ ├── main │ │ ├── fragments │ │ │ ├── header.ts │ │ │ ├── login.ts │ │ │ └── register.ts │ │ └── index.ts │ │ └── tables │ │ └── index.ts ├── lib │ ├── base.fragment.implementation.ts │ ├── base.page.implementation.ts │ ├── browser │ │ └── index.ts │ ├── elements │ │ ├── base.element.ts │ │ └── index.ts │ ├── helpers │ │ ├── index.ts │ │ ├── make.singleton.ts │ │ └── waits.ts │ ├── index.ts │ └── page.conditions.ts ├── package-lock.json ├── package.json ├── specs │ └── noop.spec.ts └── tsconfig.json ├── livecoding-playwright ├── .gitignore ├── example.png ├── framework │ ├── index.js │ └── pages │ │ ├── index.js │ │ ├── main │ │ ├── fragments │ │ │ ├── header.js │ │ │ ├── login.js │ │ │ └── register.js │ │ └── index.js │ │ └── tables │ │ └── index.js ├── lib │ ├── helpers │ │ ├── index.js │ │ ├── make.singleton.js │ │ └── waits.js │ ├── index.js │ └── page.conditions.js ├── package-lock.json ├── package.json └── specs │ └── noop.spec.js ├── livecoding-promod ├── .gitignore ├── mocha.hooks.ts ├── package-lock.json ├── package.json ├── pages │ └── main │ │ ├── fragments │ │ └── login.fragment.ts │ │ └── page.ts ├── specs │ └── example.spec.ts └── tsconfig.json ├── livecoding-puppeteer-video-1 ├── .gitignore ├── framework │ └── pages │ │ └── main.page.ts ├── lib │ ├── base.page.ts │ ├── browser.ts │ ├── element.ts │ ├── index.ts │ ├── pubsub.ts │ └── reporter │ │ ├── allure.ts │ │ ├── console.ts │ │ └── index.ts ├── package.json ├── specs │ └── login.spec.ts └── tsconfig.json ├── livecoding-puppeteer-video-2 ├── .gitignore ├── framework │ └── pages │ │ ├── index.ts │ │ ├── main.page.ts │ │ └── table.page.ts ├── lib │ ├── base.page.ts │ ├── browser.ts │ ├── elements │ │ ├── button.ts │ │ ├── element.ts │ │ ├── index.ts │ │ ├── input.ts │ │ └── text.ts │ ├── helpers │ │ ├── index.ts │ │ ├── logger.ts │ │ └── waits.ts │ ├── index.ts │ ├── pubsub.ts │ └── reporter │ │ ├── allure.ts │ │ ├── console.ts │ │ └── index.ts ├── package.json ├── specs │ └── login.spec.ts └── tsconfig.json ├── livecoding-puppeteer-video-3 ├── .gitignore ├── framework │ └── pages │ │ ├── index.ts │ │ ├── main.page.ts │ │ └── table.page.ts ├── lib │ ├── base.page.ts │ ├── browser.ts │ ├── elements │ │ ├── button.ts │ │ ├── element.ts │ │ ├── index.ts │ │ ├── input.ts │ │ ├── table.ts │ │ └── text.ts │ ├── helpers │ │ ├── index.ts │ │ ├── logger.ts │ │ └── waits.ts │ ├── index.ts │ ├── pubsub.ts │ └── reporter │ │ ├── allure.ts │ │ ├── console.ts │ │ └── index.ts ├── package.json ├── specs │ ├── login.spec.ts │ └── machine.spec.ts └── tsconfig.json ├── livecoding-puppeteer-video-4 ├── .gitignore ├── execution │ └── index.js ├── framework │ ├── index.ts │ └── pages │ │ ├── index.ts │ │ ├── main.page.ts │ │ └── table.page.ts ├── lib │ ├── base.page.ts │ ├── browser.ts │ ├── elements │ │ ├── button.ts │ │ ├── element.ts │ │ ├── index.ts │ │ ├── input.ts │ │ ├── table.ts │ │ └── text.ts │ ├── helpers │ │ ├── index.ts │ │ ├── logger.ts │ │ └── waits.ts │ ├── index.ts │ ├── init.singleton.ts │ ├── mocha.utils.ts │ ├── pubsub.ts │ └── reporter │ │ ├── allure.ts │ │ ├── console.ts │ │ └── index.ts ├── package.json ├── specs │ ├── login.spec.ts │ └── machine.spec.ts └── tsconfig.json ├── package-lock.json ├── wdio-soft ├── .gitignore ├── package.json └── test │ ├── pageobjects │ ├── login.page.ts │ ├── page.ts │ └── secure.page.ts │ ├── specs │ └── example.e2e.ts │ ├── tsconfig.json │ └── wdio.conf.js ├── webdriver-lazy-element ├── .gitignore ├── framework │ ├── index.ts │ └── pages │ │ ├── index.ts │ │ └── main.ts ├── lib │ ├── element.utils.ts │ ├── index.ts │ └── super.element.utils.ts ├── package-lock.json ├── package.json ├── specs │ └── base.spec.ts ├── tsconfig.json └── wdio.conf.js └── webdriverio-test ├── .gitignore ├── framework ├── index.ts └── pages │ ├── index.ts │ └── main.ts ├── lib ├── element.utils.ts └── index.ts ├── package-lock.json ├── package.json ├── specs └── base.spec.ts ├── tsconfig.json └── wdio.conf.js /livecode-rest-backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /livecode-rest-backend/framework/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './urls'; 2 | -------------------------------------------------------------------------------- /livecode-rest-backend/framework/config/urls.ts: -------------------------------------------------------------------------------- 1 | const urls = { 2 | userService: 'http://localhost:8888', 3 | adminService: 'http://localhost:8081', 4 | } 5 | 6 | export { 7 | urls 8 | } 9 | -------------------------------------------------------------------------------- /livecode-rest-backend/framework/index.ts: -------------------------------------------------------------------------------- 1 | import {UserService, AdminService} from './services'; 2 | 3 | const serviceProvider = { 4 | user: new UserService(), 5 | admin: new AdminService(), 6 | } 7 | 8 | export * from './config'; 9 | export { 10 | serviceProvider 11 | } 12 | -------------------------------------------------------------------------------- /livecode-rest-backend/framework/services/admin.service.ts: -------------------------------------------------------------------------------- 1 | import {BaseInterface, decorateService} from '../../lib'; 2 | import {urls} from '../config' 3 | 4 | class AdminService extends BaseInterface { 5 | constructor(host = urls.adminService) { 6 | super(host); 7 | } 8 | } 9 | decorateService(AdminService); 10 | 11 | export { 12 | AdminService 13 | } 14 | -------------------------------------------------------------------------------- /livecode-rest-backend/framework/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './admin.service'; 2 | export * from './user.service'; 3 | -------------------------------------------------------------------------------- /livecode-rest-backend/framework/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import {BaseInterface, decorateService} from '../../lib'; 2 | import {urls} from '../config' 3 | 4 | class UserService extends BaseInterface { 5 | constructor(host = urls.userService) { 6 | super(host); 7 | } 8 | 9 | async getUserData() { 10 | return this.req.get({path: '/user-data'}) 11 | } 12 | 13 | async userLogin(body: {username?: string, password?: string}) { 14 | return this.req.post({path: '/user-login', body}) 15 | } 16 | 17 | } 18 | decorateService(UserService); 19 | 20 | export { 21 | UserService 22 | } 23 | -------------------------------------------------------------------------------- /livecode-rest-backend/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './service.base.interface'; 2 | export {decorateService} from './report'; -------------------------------------------------------------------------------- /livecode-rest-backend/lib/report/index.ts: -------------------------------------------------------------------------------- 1 | import * as chalk from 'chalk'; 2 | 3 | function logger(message, ...args) { 4 | console.log(chalk.green(message), ...args) 5 | } 6 | 7 | /** 8 | * 9 | * @param {object} class 10 | */ 11 | function decorateService(target) { 12 | const methods = Object 13 | .getOwnPropertyNames(target.prototype) 14 | .filter((method) => method !== 'constructor') 15 | .filter((method) => typeof target.prototype[method] === 'function'); 16 | 17 | methods.forEach((method) => { 18 | const fn = target.prototype[method]; 19 | 20 | target.prototype[method] = function(...args) { 21 | logger(`${target.prototype.constructor.name} call method ${method}`); 22 | return fn.call(this, ...args); 23 | } 24 | }) 25 | } 26 | 27 | export { 28 | logger, 29 | decorateService 30 | } -------------------------------------------------------------------------------- /livecode-rest-backend/lib/request/index.ts: -------------------------------------------------------------------------------- 1 | import * as fetchy from 'node-fetch'; 2 | import * as QS from 'querystring'; 3 | import * as URL from 'url'; 4 | import {logger} from '../report' 5 | 6 | interface IRequestParams { 7 | path: string; 8 | body?: any 9 | headers?: object; 10 | qeuries?: object; 11 | } 12 | 13 | interface IRespose { 14 | body: any; 15 | status: number; 16 | headers: object; 17 | } 18 | 19 | interface IRequest { 20 | get(arg: IRequestParams): Promise; 21 | post(arg: IRequestParams): Promise; 22 | put(arg: IRequestParams): Promise; 23 | del(arg: IRequestParams): Promise 24 | } 25 | 26 | const methods = { 27 | post: 'POST', 28 | del: 'DELETE', 29 | get: 'GET', 30 | put: 'PUT' 31 | } 32 | 33 | function createReqBody(body: any, method: string) { 34 | if (method === methods.get) { 35 | return; 36 | } 37 | 38 | if (typeof body === 'object') { 39 | return JSON.stringify(body); 40 | } else if (typeof body === 'string') { 41 | return body; 42 | } 43 | } 44 | 45 | async function _fetch(host: string, method: string, {path, body, headers, qeuries}) { 46 | qeuries = qeuries ? `?${QS.stringify(qeuries)}` : ''; 47 | body = createReqBody(body, method); 48 | headers = headers || {'Content-Type': 'application/json'}; 49 | 50 | 51 | const requestUrl = `${URL.resolve(host, path)}${qeuries}`; 52 | logger(`\t${method} Request to ${requestUrl}`, body, headers, qeuries); 53 | const response = await fetchy(requestUrl, {method, headers, body}); 54 | 55 | const responseHeaders = Array 56 | .from(response.headers.entries()) 57 | .reduce((acc, [key, value]) => {acc[key] = value.toLowerCase(); return acc}, {}) 58 | 59 | const reponseBodyMethod = responseHeaders['content-type'].includes('application/json') ? 'json' : 'text' 60 | const responseData = { 61 | body: await response[reponseBodyMethod](), 62 | status: response.status, 63 | headers: responseHeaders 64 | }; 65 | 66 | logger(`\tResponse data`, responseData.body, responseData.status); 67 | 68 | return responseData; 69 | } 70 | 71 | function buildRequest(host: string): IRequest { 72 | return { 73 | get: _fetch.bind(_fetch, host, methods.get), 74 | post: _fetch.bind(_fetch, host, methods.post), 75 | put: _fetch.bind(_fetch, host, methods.put), 76 | del: _fetch.bind(_fetch, host, methods.post) 77 | } 78 | } 79 | 80 | 81 | export { 82 | buildRequest, 83 | IRequest 84 | } -------------------------------------------------------------------------------- /livecode-rest-backend/lib/service.base.interface.ts: -------------------------------------------------------------------------------- 1 | import {buildRequest, IRequest} from './request'; 2 | 3 | class BaseInterface { 4 | protected req: IRequest; 5 | 6 | constructor(host: string) { 7 | this.req = buildRequest(host); 8 | } 9 | } 10 | 11 | export { 12 | BaseInterface 13 | } -------------------------------------------------------------------------------- /livecode-rest-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livecode-rest-backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha ./specs/**/*.spec.* --require ts-node/register --timeout 10000" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "test-fake-server": "^2.6.1" 14 | }, 15 | "dependencies": { 16 | "@types/chai": "^4.2.11", 17 | "@types/mocha": "^7.0.2", 18 | "@types/node": "^14.0.9", 19 | "chai": "^4.2.0", 20 | "chalk": "^4.0.0", 21 | "mocha": "^7.2.0", 22 | "node-fetch": "^2.6.0", 23 | "ts-node": "^8.10.2", 24 | "typescript": "^3.9.3" 25 | } 26 | } -------------------------------------------------------------------------------- /livecode-rest-backend/playground.test.ts: -------------------------------------------------------------------------------- 1 | import {buildRequest} from './lib/request' 2 | 3 | test() 4 | 5 | 6 | async function test() { 7 | const req = buildRequest('http://localhost:8888/') 8 | const {body, status, headers} = await req.get({path: '/user-data'}) 9 | // console.log(body, status, headers); 10 | } -------------------------------------------------------------------------------- /livecode-rest-backend/servers.stub.js: -------------------------------------------------------------------------------- 1 | const fakeServer = require('test-fake-server') 2 | 3 | const modelAdmin = { 4 | port: 8081, 5 | api: [ 6 | { 7 | method: "GET", 8 | path: "/admin-user-details", 9 | response_from_url: { 10 | status: 201, 11 | method: "GET", 12 | url: "http://localhost:8888/user-data", 13 | merge_with: { 14 | admin: { 15 | userType: 'customer' 16 | } 17 | } 18 | } 19 | }, 20 | { 21 | method: "GET", 22 | path: '/admin-user-list', 23 | response: { 24 | userInitial: 'admin', 25 | userFirst: 'ivan', 26 | userSecond: 'petro' 27 | } 28 | } 29 | ] 30 | } 31 | 32 | const modelUser = { 33 | port: 8888, 34 | api: [ 35 | { 36 | method: "GET", 37 | path: "/user-data", 38 | response: { 39 | username: "some username", 40 | postal_code: 3212654 41 | } 42 | }, 43 | { 44 | method: "POST", 45 | path: "/user-login", 46 | 47 | response: { 48 | authorization: 'success' 49 | }, 50 | 51 | request_body_equal: { 52 | status: 404, 53 | not_equal_response: 54 | { 55 | authorization: 'fail' 56 | }, 57 | expected_body: 58 | { 59 | username: "test_user", 60 | password: "test_pass" 61 | } 62 | }, 63 | } 64 | ] 65 | } 66 | 67 | async function startFakeServices() { 68 | await fakeServer(modelAdmin); 69 | await fakeServer(modelUser); 70 | } 71 | 72 | startFakeServices() 73 | 74 | /* 75 | 76 | fetch('http://localhost:8888/user-login', { 77 | method: 'POST', headers: {'content-type': 'application/json'}, body: JSON.stringify({ 78 | username: "test_user", 79 | password: "test_pass" 80 | }) 81 | }).then(r => r.text()).then(console.log) 82 | 83 | */ -------------------------------------------------------------------------------- /livecode-rest-backend/specs/user.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {serviceProvider} from '../framework' 3 | 4 | describe('User', function() { 5 | it('user-data', async () => { 6 | const {body, status} = await serviceProvider.user.getUserData() 7 | expect(status).to.equal(200); 8 | expect(body).to.include.keys('username', 'postal_code') 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /livecode-rest-backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "outDir": "built", 5 | "target": "ES2017" 6 | }, 7 | "exclude": [ 8 | "node_modules" 9 | ], 10 | "include": [ 11 | "lib", 12 | "framework", 13 | "specs" 14 | ] 15 | } -------------------------------------------------------------------------------- /livecode-selenium/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /livecode-selenium/lib/browser.js: -------------------------------------------------------------------------------- 1 | const {Builder} = require('selenium-webdriver'); 2 | const {setBrowserDriver} = require('./share') 3 | 4 | class Browser { 5 | constructor() { 6 | 7 | } 8 | 9 | async init() { 10 | this.driver = await new Builder() 11 | .usingServer('http://localhost:4444/wd/hub') 12 | .forBrowser('chrome') 13 | .build(); 14 | 15 | setBrowserDriver(this.driver) 16 | } 17 | 18 | async get(url) { 19 | if(this.driver) { 20 | await this.driver.get(url) 21 | } else { 22 | await this.init() 23 | await this.driver.get(url) 24 | } 25 | } 26 | 27 | async wait(...args) { 28 | if(this.driver) { 29 | await this.driver.wait(...args) 30 | } else { 31 | await this.init() 32 | await this.driver.wait(...args) 33 | } 34 | } 35 | 36 | async close() { 37 | await this.driver.quit() 38 | } 39 | } 40 | 41 | module.exports = { 42 | Browser 43 | } -------------------------------------------------------------------------------- /livecode-selenium/lib/element.js: -------------------------------------------------------------------------------- 1 | const {getBrowserDriver} = require('./share') 2 | 3 | class Element { 4 | constructor(driver, locatorBy, parent) { 5 | this._driver = driver; 6 | this.locatorBy = locatorBy; 7 | this._parent = parent; 8 | } 9 | 10 | get driver() { 11 | return this._driver; 12 | } 13 | 14 | set driver(driver) { 15 | this._driver = driver 16 | } 17 | 18 | async init() { 19 | if(this._parent && this.driver) { 20 | this.driver = this._parent.driver; 21 | this._root = this._parent._root.findElement(this.locatorBy) 22 | } else if(this._parent && !this.driver) { 23 | 24 | await this._parent.init() 25 | this.driver = this._parent.driver; 26 | this._root = this._parent._root.findElement(this.locatorBy) 27 | 28 | } else if(!this._parent) { 29 | this.driver = this.driver || getBrowserDriver() 30 | this._root = await this.driver.findElement(this.locatorBy) 31 | } 32 | } 33 | 34 | async sendKeys(...args) { 35 | if(this._root) { 36 | await this._root.sendKeys(...args) 37 | } else { 38 | await this.init() 39 | await this._root.sendKeys(...args) 40 | } 41 | } 42 | 43 | $(locatorBy) { 44 | return new Element(this.driver, locatorBy, this) 45 | } 46 | } 47 | 48 | function $(locatorBy) { 49 | return new Element(null, locatorBy) 50 | } 51 | 52 | module.exports = { 53 | $ 54 | } -------------------------------------------------------------------------------- /livecode-selenium/lib/index.js: -------------------------------------------------------------------------------- 1 | const {By, Key, until} = require('selenium-webdriver'); 2 | const {Browser } = require('./browser') 3 | const {$} = require('./element') 4 | 5 | module.exports = { 6 | Browser, 7 | $, By,Key, until 8 | } -------------------------------------------------------------------------------- /livecode-selenium/lib/share.js: -------------------------------------------------------------------------------- 1 | let browserDriver = null 2 | 3 | function setBrowserDriver(driver) { 4 | browserDriver = driver 5 | } 6 | 7 | function getBrowserDriver() { 8 | return browserDriver 9 | } 10 | 11 | module.exports = { 12 | setBrowserDriver, 13 | getBrowserDriver 14 | } -------------------------------------------------------------------------------- /livecode-selenium/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livecode-selenium", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha ./specs/**/*.spec.js --timeout 10000" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "mocha": "^8.2.1", 14 | "selenium-webdriver": "^4.0.0-alpha.8", 15 | "webdriver-manager": "^12.1.8" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /livecode-selenium/specs/index.spec.js: -------------------------------------------------------------------------------- 1 | const {Browser, $, until, By, Key} = require('../lib') 2 | 3 | describe('Example usage', function() { 4 | 5 | const searchGoogleField = $(By.css('body')).$(By.name('q')) 6 | const browser = new Browser(); 7 | 8 | afterEach(async () => { 9 | await browser.close() 10 | }) 11 | 12 | it('first spec', async function() { 13 | let str = '' 14 | for(let i = 0; i < 500; i++) { 15 | str+= 'a' 16 | } 17 | console.log(str) 18 | await browser.get('http://www.google.com/ncr'); 19 | await searchGoogleField.sendKeys(str, Key.RETURN); 20 | await browser.wait(until.titleIs('webdriver - Google Search'), 1000); 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /livecode-selenium/test.js: -------------------------------------------------------------------------------- 1 | const {By, Key, until} = require('selenium-webdriver'); 2 | const {Browser} = require('./lib/browser') 3 | const {$} = require('./lib/element'); 4 | 5 | example() 6 | async function example() { 7 | 8 | const searchGoogleField = $(By.css('body')).$(By.name('q')) 9 | 10 | const browser = new Browser(); 11 | 12 | try { 13 | await browser.get('http://www.google.com/ncr'); 14 | await searchGoogleField.sendKeys('webdriver', Key.RETURN); 15 | await browser.wait(until.titleIs('webdriver - Google Search'), 1000); 16 | } finally { 17 | await browser.close(); 18 | } 19 | }; -------------------------------------------------------------------------------- /livecode/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /livecode/framework/config/protractor.conf.ts: -------------------------------------------------------------------------------- 1 | import {Config, browser} from 'protractor' 2 | 3 | const config: Config = { 4 | framework: 'mocha', 5 | 6 | seleniumAddress: 'http://localhost:4444/wd/hub', 7 | logLevel: 'ERROR', 8 | mochaOpts: { 9 | timeout: 35000 10 | }, 11 | specs: ["./specs/**/*.spec.ts"], 12 | 13 | SELENIUM_PROMISE_MANAGER: false, 14 | 15 | onPrepare: async () => { 16 | await browser.waitForAngularEnabled(false); 17 | } 18 | } 19 | 20 | export { 21 | config 22 | } -------------------------------------------------------------------------------- /livecode/framework/index.ts: -------------------------------------------------------------------------------- 1 | import {MainPage, IMainPage} from './pages'; 2 | 3 | const provider = { 4 | pages: { 5 | main(): IMainPage { 6 | return new MainPage(); 7 | } 8 | } 9 | } 10 | 11 | export { 12 | provider 13 | } 14 | -------------------------------------------------------------------------------- /livecode/framework/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './main/page'; 2 | -------------------------------------------------------------------------------- /livecode/framework/pages/main/fragments/header.fragment.ts: -------------------------------------------------------------------------------- 1 | import {ButtonElement, BaseFragmentInterface} from '../../../../lib'; 2 | 3 | class HeaderFragment extends BaseFragmentInterface { 4 | private login: ButtonElement; 5 | private register: ButtonElement; 6 | 7 | constructor(rootEl, name = HeaderFragment.name) { 8 | super(rootEl, name); 9 | this.login = this.initChild(ButtonElement, '.user_buttons button:nth-child(1)', 'Login button'); 10 | this.register = this.initChild(ButtonElement, '.user_buttons button:nth-child(2)', 'Register button'); 11 | } 12 | } 13 | 14 | export { 15 | HeaderFragment 16 | } -------------------------------------------------------------------------------- /livecode/framework/pages/main/fragments/index.ts: -------------------------------------------------------------------------------- 1 | export * from './header.fragment'; 2 | export * from './login.fragment'; 3 | export * from './register.fragment'; 4 | -------------------------------------------------------------------------------- /livecode/framework/pages/main/fragments/login.fragment.ts: -------------------------------------------------------------------------------- 1 | import {ButtonElement, BaseFragmentInterface} from '../../../../lib'; 2 | 3 | class LoginFragment extends BaseFragmentInterface { 4 | 5 | constructor(rootEl, name = LoginFragment.name) { 6 | super(rootEl, name); 7 | } 8 | } 9 | 10 | export { 11 | LoginFragment 12 | } -------------------------------------------------------------------------------- /livecode/framework/pages/main/fragments/register.fragment.ts: -------------------------------------------------------------------------------- 1 | import {ButtonElement, BaseFragmentInterface} from '../../../../lib'; 2 | 3 | class RegisterFragment extends BaseFragmentInterface { 4 | 5 | constructor(rootEl, name = RegisterFragment.name) { 6 | super(rootEl, name); 7 | } 8 | } 9 | 10 | export { 11 | RegisterFragment 12 | } -------------------------------------------------------------------------------- /livecode/framework/pages/main/page.ts: -------------------------------------------------------------------------------- 1 | import {BasePageInterface} from '../../../lib'; 2 | import {HeaderFragment} from './fragments'; 3 | 4 | interface IMainPageExpectedArg { 5 | header?: {login?: null, register?: null} 6 | } 7 | 8 | interface IMainPage { 9 | click(clickObj: IMainPageExpectedArg): void; 10 | } 11 | 12 | class MainPage extends BasePageInterface { 13 | private header: HeaderFragment; 14 | 15 | constructor() { 16 | super('#main_page', 'Main page'); 17 | this.header = this.initChild(HeaderFragment, '.main_header', 'Main page header'); 18 | } 19 | } 20 | 21 | export { 22 | MainPage, 23 | IMainPage 24 | } -------------------------------------------------------------------------------- /livecode/lib/base.fragment.interface.ts: -------------------------------------------------------------------------------- 1 | import {ElementFinder, $} from 'protractor'; 2 | import {waiter} from './element_utils' 3 | import {step} from './report' 4 | 5 | class BaseFragmentInterface { 6 | private root: ElementFinder; 7 | private name: string; 8 | 9 | constructor(elementRoor: ElementFinder, name) { 10 | this.root = elementRoor 11 | this.name = name ? name : BaseFragmentInterface.name; 12 | } 13 | 14 | @step((name) => `${name} click`) 15 | async click(clickObj?: object) { 16 | if (!clickObj) { 17 | throw new Error(`${this.name} click argument should be an object`); 18 | } 19 | await this.waitVisible(); 20 | for (const key of Object.keys(clickObj)) { 21 | if (!this[key]) { 22 | throw new Error(`${this.name} does not have ${key}`); 23 | } 24 | await this[key].click(clickObj[key]); 25 | } 26 | } 27 | 28 | @step((name) => `${name} get`) 29 | async get(getObj: object) { 30 | if (!getObj) { 31 | throw new Error(`${this.name} get argument should be an object`); 32 | } 33 | await this.waitVisible(); 34 | const tempGet = {...getObj}; 35 | for (const key of Object.keys(tempGet)) { 36 | if (!this[key]) { 37 | throw new Error(`${this.name} does not have ${key}`); 38 | } 39 | tempGet[key] = await this[key].get(tempGet[key]); 40 | } 41 | 42 | return tempGet; 43 | } 44 | 45 | @step((name) => `${name} send keys`) 46 | async sendKeys(sendObj: object) { 47 | if (!sendObj) { 48 | throw new Error(`${this.name} send keys argument should be an object`); 49 | } 50 | 51 | await this.waitVisible(); 52 | 53 | for (const key of Object.keys(sendObj)) { 54 | if (!this[key]) { 55 | throw new Error(`${this.name} does not have ${key}`); 56 | } 57 | await this[key].sendKeys(sendObj[key]); 58 | } 59 | } 60 | 61 | @step((name) => `${name} is displayed`) 62 | async isDisplay(dispayObj) { 63 | if (dispayObj === null) { 64 | return (await this.root.isPresent()) && this.root.isDisplayed() 65 | } 66 | const tempDisplayed = {...dispayObj}; 67 | for (const key of Object.keys(tempDisplayed)) { 68 | if (!this[key]) { 69 | throw new Error(`${this.name} does not have ${key}`); 70 | } 71 | tempDisplayed[key] = await this[key].isDisplay(tempDisplayed[key]); 72 | } 73 | return tempDisplayed; 74 | } 75 | 76 | async waitVisible() { 77 | await waiter.waitForVisible(this); 78 | } 79 | 80 | 81 | initChild(childClass, elementSelector, ...args) { 82 | return new childClass(this.root.$(elementSelector), ...args); 83 | } 84 | } 85 | 86 | export { 87 | BaseFragmentInterface 88 | } -------------------------------------------------------------------------------- /livecode/lib/base.page.interface.ts: -------------------------------------------------------------------------------- 1 | import {ElementFinder, $} from 'protractor'; 2 | import {waiter} from './element_utils' 3 | import {step} from './report' 4 | 5 | class BasePageInterface { 6 | private root: ElementFinder; 7 | private name: string; 8 | 9 | constructor(element: string, name) { 10 | this.root = $(element); 11 | this.name = name ? name : BasePageInterface.name; 12 | } 13 | 14 | @step((name) => `${name} click`) 15 | async click(clickObj?: object) { 16 | if (!clickObj) { 17 | throw new Error(`${this.name} click argument should be an object`); 18 | } 19 | await this.waitVisible(); 20 | for (const key of Object.keys(clickObj)) { 21 | if (!this[key]) { 22 | throw new Error(`${this.name} does not have ${key}`); 23 | } 24 | await this[key].click(clickObj[key]); 25 | } 26 | } 27 | 28 | @step((name) => `${name} get`) 29 | async get(getObj: object) { 30 | if (!getObj) { 31 | throw new Error(`${this.name} get argument should be an object`); 32 | } 33 | await this.waitVisible(); 34 | const tempGet = {...getObj}; 35 | for (const key of Object.keys(tempGet)) { 36 | if (!this[key]) { 37 | throw new Error(`${this.name} does not have ${key}`); 38 | } 39 | tempGet[key] = await this[key].get(tempGet[key]); 40 | } 41 | 42 | return tempGet; 43 | } 44 | 45 | @step((name) => `${name} send keys`) 46 | async sendKeys(sendObj: object) { 47 | if (!sendObj) { 48 | throw new Error(`${this.name} send keys argument should be an object`); 49 | } 50 | 51 | await this.waitVisible(); 52 | 53 | for (const key of Object.keys(sendObj)) { 54 | if (!this[key]) { 55 | throw new Error(`${this.name} does not have ${key}`); 56 | } 57 | await this[key].sendKeys(sendObj[key]); 58 | } 59 | } 60 | 61 | @step((name) => `${name} is displayed`) 62 | async isDisplay(dispayObj) { 63 | if (dispayObj === null) { 64 | return (await this.root.isPresent()) && this.root.isDisplayed() 65 | } 66 | const tempDisplayed = {...dispayObj}; 67 | for (const key of Object.keys(tempDisplayed)) { 68 | if (!this[key]) { 69 | throw new Error(`${this.name} does not have ${key}`); 70 | } 71 | tempDisplayed[key] = await this[key].isDisplay(tempDisplayed[key]); 72 | } 73 | return tempDisplayed; 74 | } 75 | 76 | async waitVisible() { 77 | await waiter.waitForVisible(this); 78 | } 79 | 80 | 81 | initChild(childClass, elementSelector, ...args) { 82 | return new childClass(this.root.$(elementSelector), ...args); 83 | } 84 | } 85 | 86 | export { 87 | BasePageInterface 88 | } -------------------------------------------------------------------------------- /livecode/lib/element_utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './waiter'; 2 | -------------------------------------------------------------------------------- /livecode/lib/element_utils/waiter.ts: -------------------------------------------------------------------------------- 1 | import {ExpectedConditions as EC, browser} from 'protractor'; 2 | 3 | const waiter = { 4 | waitForVisible: async (ctx) => { 5 | const {root, name} = ctx; 6 | await browser.wait(EC.visibilityOf(root), 5000, `${name} should be visible`); 7 | } 8 | } 9 | 10 | export { 11 | waiter 12 | } 13 | -------------------------------------------------------------------------------- /livecode/lib/elements/base.element.ts: -------------------------------------------------------------------------------- 1 | import {ElementFinder} from 'protractor' 2 | import {waiter} from '../element_utils' 3 | 4 | class BaseElement { 5 | private root: ElementFinder; 6 | private name: string; 7 | 8 | constructor(element: ElementFinder, name?: string) { 9 | this.root = element; 10 | this.name = name ? name : BaseElement.name; 11 | } 12 | 13 | get elName() { 14 | return this.name; 15 | } 16 | 17 | get elRoot() { 18 | return this.root; 19 | } 20 | 21 | async click() { 22 | await this.waitVisible(); 23 | await this.root.click(); 24 | } 25 | 26 | async get() { 27 | await this.waitVisible(); 28 | return this.root.getText(); 29 | } 30 | 31 | async sendKeys(keys: string) { 32 | await this.waitVisible(); 33 | await this.root.sendKeys(keys) 34 | } 35 | 36 | async isDisplay() { 37 | return (await this.root.isPresent()) && await this.root.isDisplayed(); 38 | } 39 | 40 | async waitVisible() { 41 | await waiter.waitForVisible(this); 42 | } 43 | } 44 | 45 | export { 46 | BaseElement 47 | } -------------------------------------------------------------------------------- /livecode/lib/elements/button.ts: -------------------------------------------------------------------------------- 1 | import {BaseElement} from './base.element'; 2 | 3 | class ButtonElement extends BaseElement { 4 | constructor(element, id) { 5 | super(element, id); 6 | } 7 | 8 | async sendKeys() { 9 | throw new Error(`${this.elName} is button, button does not have sendKeys`); 10 | } 11 | } 12 | 13 | export { 14 | ButtonElement 15 | } -------------------------------------------------------------------------------- /livecode/lib/elements/index.ts: -------------------------------------------------------------------------------- 1 | export * from './button'; 2 | export * from './input'; 3 | export * from './text'; 4 | -------------------------------------------------------------------------------- /livecode/lib/elements/input.ts: -------------------------------------------------------------------------------- 1 | import {BaseElement} from './base.element'; 2 | 3 | class InputElement extends BaseElement { 4 | constructor(element, id) { 5 | super(element, id); 6 | } 7 | 8 | async get() { 9 | return this.elRoot.getAttribute('value'); 10 | } 11 | } 12 | 13 | export { 14 | InputElement 15 | } 16 | -------------------------------------------------------------------------------- /livecode/lib/elements/text.ts: -------------------------------------------------------------------------------- 1 | import {BaseElement} from './base.element'; 2 | 3 | class TextElement extends BaseElement { 4 | constructor(element, id) { 5 | super(element, id); 6 | } 7 | 8 | async sendKeys() { 9 | throw new Error(`${this.elName} is text, text does not have sendKeys`); 10 | } 11 | } 12 | 13 | export { 14 | TextElement 15 | } 16 | -------------------------------------------------------------------------------- /livecode/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base.page.interface'; 2 | export * from './base.fragment.interface'; 3 | export * from './elements'; 4 | -------------------------------------------------------------------------------- /livecode/lib/report/console.ts: -------------------------------------------------------------------------------- 1 | import * as chalk from 'chalk'; 2 | 3 | function stepLog(message) { 4 | console.log(message); 5 | } 6 | 7 | export { 8 | stepLog 9 | } 10 | -------------------------------------------------------------------------------- /livecode/lib/report/index.ts: -------------------------------------------------------------------------------- 1 | import {stepLog} from './console'; 2 | 3 | function step(stepName: string | Function) { 4 | return function(_target, _name, descriptor) { 5 | 6 | const originalValue = descriptor.value; 7 | 8 | descriptor.value = function(...args) { 9 | 10 | stepName = '\n' + ((typeof stepName === 'string' ? stepName : stepName(this.name)) as string); 11 | if (this.constructor.name.includes('Fragment')) { 12 | stepName = `\t ${stepName} arguments ${JSON.stringify(args)}` 13 | } 14 | stepLog(stepName); 15 | return originalValue.call(this, ...args); 16 | } 17 | 18 | return descriptor; 19 | } 20 | } 21 | 22 | export { 23 | step 24 | } -------------------------------------------------------------------------------- /livecode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livecode", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@types/mocha": "^7.0.2", 14 | "@types/node": "^14.0.9", 15 | "chai": "^4.2.0", 16 | "chalk": "^4.0.0", 17 | "mocha": "^7.2.0", 18 | "protractor": "^7.0.0", 19 | "ts-node": "^8.10.2", 20 | "typescript": "^3.9.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /livecode/protractor.conf.js: -------------------------------------------------------------------------------- 1 | require('ts-node').register({ 2 | project: './tsconfig.json' 3 | }); 4 | 5 | module.exports = require('./framework/config/protractor.conf.ts') -------------------------------------------------------------------------------- /livecode/specs/noop.spec.ts: -------------------------------------------------------------------------------- 1 | import {browser} from 'protractor'; 2 | import {provider} from '../framework' 3 | 4 | describe('Noop', function() { 5 | it('it noop', async function() { 6 | const page = provider.pages.main(); 7 | await browser.get('http://localhost:3000') 8 | 9 | 10 | await page.click({header: {register: null}}); 11 | await page.click({header: {login: null}}); 12 | 13 | 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /livecode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "outDir": "built", 5 | "target": "es2017", 6 | "experimentalDecorators": true, 7 | "allowJs": true, 8 | "sourceMap": true 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ], 13 | "include": ["lib", "framework", "specs"] 14 | } -------------------------------------------------------------------------------- /livecoding-codeceptjs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /livecoding-codeceptjs/codecept.conf.js: -------------------------------------------------------------------------------- 1 | const {setHeadlessWhen} = require('@codeceptjs/configure'); 2 | 3 | // turn on headless mode when running with HEADLESS=true environment variable 4 | // HEADLESS=true npx codecept run 5 | setHeadlessWhen(process.env.HEADLESS); 6 | 7 | exports.config = { 8 | tests: './specs/**/table.spec.js', 9 | output: './output', 10 | helpers: { 11 | Puppeteer: { 12 | url: 'http://localhost', 13 | show: true, 14 | windowSize: '1200x900' 15 | } 16 | }, 17 | include: { 18 | I: './steps_file.js', 19 | 20 | mainLoginFragment: './pages/main.login.fragment.js', 21 | mainRegisterFragment: './pages/main.register.fragment.js', 22 | 23 | tableFragment: './pages/table.fragment.js' 24 | }, 25 | bootstrap: null, 26 | mocha: {}, 27 | name: 'livecoding-codeceptjs', 28 | plugins: { 29 | retryFailedStep: { 30 | enabled: true 31 | }, 32 | screenshotOnFail: { 33 | // TODO should be adaptive 34 | enabled: true 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /livecoding-codeceptjs/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true 4 | } 5 | } -------------------------------------------------------------------------------- /livecoding-codeceptjs/output/Open_machine_details.failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/potapovDim/livecoding-s/9a240fe35797c30342277cb786279dbc0a3e887f/livecoding-codeceptjs/output/Open_machine_details.failed.png -------------------------------------------------------------------------------- /livecoding-codeceptjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livecoding-codeceptjs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "codeceptjs": "^2.6.6", 14 | "puppeteer": "^4.0.1" 15 | }, 16 | "dependencies": { 17 | "chai": "^4.2.0", 18 | "faker": "^4.1.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /livecoding-codeceptjs/pages/main.login.fragment.js: -------------------------------------------------------------------------------- 1 | const {I} = inject(); 2 | 3 | module.exports = { 4 | 5 | // setting locators 6 | fields: { 7 | username: '.login_form .modal .form-group:nth-child(1) input', 8 | password: '.login_form .modal .form-group:nth-child(2) input', 9 | }, 10 | alert: '.alert.alert-danger', 11 | toLogin: '.user_buttons button:nth-child(1)', 12 | submitButton: '.login_form .modal button', 13 | 14 | seeAlertMessage() { 15 | I.see('Неможливо авторизуватися, перевірте ваші данні', this.alert); 16 | }, 17 | 18 | // introducing methods 19 | login(userData) { 20 | I.click(this.toLogin); 21 | for(const fieldName of Object.keys(userData)) { 22 | I.fillField(this.fields[fieldName], userData[fieldName]); 23 | } 24 | I.click(this.submitButton); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /livecoding-codeceptjs/pages/main.register.fragment.js: -------------------------------------------------------------------------------- 1 | const {I} = inject(); 2 | 3 | module.exports = { 4 | 5 | // setting locators 6 | fields: { 7 | username: '.registration_form .modal .form-group:nth-child(1) input', 8 | name: '.registration_form .modal .form-group:nth-child(2) input', 9 | email: '.registration_form .modal .form-group:nth-child(3) input', 10 | password: '.registration_form .modal .form-group:nth-child(4) input', 11 | }, 12 | // Користувач не може бути зареєстрований 13 | alert: '.alert.alert-danger', 14 | toRegister: '.user_buttons button:nth-child(2)', 15 | submitButton: '.registration_form .modal button', 16 | 17 | // introducing methods 18 | register(userData) { 19 | I.click(this.toRegister); 20 | for(const fieldName of Object.keys(userData)) { 21 | I.fillField(this.fields[fieldName], userData[fieldName]); 22 | } 23 | I.click(this.submitButton); 24 | }, 25 | seeAlertMessage() { 26 | I.see('Користувач не може бути зареєстрований', this.alert) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /livecoding-codeceptjs/pages/table.fragment.js: -------------------------------------------------------------------------------- 1 | const {expect} = require('chai') 2 | 3 | const {I} = inject(); 4 | 5 | module.exports = { 6 | // setting locators 7 | header: '.machies_list_section thead', 8 | tbody: '.machies_list_section tbody', 9 | machineModal: '.modal', 10 | 11 | checkModalMachineDetails(manufactorer) { 12 | within(this.machineModal, async () => { 13 | const modalTitle = await I.grabTextFrom('h1'); 14 | expect(modalTitle).to.includes(manufactorer) 15 | }) 16 | }, 17 | openMachineDetails(manufactorer) { 18 | const headerMap = {}; 19 | 20 | // TODO this is for future 21 | within(this.header, async () => { 22 | const rowsCount = await I.grabNumberOfVisibleElements('td') 23 | for(let i = 0; i < rowsCount; i++) { 24 | headerMap[await I.grabTextFrom(`//td[${i + 1}]`)] = i; 25 | } 26 | }); 27 | 28 | within(this.tbody, async () => { 29 | const rowsCount = await I.grabNumberOfVisibleElements('tr'); 30 | for(let i = 0; i < rowsCount; i++) { 31 | const currentRowText = await I.grabTextFrom(`//tr[${i + 1}]/td[1]`) 32 | if(currentRowText.includes(manufactorer)) { 33 | I.click(`//tr[${i + 1}]/td[1]`) 34 | return; 35 | } 36 | } 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /livecoding-codeceptjs/specs/main/main.login.spec.js: -------------------------------------------------------------------------------- 1 | const {I} = inject(); 2 | 3 | Feature('Login'); 4 | 5 | Scenario('User success login ', () => { 6 | const userData = {username: 'admin', password: 'admin'}; 7 | 8 | I.amOnPage('http://localhost:4000/'); 9 | I.login(userData); 10 | I.see(`Таблиці, Привіт ${userData.username}`); 11 | }); 12 | 13 | Scenario('User failed login', () => { 14 | const userData = {username: 'admin'}; 15 | 16 | I.amOnPage('http://localhost:4000/'); 17 | I.login(userData); 18 | I.checkLoginErrorMessage(); 19 | }) -------------------------------------------------------------------------------- /livecoding-codeceptjs/specs/main/main.register.spec.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker') 2 | 3 | const {I} = inject(); 4 | 5 | Feature('Register'); 6 | 7 | Scenario('User failed registration', () => { 8 | const userData = {username: 'admin'}; 9 | 10 | I.amOnPage('http://localhost:4000/'); 11 | I.register(userData); 12 | I.checkRegisterErrorMessage(); 13 | }) 14 | 15 | 16 | Scenario('User success registration', () => { 17 | const userData = { 18 | username: faker.internet.userName(), 19 | name: faker.name.firstName(), 20 | email: faker.internet.email(), 21 | password: '123123123' 22 | }; 23 | 24 | I.amOnPage('http://localhost:4000/'); 25 | I.register(userData); 26 | I.see(`Таблиці, Привіт ${userData.username}`); 27 | }) -------------------------------------------------------------------------------- /livecoding-codeceptjs/specs/main/table.spec.js: -------------------------------------------------------------------------------- 1 | const {I} = inject(); 2 | 3 | Feature('Table'); 4 | 5 | Scenario('Open machine details', () => { 6 | const userData = {username: 'admin', password: 'admin'}; 7 | const manufacturer = 'ITALMIX DUPLEX'; 8 | 9 | I.amOnPage('http://localhost:4000/'); 10 | I.login(userData); 11 | I.see(`Таблиці, Привіт ${userData.username}`); 12 | I.openMachineDetails(manufacturer); 13 | I.checkModalMachineDetails(manufacturer); 14 | }); 15 | -------------------------------------------------------------------------------- /livecoding-codeceptjs/steps.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | type steps_file = typeof import('./steps_file.js'); 3 | 4 | declare namespace CodeceptJS { 5 | interface SupportObject { I: CodeceptJS.I } 6 | interface CallbackOrder { [0]: CodeceptJS.I } 7 | interface Methods extends CodeceptJS.Puppeteer {} 8 | interface I extends ReturnType {} 9 | namespace Translation { 10 | interface Actions {} 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /livecoding-codeceptjs/steps_file.js: -------------------------------------------------------------------------------- 1 | const {mainLoginFragment, mainRegisterFragment, tableFragment} = inject(); 2 | 3 | module.exports = function() { 4 | return actor({ 5 | 6 | login: function(userData) { 7 | mainLoginFragment.login(userData) 8 | }, 9 | 10 | register: function(registerData) { 11 | mainRegisterFragment.register(registerData) 12 | }, 13 | 14 | checkLoginErrorMessage() { 15 | mainLoginFragment.seeAlertMessage() 16 | }, 17 | 18 | checkRegisterErrorMessage() { 19 | mainRegisterFragment.seeAlertMessage() 20 | }, 21 | 22 | openMachineDetails(manufacturer) { 23 | tableFragment.openMachineDetails(manufacturer) 24 | }, 25 | 26 | checkModalMachineDetails(manufacturer) { 27 | tableFragment.checkModalMachineDetails(manufacturer) 28 | } 29 | }); 30 | } 31 | 32 | -------------------------------------------------------------------------------- /livecoding-playwright-part-2/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules -------------------------------------------------------------------------------- /livecoding-playwright-part-2/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/potapovDim/livecoding-s/9a240fe35797c30342277cb786279dbc0a3e887f/livecoding-playwright-part-2/example.png -------------------------------------------------------------------------------- /livecoding-playwright-part-2/framework/index.js: -------------------------------------------------------------------------------- 1 | const {MainPage, TablesPage} = require('./pages') 2 | const {makeSingleTon} = require('../lib') 3 | 4 | const pageProvider = (page) => { 5 | return { 6 | main: () => makeSingleTon(MainPage, page), 7 | tables: () => makeSingleTon(TablesPage, page) 8 | } 9 | } 10 | module.exports = { 11 | pageProvider 12 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-2/framework/pages/index.js: -------------------------------------------------------------------------------- 1 | const main = require('./main'); 2 | const tables = require('./tables'); 3 | 4 | module.exports = { 5 | ...main, 6 | ...tables 7 | }; 8 | -------------------------------------------------------------------------------- /livecoding-playwright-part-2/framework/pages/main/fragments/header.js: -------------------------------------------------------------------------------- 1 | const {decoratePage, BaseFragment, $element} = require('../../../../lib'); 2 | 3 | class HeaderFragment extends BaseFragment { 4 | constructor(page, rootFragmentSelector = '.main_header') { 5 | super(page, rootFragmentSelector) 6 | this.login = $element(this.page, '.user_buttons button:nth-child(1)') 7 | this.register = $element(this.page, '.user_buttons button:nth-child(2)') 8 | } 9 | 10 | async toLogin() { 11 | await this.login.click() 12 | } 13 | 14 | async toRegister() { 15 | await this.register.click() 16 | } 17 | } 18 | decoratePage(HeaderFragment) 19 | 20 | 21 | module.exports = { 22 | HeaderFragment 23 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-2/framework/pages/main/fragments/login.js: -------------------------------------------------------------------------------- 1 | const {decoratePage, BaseFragment, $element} = require('../../../../lib'); 2 | 3 | class LoginFragment extends BaseFragment { 4 | constructor(page, rootFragmentSelector = '.login_form') { 5 | super(page, rootFragmentSelector) 6 | this.username = $element(this.page, '.login_form .modal .form-group:nth-child(1) input') 7 | this.password = $element(this.page, '.login_form .modal .form-group:nth-child(2) input') 8 | this.submin = $element(this.page, '.login_form .modal button') 9 | } 10 | 11 | async login(username, password) { 12 | // TODO should be refactored 13 | await this.username.type(username) 14 | await this.password.type(password) 15 | await this.submin.click() 16 | } 17 | } 18 | decoratePage(LoginFragment) 19 | 20 | module.exports = { 21 | LoginFragment 22 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-2/framework/pages/main/fragments/register.js: -------------------------------------------------------------------------------- 1 | const {decoratePage, BaseFragment, $element} = require('../../../../lib'); 2 | 3 | class RegisterFragment extends BaseFragment { 4 | constructor(page, rootFragmentSelector = '.registration_form') { 5 | super(page, rootFragmentSelector) 6 | this.name = $element(this.page, '.registration_form .modal .form-group:nth-child(1) input'); 7 | this.username = $element(this.page, '.registration_form .modal .form-group:nth-child(2) input'); 8 | this.email = $element(this.page, '.registration_form .modal .form-group:nth-child(3) input'); 9 | this.password = $element(this.page, '.registration_form .modal .form-group:nth-child(4) input'); 10 | this.submit = $element(this.page, '.registration_form .modal button') 11 | } 12 | 13 | async register(name, username, email, password) { 14 | // TODO should be refactored 15 | await this.name.type(name); 16 | await this.username.type(username); 17 | await this.email.type(email); 18 | await this.password.type(password) 19 | await this.submit.click() 20 | } 21 | } 22 | decoratePage(RegisterFragment) 23 | 24 | module.exports = { 25 | RegisterFragment 26 | } 27 | -------------------------------------------------------------------------------- /livecoding-playwright-part-2/framework/pages/main/index.js: -------------------------------------------------------------------------------- 1 | const {LoginFragment} = require('./fragments/login'); 2 | const {RegisterFragment} = require('./fragments/register'); 3 | const {HeaderFragment} = require('./fragments/header'); 4 | const {decoratePage, BasePage} = require('../../../lib'); 5 | 6 | class MainPage extends BasePage { 7 | 8 | constructor(page, pageRootSelector = '#main_page') { 9 | super(page, pageRootSelector) 10 | this.loginFragment = new LoginFragment(page); 11 | this.registerFragment = new RegisterFragment(page); 12 | this.headerFragment = new HeaderFragment(page); 13 | } 14 | 15 | async login(username, password) { 16 | // TODO should be refactored 17 | await this.headerFragment.toLogin(); 18 | await this.loginFragment.login(username, password) 19 | } 20 | 21 | async register(name, username, email, password) { 22 | // TODO should be refactored 23 | await this.headerFragment.toRegister(); 24 | await this.registerFragment.register(name, username, email, password) 25 | } 26 | } 27 | decoratePage(MainPage); 28 | 29 | module.exports = { 30 | MainPage 31 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-2/framework/pages/tables/index.js: -------------------------------------------------------------------------------- 1 | const {decoratePage, BasePage, $element} = require('../../../lib'); 2 | 3 | class TablesPage extends BasePage { 4 | constructor(page, pageRootSelector = '#table_page') { 5 | super(page, pageRootSelector) 6 | this.header = $element(this.page, '.header h3') 7 | } 8 | 9 | async getPageHeaderTitleContent() { 10 | return this.header.textContent(); 11 | } 12 | } 13 | decoratePage(TablesPage) 14 | 15 | module.exports = { 16 | TablesPage 17 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-2/lib/base.fragment.implementation.js: -------------------------------------------------------------------------------- 1 | class BaseFragment { 2 | constructor(page, pageRootSelector) { 3 | this.page = page 4 | this.rootSelector = pageRootSelector; 5 | } 6 | 7 | _replacePage(page) { 8 | this.page = page 9 | 10 | const excludeProps = ['page', 'rootSelector'] 11 | 12 | this.page = page 13 | 14 | const exptectedProps = Object 15 | .getOwnPropertyNames(this) 16 | .filter((p) => !excludeProps.includes(p)) 17 | 18 | exptectedProps.forEach((p) => { 19 | this[p]._replacePage.call(this[p], page) 20 | }) 21 | } 22 | } 23 | 24 | module.exports = { 25 | BaseFragment 26 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-2/lib/base.page.implementation.js: -------------------------------------------------------------------------------- 1 | class BasePage { 2 | constructor(page, pageRootSelector) { 3 | this.page = page 4 | this.rootSelector = pageRootSelector; 5 | } 6 | 7 | _replacePage(page) { 8 | const excludeProps = ['page', 'rootSelector'] 9 | 10 | this.page = page 11 | 12 | const exptectedProps = Object 13 | .getOwnPropertyNames(this) 14 | .filter((p) => !excludeProps.includes(p)) 15 | 16 | exptectedProps.forEach((p) => { 17 | this[p]._replacePage.call(this[p], page) 18 | }) 19 | } 20 | } 21 | 22 | module.exports = { 23 | BasePage 24 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-2/lib/elements/base.element.js: -------------------------------------------------------------------------------- 1 | const {waits} = require('../helpers') 2 | const chalk = require('chalk'); 3 | 4 | 5 | class BaseElement { 6 | constructor(page, selector, elementName) { 7 | this.page = page; 8 | this.selector = selector 9 | this.currentElement = null 10 | this.name = elementName 11 | } 12 | 13 | 14 | _replacePage(page) { 15 | this.page = page 16 | this.currentElement = null 17 | } 18 | 19 | async initThisElement() { 20 | // TODO not use for isPresent/isDisplayed 21 | await waits(this.page).waitVisibility(this.selector); 22 | 23 | if(this.currentElement) { 24 | return this.currentElement; 25 | } 26 | 27 | const el = await this.page.$(this.selector); 28 | this.currentElement = el; 29 | return this.currentElement 30 | } 31 | } 32 | 33 | function $element(page, selector) { 34 | const baseEl = new BaseElement(page, selector) 35 | return new Proxy(baseEl, { 36 | get(_t, value) { 37 | 38 | if(value === '_replacePage') { 39 | return (page) => baseEl._replacePage(page) 40 | } 41 | 42 | return (...args) => baseEl.initThisElement().then((curEl) => { 43 | if(!baseEl.name) { 44 | baseEl.name = `BaseElement` 45 | } 46 | let message = `\t\t\t ${baseEl.name} execute ${value} ` 47 | if(args.length) { 48 | message = `${message} with arguments ${JSON.stringify(args)}` 49 | } 50 | console.log(chalk.green(message)) 51 | return curEl[value].call(curEl, ...args) 52 | }) 53 | } 54 | }) 55 | } 56 | 57 | module.exports = { 58 | $element 59 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-2/lib/elements/index.js: -------------------------------------------------------------------------------- 1 | const baseElement = require('./base.element'); 2 | 3 | module.exports = { 4 | ...baseElement 5 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-2/lib/helpers/index.js: -------------------------------------------------------------------------------- 1 | const waits = require('./waits'); 2 | const makeSingleton = require('./make.singleton'); 3 | 4 | 5 | module.exports = { 6 | ...waits, 7 | ...makeSingleton 8 | } 9 | -------------------------------------------------------------------------------- /livecoding-playwright-part-2/lib/helpers/make.singleton.js: -------------------------------------------------------------------------------- 1 | function makeSingleTon(PagePO, ctxPage) { 2 | 3 | if(PagePO.__instance) { 4 | // TODO can fail here 5 | PagePO.__instance._replacePage.call(PagePO.__instance, ctxPage) 6 | 7 | return PagePO.__instance; 8 | } 9 | 10 | const page = new PagePO(ctxPage); 11 | 12 | PagePO.__instance = page; 13 | return PagePO.__instance; 14 | } 15 | 16 | module.exports = { 17 | makeSingleTon 18 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-2/lib/helpers/waits.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 4 | * @param {playwright page} page 5 | * @returns {object<{waitVisibility: () => Promise}>} 6 | */ 7 | function waits(page) { 8 | return { 9 | waitVisibility: (selector) => page.waitForSelector(selector, {state: 'attached', timeout: 1000}) 10 | } 11 | } 12 | 13 | module.exports = { 14 | waits 15 | } 16 | -------------------------------------------------------------------------------- /livecoding-playwright-part-2/lib/index.js: -------------------------------------------------------------------------------- 1 | const helpers = require('./helpers') 2 | const pageConditions = require('./page.conditions') 3 | const basePage = require('./base.page.implementation') 4 | const baseFragment = require('./base.fragment.implementation') 5 | const elements = require('./elements') 6 | 7 | module.exports = { 8 | ...helpers, 9 | ...pageConditions, 10 | ...basePage, 11 | ...baseFragment, 12 | ...elements 13 | } 14 | -------------------------------------------------------------------------------- /livecoding-playwright-part-2/lib/page.conditions.js: -------------------------------------------------------------------------------- 1 | const {waits} = require('./helpers') 2 | const chalk = require('chalk'); 3 | 4 | /** 5 | * 6 | * @param {function Constructor context} page 7 | */ 8 | function decoratePage(page) { 9 | const {name} = page; 10 | const requiredToDecorate = Object.getOwnPropertyNames(page.prototype) 11 | .filter(prop => { 12 | if(prop === 'constructor') { 13 | return false 14 | } 15 | const descriptor = Object.getOwnPropertyDescriptor(page.prototype, prop) 16 | return !!descriptor.value; 17 | }) 18 | 19 | requiredToDecorate.forEach(prop => { 20 | const originalProp = page.prototype[prop] 21 | page.prototype[prop] = async function(...args) { 22 | /** 23 | * @example 24 | * every page should have rootSelector 25 | * this selector will be used for wait visibility of current page 26 | */ 27 | let message = `${name} execute ${prop}`; 28 | if(name.includes('Fragment')) { 29 | message = `\t${message}`; 30 | } 31 | console.log(chalk.green(message)) 32 | await waits(this.page).waitVisibility(this.rootSelector) 33 | return originalProp.call(this, ...args); 34 | } 35 | }) 36 | } 37 | 38 | module.exports = { 39 | decoratePage 40 | } 41 | 42 | -------------------------------------------------------------------------------- /livecoding-playwright-part-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livecoding-playwrigth", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha ./specs/**/*.spec.js --timeout 10000 -R spec" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "chai": "^4.2.0", 14 | "chalk": "^4.1.0", 15 | "mocha": "^8.0.1", 16 | "playwright": "^1.1.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /livecoding-playwright-part-2/specs/noop.spec.js: -------------------------------------------------------------------------------- 1 | const {expect} = require('chai'); 2 | const {chromium} = require('playwright'); 3 | const {pageProvider} = require('../framework'); 4 | 5 | describe('Noop spec', function() { 6 | let browser = null; 7 | let page = null; 8 | 9 | beforeEach(async () => { 10 | browser = await chromium.launch({headless: false}); 11 | const context = await browser.newContext(); 12 | page = await context.newPage(); 13 | await page.goto('http://localhost:4000'); 14 | }) 15 | 16 | afterEach(async () => { 17 | await browser.close(); 18 | }) 19 | 20 | it('login', async function() { 21 | const mainPage = pageProvider(page).main(); 22 | const tablesPage = pageProvider(page).tables(); 23 | await mainPage.login('admin', 'admin'); 24 | expect(await tablesPage.getPageHeaderTitleContent()).to.includes('admin'); 25 | }) 26 | 27 | it('register', async function() { 28 | const mainPage = pageProvider(page).main(); 29 | const tablesPage = pageProvider(page).tables(); 30 | await mainPage.register('admin1', 'admin1', 'admin1@admin1.com', 'admin1'); 31 | expect(await tablesPage.getPageHeaderTitleContent()).to.includes('admin1') 32 | }) 33 | }) -------------------------------------------------------------------------------- /livecoding-playwright-part-3/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/0b10885f-4e4c-4e21-abe8-b8ac751077bd-attachment.json: -------------------------------------------------------------------------------- 1 | "Таблиці, Привіт admin" -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/4192f607-8a20-4407-886a-646ce5182edc-attachment.json: -------------------------------------------------------------------------------- 1 | "Таблиці, Привіт admin" -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/4d8703f3-ff5f-49c6-a123-9209cc70e299-attachment.json: -------------------------------------------------------------------------------- 1 | "Таблиці, Привіт admin1" -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/566ebb7c-6e27-440b-88e1-c1f852e97d94-attachment.json: -------------------------------------------------------------------------------- 1 | "Таблиці, Привіт admin" -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/57e46dca-d59d-4f6d-88e3-98181c6ff220-result.json: -------------------------------------------------------------------------------- 1 | {"status":"passed","statusDetails":{},"stage":"finished","steps":[{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[],"parameters":[],"start":1598101924881,"name":"\t BaseElement execute click ","stop":1598101924962},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[],"parameters":[],"start":1598101924976,"name":"\t Name field execute type with arguments [\"admin1\"]","stop":1598101925006},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[],"parameters":[],"start":1598101925021,"name":"\t User name filed execute type with arguments [\"admin1\"]","stop":1598101925047},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[],"parameters":[],"start":1598101925057,"name":"\t Email field execute type with arguments [\"admin1@admin1.com\"]","stop":1598101925117},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[],"parameters":[],"start":1598101925127,"name":"\t Password filed execute type with arguments [\"admin1\"]","stop":1598101925153},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[],"parameters":[],"start":1598101925163,"name":"\t Submit button execute click ","stop":1598101925213}],"attachments":[],"parameters":[],"start":1598101924769,"name":"MainPage execute register","stop":1598101925213},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[{"name":"Step result","type":"application/json","source":"f84fa715-a170-4eb8-bb92-9c49291561ed-attachment.json"}],"parameters":[],"start":1598101925313,"name":"\t BaseElement execute textContent ","stop":1598101925317}],"attachments":[{"name":"Step result","type":"application/json","source":"8211c1d8-b407-4edd-beb9-9a37c3c5e6d2-attachment.json"}],"parameters":[],"start":1598101925213,"name":"TablesPage execute getPageHeaderTitleContent","stop":1598101925318},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[{"name":"Expected value","type":"application/json","source":"b119174c-b6b1-4e5d-8102-dbce4e6fb207-attachment.json"}],"parameters":[],"start":1598101925318,"name":"Title should contails username","stop":1598101925318}],"attachments":[],"parameters":[],"labels":[{"name":"suite","value":"Noop spec"}],"links":[],"uuid":"57e46dca-d59d-4f6d-88e3-98181c6ff220","historyId":"d39cbe4db3bbd537653e3d7c8ac20880","start":1598101923700,"name":"register","fullName":"register","stop":1598101925318} -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/6617f414-ac37-469f-acdc-294d245a36d0-attachment.json: -------------------------------------------------------------------------------- 1 | "Таблиці, Привіт admin" -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/757c86b0-6dc9-45e5-992e-a7953aa902f9-result.json: -------------------------------------------------------------------------------- 1 | {"status":"passed","statusDetails":{},"stage":"finished","steps":[{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[],"parameters":[],"start":1598101923189,"name":"\t BaseElement execute click ","stop":1598101923281},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[],"parameters":[],"start":1598101923295,"name":"\t BaseElement execute type with arguments [\"admin\"]","stop":1598101923319},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[],"parameters":[],"start":1598101923332,"name":"\t BaseElement execute type with arguments [\"admin\"]","stop":1598101923353},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[],"parameters":[],"start":1598101923365,"name":"\t BaseElement execute click ","stop":1598101923412}],"attachments":[],"parameters":[],"start":1598101923083,"name":"MainPage execute login","stop":1598101923412},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[{"name":"Step result","type":"application/json","source":"d221c36d-1f50-4887-880d-1cfdbd1fa1c9-attachment.json"}],"parameters":[],"start":1598101923512,"name":"\t BaseElement execute textContent ","stop":1598101923518}],"attachments":[{"name":"Step result","type":"application/json","source":"6617f414-ac37-469f-acdc-294d245a36d0-attachment.json"}],"parameters":[],"start":1598101923412,"name":"TablesPage execute getPageHeaderTitleContent","stop":1598101923518},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[{"name":"Expected value","type":"application/json","source":"566ebb7c-6e27-440b-88e1-c1f852e97d94-attachment.json"}],"parameters":[],"start":1598101923518,"name":"Title should contails username","stop":1598101923519}],"attachments":[],"parameters":[],"labels":[{"name":"suite","value":"Noop spec"}],"links":[],"uuid":"757c86b0-6dc9-45e5-992e-a7953aa902f9","historyId":"b74f4aa33199de14fa7e5045065a8329","start":1598101922004,"name":"login","fullName":"login","stop":1598101923519} -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/8211c1d8-b407-4edd-beb9-9a37c3c5e6d2-attachment.json: -------------------------------------------------------------------------------- 1 | "Таблиці, Привіт admin1" -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/84577fa2-31f4-486f-bb56-099fd4ae395e-attachment.json: -------------------------------------------------------------------------------- 1 | "Таблиці, Привіт admin" -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/8f0a22b3-8e93-4387-893e-889cd31de987-result.json: -------------------------------------------------------------------------------- 1 | {"status":"broken","statusDetails":{"message":"The \"data\" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received undefined","trace":"TypeError [ERR_INVALID_ARG_TYPE]: The \"data\" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received undefined\n at Object.writeFileSync (fs.js:1409:5)\n at AllureRuntime.writeAttachment (node_modules/allure2-js-commons/src/AllureRuntime.ts:46:3)\n at AllureReporter.writeAttachment (node_modules/mocha-allure2-reporter/src/AllureReporter.ts:149:25)\n at MochaAllureInterface.attachment (node_modules/mocha-allure2-reporter/src/MochaAllureInterface.ts:99:32)\n at allureStep (lib/page.conditions.ts:13:10)\n at Object.postAssertCall (node_modules/assertior/lib/assertions.utils.ts:5:5)\n at Object.stringIncludesSubstring (node_modules/assertior/lib/type.string.assertions.ts:24:3)\n at Object.stringIncludesSubstring (node_modules/assertior/lib/index.ts:36:7)\n at Context. (specs/noop.spec.ts:20:92)"},"stage":"finished","steps":[{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[],"parameters":[],"start":1598101870329,"name":"\t BaseElement execute click ","stop":1598101870420},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[],"parameters":[],"start":1598101870433,"name":"\t BaseElement execute type with arguments [\"admin\"]","stop":1598101870460},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[],"parameters":[],"start":1598101870471,"name":"\t BaseElement execute type with arguments [\"admin\"]","stop":1598101870492},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[],"parameters":[],"start":1598101870505,"name":"\t BaseElement execute click ","stop":1598101870554}],"attachments":[],"parameters":[],"start":1598101870222,"name":"MainPage execute login","stop":1598101870554},{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[{"status":"passed","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[{"name":"Step result","type":"application/json","source":"84577fa2-31f4-486f-bb56-099fd4ae395e-attachment.json"}],"parameters":[],"start":1598101870661,"name":"\t BaseElement execute textContent ","stop":1598101870665}],"attachments":[{"name":"Step result","type":"application/json","source":"4192f607-8a20-4407-886a-646ce5182edc-attachment.json"}],"parameters":[],"start":1598101870554,"name":"TablesPage execute getPageHeaderTitleContent","stop":1598101870665},{"status":"broken","statusDetails":{},"stage":"scheduled","steps":[],"attachments":[{"name":"Expected value","type":"application/json","source":"0b10885f-4e4c-4e21-abe8-b8ac751077bd-attachment.json"}],"parameters":[],"start":1598101870666,"name":"Title should contails username"}],"attachments":[],"parameters":[],"labels":[{"name":"suite","value":"Noop spec"}],"links":[],"uuid":"8f0a22b3-8e93-4387-893e-889cd31de987","historyId":"b74f4aa33199de14fa7e5045065a8329","start":1598101869145,"name":"login","fullName":"login","stop":1598101870693} -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/b119174c-b6b1-4e5d-8102-dbce4e6fb207-attachment.json: -------------------------------------------------------------------------------- 1 | "Таблиці, Привіт admin1" -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/c6752d0d-be8d-45ea-9ea5-25333ba23174-attachment.json: -------------------------------------------------------------------------------- 1 | "Таблиці, Привіт admin1" -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/d221c36d-1f50-4887-880d-1cfdbd1fa1c9-attachment.json: -------------------------------------------------------------------------------- 1 | "Таблиці, Привіт admin" -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/d4f5cc31-5526-48b3-b856-274ed2784cb8-container.json: -------------------------------------------------------------------------------- 1 | {"children":["8f0a22b3-8e93-4387-893e-889cd31de987","e9f2d824-5b03-4a9f-b355-b2a40882e49d"],"befores":[{"status":"passed","statusDetails":{},"stage":"finished","steps":[],"attachments":[],"parameters":[]},{"status":"passed","statusDetails":{},"stage":"finished","steps":[],"attachments":[],"parameters":[]}],"afters":[{"status":"passed","statusDetails":{},"stage":"finished","steps":[],"attachments":[],"parameters":[]},{"status":"passed","statusDetails":{},"stage":"finished","steps":[],"attachments":[],"parameters":[]}],"links":[],"uuid":"d4f5cc31-5526-48b3-b856-274ed2784cb8","start":1598101869144,"name":"Noop spec","stop":1598101872818} -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/e302f7e2-343d-4485-b06d-8d8e7c8935f7-container.json: -------------------------------------------------------------------------------- 1 | {"children":["757c86b0-6dc9-45e5-992e-a7953aa902f9","57e46dca-d59d-4f6d-88e3-98181c6ff220"],"befores":[{"status":"passed","statusDetails":{},"stage":"finished","steps":[],"attachments":[],"parameters":[]},{"status":"passed","statusDetails":{},"stage":"finished","steps":[],"attachments":[],"parameters":[]}],"afters":[{"status":"passed","statusDetails":{},"stage":"finished","steps":[],"attachments":[],"parameters":[]},{"status":"passed","statusDetails":{},"stage":"finished","steps":[],"attachments":[],"parameters":[]}],"links":[],"uuid":"e302f7e2-343d-4485-b06d-8d8e7c8935f7","start":1598101922003,"name":"Noop spec","stop":1598101925484} -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/e9f2d824-5b03-4a9f-b355-b2a40882e49d-result.json: -------------------------------------------------------------------------------- 1 | {"status":"broken","statusDetails":{"message":"The \"data\" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received undefined","trace":"TypeError [ERR_INVALID_ARG_TYPE]: The \"data\" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received undefined\n at Object.writeFileSync (fs.js:1409:5)\n at AllureRuntime.writeAttachment (node_modules/allure2-js-commons/src/AllureRuntime.ts:46:3)\n at AllureReporter.writeAttachment (node_modules/mocha-allure2-reporter/src/AllureReporter.ts:149:25)\n at MochaAllureInterface.attachment (node_modules/mocha-allure2-reporter/src/MochaAllureInterface.ts:99:32)\n at allureStep (lib/page.conditions.ts:13:10)\n at Object.postAssertCall (node_modules/assertior/lib/assertions.utils.ts:5:5)\n at Object.stringIncludesSubstring (node_modules/assertior/lib/type.string.assertions.ts:24:3)\n at Object.stringIncludesSubstring (node_modules/assertior/lib/index.ts:36:7)\n at Context. (specs/noop.spec.ts:27:92)"},"stage":"finished","steps":[],"attachments":[],"parameters":[],"labels":[{"name":"suite","value":"Noop spec"}],"links":[],"uuid":"e9f2d824-5b03-4a9f-b355-b2a40882e49d","historyId":"d39cbe4db3bbd537653e3d7c8ac20880","start":1598101870869,"name":"register","fullName":"register","stop":1598101872662} -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/f84fa715-a170-4eb8-bb92-9c49291561ed-attachment.json: -------------------------------------------------------------------------------- 1 | "Таблиці, Привіт admin1" -------------------------------------------------------------------------------- /livecoding-playwright-part-3/allure-results/fc55c526-4841-4967-aafb-be0f1e959ba2-attachment.json: -------------------------------------------------------------------------------- 1 | "Таблиці, Привіт admin1" -------------------------------------------------------------------------------- /livecoding-playwright-part-3/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/potapovDim/livecoding-s/9a240fe35797c30342277cb786279dbc0a3e887f/livecoding-playwright-part-3/example.png -------------------------------------------------------------------------------- /livecoding-playwright-part-3/framework/index.ts: -------------------------------------------------------------------------------- 1 | import {MainPage, TablesPage} from './pages' 2 | import {makeSingleTon, Browser} from '../lib'; 3 | 4 | 5 | 6 | const pageProvider = (page) => { 7 | return { 8 | main: (): MainPage => makeSingleTon(MainPage, page), 9 | tables: (): TablesPage => makeSingleTon(TablesPage, page) 10 | } 11 | } 12 | 13 | const provider = { 14 | browser: new Browser(), 15 | } 16 | 17 | 18 | export { 19 | pageProvider, 20 | provider 21 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-3/framework/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './main'; 2 | export * from './tables'; 3 | -------------------------------------------------------------------------------- /livecoding-playwright-part-3/framework/pages/main/fragments/header.ts: -------------------------------------------------------------------------------- 1 | import {BaseFragment, $element} from '../../../../lib'; 2 | 3 | class HeaderFragment extends BaseFragment { 4 | login; 5 | register; 6 | constructor(page, rootFragmentSelector = '.main_header') { 7 | super(page, rootFragmentSelector) 8 | this.login = $element(this.page, '.user_buttons button:nth-child(1)') 9 | this.register = $element(this.page, '.user_buttons button:nth-child(2)') 10 | } 11 | 12 | async toLogin() { 13 | await this.login.click() 14 | } 15 | 16 | async toRegister() { 17 | await this.register.click() 18 | } 19 | } 20 | 21 | 22 | export { 23 | HeaderFragment 24 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-3/framework/pages/main/fragments/login.ts: -------------------------------------------------------------------------------- 1 | import {BaseFragment, $element} from '../../../../lib'; 2 | 3 | class LoginFragment extends BaseFragment { 4 | username 5 | password; 6 | submit; 7 | constructor(page, rootFragmentSelector = '.login_form') { 8 | super(page, rootFragmentSelector) 9 | this.username = $element(this.page, '.login_form .modal .form-group:nth-child(1) input') 10 | this.password = $element(this.page, '.login_form .modal .form-group:nth-child(2) input') 11 | this.submit = $element(this.page, '.login_form .modal button') 12 | } 13 | 14 | async login(username, password) { 15 | // TODO should be refactored 16 | await this.username.type(username) 17 | await this.password.type(password) 18 | await this.submit.click() 19 | } 20 | } 21 | 22 | export { 23 | LoginFragment 24 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-3/framework/pages/main/fragments/register.ts: -------------------------------------------------------------------------------- 1 | import {BaseFragment, $element} from '../../../../lib'; 2 | 3 | class RegisterFragment extends BaseFragment { 4 | 5 | name; 6 | username; 7 | email; 8 | password; 9 | submit; 10 | 11 | private id: string; 12 | 13 | constructor(page, rootFragmentSelector = '.registration_form') { 14 | super(page, rootFragmentSelector) 15 | 16 | this.id = 'Register fragment'; 17 | 18 | this.name = $element(this.page, '.registration_form .modal .form-group:nth-child(1) input', 'Name field'); 19 | this.username = $element(this.page, '.registration_form .modal .form-group:nth-child(2) input', 'User name filed'); 20 | this.email = $element(this.page, '.registration_form .modal .form-group:nth-child(3) input', 'Email field'); 21 | this.password = $element(this.page, '.registration_form .modal .form-group:nth-child(4) input', 'Password filed'); 22 | this.submit = $element(this.page, '.registration_form .modal button', 'Submit button'); 23 | } 24 | 25 | async register(name, username, email, password) { 26 | // TODO should be refactored 27 | await this.name.type(name); 28 | await this.username.type(username); 29 | await this.email.type(email); 30 | await this.password.type(password) 31 | await this.submit.click() 32 | } 33 | } 34 | 35 | 36 | export { 37 | RegisterFragment 38 | } 39 | -------------------------------------------------------------------------------- /livecoding-playwright-part-3/framework/pages/main/index.ts: -------------------------------------------------------------------------------- 1 | import {LoginFragment} from './fragments/login'; 2 | import {RegisterFragment} from './fragments/register'; 3 | import {HeaderFragment} from './fragments/header'; 4 | import {BasePage} from '../../../lib'; 5 | 6 | class MainPage extends BasePage { 7 | loginFragment; 8 | registerFragment; 9 | headerFragment; 10 | constructor(page, pageRootSelector = '#main_page') { 11 | super(page, pageRootSelector) 12 | this.loginFragment = new LoginFragment(page); 13 | this.registerFragment = new RegisterFragment(page); 14 | this.headerFragment = new HeaderFragment(page); 15 | } 16 | 17 | async login(username, password) { 18 | // TODO should be refactored 19 | await this.headerFragment.toLogin(); 20 | await this.loginFragment.login(username, password) 21 | } 22 | 23 | async register(name, username, email, password) { 24 | // TODO should be refactored 25 | await this.headerFragment.toRegister(); 26 | await this.registerFragment.register(name, username, email, password) 27 | } 28 | } 29 | 30 | export { 31 | MainPage 32 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-3/framework/pages/tables/index.ts: -------------------------------------------------------------------------------- 1 | import {BasePage, $element} from '../../../lib'; 2 | 3 | class TablesPage extends BasePage { 4 | header; 5 | constructor(page, pageRootSelector = '#table_page') { 6 | super(page, pageRootSelector) 7 | this.header = $element(this.page, '.header h3') 8 | } 9 | 10 | async getPageHeaderTitleContent() { 11 | return this.header.textContent(); 12 | } 13 | } 14 | 15 | export { 16 | TablesPage 17 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-3/lib/base.fragment.implementation.ts: -------------------------------------------------------------------------------- 1 | class BaseFragment { 2 | page; 3 | rootSelector; 4 | constructor(page, pageRootSelector) { 5 | this.page = page 6 | this.rootSelector = pageRootSelector; 7 | } 8 | 9 | _replacePage(page) { 10 | this.page = page 11 | 12 | const excludeProps = ['page', 'rootSelector', 'id'] 13 | 14 | this.page = page 15 | 16 | const exptectedProps = Object 17 | .getOwnPropertyNames(this) 18 | .filter((p) => !excludeProps.includes(p)) 19 | 20 | exptectedProps.forEach((p) => { 21 | this[p]._replacePage.call(this[p], page); 22 | }) 23 | } 24 | } 25 | 26 | export { 27 | BaseFragment 28 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-3/lib/base.page.implementation.ts: -------------------------------------------------------------------------------- 1 | import {decoratePage} from './page.conditions' 2 | 3 | class BasePage { 4 | page; 5 | rootSelector; 6 | constructor(page, pageRootSelector) { 7 | decoratePage(this); 8 | this.page = page 9 | this.rootSelector = pageRootSelector; 10 | } 11 | 12 | _replacePage(page) { 13 | const excludeProps = ['page', 'rootSelector'] 14 | 15 | this.page = page 16 | 17 | const exptectedProps = Object 18 | .getOwnPropertyNames(this) 19 | .filter((p) => !excludeProps.includes(p)) 20 | 21 | exptectedProps.forEach((p) => { 22 | this[p]._replacePage.call(this[p], page) 23 | }) 24 | } 25 | } 26 | 27 | export { 28 | BasePage 29 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-3/lib/browser/index.ts: -------------------------------------------------------------------------------- 1 | import {chromium, Page} from 'playwright'; 2 | 3 | class Browser { 4 | page; 5 | browser; 6 | 7 | constructor() { 8 | // 9 | } 10 | 11 | async init(): Promise { 12 | this.browser = await chromium.launch({headless: false}); 13 | const context = await this.browser.newContext(); 14 | this.page = await context.newPage(); 15 | return this.page; 16 | } 17 | 18 | async get(url: string) { 19 | await this.page.goto(url); 20 | } 21 | 22 | async close() { 23 | await this.browser.close(); 24 | } 25 | } 26 | 27 | export { 28 | Browser 29 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-3/lib/elements/base.element.ts: -------------------------------------------------------------------------------- 1 | import {waits} from '../helpers'; 2 | import * as chalk from 'chalk'; 3 | import {allureInterfaceStep} from '../page.conditions'; 4 | 5 | const {ALLURE} = process.env; 6 | 7 | class BaseElement { 8 | page; 9 | selector; 10 | currentElement; 11 | id; 12 | constructor(page, selector, elementName?) { 13 | this.page = page; 14 | this.selector = selector 15 | this.currentElement = null 16 | this.id = elementName 17 | } 18 | 19 | _replacePage(page) { 20 | this.page = page 21 | this.currentElement = null 22 | } 23 | 24 | async initThisElement() { 25 | // TODO not use for isPresent/isDisplayed 26 | await waits(this.page).waitVisibility(this.selector); 27 | 28 | if (this.currentElement) { 29 | return this.currentElement; 30 | } 31 | 32 | const el = await this.page.$(this.selector); 33 | this.currentElement = el; 34 | return this.currentElement 35 | } 36 | } 37 | 38 | function $element(page, selector, elementName?) { 39 | const baseEl = new BaseElement(page, selector, elementName); 40 | return new Proxy(baseEl, { 41 | get(_t, value) { 42 | 43 | if (value === '_replacePage') { 44 | return (page) => baseEl._replacePage(page) 45 | } 46 | 47 | return (...args) => baseEl.initThisElement().then((curEl) => { 48 | if (!baseEl.id) { 49 | baseEl.id = `BaseElement` 50 | } 51 | 52 | let message = `\t ${baseEl.id} execute ${value as string} ` 53 | 54 | if (args.length) { 55 | message = `${message} with arguments ${JSON.stringify(args)}` 56 | } 57 | 58 | if (ALLURE) { 59 | return allureInterfaceStep(message, curEl[value].bind(curEl, ...args)) 60 | } 61 | console.log(chalk.green(message)) 62 | return curEl[value].call(curEl, ...args) 63 | }) 64 | } 65 | }) 66 | } 67 | 68 | export { 69 | $element 70 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-3/lib/elements/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base.element'; 2 | -------------------------------------------------------------------------------- /livecoding-playwright-part-3/lib/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './waits'; 2 | export * from './make.singleton'; 3 | -------------------------------------------------------------------------------- /livecoding-playwright-part-3/lib/helpers/make.singleton.ts: -------------------------------------------------------------------------------- 1 | export function makeSingleTon(PagePO, ctxPage) { 2 | 3 | if (PagePO.__instance) { 4 | // TODO can fail here 5 | PagePO.__instance._replacePage.call(PagePO.__instance, ctxPage) 6 | 7 | return PagePO.__instance; 8 | } 9 | 10 | const page = new PagePO(ctxPage); 11 | 12 | PagePO.__instance = page; 13 | return PagePO.__instance; 14 | } 15 | -------------------------------------------------------------------------------- /livecoding-playwright-part-3/lib/helpers/waits.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 4 | * @param {playwright page} page 5 | * @returns {object<{waitVisibility: () => Promise}>} 6 | */ 7 | export function waits(page) { 8 | return { 9 | waitVisibility: (selector) => page.waitForSelector(selector, {state: 'attached', timeout: 1000}) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /livecoding-playwright-part-3/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helpers'; 2 | export * from './page.conditions'; 3 | export * from './base.page.implementation'; 4 | export * from './base.fragment.implementation'; 5 | export * from './elements'; 6 | export * from './browser'; 7 | -------------------------------------------------------------------------------- /livecoding-playwright-part-3/lib/page.conditions.ts: -------------------------------------------------------------------------------- 1 | import {waits} from './helpers'; 2 | import * as chalk from 'chalk'; 3 | import {initStepDeclarator} from 'assertior'; 4 | const {ALLURE} = process.env; 5 | 6 | 7 | declare const allure; 8 | 9 | function allureStep(stepAssertionName: string, error, expected, current) { 10 | const step = allure.startStep(stepAssertionName); 11 | 12 | if (expected) { 13 | allure.attachment('Expected value', JSON.stringify(expected, null, 2), 'application/json'); 14 | } 15 | if (current) { 16 | allure.attachment('Current value', JSON.stringify(current, null, 2), 'application/json'); 17 | } 18 | 19 | if (error) { 20 | allure.attachment('Error', JSON.stringify(error, null, 2), 'application/json'); 21 | } 22 | step.step.stepResult.status = error ? 'broken' : 'passed'; 23 | step.endStep(); 24 | } 25 | 26 | async function allureInterfaceStep(stepName, cb) { 27 | const step = allure.startStep(stepName); 28 | 29 | try { 30 | const result = await cb(); 31 | 32 | if (result) { 33 | allure.attachment('Step result', JSON.stringify(result, null, 2), 'application/json'); 34 | } 35 | 36 | step.step.stepResult.status = 'passed'; 37 | step.endStep(); 38 | 39 | return result; 40 | } catch (error) { 41 | allure.attachment('Error', JSON.stringify(error, null, 2), 'application/json'); 42 | step.step.stepResult.status = 'broken'; 43 | step.endStep(); 44 | throw error; 45 | } 46 | } 47 | 48 | if (ALLURE) { 49 | initStepDeclarator(allureStep); 50 | } 51 | 52 | /** 53 | * 54 | * @param {function Constructor context} page 55 | */ 56 | function decoratePage(pageOrFramgent) { 57 | const name = pageOrFramgent.id || pageOrFramgent.__proto__.constructor.name; 58 | 59 | const requiredToDecorate = Object.getOwnPropertyNames(pageOrFramgent.__proto__) 60 | .filter(prop => { 61 | if (prop !== 'constructor' && (typeof pageOrFramgent.__proto__[prop]).includes('function')) { 62 | return true 63 | } 64 | return false 65 | }) 66 | 67 | requiredToDecorate.forEach(prop => { 68 | const originalProp = pageOrFramgent.__proto__[prop] 69 | pageOrFramgent.__proto__[prop] = async function(...args) { 70 | /** 71 | * @example 72 | * every page should have rootSelector 73 | * this selector will be used for wait visibility of current page 74 | */ 75 | let message = `${name} execute ${prop}`; 76 | 77 | if (name.includes('Fragment')) { 78 | message = `\t${message}`; 79 | } 80 | 81 | async function currentCall(...currentCallArgs) { 82 | await waits(this.page).waitVisibility(this.rootSelector) 83 | return originalProp.call(this, ...currentCallArgs); 84 | } 85 | 86 | if (ALLURE) { 87 | 88 | return allureInterfaceStep(message, currentCall.bind(this, ...args)); 89 | } 90 | 91 | console.log(chalk.green(message)) 92 | 93 | return currentCall.call(this, ...args); 94 | } 95 | }) 96 | } 97 | 98 | export { 99 | decoratePage, 100 | allureInterfaceStep 101 | } 102 | 103 | -------------------------------------------------------------------------------- /livecoding-playwright-part-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livecoding-playwrigth", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "find ./specs/ -name '*.spec.ts' | xargs mocha --require ts-node/register --timeout 300000", 8 | "test:allure": "ALLURE=1 mocha ./specs/**/*.spec.ts --require ts-node/register --timeout 300000 --reporter mocha-allure2-reporter" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@types/mocha": "^8.0.3", 15 | "@types/node": "^14.6.0", 16 | "assertior": "0.0.9", 17 | "chalk": "^4.1.0", 18 | "mocha": "^8.1.1", 19 | "mocha-allure2-reporter": "0.0.3", 20 | "playwright": "^1.3.0", 21 | "ts-node": "^9.0.0", 22 | "typescript": "^4.0.2" 23 | } 24 | } -------------------------------------------------------------------------------- /livecoding-playwright-part-3/specs/noop.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'assertior'; 2 | import {pageProvider, provider} from '../framework'; 3 | 4 | describe('Noop spec', function() { 5 | let page = null; 6 | 7 | beforeEach(async () => { 8 | page = await provider.browser.init(); 9 | await provider.browser.get('http://localhost:4000'); 10 | }) 11 | 12 | afterEach(async () => { 13 | await provider.browser.close(); 14 | }) 15 | 16 | it('login', async function() { 17 | const mainPage = pageProvider(page).main(); 18 | const tablesPage = pageProvider(page).tables(); 19 | await mainPage.login('admin', 'admin'); 20 | expect(await tablesPage.getPageHeaderTitleContent(), 'Title should contails username').stringIncludesSubstring('admin'); 21 | }) 22 | 23 | it('register', async function() { 24 | const mainPage = pageProvider(page).main(); 25 | const tablesPage = pageProvider(page).tables(); 26 | await mainPage.register('admin1', 'admin1', 'admin1@admin1.com', 'admin1'); 27 | expect(await tablesPage.getPageHeaderTitleContent(), 'Title should contails username').stringIncludesSubstring('admin1') 28 | }) 29 | }) -------------------------------------------------------------------------------- /livecoding-playwright-part-3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2018", 5 | "sourceMap": true, 6 | "outDir": "built", 7 | "declaration": true, 8 | "experimentalDecorators": true, 9 | "allowJs": true 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | "built" 14 | ] 15 | } -------------------------------------------------------------------------------- /livecoding-playwright/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules -------------------------------------------------------------------------------- /livecoding-playwright/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/potapovDim/livecoding-s/9a240fe35797c30342277cb786279dbc0a3e887f/livecoding-playwright/example.png -------------------------------------------------------------------------------- /livecoding-playwright/framework/index.js: -------------------------------------------------------------------------------- 1 | const {MainPage, TablesPage} = require('./pages') 2 | const {makeSingleTon} = require('../lib') 3 | 4 | const pageProvider = (page) => { 5 | return { 6 | main: () => makeSingleTon(MainPage, page), 7 | tables: () => makeSingleTon(TablesPage, page) 8 | } 9 | } 10 | module.exports = { 11 | pageProvider 12 | } -------------------------------------------------------------------------------- /livecoding-playwright/framework/pages/index.js: -------------------------------------------------------------------------------- 1 | const main = require('./main'); 2 | const tables = require('./tables'); 3 | 4 | module.exports = { 5 | ...main, 6 | ...tables 7 | }; 8 | -------------------------------------------------------------------------------- /livecoding-playwright/framework/pages/main/fragments/header.js: -------------------------------------------------------------------------------- 1 | const {decoratePage} = require('../../../../lib'); 2 | 3 | class HeaderFragment { 4 | constructor(page, rootFragmentSelector = '.main_header') { 5 | this.page = page 6 | this.rootSelector = rootFragmentSelector; 7 | } 8 | 9 | get login() { 10 | return this.page.$('.user_buttons button:nth-child(1)') 11 | } 12 | get register() { 13 | return this.page.$('.user_buttons button:nth-child(2)') 14 | } 15 | 16 | async toLogin() { 17 | await (await this.login).click() 18 | } 19 | 20 | async toRegister() { 21 | await (await this.register).click() 22 | } 23 | } 24 | decoratePage(HeaderFragment) 25 | 26 | 27 | module.exports = { 28 | HeaderFragment 29 | } -------------------------------------------------------------------------------- /livecoding-playwright/framework/pages/main/fragments/login.js: -------------------------------------------------------------------------------- 1 | const {decoratePage} = require('../../../../lib'); 2 | 3 | class LoginFragment { 4 | constructor(page, rootFragmentSelector = '.login_form') { 5 | this.page = page 6 | this.rootSelector = rootFragmentSelector; 7 | } 8 | 9 | async login(username, password) { 10 | // TODO should be refactored 11 | await this.page.fill('.login_form .modal .form-group:nth-child(1) input', username) 12 | await this.page.fill('.login_form .modal .form-group:nth-child(2) input', password) 13 | await this.page.click('.login_form .modal button') 14 | } 15 | } 16 | decoratePage(LoginFragment) 17 | 18 | module.exports = { 19 | LoginFragment 20 | } -------------------------------------------------------------------------------- /livecoding-playwright/framework/pages/main/fragments/register.js: -------------------------------------------------------------------------------- 1 | const {decoratePage} = require('../../../../lib'); 2 | 3 | class RegisterFragment { 4 | constructor(page, rootFragmentSelector = '.registration_form') { 5 | this.page = page 6 | this.rootSelector = rootFragmentSelector; 7 | } 8 | 9 | get name() { 10 | return this.page.$('.registration_form .modal .form-group:nth-child(1) input'); 11 | } 12 | 13 | get username() { 14 | return this.page.$('.registration_form .modal .form-group:nth-child(2) input'); 15 | } 16 | 17 | get email() { 18 | return this.page.$('.registration_form .modal .form-group:nth-child(3) input'); 19 | } 20 | 21 | get password() { 22 | return this.page.$('.registration_form .modal .form-group:nth-child(4) input'); 23 | } 24 | 25 | get submit() { 26 | return this.page.$('.registration_form .modal button') 27 | } 28 | 29 | async register(name, username, email, password) { 30 | // TODO should be refactored 31 | await (await this.name).type(name); 32 | await (await this.username).type(username); 33 | await (await this.email).type(email); 34 | await (await this.password).type(password) 35 | await (await this.submit).click() 36 | } 37 | } 38 | decoratePage(RegisterFragment) 39 | 40 | module.exports = { 41 | RegisterFragment 42 | } 43 | -------------------------------------------------------------------------------- /livecoding-playwright/framework/pages/main/index.js: -------------------------------------------------------------------------------- 1 | const {LoginFragment} = require('./fragments/login'); 2 | const {RegisterFragment} = require('./fragments/register'); 3 | const {HeaderFragment} = require('./fragments/header'); 4 | const {decoratePage} = require('../../../lib'); 5 | 6 | class MainPage { 7 | 8 | constructor(page, pageRootSelector = '#main_page') { 9 | 10 | this.page = page 11 | this.rootSelector = pageRootSelector; 12 | 13 | this.loginFragment = new LoginFragment(page); 14 | this.registerFragment = new RegisterFragment(page); 15 | this.headerFragment = new HeaderFragment(page); 16 | } 17 | 18 | 19 | async login(username, password) { 20 | // TODO should be refactored 21 | await this.headerFragment.toLogin(); 22 | await this.loginFragment.login(username, password) 23 | } 24 | 25 | async register(name, username, email, password) { 26 | // TODO should be refactored 27 | await this.headerFragment.toRegister(); 28 | await this.registerFragment.register(name, username, email, password) 29 | } 30 | } 31 | decoratePage(MainPage); 32 | 33 | module.exports = { 34 | MainPage 35 | } -------------------------------------------------------------------------------- /livecoding-playwright/framework/pages/tables/index.js: -------------------------------------------------------------------------------- 1 | const {waits} = require('../../../lib'); 2 | const {decoratePage} = require('../../../lib'); 3 | 4 | class TablesPage { 5 | constructor(page, pageRootSelector = '#table_page') { 6 | this.rootSelector = pageRootSelector; 7 | this.page = page; 8 | 9 | } 10 | 11 | // 12 | set replacePage(page) { 13 | this.page = page 14 | } 15 | 16 | async getPageHeaderTitleContent() { 17 | // TODO should be locate in base PAGE 18 | const elementHandle = await this.page.$('.header h3') 19 | return elementHandle.textContent(); 20 | } 21 | } 22 | decoratePage(TablesPage) 23 | 24 | module.exports = { 25 | TablesPage 26 | } -------------------------------------------------------------------------------- /livecoding-playwright/lib/helpers/index.js: -------------------------------------------------------------------------------- 1 | const waits = require('./waits'); 2 | const makeSingleton = require('./make.singleton'); 3 | 4 | 5 | module.exports = { 6 | ...waits, 7 | ...makeSingleton 8 | } 9 | -------------------------------------------------------------------------------- /livecoding-playwright/lib/helpers/make.singleton.js: -------------------------------------------------------------------------------- 1 | function makeSingleTon(Page, ...args) { 2 | // console.log(...args, Page) 3 | // if(Page.__instance) { 4 | // return Page.__instance; 5 | // } 6 | const page = new Page(...args); 7 | 8 | Page.__instance = page; 9 | return Page.__instance; 10 | } 11 | 12 | module.exports = { 13 | makeSingleTon 14 | } -------------------------------------------------------------------------------- /livecoding-playwright/lib/helpers/waits.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 4 | * @param {playwright page} page 5 | * @returns {object<{waitVisibility: () => Promise}>} 6 | */ 7 | function waits(page) { 8 | return { 9 | waitVisibility: (selector) => page.waitForSelector(selector, {state: 'attached', timeout: 1000}) 10 | } 11 | } 12 | 13 | module.exports = { 14 | waits 15 | } 16 | -------------------------------------------------------------------------------- /livecoding-playwright/lib/index.js: -------------------------------------------------------------------------------- 1 | const helpers = require('./helpers') 2 | const pageConditions = require('./page.conditions') 3 | 4 | module.exports = { 5 | ...helpers, 6 | ...pageConditions 7 | } 8 | -------------------------------------------------------------------------------- /livecoding-playwright/lib/page.conditions.js: -------------------------------------------------------------------------------- 1 | const {waits} = require('./helpers') 2 | const chalk = require('chalk'); 3 | 4 | /** 5 | * 6 | * @param {function Constructor context} page 7 | */ 8 | function decoratePage(page) { 9 | const {name} = page; 10 | const requiredToDecorate = Object.getOwnPropertyNames(page.prototype) 11 | .filter(prop => { 12 | if(prop === 'constructor') { 13 | return false 14 | } 15 | const descriptor = Object.getOwnPropertyDescriptor(page.prototype, prop) 16 | return !!descriptor.value; 17 | }) 18 | 19 | requiredToDecorate.forEach(prop => { 20 | const originalProp = page.prototype[prop] 21 | page.prototype[prop] = async function(...args) { 22 | /** 23 | * @example 24 | * every page should have rootSelector 25 | * this selector will be used for wait visibility of current page 26 | */ 27 | let message = `${name} execute ${prop}`; 28 | if(name.includes('Fragment')) { 29 | message = `\t${message}`; 30 | } 31 | console.log(chalk.green(message)) 32 | await waits(this.page).waitVisibility(this.rootSelector) 33 | return originalProp.call(this, ...args); 34 | } 35 | }) 36 | } 37 | 38 | module.exports = { 39 | decoratePage 40 | } 41 | 42 | -------------------------------------------------------------------------------- /livecoding-playwright/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livecoding-playwrigth", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha ./specs/**/*.spec.js --timeout 10000 -R spec" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "chai": "^4.2.0", 14 | "chalk": "^4.1.0", 15 | "mocha": "^8.0.1", 16 | "playwright": "^1.1.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /livecoding-playwright/specs/noop.spec.js: -------------------------------------------------------------------------------- 1 | const {expect} = require('chai'); 2 | const {chromium} = require('playwright'); 3 | const {pageProvider} = require('../framework'); 4 | 5 | describe('Noop spec', function() { 6 | let browser = null; 7 | let page = null; 8 | 9 | beforeEach(async () => { 10 | browser = await chromium.launch({headless: false}); 11 | const context = await browser.newContext(); 12 | page = await context.newPage(); 13 | await page.goto('http://localhost:4000'); 14 | }) 15 | 16 | afterEach(async () => { 17 | await browser.close(); 18 | }) 19 | 20 | it('login', async function() { 21 | const mainPage = pageProvider(page).main(); 22 | const tablesPage = pageProvider(page).tables(); 23 | await mainPage.login('admin', 'admin'); 24 | expect(await tablesPage.getPageHeaderTitleContent()).to.includes('admin'); 25 | }) 26 | 27 | it('register', async function() { 28 | const mainPage = pageProvider(page).main(); 29 | const tablesPage = pageProvider(page).tables(); 30 | await mainPage.register('admin1', 'admin1', 'admin1@admin1.com', 'admin1'); 31 | expect(await tablesPage.getPageHeaderTitleContent()).to.includes('admin1') 32 | }) 33 | }) -------------------------------------------------------------------------------- /livecoding-promod/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /livecoding-promod/mocha.hooks.ts: -------------------------------------------------------------------------------- 1 | import {seleniumWD} from 'promod'; 2 | let br = null 3 | before(async function() { 4 | const {getSeleniumDriver, browser} = seleniumWD; 5 | 6 | await getSeleniumDriver({}, browser); 7 | 8 | br = browser; 9 | }); 10 | 11 | after(async function() { 12 | await br.quit(); 13 | }); -------------------------------------------------------------------------------- /livecoding-promod/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livecoding-promod", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha $(find specs -name '*.spec.*') --file ./mocha.hooks.ts --require ts-node/register --timeout 500000", 8 | "driver:install": "selenium-standalone install" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@types/mocha": "^9.0.0", 15 | "mocha": "^9.1.0", 16 | "promod": "0.0.11", 17 | "ts-node": "^10.2.1", 18 | "typescript": "^4.3.5" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /livecoding-promod/pages/main/fragments/login.fragment.ts: -------------------------------------------------------------------------------- 1 | import {PromodSeleniumElementType, seleniumWD} from 'promod'; 2 | const {$} = seleniumWD; 3 | 4 | class LoginFragment { 5 | public username: PromodSeleniumElementType; 6 | public password: PromodSeleniumElementType; 7 | public signIn: PromodSeleniumElementType; 8 | 9 | constructor() { 10 | this.username = $('[placeholder="Ім\'я користувача"]'); 11 | this.password = $('xpath=//*[@placeholder="пароль"]'); 12 | this.signIn = $('.login_form button'); 13 | } 14 | } 15 | 16 | export {LoginFragment} -------------------------------------------------------------------------------- /livecoding-promod/pages/main/page.ts: -------------------------------------------------------------------------------- 1 | import {LoginFragment} from './fragments/login.fragment'; 2 | 3 | class MainPage { 4 | private loginForm: LoginFragment; 5 | 6 | constructor() { 7 | this.loginForm = new LoginFragment(); 8 | } 9 | 10 | async login(username, password) { 11 | await this.loginForm.username.sendKeys(username); 12 | await this.loginForm.password.sendKeys(password); 13 | await this.loginForm.signIn.click(); 14 | } 15 | } 16 | 17 | export {MainPage} -------------------------------------------------------------------------------- /livecoding-promod/specs/example.spec.ts: -------------------------------------------------------------------------------- 1 | import {seleniumWD} from 'promod'; 2 | 3 | import {MainPage} from '../pages/main/page' 4 | const {browser} = seleniumWD; 5 | 6 | describe('test', () => { 7 | it('test', async () => { 8 | await browser.get('http://localhost:4000/'); 9 | await new MainPage().login('admin', 'admin') 10 | await browser.sleep(5000); 11 | await browser.wait(() => {return {}}, {timeout: 2500, interval: 2500, throwCustom: TypeError}) 12 | }) 13 | }) -------------------------------------------------------------------------------- /livecoding-promod/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "sourceMap": true, 6 | "outDir": "built", 7 | "declaration": true, 8 | "experimentalDecorators": true 9 | }, 10 | "include": [ 11 | "lib" 12 | ], 13 | "exclude": [ 14 | "node_modules", 15 | "built", 16 | "specs", 17 | "example" 18 | ] 19 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-1/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | built 4 | allure-results -------------------------------------------------------------------------------- /livecoding-puppeteer-video-1/framework/pages/main.page.ts: -------------------------------------------------------------------------------- 1 | import {BasePage, _$, step, BaseElement} from '../../lib/index'; 2 | 3 | const usernameSelector = 'input.form-control'; 4 | const passwordSelector = 'input[type="password"]'; 5 | const submitSelector = '.login_form .modal button' 6 | 7 | class MainPage extends BasePage { 8 | private userName: BaseElement; 9 | private password: BaseElement; 10 | private submit: BaseElement; 11 | 12 | constructor() { 13 | super('#main_page', 'Main page') 14 | this.userName = _$(usernameSelector, 'User name field'); 15 | this.password = _$(passwordSelector, 'Password field'); 16 | this.submit = _$(submitSelector, 'Login button'); 17 | } 18 | 19 | @step((name) => `${name} execute login to the system`) 20 | async loginToSystem(userName, password) { 21 | await this.userName.sendKeys(userName); 22 | await this.password.sendKeys(password); 23 | await this.submit.click() 24 | } 25 | } 26 | 27 | export { 28 | MainPage 29 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-1/lib/base.page.ts: -------------------------------------------------------------------------------- 1 | import {pubsub, messagesList} from './pubsub'; 2 | // import {step} from './reporter/index'; 3 | 4 | class BasePage { 5 | private selector: string; 6 | private name: string; 7 | private page: any; 8 | 9 | constructor(selector, name) { 10 | this.selector = selector; 11 | this.name = name; 12 | 13 | pubsub.subscribe(messagesList.currentPage, this.initPage.bind(this)); 14 | } 15 | 16 | initPage(page) { 17 | this.page = page 18 | } 19 | 20 | get id() { 21 | return this.name; 22 | } 23 | } 24 | 25 | export { 26 | BasePage 27 | } 28 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-1/lib/browser.ts: -------------------------------------------------------------------------------- 1 | import {Page, Browser} from 'puppeteer'; 2 | import * as puppeteer from 'puppeteer'; 3 | import {pubsub, messagesList} from './pubsub'; 4 | 5 | const caps = { 6 | defaultViewport: { 7 | width: 1200, 8 | height: 800 9 | }, 10 | headless: false 11 | } 12 | 13 | class BrowserAddapter { 14 | private currentPage: Page; 15 | private browser: Browser; 16 | 17 | constructor() { 18 | // tbd 19 | } 20 | 21 | async initCurrentPage() { 22 | this.browser = await puppeteer.launch(caps); 23 | this.currentPage = await this.browser.newPage(); 24 | 25 | pubsub.publish(messagesList.currentPage, this.currentPage); 26 | } 27 | 28 | async goto(url: string) { 29 | if (!this.currentPage) { 30 | await this.initCurrentPage(); 31 | } 32 | 33 | await this.currentPage.goto(url); 34 | } 35 | 36 | async close() { 37 | await this.browser.close(); 38 | 39 | pubsub.publish(messagesList.closeBrowser, this.currentPage); 40 | } 41 | 42 | async sleep(time = 1000) { 43 | await (() => new Promise(res => setTimeout(res, time)))(); 44 | } 45 | } 46 | 47 | 48 | const browser = new BrowserAddapter(); 49 | 50 | export { 51 | browser 52 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-1/lib/element.ts: -------------------------------------------------------------------------------- 1 | import {pubsub, messagesList} from './pubsub'; 2 | import {step} from './reporter/index'; 3 | 4 | class BaseElement { 5 | private page: any; 6 | private selector: string; 7 | private currentElement: any; 8 | private name: string 9 | 10 | constructor({page, selector, name}: {page?: any, selector: string, name?: string}) { 11 | this.page = page; 12 | this.selector = selector 13 | this.name = name || BaseElement.name; 14 | 15 | pubsub.subscribe(messagesList.currentPage, this.initPage.bind(this)) 16 | } 17 | 18 | get id() { 19 | return this.name; 20 | } 21 | 22 | initPage(page) { 23 | this.page = page 24 | } 25 | 26 | private async initElement() { 27 | this.currentElement = await this.page.$(this.selector); 28 | } 29 | 30 | @step((name) => `${name} call send keys`) 31 | async sendKeys(value: string) { 32 | if (!this.currentElement) { 33 | await this.initElement(); 34 | } 35 | await this.currentElement.type(value); 36 | } 37 | 38 | @step((name) => `${name} call click`) 39 | async click() { 40 | if (!this.currentElement) { 41 | await this.initElement(); 42 | } 43 | await this.currentElement.click(); 44 | } 45 | } 46 | 47 | function _$(selector: string, name?: string): BaseElement { 48 | return new BaseElement({selector, name}) 49 | } 50 | 51 | export { 52 | _$, 53 | BaseElement 54 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-1/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './browser'; 2 | export * from './element'; 3 | export * from './base.page'; 4 | export * from './reporter'; 5 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-1/lib/pubsub.ts: -------------------------------------------------------------------------------- 1 | const messagesList = { 2 | currentPage: 'current_page', 3 | closeBrowser: 'close_browser' 4 | } 5 | 6 | function pubSub() { 7 | const handlers = {}; 8 | 9 | function publish(msgName: string, data: any) { 10 | if (!handlers[msgName]) { 11 | return; 12 | } 13 | handlers[msgName].forEach((handler) => { 14 | handler(data); 15 | }); 16 | } 17 | 18 | function subscribe(msgName: string, handler: Function) { 19 | if (!handlers[msgName]) { 20 | handlers[msgName] = []; 21 | } 22 | handlers[msgName].push(handler); 23 | } 24 | 25 | return { 26 | publish, 27 | subscribe 28 | } 29 | } 30 | 31 | const pubsub = pubSub(); 32 | 33 | export { 34 | pubsub, 35 | messagesList 36 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-1/lib/reporter/allure.ts: -------------------------------------------------------------------------------- 1 | declare const allure: any; 2 | 3 | function stepAllure(stepName: string | Function) { 4 | return function(_target, propName, descriptor) { 5 | const originalMethod = descriptor.value; 6 | 7 | descriptor.value = function(...args) { 8 | return allure.step(typeof stepName === 'function' ? stepName(this.id) : stepName, () => originalMethod.call(this, ...args)); 9 | } 10 | 11 | return descriptor 12 | } 13 | } 14 | 15 | export { 16 | stepAllure 17 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-1/lib/reporter/console.ts: -------------------------------------------------------------------------------- 1 | function stepConsole(stepName: string | Function) { 2 | return function(_target, propName, descriptor) { 3 | const originalMethod = descriptor.value; 4 | 5 | descriptor.value = function(...args) { 6 | console.log(typeof stepName === 'function' ? stepName(this.id) : stepName) 7 | 8 | return originalMethod.call(this, ...args); 9 | } 10 | 11 | return descriptor 12 | } 13 | } 14 | 15 | export { 16 | stepConsole 17 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-1/lib/reporter/index.ts: -------------------------------------------------------------------------------- 1 | import {stepAllure} from './allure'; 2 | import {stepConsole} from './console'; 3 | const {REPORTER} = process.env; 4 | 5 | 6 | function step(stepName) { 7 | if (REPORTER === 'allure') { 8 | return stepAllure(stepName); 9 | } else { 10 | return stepConsole(stepName); 11 | } 12 | } 13 | 14 | export { 15 | step 16 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livecoding-puppeteer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --require ts-node/register ./specs/**/*.spec.ts --timeout 25000", 8 | "test:allure": "REPORTER=allure mocha --require ts-node/register ./specs/**/*.spec.ts --timeout 25000 -R mocha-allure2-reporter", 9 | "build": "tsc" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@types/chai": "^4.2.11", 16 | "@types/mocha": "^7.0.2", 17 | "@types/node": "^14.0.22", 18 | "@types/puppeteer": "^3.0.1", 19 | "chai": "^4.2.0", 20 | "puppeteer": "^5.0.0", 21 | "ts-node": "^8.10.2", 22 | "typescript": "^3.9.6" 23 | }, 24 | "devDependencies": { 25 | "mocha": "^8.0.1", 26 | "mocha-allure2-reporter": "0.0.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-1/specs/login.spec.ts: -------------------------------------------------------------------------------- 1 | import {browser} from '../lib/browser'; 2 | import {_$} from '../lib/element'; 3 | import {MainPage} from '../framework/pages/main.page'; 4 | 5 | const mainPage = new MainPage(); 6 | 7 | describe('Main page', function() { 8 | beforeEach(async () => { 9 | await browser.goto('http://localhost:4000/'); 10 | }) 11 | afterEach(async () => { 12 | await browser.close(); 13 | }) 14 | 15 | it("Login", async function() { 16 | await mainPage.loginToSystem('admin', 'admin') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "sourceMap": true, 5 | "module": "commonjs", 6 | "outDir": "built", 7 | "experimentalDecorators": true, 8 | "types": [ 9 | "@types/puppeteer", 10 | "@types/mocha", 11 | "@types/chai", 12 | ] 13 | }, 14 | "include": [ 15 | "lib/**/*.ts", 16 | "specs/**/*.ts", 17 | "framework/**/*.ts" 18 | ] 19 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | built 4 | allure-results -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/framework/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './main.page'; 2 | export * from './table.page'; 3 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/framework/pages/main.page.ts: -------------------------------------------------------------------------------- 1 | import {BasePage, step, BaseElement} from '../../lib'; 2 | import {Input, Button} from '../../lib'; 3 | 4 | class MainPage extends BasePage { 5 | private userName: BaseElement; 6 | private password: BaseElement; 7 | private submit: BaseElement; 8 | 9 | constructor() { 10 | super('#main_page', 'Main page') 11 | this.userName = this.initChild(Input, 'input.form-control', 'User name field'); 12 | this.password = this.initChild(Input, 'input[type="password"]', 'Password field'); 13 | this.submit = this.initChild(Button, '.login_form .modal button', 'Login button'); 14 | } 15 | 16 | @step((name) => `${name} execute login to the system`) 17 | async loginToSystem(userName, password) { 18 | await this.userName.sendKeys(userName); 19 | await this.password.sendKeys(password); 20 | await this.submit.click() 21 | } 22 | } 23 | 24 | export { 25 | MainPage 26 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/framework/pages/table.page.ts: -------------------------------------------------------------------------------- 1 | import {BasePage, step} from '../../lib'; 2 | import {Text} from '../../lib'; 3 | 4 | class TablePage extends BasePage { 5 | private headerTitle: Text; 6 | 7 | constructor() { 8 | super('#table_page', 'Stern machine table page') 9 | this.headerTitle = this.initChild(Text, '.header h3', 'Header title'); 10 | } 11 | 12 | @step((name) => `${name} get header title`) 13 | async getHeaderTitle() { 14 | return this.headerTitle.get(); 15 | } 16 | } 17 | 18 | export { 19 | TablePage 20 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/lib/base.page.ts: -------------------------------------------------------------------------------- 1 | import {pubsub, messagesList} from './pubsub'; 2 | import {step} from './reporter/index'; 3 | 4 | class BasePage { 5 | private selector: string; 6 | private name: string; 7 | private page: any; 8 | 9 | constructor(selector, name) { 10 | this.selector = selector; 11 | this.name = name; 12 | 13 | pubsub.subscribe(messagesList.currentPage, this.initPage.bind(this)); 14 | pubsub.publish(messagesList.entityInit, this); 15 | } 16 | 17 | initPage(page) { 18 | this.page = page 19 | } 20 | 21 | get id() { 22 | return this.name; 23 | } 24 | 25 | 26 | @step((name) => `${name} init child`, true) 27 | initChild(childClass, childSelector, childName) { 28 | return new childClass({selector: childSelector, name: childName}) 29 | } 30 | } 31 | 32 | export { 33 | BasePage 34 | } 35 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/lib/browser.ts: -------------------------------------------------------------------------------- 1 | import {Page, Browser} from 'puppeteer'; 2 | import * as puppeteer from 'puppeteer'; 3 | import {pubsub, messagesList} from './pubsub'; 4 | 5 | const caps = { 6 | defaultViewport: { 7 | width: 1200, 8 | height: 800 9 | }, 10 | headless: false 11 | } 12 | 13 | class BrowserAddapter { 14 | private currentPage: Page; 15 | private browser: Browser; 16 | 17 | constructor() { 18 | pubsub.subscribe(messagesList.entityInit, this.initPageToRemoteContext.bind(this)) 19 | } 20 | 21 | initPageToRemoteContext(ctx) { 22 | ctx.initPage(this.currentPage) 23 | } 24 | 25 | async initCurrentPage() { 26 | this.browser = await puppeteer.launch(caps); 27 | this.currentPage = await this.browser.newPage(); 28 | 29 | pubsub.publish(messagesList.currentPage, this.currentPage); 30 | } 31 | 32 | async goto(url: string) { 33 | if (!this.currentPage) { 34 | await this.initCurrentPage(); 35 | } 36 | 37 | await this.currentPage.goto(url); 38 | } 39 | 40 | async close() { 41 | await this.browser.close(); 42 | 43 | pubsub.publish(messagesList.closeBrowser, this.currentPage); 44 | } 45 | 46 | async sleep(time = 1000) { 47 | await (() => new Promise(res => setTimeout(res, time)))(); 48 | } 49 | } 50 | 51 | 52 | const browser = new BrowserAddapter(); 53 | 54 | export { 55 | browser 56 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/lib/elements/button.ts: -------------------------------------------------------------------------------- 1 | import {BaseElement} from './element'; 2 | import {step} from '../reporter' 3 | 4 | class Button extends BaseElement { 5 | constructor({page, selector, name}: {page?: any, selector: string, name?: string}) { 6 | super({page, selector, name}); 7 | } 8 | 9 | @step((name) => `${name} call send keys`) 10 | async sendKeys(value: string) { 11 | throw new Error('Button can not fill any value'); 12 | } 13 | } 14 | 15 | export { 16 | Button 17 | } 18 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/lib/elements/element.ts: -------------------------------------------------------------------------------- 1 | import {pubsub, messagesList} from '../pubsub'; 2 | import {step} from '../reporter/index'; 3 | import {waits} from '../helpers' 4 | 5 | class BaseElement { 6 | private page: any; 7 | private selector: string; 8 | private currentElement: any; 9 | private name: string 10 | 11 | constructor({page, selector, name}: {page?: any, selector: string, name?: string}) { 12 | this.page = page; 13 | this.selector = selector 14 | this.name = name || BaseElement.name; 15 | 16 | pubsub.subscribe(messagesList.currentPage, this.initPage.bind(this)) 17 | pubsub.publish(messagesList.entityInit, this); 18 | } 19 | 20 | get id() { 21 | return this.name; 22 | } 23 | 24 | initPage(page) { 25 | this.page = page 26 | } 27 | 28 | private async initElement() { 29 | await waits.waitForVisible(this, 2500); 30 | this.currentElement = await this.page.$(this.selector); 31 | } 32 | 33 | @step((name) => `${name} call send keys`) 34 | async sendKeys(value: string) { 35 | if (!this.currentElement) { 36 | await this.initElement(); 37 | } 38 | await this.currentElement.type(value); 39 | } 40 | 41 | @step((name) => `${name} call get data`) 42 | async get() { 43 | if (!this.currentElement) { 44 | await this.initElement(); 45 | } 46 | return this.currentElement.evaluate(node => node.textContent); 47 | } 48 | 49 | @step((name) => `${name} call click`) 50 | async click() { 51 | if (!this.currentElement) { 52 | await this.initElement(); 53 | } 54 | await this.currentElement.click(); 55 | } 56 | } 57 | 58 | 59 | function _$(selector: string, name?: string): BaseElement { 60 | return new BaseElement({selector, name}) 61 | } 62 | 63 | 64 | export { 65 | _$, 66 | BaseElement 67 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/lib/elements/index.ts: -------------------------------------------------------------------------------- 1 | export * from './input'; 2 | export * from './button'; 3 | export * from './text'; 4 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/lib/elements/input.ts: -------------------------------------------------------------------------------- 1 | import {BaseElement} from './element'; 2 | // import {step} from '../reporter' 3 | 4 | class Input extends BaseElement { 5 | constructor({page, selector, name}: {page?: any, selector: string, name?: string}) { 6 | super({page, selector, name}); 7 | } 8 | } 9 | 10 | export { 11 | Input 12 | } 13 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/lib/elements/text.ts: -------------------------------------------------------------------------------- 1 | import {BaseElement} from './element'; 2 | // import {step} from '../reporter' 3 | 4 | class Text extends BaseElement { 5 | constructor({page, selector, name}: {page?: any, selector: string, name?: string}) { 6 | super({page, selector, name}); 7 | } 8 | } 9 | 10 | export { 11 | Text 12 | } 13 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/lib/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger'; 2 | export * from './waits'; 3 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/lib/helpers/logger.ts: -------------------------------------------------------------------------------- 1 | import * as chalk from 'chalk'; 2 | 3 | const logger = { 4 | tabGreen: (msg) => console.log(chalk.green(`\t${msg}`)), 5 | spaceGreen: (msg) => console.log(chalk.green(` ${msg}`)), 6 | spaceYellow: (msg) => console.log(chalk.yellow(` ${msg}`)) 7 | } 8 | 9 | export { 10 | logger 11 | } 12 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/lib/helpers/waits.ts: -------------------------------------------------------------------------------- 1 | import {logger} from './logger' 2 | const {TECH_INFO} = process.env; 3 | 4 | const waits = { 5 | waitForVisible: async (ctx, timeout) => { 6 | const {page, selector} = ctx; 7 | if (TECH_INFO) { 8 | logger.spaceYellow(`Wait ${selector} during ${timeout} ms`); 9 | } 10 | await page.waitForSelector(selector, {visible: true, timeout}) 11 | } 12 | } 13 | 14 | export { 15 | waits 16 | } 17 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './browser'; 2 | export * from './elements/element'; 3 | export * from './base.page'; 4 | export * from './reporter'; 5 | export * from './elements'; 6 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/lib/pubsub.ts: -------------------------------------------------------------------------------- 1 | const messagesList = { 2 | currentPage: 'current_page', 3 | closeBrowser: 'close_browser', 4 | entityInit: 'entity_initialization' 5 | } 6 | 7 | function pubSub() { 8 | const handlers = {}; 9 | 10 | function publish(msgName: string, data: any) { 11 | if (!handlers[msgName]) { 12 | return; 13 | } 14 | handlers[msgName].forEach((handler) => { 15 | handler(data); 16 | }); 17 | } 18 | 19 | function subscribe(msgName: string, handler: Function) { 20 | if (!handlers[msgName]) { 21 | handlers[msgName] = []; 22 | } 23 | handlers[msgName].push(handler); 24 | } 25 | 26 | return { 27 | publish, 28 | subscribe 29 | } 30 | } 31 | 32 | const pubsub = pubSub(); 33 | 34 | export { 35 | pubsub, 36 | messagesList 37 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/lib/reporter/allure.ts: -------------------------------------------------------------------------------- 1 | declare const allure: any; 2 | const {TECH_INFO} = process.env; 3 | 4 | function stepAllure(stepName: string | Function, isTechInfo: boolean = false) { 5 | return function(_target, propName, descriptor) { 6 | const originalMethod = descriptor.value; 7 | 8 | descriptor.value = function(...args) { 9 | 10 | if (isTechInfo && TECH_INFO) { 11 | return allure.step(typeof stepName === 'function' ? stepName(this.id) : stepName, () => originalMethod.call(this, ...args)); 12 | } else if (isTechInfo && !TECH_INFO) { 13 | return originalMethod.call(this, ...args); 14 | } 15 | return allure.step(typeof stepName === 'function' ? stepName(this.id) : stepName, () => originalMethod.call(this, ...args)); 16 | } 17 | 18 | return descriptor 19 | } 20 | } 21 | 22 | export { 23 | stepAllure 24 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/lib/reporter/console.ts: -------------------------------------------------------------------------------- 1 | import {logger} from '../helpers'; 2 | const {TECH_INFO} = process.env; 3 | 4 | function stepConsole(stepName: string | Function, isTechInfo: boolean = false) { 5 | return function(_target, propName, descriptor) { 6 | const originalMethod = descriptor.value; 7 | 8 | descriptor.value = function(...args) { 9 | const caller = _target.constructor.name.includes('Page') ? 'spaceGreen' : 'tabGreen' 10 | if (isTechInfo && TECH_INFO) { 11 | logger.spaceYellow(typeof stepName === 'function' ? stepName(this.id) : stepName) 12 | } else if (!isTechInfo) { 13 | logger[caller](typeof stepName === 'function' ? stepName(this.id) : stepName) 14 | } 15 | 16 | return originalMethod.call(this, ...args); 17 | } 18 | 19 | return descriptor 20 | } 21 | } 22 | 23 | export { 24 | stepConsole 25 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/lib/reporter/index.ts: -------------------------------------------------------------------------------- 1 | import {stepAllure} from './allure'; 2 | import {stepConsole} from './console'; 3 | const {REPORTER} = process.env; 4 | 5 | 6 | function step(stepName, isTechInfo = false) { 7 | if (REPORTER === 'allure') { 8 | return stepAllure(stepName, isTechInfo); 9 | } else { 10 | return stepConsole(stepName, isTechInfo); 11 | } 12 | } 13 | 14 | export { 15 | step 16 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livecoding-puppeteer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --require ts-node/register ./specs/**/*.spec.ts --timeout 25000", 8 | "test:allure": "REPORTER=allure mocha --require ts-node/register ./specs/**/*.spec.ts --timeout 25000 -R mocha-allure2-reporter", 9 | "test:tech:console": "TECH_INFO=1 mocha --require ts-node/register ./specs/**/*.spec.ts --timeout 25000", 10 | "build": "tsc" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@types/chai": "^4.2.11", 17 | "@types/mocha": "^7.0.2", 18 | "@types/node": "^14.0.22", 19 | "@types/puppeteer": "^3.0.1", 20 | "chai": "^4.2.0", 21 | "chalk": "^4.1.0", 22 | "puppeteer": "^5.0.0", 23 | "ts-node": "^8.10.2", 24 | "typescript": "^3.9.6" 25 | }, 26 | "devDependencies": { 27 | "mocha": "^8.0.1", 28 | "mocha-allure2-reporter": "0.0.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/specs/login.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {browser} from '../lib/browser'; 3 | import {MainPage, TablePage} from '../framework/pages'; 4 | 5 | describe('Main page', function() { 6 | 7 | beforeEach(async () => { 8 | await browser.goto('http://localhost:4000/'); 9 | }) 10 | 11 | afterEach(async () => { 12 | await browser.close(); 13 | }) 14 | 15 | it("Login", async function() { 16 | const mainPage = new MainPage(); 17 | const tablePage = new TablePage(); 18 | await mainPage.loginToSystem('admin', 'admin'); 19 | expect(await tablePage.getHeaderTitle()).to.includes('admin'); 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "sourceMap": true, 5 | "module": "commonjs", 6 | "outDir": "built", 7 | "experimentalDecorators": true, 8 | "types": [ 9 | "@types/puppeteer", 10 | "@types/mocha", 11 | "@types/chai", 12 | ] 13 | }, 14 | "include": [ 15 | "lib/**/*.ts", 16 | "specs/**/*.ts", 17 | "framework/**/*.ts" 18 | ] 19 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | built 4 | allure-results -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/framework/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './main.page'; 2 | export * from './table.page'; 3 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/framework/pages/main.page.ts: -------------------------------------------------------------------------------- 1 | import {BasePage, step, BaseElement} from '../../lib'; 2 | import {Input, Button} from '../../lib'; 3 | 4 | class MainPage extends BasePage { 5 | private userName: BaseElement; 6 | private password: BaseElement; 7 | private submit: BaseElement; 8 | 9 | constructor() { 10 | super('#main_page', 'Main page') 11 | this.userName = this.initChild(Input, 'input.form-control', 'User name field'); 12 | this.password = this.initChild(Input, 'input[type="password"]', 'Password field'); 13 | this.submit = this.initChild(Button, '.login_form .modal button', 'Login button'); 14 | } 15 | 16 | @step((name) => `${name} execute login to the system`) 17 | async loginToSystem(userName, password) { 18 | await this.userName.sendKeys(userName); 19 | await this.password.sendKeys(password); 20 | await this.submit.click() 21 | } 22 | } 23 | 24 | export { 25 | MainPage 26 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/framework/pages/table.page.ts: -------------------------------------------------------------------------------- 1 | import {BasePage, step} from '../../lib'; 2 | import {Text, Input, Button, Table} from '../../lib'; 3 | 4 | const transformData = { 5 | 'Виробник': 'manufacturer', 6 | "Робочий об'єм, М": 'volume', 7 | 'Довжина, М': 'length', 8 | 'Ширина, М': 'width', 9 | 'Маса, КГ': 'weigth', 10 | 'Потужність трактора, кВт': 'power', 11 | 'Ціна, грн': 'price' 12 | } 13 | 14 | class TablePage extends BasePage { 15 | private headerTitle: Text; 16 | private newMachineManufacturer: Input; 17 | private newMachineVolume: Input; 18 | private newMachineLength: Input; 19 | private newMachinePrice: Input; 20 | private newMachineWidth: Input; 21 | private newMachineWeigth: Input; 22 | private newMachinePower: Input; 23 | private submitNewMachine: Button; 24 | private machineTable: Table; 25 | 26 | constructor() { 27 | super('#table_page', 'Stern machine table page') 28 | this.headerTitle = this.initChild(Text, '.header h3', 'Header title'); 29 | this.machineTable = this.initChild(Table, 'table.machines_list', 'Stern machines table'); 30 | // init transform 31 | this.machineTable.transformer = transformData; 32 | this.newMachineManufacturer = this.initChild(Input, '.add_machine tr td:nth-child(1) input', 'Manufacturer field'); 33 | this.newMachineVolume = this.initChild(Input, '.add_machine tr td:nth-child(2) input', 'Volume field'); 34 | this.newMachineLength = this.initChild(Input, '.add_machine tr td:nth-child(3) input', 'Length field'); 35 | this.newMachineWidth = this.initChild(Input, '.add_machine tr td:nth-child(4) input', 'Width field'); 36 | this.newMachineWeigth = this.initChild(Input, '.add_machine tr td:nth-child(5) input', 'Weigth field'); 37 | this.newMachinePower = this.initChild(Input, '.add_machine tr td:nth-child(6) input', 'Power field'); 38 | this.newMachinePrice = this.initChild(Input, '.add_machine tr td:nth-child(7) input', 'Price'); 39 | this.submitNewMachine = this.initChild(Button, '.add_machine button', 'Add new machine button'); 40 | } 41 | 42 | @step((name) => `${name} get header title`) 43 | async getHeaderTitle() { 44 | return this.headerTitle.get(); 45 | } 46 | 47 | @step((name) => `${name} get header title`) 48 | async addNewMachine({manufacturer, volume, length, weigth, width, power, price}) { 49 | await this.newMachineManufacturer.sendKeys(manufacturer); 50 | await this.newMachineVolume.sendKeys(volume); 51 | await this.newMachineLength.sendKeys(length); 52 | await this.newMachineWidth.sendKeys(width); 53 | await this.newMachineWeigth.sendKeys(weigth); 54 | await this.newMachinePower.sendKeys(power); 55 | await this.newMachinePrice.sendKeys(price); 56 | await this.submitNewMachine.click(); 57 | } 58 | 59 | @step((name) => `${name} get machines list`) 60 | async getMachinesList() { 61 | return this.machineTable.get(); 62 | } 63 | } 64 | 65 | export { 66 | TablePage 67 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/base.page.ts: -------------------------------------------------------------------------------- 1 | import {pubsub, messagesList} from './pubsub'; 2 | import {step} from './reporter/index'; 3 | 4 | class BasePage { 5 | private selector: string; 6 | private name: string; 7 | private page: any; 8 | 9 | constructor(selector, name) { 10 | this.selector = selector; 11 | this.name = name; 12 | 13 | pubsub.subscribe(messagesList.currentPage, this.initPage.bind(this)); 14 | pubsub.publish(messagesList.entityInit, this); 15 | } 16 | 17 | initPage(page) { 18 | this.page = page 19 | } 20 | 21 | get id() { 22 | return this.name; 23 | } 24 | 25 | 26 | @step((name) => `${name} init child`, true) 27 | initChild(childClass, childSelector, childName) { 28 | return new childClass({selector: childSelector, name: childName}) 29 | } 30 | } 31 | 32 | export { 33 | BasePage 34 | } 35 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/browser.ts: -------------------------------------------------------------------------------- 1 | import {Page, Browser} from 'puppeteer'; 2 | import * as puppeteer from 'puppeteer'; 3 | import {pubsub, messagesList} from './pubsub'; 4 | 5 | const caps = { 6 | defaultViewport: { 7 | width: 1200, 8 | height: 800 9 | }, 10 | headless: false 11 | } 12 | 13 | class BrowserAddapter { 14 | private currentPage: Page; 15 | private browser: Browser; 16 | 17 | constructor() { 18 | pubsub.subscribe(messagesList.entityInit, this.initPageToRemoteContext.bind(this)) 19 | } 20 | 21 | initPageToRemoteContext(ctx) { 22 | ctx.initPage(this.currentPage) 23 | } 24 | 25 | async initCurrentPage() { 26 | this.browser = await puppeteer.launch(caps); 27 | this.currentPage = await this.browser.newPage(); 28 | 29 | pubsub.publish(messagesList.currentPage, this.currentPage); 30 | } 31 | 32 | async goto(url: string) { 33 | if (!this.currentPage) { 34 | await this.initCurrentPage(); 35 | } 36 | 37 | await this.currentPage.goto(url); 38 | } 39 | 40 | async close() { 41 | await this.browser.close(); 42 | 43 | pubsub.publish(messagesList.closeBrowser, this.currentPage); 44 | } 45 | 46 | async sleep(time = 1000) { 47 | await (() => new Promise(res => setTimeout(res, time)))(); 48 | } 49 | } 50 | 51 | 52 | const browser = new BrowserAddapter(); 53 | 54 | export { 55 | browser 56 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/elements/button.ts: -------------------------------------------------------------------------------- 1 | import {BaseElement} from './element'; 2 | import {step} from '../reporter' 3 | 4 | class Button extends BaseElement { 5 | constructor({page, selector, name}: {page?: any, selector: string, name?: string}) { 6 | super({page, selector, name}); 7 | } 8 | 9 | @step((name) => `${name} call send keys`) 10 | async sendKeys(value: string) { 11 | throw new Error('Button can not fill any value'); 12 | } 13 | } 14 | 15 | export { 16 | Button 17 | } 18 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/elements/element.ts: -------------------------------------------------------------------------------- 1 | import {pubsub, messagesList} from '../pubsub'; 2 | import {step} from '../reporter/index'; 3 | import {waits} from '../helpers' 4 | 5 | class BaseElement { 6 | private page: any; 7 | private selector: string; 8 | protected currentElement: any; 9 | private name: string 10 | 11 | constructor({page, selector, name}: {page?: any, selector: string, name?: string}) { 12 | this.page = page; 13 | this.selector = selector 14 | this.name = name || BaseElement.name; 15 | 16 | pubsub.subscribe(messagesList.currentPage, this.initPage.bind(this)) 17 | pubsub.publish(messagesList.entityInit, this); 18 | } 19 | 20 | get id() { 21 | return this.name; 22 | } 23 | 24 | initPage(page) { 25 | this.page = page 26 | } 27 | 28 | protected async initElement() { 29 | await waits.waitForVisible(this, 2500); 30 | this.currentElement = await this.page.$(this.selector); 31 | } 32 | 33 | @step((name) => `${name} call send keys`) 34 | async sendKeys(value: string) { 35 | if (!this.currentElement) { 36 | await this.initElement(); 37 | } 38 | await this.currentElement.type(value); 39 | } 40 | 41 | @step((name) => `${name} call get data`) 42 | async get() { 43 | if (!this.currentElement) { 44 | await this.initElement(); 45 | } 46 | return this.currentElement.evaluate(node => node.textContent); 47 | } 48 | 49 | @step((name) => `${name} call click`) 50 | async click() { 51 | if (!this.currentElement) { 52 | await this.initElement(); 53 | } 54 | await this.currentElement.click(); 55 | } 56 | } 57 | 58 | 59 | function _$(selector: string, name?: string): BaseElement { 60 | return new BaseElement({selector, name}) 61 | } 62 | 63 | 64 | export { 65 | _$, 66 | BaseElement 67 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/elements/index.ts: -------------------------------------------------------------------------------- 1 | export * from './input'; 2 | export * from './button'; 3 | export * from './text'; 4 | export * from './table'; 5 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/elements/input.ts: -------------------------------------------------------------------------------- 1 | import {BaseElement} from './element'; 2 | // import {step} from '../reporter' 3 | 4 | class Input extends BaseElement { 5 | constructor({page, selector, name}: {page?: any, selector: string, name?: string}) { 6 | super({page, selector, name}); 7 | } 8 | } 9 | 10 | export { 11 | Input 12 | } 13 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/elements/table.ts: -------------------------------------------------------------------------------- 1 | import {BaseElement} from './element'; 2 | import {step} from '../reporter' 3 | 4 | class Table extends BaseElement { 5 | private transformData: object; 6 | constructor({page, selector, name}: {page?: any, selector: string, name?: string}) { 7 | // TODO selector should include all table 8 | // TABLE ROOT! 9 | super({page, selector, name}); 10 | } 11 | 12 | set transformer(dataObj) { 13 | this.transformData = dataObj; 14 | } 15 | 16 | @step((name) => `${name} call get data`) 17 | async get() { 18 | const headerMap = await this.getHeaderMap(); 19 | const data = await this.getBodyData(headerMap); 20 | if (this.transformData) { 21 | return this.transfromInutualData(data) 22 | } 23 | return data; 24 | } 25 | 26 | private transfromInutualData(data) { 27 | const transformKeys = Object.keys(this.transformData) 28 | return data.map((dataItem) => { 29 | const newDataItem = {} 30 | for (const key of transformKeys) { 31 | newDataItem[this.transformData[key]] = dataItem[key] 32 | } 33 | return newDataItem; 34 | }) 35 | } 36 | 37 | 38 | private async getBodyData(neaderMap) { 39 | const data = []; 40 | const bodyRows = await this.currentElement.$$('table.machines_list tbody tr') 41 | for (const row of bodyRows) { 42 | data.push(await this.getRowData(row, neaderMap)); 43 | } 44 | return data 45 | } 46 | 47 | private async getRowData(row, headerMap) { 48 | const rowCells = await row.$$('td'); 49 | const rowData = {} 50 | for (const key of Object.keys(headerMap)) { 51 | rowData[key] = await rowCells[headerMap[key]].evaluate(node => node.textContent.trim()) 52 | } 53 | return rowData; 54 | } 55 | 56 | private async getHeaderMap() { 57 | if (!this.currentElement) { 58 | await this.initElement(); 59 | } 60 | const headerMap = {}; 61 | const headerCells = await this.currentElement.$$('thead td') 62 | for (let i = 0; i < headerCells.length; i++) { 63 | headerMap[await headerCells[i].evaluate(node => node.textContent.trim())] = i 64 | } 65 | 66 | return headerMap; 67 | } 68 | } 69 | 70 | export { 71 | Table 72 | } 73 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/elements/text.ts: -------------------------------------------------------------------------------- 1 | import {BaseElement} from './element'; 2 | // import {step} from '../reporter' 3 | 4 | class Text extends BaseElement { 5 | constructor({page, selector, name}: {page?: any, selector: string, name?: string}) { 6 | super({page, selector, name}); 7 | } 8 | } 9 | 10 | export { 11 | Text 12 | } 13 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger'; 2 | export * from './waits'; 3 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/helpers/logger.ts: -------------------------------------------------------------------------------- 1 | import * as chalk from 'chalk'; 2 | 3 | const logger = { 4 | tabGreen: (msg) => console.log(chalk.green(`\t${msg}`)), 5 | spaceGreen: (msg) => console.log(chalk.green(` ${msg}`)), 6 | spaceYellow: (msg) => console.log(chalk.yellow(` ${msg}`)) 7 | } 8 | 9 | export { 10 | logger 11 | } 12 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/helpers/waits.ts: -------------------------------------------------------------------------------- 1 | import {logger} from './logger' 2 | const {TECH_INFO} = process.env; 3 | 4 | const waits = { 5 | waitForVisible: async (ctx, timeout) => { 6 | const {page, selector} = ctx; 7 | if (TECH_INFO) { 8 | logger.spaceYellow(`Wait ${selector} during ${timeout} ms`); 9 | } 10 | await page.waitForSelector(selector, {visible: true, timeout}) 11 | } 12 | } 13 | 14 | export { 15 | waits 16 | } 17 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './browser'; 2 | export * from './elements/element'; 3 | export * from './base.page'; 4 | export * from './reporter'; 5 | export * from './elements'; 6 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/pubsub.ts: -------------------------------------------------------------------------------- 1 | const messagesList = { 2 | currentPage: 'current_page', 3 | closeBrowser: 'close_browser', 4 | entityInit: 'entity_initialization' 5 | } 6 | 7 | function pubSub() { 8 | const handlers = {}; 9 | 10 | function publish(msgName: string, data: any) { 11 | if (!handlers[msgName]) { 12 | return; 13 | } 14 | handlers[msgName].forEach((handler) => { 15 | handler(data); 16 | }); 17 | } 18 | 19 | function subscribe(msgName: string, handler: Function) { 20 | if (!handlers[msgName]) { 21 | handlers[msgName] = []; 22 | } 23 | handlers[msgName].push(handler); 24 | } 25 | 26 | return { 27 | publish, 28 | subscribe 29 | } 30 | } 31 | 32 | const pubsub = pubSub(); 33 | 34 | export { 35 | pubsub, 36 | messagesList 37 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/reporter/allure.ts: -------------------------------------------------------------------------------- 1 | declare const allure: any; 2 | const {TECH_INFO} = process.env; 3 | 4 | function stepAllure(stepName: string | Function, isTechInfo: boolean = false) { 5 | return function(_target, propName, descriptor) { 6 | const originalMethod = descriptor.value; 7 | 8 | descriptor.value = function(...args) { 9 | 10 | if (isTechInfo && TECH_INFO) { 11 | return allure.step(typeof stepName === 'function' ? stepName(this.id) : stepName, () => originalMethod.call(this, ...args)); 12 | } else if (isTechInfo && !TECH_INFO) { 13 | return originalMethod.call(this, ...args); 14 | } 15 | return allure.step(typeof stepName === 'function' ? stepName(this.id) : stepName, () => originalMethod.call(this, ...args)); 16 | } 17 | 18 | return descriptor 19 | } 20 | } 21 | 22 | export { 23 | stepAllure 24 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/reporter/console.ts: -------------------------------------------------------------------------------- 1 | import {logger} from '../helpers'; 2 | const {TECH_INFO} = process.env; 3 | 4 | function stepConsole(stepName: string | Function, isTechInfo: boolean = false) { 5 | return function(_target, propName, descriptor) { 6 | const originalMethod = descriptor.value; 7 | 8 | descriptor.value = function(...args) { 9 | const caller = _target.constructor.name.includes('Page') ? 'spaceGreen' : 'tabGreen' 10 | const isElement = _target.constructor.name.includes('Element') 11 | if (isTechInfo && TECH_INFO) { 12 | logger.spaceYellow(typeof stepName === 'function' ? stepName(this.id) : stepName) 13 | } else if (!isTechInfo) { 14 | const endMessage = isElement && args.length 15 | ? `${typeof stepName === 'function' ? stepName(this.id) : stepName} args: ${JSON.stringify(args)}` 16 | : `${typeof stepName === 'function' ? stepName(this.id) : stepName}` 17 | logger[caller](endMessage) 18 | } 19 | 20 | return originalMethod.call(this, ...args); 21 | } 22 | 23 | return descriptor 24 | } 25 | } 26 | 27 | export { 28 | stepConsole 29 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/lib/reporter/index.ts: -------------------------------------------------------------------------------- 1 | import {stepAllure} from './allure'; 2 | import {stepConsole} from './console'; 3 | const {REPORTER} = process.env; 4 | 5 | 6 | function step(stepName, isTechInfo = false) { 7 | if (REPORTER === 'allure') { 8 | return stepAllure(stepName, isTechInfo); 9 | } else { 10 | return stepConsole(stepName, isTechInfo); 11 | } 12 | } 13 | 14 | export { 15 | step 16 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livecoding-puppeteer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --require ts-node/register ./specs/**/*.spec.ts --timeout 25000", 8 | "test:allure": "REPORTER=allure mocha --require ts-node/register ./specs/**/*.spec.ts --timeout 25000 -R mocha-allure2-reporter", 9 | "test:tech:console": "TECH_INFO=1 mocha --require ts-node/register ./specs/**/*.spec.ts --timeout 25000", 10 | "build": "tsc" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@types/chai": "^4.2.11", 17 | "@types/mocha": "^7.0.2", 18 | "@types/node": "^14.0.22", 19 | "@types/puppeteer": "^3.0.1", 20 | "chai": "^4.2.0", 21 | "chalk": "^4.1.0", 22 | "puppeteer": "^5.0.0", 23 | "ts-node": "^8.10.2", 24 | "typescript": "^3.9.6" 25 | }, 26 | "devDependencies": { 27 | "mocha": "^8.0.1", 28 | "mocha-allure2-reporter": "0.0.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/specs/login.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {browser} from '../lib/browser'; 3 | import {MainPage, TablePage} from '../framework/pages'; 4 | 5 | describe('Main page', function() { 6 | 7 | beforeEach(async () => { 8 | await browser.goto('http://localhost:4000/'); 9 | }) 10 | 11 | afterEach(async () => { 12 | await browser.close(); 13 | }) 14 | 15 | it("Login", async function() { 16 | const mainPage = new MainPage(); 17 | const tablePage = new TablePage(); 18 | await mainPage.loginToSystem('admin', 'admin'); 19 | expect(await tablePage.getHeaderTitle()).to.includes('admin'); 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/specs/machine.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {browser} from '../lib/browser'; 3 | import {MainPage, TablePage} from '../framework/pages'; 4 | 5 | describe('Machines page', function() { 6 | 7 | beforeEach(async () => { 8 | await browser.goto('http://localhost:4000/'); 9 | }) 10 | 11 | afterEach(async () => { 12 | await browser.close(); 13 | }) 14 | 15 | it("Add new machine", async function() { 16 | const mainPage = new MainPage(); 17 | const tablePage = new TablePage(); 18 | await mainPage.loginToSystem('admin', 'admin'); 19 | const newMachine = { 20 | manufacturer: 'testvalue', 21 | volume: 'testvalue', 22 | length: 'testvalue', 23 | weigth: 'testvalue', 24 | width: 'testvalue', 25 | power: 'testvalue', 26 | price: 'testvalue' 27 | } 28 | await tablePage.addNewMachine(newMachine) 29 | const data = await tablePage.getMachinesList(); 30 | const requiredAddedMachine = data.find(({manufacturer, volume}) => { 31 | return newMachine.manufacturer === manufacturer && newMachine.volume === volume 32 | }); 33 | 34 | expect(newMachine).to.deep.equal(requiredAddedMachine); 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "sourceMap": true, 5 | "module": "commonjs", 6 | "outDir": "built", 7 | "experimentalDecorators": true, 8 | "types": [ 9 | "@types/puppeteer", 10 | "@types/mocha", 11 | "@types/chai", 12 | ] 13 | }, 14 | "include": [ 15 | "lib/**/*.ts", 16 | "specs/**/*.ts", 17 | "framework/**/*.ts" 18 | ] 19 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | built 4 | allure-results -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/execution/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {getReruner, getFilesList} = require('process-rerun'); 3 | 4 | const specFilesList = getFilesList(path.resolve(process.cwd(), 'specs')) 5 | 6 | const formCommand = (filePath) => { 7 | return `REPORTER=allure mocha ${filePath} --require ts-node/register --timeout 25000 -R mocha-allure2-reporter` 8 | } 9 | 10 | const commands = specFilesList.map(formCommand) 11 | 12 | 13 | const execute = async () => { 14 | 15 | const runner = getReruner({ 16 | longestProcessTime: 25 * 1000, 17 | maxSessionCount: 10, 18 | debugProcess: true, 19 | attemptsCount: 5, 20 | stackAnalize: () => true, 21 | pollTime: 50 22 | }); 23 | 24 | const result = await runner(commands); 25 | } 26 | 27 | execute() -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/framework/index.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {pagesProvider} from './pages'; 3 | import {it, browser} from '../lib' 4 | 5 | const provider = { 6 | // pages from pages folder 7 | ...pagesProvider, 8 | 9 | test: () => { 10 | return {it} 11 | }, 12 | pages: () => { 13 | return { 14 | expect, 15 | browser 16 | } 17 | } 18 | } 19 | 20 | export { 21 | provider 22 | } 23 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/framework/pages/index.ts: -------------------------------------------------------------------------------- 1 | import {MainPage} from './main.page'; 2 | import {TablePage} from './table.page'; 3 | import {initSingleton} from '../../lib' 4 | 5 | const pagesProvider = { 6 | main: () => initSingleton(MainPage), 7 | table: () => initSingleton(TablePage) 8 | } 9 | 10 | export { 11 | pagesProvider 12 | } 13 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/framework/pages/main.page.ts: -------------------------------------------------------------------------------- 1 | import {BasePage, step, BaseElement} from '../../lib'; 2 | import {Input, Button} from '../../lib'; 3 | 4 | class MainPage extends BasePage { 5 | private userName: BaseElement; 6 | private password: BaseElement; 7 | private submit: BaseElement; 8 | 9 | constructor() { 10 | super('#main_page', 'Main page') 11 | this.userName = this.initChild(Input, 'input.form-control', 'User name field'); 12 | this.password = this.initChild(Input, 'input[type="password"]', 'Password field'); 13 | this.submit = this.initChild(Button, '.login_form .modal button', 'Login button'); 14 | } 15 | 16 | @step((name) => `${name} execute login to the system`) 17 | async loginToSystem(userName, password) { 18 | await this.userName.sendKeys(userName); 19 | await this.password.sendKeys(password); 20 | await this.submit.click() 21 | } 22 | } 23 | 24 | export { 25 | MainPage 26 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/framework/pages/table.page.ts: -------------------------------------------------------------------------------- 1 | import {BasePage, step} from '../../lib'; 2 | import {Text, Input, Button, Table} from '../../lib'; 3 | 4 | const transformData = { 5 | 'Виробник': 'manufacturer', 6 | "Робочий об'єм, М": 'volume', 7 | 'Довжина, М': 'length', 8 | 'Ширина, М': 'width', 9 | 'Маса, КГ': 'weigth', 10 | 'Потужність трактора, кВт': 'power', 11 | 'Ціна, грн': 'price' 12 | } 13 | 14 | class TablePage extends BasePage { 15 | private headerTitle: Text; 16 | private newMachineManufacturer: Input; 17 | private newMachineVolume: Input; 18 | private newMachineLength: Input; 19 | private newMachinePrice: Input; 20 | private newMachineWidth: Input; 21 | private newMachineWeigth: Input; 22 | private newMachinePower: Input; 23 | private submitNewMachine: Button; 24 | private machineTable: Table; 25 | 26 | constructor() { 27 | super('#table_page', 'Stern machine table page') 28 | this.headerTitle = this.initChild(Text, '.header h3', 'Header title'); 29 | this.machineTable = this.initChild(Table, 'table.machines_list', 'Stern machines table'); 30 | // init transform 31 | this.machineTable.transformer = transformData; 32 | this.newMachineManufacturer = this.initChild(Input, '.add_machine tr td:nth-child(1) input', 'Manufacturer field'); 33 | this.newMachineVolume = this.initChild(Input, '.add_machine tr td:nth-child(2) input', 'Volume field'); 34 | this.newMachineLength = this.initChild(Input, '.add_machine tr td:nth-child(3) input', 'Length field'); 35 | this.newMachineWidth = this.initChild(Input, '.add_machine tr td:nth-child(4) input', 'Width field'); 36 | this.newMachineWeigth = this.initChild(Input, '.add_machine tr td:nth-child(5) input', 'Weigth field'); 37 | this.newMachinePower = this.initChild(Input, '.add_machine tr td:nth-child(6) input', 'Power field'); 38 | this.newMachinePrice = this.initChild(Input, '.add_machine tr td:nth-child(7) input', 'Price'); 39 | this.submitNewMachine = this.initChild(Button, '.add_machine button', 'Add new machine button'); 40 | } 41 | 42 | @step((name) => `${name} get header title`) 43 | async getHeaderTitle() { 44 | return this.headerTitle.get(); 45 | } 46 | 47 | @step((name) => `${name} get header title`) 48 | async addNewMachine({manufacturer, volume, length, weigth, width, power, price}) { 49 | await this.newMachineManufacturer.sendKeys(manufacturer); 50 | await this.newMachineVolume.sendKeys(volume); 51 | await this.newMachineLength.sendKeys(length); 52 | await this.newMachineWidth.sendKeys(width); 53 | await this.newMachineWeigth.sendKeys(weigth); 54 | await this.newMachinePower.sendKeys(power); 55 | await this.newMachinePrice.sendKeys(price); 56 | await this.submitNewMachine.click(); 57 | } 58 | 59 | @step((name) => `${name} get machines list`) 60 | async getMachinesList() { 61 | return this.machineTable.get(); 62 | } 63 | } 64 | 65 | export { 66 | TablePage 67 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/base.page.ts: -------------------------------------------------------------------------------- 1 | import {pubsub, messagesList} from './pubsub'; 2 | import {step} from './reporter/index'; 3 | 4 | class BasePage { 5 | private selector: string; 6 | private name: string; 7 | private page: any; 8 | 9 | constructor(selector, name) { 10 | this.selector = selector; 11 | this.name = name; 12 | 13 | pubsub.subscribe(messagesList.currentPage, this.initPage.bind(this)); 14 | pubsub.publish(messagesList.entityInit, this); 15 | } 16 | 17 | initPage(page) { 18 | this.page = page 19 | } 20 | 21 | get id() { 22 | return this.name; 23 | } 24 | 25 | 26 | @step((name) => `${name} init child`, true) 27 | initChild(childClass, childSelector, childName) { 28 | return new childClass({selector: childSelector, name: childName}) 29 | } 30 | } 31 | 32 | export { 33 | BasePage 34 | } 35 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/browser.ts: -------------------------------------------------------------------------------- 1 | import {Page, Browser} from 'puppeteer'; 2 | import * as puppeteer from 'puppeteer'; 3 | import {pubsub, messagesList} from './pubsub'; 4 | import * as fs from 'fs'; 5 | 6 | const caps = { 7 | defaultViewport: { 8 | width: 1200, 9 | height: 800 10 | }, 11 | headless: false 12 | } 13 | 14 | class BrowserAddapter { 15 | private currentPage: Page; 16 | private browser: Browser; 17 | 18 | constructor() { 19 | pubsub.subscribe(messagesList.entityInit, this.initPageToRemoteContext.bind(this)) 20 | } 21 | 22 | initPageToRemoteContext(ctx) { 23 | ctx.initPage(this.currentPage) 24 | } 25 | 26 | async initCurrentPage() { 27 | this.browser = await puppeteer.launch(caps); 28 | this.currentPage = await this.browser.newPage(); 29 | 30 | pubsub.publish(messagesList.currentPage, this.currentPage); 31 | } 32 | 33 | async goto(url: string) { 34 | if (!this.currentPage) { 35 | await this.initCurrentPage(); 36 | } 37 | 38 | await this.currentPage.goto(url); 39 | } 40 | 41 | async close() { 42 | await this.browser.close(); 43 | 44 | this.browser = null; 45 | this.currentPage = null; 46 | 47 | pubsub.publish(messagesList.closeBrowser, this.currentPage); 48 | } 49 | 50 | async takeScreenshot(pathTo) { 51 | await this.currentPage.screenshot({path: pathTo}); 52 | const screenData = fs.readFileSync(pathTo); 53 | fs.unlinkSync(pathTo); 54 | return screenData; 55 | } 56 | 57 | async sleep(time = 1000) { 58 | await (() => new Promise(res => setTimeout(res, time)))(); 59 | } 60 | } 61 | 62 | const browser = new BrowserAddapter(); 63 | 64 | export { 65 | browser 66 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/elements/button.ts: -------------------------------------------------------------------------------- 1 | import {BaseElement} from './element'; 2 | import {step} from '../reporter' 3 | 4 | class Button extends BaseElement { 5 | constructor({page, selector, name}: {page?: any, selector: string, name?: string}) { 6 | super({page, selector, name}); 7 | } 8 | 9 | @step((name) => `${name} call send keys`) 10 | async sendKeys(value: string) { 11 | throw new Error('Button can not fill any value'); 12 | } 13 | } 14 | 15 | export { 16 | Button 17 | } 18 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/elements/element.ts: -------------------------------------------------------------------------------- 1 | import {pubsub, messagesList} from '../pubsub'; 2 | import {step} from '../reporter/index'; 3 | import {waits} from '../helpers' 4 | 5 | class BaseElement { 6 | private page: any; 7 | private selector: string; 8 | protected currentElement: any; 9 | private name: string 10 | 11 | constructor({page, selector, name}: {page?: any, selector: string, name?: string}) { 12 | this.page = page; 13 | this.selector = selector 14 | this.name = name || BaseElement.name; 15 | 16 | pubsub.subscribe(messagesList.currentPage, this.initPage.bind(this)) 17 | pubsub.publish(messagesList.entityInit, this); 18 | } 19 | 20 | get id() { 21 | return this.name; 22 | } 23 | 24 | initPage(page) { 25 | this.currentElement = null; 26 | this.page = page; 27 | } 28 | 29 | protected async initElement() { 30 | await waits.waitForVisible(this, 2500); 31 | this.currentElement = await this.page.$(this.selector); 32 | } 33 | 34 | @step((name) => `${name} call send keys`) 35 | async sendKeys(value: string) { 36 | if (!this.currentElement) { 37 | await this.initElement(); 38 | } 39 | await this.currentElement.type(value); 40 | } 41 | 42 | @step((name) => `${name} call get data`) 43 | async get() { 44 | if (!this.currentElement) { 45 | await this.initElement(); 46 | } 47 | return this.currentElement.evaluate(node => node.textContent); 48 | } 49 | 50 | @step((name) => `${name} call click`) 51 | async click() { 52 | if (!this.currentElement) { 53 | await this.initElement(); 54 | } 55 | await this.currentElement.click(); 56 | } 57 | } 58 | 59 | 60 | function _$(selector: string, name?: string): BaseElement { 61 | return new BaseElement({selector, name}) 62 | } 63 | 64 | 65 | export { 66 | _$, 67 | BaseElement 68 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/elements/index.ts: -------------------------------------------------------------------------------- 1 | export * from './input'; 2 | export * from './button'; 3 | export * from './text'; 4 | export * from './table'; 5 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/elements/input.ts: -------------------------------------------------------------------------------- 1 | import {BaseElement} from './element'; 2 | // import {step} from '../reporter' 3 | 4 | class Input extends BaseElement { 5 | constructor({page, selector, name}: {page?: any, selector: string, name?: string}) { 6 | super({page, selector, name}); 7 | } 8 | } 9 | 10 | export { 11 | Input 12 | } 13 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/elements/table.ts: -------------------------------------------------------------------------------- 1 | import {BaseElement} from './element'; 2 | import {step} from '../reporter' 3 | 4 | class Table extends BaseElement { 5 | private transformData: object; 6 | constructor({page, selector, name}: {page?: any, selector: string, name?: string}) { 7 | // TODO selector should include all table 8 | // TABLE ROOT! 9 | super({page, selector, name}); 10 | } 11 | 12 | set transformer(dataObj) { 13 | this.transformData = dataObj; 14 | } 15 | 16 | @step((name) => `${name} call get data`) 17 | async get() { 18 | const headerMap = await this.getHeaderMap(); 19 | const data = await this.getBodyData(headerMap); 20 | if (this.transformData) { 21 | return this.transfromInutualData(data) 22 | } 23 | return data; 24 | } 25 | 26 | private transfromInutualData(data) { 27 | const transformKeys = Object.keys(this.transformData) 28 | return data.map((dataItem) => { 29 | const newDataItem = {} 30 | for (const key of transformKeys) { 31 | newDataItem[this.transformData[key]] = dataItem[key] 32 | } 33 | return newDataItem; 34 | }) 35 | } 36 | 37 | 38 | private async getBodyData(neaderMap) { 39 | const data = []; 40 | const bodyRows = await this.currentElement.$$('table.machines_list tbody tr') 41 | for (const row of bodyRows) { 42 | data.push(await this.getRowData(row, neaderMap)); 43 | } 44 | return data 45 | } 46 | 47 | private async getRowData(row, headerMap) { 48 | const rowCells = await row.$$('td'); 49 | const rowData = {} 50 | for (const key of Object.keys(headerMap)) { 51 | rowData[key] = await rowCells[headerMap[key]].evaluate(node => node.textContent.trim()) 52 | } 53 | return rowData; 54 | } 55 | 56 | private async getHeaderMap() { 57 | if (!this.currentElement) { 58 | await this.initElement(); 59 | } 60 | const headerMap = {}; 61 | const headerCells = await this.currentElement.$$('thead td') 62 | for (let i = 0; i < headerCells.length; i++) { 63 | headerMap[await headerCells[i].evaluate(node => node.textContent.trim())] = i 64 | } 65 | 66 | return headerMap; 67 | } 68 | } 69 | 70 | export { 71 | Table 72 | } 73 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/elements/text.ts: -------------------------------------------------------------------------------- 1 | import {BaseElement} from './element'; 2 | // import {step} from '../reporter' 3 | 4 | class Text extends BaseElement { 5 | constructor({page, selector, name}: {page?: any, selector: string, name?: string}) { 6 | super({page, selector, name}); 7 | } 8 | } 9 | 10 | export { 11 | Text 12 | } 13 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger'; 2 | export * from './waits'; 3 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/helpers/logger.ts: -------------------------------------------------------------------------------- 1 | import * as chalk from 'chalk'; 2 | 3 | const logger = { 4 | tabGreen: (msg) => console.log(chalk.green(`\t${msg}`)), 5 | spaceGreen: (msg) => console.log(chalk.green(` ${msg}`)), 6 | spaceYellow: (msg) => console.log(chalk.yellow(` ${msg}`)), 7 | spaceRed: (msg) => console.log(chalk.red(` ${msg}`)) 8 | } 9 | 10 | export { 11 | logger 12 | } 13 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/helpers/waits.ts: -------------------------------------------------------------------------------- 1 | import {logger} from './logger' 2 | const {TECH_INFO} = process.env; 3 | 4 | const waits = { 5 | waitForVisible: async (ctx, timeout) => { 6 | const {page, selector} = ctx; 7 | if (TECH_INFO) { 8 | logger.spaceYellow(`Wait ${selector} during ${timeout} ms`); 9 | } 10 | await page.waitForSelector(selector, {visible: true, timeout}) 11 | } 12 | } 13 | 14 | export { 15 | waits 16 | } 17 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './browser'; 2 | export * from './elements/element'; 3 | export * from './base.page'; 4 | export * from './reporter'; 5 | export * from './elements'; 6 | export * from './init.singleton'; 7 | export * from './mocha.utils'; 8 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/init.singleton.ts: -------------------------------------------------------------------------------- 1 | function initSingleton(ClassPage) { 2 | if (ClassPage._instance) { 3 | return ClassPage._instance; 4 | } 5 | const page = new ClassPage(); 6 | ClassPage._instance = page; 7 | return ClassPage._instance; 8 | } 9 | 10 | export { 11 | initSingleton 12 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/mocha.utils.ts: -------------------------------------------------------------------------------- 1 | import {logger} from './helpers'; 2 | import {browser} from './browser'; 3 | import * as path from 'path'; 4 | 5 | 6 | declare const allure: any; 7 | 8 | const {REPORTER} = process.env; 9 | 10 | function itDecorted(itTitle, testFn) { 11 | it(itTitle, decorateTest(itTitle, testFn)) 12 | } 13 | 14 | function decorateTest(itTitle, testFn) { 15 | return async function() { 16 | try { 17 | await testFn() 18 | } catch (error) { 19 | if (error.toString().includes('AssertionError')) { 20 | logger.spaceRed(`[ASSERTION:${itTitle}]`) 21 | } else { 22 | logger.spaceYellow(`[BROKEN:${itTitle}]`) 23 | } 24 | if (REPORTER === 'allure') { 25 | // await allure.step('Failed screen', async () => { 26 | const screenshot = await browser.takeScreenshot(path.resolve(process.cwd(), `./${itTitle}.failed.png`)) 27 | allure.createAttachment(`${itTitle} failed`, screenshot, 'image/png') 28 | // }) 29 | } 30 | 31 | throw error; 32 | } 33 | } 34 | } 35 | 36 | 37 | export { 38 | itDecorted as it 39 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/pubsub.ts: -------------------------------------------------------------------------------- 1 | const messagesList = { 2 | currentPage: 'current_page', 3 | closeBrowser: 'close_browser', 4 | entityInit: 'entity_initialization' 5 | } 6 | 7 | function pubSub() { 8 | const handlers = {}; 9 | 10 | function publish(msgName: string, data: any) { 11 | if (!handlers[msgName]) { 12 | return; 13 | } 14 | handlers[msgName].forEach((handler) => { 15 | handler(data); 16 | }); 17 | } 18 | 19 | function subscribe(msgName: string, handler: Function) { 20 | if (!handlers[msgName]) { 21 | handlers[msgName] = []; 22 | } 23 | handlers[msgName].push(handler); 24 | } 25 | 26 | return { 27 | publish, 28 | subscribe 29 | } 30 | } 31 | 32 | const pubsub = pubSub(); 33 | 34 | export { 35 | pubsub, 36 | messagesList 37 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/reporter/allure.ts: -------------------------------------------------------------------------------- 1 | declare const allure: any; 2 | const {TECH_INFO} = process.env; 3 | 4 | function stepAllure(stepName: string | Function, isTechInfo: boolean = false) { 5 | return function(_target, propName, descriptor) { 6 | const originalMethod = descriptor.value; 7 | 8 | descriptor.value = function(...args) { 9 | 10 | if (isTechInfo && TECH_INFO) { 11 | return allure.step(typeof stepName === 'function' ? stepName(this.id) : stepName, () => originalMethod.call(this, ...args)); 12 | } else if (isTechInfo && !TECH_INFO) { 13 | return originalMethod.call(this, ...args); 14 | } 15 | return allure.step(typeof stepName === 'function' ? stepName(this.id) : stepName, () => originalMethod.call(this, ...args)); 16 | } 17 | 18 | return descriptor 19 | } 20 | } 21 | 22 | export { 23 | stepAllure 24 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/reporter/console.ts: -------------------------------------------------------------------------------- 1 | import {logger} from '../helpers'; 2 | const {TECH_INFO} = process.env; 3 | 4 | function stepConsole(stepName: string | Function, isTechInfo: boolean = false) { 5 | return function(_target, propName, descriptor) { 6 | const originalMethod = descriptor.value; 7 | 8 | descriptor.value = function(...args) { 9 | const caller = _target.constructor.name.includes('Page') ? 'spaceGreen' : 'tabGreen' 10 | const isElement = _target.constructor.name.includes('Element') 11 | if (isTechInfo && TECH_INFO) { 12 | logger.spaceYellow(typeof stepName === 'function' ? stepName(this.id) : stepName) 13 | } else if (!isTechInfo) { 14 | const endMessage = isElement && args.length 15 | ? `${typeof stepName === 'function' ? stepName(this.id) : stepName} args: ${JSON.stringify(args)}` 16 | : `${typeof stepName === 'function' ? stepName(this.id) : stepName}` 17 | logger[caller](endMessage) 18 | } 19 | 20 | return originalMethod.call(this, ...args); 21 | } 22 | 23 | return descriptor 24 | } 25 | } 26 | 27 | export { 28 | stepConsole 29 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/lib/reporter/index.ts: -------------------------------------------------------------------------------- 1 | import {stepAllure} from './allure'; 2 | import {stepConsole} from './console'; 3 | const {REPORTER} = process.env; 4 | 5 | 6 | function step(stepName, isTechInfo = false) { 7 | if (REPORTER === 'allure') { 8 | return stepAllure(stepName, isTechInfo); 9 | } else { 10 | return stepConsole(stepName, isTechInfo); 11 | } 12 | } 13 | 14 | export { 15 | step 16 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livecoding-puppeteer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --require ts-node/register ./specs/**/*.spec.ts --timeout 25000", 8 | "test:allure": "REPORTER=allure mocha --require ts-node/register ./specs/**/*.spec.ts --timeout 25000 -R mocha-allure2-reporter", 9 | "test:tech:console": "TECH_INFO=1 mocha --require ts-node/register ./specs/**/*.spec.ts --timeout 25000", 10 | "test:parallel": "node ./execution/index.js", 11 | "build": "tsc" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@types/chai": "^4.2.11", 18 | "@types/mocha": "^7.0.2", 19 | "@types/node": "^14.0.22", 20 | "@types/puppeteer": "^3.0.1", 21 | "chai": "^4.2.0", 22 | "chalk": "^4.1.0", 23 | "process-rerun": "0.0.17", 24 | "puppeteer": "^5.0.0", 25 | "ts-node": "^8.10.2", 26 | "typescript": "^3.9.6" 27 | }, 28 | "devDependencies": { 29 | "mocha": "^8.0.1", 30 | "mocha-allure2-reporter": "0.0.3" 31 | } 32 | } -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/specs/login.spec.ts: -------------------------------------------------------------------------------- 1 | import {provider} from '../framework'; 2 | 3 | const {it} = provider.test(); 4 | const {expect, browser} = provider.pages(); 5 | 6 | describe('Main page', function() { 7 | 8 | beforeEach(async () => { 9 | await browser.goto('http://localhost:4000/'); 10 | }) 11 | 12 | afterEach(async () => { 13 | await browser.close(); 14 | }) 15 | 16 | it("Login", async function() { 17 | const mainPage = provider.main(); 18 | const tablePage = provider.table(); 19 | await mainPage.loginToSystem('admin', 'admin'); 20 | expect(await tablePage.getHeaderTitle()).to.includes('valera'); 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/specs/machine.spec.ts: -------------------------------------------------------------------------------- 1 | import {provider} from '../framework'; 2 | 3 | const {expect, browser} = provider.pages(); 4 | const {it} = provider.test(); 5 | 6 | describe('Machines page', function() { 7 | 8 | beforeEach(async () => { 9 | await browser.goto('http://localhost:4000/'); 10 | }) 11 | 12 | afterEach(async () => { 13 | await browser.close(); 14 | }) 15 | 16 | it("Add new machine", async function() { 17 | const mainPage = provider.main(); 18 | const tablePage = provider.table(); 19 | await mainPage.loginToSystem('admin', 'admin'); 20 | const newMachine = { 21 | manufacturer: 'testvalue', 22 | volume: 'testvalue', 23 | length: 'testvalue', 24 | weigth: 'testvalue', 25 | width: 'testvalue', 26 | power: 'testvalue', 27 | price: 'testvalue' 28 | } 29 | await tablePage.addNewMachine(newMachine) 30 | const data = await tablePage.getMachinesList(); 31 | const requiredAddedMachine = data.find(({manufacturer, volume}) => { 32 | return newMachine.manufacturer === manufacturer && newMachine.volume === volume 33 | }); 34 | 35 | expect(newMachine).to.deep.equal(requiredAddedMachine); 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /livecoding-puppeteer-video-4/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "sourceMap": true, 5 | "module": "commonjs", 6 | "outDir": "built", 7 | "experimentalDecorators": true, 8 | "types": [ 9 | "@types/puppeteer", 10 | "@types/mocha", 11 | "@types/chai", 12 | ] 13 | }, 14 | "include": [ 15 | "lib/**/*.ts", 16 | "specs/**/*.ts", 17 | "framework/**/*.ts" 18 | ] 19 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1 3 | } 4 | -------------------------------------------------------------------------------- /wdio-soft/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | allure-results -------------------------------------------------------------------------------- /wdio-soft/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@wdio/allure-reporter": "^7.16.14", 4 | "@wdio/cli": "^7.16.15", 5 | "@wdio/local-runner": "^7.16.15", 6 | "@wdio/mocha-framework": "^7.16.15", 7 | "@wdio/spec-reporter": "^7.16.14", 8 | "chromedriver": "^98.0.1", 9 | "ts-node": "^10.5.0", 10 | "typescript": "^4.5.5", 11 | "wdio-chromedriver-service": "^7.2.8" 12 | }, 13 | "scripts": { 14 | "wdio": "wdio run ./test/wdio.conf.js" 15 | }, 16 | "dependencies": { 17 | "assertior": "^0.0.27" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /wdio-soft/test/pageobjects/login.page.ts: -------------------------------------------------------------------------------- 1 | import { ChainablePromiseElement } from 'webdriverio'; 2 | 3 | import Page from './page'; 4 | 5 | /** 6 | * sub page containing specific selectors and methods for a specific page 7 | */ 8 | class LoginPage extends Page { 9 | /** 10 | * define selectors using getter methods 11 | */ 12 | public get inputUsername(): ChainablePromiseElement> { 13 | return $('#username'); 14 | } 15 | 16 | public get inputPassword(): ChainablePromiseElement> { 17 | return $('#password'); 18 | } 19 | 20 | public get btnSubmit(): ChainablePromiseElement> { 21 | return $('button[type="submit"]'); 22 | } 23 | 24 | /** 25 | * a method to encapsule automation code to interact with the page 26 | * e.g. to login using username and password 27 | */ 28 | public async login (username: string, password: string): Promise { 29 | await this.inputUsername.setValue(username); 30 | await this.inputPassword.setValue(password); 31 | await this.btnSubmit.click(); 32 | } 33 | 34 | /** 35 | * overwrite specific options to adapt it to page object 36 | */ 37 | public open(): Promise { 38 | return super.open('login'); 39 | } 40 | } 41 | 42 | export default new LoginPage(); 43 | -------------------------------------------------------------------------------- /wdio-soft/test/pageobjects/page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * main page object containing all methods, selectors and functionality 3 | * that is shared across all page objects 4 | */ 5 | export default class Page { 6 | /** 7 | * Opens a sub page of the page 8 | * @param path path of the sub page (e.g. /path/to/page.html) 9 | */ 10 | public open(path: string): Promise { 11 | return browser.url(`https://the-internet.herokuapp.com/${path}`) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /wdio-soft/test/pageobjects/secure.page.ts: -------------------------------------------------------------------------------- 1 | import { ChainablePromiseElement } from 'webdriverio'; 2 | 3 | import Page from './page'; 4 | 5 | /** 6 | * sub page containing specific selectors and methods for a specific page 7 | */ 8 | class SecurePage extends Page { 9 | /** 10 | * define selectors using getter methods 11 | */ 12 | public get flashAlert(): ChainablePromiseElement> { 13 | return $('#flash'); 14 | } 15 | } 16 | 17 | export default new SecurePage(); 18 | -------------------------------------------------------------------------------- /wdio-soft/test/specs/example.e2e.ts: -------------------------------------------------------------------------------- 1 | import {expect, initStepDeclarator} from 'assertior' 2 | import LoginPage from '../pageobjects/login.page'; 3 | import allure from '@wdio/allure-reporter' 4 | 5 | allure.prototype.onTestPass = function() { 6 | const failedOrBrokenCase = this._allure.getCurrentTest().steps.find(item => item.status === 'broken' || item.status === 'failed'); 7 | if (failedOrBrokenCase) { 8 | return this._allure.endCase(failedOrBrokenCase.status); 9 | } 10 | return this._allure.endCase('passed'); 11 | } 12 | 13 | function allureStep(stepAssertionName: string, error, expected, current) { 14 | allure.startStep(stepAssertionName); 15 | if (error) { 16 | allure.addAttachment('Expected value', JSON.stringify(expected, null, 2), 'application/json'); 17 | allure.addAttachment('Current value', JSON.stringify(current, null, 2), 'application/json'); 18 | allure.addAttachment('Error', JSON.stringify(error, null, 2), 'application/json'); 19 | allure.endStep('broken'); 20 | } else { 21 | allure.endStep('passed'); 22 | } 23 | } 24 | 25 | initStepDeclarator(allureStep); 26 | 27 | describe('My Login application', () => { 28 | it('should login with valid credentials', async function () { 29 | await LoginPage.open(); 30 | await LoginPage.login('tomsmith', 'SuperSecretPassword!'); 31 | // expect.soft(1).toEqual(1); 32 | expect.soft(1).toEqual(2); 33 | }); 34 | }); 35 | 36 | 37 | -------------------------------------------------------------------------------- /wdio-soft/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [ 4 | "node", 5 | "webdriverio/async", 6 | "@wdio/mocha-framework", 7 | "expect-webdriverio" 8 | ], 9 | "target": "ES5" 10 | } 11 | } -------------------------------------------------------------------------------- /webdriver-lazy-element/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /webdriver-lazy-element/framework/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pages' -------------------------------------------------------------------------------- /webdriver-lazy-element/framework/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './main' -------------------------------------------------------------------------------- /webdriver-lazy-element/framework/pages/main.ts: -------------------------------------------------------------------------------- 1 | import {_$, IElement, IElementArray, _$$} from '../../lib' 2 | 3 | class MainPage { 4 | private root: IElement; 5 | private headerNavigateToRegisterButton: IElement; 6 | private headerNavigateToLoginButton: IElement; 7 | private username: IElement; 8 | private password: IElement; 9 | private subminLogin: IElement; 10 | 11 | public constructor() { 12 | this.root = _$('#main_page') 13 | this.headerNavigateToLoginButton = this.root._$('.main_header')._$('.user_buttons')._$('button'); 14 | this.headerNavigateToRegisterButton = this.root._$('.main_header')._$('.user_buttons button:nth-child(2)'); 15 | this.username = this.root._$$('.login_form input').get(0); 16 | this.password = this.root._$$('.login_form input').get(1); 17 | this.subminLogin = this.root._$('.login_form button'); 18 | } 19 | 20 | async navigeteToRegister(): Promise { 21 | return this.headerNavigateToRegisterButton.click(); 22 | } 23 | 24 | async navigeteToLogin(): Promise { 25 | return this.headerNavigateToLoginButton.click(); 26 | } 27 | 28 | async tenClicks() { 29 | for (let i = 0; i < 10; i++) { 30 | await this.headerNavigateToRegisterButton.click(); 31 | } 32 | } 33 | 34 | async login(username, password): Promise { 35 | await this.username.setValue(username); 36 | await this.password.setValue(password); 37 | await this.subminLogin.click(); 38 | } 39 | } 40 | 41 | export { 42 | MainPage 43 | } 44 | -------------------------------------------------------------------------------- /webdriver-lazy-element/lib/element.utils.ts: -------------------------------------------------------------------------------- 1 | import {Element, ElementArray} from 'webdriverio'; 2 | 3 | interface IElement extends Element { 4 | _$(selector: string): IElement; 5 | _$$(selector: string): IElementArray; 6 | } 7 | 8 | interface IElementArray extends ElementArray { 9 | get(index: number): IElement 10 | } 11 | 12 | const lazy = ['_$', '_$$', 'get', '$', '$$'] 13 | 14 | function _$(selector: string | number, parentHandler?): IElement { 15 | async function getCurrentElement() { 16 | if (parentHandler && typeof selector === 'number') { 17 | const parentsList = await parentHandler(); 18 | return parentsList[selector] 19 | } 20 | 21 | if (parentHandler) { 22 | const parent = await parentHandler() as Element; 23 | console.log('Call from parent') 24 | return parent.$(selector as string); 25 | } else { 26 | console.log('call as parent') 27 | return $(selector as string); 28 | } 29 | } 30 | 31 | return new Proxy({}, { 32 | get(_t, value) { 33 | if (value === '$$') { 34 | return (selector) => _$$(selector, getCurrentElement); 35 | } 36 | if (value === '_$$') { 37 | return (selector) => _$$(selector, getCurrentElement); 38 | } 39 | if (lazy.includes(value as string)) { 40 | return (_selector) => _$(_selector, getCurrentElement); 41 | } else { 42 | return (...args) => getCurrentElement().then((el) => el[value].call(el, ...args)); 43 | } 44 | } 45 | }) as IElement; 46 | } 47 | 48 | function _$$(selector, parentHandler?): IElementArray { 49 | 50 | async function getCurrentElement() { 51 | if (parentHandler) { 52 | const parent = await parentHandler() as Element; 53 | return parent.$$(selector); 54 | } else { 55 | return $$(selector); 56 | } 57 | } 58 | 59 | return new Proxy({}, { 60 | get(_t, value) { 61 | if (lazy.includes(value as string)) { 62 | return (_selector) => _$(_selector, getCurrentElement) 63 | } else { 64 | return (...args) => getCurrentElement().then((el) => el[value](...args)); 65 | } 66 | } 67 | }) as IElementArray; 68 | } 69 | 70 | export { 71 | _$, 72 | _$$, 73 | IElement, 74 | IElementArray 75 | } 76 | -------------------------------------------------------------------------------- /webdriver-lazy-element/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './element.utils' 2 | -------------------------------------------------------------------------------- /webdriver-lazy-element/lib/super.element.utils.ts: -------------------------------------------------------------------------------- 1 | import {Element, ElementArray} from 'webdriverio'; 2 | 3 | const lazyInterface = ['_$', '_$$', 'get']; 4 | 5 | 6 | interface IElement extends Element { 7 | _$(selector: string): IElement; 8 | _$$(selector: string): IElementArray; 9 | } 10 | 11 | interface IElementArray extends ElementArray { 12 | get(index: number): IElement 13 | } 14 | 15 | class LazyElements { 16 | private selector: string; 17 | private initParent: Function; 18 | private index: number 19 | private parent: any 20 | private currentElement: any; 21 | 22 | constructor(selector, initParent?, index = 0) { 23 | this.index = index; 24 | this.initParent = initParent; 25 | this.selector = selector; 26 | } 27 | 28 | _$(selector: string) { 29 | return startChainig$$(selector, this._getCurrentElements.bind(this)) 30 | } 31 | 32 | _$$(selector: string) { 33 | return startChainig$$(selector, this._getCurrentElements.bind(this)) 34 | } 35 | 36 | get(index: number) { 37 | const costyl = function() { 38 | if (this.parent) { 39 | return this.parent 40 | } else if (this.initParent) { 41 | return this.initParent() 42 | } 43 | console.trace() 44 | throw new Error('This is proval') 45 | } 46 | return startChainig$$(this.selector, costyl.bind(this), index) 47 | } 48 | 49 | async _getCurrentElements() { 50 | if (this.currentElement) { 51 | console.log('Less time, we are using current element') 52 | return this.currentElement; 53 | } 54 | 55 | if (this.initParent) { 56 | this.parent = await this.initParent() 57 | const collection = await this.parent.$$(this.selector); 58 | this.currentElement = collection[this.index]; 59 | } else { 60 | const collection = await $$(this.selector); 61 | this.currentElement = collection[this.index]; 62 | } 63 | if (!this.currentElement) { 64 | let errorMessage = `${this.selector} with index ${this.index} was not found` 65 | 66 | if (this.parent) { 67 | errorMessage = `${this.parent.selector} does not have child ${this.selector} with index ${this.index}` 68 | } 69 | throw new Error(errorMessage) 70 | } 71 | 72 | return this.currentElement; 73 | } 74 | } 75 | 76 | function startChainig$$(selector, initParent = null, index = 0): IElement { 77 | const lazyElements = new LazyElements(selector, initParent, index) 78 | 79 | return new Proxy(lazyElements, { 80 | get(target, propName) { 81 | if (lazyInterface.includes(propName as string)) { 82 | return (...args) => target[propName](...args); 83 | } else { 84 | return (...args) => target._getCurrentElements().then((el) => { 85 | return el[propName].call(el, ...args) 86 | }) 87 | } 88 | } 89 | }) as any; 90 | } 91 | 92 | const _$ = (selector): IElement => startChainig$$(selector); 93 | const _$$ = (selector): IElementArray => startChainig$$(selector) as any; 94 | 95 | export { 96 | _$, 97 | _$$ 98 | } 99 | -------------------------------------------------------------------------------- /webdriver-lazy-element/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webdriverio-test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "wdio.conf.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@types/node": "^14.0.11", 14 | "@wdio/cli": "^6.1.16", 15 | "@wdio/local-runner": "^6.1.16", 16 | "@wdio/mocha-framework": "^6.1.14", 17 | "@wdio/selenium-standalone-service": "^6.1.14", 18 | "@wdio/spec-reporter": "^6.1.14", 19 | "ts-node": "^8.10.2", 20 | "typescript": "^3.9.5" 21 | }, 22 | "dependencies": { 23 | "@types/mocha": "^7.0.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /webdriver-lazy-element/specs/base.spec.ts: -------------------------------------------------------------------------------- 1 | import {MainPage} from '../framework/pages' 2 | import {_$, _$$} from '../lib/super.element.utils' 3 | 4 | describe('webdriver.io page', () => { 5 | it('should have the right title', async () => { 6 | await browser.url('http://localhost:3000') 7 | 8 | 9 | 10 | await new MainPage().navigeteToLogin() 11 | await new MainPage().navigeteToLogin() 12 | 13 | // const modalInputs = _$('#main_page')._$('.login_form')._$('.modal')._$$('form').get(0)._$$('lol'); 14 | 15 | // for (let i = 0; i < 10; i++) { 16 | // await modalInputs.get(1).setValue('admin') 17 | // } 18 | 19 | await browser.pause(25000) 20 | }) 21 | }) -------------------------------------------------------------------------------- /webdriver-lazy-element/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "module": "commonjs", 5 | "target": "es2017", 6 | "types": [ 7 | "node", 8 | "webdriverio", 9 | "@wdio/mocha-framework", 10 | "@wdio/selenium-standalone-service" 11 | ] 12 | }, 13 | "exclude": [ 14 | "node_modules" 15 | ], 16 | "include": [ 17 | "lib", 18 | "framework", 19 | "specs" 20 | ] 21 | } -------------------------------------------------------------------------------- /webdriver-lazy-element/wdio.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // 3 | // ==================== 4 | // Runner Configuration 5 | // ==================== 6 | // 7 | // WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally or 8 | // on a remote machine). 9 | runner: 'local', 10 | // 11 | // ================== 12 | // Specify Test Files 13 | // ================== 14 | // Define which test specs should run. The pattern is relative to the directory 15 | // from which `wdio` was called. Notice that, if you are calling `wdio` from an 16 | // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working 17 | // directory is where your package.json resides, so `wdio` will be called from there. 18 | // 19 | specs: [ 20 | './specs/**/*.spec.ts' 21 | ], 22 | // Patterns to exclude. 23 | exclude: [ 24 | // 'path/to/excluded/files' 25 | ], 26 | // 27 | // ============ 28 | // Capabilities 29 | // ============ 30 | // Define your capabilities here. WebdriverIO can run multiple capabilities at the same 31 | // time. Depending on the number of capabilities, WebdriverIO launches several test 32 | // sessions. Within your capabilities you can overwrite the spec and exclude options in 33 | // order to group specific specs to a specific capability. 34 | // 35 | // First, you can define how many instances should be started at the same time. Let's 36 | // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have 37 | // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec 38 | // files and you set maxInstances to 10, all spec files will get tested at the same time 39 | // and 30 processes will get spawned. The property handles how many capabilities 40 | // from the same test should run tests. 41 | // 42 | // 43 | // If you have trouble getting all important capabilities together, check out the 44 | // Sauce Labs platform configurator - a great tool to configure your capabilities: 45 | // https://docs.saucelabs.com/reference/platforms-configurator 46 | // 47 | capabilities: [{ 48 | 49 | // maxInstances can get overwritten per capability. So if you have an in-house Selenium 50 | // grid with only 5 firefox instances available you can make sure that not more than 51 | // 5 instances get started at a time. 52 | // 53 | browserName: 'chrome', 54 | // If outputDir is provided WebdriverIO can capture driver session logs 55 | // it is possible to configure which logTypes to include/exclude. 56 | // excludeDriverLogs: ['*'], // pass '*' to exclude all driver session logs 57 | // excludeDriverLogs: ['bugreport', 'server'], 58 | }], 59 | // 60 | // =================== 61 | // Test Configurations 62 | // =================== 63 | // Define all options that are relevant for the WebdriverIO instance here 64 | // 65 | // Level of logging verbosity: trace | debug | info | warn | error | silent 66 | logLevel: 'silent', 67 | // 68 | // Set specific log levels per logger 69 | // loggers: 70 | // - webdriver, webdriverio 71 | // - @wdio/applitools-service, @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service 72 | // - @wdio/mocha-framework, @wdio/jasmine-framework 73 | // - @wdio/local-runner, @wdio/lambda-runner 74 | // - @wdio/sumologic-reporter 75 | // - @wdio/cli, @wdio/config, @wdio/sync, @wdio/utils 76 | // Level of logging verbosity: trace | debug | info | warn | error | silent 77 | // logLevels: { 78 | // webdriver: 'info', 79 | // '@wdio/applitools-service': 'info' 80 | // }, 81 | // 82 | // If you only want to run your tests until a specific amount of tests have failed use 83 | // bail (default is 0 - don't bail, run all tests). 84 | bail: 0, 85 | // 86 | // Set a base URL in order to shorten url command calls. If your `url` parameter starts 87 | // with `/`, the base url gets prepended, not including the path portion of your baseUrl. 88 | // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url 89 | // gets prepended directly. 90 | baseUrl: 'http://localhost', 91 | // 92 | // Default timeout for all waitFor* commands. 93 | waitforTimeout: 10000, 94 | // 95 | // Default timeout in milliseconds for request 96 | // if browser driver or grid doesn't send response 97 | connectionRetryTimeout: 120000, 98 | // 99 | // Default request retries count 100 | connectionRetryCount: 3, 101 | // 102 | // Test runner services 103 | // Services take over a specific job you don't want to take care of. They enhance 104 | // your test setup with almost no effort. Unlike plugins, they don't add new 105 | // commands. Instead, they hook themselves up into the test process. 106 | services: ['selenium-standalone'], 107 | 108 | // Framework you want to run your specs with. 109 | // The following are supported: Mocha, Jasmine, and Cucumber 110 | // see also: https://webdriver.io/docs/frameworks.html 111 | // 112 | // Make sure you have the wdio adapter package for the specific framework installed 113 | // before running any tests. 114 | framework: 'mocha', 115 | // 116 | // The number of times to retry the entire specfile when it fails as a whole 117 | // specFileRetries: 1, 118 | // 119 | // Whether or not retried specfiles should be retried immediately or deferred to the end of the queue 120 | // specFileRetriesDeferred: false, 121 | // 122 | // Test reporter for stdout. 123 | // The only one supported by default is 'dot' 124 | // see also: https://webdriver.io/docs/dot-reporter.html 125 | reporters: ['spec'], 126 | 127 | 128 | 129 | // 130 | // Options to be passed to Mocha. 131 | // See the full list at http://mochajs.org/ 132 | mochaOpts: { 133 | // TypeScript setup 134 | require: ['ts-node/register'], 135 | ui: 'bdd', 136 | timeout: 60000 137 | }, 138 | // 139 | // ===== 140 | // Hooks 141 | // ===== 142 | // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance 143 | // it and to build services around it. You can either apply a single function or an array of 144 | // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got 145 | // resolved to continue. 146 | /** 147 | * Gets executed once before all workers get launched. 148 | * @param {Object} config wdio configuration object 149 | * @param {Array.} capabilities list of capabilities details 150 | */ 151 | // onPrepare: function (config, capabilities) { 152 | // }, 153 | /** 154 | * Gets executed before a worker process is spawned and can be used to initialise specific service 155 | * for that worker as well as modify runtime environments in an async fashion. 156 | * @param {String} cid capability id (e.g 0-0) 157 | * @param {[type]} caps object containing capabilities for session that will be spawn in the worker 158 | * @param {[type]} specs specs to be run in the worker process 159 | * @param {[type]} args object that will be merged with the main configuration once worker is initialised 160 | * @param {[type]} execArgv list of string arguments passed to the worker process 161 | */ 162 | // onWorkerStart: function (cid, caps, specs, args, execArgv) { 163 | // }, 164 | /** 165 | * Gets executed just before initialising the webdriver session and test framework. It allows you 166 | * to manipulate configurations depending on the capability or spec. 167 | * @param {Object} config wdio configuration object 168 | * @param {Array.} capabilities list of capabilities details 169 | * @param {Array.} specs List of spec file paths that are to be run 170 | */ 171 | // beforeSession: function (config, capabilities, specs) { 172 | // }, 173 | /** 174 | * Gets executed before test execution begins. At this point you can access to all global 175 | * variables like `browser`. It is the perfect place to define custom commands. 176 | * @param {Array.} capabilities list of capabilities details 177 | * @param {Array.} specs List of spec file paths that are to be run 178 | */ 179 | // before: function (capabilities, specs) { 180 | // }, 181 | /** 182 | * Runs before a WebdriverIO command gets executed. 183 | * @param {String} commandName hook command name 184 | * @param {Array} args arguments that command would receive 185 | */ 186 | // beforeCommand: function (commandName, args) { 187 | // }, 188 | /** 189 | * Hook that gets executed before the suite starts 190 | * @param {Object} suite suite details 191 | */ 192 | // beforeSuite: function (suite) { 193 | // }, 194 | /** 195 | * Function to be executed before a test (in Mocha/Jasmine) starts. 196 | */ 197 | // beforeTest: function (test, context) { 198 | // }, 199 | /** 200 | * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling 201 | * beforeEach in Mocha) 202 | */ 203 | // beforeHook: function (test, context) { 204 | // }, 205 | /** 206 | * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling 207 | * afterEach in Mocha) 208 | */ 209 | // afterHook: function (test, context, { error, result, duration, passed, retries }) { 210 | // }, 211 | /** 212 | * Function to be executed after a test (in Mocha/Jasmine). 213 | */ 214 | // afterTest: function(test, context, { error, result, duration, passed, retries }) { 215 | // }, 216 | 217 | 218 | /** 219 | * Hook that gets executed after the suite has ended 220 | * @param {Object} suite suite details 221 | */ 222 | // afterSuite: function (suite) { 223 | // }, 224 | /** 225 | * Runs after a WebdriverIO command gets executed 226 | * @param {String} commandName hook command name 227 | * @param {Array} args arguments that command would receive 228 | * @param {Number} result 0 - command success, 1 - command error 229 | * @param {Object} error error object if any 230 | */ 231 | // afterCommand: function (commandName, args, result, error) { 232 | // }, 233 | /** 234 | * Gets executed after all tests are done. You still have access to all global variables from 235 | * the test. 236 | * @param {Number} result 0 - test pass, 1 - test fail 237 | * @param {Array.} capabilities list of capabilities details 238 | * @param {Array.} specs List of spec file paths that ran 239 | */ 240 | // after: function (result, capabilities, specs) { 241 | // }, 242 | /** 243 | * Gets executed right after terminating the webdriver session. 244 | * @param {Object} config wdio configuration object 245 | * @param {Array.} capabilities list of capabilities details 246 | * @param {Array.} specs List of spec file paths that ran 247 | */ 248 | // afterSession: function (config, capabilities, specs) { 249 | // }, 250 | /** 251 | * Gets executed after all workers got shut down and the process is about to exit. An error 252 | * thrown in the onComplete hook will result in the test run failing. 253 | * @param {Object} exitCode 0 - success, 1 - fail 254 | * @param {Object} config wdio configuration object 255 | * @param {Array.} capabilities list of capabilities details 256 | * @param {} results object containing test results 257 | */ 258 | // onComplete: function(exitCode, config, capabilities, results) { 259 | // }, 260 | /** 261 | * Gets executed when a refresh happens. 262 | * @param {String} oldSessionId session ID of the old session 263 | * @param {String} newSessionId session ID of the new session 264 | */ 265 | //onReload: function(oldSessionId, newSessionId) { 266 | //} 267 | } 268 | -------------------------------------------------------------------------------- /webdriverio-test/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /webdriverio-test/framework/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pages' -------------------------------------------------------------------------------- /webdriverio-test/framework/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './main' -------------------------------------------------------------------------------- /webdriverio-test/framework/pages/main.ts: -------------------------------------------------------------------------------- 1 | import {_$, IElement, IElementArray, _$$} from '../../lib' 2 | 3 | class MainPage { 4 | private root: IElement; 5 | private headerNavigateToRegisterButton: IElement; 6 | private headerNavigateToLoginButton: IElement; 7 | private username: IElement; 8 | private password: IElement; 9 | private subminLogin: IElement; 10 | 11 | public constructor() { 12 | this.root = _$('#main_page') 13 | this.headerNavigateToRegisterButton = this.root._$('.user_buttons button:nth-child(2)')._$('.header'); 14 | this.headerNavigateToLoginButton = this.root._$('.user_buttons button:nth-child(1)'); 15 | this.username = this.root._$$('.login_form input').get(0); 16 | this.password = this.root._$$('.login_form input').get(1); 17 | this.subminLogin = this.root._$('.login_form button'); 18 | } 19 | 20 | async navigeteToRegister(): Promise { 21 | return this.headerNavigateToRegisterButton.click(); 22 | } 23 | 24 | async navigeteToLogin(): Promise { 25 | return this.headerNavigateToLoginButton.click(); 26 | } 27 | 28 | async login(username, password) { 29 | await this.username.setValue(username); 30 | await this.password.setValue(password); 31 | await this.subminLogin.click(); 32 | } 33 | } 34 | 35 | export { 36 | MainPage 37 | } 38 | -------------------------------------------------------------------------------- /webdriverio-test/lib/element.utils.ts: -------------------------------------------------------------------------------- 1 | import {Element, ElementArray} from 'webdriverio'; 2 | 3 | interface IElement extends Element { 4 | _$(selector: string): IElement; 5 | _$$(selector: string): IElementArray; 6 | } 7 | 8 | interface IElementArray extends ElementArray { 9 | get(index: number): IElement 10 | } 11 | 12 | const lazy = ['_$', '_$$', 'get', '$', '$$'] 13 | 14 | function _$(selector: string | number, parentHandler?): IElement { 15 | async function getCurrentElement() { 16 | if (parentHandler && typeof selector === 'number') { 17 | const parentsList = await parentHandler(); 18 | return parentsList[selector] 19 | } 20 | 21 | if (parentHandler) { 22 | const parent = await parentHandler() as Element; 23 | return parent.$(selector as string); 24 | } else { 25 | return $(selector as string); 26 | } 27 | } 28 | 29 | return new Proxy({}, { 30 | get(_t, value) { 31 | if (value === '$$') { 32 | return (selector) => _$$(selector, getCurrentElement); 33 | } 34 | if (value === '_$$') { 35 | return (selector) => _$$(selector, getCurrentElement); 36 | } 37 | if (lazy.includes(value as string)) { 38 | return (_selector) => _$(_selector, getCurrentElement); 39 | } else { 40 | return (...args) => getCurrentElement().then((el) => el[value].call(el, ...args)); 41 | } 42 | } 43 | }) as IElement; 44 | } 45 | 46 | function _$$(selector, parentHandler?): IElementArray { 47 | 48 | async function getCurrentElement() { 49 | if (parentHandler) { 50 | const parent = await parentHandler() as Element; 51 | return parent.$$(selector); 52 | } else { 53 | return $$(selector); 54 | } 55 | } 56 | 57 | return new Proxy({}, { 58 | get(_t, value) { 59 | if (lazy.includes(value as string)) { 60 | return (_selector) => _$(_selector, getCurrentElement) 61 | } else { 62 | return (...args) => getCurrentElement().then((el) => el[value](...args)); 63 | } 64 | } 65 | }) as IElementArray; 66 | } 67 | 68 | export { 69 | _$, 70 | _$$, 71 | IElement, 72 | IElementArray 73 | } 74 | -------------------------------------------------------------------------------- /webdriverio-test/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './element.utils' 2 | -------------------------------------------------------------------------------- /webdriverio-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webdriverio-test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "wdio.conf.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@types/node": "^14.0.11", 14 | "@wdio/cli": "^6.1.16", 15 | "@wdio/local-runner": "^6.1.16", 16 | "@wdio/mocha-framework": "^6.1.14", 17 | "@wdio/selenium-standalone-service": "^6.1.14", 18 | "@wdio/spec-reporter": "^6.1.14", 19 | "ts-node": "^8.10.2", 20 | "typescript": "^3.9.5" 21 | }, 22 | "dependencies": { 23 | "@types/mocha": "^7.0.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /webdriverio-test/specs/base.spec.ts: -------------------------------------------------------------------------------- 1 | import {MainPage} from '../framework/pages' 2 | 3 | describe('webdriver.io page', () => { 4 | it('should have the right title', async () => { 5 | await browser.url('http://localhost:3000') 6 | await new MainPage().login('admin', 'admin'); 7 | }) 8 | }) -------------------------------------------------------------------------------- /webdriverio-test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "module": "commonjs", 5 | "target": "es2017", 6 | "types": [ 7 | "node", 8 | "webdriverio", 9 | "@wdio/mocha-framework", 10 | "@wdio/selenium-standalone-service" 11 | ] 12 | }, 13 | "exclude": [ 14 | "node_modules" 15 | ], 16 | "include": [ 17 | "lib", 18 | "framework", 19 | "specs" 20 | ] 21 | } -------------------------------------------------------------------------------- /webdriverio-test/wdio.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // 3 | // ==================== 4 | // Runner Configuration 5 | // ==================== 6 | // 7 | // WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally or 8 | // on a remote machine). 9 | runner: 'local', 10 | // 11 | // ================== 12 | // Specify Test Files 13 | // ================== 14 | // Define which test specs should run. The pattern is relative to the directory 15 | // from which `wdio` was called. Notice that, if you are calling `wdio` from an 16 | // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working 17 | // directory is where your package.json resides, so `wdio` will be called from there. 18 | // 19 | specs: [ 20 | './specs/**/*.spec.ts' 21 | ], 22 | // Patterns to exclude. 23 | exclude: [ 24 | // 'path/to/excluded/files' 25 | ], 26 | // 27 | // ============ 28 | // Capabilities 29 | // ============ 30 | // Define your capabilities here. WebdriverIO can run multiple capabilities at the same 31 | // time. Depending on the number of capabilities, WebdriverIO launches several test 32 | // sessions. Within your capabilities you can overwrite the spec and exclude options in 33 | // order to group specific specs to a specific capability. 34 | // 35 | // First, you can define how many instances should be started at the same time. Let's 36 | // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have 37 | // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec 38 | // files and you set maxInstances to 10, all spec files will get tested at the same time 39 | // and 30 processes will get spawned. The property handles how many capabilities 40 | // from the same test should run tests. 41 | // 42 | // 43 | // If you have trouble getting all important capabilities together, check out the 44 | // Sauce Labs platform configurator - a great tool to configure your capabilities: 45 | // https://docs.saucelabs.com/reference/platforms-configurator 46 | // 47 | capabilities: [{ 48 | 49 | // maxInstances can get overwritten per capability. So if you have an in-house Selenium 50 | // grid with only 5 firefox instances available you can make sure that not more than 51 | // 5 instances get started at a time. 52 | // 53 | browserName: 'chrome', 54 | // If outputDir is provided WebdriverIO can capture driver session logs 55 | // it is possible to configure which logTypes to include/exclude. 56 | // excludeDriverLogs: ['*'], // pass '*' to exclude all driver session logs 57 | // excludeDriverLogs: ['bugreport', 'server'], 58 | }], 59 | // 60 | // =================== 61 | // Test Configurations 62 | // =================== 63 | // Define all options that are relevant for the WebdriverIO instance here 64 | // 65 | // Level of logging verbosity: trace | debug | info | warn | error | silent 66 | logLevel: 'silent', 67 | // 68 | // Set specific log levels per logger 69 | // loggers: 70 | // - webdriver, webdriverio 71 | // - @wdio/applitools-service, @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service 72 | // - @wdio/mocha-framework, @wdio/jasmine-framework 73 | // - @wdio/local-runner, @wdio/lambda-runner 74 | // - @wdio/sumologic-reporter 75 | // - @wdio/cli, @wdio/config, @wdio/sync, @wdio/utils 76 | // Level of logging verbosity: trace | debug | info | warn | error | silent 77 | // logLevels: { 78 | // webdriver: 'info', 79 | // '@wdio/applitools-service': 'info' 80 | // }, 81 | // 82 | // If you only want to run your tests until a specific amount of tests have failed use 83 | // bail (default is 0 - don't bail, run all tests). 84 | bail: 0, 85 | // 86 | // Set a base URL in order to shorten url command calls. If your `url` parameter starts 87 | // with `/`, the base url gets prepended, not including the path portion of your baseUrl. 88 | // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url 89 | // gets prepended directly. 90 | baseUrl: 'http://localhost', 91 | // 92 | // Default timeout for all waitFor* commands. 93 | waitforTimeout: 10000, 94 | // 95 | // Default timeout in milliseconds for request 96 | // if browser driver or grid doesn't send response 97 | connectionRetryTimeout: 120000, 98 | // 99 | // Default request retries count 100 | connectionRetryCount: 3, 101 | // 102 | // Test runner services 103 | // Services take over a specific job you don't want to take care of. They enhance 104 | // your test setup with almost no effort. Unlike plugins, they don't add new 105 | // commands. Instead, they hook themselves up into the test process. 106 | services: ['selenium-standalone'], 107 | 108 | // Framework you want to run your specs with. 109 | // The following are supported: Mocha, Jasmine, and Cucumber 110 | // see also: https://webdriver.io/docs/frameworks.html 111 | // 112 | // Make sure you have the wdio adapter package for the specific framework installed 113 | // before running any tests. 114 | framework: 'mocha', 115 | // 116 | // The number of times to retry the entire specfile when it fails as a whole 117 | // specFileRetries: 1, 118 | // 119 | // Whether or not retried specfiles should be retried immediately or deferred to the end of the queue 120 | // specFileRetriesDeferred: false, 121 | // 122 | // Test reporter for stdout. 123 | // The only one supported by default is 'dot' 124 | // see also: https://webdriver.io/docs/dot-reporter.html 125 | reporters: ['spec'], 126 | 127 | 128 | 129 | // 130 | // Options to be passed to Mocha. 131 | // See the full list at http://mochajs.org/ 132 | mochaOpts: { 133 | // TypeScript setup 134 | require: ['ts-node/register'], 135 | ui: 'bdd', 136 | timeout: 60000 137 | }, 138 | // 139 | // ===== 140 | // Hooks 141 | // ===== 142 | // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance 143 | // it and to build services around it. You can either apply a single function or an array of 144 | // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got 145 | // resolved to continue. 146 | /** 147 | * Gets executed once before all workers get launched. 148 | * @param {Object} config wdio configuration object 149 | * @param {Array.} capabilities list of capabilities details 150 | */ 151 | // onPrepare: function (config, capabilities) { 152 | // }, 153 | /** 154 | * Gets executed before a worker process is spawned and can be used to initialise specific service 155 | * for that worker as well as modify runtime environments in an async fashion. 156 | * @param {String} cid capability id (e.g 0-0) 157 | * @param {[type]} caps object containing capabilities for session that will be spawn in the worker 158 | * @param {[type]} specs specs to be run in the worker process 159 | * @param {[type]} args object that will be merged with the main configuration once worker is initialised 160 | * @param {[type]} execArgv list of string arguments passed to the worker process 161 | */ 162 | // onWorkerStart: function (cid, caps, specs, args, execArgv) { 163 | // }, 164 | /** 165 | * Gets executed just before initialising the webdriver session and test framework. It allows you 166 | * to manipulate configurations depending on the capability or spec. 167 | * @param {Object} config wdio configuration object 168 | * @param {Array.} capabilities list of capabilities details 169 | * @param {Array.} specs List of spec file paths that are to be run 170 | */ 171 | // beforeSession: function (config, capabilities, specs) { 172 | // }, 173 | /** 174 | * Gets executed before test execution begins. At this point you can access to all global 175 | * variables like `browser`. It is the perfect place to define custom commands. 176 | * @param {Array.} capabilities list of capabilities details 177 | * @param {Array.} specs List of spec file paths that are to be run 178 | */ 179 | // before: function (capabilities, specs) { 180 | // }, 181 | /** 182 | * Runs before a WebdriverIO command gets executed. 183 | * @param {String} commandName hook command name 184 | * @param {Array} args arguments that command would receive 185 | */ 186 | // beforeCommand: function (commandName, args) { 187 | // }, 188 | /** 189 | * Hook that gets executed before the suite starts 190 | * @param {Object} suite suite details 191 | */ 192 | // beforeSuite: function (suite) { 193 | // }, 194 | /** 195 | * Function to be executed before a test (in Mocha/Jasmine) starts. 196 | */ 197 | // beforeTest: function (test, context) { 198 | // }, 199 | /** 200 | * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling 201 | * beforeEach in Mocha) 202 | */ 203 | // beforeHook: function (test, context) { 204 | // }, 205 | /** 206 | * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling 207 | * afterEach in Mocha) 208 | */ 209 | // afterHook: function (test, context, { error, result, duration, passed, retries }) { 210 | // }, 211 | /** 212 | * Function to be executed after a test (in Mocha/Jasmine). 213 | */ 214 | // afterTest: function(test, context, { error, result, duration, passed, retries }) { 215 | // }, 216 | 217 | 218 | /** 219 | * Hook that gets executed after the suite has ended 220 | * @param {Object} suite suite details 221 | */ 222 | // afterSuite: function (suite) { 223 | // }, 224 | /** 225 | * Runs after a WebdriverIO command gets executed 226 | * @param {String} commandName hook command name 227 | * @param {Array} args arguments that command would receive 228 | * @param {Number} result 0 - command success, 1 - command error 229 | * @param {Object} error error object if any 230 | */ 231 | // afterCommand: function (commandName, args, result, error) { 232 | // }, 233 | /** 234 | * Gets executed after all tests are done. You still have access to all global variables from 235 | * the test. 236 | * @param {Number} result 0 - test pass, 1 - test fail 237 | * @param {Array.} capabilities list of capabilities details 238 | * @param {Array.} specs List of spec file paths that ran 239 | */ 240 | // after: function (result, capabilities, specs) { 241 | // }, 242 | /** 243 | * Gets executed right after terminating the webdriver session. 244 | * @param {Object} config wdio configuration object 245 | * @param {Array.} capabilities list of capabilities details 246 | * @param {Array.} specs List of spec file paths that ran 247 | */ 248 | // afterSession: function (config, capabilities, specs) { 249 | // }, 250 | /** 251 | * Gets executed after all workers got shut down and the process is about to exit. An error 252 | * thrown in the onComplete hook will result in the test run failing. 253 | * @param {Object} exitCode 0 - success, 1 - fail 254 | * @param {Object} config wdio configuration object 255 | * @param {Array.} capabilities list of capabilities details 256 | * @param {} results object containing test results 257 | */ 258 | // onComplete: function(exitCode, config, capabilities, results) { 259 | // }, 260 | /** 261 | * Gets executed when a refresh happens. 262 | * @param {String} oldSessionId session ID of the old session 263 | * @param {String} newSessionId session ID of the new session 264 | */ 265 | //onReload: function(oldSessionId, newSessionId) { 266 | //} 267 | } 268 | --------------------------------------------------------------------------------