├── 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 | Toggle
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 | X
29 |
);
30 | };
31 |
32 | render () {
33 | return (
34 |
35 | {this.state.counters.map(this.renderSingle)}
36 | Add
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 |
28 | Mouse Events
29 |
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 |
49 |
50 | {#each items as item, index}
51 |
53 | {/each}
54 |
55 |
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} x
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 |
54 | Mouse Events
55 |
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 |
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 |
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 | Toggle
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 |
91 |
92 | {itemsComp}
93 |
94 |
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: 
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 | 
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 | [](https://www.npmjs.com/package/unidriver)
7 | [](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 = bob ;
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 = bob ;
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 |
78 | bob
79 |
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 |
101 | bob
102 |
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 |
125 | bob
126 |
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 |
190 | button A
191 |
192 |
193 | button B
194 |
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 |
221 | button A
222 |
223 |
224 | button B
225 |
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 |
250 | button A
251 |
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
--------------------------------------------------------------------------------