├── website ├── static │ ├── .nojekyll │ └── img │ │ ├── PNG │ │ ├── black.png │ │ ├── blue.png │ │ ├── color.png │ │ └── white.png │ │ ├── favicon.ico │ │ ├── SVG │ │ ├── black.svg │ │ ├── blue.svg │ │ └── white.svg │ │ ├── logo.svg │ │ ├── undraw_analyze_17kw.svg │ │ ├── undraw_Documents_re_isxv.svg │ │ ├── undraw_shared_goals_3d12.svg │ │ └── undraw_docusaurus_tree.svg ├── babel.config.js ├── sidebars.js ├── docs │ ├── doc2.md │ ├── mdx.md │ ├── doc3.md │ └── doc1.md ├── .gitignore ├── blog │ ├── 2019-05-28-hola.md │ ├── 2019-05-29-hello-world.md │ └── 2019-05-30-welcome.md ├── src │ ├── pages │ │ ├── styles.module.css │ │ └── index.js │ └── css │ │ └── custom.css ├── README.md ├── package.json └── docusaurus.config.js ├── core ├── .DS_Store ├── tsconfig.json ├── package-lock.json ├── package.json └── src │ ├── utils.spec.ts │ ├── key-types.ts │ ├── utils.ts │ └── index.ts ├── adapters ├── protractor │ ├── .vscode │ │ └── settings.json │ ├── tsconfig.json │ ├── package.json │ └── src │ │ ├── protractor.conf.ts │ │ ├── spec.ts │ │ └── index.ts ├── puppeteer │ ├── tsconfig.json │ ├── src │ │ ├── pptrVersionSelector.ts │ │ ├── pptr.spec.ts │ │ └── pptr-core.spec.ts │ └── package.json ├── selenium │ ├── tsconfig.json │ ├── package.json │ └── src │ │ ├── spec.ts │ │ └── index.ts ├── jsdom-react │ ├── tsconfig.json │ ├── wallaby.js │ ├── package.json │ └── src │ │ ├── index.ts │ │ └── spec.tsx ├── jsdom-svelte │ ├── tsconfig.json │ ├── package-lock.json │ ├── wallaby.js │ ├── src │ │ ├── spec.ts │ │ └── index.ts │ └── package.json └── playwright │ ├── tsconfig.json │ ├── package.json │ └── src │ └── spec.ts ├── test-suite ├── src │ ├── app │ │ ├── svelte │ │ │ ├── main.js │ │ │ ├── renderSvelteApp.d.ts │ │ │ ├── Events │ │ │ │ ├── MouseEvent.svelte │ │ │ │ ├── KeyboardEvent.svelte │ │ │ │ └── Events.svelte │ │ │ ├── renderSvelteApp.js │ │ │ ├── App.svelte │ │ │ ├── index.html │ │ │ └── Todo │ │ │ │ ├── TodoItem.svelte │ │ │ │ └── Todo.svelte │ │ └── react │ │ │ ├── index.tsx │ │ │ ├── events │ │ │ └── index.tsx │ │ │ └── todo-app │ │ │ └── index.tsx │ ├── utils.ts │ ├── server │ │ ├── client.ts │ │ ├── index.ejs │ │ └── index.ts │ ├── types.ts │ └── index.ts ├── README.md ├── tsconfig.json └── package.json ├── examples ├── src │ ├── index.ts │ ├── utils.ts │ ├── counter │ │ ├── counter.driver.ts │ │ ├── counter.tsx │ │ ├── counter.e2e.tsx │ │ └── counter.spec.tsx │ ├── client.tsx │ ├── index.ejs │ ├── server.ts │ ├── todo-app │ │ ├── todo-app.driver.ts │ │ ├── todo-app.e2e.tsx │ │ ├── todo-app.spec.tsx │ │ ├── todo-app-selenium.e2e.tsx │ │ └── todo-app.tsx │ ├── multi-counter │ │ ├── multi-counter.driver.ts │ │ ├── multi-counter.tsx │ │ ├── multi-counter.spec.tsx │ │ └── multi-counter.e2e.tsx │ └── shared.e2e.ts ├── tsconfig.json └── package.json ├── lerna.json ├── .gitignore ├── tsconfig.json ├── migrating.md ├── package.json ├── LICENSE ├── tslint.json ├── .circleci └── config.yml └── README.md /website/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wix-incubator/unidriver/HEAD/core/.DS_Store -------------------------------------------------------------------------------- /adapters/protractor/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /website/static/img/PNG/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wix-incubator/unidriver/HEAD/website/static/img/PNG/black.png -------------------------------------------------------------------------------- /website/static/img/PNG/blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wix-incubator/unidriver/HEAD/website/static/img/PNG/blue.png -------------------------------------------------------------------------------- /website/static/img/PNG/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wix-incubator/unidriver/HEAD/website/static/img/PNG/color.png -------------------------------------------------------------------------------- /website/static/img/PNG/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wix-incubator/unidriver/HEAD/website/static/img/PNG/white.png -------------------------------------------------------------------------------- /website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wix-incubator/unidriver/HEAD/website/static/img/favicon.ico -------------------------------------------------------------------------------- /website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /test-suite/src/app/svelte/main.js: -------------------------------------------------------------------------------- 1 | import { renderSvelteApp } from './renderSvelteApp'; 2 | 3 | renderSvelteApp(document.body); 4 | -------------------------------------------------------------------------------- /examples/src/index.ts: -------------------------------------------------------------------------------- 1 | import { startServer } from './server'; 2 | 3 | startServer(4013); 4 | 5 | console.log('running on port 4013'); -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | someSidebar: { 3 | Docusaurus: ['doc1', 'doc2', 'doc3'], 4 | Features: ['mdx'], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /test-suite/src/app/svelte/renderSvelteApp.d.ts: -------------------------------------------------------------------------------- 1 | import {RenderTestApp} from "../../types"; 2 | 3 | export declare const renderSvelteApp: RenderTestApp; 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "independent", 3 | "packages": [ 4 | "core", 5 | "test-suite", 6 | "examples", 7 | "adapters/*" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /website/docs/doc2.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: doc2 3 | title: Document Number 2 4 | --- 5 | 6 | This is a link to [another document.](doc3.md) This is a link to an [external page.](http://www.example.com/) 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | npm-debug.log 4 | # package-lock.json 5 | /*.js 6 | /*.d.ts 7 | !wallaby.js 8 | !protractor.conf.js 9 | .idea 10 | lerna-debug.log 11 | .DS_Store 12 | .cache 13 | -------------------------------------------------------------------------------- /test-suite/src/app/svelte/Events/MouseEvent.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 |
7 | {event.type} 8 |
9 | -------------------------------------------------------------------------------- /test-suite/README.md: -------------------------------------------------------------------------------- 1 | # Test Suite 2 | 3 | 4 | ## Test App 5 | This is a helper app consisting of a working "to-do app" and an events tester app. 6 | While assuming the apps themselves work properly, the testsuite will run adapter tests on it. -------------------------------------------------------------------------------- /test-suite/src/app/svelte/Events/KeyboardEvent.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 |
7 | {event.key}{event.keyCode} 8 |
9 | -------------------------------------------------------------------------------- /core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src/**/*.ts", 8 | "src/**/*.tsx" 9 | ], 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } -------------------------------------------------------------------------------- /test-suite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "types": ["mocha", "node"] 6 | }, 7 | "include": [ 8 | "src/**/*.ts", 9 | "src/**/*.tsx" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /adapters/puppeteer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src/**/*.ts", 8 | "src/**/*.tsx" 9 | ], 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } -------------------------------------------------------------------------------- /adapters/selenium/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src/**/*.ts", 8 | "src/**/*.tsx" 9 | ], 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } -------------------------------------------------------------------------------- /adapters/jsdom-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src/**/*.ts", 8 | "src/**/*.tsx" 9 | ], 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } -------------------------------------------------------------------------------- /adapters/jsdom-svelte/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src/**/*.ts", 8 | "src/**/*.tsx" 9 | ], 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } -------------------------------------------------------------------------------- /adapters/playwright/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src/**/*.ts", 8 | "src/**/*.tsx" 9 | ], 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } -------------------------------------------------------------------------------- /adapters/protractor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src/**/*.ts", 8 | "src/**/*.tsx", 9 | ], 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "types": ["mocha", "node"] 6 | }, 7 | "include": [ 8 | "src/**/*.ts", 9 | "src/**/*.tsx" 10 | ], 11 | "exclude": [ 12 | "node_modules" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /test-suite/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { TodoItem } from "./types"; 2 | 3 | export const itemCreator = (partial: Partial) => { 4 | return { 5 | label: "default", 6 | completed: false, 7 | ...partial, 8 | }; 9 | }; 10 | 11 | export const sleep = (milliseconds: number) => 12 | new Promise((resolve) => setTimeout(resolve, milliseconds)); 13 | -------------------------------------------------------------------------------- /core/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unidriver/core", 3 | "version": "1.3.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "typescript": { 8 | "version": "3.9.7", 9 | "resolved": "https://repo.dev.wixpress.com/artifactory/api/npm/npm-repos/typescript/-/typescript-3.9.7.tgz", 10 | "integrity": "sha1-mNYApevcOPQMsndSLxLcgA6eJfo=", 11 | "dev": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Browser, Page } from 'puppeteer'; 2 | 3 | export const freePort = require('find-free-port-sync')(); 4 | export const defaultUrl = `http://localhost:${freePort}`; 5 | 6 | export const goAndWait = async (browser: Browser, url: string = defaultUrl): Promise => { 7 | const page = await browser.newPage(); 8 | await page.goto(url, {waitUntil: 'networkidle2'}); 9 | return page; 10 | }; 11 | -------------------------------------------------------------------------------- /adapters/jsdom-svelte/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unidriver/jsdom-svelte", 3 | "version": "1.2.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "svelte": { 8 | "version": "3.30.1", 9 | "resolved": "https://repo.dev.wixpress.com/artifactory/api/npm/npm-repos/svelte/-/svelte-3.30.1.tgz", 10 | "integrity": "sha1-k1K/e8gidzU8+qOmr8RtOqNWPg0=", 11 | "dev": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test-suite/src/app/svelte/renderSvelteApp.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | export const renderSvelteApp = (element, params = {}) => { 4 | const app = new App({ 5 | target: element, 6 | props: { 7 | initialItems: params.items, 8 | initialText: params.initialText, 9 | inputDisabled: params.inputDisabled, 10 | inputReadOnly: params.inputReadOnly, 11 | }, 12 | }); 13 | 14 | return () => app.$destroy(); 15 | }; 16 | -------------------------------------------------------------------------------- /test-suite/src/server/client.ts: -------------------------------------------------------------------------------- 1 | import { renderTestApp } from "../app/react"; 2 | 3 | const w: any = window; 4 | 5 | const b64 = w.__init; 6 | 7 | let data = {items: []}; 8 | 9 | try { 10 | const raw = atob(b64); 11 | data = JSON.parse(raw); 12 | } catch (e) { 13 | console.error('error parsing data, falling back to default'); 14 | } 15 | 16 | const root = document.getElementById('root') as HTMLElement; 17 | renderTestApp(root, data); 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /website/blog/2019-05-28-hola.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: hola 3 | title: Hola 4 | author: Gao Wei 5 | author_title: Docusaurus Core Team 6 | author_url: https://github.com/wgao19 7 | author_image_url: https://avatars1.githubusercontent.com/u/2055384?v=4 8 | tags: [hola, docusaurus] 9 | --- 10 | 11 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 12 | -------------------------------------------------------------------------------- /adapters/puppeteer/src/pptrVersionSelector.ts: -------------------------------------------------------------------------------- 1 | import { ElementHandle as pptrElementHandle, Page as pptrPage, Frame as pptrFrame } from 'puppeteer'; 2 | import { ElementHandle as pptrCoreElementHandle, Page as pptrCorePage, Frame as pptrCoreFrame } from 'puppeteer-core'; 3 | 4 | export { pptrPage, pptrCorePage}; 5 | 6 | export type ElementHandle = pptrCoreElementHandle | pptrElementHandle; 7 | export type Page = pptrCorePage | pptrPage; 8 | export type Frame = pptrCoreFrame | pptrFrame; 9 | -------------------------------------------------------------------------------- /examples/src/counter/counter.driver.ts: -------------------------------------------------------------------------------- 1 | import { UniDriver } from '@unidriver/core'; 2 | 3 | export type CounterDriver = { 4 | val: () => Promise, 5 | increase: () => Promise, 6 | decrease: () => Promise 7 | }; 8 | 9 | export const counterDriver = (base: UniDriver): CounterDriver => { 10 | return { 11 | val: async () => base.$('.value').text().then(parseInt), 12 | increase: base.$('.inc').click, 13 | decrease: base.$('.dec').click 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /test-suite/src/app/svelte/App.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 | 14 |
15 | -------------------------------------------------------------------------------- /test-suite/src/types.ts: -------------------------------------------------------------------------------- 1 | export type TodoItem = { 2 | label: string; 3 | completed: boolean; 4 | id?: string; 5 | }; 6 | 7 | export type TodoAppProps = { 8 | items: TodoItem[]; 9 | initialText?: string; 10 | inputDisabled?: boolean; 11 | inputReadOnly?: boolean; 12 | }; 13 | 14 | 15 | export type EventsAppProps = {}; 16 | 17 | export type TestAppProps = TodoAppProps & EventsAppProps; 18 | 19 | export type RenderTestApp = (element: HTMLElement, props?: TestAppProps) => () => any; 20 | -------------------------------------------------------------------------------- /website/blog/2019-05-29-hello-world.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: hello-world 3 | title: Hello 4 | author: Endilie Yacop Sucipto 5 | author_title: Maintainer of Docusaurus 6 | author_url: https://github.com/endiliey 7 | author_image_url: https://avatars1.githubusercontent.com/u/17883920?s=460&v=4 8 | tags: [hello, docusaurus] 9 | --- 10 | 11 | Welcome to this blog. This blog is created with [**Docusaurus 2 alpha**](https://v2.docusaurus.io/). 12 | 13 | 14 | 15 | This is a test post. 16 | 17 | A whole bunch of other information. 18 | -------------------------------------------------------------------------------- /website/blog/2019-05-30-welcome.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: welcome 3 | title: Welcome 4 | author: Yangshun Tay 5 | author_title: Front End Engineer @ Facebook 6 | author_url: https://github.com/yangshun 7 | author_image_url: https://avatars0.githubusercontent.com/u/1315101?s=400&v=4 8 | tags: [facebook, hello, docusaurus] 9 | --- 10 | 11 | Blog features are powered by the blog plugin. Simply add files to the `blog` directory. It supports tags as well! 12 | 13 | Delete the whole directory if you don't want the blog features. As simple as that! 14 | -------------------------------------------------------------------------------- /core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unidriver/core", 3 | "version": "1.3.0", 4 | "description": "UniDriver core", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "rm -rf dist && tsc -p .", 9 | "test": "mocha ./dist/**/*.spec.js" 10 | }, 11 | "devDependencies": { 12 | "@types/chai": "^4.1.7", 13 | "chai": "^4.2.0", 14 | "mocha": "^6.0.0", 15 | "typescript": "^3.3.3" 16 | }, 17 | "author": "Wix.com", 18 | "license": "MIT", 19 | "gitHead": "c793eb6b7c1d687fe054bf47fb11224028b51f77" 20 | } 21 | -------------------------------------------------------------------------------- /examples/src/client.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { TodoApp } from './todo-app/todo-app'; 4 | import { Counter } from './counter/counter'; 5 | import { MultiCounter } from './multi-counter/multi-counter'; 6 | 7 | const showCase = (
8 |

Todo app

9 | 10 | 11 |

Counter

12 |
13 | 14 |
15 | 16 |

Multi Counter

17 | 18 |
); 19 | 20 | ReactDOM.render(showCase, document.querySelector('#root')); 21 | -------------------------------------------------------------------------------- /test-suite/src/app/svelte/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Svelte + Typescript 8 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /website/docs/mdx.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: mdx 3 | title: Powered by MDX 4 | --- 5 | 6 | You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/). 7 | 8 | export const Highlight = ({children, color}) => ( {children} ); 14 | 15 | Docusaurus green and Facebook blue are my favorite colors. 16 | 17 | I can write **Markdown** alongside my _JSX_! 18 | -------------------------------------------------------------------------------- /adapters/jsdom-react/wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function (wallaby) { 2 | 3 | 4 | return { 5 | files: [ 6 | 'src/**/*.ts', 7 | 'src/**/*.tsx', 8 | {pattern: 'src/**/spec.tsx', ignore: true}, 9 | {pattern: 'src/**/spec.ts', ignore: true}, 10 | {pattern: 'node_modules/**/*', ignore: true} 11 | ], 12 | tests: [ 13 | 'src/spec.tsx' 14 | ], 15 | compilers: { 16 | 'src/**/*.tsx?': wallaby.compilers.typeScript({ 17 | module: 'commonjs', 18 | jsx: 'react' 19 | }) 20 | }, 21 | testFramework: 'mocha', 22 | env: { 23 | type: 'node' 24 | }, 25 | setup: function (wallaby) { 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /adapters/jsdom-svelte/wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function (wallaby) { 2 | 3 | 4 | return { 5 | files: [ 6 | 'src/**/*.ts', 7 | 'src/**/*.tsx', 8 | {pattern: 'src/**/spec.tsx', ignore: true}, 9 | {pattern: 'src/**/spec.ts', ignore: true}, 10 | {pattern: 'node_modules/**/*', ignore: true} 11 | ], 12 | tests: [ 13 | 'src/spec.tsx' 14 | ], 15 | compilers: { 16 | 'src/**/*.tsx?': wallaby.compilers.typeScript({ 17 | module: 'commonjs', 18 | jsx: 'react' 19 | }) 20 | }, 21 | testFramework: 'mocha', 22 | env: { 23 | type: 'node' 24 | }, 25 | setup: function (wallaby) { 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2017", 5 | "strict": true, 6 | "noImplicitAny": true, 7 | "noImplicitReturns": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "sourceMap": true, 12 | "skipLibCheck": true, 13 | "outDir": "dist", 14 | "moduleResolution": "node", 15 | "lib": ["dom", "es2016"], 16 | "jsx": "react", 17 | "declaration": true, 18 | "declarationMap": true 19 | }, 20 | "exclude": [ 21 | "node_modules" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /core/src/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | 3 | import { waitFor } from './utils'; 4 | 5 | describe('utils', () => { 6 | describe('waitFor', () => { 7 | const createLongFn = (ms: number) => { 8 | const now = Date.now(); 9 | return async () => { 10 | return (Date.now() - now) > ms; 11 | }; 12 | }; 13 | 14 | it('works', async () => { 15 | await waitFor(createLongFn(100)); 16 | }); 17 | 18 | it('has configurable timeout', async () => { 19 | try { 20 | await waitFor(createLongFn(100), 50); 21 | assert.equal('should not', 'get here'); 22 | } catch (e) { 23 | // this is good 24 | } 25 | }); 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /test-suite/src/index.ts: -------------------------------------------------------------------------------- 1 | import { UniDriver } from '@unidriver/core'; 2 | 3 | import { TestAppProps } from "./types"; 4 | 5 | export type SetupFn = (data: TestAppProps) => Promise<{ driver: UniDriver, tearDown: () => Promise }>; 6 | 7 | export type TestSuiteParams = { 8 | setup: SetupFn; 9 | before?: () => Promise, 10 | after?: () => Promise, 11 | }; 12 | 13 | 14 | export { renderTestApp } from './app/react'; 15 | export { renderSvelteApp } from './app/svelte/renderSvelteApp'; 16 | 17 | export { startServer as startTestAppServer, getTestAppUrl } from './server'; 18 | 19 | export { runTestSuite } from './run-test-suite'; 20 | export { itemCreator } from './utils'; 21 | -------------------------------------------------------------------------------- /examples/src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Title 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 |
19 | Loading 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test-suite/src/app/react/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ReactDOM = require('react-dom'); 3 | 4 | import { TodoApp } from './todo-app'; 5 | import { EventsApp } from './events'; 6 | import {RenderTestApp, TestAppProps} from '../../types'; 7 | 8 | export class TestApp extends React.Component { 9 | render() { 10 | return (
11 | 12 | 13 |
); 14 | } 15 | } 16 | 17 | export const renderTestApp: RenderTestApp = (element, params) => { 18 | const comp = React.createElement(TestApp, params); 19 | ReactDOM.render(comp, element); 20 | return () => ReactDOM.unmountComponentAtNode(element); 21 | }; 22 | export {TestAppProps}; 23 | -------------------------------------------------------------------------------- /test-suite/src/app/svelte/Todo/TodoItem.svelte: -------------------------------------------------------------------------------- 1 | 10 | 13 | 14 |
19 | 20 | {item.label} 21 | {#if item.completed} 22 | Completed! 23 | {/if} 24 | 25 |
26 | -------------------------------------------------------------------------------- /adapters/protractor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unidriver/protractor", 3 | "version": "3.1.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "rm -rf dist && tsc -p .", 9 | "pretest": "test $CI || webdriver-manager update", 10 | "test": "(protractor ./dist/protractor.conf.js) || true" 11 | }, 12 | "author": "Wix.com", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@unidriver/core": "^1.3.0", 16 | "chromedriver": "^96.0.0", 17 | "selenium-webdriver": "^3.6.0" 18 | }, 19 | "peerDependencies": { 20 | "protractor": "^5.0.0" 21 | }, 22 | "devDependencies": { 23 | "@unidriver/test-suite": "^1.3.0", 24 | "protractor": "^5.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /adapters/playwright/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unidriver/playwright", 3 | "version": "1.1.6", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "rm -rf dist && tsc -p .", 9 | "test": "mocha --timeout 20000 dist/**/spec.js" 10 | }, 11 | "keywords": [], 12 | "author": "Wix.com", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@unidriver/core": "^1.3.0" 16 | }, 17 | "devDependencies": { 18 | "@unidriver/test-suite": "^1.3.0", 19 | "find-free-port-sync": "^1.0.0", 20 | "mocha": "^6.0.0", 21 | "playwright": "^1.8.0" 22 | }, 23 | "peerDependencies": { 24 | "playwright": "^1.8.0" 25 | }, 26 | "gitHead": "c793eb6b7c1d687fe054bf47fb11224028b51f77" 27 | } 28 | -------------------------------------------------------------------------------- /examples/src/server.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as ejs from 'ejs'; 3 | import {readFileSync} from 'fs'; 4 | import * as path from 'path'; 5 | import {Server} from 'http'; 6 | 7 | export const startServer = (port: number): Promise => { 8 | 9 | const app = express(); 10 | 11 | return new Promise((resolve) => { 12 | 13 | app.use(express.static('dist')); 14 | app.use(express.static('node_modules')); 15 | 16 | app.get('/', (_, res) => { 17 | const template = readFileSync(path.resolve(__dirname, './index.ejs'), 'utf-8'); 18 | res.send(ejs.render(template, {bundleUrl: 'bundle.js'})); 19 | }); 20 | 21 | const server = app.listen(port); 22 | resolve(server); 23 | 24 | }); 25 | }; 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /website/src/pages/styles.module.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | 3 | /** 4 | * CSS files with the .module.css suffix will be treated as CSS modules 5 | * and scoped locally. 6 | */ 7 | 8 | .heroBanner { 9 | padding: 4rem 0; 10 | text-align: center; 11 | position: relative; 12 | overflow: hidden; 13 | } 14 | 15 | @media screen and (max-width: 966px) { 16 | .heroBanner { 17 | padding: 2rem; 18 | } 19 | } 20 | 21 | .buttons { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | } 26 | 27 | .features { 28 | display: flex; 29 | align-items: center; 30 | padding: 2rem 0; 31 | width: 100%; 32 | } 33 | 34 | .featureImage { 35 | height: 200px; 36 | width: 200px; 37 | } 38 | -------------------------------------------------------------------------------- /adapters/jsdom-svelte/src/spec.ts: -------------------------------------------------------------------------------- 1 | import {renderSvelteApp, SetupFn, runTestSuite} from '@unidriver/test-suite'; 2 | import {jsdomSvelteUniDriver} from './index'; 3 | 4 | const setup: SetupFn = async (params) => { 5 | const cleanJsdom = require('jsdom-global')(); 6 | const div = document.createElement('div'); 7 | document.body.appendChild(div); 8 | const cleanApp = renderSvelteApp(div, params); 9 | const driver = jsdomSvelteUniDriver(div); 10 | 11 | const tearDown = () => { 12 | cleanApp(); 13 | cleanJsdom(); 14 | return Promise.resolve(); 15 | }; 16 | 17 | return {driver, tearDown}; 18 | }; 19 | // 20 | describe('svelte base driver - test suite', () => { 21 | runTestSuite({ 22 | setup 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/src/counter/counter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export type CounterProps = { 4 | init: number; 5 | }; 6 | 7 | export type State = { 8 | counter: number; 9 | }; 10 | 11 | export class Counter extends React.PureComponent { 12 | state: State = { 13 | counter: this.props.init 14 | }; 15 | 16 | incBy = (d: number) => this.setState({counter: this.state.counter + d}); 17 | 18 | inc = () => this.incBy(1); 19 | 20 | dec = () => this.incBy(-1); 21 | 22 | render () { 23 | return ( 24 |
25 | 26 | {this.state.counter} 27 | 28 |
); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /adapters/selenium/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unidriver/selenium", 3 | "version": "1.3.6", 4 | "description": "", 5 | "typings": "dist/index.d.ts", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "build": "rm -rf dist && tsc -p .", 9 | "test": "mocha --timeout 20000 dist/**/spec.js" 10 | }, 11 | "dependencies": { 12 | "@unidriver/core": "^1.3.0", 13 | "chromedriver": "^103.0.0", 14 | "selenium-webdriver": "^3.6.0" 15 | }, 16 | "devDependencies": { 17 | "@types/selenium-webdriver": "^3.0.15", 18 | "@unidriver/test-suite": "^1.3.0", 19 | "find-free-port-sync": "^1.0.0", 20 | "mocha": "^6.0.0" 21 | }, 22 | "keywords": [], 23 | "author": "Wix.com", 24 | "license": "MIT", 25 | "gitHead": "c793eb6b7c1d687fe054bf47fb11224028b51f77" 26 | } 27 | -------------------------------------------------------------------------------- /test-suite/src/server/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Title 5 | 6 | 7 | 8 | 9 | 10 | 16 | 19 | 20 | 21 |
22 | Loading 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /migrating.md: -------------------------------------------------------------------------------- 1 | ### Migration from `unidriver@~2.0.1` to `@unidriver/* latest` 2 | 3 | If you are migrating from a previous version of `unidriver`, before the seperation of adapters, you will need to take the following breaking change into account: 4 | 5 | | method | `unidriver@2.0.1` | `@unidriver/* latest` | 6 | |-----------|-------------------|---------------------| 7 | | `pupUniDriver` | `(el: ElementGetter): UniDriver` | `( el: ElementGetter \| BaseElementContainer): UniDriver` | 8 | | `reactUniDriver` | `reactUniDriver` | `jsdomReactUniDriver`| 9 | | `attr` | `(name: string) => Promise;` | `(name: string) => Promise` | 10 | | `$$(selector).get(idx)` - When selector did not yield any element | `get(idx).exists()` returns true | `get(idx).exists()` returns false | 11 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. 4 | 5 | ## Installation 6 | 7 | ```console 8 | yarn install 9 | ``` 10 | 11 | ## Local Development 12 | 13 | ```console 14 | yarn start 15 | ``` 16 | 17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ## Build 20 | 21 | ```console 22 | yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ## Deployment 28 | 29 | ```console 30 | GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /adapters/protractor/src/protractor.conf.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'http'; 2 | import { browser } from 'protractor'; 3 | import { startTestAppServer } from '@unidriver/test-suite'; 4 | 5 | export const port = require('find-free-port-sync')(); 6 | let server: Server; 7 | const args = process.env.CI ? [ 8 | '--no-sandbox', 9 | '--headless', 10 | '--disable-dev-shm-usage' 11 | ] : []; 12 | 13 | exports.config = { 14 | framework: 'jasmine', 15 | onPrepare: async () => { 16 | await browser.waitForAngularEnabled(false); 17 | }, 18 | capabilities: { 19 | 'browserName': 'chrome', 20 | 'chromeOptions': { 21 | 'args': args 22 | } 23 | }, 24 | directConnect: true, 25 | specs: ['./**/spec.js'], 26 | beforeLaunch: function () { 27 | startTestAppServer( port ).then(srvr => server = srvr); 28 | }, 29 | afterLaunch: function () { 30 | server.close(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /adapters/puppeteer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unidriver/puppeteer", 3 | "version": "2.2.6", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "rm -rf dist && tsc -p .", 9 | "test": "mocha --timeout 20000 dist/**/*.spec.js" 10 | }, 11 | "keywords": [], 12 | "author": "Wix.com", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@unidriver/core": "^1.3.0" 16 | }, 17 | "devDependencies": { 18 | "@unidriver/test-suite": "^1.3.0", 19 | "find-free-port-sync": "^1.0.0", 20 | "locate-chrome": "^0.1.1", 21 | "mocha": "^6.0.0", 22 | "puppeteer": "^14.0.0", 23 | "puppeteer-core": "^14.0.0" 24 | }, 25 | "peerDependencies": { 26 | "puppeteer": "^5.5.0 || ^14.0.0", 27 | "puppeteer-core": "^10.1.0 || ^14.0.0" 28 | }, 29 | "gitHead": "c793eb6b7c1d687fe054bf47fb11224028b51f77" 30 | } 31 | -------------------------------------------------------------------------------- /adapters/jsdom-svelte/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unidriver/jsdom-svelte", 3 | "version": "1.2.0", 4 | "description": "JSDom Svelte adapter for Unidriver", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "rm -rf dist && tsc -p .", 9 | "test": "mocha dist/spec.js" 10 | }, 11 | "author": "Wix.com", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@unidriver/core": "^1.3.0" 15 | }, 16 | "devDependencies": { 17 | "@testing-library/svelte": "^1.11.0", 18 | "@types/chai": "^4.1.7", 19 | "@types/sinon": "^7.0.10", 20 | "@unidriver/test-suite": "^1.3.0", 21 | "chai": "^4.2.0", 22 | "jsdom": "15.1.1", 23 | "jsdom-global": "^3.0.2", 24 | "mocha": "^6.0.0", 25 | "sinon": "^7.3.0", 26 | "svelte": "~3.27.0" 27 | }, 28 | "peerDependencies": { 29 | "svelte": "^3.0.0" 30 | }, 31 | "gitHead": "c793eb6b7c1d687fe054bf47fb11224028b51f77" 32 | } 33 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "serve": "docusaurus serve", 12 | "clear": "docusaurus clear" 13 | }, 14 | "dependencies": { 15 | "@docusaurus/core": "2.0.0-alpha.70", 16 | "@docusaurus/preset-classic": "2.0.0-alpha.70", 17 | "@mdx-js/react": "^1.6.21", 18 | "clsx": "^1.1.1", 19 | "react": "^16.8.4", 20 | "react-dom": "^16.8.4" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.5%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | } 34 | } -------------------------------------------------------------------------------- /test-suite/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "http"; 2 | import * as express from 'express'; 3 | import { readFileSync } from 'fs'; 4 | import * as ejs from 'ejs'; 5 | import * as path from 'path'; 6 | import { TestAppProps } from "../types"; 7 | 8 | export const startServer = (port: number): Promise => { 9 | const app = express(); 10 | 11 | return new Promise((resolve) => { 12 | app.use(express.static(path.join(__dirname, '..'))); 13 | 14 | app.get('/', (req, res) => { 15 | const template = readFileSync(path.resolve(__dirname, 'index.ejs'), 'utf-8'); 16 | res.send(ejs.render(template, {initData: req.query.data})); 17 | }); 18 | 19 | const server = app.listen(port); 20 | resolve(server); 21 | }); 22 | }; 23 | 24 | export const getTestAppUrl = (data: Partial) => { 25 | const str = JSON.stringify(data); 26 | const b64 = Buffer.from(str).toString('base64'); 27 | return `/?data=${b64}`; 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | /** 3 | * Any CSS included here will be global. The classic template 4 | * bundles Infima by default. Infima is a CSS framework designed to 5 | * work well for content-centric websites. 6 | */ 7 | 8 | /* You can override the default Infima variables here. */ 9 | :root { 10 | --ifm-color-primary: #552ce5; 11 | --ifm-color-primary-dark: #451bdb; 12 | --ifm-color-primary-darker: #4219cf; 13 | --ifm-color-primary-darkest: #3615aa; 14 | --ifm-color-primary-light: #6944e8; 15 | --ifm-color-primary-lighter: #7250e9; 16 | --ifm-color-primary-lightest: #9075ee; 17 | --ifm-code-font-size: 95%; 18 | } 19 | 20 | .docusaurus-highlight-code-line { 21 | background-color: rgb(72, 77, 91); 22 | display: block; 23 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 24 | padding: 0 var(--ifm-pre-padding); 25 | } 26 | 27 | .button.button--secondary { 28 | color: white !important; 29 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unidriver-monorepo", 3 | "repository": "https://github.com/wix-incubator/unidriver", 4 | "scripts": { 5 | "bootstrap": "lerna bootstrap --hoist", 6 | "build": "lerna run --stream build", 7 | "test": "lerna run --stream test --concurrency 1", 8 | "test-no-bail": "lerna run --stream --no-bail test", 9 | "clean": "lerna clean -y", 10 | "release": "lerna publish", 11 | "start": "npm run build && npm run test", 12 | "prerelease": "npm run build && npm run test" 13 | }, 14 | "author": "Wix.com", 15 | "license": "MIT", 16 | "files": [ 17 | "dist", 18 | "src", 19 | "*.d.ts", 20 | "*.js" 21 | ], 22 | "private": true, 23 | "devDependencies": { 24 | "@types/mocha": "^2.2.37", 25 | "@types/node": "^7.0.0", 26 | "lerna": "^3.11.1", 27 | "typescript": "^3.8.0" 28 | }, 29 | "version": "1.0.0", 30 | "main": "index.js", 31 | "workspaces": [ 32 | "core", 33 | "test-suite", 34 | "examples", 35 | "adapters/*" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /adapters/protractor/src/spec.ts: -------------------------------------------------------------------------------- 1 | import { browser, $ } from 'protractor'; 2 | import { SetupFn, runTestSuite, getTestAppUrl } from '@unidriver/test-suite'; 3 | import { protractorUniDriver } from '.'; 4 | import { assert } from 'chai'; 5 | 6 | import { port } from './protractor.conf'; 7 | 8 | const setup: SetupFn = async (data) => { 9 | await browser.get(`http://localhost:${port}${getTestAppUrl(data)}`); 10 | const driver = protractorUniDriver(() => Promise.resolve($('body'))); 11 | const tearDown = async () => {}; 12 | return { driver, tearDown }; 13 | }; 14 | 15 | describe('protractor', () => { 16 | runTestSuite({ setup }); 17 | }); 18 | 19 | describe('protractor specific tests', () => { 20 | it(`doesn't attempt to clear value when shouldClear is false`, async () => { 21 | const { driver } = await setup({ items: [], initialText: 'hello' }); 22 | await driver 23 | .$('header input') 24 | .enterValue(' world!', { shouldClear: false }); 25 | assert.equal(await driver.$('header input').value(), 'hello world!'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /adapters/jsdom-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unidriver/jsdom-react", 3 | "version": "1.7.0", 4 | "description": "JSDom React adapter for UniDriver", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "rm -rf dist && tsc -p .", 9 | "test": "mocha --timeout 20000 dist/spec.js" 10 | }, 11 | "author": "Wix.com", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@unidriver/core": "^1.3.0" 15 | }, 16 | "devDependencies": { 17 | "@types/chai": "^4.1.7", 18 | "@types/react": "^16.0.0", 19 | "@types/react-dom": "^16.0.0", 20 | "@types/sinon": "^7.0.10", 21 | "@unidriver/test-suite": "^1.3.0", 22 | "chai": "^4.2.0", 23 | "jsdom": "15.1.1", 24 | "jsdom-global": "^3.0.2", 25 | "mocha": "^6.0.0", 26 | "react": "^16.0.0", 27 | "react-dom": "^16.0.0", 28 | "sinon": "^7.3.0" 29 | }, 30 | "peerDependencies": { 31 | "@types/react": "^16.0.0", 32 | "@types/react-dom": "^16.0.0", 33 | "react": "^16.0.0", 34 | "react-dom": "^16.0.0" 35 | }, 36 | "gitHead": "c793eb6b7c1d687fe054bf47fb11224028b51f77" 37 | } 38 | -------------------------------------------------------------------------------- /core/src/key-types.ts: -------------------------------------------------------------------------------- 1 | import {keyDefinitions, PuppeteerEventDefinition} from './puppeteer-us-keyboard-layout'; 2 | 3 | export type KeyDefinitionType = keyof typeof keyDefinitions; 4 | 5 | const nonTextKeyTypes: KeyDefinitionType[] = [ 6 | 'Backspace', 'Tab', 'Enter', 'Pause', 'Escape', 'PageUp', 'PageDown', 'End', 'Home', 7 | 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'Insert', 'Delete', 'Semicolon', 'Space', 8 | // 'Numpad0', 'Numpad1', 'Numpad2', 'Numpad3', 'Numpad4', 'Numpad5', 'Numpad6', 'Numpad7', 'Numpad8', 'Numpad9', 9 | 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'Shift', 'Control', 'Alt', 'Meta' 10 | ]; 11 | 12 | const normalizeEventData = ({key, keyCode}: PuppeteerEventDefinition) => ({ key, keyCode }); 13 | 14 | export const getDefinitionForKeyType = (keyType: KeyDefinitionType) => normalizeEventData(keyDefinitions[keyType] as PuppeteerEventDefinition); 15 | 16 | export const getAllNonTextKeyTypes = () => (Object.keys(keyDefinitions) as KeyDefinitionType[]).filter(keyType => nonTextKeyTypes.includes(keyType)) as KeyDefinitionType[]; 17 | -------------------------------------------------------------------------------- /examples/src/todo-app/todo-app.driver.ts: -------------------------------------------------------------------------------- 1 | import { UniDriver } from '@unidriver/core'; 2 | 3 | export type TodoAppDriver = { 4 | getItems: () => Promise, 5 | addItem: (text: string) => Promise, 6 | deleteItem: (idx: number) => Promise, 7 | getCount: () => Promise, 8 | isDone: (idx: number) => Promise, 9 | toggleItem: (idx: number) => Promise 10 | }; 11 | 12 | export const todoAppDriver = (wrapper: UniDriver): TodoAppDriver => { 13 | 14 | const base = wrapper.$('.todo-app'); 15 | return { 16 | getItems: async () => base.$$('.title').text(), 17 | addItem: async (text: string) => { 18 | await base.$('input').enterValue(text); 19 | await base.$('.add').click(); 20 | }, 21 | deleteItem: async (idx: number) => base.$$('.item').get(idx).$('.delete').click(), 22 | getCount: async () => parseInt(await base.$('.count').text(), 10), 23 | isDone: async (idx: number) => { 24 | return base.$$('.item').get(idx).hasClass('done'); 25 | }, 26 | toggleItem: async (idx: number) => base.$$('.item').get(idx).$('.title').click() 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /core/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); 2 | 3 | export const waitFor = async (fn: () => Promise, timeout = 1200, retryDelay = 30, customError?: string): Promise => { 4 | if (timeout < 0) { 5 | const err = customError || `[Timeout exceeded while waiting for value to become true]`; 6 | throw new Error(err); 7 | } 8 | const val = await fn(); 9 | if (!val) { 10 | const now = Date.now(); 11 | await delay(retryDelay); 12 | const delta = Date.now() - now; 13 | return waitFor(fn, timeout - delta, retryDelay, customError); 14 | } 15 | }; 16 | 17 | export const eventually = async (callback: () => void, timeout = 1200, retryDelay = 30, lastError: any = null): Promise => { 18 | if (timeout < 0) { 19 | throw new Error(`[Eventually timeout exceeded after: timeout with error]: ${lastError}`); 20 | } 21 | try { 22 | await callback(); 23 | } catch (e) { 24 | const now = Date.now(); 25 | await delay(retryDelay); 26 | const delta = Date.now() - now; 27 | return eventually(callback, timeout - delta, retryDelay, e); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /examples/src/multi-counter/multi-counter.driver.ts: -------------------------------------------------------------------------------- 1 | import { UniDriver } from '@unidriver/core'; 2 | import { counterDriver } from '../counter/counter.driver'; 3 | 4 | export type CounterDriver = { 5 | val: (idx: number) => Promise, 6 | increase: (idx: number) => Promise, 7 | decrease: (idx: number) => Promise, 8 | add: () => Promise, 9 | remove: (idx: number) => Promise, 10 | count: () => Promise 11 | }; 12 | 13 | export const multiCounterDriver = (wrapper: UniDriver): CounterDriver => { 14 | const counterDriverByIdx = (idx: number) => { 15 | return counterDriver(base.$$('.counter-wrapper').get(idx)); 16 | }; 17 | 18 | const base = wrapper.$('.multi-counter'); 19 | return { 20 | val: (idx: number) => counterDriverByIdx(idx).val(), 21 | increase: (idx: number) => counterDriverByIdx(idx).increase(), 22 | decrease: (idx: number) => counterDriverByIdx(idx).decrease(), 23 | add: () => base.$('.add').click(), 24 | remove: (idx: number) => base.$$('.counter-wrapper').get(idx).$('.remove').click(), 25 | count: () => base.$$('.counter-wrapper').count() 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wix Incubator 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/src/multi-counter/multi-counter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Counter } from '../counter/counter'; 3 | 4 | export type State = { 5 | counters: string[]; 6 | }; 7 | 8 | export class MultiCounter extends React.PureComponent<{}, State> { 9 | state: State = { 10 | counters: ['init'] 11 | }; 12 | 13 | add = () => { 14 | const newKey = Math.floor(Math.random() * 99999).toString(32); 15 | this.setState({counters: [...this.state.counters, newKey]}); 16 | }; 17 | 18 | remove = (idx: number) => () => { 19 | const {counters} = this.state; 20 | const filtered = counters.filter((_, i) => i !== idx); 21 | this.setState({counters: filtered}); 22 | }; 23 | 24 | renderSingle = (key: string, idx: number) => { 25 | return ( 26 |
27 | 28 | 29 |
); 30 | }; 31 | 32 | render () { 33 | return ( 34 |
35 | {this.state.counters.map(this.renderSingle)} 36 | 37 |
); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unidriver-examples", 3 | "version": "1.4.6", 4 | "description": "", 5 | "main": "index.js", 6 | "private": true, 7 | "scripts": { 8 | "start": "node dist/index.js", 9 | "build-ts": "tsc -p .", 10 | "build-rest": "cp src/index.ejs dist/index.ejs && browserify dist/client.js > dist/bundle.js", 11 | "build": "npm run build-ts && npm run build-rest", 12 | "test-jsdom": "mocha -r jsdom-global/register dist/**/*.spec.js --exit --timeout 20000", 13 | "test-e2e": "mocha dist/**/*.e2e.js --exit --timeout 20000", 14 | "test": "npm run test-jsdom && npm run test-e2e" 15 | }, 16 | "author": "", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@unidriver/core": "^1.3.0", 20 | "@unidriver/jsdom-react": "^1.7.0", 21 | "@unidriver/puppeteer": "^1.2.2", 22 | "@unidriver/selenium": "^1.3.6", 23 | "browserify": "^16.0.0", 24 | "chromedriver": "^103.0.0", 25 | "express": "^4.16.0", 26 | "find-free-port-sync": "^1.0.0", 27 | "jsdom": "^13.2.0", 28 | "jsdom-global": "^3.0.2", 29 | "mocha": "^6.0.0" 30 | }, 31 | "devDependencies": { 32 | "@types/puppeteer": "^5.4.0", 33 | "puppeteer": "^14.0.0", 34 | "ts-node": "^7.0.0", 35 | "typescript": "^3.7.5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/src/shared.e2e.ts: -------------------------------------------------------------------------------- 1 | require('chromedriver'); 2 | import { startServer } from './server'; 3 | import * as puppeteer from 'puppeteer'; 4 | import { ThenableWebDriver, Builder } from 'selenium-webdriver'; 5 | import * as chrome from 'selenium-webdriver/chrome'; 6 | import { defaultUrl, freePort } from './utils'; 7 | let server: any = null; 8 | let browser: puppeteer.Browser; 9 | let wd: ThenableWebDriver; 10 | 11 | before(async function () { 12 | this.timeout(20000); 13 | server = await startServer(freePort); 14 | browser = await puppeteer.launch(); 15 | 16 | // cache init stuff 17 | const cachePup = async () => { 18 | const page = await browser.newPage(); 19 | await page.goto(defaultUrl, {waitUntil: 'networkidle2'}); 20 | await page.close(); 21 | }; 22 | 23 | const cacheWd = async () => wd.get(defaultUrl); 24 | 25 | const chromeOptions = new chrome.Options(); 26 | if (!!process.env.CI) { 27 | chromeOptions.headless(); 28 | } 29 | 30 | wd = new Builder() 31 | .forBrowser('chrome') 32 | .setChromeOptions(chromeOptions) 33 | .build(); 34 | 35 | await Promise.all([cachePup(), cacheWd()]); 36 | }); 37 | 38 | after(async () => { 39 | server.close(); 40 | wd.quit(); 41 | browser.close(); 42 | }); 43 | 44 | 45 | 46 | export const getWd = () => wd; 47 | export const getBrowser = () => browser; 48 | 49 | -------------------------------------------------------------------------------- /test-suite/src/app/svelte/Events/Events.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 24 |
25 |
26 | 30 | {#each mouseEvents as event} 31 | 32 | {/each} 33 |
34 |
35 | 36 | {#each keyboardEvents as event} 37 | 38 | {/each} 39 |
40 |
41 | -------------------------------------------------------------------------------- /examples/src/counter/counter.e2e.tsx: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { pupUniDriver } from '@unidriver/puppeteer'; 3 | import { counterDriver } from './counter.driver'; 4 | import { getBrowser } from '../shared.e2e'; 5 | import { goAndWait } from '../utils'; 6 | import { Page } from 'puppeteer'; 7 | 8 | describe('counter - e2e', () => { 9 | 10 | let page: Page; 11 | 12 | beforeEach(async () => page = await goAndWait(getBrowser())); 13 | afterEach(() => page.close()); 14 | 15 | const begin = async () => { 16 | const base = pupUniDriver(async() => { 17 | const selector = 'body'; 18 | return{ 19 | element: await page.$(selector), 20 | page, 21 | selector 22 | } 23 | }); 24 | return counterDriver(base.$('.single-counter-wrapper')); 25 | }; 26 | 27 | it('shows initial value', async () => { 28 | const driver = await begin(); 29 | assert.equal(await driver.val(), 0); 30 | }); 31 | 32 | it('increases value', async () => { 33 | const driver = await begin(); 34 | await driver.increase(); 35 | assert.equal(await driver.val(), 1); 36 | await driver.increase(); 37 | await driver.increase(); 38 | assert.equal(await driver.val(), 3); 39 | }); 40 | 41 | it('decreases value', async () => { 42 | const driver = await begin(); 43 | await driver.decrease(); 44 | assert.equal(await driver.val(), -1); 45 | await driver.decrease(); 46 | await driver.decrease(); 47 | assert.equal(await driver.val(), -3); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /examples/src/counter/counter.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | 4 | import { assert } from 'chai'; 5 | import { counterDriver } from './counter.driver'; 6 | import { jsdomReactUniDriver } from '@unidriver/jsdom-react'; 7 | import { Counter } from './counter'; 8 | 9 | const render = (val: number) => { 10 | const div = document.createElement('div'); 11 | ReactDOM.render(, div); 12 | return div; 13 | }; 14 | 15 | const renderAppAndCreateDriver = (val: number) => { 16 | const app = render(val); 17 | const base = jsdomReactUniDriver(app); 18 | return counterDriver(base); 19 | }; 20 | 21 | describe('counter', async () => { 22 | it('shows initial value', async () => { 23 | const driver = renderAppAndCreateDriver(5); 24 | assert.equal(await driver.val(), 5); 25 | }); 26 | 27 | it('increases value', async () => { 28 | [5, 8, 58, 124, -214].forEach(async (num) => { 29 | const driver = renderAppAndCreateDriver(num); 30 | await driver.increase(); 31 | assert.equal(await driver.val(), num + 1); 32 | await driver.increase(); 33 | await driver.increase(); 34 | assert.equal(await driver.val(), num + 3); 35 | }); 36 | }); 37 | 38 | it('decreases value', async () => { 39 | [5, 8, 58, 124, -214].forEach(async (num) => { 40 | const driver = renderAppAndCreateDriver(num); 41 | await driver.decrease(); 42 | assert.equal(await driver.val(), num - 1); 43 | await driver.decrease(); 44 | await driver.decrease(); 45 | assert.equal(await driver.val(), num - 3); 46 | }); 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /website/static/img/SVG/black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /website/static/img/SVG/blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/static/img/SVG/white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test-suite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unidriver/test-suite", 3 | "version": "1.3.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "rm -rf dist && npm run build-svelte && tsc -p . && npm run build-test-suite", 9 | "build-svelte": "parcel build src/app/svelte/renderSvelteApp.js -d dist/app/svelte && cp src/app/svelte/renderSvelteApp.d.ts dist/app/svelte/renderSvelteApp.d.ts", 10 | "build-test-suite": "cp src/server/index.ejs dist/server/index.ejs && browserify dist/server/client.js > dist/bundle.js", 11 | "serve-svelte": "parcel src/app/svelte/index.html --open", 12 | "test-suite-server": "node -e 'require(\"./dist/index.js\").startTestAppServer(3000)'", 13 | "test": ":" 14 | }, 15 | "dependencies": { 16 | "@unidriver/core": "^1.3.0", 17 | "browserify": "^16.0.0", 18 | "chai": "^4.2.0", 19 | "classnames": "^2.2.6", 20 | "ejs": "^2.5.7", 21 | "express": "^4.16.0", 22 | "mocha": "^6.0.0", 23 | "puppeteer": "^1.11.0", 24 | "react": "^16.0.0", 25 | "react-dom": "^16.0.0" 26 | }, 27 | "devDependencies": { 28 | "@types/chai": "^4.1.7", 29 | "@types/classnames": "^2.2.7", 30 | "@types/ejs": "^2.5.0", 31 | "@types/express": "^4.11.0", 32 | "@types/react": "^16.0.0", 33 | "@types/react-dom": "^16.0.0", 34 | "parcel": "^1.12.4", 35 | "parcel-bundler": "^1.12.4", 36 | "parcel-plugin-svelte": "^4.0.6", 37 | "svelte": "~3.27.0", 38 | "typescript": "^3.3.3" 39 | }, 40 | "author": "Wix.com", 41 | "license": "MIT", 42 | "gitHead": "c793eb6b7c1d687fe054bf47fb11224028b51f77", 43 | "browserslist": [ 44 | "last 1 chrome versions" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /examples/src/multi-counter/multi-counter.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | 4 | import { assert } from 'chai'; 5 | import { jsdomReactUniDriver } from '@unidriver/jsdom-react'; 6 | import { MultiCounter } from './multi-counter'; 7 | import { multiCounterDriver } from './multi-counter.driver'; 8 | 9 | const render = () => { 10 | const div = document.createElement('div'); 11 | ReactDOM.render(, div); 12 | return div; 13 | }; 14 | 15 | const renderAppAndCreateDriver = () => { 16 | const app = render(); 17 | const base = jsdomReactUniDriver(app); 18 | return multiCounterDriver(base); 19 | }; 20 | 21 | describe('multi counters', async () => { 22 | it('shows one counter with 0', async () => { 23 | const driver = renderAppAndCreateDriver(); 24 | assert.equal(await driver.count(), 1); 25 | assert.equal(await driver.val(0), 0); 26 | }); 27 | 28 | it('adds counters', async () => { 29 | const driver = renderAppAndCreateDriver(); 30 | await driver.add(); 31 | assert.equal(await driver.count(), 2); 32 | await driver.add(); 33 | await driver.add(); 34 | assert.equal(await driver.count(), 4); 35 | }); 36 | 37 | it('has independently working counters', async () => { 38 | const driver = renderAppAndCreateDriver(); 39 | await driver.add(); 40 | await driver.add(); 41 | await driver.increase(0); 42 | assert.equal(await driver.val(0), 1); 43 | assert.equal(await driver.val(1), 0); 44 | await driver.increase(1); 45 | assert.equal(await driver.val(1), 1); 46 | }); 47 | 48 | it('removes counters', async () => { 49 | const driver = renderAppAndCreateDriver(); 50 | await driver.remove(0); 51 | assert.equal(await driver.count(), 0); 52 | }); 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /examples/src/multi-counter/multi-counter.e2e.tsx: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { pupUniDriver } from '@unidriver/puppeteer'; 3 | import { multiCounterDriver } from './multi-counter.driver'; 4 | import { getBrowser } from '../shared.e2e'; 5 | import { goAndWait } from '../utils'; 6 | import { Page } from 'puppeteer'; 7 | 8 | describe('multi counter', () => { 9 | 10 | let page: Page; 11 | 12 | beforeEach(async () => page = await goAndWait(getBrowser())); 13 | afterEach(() => page.close()); 14 | 15 | const begin = async () => { 16 | const base = pupUniDriver(async() => { 17 | const selector = 'body'; 18 | return{ 19 | element: await page.$(selector), 20 | page, 21 | selector 22 | } 23 | }); 24 | return multiCounterDriver(base); 25 | }; 26 | 27 | it('shows one counter with 0', async () => { 28 | const driver = await begin(); 29 | assert.equal(await driver.count(), 1); 30 | assert.equal(await driver.val(0), 0); 31 | }); 32 | 33 | it('adds counters', async () => { 34 | const driver = await begin(); 35 | await driver.add(); 36 | assert.equal(await driver.count(), 2); 37 | await driver.add(); 38 | await driver.add(); 39 | assert.equal(await driver.count(), 4); 40 | }); 41 | 42 | it('has independently working counters', async () => { 43 | const driver = await begin(); 44 | await driver.add(); 45 | await driver.add(); 46 | await driver.increase(0); 47 | assert.equal(await driver.val(0), 1); 48 | assert.equal(await driver.val(1), 0); 49 | await driver.increase(1); 50 | assert.equal(await driver.val(1), 1); 51 | }); 52 | 53 | it('removes counters', async () => { 54 | const driver = await begin(); 55 | await driver.remove(0); 56 | assert.equal(await driver.count(), 0); 57 | }); 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /examples/src/todo-app/todo-app.e2e.tsx: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { pupUniDriver } from '@unidriver/puppeteer'; 3 | import { todoAppDriver } from './todo-app.driver'; 4 | import { getBrowser } from '../shared.e2e'; 5 | import { goAndWait } from '../utils'; 6 | import { Page } from 'puppeteer'; 7 | 8 | describe('todo app', () => { 9 | 10 | let page: Page; 11 | 12 | beforeEach(async () => page = await goAndWait(getBrowser())); 13 | afterEach(() => page.close()); 14 | 15 | const begin = async () => { 16 | const base = pupUniDriver(async() => { 17 | const selector = 'body'; 18 | return{ 19 | element: await page.$(selector), 20 | page, 21 | selector 22 | } 23 | }); 24 | return todoAppDriver(base); 25 | }; 26 | 27 | it('adds new item', async () => { 28 | const driver = await begin(); 29 | assert.notInclude(await driver.getItems(), 'New bob'); 30 | await driver.addItem('New bob'); 31 | assert.include(await driver.getItems(), 'New bob'); 32 | }); 33 | 34 | it('adds toggles items', async () => { 35 | const driver = await begin(); 36 | assert.equal(await driver.isDone(0), true); 37 | await driver.toggleItem(0); 38 | assert.equal(await driver.isDone(0), false); 39 | }); 40 | 41 | it('adds removes items', async () => { 42 | const driver = await begin(); 43 | assert.equal((await driver.getItems()).length, 2); 44 | await driver.deleteItem(0); 45 | assert.equal((await driver.getItems()).length, 1); 46 | await driver.deleteItem(0); 47 | 48 | assert.equal((await driver.getItems()).length, 0); 49 | }); 50 | 51 | it('has a counter', async () => { 52 | const driver = await begin(); 53 | assert.equal(await driver.getCount(), 2); 54 | await driver.addItem('bob'); 55 | assert.equal(await driver.getCount(), 3); 56 | await driver.deleteItem(2); 57 | await driver.deleteItem(1); 58 | assert.equal(await driver.getCount(), 1); 59 | }); 60 | 61 | }); 62 | -------------------------------------------------------------------------------- /examples/src/todo-app/todo-app.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | 4 | import { TodoApp } from './todo-app'; 5 | 6 | import { todoAppDriver } from './todo-app.driver'; 7 | import { assert } from 'chai'; 8 | import { jsdomReactUniDriver } from '@unidriver/jsdom-react'; 9 | 10 | const renderApp = () => { 11 | const div = document.createElement('div'); 12 | ReactDOM.render(, div); 13 | return div; 14 | }; 15 | 16 | const renderAppAndCreateDriver = () => { 17 | const app = renderApp(); 18 | const base = jsdomReactUniDriver(app); 19 | return todoAppDriver(base); 20 | }; 21 | 22 | describe('todo app', async () => { 23 | it('adds new item', async () => { 24 | const driver = renderAppAndCreateDriver(); 25 | 26 | assert.notInclude(await driver.getItems(), 'New bob'); 27 | await driver.addItem('New bob'); 28 | assert.include(await driver.getItems(), 'New bob'); 29 | }); 30 | 31 | it('marks items as done or undone', async () => { 32 | const driver = renderAppAndCreateDriver(); 33 | 34 | assert.equal(await driver.isDone(0), true); 35 | await driver.toggleItem(0); 36 | assert.equal(await driver.isDone(0), false); 37 | }); 38 | 39 | it('removes items', async () => { 40 | const driver = renderAppAndCreateDriver(); 41 | 42 | assert.deepEqual(await driver.getItems(), ['Code', 'Eat']); 43 | 44 | await driver.deleteItem(0); 45 | assert.deepEqual(await driver.getItems(), ['Eat']); 46 | await driver.deleteItem(0); 47 | assert.deepEqual(await driver.getItems(), []); 48 | }); 49 | 50 | it('has a counter', async () => { 51 | const driver = renderAppAndCreateDriver(); 52 | 53 | assert.equal(await driver.getCount(), 2); 54 | await driver.addItem('bob'); 55 | assert.equal(await driver.getCount(), 3); 56 | await driver.deleteItem(2); 57 | await driver.deleteItem(1); 58 | assert.equal(await driver.getCount(), 1); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /examples/src/todo-app/todo-app-selenium.e2e.tsx: -------------------------------------------------------------------------------- 1 | require('chromedriver'); 2 | import { assert } from 'chai'; 3 | import { todoAppDriver } from './todo-app.driver'; 4 | import { getWd } from '../shared.e2e'; 5 | import { defaultUrl } from '../utils'; 6 | import { seleniumUniDriver } from '@unidriver/selenium'; 7 | import { WebElement, By } from 'selenium-webdriver'; 8 | 9 | describe('todo app - selenium', function () { 10 | 11 | // tslint:disable-next-line:no-invalid-this 12 | this.timeout(20000); 13 | 14 | beforeEach(async () => getWd().get(defaultUrl)); 15 | 16 | const begin = async () => { 17 | const base = seleniumUniDriver(async () => { 18 | const el: any = getWd().findElement(By.css('body')); 19 | return el as Promise; 20 | }); 21 | return todoAppDriver(base); 22 | }; 23 | 24 | it('adds new item', async () => { 25 | const driver = await begin(); 26 | assert.notInclude(await driver.getItems(), 'New bob'); 27 | await driver.addItem('New bob'); 28 | assert.include(await driver.getItems(), 'New bob'); 29 | }); 30 | 31 | it('adds toggles items', async () => { 32 | const driver = await begin(); 33 | assert.equal(await driver.isDone(0), true); 34 | await driver.toggleItem(0); 35 | assert.equal(await driver.isDone(0), false); 36 | }); 37 | 38 | it('adds removes items', async () => { 39 | const driver = await begin(); 40 | assert.equal((await driver.getItems()).length, 2); 41 | await driver.deleteItem(0); 42 | assert.equal((await driver.getItems()).length, 1); 43 | await driver.deleteItem(0); 44 | 45 | assert.equal((await driver.getItems()).length, 0); 46 | }); 47 | 48 | it('has a counter', async () => { 49 | const driver = await begin(); 50 | assert.equal(await driver.getCount(), 2); 51 | await driver.addItem('bob'); 52 | assert.equal(await driver.getCount(), 3); 53 | await driver.deleteItem(2); 54 | await driver.deleteItem(1); 55 | assert.equal(await driver.getCount(), 1); 56 | }); 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /test-suite/src/app/svelte/Todo/Todo.svelte: -------------------------------------------------------------------------------- 1 | 41 | 42 |
43 |
44 | 46 | 48 |
49 |
50 | {#each items as item, index} 51 | 53 | {/each} 54 |
55 |
56 | Mark all as completed
57 | Items count: {items.length} 58 |
59 |
60 | -------------------------------------------------------------------------------- /website/docs/doc3.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: doc3 3 | title: This is Document Number 3 4 | --- 5 | 6 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. In ac euismod odio, eu consequat dui. Nullam molestie consectetur risus id imperdiet. Proin sodales ornare turpis, non mollis massa ultricies id. Nam at nibh scelerisque, feugiat ante non, dapibus tortor. Vivamus volutpat diam quis tellus elementum bibendum. Praesent semper gravida velit quis aliquam. Etiam in cursus neque. Nam lectus ligula, malesuada et mauris a, bibendum faucibus mi. Phasellus ut interdum felis. Phasellus in odio pulvinar, porttitor urna eget, fringilla lectus. Aliquam sollicitudin est eros. Mauris consectetur quam vitae mauris interdum hendrerit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 7 | 8 | Duis et egestas libero, imperdiet faucibus ipsum. Sed posuere eget urna vel feugiat. Vivamus a arcu sagittis, fermentum urna dapibus, congue lectus. Fusce vulputate porttitor nisl, ac cursus elit volutpat vitae. Nullam vitae ipsum egestas, convallis quam non, porta nibh. Morbi gravida erat nec neque bibendum, eu pellentesque velit posuere. Fusce aliquam erat eu massa eleifend tristique. 9 | 10 | Sed consequat sollicitudin ipsum eget tempus. Integer a aliquet velit. In justo nibh, pellentesque non suscipit eget, gravida vel lacus. Donec odio ante, malesuada in massa quis, pharetra tristique ligula. Donec eros est, tristique eget finibus quis, semper non nisl. Vivamus et elit nec enim ornare placerat. Sed posuere odio a elit cursus sagittis. 11 | 12 | Phasellus feugiat purus eu tortor ultrices finibus. Ut libero nibh, lobortis et libero nec, dapibus posuere eros. Sed sagittis euismod justo at consectetur. Nulla finibus libero placerat, cursus sapien at, eleifend ligula. Vivamus elit nisl, hendrerit ac nibh eu, ultrices tempus dui. Nam tellus neque, commodo non rhoncus eu, gravida in risus. Nullam id iaculis tortor. 13 | 14 | Nullam at odio in sem varius tempor sit amet vel lorem. Etiam eu hendrerit nisl. Fusce nibh mauris, vulputate sit amet ex vitae, congue rhoncus nisl. Sed eget tellus purus. Nullam tempus commodo erat ut tristique. Cras accumsan massa sit amet justo consequat eleifend. Integer scelerisque vitae tellus id consectetur. 15 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-react"], 3 | "rulesDirectory": [ 4 | "tslint-rules" 5 | ], 6 | "rules": { 7 | "lower-case-module-imports": true, 8 | "no-dev-translations": true, 9 | "no-namespace": [true, "allow-declarations"], 10 | "no-reference": false, 11 | "only-arrow-functions": [true, "allow-declarations"], 12 | "typedef-whitespace": [true, 13 | {"variable-declaration": "nospace", "call-signature": "nospace", "parameter": "nospace", "property-declaration": "nospace"}, 14 | {"variable-declaration": "onespace", "call-signature": "onespace", "parameter": "onespace", "property-declaration": "onespace"} 15 | ], 16 | "no-submodule-imports": false, 17 | "interface-over-type-literal": false, 18 | "space-before-function-paren": true, 19 | "ordered-imports": [false], 20 | "member-access": false, 21 | "curly": true, 22 | "no-bitwise": true, 23 | "no-conditional-assignment": true, 24 | "no-console": [true, "log", "error"], 25 | "no-debugger": true, 26 | "no-empty": true, 27 | "no-eval": true, 28 | "no-invalid-this": [true, "check-function-in-method"], 29 | "no-shadowed-variable": true, 30 | "no-switch-case-fall-through": true, 31 | "no-var-keyword": true, 32 | "switch-default": false, 33 | "triple-equals": true, 34 | "eofline": true, 35 | "indent": [true, "tabs"], 36 | "no-trailing-whitespace": true, 37 | "trailing-comma": [true, {"singleline": "never"}], 38 | "class-name": true, 39 | "no-consecutive-blank-lines": [true], 40 | "no-implicit-dependencies": false, 41 | "object-literal-key-quotes": [true, "as-needed"], 42 | "one-line": [true, "check-catch", "check-finally", "check-else", "check-open-brace", "check-whitespace"], 43 | "quotemark": [true, "single", "jsx-single"], 44 | "semicolon": [true, "always"], 45 | "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type"], 46 | "cyclomatic-complexity": [true, 20], 47 | "interface-name": [true, "never-prefix"], 48 | "jsx-alignment": true, 49 | "jsx-no-lambda": true, 50 | "jsx-curly-spacing": [true, "never"], 51 | "jsx-no-multiline-js": true, 52 | "jsx-no-string-ref": true, 53 | "jsx-self-close": true, 54 | "object-literal-sort-keys": false 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /adapters/selenium/src/spec.ts: -------------------------------------------------------------------------------- 1 | require('chromedriver'); 2 | import { seleniumUniDriver } from './'; 3 | import { Server } from 'http'; 4 | import { ThenableWebDriver, Builder, WebElement, By } from 'selenium-webdriver'; 5 | import * as chrome from 'selenium-webdriver/chrome'; 6 | import { 7 | SetupFn, 8 | runTestSuite, 9 | startTestAppServer, 10 | getTestAppUrl, 11 | } from '@unidriver/test-suite'; 12 | import { assert } from 'chai'; 13 | 14 | const port = require('find-free-port-sync')(); 15 | 16 | let server: Server; 17 | let wd: ThenableWebDriver; 18 | 19 | const beforeFn = async () => { 20 | const headless = !!process.env.CI; 21 | const chromeOptions = new chrome.Options(); 22 | 23 | if (headless) { 24 | chromeOptions.headless(); 25 | } 26 | 27 | server = await startTestAppServer(port); 28 | wd = new Builder() 29 | .forBrowser('chrome') 30 | .setChromeOptions(chromeOptions) 31 | .build(); 32 | 33 | await wd.get(`http://localhost:${port}${getTestAppUrl({})}`); 34 | 35 | const driver = seleniumUniDriver(() => { 36 | const el: any = wd.findElement(By.css('body')); 37 | return el as Promise; 38 | }); 39 | 40 | await driver.wait(); 41 | }; 42 | 43 | const afterFn = async () => { 44 | server.close(); 45 | await wd.quit(); 46 | }; 47 | 48 | const setup: SetupFn = async (data) => { 49 | await wd.get(`http://localhost:${port}${getTestAppUrl(data)}`); 50 | const driver = seleniumUniDriver(() => { 51 | const el: any = wd.findElement(By.css('body')); 52 | return el as Promise; 53 | }); 54 | 55 | const tearDown = async () => { 56 | // await wd.close(); 57 | }; 58 | 59 | await driver.wait(); 60 | 61 | return { driver, tearDown }; 62 | }; 63 | 64 | describe('selenium', () => { 65 | runTestSuite({ setup, before: beforeFn, after: afterFn }); 66 | }); 67 | 68 | describe('selenium specific tests', () => { 69 | before(beforeFn); 70 | after(afterFn); 71 | describe('enterValue', () => { 72 | it(`doesn't attempt to clear value when shouldClear is false`, async () => { 73 | const { driver } = await setup({ items: [], initialText: 'hello' }); 74 | await driver 75 | .$('header input') 76 | .enterValue(' world!', { shouldClear: false }); 77 | assert.equal(await driver.$('header input').value(), 'hello world!'); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /adapters/playwright/src/spec.ts: -------------------------------------------------------------------------------- 1 | // import { Browser, Page, chromium, webkit, BrowserType, firefox } from 'playwright'; 2 | import { Browser, Page, webkit, BrowserType, firefox } from 'playwright'; 3 | import { 4 | getTestAppUrl, 5 | startTestAppServer, 6 | SetupFn, 7 | runTestSuite, 8 | } from '@unidriver/test-suite'; 9 | import { playwrightUniDriver } from './'; 10 | import { Server } from 'http'; 11 | import { assert } from 'chai'; 12 | 13 | const port = require('find-free-port-sync')(); 14 | 15 | let server: Server; 16 | let browser: Browser; 17 | let page: Page; 18 | 19 | const beforeFn = async (browserType: BrowserType) => { 20 | const args = process.env.CI ? ['--no-sandbox'] : []; 21 | const headless = !!process.env.CI; 22 | server = await startTestAppServer(port); 23 | browser = await browserType.launch({ 24 | headless, 25 | args 26 | }); 27 | page = await browser.newPage(); 28 | }; 29 | 30 | const afterFn = async () => { 31 | server.close(); 32 | await page.close(); 33 | await browser.close(); 34 | }; 35 | 36 | const setup: SetupFn = async (params) => { 37 | await page.goto(`http://localhost:${port}${getTestAppUrl(params)}`); 38 | const driver = playwrightUniDriver({ 39 | page, 40 | selector: 'body', 41 | }); 42 | 43 | const tearDown = async () => {}; 44 | 45 | return { driver, tearDown }; 46 | }; 47 | 48 | const browserTypes = [firefox]; 49 | // const browserTypes = [chromium, firefox]; 50 | if (!process.env.CI) { 51 | // https://circleci.com/developer/orbs/orb/circleci/browser-tools doesn't seem to support webkit 52 | browserTypes.push(webkit); 53 | } 54 | 55 | for (const browserType of browserTypes) { 56 | describe.skip(`playwright - ${browserType.name()}`, () => { 57 | runTestSuite({ setup, before: () => beforeFn(browserType), after: afterFn }); 58 | }); 59 | 60 | describe.skip(`playwright specific tests - ${browserType.name()}`, () => { 61 | before(() => beforeFn(browserType)); 62 | after(afterFn); 63 | describe('enterValue', () => { 64 | it(`doesn't attempt to clear value when shouldClear is false`, async () => { 65 | const { driver } = await setup({ items: [], initialText: 'hello' }); 66 | await driver 67 | .$('header input') 68 | .enterValue(' world!', { shouldClear: false }); 69 | assert.equal(await driver.$('header input').value(), 'hello world!'); 70 | }); 71 | }); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /examples/src/todo-app/todo-app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export type TodoItem = { 4 | title: string; 5 | done: boolean; 6 | } 7 | 8 | export type ValComp = {val: T, onChange: (newVal: T, oldVal: T) => void}; 9 | 10 | 11 | export type TodoItemViewProps = ValComp & { 12 | onDelete: (val: TodoItem) => void; 13 | } 14 | 15 | export type State = { 16 | items: TodoItem[], 17 | newItemTitle: string 18 | } 19 | 20 | export class TodoItemView extends React.PureComponent { 21 | onToggle = () => { 22 | const {val, onChange} = this.props; 23 | onChange({...val, done: !val.done}, val); 24 | }; 25 | 26 | onDelete = () => this.props.onDelete(this.props.val); 27 | 28 | render() { 29 | const item = this.props.val; 30 | return (
  • 31 | {item.title} 32 |
  • ); 33 | } 34 | } 35 | 36 | export class TodoApp extends React.PureComponent<{}, State> { 37 | 38 | state: State = { 39 | items: [{ 40 | title: 'Code', 41 | done: true 42 | }, { 43 | title: 'Eat', 44 | done: false 45 | }], 46 | newItemTitle: '' 47 | }; 48 | 49 | onChangeNewTitle = (e: any) => this.setState({newItemTitle: e.target.value}); 50 | 51 | add = () => { 52 | const {items, newItemTitle} = this.state; 53 | this.setState({ 54 | items: [{title: newItemTitle, done: false}, ...items, ], 55 | newItemTitle: '' 56 | }); 57 | }; 58 | 59 | change = (newVal: TodoItem, oldVal: TodoItem) => { 60 | const {items} = this.state; 61 | 62 | this.setState({items: items.map((item) => { 63 | return item === oldVal ? newVal : item; 64 | })}); 65 | }; 66 | 67 | remove = (itemToRemove: TodoItem) => { 68 | const {items} = this.state; 69 | this.setState({items: items.filter((item) => { 70 | return item !== itemToRemove; 71 | })}); 72 | }; 73 | 74 | handleKeyDown = (e: any) => { 75 | if (e.keyCode === 13) { 76 | this.add(); 77 | } 78 | }; 79 | 80 | render() { 81 | const {state} = this; 82 | 83 | return (
    84 |
    85 |
    86 |

    Items: {state.items.length}

    87 |
      {state.items.map((item, idx) => )}
    88 |
    89 |
    ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | version: 2.1 4 | 5 | orbs: 6 | # The Node.js orb contains a set of prepackaged CircleCI configuration you can utilize 7 | # Orbs reduce the amount of configuration required for common tasks. 8 | # See the orb documentation here: https://circleci.com/developer/orbs/orb/circleci/node 9 | node: circleci/node@4.1 10 | browser-tools: circleci/browser-tools@1.1.1 11 | 12 | jobs: 13 | # Below is the definition of your job to build and test your app, you can rename and customize it as you want. 14 | build-and-test: 15 | # These next lines define a docker executor: https://circleci.com/docs/2.0/executor-types/ 16 | # You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 17 | # A list of available CircleCI docker Convenience Images are available here: https://circleci.com/developer/images/image/cimg/node 18 | docker: 19 | - image: cimg/node:15.1 20 | # Then run your tests! 21 | # CircleCI will report the results back to your VCS provider. 22 | steps: 23 | # Checkout the code as the first step. 24 | - checkout 25 | # Next, the node orb's install-packages step will install the dependencies from a package.json. 26 | # The orb install-packages step will also automatically cache them for faster future runs. 27 | # - node/install-packages 28 | # If you are using yarn instead npm, remove the line above and uncomment the two lines below. 29 | - browser-tools/install-browser-tools 30 | - node/install-packages: 31 | pkg-manager: yarn 32 | - run: 33 | name: webdriver manager update 34 | command: node_modules/protractor/bin/webdriver-manager update 35 | - run: 36 | name: Run Build 37 | command: npm run build 38 | - run: 39 | name: Run tests 40 | command: npm run test 41 | 42 | workflows: 43 | # Below is the definition of your workflow. 44 | # Inside the workflow, you provide the jobs you want to run, e.g this workflow runs the build-and-test job above. 45 | # CircleCI will run this workflow on every commit. 46 | # For more details on extending your workflow, see the configuration docs: https://circleci.com/docs/2.0/configuration-reference/#workflows 47 | sample: 48 | jobs: 49 | - build-and-test 50 | # For running simple node tests, you could optionally use the node/test job from the orb to replicate and replace the job above in fewer lines. 51 | # - node/test 52 | -------------------------------------------------------------------------------- /test-suite/src/app/react/events/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { EventsAppProps } from '../../../types'; 3 | 4 | export type EventsAppState = { 5 | keyboardEvents: React.KeyboardEvent[]; 6 | mouseEvents: React.MouseEvent[]; 7 | } 8 | 9 | const keyboardEventComp = (event: React.KeyboardEvent, idx: number) => { 10 | const normalizedKey = event.key === 'OS' ? 'Meta' : event.key; // on FF Meta key press ends up named "OS", so we normalize it (https://stackoverflow.com/questions/39292111/can-firefox-detect-metakey) 11 | // `whiteSpace: pre` is needed for tests that run in browsers + Space key 12 | return ( 13 |
    14 | {normalizedKey} 15 | {event.keyCode} 16 |
    17 | ); 18 | }; 19 | 20 | const mouseEventComp = (event: React.MouseEvent, idx: number) => { 21 | return ( 22 |
    23 | {event.type} 24 |
    25 | ) 26 | } 27 | 28 | export class EventsApp extends React.Component { 29 | 30 | state = { 31 | keyboardEvents: [], 32 | mouseEvents: [] 33 | } 34 | 35 | onKeyboardEvent = (e: React.KeyboardEvent) => { 36 | const {keyboardEvents} = this.state; 37 | const eventObj = {...e, key: e.key, keyCode: e.keyCode}; 38 | this.setState({keyboardEvents: [...keyboardEvents, eventObj]}); 39 | } 40 | 41 | onMouseEvent = (e: React.MouseEvent) => { 42 | const {mouseEvents} = this.state; 43 | const eventObj = {...e, type: e.type, clientX: e.clientX, clientY: e.clientY}; 44 | this.setState({mouseEvents: [...mouseEvents, eventObj]}); 45 | } 46 | 47 | render () { 48 | const { state } = this; 49 | 50 | return ( 51 |
    52 |
    53 | 56 |
    {state.mouseEvents.map(mouseEventComp)}
    57 |
    58 |
    59 | 60 |
    {state.keyboardEvents.map(keyboardEventComp)}
    61 |
    62 |
    63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /adapters/puppeteer/src/pptr.spec.ts: -------------------------------------------------------------------------------- 1 | import * as puppeteer from 'puppeteer'; 2 | import { Browser, Page } from 'puppeteer'; 3 | import { 4 | getTestAppUrl, 5 | startTestAppServer, 6 | SetupFn, 7 | itemCreator, 8 | runTestSuite 9 | } from '@unidriver/test-suite'; 10 | import { pupUniDriver } from '.'; 11 | import { Server } from 'http'; 12 | import { assert } from 'chai'; 13 | 14 | const port = require('find-free-port-sync')(); 15 | 16 | let server: Server; 17 | let browser: Browser; 18 | let page: Page; 19 | 20 | const beforeFn = async () => { 21 | const args = process.env.CI ? ['--no-sandbox'] : []; 22 | const headless = !!process.env.CI || undefined; 23 | server = await startTestAppServer(port); 24 | browser = await puppeteer.launch({ 25 | headless, 26 | args, 27 | }); 28 | page = await browser.newPage(); 29 | }; 30 | 31 | const afterFn = async () => { 32 | server.close(); 33 | await page.close(); 34 | await browser.close(); 35 | }; 36 | 37 | const setup: SetupFn = async (params) => { 38 | await page.goto(`http://localhost:${port}${getTestAppUrl(params)}`); 39 | const driver = pupUniDriver({ 40 | page, 41 | selector: 'body', 42 | }); 43 | 44 | const tearDown = async () => { }; 45 | 46 | return { driver, tearDown }; 47 | }; 48 | 49 | describe('puppeteer', () => { 50 | runTestSuite({ setup, before: beforeFn, after: afterFn }); 51 | }); 52 | 53 | describe('puppeteer specific tests', () => { 54 | before(beforeFn); 55 | after(afterFn); 56 | describe('enterValue', () => { 57 | it(`doesn't attempt to clear value when shouldClear is false`, async () => { 58 | const { driver } = await setup({ items: [], initialText: 'hello' }); 59 | await driver 60 | .$('header input') 61 | .enterValue(' world!', { shouldClear: false }); 62 | assert.equal(await driver.$('header input').value(), 'hello world!'); 63 | }); 64 | }); 65 | describe('text', () => { 66 | it(`checking text return value when the text is empty`, async () => { 67 | const { driver } = await setup({ items: [itemCreator({ label: '' })], initialText: '' }); 68 | const labelText = await driver.$$('.label').text(); 69 | assert.equal(labelText[0], ""); 70 | }); 71 | it(`checking text return value when the text is not empty`, async () => { 72 | const { driver } = await setup({ items: [itemCreator({ label: 'hello' })], initialText: '' }); 73 | const labelText = await driver.$$('.label').text(); 74 | assert.equal(labelText[0], "hello"); 75 | }); 76 | it(`checking text return value when the component doesn't have textContent property`, async () => { 77 | const { driver } = await setup({ items: [], initialText: 'hello' }); 78 | 79 | let textThatDoesNotExists = await driver.$('header input').text(); 80 | assert.equal(textThatDoesNotExists, ''); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /adapters/puppeteer/src/pptr-core.spec.ts: -------------------------------------------------------------------------------- 1 | import * as puppeteer from 'puppeteer-core'; 2 | import { Browser, Page } from 'puppeteer-core'; 3 | import { 4 | getTestAppUrl, 5 | startTestAppServer, 6 | SetupFn, 7 | itemCreator, 8 | runTestSuite 9 | } from '@unidriver/test-suite'; 10 | import { pupUniDriver } from '.'; 11 | import { Server } from 'http'; 12 | import { assert } from 'chai'; 13 | 14 | const port = require('find-free-port-sync')(); 15 | const locateChrome = require('locate-chrome'); 16 | 17 | let server: Server; 18 | let browser: Browser; 19 | let page: Page; 20 | 21 | const beforeFn = async () => { 22 | const args = process.env.CI ? ['--no-sandbox'] : []; 23 | const headless = !!process.env.CI || undefined; 24 | const pathToChrome = await locateChrome(); 25 | server = await startTestAppServer(port); 26 | browser = await puppeteer.launch({ 27 | executablePath:pathToChrome, 28 | headless, 29 | args, 30 | }); 31 | page = await browser.newPage(); 32 | }; 33 | 34 | const afterFn = async () => { 35 | server.close(); 36 | await page.close(); 37 | await browser.close(); 38 | }; 39 | 40 | const setup: SetupFn = async (params) => { 41 | await page.goto(`http://localhost:${port}${getTestAppUrl(params)}`); 42 | const driver = pupUniDriver({ 43 | page, 44 | selector: 'body', 45 | }); 46 | 47 | const tearDown = async () => { }; 48 | 49 | return { driver, tearDown }; 50 | }; 51 | 52 | describe('puppeteer-core', () => { 53 | runTestSuite({ setup, before: beforeFn, after: afterFn }); 54 | }); 55 | 56 | describe('puppeteer-core specific tests', () => { 57 | before(beforeFn); 58 | after(afterFn); 59 | describe('enterValue', () => { 60 | it(`doesn't attempt to clear value when shouldClear is false`, async () => { 61 | const { driver } = await setup({ items: [], initialText: 'hello' }); 62 | await driver 63 | .$('header input') 64 | .enterValue(' world!', { shouldClear: false }); 65 | assert.equal(await driver.$('header input').value(), 'hello world!'); 66 | }); 67 | }); 68 | describe('text', () => { 69 | it(`checking text return value when the text is empty`, async () => { 70 | const { driver } = await setup({ items: [itemCreator({ label: '' })], initialText: '' }); 71 | const labelText = await driver.$$('.label').text(); 72 | assert.equal(labelText[0], ""); 73 | }); 74 | it(`checking text return value when the text is not empty`, async () => { 75 | const { driver } = await setup({ items: [itemCreator({ label: 'hello' })], initialText: '' }); 76 | const labelText = await driver.$$('.label').text(); 77 | assert.equal(labelText[0], "hello"); 78 | }); 79 | it(`checking text return value when the component doesn't have textContent property`, async () => { 80 | const { driver } = await setup({ items: [], initialText: 'hello' }); 81 | 82 | let textThatDoesNotExists = await driver.$('header input').text(); 83 | assert.equal(textThatDoesNotExists, ''); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /website/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Layout from '@theme/Layout'; 4 | import Link from '@docusaurus/Link'; 5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 6 | import useBaseUrl from '@docusaurus/useBaseUrl'; 7 | import styles from './styles.module.css'; 8 | 9 | const features = [ 10 | { 11 | title: 'Truly Flexible', 12 | imageUrl: 'img/undraw_pilates_gpdb.svg', 13 | description: ( 14 | <> 15 | Compatible with Node (via JSDom), Selenium, Protractor, Puppeteer, Playwright and Cypress.io! 16 | Use any test runner and any assertion library you want. 17 | 18 | ), 19 | }, 20 | { 21 | title: 'Write Once, Test Everywhere', 22 | imageUrl: 'img/undraw_analyze_17kw.svg', 23 | description: ( 24 | <> 25 | Create drivers for you components using UniDriver once, and re-use them across all test levels. From unit testing and up to browser testing in production. 26 | 27 | ), 28 | }, 29 | { 30 | title: 'Enable True Collaboration', 31 | imageUrl: 'img/undraw_shared_goals_3d12.svg', 32 | description: ( 33 | <> 34 | Break the barriers between product engineers and QA engineers. Share test code and reduce selector related breaking tests. 35 | 36 | ), 37 | }, 38 | ]; 39 | 40 | function Feature({imageUrl, title, description}) { 41 | const imgUrl = useBaseUrl(imageUrl); 42 | return ( 43 |
    44 | {imgUrl && ( 45 |
    46 | {title} 47 |
    48 | )} 49 |

    {title}

    50 |

    {description}

    51 |
    52 | ); 53 | } 54 | 55 | function Home() { 56 | const context = useDocusaurusContext(); 57 | const {siteConfig = {}} = context; 58 | return ( 59 | 62 |
    63 |
    64 |

    {siteConfig.title}

    65 |

    {siteConfig.tagline}

    66 |
    67 | 73 | Get Started 74 | 75 |
    76 |
    77 |
    78 |
    79 | {features && features.length > 0 && ( 80 |
    81 |
    82 |
    83 | {features.map((props, idx) => ( 84 | 85 | ))} 86 |
    87 |
    88 |
    89 | )} 90 |
    91 |
    92 | ); 93 | } 94 | 95 | export default Home; 96 | -------------------------------------------------------------------------------- /website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'UniDriver', 3 | tagline: 'TBD Cool Tag-line Here', 4 | url: 'https://wix-incubator.github.io', 5 | baseUrl: '/unidriver/', 6 | onBrokenLinks: 'throw', 7 | onBrokenMarkdownLinks: 'warn', 8 | favicon: 'img/favicon.ico', 9 | organizationName: 'wix-incubator', // Usually your GitHub org/user name. 10 | projectName: 'unidriver', // Usually your repo name. 11 | themeConfig: { 12 | navbar: { 13 | title: 'UniDriver', 14 | logo: { 15 | alt: 'UniDriver Logo', 16 | src: 'img/SVG/blue.svg', 17 | }, 18 | items: [ 19 | { 20 | to: 'docs/', 21 | activeBasePath: 'docs', 22 | label: 'Docs', 23 | position: 'left', 24 | }, 25 | {to: 'blog', label: 'Blog', position: 'left'}, 26 | { 27 | href: 'https://github.com/wix-incubator/unidriver', 28 | label: 'GitHub', 29 | position: 'right', 30 | }, 31 | ], 32 | }, 33 | footer: { 34 | style: 'dark', 35 | links: [ 36 | { 37 | title: 'Docs', 38 | items: [ 39 | { 40 | label: 'Style Guide', 41 | to: 'docs/', 42 | }, 43 | { 44 | label: 'Second Doc', 45 | to: 'docs/doc2/', 46 | }, 47 | ], 48 | }, 49 | { 50 | title: 'Community', 51 | items: [ 52 | { 53 | label: 'Stack Overflow', 54 | href: 'https://stackoverflow.com/questions/tagged/unidriver', 55 | }, 56 | { 57 | label: 'Discord', 58 | href: 'https://discordapp.com/invite/unidriver', 59 | }, 60 | { 61 | label: 'Twitter', 62 | href: 'https://twitter.com/wixeng', 63 | }, 64 | ], 65 | }, 66 | { 67 | title: 'More', 68 | items: [ 69 | { 70 | label: 'Blog', 71 | to: 'blog', 72 | }, 73 | { 74 | label: 'GitHub', 75 | href: 'https://github.com/wix-incubator/unidriver', 76 | }, 77 | ], 78 | }, 79 | ], 80 | copyright: `Copyright © ${new Date().getFullYear()} UniDriver, Inc. Built with Docusaurus.`, 81 | }, 82 | }, 83 | presets: [ 84 | [ 85 | '@docusaurus/preset-classic', 86 | { 87 | docs: { 88 | sidebarPath: require.resolve('./sidebars.js'), 89 | // Please change this to your repo. 90 | editUrl: 91 | 'https://github.com/facebook/docusaurus/edit/master/website/', 92 | }, 93 | blog: { 94 | showReadingTime: true, 95 | // Please change this to your repo. 96 | editUrl: 97 | 'https://github.com/facebook/docusaurus/edit/master/website/blog/', 98 | }, 99 | theme: { 100 | customCss: require.resolve('./src/css/custom.css'), 101 | }, 102 | }, 103 | ], 104 | ], 105 | }; 106 | -------------------------------------------------------------------------------- /test-suite/src/app/react/todo-app/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as classNames from 'classnames'; 3 | import {TodoAppProps, TodoItem} from "../../../types"; 4 | 5 | export type TodoItemProps = { 6 | item: TodoItem; 7 | isActive: boolean; 8 | onToggle: () => void; 9 | onHover: () => void; 10 | onBlur: () => void; 11 | }; 12 | 13 | class TodoAppItem extends React.Component { 14 | render () { 15 | const {item, isActive, onToggle, onHover, onBlur} = this.props; 16 | const cn = classNames({ 17 | active: isActive, 18 | done: item.completed 19 | }); 20 | 21 | const style = {backgroundColor: isActive ? 'red' : ''}; 22 | 23 | return ( 24 |
    25 | 26 | {item.label} 27 | {item.completed ? Completed! : null} 28 | 29 |
    30 | ); 31 | } 32 | } 33 | 34 | type TodoAppState = { 35 | newItem: string; 36 | items: TodoItem[]; 37 | activeItem: number; 38 | }; 39 | 40 | export class TodoApp extends React.Component { 41 | state = { 42 | newItem: this.props.initialText || '', 43 | items: this.props.items, 44 | activeItem: -1 45 | }; 46 | 47 | onToggle = (idx: number) => () => { 48 | const items = this.state.items; 49 | const item = items[idx]; 50 | const newItems = items.map((i, itemIdx) => idx === itemIdx ? {...i, completed: !item.completed} : i); 51 | this.setState({items: newItems}); 52 | }; 53 | 54 | onChange = (e: any) => { 55 | this.setState({newItem: e.target.value}); 56 | }; 57 | 58 | onAdd = () => { 59 | const items = this.state.items; 60 | this.setState({items: [...items, {label: this.state.newItem, completed: false}], newItem: ''}); 61 | }; 62 | 63 | onKeyDown = (e: React.KeyboardEvent) => { 64 | e.preventDefault(); 65 | if (e.key === 'Enter') { 66 | this.onAdd(); 67 | } 68 | }; 69 | 70 | onHover = (idx: number) => () => { 71 | this.setState({activeItem: idx}); 72 | }; 73 | 74 | onBlur = () => this.setState({activeItem: -1}); 75 | 76 | render () { 77 | const {items, activeItem} = this.state; 78 | const itemsComp = items.map((item, idx) => { 79 | const isActive = idx === activeItem; 80 | return ( 81 | 82 | ); 83 | }); 84 | 85 | return ( 86 |
    87 |
    88 | 89 | 90 |
    91 |
    92 | {itemsComp} 93 |
    94 |
    95 | Mark all as completed
    96 | Items count: {items.length} 97 |
    98 |
    ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /core/src/index.ts: -------------------------------------------------------------------------------- 1 | import { KeyDefinitionType } from './key-types'; 2 | 3 | export * from './utils'; 4 | export * from './key-types'; 5 | 6 | export type Locator = string; 7 | 8 | export type MapFn = (e: UniDriver, idx?: number, array?: UniDriver[]) => Promise; 9 | 10 | export type PredicateFn = (e: UniDriver, idx?: number, array?: UniDriver[]) => Promise; 11 | 12 | export type ReducerFn = (acc: T, curr: UniDriver, idx?: number, array?: UniDriver[]) => T; 13 | 14 | export type MouseUniDriver = { 15 | moveTo: (to: UniDriver) => Promise; 16 | press: () => Promise; 17 | release: () => Promise; 18 | leave: () => Promise; 19 | } 20 | 21 | export type UniDriverList = { 22 | get: (idx: number) => UniDriver, 23 | text: () => Promise, 24 | count: () => Promise, 25 | map: (mapFn: MapFn) => Promise, 26 | filter: (predicate: PredicateFn) => UniDriverList 27 | }; 28 | 29 | export type EnterValueOptions = { 30 | delay?: number; // Time to wait between key presses in milliseconds. 31 | shouldClear?: boolean; // Whether to try and clear input value before entering text. (default: true) 32 | } 33 | 34 | export type UniDriver = { 35 | $: (selector: Locator) => UniDriver; 36 | $$: (selector: Locator) => UniDriverList; 37 | text: () => Promise; 38 | click: () => Promise; 39 | hover: () => Promise; 40 | pressKey: (key: KeyDefinitionType) => Promise 41 | value: () => Promise; 42 | enterValue: (value: string, options?: EnterValueOptions) => Promise; 43 | attr: (name: string) => Promise; 44 | hasClass: (name: string) => Promise; 45 | exists: () => Promise; 46 | isDisplayed: () => Promise; 47 | wait: (timeout?: number) => Promise; 48 | mouse: MouseUniDriver; 49 | type: string; 50 | scrollIntoView: () => Promise<{}>; 51 | getNative: () => Promise; 52 | /** Gets a html element's property value by property name. @returns null if property is not defined */ 53 | _prop: (name: string) => Promise; 54 | }; 55 | 56 | export enum ErrorTypes { 57 | NO_ELEMENTS_WITH_SELECTOR = 'no-elements-with-selector', 58 | MULTIPLE_ELEMENTS_WITH_SELECTOR = 'multiple-elements-with-selector' 59 | } 60 | 61 | export class NoElementWithLocatorError extends Error { 62 | 63 | type = ErrorTypes.NO_ELEMENTS_WITH_SELECTOR; 64 | 65 | constructor(locator: string) { 66 | super(`Cannot find element with locator: ${locator}`) 67 | } 68 | } 69 | 70 | export class MultipleElementsWithLocatorError extends Error { 71 | 72 | type = ErrorTypes.MULTIPLE_ELEMENTS_WITH_SELECTOR; 73 | 74 | constructor(count: number, locator: string) { 75 | super(`Found ${count} elements with locator [${locator}]. Only 1 is expected. This is either a bug or not-specific-enough locator`); 76 | } 77 | } 78 | 79 | export type DriverContext = { 80 | parent?: DriverContext; 81 | selector?: string; 82 | idx?: number; 83 | }; 84 | 85 | export const rootDriver: DriverContext = { 86 | selector: '' 87 | } 88 | 89 | export const contextToSelectorString = (context: DriverContext): string => { 90 | const maybeIndexDescription = 91 | typeof context.idx === "number" && context.idx >= 0 92 | ? ` at index #${context.idx}` 93 | : ""; 94 | const maybeParentDescription = context.parent 95 | ? `${contextToSelectorString(context.parent)} => ` 96 | : ""; 97 | return `${maybeParentDescription}${context.selector}${maybeIndexDescription}`; 98 | }; 99 | 100 | export const contextToWaitError = (context: DriverContext): string => { 101 | return `Timeout waiting for element: ${contextToSelectorString(context)}`; 102 | } 103 | 104 | export const isNoElementWithLocatorError = (e: Error): e is NoElementWithLocatorError => { 105 | const type = (e as NoElementWithLocatorError).type || ''; 106 | return type === ErrorTypes.NO_ELEMENTS_WITH_SELECTOR; 107 | }; 108 | 109 | export const isMultipleElementsWithLocatorError = (e: Error): e is MultipleElementsWithLocatorError => { 110 | const type = (e as MultipleElementsWithLocatorError).type || ''; 111 | return type === ErrorTypes.MULTIPLE_ELEMENTS_WITH_SELECTOR; 112 | }; 113 | -------------------------------------------------------------------------------- /website/docs/doc1.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: doc1 3 | title: Style Guide 4 | sidebar_label: Style Guide 5 | slug: / 6 | --- 7 | 8 | You can write content using [GitHub-flavored Markdown syntax](https://github.github.com/gfm/). 9 | 10 | ## Markdown Syntax 11 | 12 | To serve as an example page when styling markdown based Docusaurus sites. 13 | 14 | ## Headers 15 | 16 | # H1 - Create the best documentation 17 | 18 | ## H2 - Create the best documentation 19 | 20 | ### H3 - Create the best documentation 21 | 22 | #### H4 - Create the best documentation 23 | 24 | ##### H5 - Create the best documentation 25 | 26 | ###### H6 - Create the best documentation 27 | 28 | --- 29 | 30 | ## Emphasis 31 | 32 | Emphasis, aka italics, with *asterisks* or _underscores_. 33 | 34 | Strong emphasis, aka bold, with **asterisks** or __underscores__. 35 | 36 | Combined emphasis with **asterisks and _underscores_**. 37 | 38 | Strikethrough uses two tildes. ~~Scratch this.~~ 39 | 40 | --- 41 | 42 | ## Lists 43 | 44 | 1. First ordered list item 45 | 1. Another item 46 | - Unordered sub-list. 47 | 1. Actual numbers don't matter, just that it's a number 48 | 1. Ordered sub-list 49 | 1. And another item. 50 | 51 | * Unordered list can use asterisks 52 | 53 | - Or minuses 54 | 55 | + Or pluses 56 | 57 | --- 58 | 59 | ## Links 60 | 61 | [I'm an inline-style link](https://www.google.com/) 62 | 63 | [I'm an inline-style link with title](https://www.google.com/ "Google's Homepage") 64 | 65 | [I'm a reference-style link][arbitrary case-insensitive reference text] 66 | 67 | [You can use numbers for reference-style link definitions][1] 68 | 69 | Or leave it empty and use the [link text itself]. 70 | 71 | URLs and URLs in angle brackets will automatically get turned into links. http://www.example.com/ or and sometimes example.com (but not on GitHub, for example). 72 | 73 | Some text to show that the reference links can follow later. 74 | 75 | [arbitrary case-insensitive reference text]: https://www.mozilla.org/ 76 | [1]: http://slashdot.org/ 77 | [link text itself]: http://www.reddit.com/ 78 | 79 | --- 80 | 81 | ## Images 82 | 83 | Here's our logo (hover to see the title text): 84 | 85 | Inline-style: ![alt text](https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png 'Logo Title Text 1') 86 | 87 | Reference-style: ![alt text][logo] 88 | 89 | [logo]: https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png 'Logo Title Text 2' 90 | 91 | Images from any folder can be used by providing path to file. Path should be relative to markdown file. 92 | 93 | ![img](../static/img/logo.svg) 94 | 95 | --- 96 | 97 | ## Code 98 | 99 | ```javascript 100 | var s = 'JavaScript syntax highlighting'; 101 | alert(s); 102 | ``` 103 | 104 | ```python 105 | s = "Python syntax highlighting" 106 | print(s) 107 | ``` 108 | 109 | ``` 110 | No language indicated, so no syntax highlighting. 111 | But let's throw in a tag. 112 | ``` 113 | 114 | ```js {2} 115 | function highlightMe() { 116 | console.log('This line can be highlighted!'); 117 | } 118 | ``` 119 | 120 | --- 121 | 122 | ## Tables 123 | 124 | Colons can be used to align columns. 125 | 126 | | Tables | Are | Cool | 127 | | ------------- | :-----------: | -----: | 128 | | col 3 is | right-aligned | \$1600 | 129 | | col 2 is | centered | \$12 | 130 | | zebra stripes | are neat | \$1 | 131 | 132 | There must be at least 3 dashes separating each header cell. The outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown. 133 | 134 | | Markdown | Less | Pretty | 135 | | -------- | --------- | ---------- | 136 | | _Still_ | `renders` | **nicely** | 137 | | 1 | 2 | 3 | 138 | 139 | --- 140 | 141 | ## Blockquotes 142 | 143 | > Blockquotes are very handy in email to emulate reply text. This line is part of the same quote. 144 | 145 | Quote break. 146 | 147 | > This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can _put_ **Markdown** into a blockquote. 148 | 149 | --- 150 | 151 | ## Inline HTML 152 | 153 |
    154 |
    Definition list
    155 |
    Is something people use sometimes.
    156 | 157 |
    Markdown in HTML
    158 |
    Does *not* work **very** well. Use HTML tags.
    159 |
    160 | 161 | --- 162 | 163 | ## Line Breaks 164 | 165 | Here's a line for us to start with. 166 | 167 | This line is separated from the one above by two newlines, so it will be a _separate paragraph_. 168 | 169 | This line is also a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the _same paragraph_. 170 | 171 | --- 172 | 173 | ## Admonitions 174 | 175 | :::note 176 | 177 | This is a note 178 | 179 | ::: 180 | 181 | :::tip 182 | 183 | This is a tip 184 | 185 | ::: 186 | 187 | :::important 188 | 189 | This is important 190 | 191 | ::: 192 | 193 | :::caution 194 | 195 | This is a caution 196 | 197 | ::: 198 | 199 | :::warning 200 | 201 | This is a warning 202 | 203 | ::: 204 | -------------------------------------------------------------------------------- /website/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/static/img/undraw_analyze_17kw.svg: -------------------------------------------------------------------------------- 1 | analyze -------------------------------------------------------------------------------- /adapters/selenium/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Locator, UniDriverList, UniDriver, MapFn, waitFor, NoElementWithLocatorError, MultipleElementsWithLocatorError, isMultipleElementsWithLocatorError, EnterValueOptions, DriverContext, contextToWaitError } from '@unidriver/core'; 2 | import { By, WebElement, Key as SeleniumKey } from 'selenium-webdriver'; 3 | 4 | export type WebElementGetter = () => Promise; 5 | export type WebElementsGetter = () => Promise; 6 | 7 | const camelCaseToHyphen = (key: string) => key.replace(/([a-z])([A-Z])/g, '$1_$2'); 8 | const interpolateSeleniumSpecialKeys = (key: string) => { 9 | switch (key) { 10 | case 'BACKSPACE': 11 | return 'BACK_SPACE'; 12 | 13 | default: 14 | return key; 15 | } 16 | }; 17 | 18 | export const seleniumUniDriverList = ( 19 | wep: WebElementsGetter, 20 | context: DriverContext = {selector: 'Root Selenium list driver'} 21 | ): UniDriverList => { 22 | 23 | const map = async (fn: MapFn) => { 24 | const els = await wep(); 25 | const promises = els.map((e, i) => { 26 | const bd = seleniumUniDriver(() => Promise.resolve(e), { 27 | parent: context, 28 | idx: i, 29 | selector: context.selector, 30 | }); 31 | return fn(bd, i); 32 | }); 33 | return Promise.all(promises); 34 | }; 35 | 36 | return { 37 | get: (idx: number) => 38 | seleniumUniDriver( 39 | async () => { 40 | const els = await wep(); 41 | return els[idx] as any; 42 | }, 43 | { parent: context, selector: context.selector, idx } 44 | ), 45 | text: async () => { 46 | return map((we) => we.text()); 47 | }, 48 | count: async () => { 49 | const els = await wep(); 50 | return els.length; 51 | }, 52 | map, 53 | filter: (fn) => { 54 | return seleniumUniDriverList(async () => { 55 | const elems = await wep(); 56 | 57 | const results = await Promise.all( 58 | elems.map((e, i) => { 59 | const bd = seleniumUniDriver(() => Promise.resolve(e), {parent: context, idx: i, selector: context.selector}); 60 | return fn(bd, i); 61 | }) 62 | ); 63 | 64 | return elems.filter((_, i) => { 65 | return results[i]; 66 | }); 67 | }, context); 68 | }, 69 | }; 70 | }; 71 | 72 | export const seleniumUniDriver = (wep: WebElementGetter, context: DriverContext = {selector: 'Root Selenium driver'}): UniDriver => { 73 | 74 | 75 | const elem = async () => { 76 | const e = await wep(); 77 | if (!e) { 78 | throw new Error(`Cannot find element`); 79 | } 80 | return e; 81 | }; 82 | 83 | const exists = async () => { 84 | try { 85 | await elem(); 86 | return true; 87 | } catch (e) { 88 | if (isMultipleElementsWithLocatorError(e)) { 89 | throw e; 90 | } else { 91 | return false; 92 | } 93 | } 94 | }; 95 | 96 | 97 | const slowType = async (element: WebElement, value: string, delay: number) => { 98 | const driver = await element.getDriver(); 99 | for (let i = 0; i < value.length; i++) { 100 | await element.sendKeys(value[i]); 101 | await driver.sleep(delay); 102 | } 103 | }; 104 | 105 | return { 106 | $: (selector: Locator) => 107 | seleniumUniDriver( 108 | async () => { 109 | const els = await (await elem()).findElements(By.css(selector)); 110 | if (els.length === 0) { 111 | throw new NoElementWithLocatorError(selector); 112 | } else if (els.length > 1) { 113 | throw new MultipleElementsWithLocatorError(els.length, selector); 114 | } else { 115 | return els[0]; 116 | } 117 | }, 118 | { parent: context, selector: selector } 119 | ), 120 | $$: (selector: Locator) => 121 | seleniumUniDriverList( 122 | async () => { 123 | const el = await elem(); 124 | return el.findElements(By.css(selector)); 125 | }, 126 | { parent: context, selector: selector } 127 | ), 128 | text: async () => { 129 | const el = await elem(); 130 | return el.getText(); 131 | }, 132 | attr: async (name) => { 133 | const el = await elem(); 134 | return el.getAttribute(name); 135 | }, 136 | value: async () => { 137 | const el = await elem(); 138 | return el.getAttribute("value"); 139 | }, 140 | click: async () => (await elem()).click(), 141 | hover: async () => { 142 | const el = await elem(); 143 | const driver = await el.getDriver(); 144 | const actions = await driver.actions(); 145 | return actions.mouseMove(el).perform(); 146 | }, 147 | pressKey: async (key) => { 148 | const el = await elem(); 149 | const realKey = interpolateSeleniumSpecialKeys( 150 | camelCaseToHyphen(`${key}`).toUpperCase() 151 | ); 152 | const value = SeleniumKey[realKey as keyof typeof SeleniumKey] as string; 153 | if (value) { 154 | await el.sendKeys(value); 155 | } else { 156 | return el.sendKeys(key); 157 | } 158 | }, 159 | hasClass: async (className: string) => { 160 | const el = await elem(); 161 | const cl = await el.getAttribute("class"); 162 | return cl.split(" ").includes(className); 163 | }, 164 | enterValue: async ( 165 | value: string, 166 | { delay, shouldClear = true }: EnterValueOptions = {} 167 | ) => { 168 | const el = await elem(); 169 | const disabled = await el.getAttribute("disabled"); 170 | const readOnly = await el.getAttribute("readOnly"); 171 | // Don't do anything if element is disabled or readOnly 172 | if (disabled || readOnly) { 173 | return; 174 | } 175 | if (shouldClear) { 176 | await el.clear(); 177 | } 178 | if (delay) { 179 | await slowType(el, value, delay); 180 | } else { 181 | await el.sendKeys(value); 182 | } 183 | }, 184 | exists, 185 | isDisplayed: async () => { 186 | const el = await elem(); 187 | 188 | const retValue: boolean = await el 189 | .getDriver() 190 | .executeScript( 191 | "const elem = arguments[0], " + 192 | " box = elem.getBoundingClientRect(), " + 193 | " cx = box.left + box.width / 2, " + 194 | " cy = box.top + box.height / 2, " + 195 | " e = document.elementFromPoint(cx, cy); " + 196 | " for (; e; e = e.parentElement) { " + 197 | " if ( e === elem) return true; " + 198 | " } " + 199 | "" + 200 | " return false;", 201 | el 202 | ); 203 | return retValue; 204 | }, 205 | mouse: { 206 | press: async () => { 207 | const el = await elem(); 208 | const driver = await el.getDriver(); 209 | const actions = await driver.actions(); 210 | await actions.mouseDown(el).perform(); 211 | }, 212 | release: async () => { 213 | const el = await elem(); 214 | const driver = await el.getDriver(); 215 | const actions = await driver.actions(); 216 | await actions.mouseUp(el).perform(); 217 | }, 218 | moveTo: async (to) => { 219 | const el = await elem(); 220 | const driver = await el.getDriver(); 221 | const actions = await driver.actions(); 222 | const native = await to.getNative(); 223 | await actions.mouseMove(native).perform(); 224 | }, 225 | leave: async () => { 226 | const el = await elem(); 227 | const driver = await el.getDriver(); 228 | const actions = await driver.actions(); 229 | await actions.mouseMove(el).perform(); 230 | await actions.mouseMove({x: -999, y: -999}).perform(); 231 | }, 232 | }, 233 | wait: async (timeout?: number) => { 234 | return waitFor(exists, timeout, 30, contextToWaitError(context)); 235 | }, 236 | type: "selenium", 237 | scrollIntoView: async () => { 238 | const el = await elem(); 239 | return el.getDriver().executeScript("arguments[0].scrollIntoView();", el); 240 | }, 241 | getNative: elem, 242 | _prop: async (name: string) => { 243 | const el = await elem(); 244 | return el 245 | .getDriver() 246 | .executeScript("return arguments[0][arguments[1]];", el, name); 247 | }, 248 | }; 249 | }; 250 | -------------------------------------------------------------------------------- /adapters/protractor/src/index.ts: -------------------------------------------------------------------------------- 1 | import {browser, ElementFinder} from 'protractor'; 2 | import {Key as SeleniumKey} from 'selenium-webdriver'; 3 | import {Locator, UniDriverList, UniDriver, MapFn, waitFor, NoElementWithLocatorError, MultipleElementsWithLocatorError, isMultipleElementsWithLocatorError, EnterValueOptions, DriverContext, contextToWaitError} from '@unidriver/core'; 4 | 5 | type TsSafeElementFinder = Omit; 6 | 7 | type ElementGetter = () => Promise; 8 | type ElementsGetter = () => Promise; 9 | 10 | 11 | const camelCaseToHyphen = (key: string) => key.replace(/([a-z])([A-Z])/g, '$1_$2'); 12 | const interpolateSeleniumSpecialKeys = (key: string) => { 13 | switch (key) { 14 | case 'BACKSPACE': 15 | return 'BACK_SPACE'; 16 | 17 | default: 18 | return key; 19 | } 20 | }; 21 | 22 | export const protractorUniDriverList = ( 23 | elems: ElementsGetter, 24 | context: DriverContext = {selector: 'Root Protractor list driver'} 25 | ): UniDriverList => { 26 | const map = async (fn: MapFn) => { 27 | const els = await elems(); 28 | const promises = els.map((e, i) => { 29 | const bd = protractorUniDriver(() => Promise.resolve(e), { parent: context, idx: i, selector: context.selector }); 30 | return fn(bd, i); 31 | }); 32 | return Promise.all(promises); 33 | }; 34 | 35 | return { 36 | get: (idx: number) => { 37 | const elem = async () => { 38 | const els: any = await elems(); // any due to element finder also having a "then" property, causing TS1058 39 | return els[idx]; 40 | }; 41 | return protractorUniDriver(elem, {parent: context, idx, selector: context.selector}); 42 | }, 43 | text: async () => { 44 | return map(d => d.text()); 45 | }, 46 | count: async () => { 47 | const els = await elems(); 48 | return els.length; 49 | }, 50 | map, 51 | filter: fn => { 52 | return protractorUniDriverList( 53 | async () => { 54 | const els = await elems(); 55 | 56 | const results = await Promise.all( 57 | els.map((e, i) => { 58 | const bd = protractorUniDriver(() => Promise.resolve(e), { 59 | parent: context, 60 | idx: i, 61 | selector: context.selector, 62 | }); 63 | return fn(bd, i); 64 | }) 65 | ); 66 | 67 | return els.filter((_, i) => { 68 | return results[i]; 69 | }); 70 | }, 71 | { parent: context, selector: context.selector } 72 | ); 73 | } 74 | }; 75 | }; 76 | 77 | export const protractorUniDriver = ( 78 | el: ElementGetter, 79 | context: DriverContext = {selector: 'Root Protractor driver'} 80 | ): UniDriver => { 81 | const safeElem: () => Promise = async () => { 82 | const e = await el(); 83 | if (!e || !(await e.isPresent())) { 84 | throw new Error(`Cannot find element`); 85 | } 86 | return e as TsSafeElementFinder; 87 | }; 88 | 89 | const exists = async () => { 90 | try { 91 | await safeElem(); 92 | return true; 93 | } catch (e) { 94 | if (isMultipleElementsWithLocatorError(e)) { 95 | throw e; 96 | } else { 97 | return false; 98 | } 99 | } 100 | }; 101 | 102 | const slowType = async (element: TsSafeElementFinder, value: string, delay: number) => { 103 | for (let i = 0; i < value.length; i++) { 104 | await element.sendKeys(value[i]); 105 | await browser.sleep(delay); 106 | } 107 | }; 108 | 109 | const adapter: UniDriver = { 110 | // done 111 | $: (newLoc: Locator) => { 112 | return protractorUniDriver( 113 | async () => { 114 | const elmArrFinder = (await safeElem()).$$(newLoc); 115 | const count = await elmArrFinder.count(); 116 | if (count === 0) { 117 | throw new NoElementWithLocatorError(newLoc); 118 | } else if (count > 1) { 119 | throw new MultipleElementsWithLocatorError( 120 | elmArrFinder.length, 121 | newLoc 122 | ); 123 | } 124 | return elmArrFinder.get(0) as TsSafeElementFinder; 125 | }, 126 | { parent: context, selector: newLoc } 127 | ); 128 | }, 129 | // done 130 | $$: (selector: Locator) => 131 | protractorUniDriverList( 132 | async () => { 133 | const element = await safeElem(); 134 | return element.$$(selector); 135 | }, 136 | { parent: context, selector } 137 | ), 138 | text: async () => { 139 | const text = await (await safeElem()).getAttribute("textContent"); 140 | return text || ""; 141 | }, 142 | click: async () => { 143 | return (await safeElem()).click(); 144 | }, 145 | hover: async () => { 146 | const e = await safeElem(); 147 | 148 | return browser.actions() 149 | .mouseMove(e as ElementFinder) 150 | // .mouseDown(e) 151 | // .mouseUp(e) 152 | .perform(); 153 | }, 154 | pressKey: async (key) => { 155 | const el = await safeElem(); 156 | const realKey = interpolateSeleniumSpecialKeys( 157 | camelCaseToHyphen(`${key}`).toUpperCase() 158 | ); 159 | const value = SeleniumKey[realKey as keyof typeof SeleniumKey] as string; 160 | if (value) { 161 | await el.sendKeys(value); 162 | } else { 163 | return el.sendKeys(key); 164 | } 165 | }, 166 | hasClass: async (className: string) => { 167 | const cm = await (await safeElem()).getAttribute("class"); 168 | return cm.split(" ").includes(className); 169 | }, 170 | enterValue: async ( 171 | value: string, 172 | { delay, shouldClear = true }: EnterValueOptions = {} 173 | ) => { 174 | const e = await safeElem(); 175 | const disabled = await e.getAttribute("disabled"); 176 | const readOnly = await e.getAttribute("readOnly"); 177 | // Don't do anything if element is disabled or readOnly 178 | if (disabled || readOnly) { 179 | return; 180 | } 181 | if (shouldClear) { 182 | await e.clear(); 183 | } 184 | if (delay) { 185 | await slowType(e, value, delay); 186 | } else { 187 | await e.sendKeys(value); 188 | } 189 | }, 190 | mouse: { 191 | press: async () => { 192 | 193 | const e = await safeElem() as ElementFinder; 194 | return browser.actions() 195 | .mouseMove(e) 196 | .mouseDown(e) 197 | .perform(); 198 | 199 | }, 200 | release: async () => { 201 | const e = await safeElem() as ElementFinder; 202 | 203 | return browser.actions() 204 | .mouseMove(e) 205 | .mouseUp(e) 206 | .perform(); 207 | }, 208 | moveTo: async (to) => { 209 | const nativeElem = await to.getNative() as ElementFinder; 210 | 211 | return browser.actions() 212 | .mouseMove(nativeElem) 213 | .perform(); 214 | }, 215 | leave: async () => { 216 | const e = await safeElem() as ElementFinder; 217 | await browser.actions() 218 | .mouseMove(e) 219 | .perform(); 220 | 221 | return browser.actions() 222 | .mouseMove({ x: -999, y: -999 }) 223 | .perform(); 224 | }, 225 | }, 226 | exists, 227 | isDisplayed: async () => { 228 | const el = await safeElem(); 229 | 230 | const retValue: boolean = await browser.executeScript( 231 | "const elem = arguments[0], " + 232 | " box = elem.getBoundingClientRect(), " + 233 | " cx = box.left + box.width / 2, " + 234 | " cy = box.top + box.height / 2, " + 235 | " e = document.elementFromPoint(cx, cy); " + 236 | " for (; e; e = e.parentElement) { " + 237 | " if ( e === elem) return true; " + 238 | " } " + 239 | "" + 240 | " return false;", 241 | el 242 | ); 243 | return retValue; 244 | }, 245 | value: async () => { 246 | const value = await (await safeElem()).getAttribute("value"); 247 | return value || ""; 248 | }, 249 | attr: async (name) => { 250 | const attr = await (await safeElem()).getAttribute(name); 251 | return attr; 252 | }, 253 | wait: async (timeout?: number) => { 254 | return waitFor(exists, timeout, 30, contextToWaitError(context)); 255 | }, 256 | type: "protractor", 257 | scrollIntoView: async () => { 258 | const el = await safeElem(); 259 | return browser.executeScript( 260 | (el: HTMLElement) => el.scrollIntoView(), 261 | el.getWebElement() 262 | ); 263 | }, 264 | getNative: safeElem, 265 | _prop: async (name: string) => { 266 | const el = await safeElem(); 267 | return browser.executeScript( 268 | function () { 269 | return arguments[0][arguments[1]]; 270 | }, 271 | el.getWebElement(), 272 | name 273 | ); 274 | }, 275 | }; 276 | 277 | return adapter; 278 | }; 279 | -------------------------------------------------------------------------------- /website/static/img/undraw_Documents_re_isxv.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /adapters/jsdom-react/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UniDriverList, 3 | Locator, 4 | UniDriver, 5 | waitFor, 6 | getDefinitionForKeyType, 7 | delay as sleep, 8 | EnterValueOptions, 9 | NoElementWithLocatorError, 10 | isMultipleElementsWithLocatorError, 11 | MultipleElementsWithLocatorError, 12 | DriverContext, 13 | contextToWaitError, 14 | } from "@unidriver/core"; 15 | import { Simulate } from 'react-dom/test-utils'; 16 | 17 | type ElementOrElementFinder = (() => Element) | Element | (() => Promise); 18 | type ElementsOrElementsFinder = (() => Element[]) | Element[] | (() => Promise); 19 | 20 | const isPromise = (a: Promise | any ): a is Promise => { 21 | return !!((a as any).then); 22 | }; 23 | 24 | export const jsdomReactUniDriverList = (containerOrFn: ElementsOrElementsFinder, context: DriverContext = {selector: 'Root React list driver'}): UniDriverList => { 25 | const elem = async () => { 26 | const elements = typeof containerOrFn === 'function' ? containerOrFn() : containerOrFn; 27 | return isPromise(elements) ? await elements : elements; 28 | }; 29 | 30 | return { 31 | get: (idx: number) => 32 | jsdomReactUniDriver(() => { 33 | return elem().then((cont) => { 34 | const elem = cont[idx]; 35 | if (!elem) { 36 | throw new Error("React base driver - element was not found"); 37 | } else { 38 | return elem; 39 | } 40 | }); 41 | }, { ...context, idx }), 42 | text: async () => (await elem()).map((e) => e.textContent || ""), 43 | count: async () => (await elem()).length, 44 | map: async (fn) => { 45 | const children = Array.from(await elem()); 46 | return Promise.all( 47 | children.map((e, idx) => { 48 | return fn( 49 | jsdomReactUniDriver(e, { 50 | parent: context, 51 | idx, 52 | selector: context.selector, 53 | }), 54 | idx 55 | ); 56 | }) 57 | ); 58 | }, 59 | filter: (fn) => { 60 | return jsdomReactUniDriverList(async () => { 61 | const elems = await elem(); 62 | 63 | const results = await Promise.all( 64 | elems.map((e, i) => { 65 | const bd = jsdomReactUniDriver(e, { parent: context, idx: i, selector: context.selector }); 66 | return fn(bd, i); 67 | }) 68 | ); 69 | 70 | return elems.filter((_, i) => { 71 | return results[i]; 72 | }); 73 | }, context); 74 | }, 75 | }; 76 | }; 77 | 78 | type HTMLFocusableElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement; 79 | 80 | const elementIsFocusable = (el: Element): el is HTMLFocusableElement => { 81 | return ( 82 | el.tagName === 'INPUT' || 83 | el.tagName === 'SELECT' || 84 | el.tagName === 'TEXTAREA' || 85 | el.tagName === 'BUTTON' || 86 | el.tagName === 'svg' 87 | ) 88 | } 89 | 90 | const isCheckable = (el: Element): boolean => { 91 | return ( 92 | el.tagName === 'INPUT' && 93 | ((el as HTMLInputElement).type == 'checkbox' || 94 | (el as HTMLInputElement).type == 'radio') 95 | ); 96 | }; 97 | 98 | const slowType = async (element: JSX.IntrinsicElements['input'], value: string, delay: number) => { 99 | const { name, type } = element; 100 | let currentValue = ""; 101 | for (let i = 0; i < value.length; i++) { 102 | currentValue += value[i]; 103 | Simulate.change(element as Element, { 104 | target: { name, type, value: currentValue } as HTMLInputElement 105 | }); 106 | await sleep(delay); 107 | } 108 | }; 109 | 110 | export const jsdomReactUniDriver = (containerOrFn: ElementOrElementFinder, context: DriverContext = {selector: 'Root React driver'}): UniDriver => { 111 | 112 | const elem = async () => { 113 | const container = typeof containerOrFn === 'function' ? containerOrFn() : containerOrFn; 114 | if (!container) { 115 | throw new Error('React base driver - element was not found'); 116 | } 117 | return container; 118 | }; 119 | 120 | const exists = async () => { 121 | try { 122 | await elem(); 123 | return true; 124 | } catch (e) { 125 | if (isMultipleElementsWithLocatorError(e)) { 126 | throw e; 127 | } else { 128 | return false; 129 | } 130 | } 131 | }; 132 | 133 | const handleCheckableInput = async (input: HTMLInputElement) => { 134 | input.checked = !input.checked; 135 | Simulate.input(input); 136 | Simulate.change(input); 137 | } 138 | 139 | return { 140 | $: (loc: Locator) => { 141 | const getElement = async () => { 142 | const container = await elem(); 143 | const elements = container.querySelectorAll(loc); 144 | if (!elements.length) { 145 | throw new NoElementWithLocatorError(loc); 146 | } else if (elements.length > 1) { 147 | throw new MultipleElementsWithLocatorError(elements.length, loc); 148 | } 149 | return elements[0]; 150 | }; 151 | return jsdomReactUniDriver(getElement, { parent: context, selector: loc }); 152 | }, 153 | $$: (selector: Locator) => jsdomReactUniDriverList(async () => { 154 | const e = await elem(); 155 | return Array.from(e.querySelectorAll(selector)); 156 | }, { parent: context, selector}), 157 | text: async () => elem().then((e) => e.textContent || ''), 158 | value: async () => { 159 | const e = (await elem()) as HTMLInputElement; 160 | return e.value; 161 | }, 162 | click: async () => { 163 | const el = await elem(); 164 | const eventData = { button: 0 }; // 0 - Main Button (Left) 165 | // setting button 0 is now needed in React 16+ as it's not set by react anymore 166 | // 15 - https://github.com/facebook/react/blob/v15.6.1/src/renderers/dom/client/syntheticEvents/SyntheticMouseEvent.js#L45 167 | // 16 - https://github.com/facebook/react/blob/master/packages/react-dom/src/events/SyntheticMouseEvent.js#L33 168 | Simulate.mouseDown(el, eventData); 169 | 170 | if (elementIsFocusable(el)) { 171 | if (document.activeElement != el) { 172 | if (document.activeElement) { 173 | Simulate.blur(document.activeElement); 174 | } 175 | 176 | if (!el.disabled) { 177 | el.focus(); 178 | 179 | Simulate.focus(el); 180 | } 181 | } 182 | } 183 | 184 | 185 | Simulate.mouseUp(el, eventData); 186 | Simulate.click(el, eventData); 187 | 188 | if (isCheckable(el)) { 189 | handleCheckableInput(el as HTMLInputElement); 190 | } 191 | }, 192 | mouse: { 193 | press: async() => { 194 | const el = await elem(); 195 | 196 | Simulate.mouseDown(el); 197 | }, 198 | release: async () => { 199 | const el = await elem(); 200 | 201 | Simulate.mouseUp(el); 202 | }, 203 | moveTo: async (to) => { 204 | const el = await elem(); 205 | const {left, top} = (await to.getNative()).getBoundingClientRect(); 206 | 207 | Simulate.mouseMove(el, {clientX: left, clientY: top}); 208 | }, 209 | leave: async () => { 210 | const el = await elem(); 211 | 212 | Simulate.mouseLeave(el); 213 | } 214 | }, 215 | hover: async () => { 216 | const el = await elem(); 217 | 218 | Simulate.mouseOver(el); 219 | Simulate.mouseEnter(el); 220 | }, 221 | pressKey: async (key) => { 222 | const el = await elem(); 223 | const def = getDefinitionForKeyType(key); 224 | 225 | 226 | // enabling this throws an error with JSDOM. Thuss, pesskey will only use Simulate 227 | /* 228 | if (document.body.contains(el)) { 229 | // const keydown = new KeyboardEvent('keydown', {...def}); 230 | // const keyup = new KeyboardEvent('keyup', {...def}); 231 | 232 | 233 | // keydown.initEvent(keydown.type, true, false); 234 | // el.dispatchEvent(keydown); 235 | // await (Promise.resolve()); 236 | // keyup.initEvent(keyup.type, true, false); 237 | // el.dispatchEvent(keyup) 238 | 239 | // } else { 240 | 241 | // } 242 | */ 243 | Simulate.keyDown(el, def); 244 | Simulate.keyUp(el, def); 245 | }, 246 | hasClass: async (className: string) => (await elem()).classList.contains(className), 247 | enterValue: async (value: string, options?: EnterValueOptions) => { 248 | const el = (await elem()) as JSX.IntrinsicElements['input']; 249 | 250 | // Don't do anything if element is disabled or readOnly 251 | if (el.disabled || el.readOnly) { 252 | return; 253 | } 254 | 255 | const { name, type, onChange } = el; 256 | // Set native value for uncontrolled component 257 | if (!onChange) { 258 | el.value = value; 259 | } 260 | if (options?.delay) { 261 | await slowType(el, value, options.delay); 262 | } 263 | Simulate.change(el as Element, { 264 | target: { name, type, value } as HTMLInputElement 265 | }); 266 | }, 267 | attr: async (name: string) => { 268 | const el = await elem(); 269 | return el.getAttribute(name); 270 | }, 271 | exists, 272 | isDisplayed: async () => { 273 | return true; 274 | }, 275 | wait: async (timeout?: number) => { 276 | return waitFor(exists, timeout, 30, contextToWaitError(context)); 277 | }, 278 | type: 'react', 279 | scrollIntoView: async () => { return {} }, 280 | getNative: () => elem(), 281 | _prop: async (name: string) => { 282 | const el = await elem(); 283 | return (el as any)[name]; 284 | } 285 | }; 286 | }; 287 | -------------------------------------------------------------------------------- /adapters/jsdom-svelte/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getDefinitionForKeyType, 3 | isMultipleElementsWithLocatorError, 4 | Locator, 5 | MultipleElementsWithLocatorError, 6 | NoElementWithLocatorError, 7 | UniDriver, UniDriverList, waitFor, 8 | delay as sleep, 9 | EnterValueOptions, 10 | DriverContext, 11 | contextToWaitError, 12 | } from "@unidriver/core"; 13 | 14 | import {fireEvent} from '@testing-library/svelte'; 15 | 16 | type HTMLFocusableElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement; 17 | type ElementOrElementFinder = (() => Element) | Element | (() => Promise); 18 | type ElementsOrElementsFinder = (() => Element[]) | Element[] | (() => Promise); 19 | 20 | const isPromise = (a: Promise | any ): a is Promise => { 21 | return !!((a as any).then); 22 | }; 23 | 24 | const elementIsFocusableAndNotAnchor = (el: Element): el is HTMLFocusableElement => { 25 | return ( 26 | el.tagName === 'INPUT' || 27 | el.tagName === 'SELECT' || 28 | el.tagName === 'TEXTAREA' || 29 | el.tagName === 'BUTTON' 30 | ) 31 | } 32 | 33 | 34 | const isCheckable = (el: Element): boolean => { 35 | return ( 36 | el.tagName === 'INPUT' && 37 | ((el as HTMLInputElement).type == 'checkbox' || 38 | (el as HTMLInputElement).type == 'radio') 39 | ); 40 | }; 41 | 42 | const slowType = async (element: HTMLInputElement, value: string, delay: number) => { 43 | const { name, type } = element; 44 | let currentValue = ""; 45 | for (let i = 0; i < value.length; i++) { 46 | currentValue += value[i]; 47 | await fireEvent.change(element, { 48 | target: { name, type, value: currentValue } 49 | }); 50 | await sleep(delay); 51 | } 52 | }; 53 | 54 | 55 | export const jsdomSvelteUniDriver = (containerOrFn: ElementOrElementFinder, context: DriverContext = {selector: 'Root Svelte driver'}): UniDriver => { 56 | 57 | const elem = async () => { 58 | const container = typeof containerOrFn === 'function' ? containerOrFn() : containerOrFn; 59 | if (!container) { 60 | throw new Error('Svelte base driver - element was not found'); 61 | } 62 | return container; 63 | }; 64 | 65 | const exists = async () => { 66 | try { 67 | await elem(); 68 | return true; 69 | } catch (e) { 70 | if (isMultipleElementsWithLocatorError(e)) { 71 | throw e; 72 | } else { 73 | return false; 74 | } 75 | } 76 | }; 77 | 78 | const handleCheckableInput = async (input: HTMLInputElement) => { 79 | input.checked = !input.checked; 80 | await fireEvent.input(input); 81 | await fireEvent.change(input); 82 | } 83 | 84 | return { 85 | $: (loc: Locator) => { 86 | const getElement = async () => { 87 | const container = await elem(); 88 | const elements = container.querySelectorAll(loc); 89 | if (!elements.length) { 90 | throw new NoElementWithLocatorError(loc); 91 | } else if (elements.length > 1) { 92 | throw new MultipleElementsWithLocatorError(elements.length, loc); 93 | } 94 | return elements[0]; 95 | }; 96 | return jsdomSvelteUniDriver(getElement, {parent: context, selector: loc}); 97 | }, 98 | $$: (selector: Locator) => jsdomSvelteUniDriverList(async () => { 99 | const e = await elem(); 100 | return Array.from(e.querySelectorAll(selector)); 101 | }, {parent: context, selector}), 102 | text: async () => elem().then((e) => e.textContent || ''), 103 | value: async () => { 104 | const e = (await elem()) as HTMLInputElement; 105 | return e.value; 106 | }, 107 | click: async () => { 108 | const el = await elem(); 109 | await fireEvent.mouseDown(el); 110 | 111 | if (elementIsFocusableAndNotAnchor(el)) { 112 | if (document.activeElement != el) { 113 | if (document.activeElement) { 114 | fireEvent.blur(document.activeElement); 115 | } 116 | 117 | if (!el.disabled) { 118 | el.focus(); 119 | await fireEvent.focus(el); 120 | } 121 | } 122 | } 123 | 124 | 125 | await fireEvent.mouseUp(el); 126 | await fireEvent.click(el); 127 | 128 | if (isCheckable(el)) { 129 | handleCheckableInput(el as HTMLInputElement); 130 | } 131 | }, 132 | mouse: { 133 | press: async() => { 134 | const el = await elem(); 135 | 136 | await fireEvent.mouseDown(el); 137 | }, 138 | release: async () => { 139 | const el = await elem(); 140 | 141 | await fireEvent.mouseUp(el); 142 | }, 143 | moveTo: async (to) => { 144 | const el = await elem(); 145 | const {left, top} = (await to.getNative()).getBoundingClientRect(); 146 | 147 | await fireEvent.mouseMove(el, {clientX: left, clientY: top}); 148 | }, 149 | leave: async () => { 150 | const el = await elem(); 151 | 152 | await fireEvent.mouseLeave(el); 153 | } 154 | }, 155 | hover: async () => { 156 | const el = await elem(); 157 | 158 | await fireEvent.mouseOver(el); 159 | await fireEvent.mouseEnter(el); 160 | }, 161 | pressKey: async (key) => { 162 | const el = await elem(); 163 | const def = getDefinitionForKeyType(key); 164 | 165 | await fireEvent.keyDown(el, def); 166 | await fireEvent.keyUp(el, def); 167 | }, 168 | hasClass: async (className: string) => (await elem()).classList.contains(className), 169 | enterValue: async (value: string, options?: EnterValueOptions) => { 170 | const el = (await elem()) as HTMLInputElement; 171 | const { name, type, disabled, readOnly } = el; 172 | // Don't do anything if element is disabled or readOnly 173 | if (disabled || readOnly) { 174 | return; 175 | } 176 | if (options?.delay) { 177 | await slowType(el, value, options.delay); 178 | } else { 179 | await fireEvent.change(el, { 180 | target: { name, type, value } 181 | }); 182 | } 183 | }, 184 | attr: async (name: string) => { 185 | const el = await elem(); 186 | return el.getAttribute(name); 187 | }, 188 | exists, 189 | isDisplayed: async () => { 190 | return true; 191 | }, 192 | wait: async (timeout?: number) => { 193 | return waitFor(exists, timeout, 30, contextToWaitError(context)); 194 | }, 195 | type: 'svelte', 196 | scrollIntoView: async () => { return {} }, 197 | getNative: () => elem(), 198 | _prop: async (name: string) => { 199 | const el = await elem(); 200 | return (el as any)[name]; 201 | } 202 | }; 203 | }; 204 | 205 | export const jsdomSvelteUniDriverList = (containerOrFn: ElementsOrElementsFinder, context: DriverContext = {selector: 'Root Svelte list driver'}): UniDriverList => { 206 | const elem = async () => { 207 | const elements = typeof containerOrFn === 'function' ? containerOrFn() : containerOrFn; 208 | return isPromise(elements) ? await elements : elements; 209 | }; 210 | 211 | return { 212 | get: (idx: number) => jsdomSvelteUniDriver(() => { 213 | return elem().then((cont) => { 214 | const elem = cont[idx]; 215 | if (!elem) { 216 | throw new Error('Svelte base driver - element was not found'); 217 | } else { 218 | return elem; 219 | } 220 | }); 221 | }, {parent: context, idx, selector: context.selector}), 222 | text: async () => (await elem()).map((e) => e.textContent || ''), 223 | count: async () => (await elem()).length, 224 | map: async (fn) => { 225 | const children = Array.from(await elem()); 226 | return Promise.all(children.map((e, idx) => { 227 | return fn(jsdomSvelteUniDriver(e, {parent: context, selector: context.selector, idx}), idx); 228 | })); 229 | }, 230 | filter: (fn) => { 231 | return jsdomSvelteUniDriverList(async () => { 232 | const elems = await elem(); 233 | 234 | const results = await Promise.all(elems.map((e, i) => { 235 | const bd = jsdomSvelteUniDriver(e, {parent: context, idx: i, selector: context.selector}); 236 | return fn(bd, i); 237 | })); 238 | 239 | return elems.filter((_, i) => { 240 | return results[i]; 241 | }); 242 | }, {parent: context, selector: context.selector}); 243 | } 244 | }; 245 | }; 246 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UniDriver 2 | Universal API to control your UI components that work in all test levels. From component jsdom tests, to browser testing using Puppeteer / Selenium. 3 | Making BDD fun in the modular UI area. 4 | 5 | 6 | [![NPM Version](https://img.shields.io/npm/v/unidriver.svg?style=flat)](https://www.npmjs.com/package/unidriver) 7 | [![Build Status](https://circleci.com/gh/wix-incubator/unidriver.svg?style=shield)](https://circleci.com/gh/wix-incubator/unidriver) 8 | 9 | 10 | UniDriver makes it saner to write UI testing. It enables creating component drivers that will work on all platforms, from jsdom to puppeteer and selenium. 11 | 12 | [Migrating from ~v2.0.1 to latest version](/migrating.md) 13 | 14 | ## Component Drivers 15 | Testing UI is hard. There are many reasons for that, but we believe a big one relies in the fact that unlike functions or services, where the API is clearly defined, when dealing with graphical user interfaces, it's up for the developer to transform it into an "API" for testing purposes. 16 | Back in the days, [PageObjects](https://martinfowler.com/bliki/PageObject.html) helped mitigate this fact, but once the world moved to modular components, our test code quality degraded and became bloated with repetition and lack of abstraction. 17 | Component drivers are just like page objects, but for your components. Just like page objects, this is merely a pattern, and is not coupled to a specific implementation. 18 | However, using UniDriver as the basis for your component drivers will help you leverage years of trial and error and be able to fully re-use your drivers across platforms. 19 | This allows you to confidently write tests that use your actual implementation and keep focusing on the *"what"* and not the *"how"* 20 | 21 | 22 | ## Philosophy 23 | UniDriver aims to provide an API for what a manual tester can do. This means that the API will not focus on implementation, but on the actual action a user would take. 24 | For example, a user doesn't `focus` an input, it clicks on it (and the browser gives it focus as a result). A user doesn't mouseUp, it clicks. It hovers. 25 | 26 | Moreover, just like users, UniDriver is lazy and async: 27 | - Lazy - all actions are evaluated only when needed. Calling `const comp = $('.bob)` will not do anything until something is requested (like `comp.text()` or `comp.click()`). This allows to create locators for parts of the UI without relying on the element to be visible. This is particularly helpful on JSDOM level tests. 28 | - Async - all actions returns are async (just like real users), and thus all interactions will return a promise. 29 | 30 | ## Examples 31 | In the [examples](/examples) folder you can find 3 small apps; a todo-app, a counter and a multi counter. 32 | Each app contains a driver that uses UniDriver, component tests and e2e tests (puppeteer, todo app has selenium too). 33 | As you can see, all test levels use the *same* driver, meaning that if the feature's implementation changes, you'll need to change the driver alone, not the tests. 34 | 35 | 36 | ## Usage 37 | This library provides an API to interact with UI elements - `UniDriver` and `UniDriverList`, that combines the common features between all popular automation libraries and solutions. This API is represented by a type definition only, and should be implemented by adapters. 38 | 39 | The idea is that a component driver is a function of the driver of it's container, and is completely agnostic to how the initial driver was originally generated. 40 | For example, a driver for a todo App might look like this: 41 | 42 | ```typescript 43 | const todoAppDriver = (base: UniDriver) => { 44 | return { 45 | getItems: async () => base.$$('.title').text(), 46 | addItem: async (text: string) => { 47 | await base.$('input').enterValue(text); 48 | await base.$('button').click(); 49 | }, 50 | deleteItem: async (idx: number) => base.$$('.item').get(idx).$('.delete').click(), 51 | getCount: async () => parseInt(await base.$('.count').text(), 10), 52 | isDone: async (idx: number) => { 53 | return base.$$('.item').get(idx).hasClass('done'); 54 | }, 55 | toggleItem: async (idx: number) => base.$$('.item').get(idx).$('.title').click() 56 | }; 57 | }; 58 | ``` 59 | By making it *a function* of a container base driver, we abstract away all implementations details of the platform we're currently running on (for example, `$` will be implemented as `.querySelector` in the DOM/React adapter but on Selenium it'll be `wd.findElement(By.css(selector))`. This makes the `todoAppDriver` shareable between contexts, enabling very DRY tests, and potentially even test sharing as well. 60 | 61 | This pattern can be nested as one see fit, enabling users to create a cross-platform automation API for their apps, from component/unit test level, and up to e2e automation testing. All sharing the using the same drivers, and by that ensuring that the driver works. 62 | 63 | ## Available Adapters 64 | 65 | - [JSDOM - React (DOM + React test utils)](adapters/jsdom-react) 66 | - [JSDOM - Svelte (DOM + @testing-library/svelte)](adapters/jsdom-svelte) 67 | - [Puppeteer](adapters/puppeteer) 68 | - [Selenium](adapters/selenium) 69 | - [Protractor](adapters/protractor) 70 | 71 | Writing an adapter is easy - you just need to implement the UniDriver API. 72 | An standard test suite to ensure the properties of the base drivers are kept through the adapters is in the road map. 73 | 74 | ## FAQ 75 | 76 | ### How do I use it with a portaled elements (i.e. popovers, modals)? 77 | 78 | *Short answer:* 79 | Pass a second UniDriver, representing the "global" popover, and use that to find your popover / modal. 80 | 81 | *Longer answer:* 82 | Some components are composed of portaled components (I'm not referring to React portals, but the concept behind it). 83 | Portaled elements are magical (just like real-life portals), meaning that while conceptually they are part of their parent, implementation wise they are not (at least in the DOM, who knows what native mobile will bring us). 84 | This means that if you want to reference them in your component's driver, you should also give the driver a unidriver wrapper for the whole "body". 85 | 86 | Example: 87 | You have a to-do app, and each row has some actions (rename, delete). The actions menu is inside a popover. 88 | 89 | A naive attempt of a driver might look like this: 90 | 91 | ```typescript 92 | export const createTodoAppDriver = (wrapper: UniDriver) => { 93 | 94 | return { 95 | addItem: (name: string) => { /*type input, click button.. */} 96 | removeItem: (idx: number) => { 97 | const item = wrapper.$$('.item').get(idx); 98 | item.click('.open-actions'); 99 | item.$('.popover').$('.delete').click(); // This won't work!! the ".popover" element is not a direct child of the component 100 | } 101 | } 102 | } 103 | ``` 104 | 105 | With passing a second argument representing the "outer world": 106 | ```typescript 107 | export const createTodoAppDriver = (wrapper: UniDriver, theOuterWorld: UniDriver) => { 108 | 109 | return { 110 | addItem: (name: string) => { /*type input, click button.. */} 111 | removeItem: (idx: number) => { 112 | const item = wrapper.$$('.item').get(idx); 113 | item.click('.open-actions'); 114 | 115 | const popover = theOuterWorld.$('.popover'); 116 | popover.$('.delete').click(); // this will work 117 | } 118 | } 119 | } 120 | ``` 121 | 122 | Because UniDrivers are *lazy*, you can even do this: 123 | 124 | ```typescript 125 | export const createPopoverDriver = (wrapper: UniDriver) => wrapper.$('.popover'); 126 | 127 | export const createTodoAppDriver = (wrapper: UniDriver, theOuterWorld: UniDriver) => { 128 | 129 | const popover = createPopoverDriver(theOuterWorld); 130 | return { 131 | addItem: (name: string) => { /*type input, click button.. */} 132 | removeItem: (idx: number) => { 133 | const item = wrapper.$$('.item').get(idx); 134 | item.click('.open-actions'); 135 | 136 | popover.$('.delete').click(); // this will still work, as the popover will be resolved only here 137 | } 138 | } 139 | } 140 | ``` 141 | 142 | ### How do I click outside MyComponent? 143 | 144 | Clicking outside means clicking on any other element that is not MyComponent. 145 | This will work nicely in real browsers, but JSDOM might not create the right events, so you can add your own simulation to them if you'd like. 146 | 147 | Example: 148 | 149 | ```typescript 150 | export const createMyCoolDriver = (wrapper: UniDriver, theOuterWorld: UniDriver) => { 151 | 152 | dismissModal: () => { 153 | 154 | wrapper.$('.some-element-that-is-not-a-modal').click(); 155 | 156 | // option 1 157 | if (wrapper.type === 'react') { 158 | const domElement = wrapper.getNative(); 159 | clickOnParentUsingDomApi(domElement); 160 | } 161 | 162 | // option 2 - might work.. 163 | theOuterWorld.$('.some-other-element').click(); 164 | } 165 | } 166 | ``` 167 | 168 | 169 | 170 | ## Developing 171 | This project is uses yarn workspaces to manage the internal dependencies 172 | 173 | 1. Set up the monorepo - `yarn` 174 | 2. Build the project - `npm run build` 175 | 3. Run tests - `npm run test` 176 | ### Test Suite 177 | A standard test-suite on each adapter to ensure proper behavior of the API on each adapter. It is given a working todo-app, and by testing it's features and assuming that it is working well, we can test the adapters functionality. 178 | Check out [the code](test-suite/src/run-test-suite.ts) for more details 179 | 180 | ## Road map 181 | - Add more users to validate idea and API 182 | - ~Choose name + rename (runner up - "UniDriver")~ 183 | - ~Add standard test suite for the base driver~ 184 | - ~add tests to current adapters~ 185 | - branding, documentation, more examples 186 | - add driver examples to complex ui components, such as Google material date picker 187 | - add enzyme adapter 188 | - drag and drop support 189 | - add some FAQ (modals, popovers, enzyme?) 190 | - experiment mobile testing 191 | - move to github.com/wix 192 | -------------------------------------------------------------------------------- /adapters/jsdom-react/src/spec.tsx: -------------------------------------------------------------------------------- 1 | import { jsdomReactUniDriver } from '.'; 2 | import { SetupFn, renderTestApp, runTestSuite } from '@unidriver/test-suite'; 3 | // import {KeyboardEventsAppSetupFn, TodoAppSetupFn} from '@unidriver/test-suite'; 4 | import * as sinon from 'sinon'; 5 | import * as React from 'react'; 6 | import * as ReactDOM from 'react-dom'; 7 | 8 | import {assert} from 'chai'; 9 | 10 | const setup: SetupFn = async (params) => { 11 | const cleanJsdom = require('jsdom-global')(); 12 | const div = document.createElement('div'); 13 | document.body.appendChild(div); 14 | const cleanApp = renderTestApp(div, params); 15 | const driver = jsdomReactUniDriver(div); 16 | 17 | const tearDown = () => { 18 | cleanApp(); 19 | cleanJsdom(); 20 | return Promise.resolve(); 21 | }; 22 | 23 | return {driver, tearDown}; 24 | }; 25 | 26 | describe('react base driver - test suite', () => { 27 | runTestSuite({ 28 | setup 29 | }); 30 | }); 31 | 32 | describe('react base driver specific tests', () => { 33 | const spy = sinon.spy; 34 | 35 | describe('click', () => { 36 | it('sends event data properly on simulated events when element is not attached to body', async () => { 37 | const cleanJsdom = require('jsdom-global')(); 38 | const s = spy(); 39 | const elem = document.createElement('div'); 40 | const btn = ; 41 | ReactDOM.render(btn, elem); 42 | 43 | const driver = jsdomReactUniDriver(elem); 44 | 45 | await driver.$('button').click(); 46 | cleanJsdom(); 47 | 48 | assert.equal(s.lastCall.args[0].target.tagName, 'BUTTON'); 49 | }); 50 | 51 | it('sends event data properly on simulated events when element is attached to body', async () => { 52 | const cleanJsdom = require('jsdom-global')(); 53 | const s = spy(); 54 | const elem = document.createElement('div'); 55 | document.body.appendChild(elem); 56 | const btn = ; 57 | ReactDOM.render(btn, elem); 58 | 59 | const driver = jsdomReactUniDriver(elem); 60 | 61 | await driver.$('button').click(); 62 | cleanJsdom(); 63 | 64 | assert.equal(s.lastCall.args[0].target.tagName, 'BUTTON'); 65 | }); 66 | 67 | it('should trigger [mouseDown, mouseUp, click] in this order and with default main-button(0) when clicked', async () => { 68 | const cleanJsdom = require('jsdom-global')(); 69 | const mouseDown = spy(); 70 | const mouseUp = spy(); 71 | const click = spy(); 72 | const elem = document.createElement('div'); 73 | const btn = ( 74 | 80 | ); 81 | ReactDOM.render(btn, elem); 82 | 83 | const driver = jsdomReactUniDriver(elem); 84 | 85 | await driver.$('button').click(); 86 | cleanJsdom(); 87 | 88 | sinon.assert.callOrder(mouseDown,mouseUp,click); 89 | const DEFAULT_BUTTON_ID = '0' 90 | assert.equal(mouseDown.lastCall.args[0].button, DEFAULT_BUTTON_ID); 91 | assert.equal(mouseUp.lastCall.args[0].button, DEFAULT_BUTTON_ID); 92 | assert.equal(click.lastCall.args[0].button, DEFAULT_BUTTON_ID); 93 | }); 94 | 95 | it('should trigger [focus] on click', async () => { 96 | const cleanJsdom = require('jsdom-global')(); 97 | const focus = spy(); 98 | const elem = document.createElement('div'); 99 | const btn = ( 100 | 103 | ); 104 | ReactDOM.render(btn, elem); 105 | 106 | const driver = jsdomReactUniDriver(elem); 107 | 108 | await driver.$('button').click(); 109 | cleanJsdom(); 110 | 111 | assert(focus.calledOnce); 112 | }); 113 | 114 | it('should trigger [mousedown, focus, mouseup, click] events on click', async () => { 115 | // https://jsbin.com/larubagiwu/1/edit?html,js,console,output 116 | // https://github.com/wix-incubator/unidriver/pull/86#issuecomment-516809527 117 | const cleanJsdom = require('jsdom-global')(); 118 | const mousedown = spy(); 119 | const focus = spy(); 120 | const mouseup = spy(); 121 | const click = spy(); 122 | const elem = document.createElement('div'); 123 | const btn = ( 124 | 127 | ); 128 | ReactDOM.render(btn, elem); 129 | 130 | const driver = jsdomReactUniDriver(elem); 131 | 132 | await driver.$('button').click(); 133 | cleanJsdom(); 134 | 135 | sinon.assert.callOrder(mousedown, focus, mouseup, click); 136 | 137 | assert(mousedown.calledOnce); 138 | assert(focus.calledOnce); 139 | assert(mouseup.calledOnce); 140 | assert(click.calledOnce); 141 | }); 142 | 143 | it('should trigger [mousedown, focus, mouseup, click, input, change] events on click on input[type=checkbox]', async () => { 144 | const cleanJsdom = require('jsdom-global')(); 145 | const mousedown = spy(); 146 | const focus = spy(); 147 | const mouseup = spy(); 148 | const click = spy(); 149 | const input = spy(); 150 | const change = spy(); 151 | const elem = document.createElement('div'); 152 | const checkbox = ( 153 | 162 | ); 163 | 164 | ReactDOM.render(checkbox, elem); 165 | 166 | const driver = jsdomReactUniDriver(elem); 167 | 168 | await driver.$('input').click(); 169 | cleanJsdom(); 170 | 171 | sinon.assert.callOrder(mousedown, focus, mouseup, click, input, change); 172 | 173 | assert(mousedown.calledOnce); 174 | assert(focus.calledOnce); 175 | assert(mouseup.calledOnce); 176 | assert(click.calledOnce); 177 | assert(input.calledOnce); 178 | assert(change.calledOnce); 179 | }) 180 | 181 | it('should trigger [focusA, blurA, focusB] when clicking two buttons', async () => { 182 | const cleanJsdom = require('jsdom-global')(); 183 | const focusA = spy(function focusA() {}); 184 | const focusB = spy(function focusB() {}); 185 | const blurA = spy(function blurA() {}); 186 | const elem = document.createElement('div'); 187 | const btn = ( 188 |
    189 | 192 | 195 |
    196 | ); 197 | ReactDOM.render(btn, elem); 198 | 199 | const driver = jsdomReactUniDriver(elem); 200 | 201 | await driver.$('button#A').click(); 202 | await driver.$('button#B').click(); 203 | cleanJsdom(); 204 | 205 | sinon.assert.callOrder(focusA, blurA, focusB); 206 | 207 | assert(focusA.calledOnce); 208 | assert(blurA.calledOnce); 209 | assert(focusB.calledOnce); 210 | }); 211 | 212 | it('should trigger [focusA, blurA] when clicking enabled and disabled button', async () => { 213 | const cleanJsdom = require('jsdom-global')(); 214 | const focusA = spy(); 215 | const focusB = spy(); 216 | const blurA = spy(); 217 | const elem = document.createElement('div'); 218 | const btn = ( 219 |
    220 | 223 | 226 |
    227 | ); 228 | ReactDOM.render(btn, elem); 229 | 230 | const driver = jsdomReactUniDriver(elem); 231 | 232 | await driver.$('button#A').click(); 233 | await driver.$('button#B').click(); 234 | cleanJsdom(); 235 | 236 | sinon.assert.callOrder(focusA, blurA); 237 | 238 | assert(focusA.calledOnce); 239 | assert(blurA.calledOnce); 240 | assert(focusB.notCalled); 241 | }); 242 | 243 | it('should trigger blur on active element when clicking an svg', async () => { 244 | const cleanJsdom = require('jsdom-global')(); 245 | const blurA = spy(); 246 | const elem = document.createElement('div'); 247 | const testApp = ( 248 |
    249 | 252 | 253 | 254 | 255 |
    256 | ); 257 | ReactDOM.render(testApp, elem); 258 | 259 | const driver = jsdomReactUniDriver(elem); 260 | 261 | await driver.$('button#A').click(); 262 | await driver.$('svg#B').click(); 263 | cleanJsdom(); 264 | 265 | assert(blurA.calledOnce); 266 | }); 267 | }) 268 | 269 | describe('enterValue', () => { 270 | it('should set event target properly', async () => { 271 | const cleanJsdom = require('jsdom-global')(); 272 | const change = spy(); 273 | const elem = document.createElement('div'); 274 | const input = ( 275 | 280 | ); 281 | 282 | ReactDOM.render(input, elem); 283 | 284 | const driver = jsdomReactUniDriver(elem); 285 | 286 | await driver.$('input').enterValue('some keywords'); 287 | assert(change.calledOnce); 288 | 289 | const eventTarget = change.args[0][0].target; 290 | assert.equal(eventTarget.name, "search"); 291 | assert.equal(eventTarget.type, "text"); 292 | assert.equal(eventTarget.value, "some keywords"); 293 | 294 | cleanJsdom(); 295 | }); 296 | 297 | it('works with uncontrolled inputs', async () => { 298 | const cleanJsdom = require('jsdom-global')(); 299 | const elem = document.createElement('div'); 300 | const input = ( 301 | 305 | ); 306 | 307 | ReactDOM.render(input, elem); 308 | 309 | const driver = jsdomReactUniDriver(elem); 310 | 311 | await driver.$('input').enterValue('some keywords'); 312 | 313 | const inputValue = await driver.$('input').value(); 314 | assert.equal(inputValue, "some keywords"); 315 | 316 | cleanJsdom(); 317 | }); 318 | }) 319 | 320 | }); 321 | -------------------------------------------------------------------------------- /website/static/img/undraw_shared_goals_3d12.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/static/img/undraw_docusaurus_tree.svg: -------------------------------------------------------------------------------- 1 | docu_tree --------------------------------------------------------------------------------