This commit is contained in:
p01 2021-07-22 17:16:27 +02:00
Родитель 369f2500b5
Коммит ac035d3be8
21 изменённых файлов: 520 добавлений и 450 удалений

12
.github/workflows/pr.yml поставляемый
Просмотреть файл

@ -2,12 +2,12 @@
name: PR build
# Controls when the action will run.
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the main branch
pull_request:
branches: [main]
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
@ -16,7 +16,7 @@ jobs:
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
@ -27,12 +27,12 @@ jobs:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run build

14
.github/workflows/release.yml поставляемый
Просмотреть файл

@ -1,6 +1,6 @@
name: Release
# Controls when the action will run.
# Controls when the action will run.
on:
workflow_dispatch:
@ -10,7 +10,7 @@ jobs:
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
@ -21,19 +21,19 @@ jobs:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run build
- run: npm config set //registry.npmjs.org/:_authToken $NPM_AUTHTOKEN
name: Set registry
env:
NPM_AUTHTOKEN: ${{ secrets.npm_authtoken }}
- run: npm publish

Просмотреть файл

@ -1,7 +1,5 @@
# Project
> This repo has been populated by an initial template to help get you started. Please
> make sure to update the content to build a great experience for community-building.
@ -14,7 +12,7 @@ As the maintainer of this project, please make a few updates:
## Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
@ -28,8 +26,8 @@ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additio
## Trademarks
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
trademarks or logos is subject to and must follow
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
trademarks or logos is subject to and must follow
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
Any use of third-party trademarks or logos are subject to those third-party's policies.

Просмотреть файл

@ -4,7 +4,7 @@
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below.
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](<https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)>), please report it to us as described below.
## Reporting Security Issues
@ -12,19 +12,19 @@ If you believe you have found a security vulnerability in any Microsoft-owned re
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
- Full paths of source file(s) related to the manifestation of the issue
- The location of the affected source code (tag/branch/commit or direct URL)
- Any special configuration required to reproduce the issue
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
@ -38,4 +38,4 @@ We prefer all communications to be in English.
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
<!-- END MICROSOFT SECURITY.MD BLOCK -->
<!-- END MICROSOFT SECURITY.MD BLOCK -->

Просмотреть файл

@ -1,25 +1,25 @@
# TODO: The maintainer of this repo has not yet edited this file
**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project?
- **No CSS support:** Fill out this template with information about how to file issues and get help.
- **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport).
- **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide.
*Then remove this first heading from this SUPPORT.MD file before publishing your repo.*
# Support
## How to file issues and get help
This project uses GitHub Issues to track bugs and feature requests. Please search the existing
issues before filing new issues to avoid duplicates. For new issues, file your bug or
feature request as a new Issue.
For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE
FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER
CHANNEL. WHERE WILL YOU HELP PEOPLE?**.
## Microsoft Support Policy
Support for this **PROJECT or PRODUCT** is limited to the resources listed above.
# TODO: The maintainer of this repo has not yet edited this file
**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project?
- **No CSS support:** Fill out this template with information about how to file issues and get help.
- **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport).
- **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide.
_Then remove this first heading from this SUPPORT.MD file before publishing your repo._
# Support
## How to file issues and get help
This project uses GitHub Issues to track bugs and feature requests. Please search the existing
issues before filing new issues to avoid duplicates. For new issues, file your bug or
feature request as a new Issue.
For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE
FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER
CHANNEL. WHERE WILL YOU HELP PEOPLE?**.
## Microsoft Support Policy
Support for this **PROJECT or PRODUCT** is limited to the resources listed above.

6
package-lock.json сгенерированный
Просмотреть файл

@ -330,6 +330,12 @@
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="
},
"prettier": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
"integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==",
"dev": true
},
"progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",

Просмотреть файл

@ -13,6 +13,9 @@
"build": "tsc",
"start": "tsc -w --preserveWatchOutput"
},
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
},
"types": "lib/index.d.ts",
"dependencies": {
"@types/react": "^17.0.8",
@ -23,6 +26,7 @@
"peerDependencies": {},
"devDependencies": {
"@types/node": "^15.6.1",
"prettier": "^2.3.2",
"typescript": "^4.1.2",
"@types/yargs": "^15.0.13"
},

Просмотреть файл

@ -1,81 +1,90 @@
import TestExecutorActions from './TestExecutorActions';
import { Keys } from './Keys';
import { StepType } from './StepTypes';
import TestExecutorActions from "./TestExecutorActions";
import { Keys } from "./Keys";
import { StepType } from "./StepTypes";
/**
* Functions exposed in browser context called from React component.
*/
export class BrowserExecutor {
public static async executesteps(steps: any[]) {
if (steps === null || steps === undefined || steps.length == 0) {
console.log(`Steps object is ${steps}`);
await TestExecutorActions.makeScreenshot();
await TestExecutorActions.done();
return;
}
for (const step of steps) {
const testName = step.name;
switch (step['type']) {
case StepType.SaveScreenshot: {
await TestExecutorActions.makeScreenshot(testName);
break;
}
case StepType.CropScreenshot: {
await TestExecutorActions.elementScreenshot(step.locator.value, testName);
break;
}
case StepType.WaitForElementPresent: {
await TestExecutorActions.wait(step.locator.value);
break;
}
case StepType.ClickElement: {
await TestExecutorActions.click(step.locator.value);
break;
}
case StepType.WaitForElementNotPresent: {
await TestExecutorActions.waitForNotFound(step.locator.value);
break;
}
case StepType.MoveTo: {
await TestExecutorActions.hover(step.locator.value);
break;
}
case StepType.SetElementText: {
await TestExecutorActions.setElementText(step.locator.value, step.text);
break;
}
case StepType.SendKeys: {
let keyFound = false;
Object.keys(Keys).map((key) => {
if (Keys[key] == step.keys) {
keyFound = true;
}
});
if (step.keys === '') {
await TestExecutorActions.focus(step.locator.value);
} else if (!keyFound) {
await TestExecutorActions.setElementText(step.locator.value, step.keys);
} else {
await TestExecutorActions.pressKey(step.locator.value, step.keys);
}
break;
}
case StepType.ExecuteScript: {
await TestExecutorActions.executeScript(step.code);
break;
}
case StepType.ClickAndHoldElement: {
await TestExecutorActions.mouseDown(step.locator.value);
break;
}
case StepType.ReleaseElement: {
await TestExecutorActions.mouseUp();
break;
}
}
}
// Once all steps are executed close the browser page.
await TestExecutorActions.done();
public static async executesteps(steps: any[]) {
if (steps === null || steps === undefined || steps.length == 0) {
console.log(`Steps object is ${steps}`);
await TestExecutorActions.makeScreenshot();
await TestExecutorActions.done();
return;
}
for (const step of steps) {
const testName = step.name;
switch (step["type"]) {
case StepType.SaveScreenshot: {
await TestExecutorActions.makeScreenshot(testName);
break;
}
case StepType.CropScreenshot: {
await TestExecutorActions.elementScreenshot(
step.locator.value,
testName
);
break;
}
case StepType.WaitForElementPresent: {
await TestExecutorActions.wait(step.locator.value);
break;
}
case StepType.ClickElement: {
await TestExecutorActions.click(step.locator.value);
break;
}
case StepType.WaitForElementNotPresent: {
await TestExecutorActions.waitForNotFound(step.locator.value);
break;
}
case StepType.MoveTo: {
await TestExecutorActions.hover(step.locator.value);
break;
}
case StepType.SetElementText: {
await TestExecutorActions.setElementText(
step.locator.value,
step.text
);
break;
}
case StepType.SendKeys: {
let keyFound = false;
Object.keys(Keys).map((key) => {
if (Keys[key] == step.keys) {
keyFound = true;
}
});
if (step.keys === "") {
await TestExecutorActions.focus(step.locator.value);
} else if (!keyFound) {
await TestExecutorActions.setElementText(
step.locator.value,
step.keys
);
} else {
await TestExecutorActions.pressKey(step.locator.value, step.keys);
}
break;
}
case StepType.ExecuteScript: {
await TestExecutorActions.executeScript(step.code);
break;
}
case StepType.ClickAndHoldElement: {
await TestExecutorActions.mouseDown(step.locator.value);
break;
}
case StepType.ReleaseElement: {
await TestExecutorActions.mouseUp();
break;
}
}
}
// Once all steps are executed close the browser page.
await TestExecutorActions.done();
}
}

Просмотреть файл

@ -1,34 +1,34 @@
export const Keys = {
alt: 'Alt',
control: 'Control',
enter: 'Enter',
escape: 'Escape',
return: 'Enter',
shift: 'Shift',
tab: 'Tab',
leftArrow: 'ArrowLeft',
upArrow: 'ArrowUp',
rightArrow: 'ArrowRight',
downArrow: 'ArrowDown',
backSpace: 'Backspace',
space: 'Space',
pageUp: 'pageUp',
pageDown: 'pageDown',
end: 'End',
home: 'Home',
insert: 'Insert',
delete: 'Delete',
f1: 'F1',
f2: 'F2',
f3: 'F3',
f4: 'F4',
f5: 'F5',
f6: 'F6',
f7: 'F7',
f8: 'F8',
f9: 'F9',
f10: 'F10',
f11: 'F11',
f12: 'F12',
command: 'Meta'
};
alt: "Alt",
control: "Control",
enter: "Enter",
escape: "Escape",
return: "Enter",
shift: "Shift",
tab: "Tab",
leftArrow: "ArrowLeft",
upArrow: "ArrowUp",
rightArrow: "ArrowRight",
downArrow: "ArrowDown",
backSpace: "Backspace",
space: "Space",
pageUp: "pageUp",
pageDown: "pageDown",
end: "End",
home: "Home",
insert: "Insert",
delete: "Delete",
f1: "F1",
f2: "F2",
f3: "F3",
f4: "F4",
f5: "F5",
f6: "F6",
f7: "F7",
f8: "F8",
f9: "F9",
f10: "F10",
f11: "F11",
f12: "F12",
command: "Meta",
};

Просмотреть файл

@ -1,13 +1,13 @@
export enum StepType {
SaveScreenshot = 'saveScreenshot',
CropScreenshot = 'cropScreenshot',
WaitForElementPresent = 'waitForElementPresent',
ClickElement = 'clickElement',
WaitForElementNotPresent = 'waitForElementNotPresent',
MoveTo = 'moveTo',
SetElementText = 'setElementText',
SendKeys = 'sendKeys',
ExecuteScript = 'executeScript',
ClickAndHoldElement = 'clickAndHoldElement',
ReleaseElement = 'releaseElement'
}
SaveScreenshot = "saveScreenshot",
CropScreenshot = "cropScreenshot",
WaitForElementPresent = "waitForElementPresent",
ClickElement = "clickElement",
WaitForElementNotPresent = "waitForElementNotPresent",
MoveTo = "moveTo",
SetElementText = "setElementText",
SendKeys = "sendKeys",
ExecuteScript = "executeScript",
ClickAndHoldElement = "clickAndHoldElement",
ReleaseElement = "releaseElement",
}

Просмотреть файл

@ -1,20 +1,20 @@
type optionsObj = {
[key: string]: any
}
[key: string]: any;
};
export class Steps {
steps = [];
public snapshot(name: string, opts?: optionsObj) {
var step = {
type: 'saveScreenshot',
type: "saveScreenshot",
name: name,
locator: {}
locator: {},
};
if (opts && typeof opts.cropTo === 'string') {
step.type = 'cropScreenshot';
if (opts && typeof opts.cropTo === "string") {
step.type = "cropScreenshot";
step.locator = {
value: opts.cropTo
value: opts.cropTo,
};
}
this.steps.push(step);
@ -23,8 +23,8 @@ export class Steps {
public url(url: string) {
var step = {
type: 'url',
url: url
type: "url",
url: url,
};
this.steps.push(step);
return this;
@ -36,11 +36,11 @@ export class Steps {
public click(selector: string, options?: optionsObj) {
var step = {
type: 'clickElement',
type: "clickElement",
locator: {
value: selector
value: selector,
},
maxTime: ''
maxTime: "",
};
if (options && options.maxTime) {
step.maxTime = options.maxTime;
@ -51,10 +51,10 @@ export class Steps {
public hover(selector: string) {
var step = {
type: 'moveTo',
type: "moveTo",
locator: {
value: selector
}
value: selector,
},
};
this.steps.push(step);
return this;
@ -62,12 +62,12 @@ export class Steps {
public mouseDown(selector: string) {
var step = {
type: 'clickAndHoldElement',
locator: {}
type: "clickAndHoldElement",
locator: {},
};
if (selector) {
step.locator = {
value: selector
value: selector,
};
}
this.steps.push(step);
@ -76,12 +76,12 @@ export class Steps {
public mouseUp(selector: string) {
var step = {
type: 'releaseElement',
locator: {}
type: "releaseElement",
locator: {},
};
if (selector) {
step.locator = {
value: selector
value: selector,
};
}
this.steps.push(step);
@ -90,12 +90,12 @@ export class Steps {
public setValue(selector: string, text: string, options?: optionsObj) {
var step = {
type: 'setElementText',
type: "setElementText",
locator: {
value: selector
value: selector,
},
text: text,
isPassword: false
isPassword: false,
};
if (options && options.isPassword) {
step.isPassword = true;
@ -106,10 +106,10 @@ export class Steps {
public clearValue(selector: string) {
var step = {
type: 'clearElementText',
type: "clearElementText",
locator: {
value: selector
}
value: selector,
},
};
this.steps.push(step);
return this;
@ -117,24 +117,24 @@ export class Steps {
public keys(selector: string, keys: string) {
var step = {
type: 'sendKeys',
type: "sendKeys",
locator: {
value: selector
value: selector,
},
keys: keys
keys: keys,
};
this.steps.push(step);
return this;
}
public focus(selector: string) {
return this.keys(selector, '');
return this.keys(selector, "");
}
public executeScript(code: string) {
var step = {
type: 'executeScript',
code: code
type: "executeScript",
code: code,
};
this.steps.push(step);
return this;
@ -142,17 +142,17 @@ export class Steps {
public wait(msOrSelector, options?: optionsObj) {
var step;
if (typeof msOrSelector === 'number') {
if (typeof msOrSelector === "number") {
step = {
type: 'pause',
waitTime: msOrSelector
type: "pause",
waitTime: msOrSelector,
};
} else {
step = {
type: 'waitForElementPresent',
type: "waitForElementPresent",
locator: {
value: msOrSelector
}
value: msOrSelector,
},
};
if (options && options.maxTime) {
step.maxTime = options.maxTime;
@ -164,11 +164,11 @@ export class Steps {
public waitForNotFound(selector: string, options?: optionsObj) {
var step = {
type: 'waitForElementNotPresent',
type: "waitForElementNotPresent",
locator: {
value: selector
value: selector,
},
maxTime: ''
maxTime: "",
};
if (options && options.maxTime) {
step.maxTime = options.maxTime;
@ -179,8 +179,8 @@ export class Steps {
public cssAnimations(isEnabled: boolean) {
var step = {
type: 'cssAnimations',
isEnabled: isEnabled
type: "cssAnimations",
isEnabled: isEnabled,
};
this.steps.push(step);
return this;
@ -188,25 +188,26 @@ export class Steps {
}
export interface Locator {
type: 'css selector';
type: "css selector";
value: string;
}
export type StepType = 'url' |
'saveScreenshot' |
'cropScreenshot' |
'clickElement' |
'moveTo' |
'clickAndHoldElement' |
'releaseElement' |
'setElementText' |
'sendKeys' |
'executeScript' |
'ignoreElements' |
'pause' |
'waitForElementPresent' |
'waitForElementNotPresent' |
'cssAnimations';
export type StepType =
| "url"
| "saveScreenshot"
| "cropScreenshot"
| "clickElement"
| "moveTo"
| "clickAndHoldElement"
| "releaseElement"
| "setElementText"
| "sendKeys"
| "executeScript"
| "ignoreElements"
| "pause"
| "waitForElementPresent"
| "waitForElementNotPresent"
| "cssAnimations";
export interface Step {
type: StepType;
@ -220,4 +221,4 @@ export interface Step {
isAsync?: boolean;
waitTime?: number;
isEnabled?: boolean;
}
}

Просмотреть файл

@ -1,12 +1,12 @@
import { createElement, Fragment, useEffect } from 'react';
import { BrowserExecutor } from './BrowserExecutor';
import { createElement, Fragment, useEffect } from "react";
import { BrowserExecutor } from "./BrowserExecutor";
/**
* Wrapper react component
* @param p
* @returns
* @param p
* @returns
*/
export const StoryWright = p => {
export const StoryWright = (p) => {
useEffect(() => {
BrowserExecutor.executesteps(p.steps);
}, []);

Просмотреть файл

@ -1,4 +1,4 @@
const TestExecutorWindow = (window as unknown) as {
const TestExecutorWindow = window as unknown as {
makeScreenshot: (testName?: string) => Promise<void>;
done: () => Promise<void>;
hover: (selector: string) => Promise<void>;

Просмотреть файл

@ -1,20 +1,23 @@
import * as playwright from 'playwright';
import { BrowserName } from './Constants';
import * as playwright from "playwright";
import { BrowserName } from "./Constants";
export class BrowserUtils {
/**
* Returns browser instance for given browser name
* @param browserName Name of browser - firefox,chromium
* @param headless Whether to start in headless mode
* @returns Playwright browser instance
*/
public static async getBrowserInstance(browserName: string, headless: boolean) {
switch (browserName) {
case BrowserName.Chromium:
return await playwright.chromium.launch({ headless });
case BrowserName.Firefox:
return await playwright.firefox.launch({ headless });
case BrowserName.Webkit:
return await playwright.webkit.launch({ headless });
}
/**
* Returns browser instance for given browser name
* @param browserName Name of browser - firefox,chromium
* @param headless Whether to start in headless mode
* @returns Playwright browser instance
*/
public static async getBrowserInstance(
browserName: string,
headless: boolean
) {
switch (browserName) {
case BrowserName.Chromium:
return await playwright.chromium.launch({ headless });
case BrowserName.Firefox:
return await playwright.firefox.launch({ headless });
case BrowserName.Webkit:
return await playwright.webkit.launch({ headless });
}
}
}
}

Просмотреть файл

@ -1,5 +1,5 @@
export enum BrowserName {
Chromium = 'chromium',
Firefox = 'firefox',
Webkit = 'webkit'
Chromium = "chromium",
Firefox = "firefox",
Webkit = "webkit",
}

Просмотреть файл

@ -1,44 +1,51 @@
import * as fs from 'fs';
import { Page } from 'playwright';
import {sep} from 'path';
import * as fs from "fs";
import { Page } from "playwright";
import { sep } from "path";
/**
* Class containing playwright exposed functions.
*/
export class PlayWrightExecutor {
private fileSuffix: number = 0;
constructor(private page: Page, private path: String, private ssNamePrefix: String, private browserName: string) { }
constructor(
private page: Page,
private path: String,
private ssNamePrefix: String,
private browserName: string
) {}
public async exposeFunctions() {
await this.page.exposeFunction('makeScreenshot', this.makeScreenshot);
await this.page.exposeFunction('click', this.click);
await this.page.exposeFunction('hover', this.hover);
await this.page.exposeFunction('wait', this.waitForSelector);
await this.page.exposeFunction('waitForNotFound', this.waitForNotFound);
await this.page.exposeFunction('elementScreenshot', this.elementScreenshot);
await this.page.exposeFunction('done', this.done);
await this.page.exposeFunction('setElementText', this.setElementText);
await this.page.exposeFunction('pressKey', this.pressKey);
await this.page.exposeFunction('executeScript', this.executeScript);
await this.page.exposeFunction('focus', this.focus);
await this.page.exposeFunction('mouseDown', this.mouseDown);
await this.page.exposeFunction('mouseUp', this.mouseUp);
await this.page.exposeFunction("makeScreenshot", this.makeScreenshot);
await this.page.exposeFunction("click", this.click);
await this.page.exposeFunction("hover", this.hover);
await this.page.exposeFunction("wait", this.waitForSelector);
await this.page.exposeFunction("waitForNotFound", this.waitForNotFound);
await this.page.exposeFunction("elementScreenshot", this.elementScreenshot);
await this.page.exposeFunction("done", this.done);
await this.page.exposeFunction("setElementText", this.setElementText);
await this.page.exposeFunction("pressKey", this.pressKey);
await this.page.exposeFunction("executeScript", this.executeScript);
await this.page.exposeFunction("focus", this.focus);
await this.page.exposeFunction("mouseDown", this.mouseDown);
await this.page.exposeFunction("mouseUp", this.mouseUp);
}
private mouseUp = async () => {
try {
await this.page.mouse.up();
} catch (err) {
console.error('ERROR: mouseUp: ', err.message);
console.error("ERROR: mouseUp: ", err.message);
throw err;
}
}
};
private mouseDown = async (selector: string) => {
try {
let element;
if (selector.charAt(0) === '#') {
element = await this.page.$(`id=${selector.substring(1, selector.length)}`)
if (selector.charAt(0) === "#") {
element = await this.page.$(
`id=${selector.substring(1, selector.length)}`
);
} else {
element = await this.page.$(selector);
}
@ -46,73 +53,73 @@ export class PlayWrightExecutor {
await this.page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await this.page.mouse.down();
} catch (err) {
console.error('ERROR: mouseDown: ', err.message);
console.error("ERROR: mouseDown: ", err.message);
throw err;
}
}
};
private focus = async (selector: string) => {
try {
await this.page.focus(selector);
} catch (err) {
console.error('ERROR: focus: ', err.message);
console.error("ERROR: focus: ", err.message);
throw err;
}
}
};
private executeScript = async (script: string) => {
try {
await this.page.evaluate(script);
} catch (err) {
console.error('ERROR: executeScript: ', err.message);
console.error("ERROR: executeScript: ", err.message);
throw err;
}
}
};
private pressKey = async (selector: string, key: string) => {
try {
await this.page.press(selector, key);
} catch (err) {
console.error('ERROR: pressKey: ', err.message);
console.error("ERROR: pressKey: ", err.message);
throw err;
}
}
};
private setElementText = async (selector: string, text: string) => {
try {
const element = await this.page.$(selector);
await element.fill(text);
} catch (err) {
console.error('ERROR: setElementText: ', err.message);
console.error("ERROR: setElementText: ", err.message);
throw err;
}
}
};
private click = async (selector: string) => {
try {
const element = await this.page.$(selector);
await element.click({
force: true
force: true,
});
console.log('element clicked');
console.log("element clicked");
} catch (err) {
console.error('ERROR: click: ', err.message);
console.error("ERROR: click: ", err.message);
throw err;
}
}
};
private makeScreenshot = async (testName?: string) => {
try {
let screenshotPath = this.getScreenshotPath(testName);
await this.page.screenshot({
path: screenshotPath
path: screenshotPath,
});
} catch (err) {
console.error('ERROR: PAGE_SCREENSHOT: ', err.message);
console.error("ERROR: PAGE_SCREENSHOT: ", err.message);
throw err;
}
}
};
private elementScreenshot = async (selector: string, testName: string) => {
try {
@ -121,80 +128,77 @@ export class PlayWrightExecutor {
let screenshotPath = this.getScreenshotPath(testName);
await element.screenshot({
path: screenshotPath
path: screenshotPath,
});
} else {
console.log('ERROR: Element NOT VISIBLE: CAPTURING PAGE');
console.log("ERROR: Element NOT VISIBLE: CAPTURING PAGE");
await this.makeScreenshot(testName);
}
} catch (err) {
console.error('ERROR: ELEMENT_SCREENSHOT: ', err.message);
console.info('Trying full page screenshot');
console.error("ERROR: ELEMENT_SCREENSHOT: ", err.message);
console.info("Trying full page screenshot");
await this.makeScreenshot(testName);
}
}
};
private hover = async (selector: string) => {
try {
const element = await this.page.$(selector);
await element.hover({
force: true
force: true,
});
} catch (err) {
console.error('ERROR: HOVER: ', err.message);
console.error("ERROR: HOVER: ", err.message);
throw err;
}
}
};
private waitForSelector = async (selector: string) => {
try {
await this.page.waitForSelector(selector);
} catch (err) {
console.error('ERROR: waitForSelector: ', err.message);
console.error("ERROR: waitForSelector: ", err.message);
throw err;
}
}
};
private waitForNotFound = async (selector: string) => {
try {
await this.page.waitForSelector(selector, { state: 'detached' });
await this.page.waitForSelector(selector, { state: "detached" });
} catch (err) {
console.error('ERROR: waitForNotFound: ', err.message);
console.error("ERROR: waitForNotFound: ", err.message);
throw err;
}
}
};
private done = async () => {
try {
await this.page.close();
} catch (err) {
console.error('ERROR: completed steps: ', err.message);
console.error("ERROR: completed steps: ", err.message);
throw err;
}
}
};
private getScreenshotPath(testName?: String) {
this.ssNamePrefix = this.ssNamePrefix.replace(/:/g, '-');
this.ssNamePrefix = this.ssNamePrefix.replace(/:/g, "-");
let screenshotPath: string;
if(testName){
testName = testName.replace(/:/g, '-');
if (testName) {
testName = testName.replace(/:/g, "-");
screenshotPath = `${this.path}${sep}${this.ssNamePrefix}.${testName}.${this.browserName}`;
}else{
} else {
screenshotPath = `${this.path}${sep}${this.ssNamePrefix}.${this.browserName}`;
}
//INFO: Append file prefix if screenshot with same name exist.
if (fs.existsSync(screenshotPath + '.png')) {
screenshotPath = screenshotPath + '_' + (++this.fileSuffix);
if (fs.existsSync(screenshotPath + ".png")) {
screenshotPath = screenshotPath + "_" + ++this.fileSuffix;
}
screenshotPath += '.png';
screenshotPath += ".png";
console.debug(`ScreenshotPath ${screenshotPath}`);
return screenshotPath;
}
}
}

Просмотреть файл

@ -10,4 +10,4 @@ export interface StoryWrightOptions {
skipSteps: boolean;
partitionIndex: number;
totalPartitions: number;
}
}

Просмотреть файл

@ -1,44 +1,56 @@
import { join } from 'path';
import { Browser, Page } from 'playwright';
import { BrowserUtils } from './BrowserUtils';
import { PlayWrightExecutor } from './PlayWrightExecutor';
import { StoryWrightOptions } from './StoryWrightOptions'
import { partitionArray } from '../utils';
import {sep} from 'path';
import { join } from "path";
import { Browser, Page } from "playwright";
import { BrowserUtils } from "./BrowserUtils";
import { PlayWrightExecutor } from "./PlayWrightExecutor";
import { StoryWrightOptions } from "./StoryWrightOptions";
import { partitionArray } from "../utils";
import { sep } from "path";
/**
* Class containing StoryWright operations
*/
export class StoryWrightProcessor {
/**
*
*
* @param options Storywright processing options
*/
public static async process(options: StoryWrightOptions) {
let startTime = Date.now();
console.log('StoryWright processor started @ ', new Date());
console.log("StoryWright processor started @ ", new Date());
const browsers: string[] = options.browsers;
for (const browserName of browsers) {
let browser: Browser;
try {
console.log(`Starting browser test for ${browserName}`);
browser = await BrowserUtils.getBrowserInstance(browserName, options.headless);
browser = await BrowserUtils.getBrowserInstance(
browserName,
options.headless
);
const context = await browser.newContext();
const page: Page = await context.newPage();
await page.goto(join(options.url, 'iframe.html'));
await page.goto(join(options.url, "iframe.html"));
let stories: object[] = await page.evaluate(
'(__STORYBOOK_CLIENT_API__?.raw() || []).map(e => ({id: e.id, kind: e.kind, name: e.name}))'
"(__STORYBOOK_CLIENT_API__?.raw() || []).map(e => ({id: e.id, kind: e.kind, name: e.name}))"
);
if (options.totalPartitions > 1) {
console.log(
"Starting partitioning with ",
"Total partitions -", options.totalPartitions, "&",
"Partition index to select -", options.partitionIndex, "&",
"Total Stories length -", stories.length
"Total partitions -",
options.totalPartitions,
"&",
"Partition index to select -",
options.partitionIndex,
"&",
"Total Stories length -",
stories.length
);
stories = partitionArray(
stories,
options.partitionIndex,
options.totalPartitions
);
stories = partitionArray(stories, options.partitionIndex, options.totalPartitions);
}
await page.close();
console.log(`${stories.length} stories found`);
@ -46,12 +58,15 @@ export class StoryWrightProcessor {
let position = 0;
while (position < stories.length) {
// Execute stories in batches concurrently in tabs.
const itemsForBatch = stories.slice(position, position + options.concurrency);
const itemsForBatch = stories.slice(
position,
position + options.concurrency
);
await Promise.all(
itemsForBatch.map(async (story: object) => {
const id: string = story['id'];
const id: string = story["id"];
// Set story category and name as prefix for screenshot name.
const ssNamePrefix = `${story['kind']}.${story['name']}`;
const ssNamePrefix = `${story["kind"]}.${story["name"]}`;
let page: Page;
try {
page = await context.newPage();
@ -63,46 +78,56 @@ export class StoryWrightProcessor {
});
//TODO: Take screenshots when user doesn't want steps to be executed.
if(options.skipSteps){
if (options.skipSteps) {
await page.goto(join(options.url, `iframe.html?id=${id}`));
console.log(`story:${++storyIndex}/${stories.length} ${id}`);
await page.screenshot({path: options.screenShotDestPath + sep + ssNamePrefix + '.png'});
}else{
await new PlayWrightExecutor(page, options.screenShotDestPath, ssNamePrefix, browserName).exposeFunctions();
await page.screenshot({
path:
options.screenShotDestPath + sep + ssNamePrefix + ".png",
});
} else {
await new PlayWrightExecutor(
page,
options.screenShotDestPath,
ssNamePrefix,
browserName
).exposeFunctions();
await page.goto(join(options.url, `iframe.html?id=${id}`));
console.log(`story:${++storyIndex}/${stories.length} ${id}`);
// Wait for close event to be fired from steps. Default timeout is 30 seconds.
await page.waitForEvent('close');
await page.waitForEvent("close");
}
} catch (err) {
console.log(`**ERROR** for story ${ssNamePrefix} ${story['id']} ${storyIndex}/${stories.length} ${err}`);
}
finally {
console.log(
`**ERROR** for story ${ssNamePrefix} ${story["id"]} ${storyIndex}/${stories.length} ${err}`
);
} finally {
if (page != null && !page.isClosed()) {
await page.close();
}
}
})
).catch(reason => {
).catch((reason) => {
console.log(`**ERROR** ${reason}`);
});
position += options.concurrency;
}
}
catch (err) {
} catch (err) {
console.log(`** ERROR ** ${err}`);
}
finally {
console.log('Closing process !!');
} finally {
console.log("Closing process !!");
if (browser != null && browser.isConnected()) {
await browser.close();
}
let endTime = Date.now();
console.log('StoryWright took ', Math.round((endTime-startTime)/1000), 'secs to complete.');
console.log('StoryWright processor completed @ ', new Date());
console.log(
"StoryWright took ",
Math.round((endTime - startTime) / 1000),
"secs to complete."
);
console.log("StoryWright processor completed @ ", new Date());
}
}
}
}
}

Просмотреть файл

@ -1,3 +1,3 @@
export * from './StoryWright/Steps';
export * from './StoryWright/StoryWright';
export * from './StoryWright/Keys';
export * from "./StoryWright/Steps";
export * from "./StoryWright/StoryWright";
export * from "./StoryWright/Keys";

Просмотреть файл

@ -1,73 +1,88 @@
#!/usr/bin/env node
import * as argv from 'yargs';
import { BrowserName } from './StoryWrightProcessor/Constants';
import { StoryWrightOptions } from './StoryWrightProcessor/StoryWrightOptions';
import { StoryWrightProcessor } from './StoryWrightProcessor/StoryWrightProcessor';
import { resolve } from 'path';
const args = argv.usage('Usage: $0 [options]').help('h').alias('h', 'help')
.option('url', {
alias: 'storybookurl',
default: 'dist',
describe: 'Url to storybook. Can be relative path to folder like dist or server url http://localhost:5555',
nargs: 1,
type: 'string'
})
.option('destpath', {
alias: 'screenshotdestpath',
default: 'dist/screenshots/storybook',
describe: 'Output directory path where screenshots should be stored',
nargs: 1,
type: 'string'
})
.option('browsers', {
alias: 'browsers',
default: [BrowserName.Chromium, BrowserName.Firefox],
describe: 'Comma seperated list of browsers to support',
nargs: 1,
type: 'array',
coerce: array => {
return array.flatMap(v => v.split(','))
},
choices: [BrowserName.Chromium, BrowserName.Firefox, BrowserName.Webkit]
})
.option('headless', {
alias: 'headless',
default: false,
describe: 'True if browser needs to be launched in headless mode else false',
nargs: 1,
type: 'boolean'
})
.option('partitionIndex', {
default: 1,
describe: "Partition index (1 to totalPartitions) to run, used in conjunction with totalPartitions parameter to run specific partition of the stories. This can be used to distribute workloads across different machines, threads or workers.",
type: "number"
})
.option('totalPartitions', {
default: 1,
describe: "Number of partitions, used in conjunction with partitionIndex parameter to run specific partition of the stories. This can be used to distribute workloads across different machines, threads or workers.",
type: "number"
})
.option('concurrency', {
alias: 'concurrency',
default: 8,
describe: 'Number of browser tabs to open in parallel',
nargs: 1,
type: 'number'
})
.option('skipSteps', {
alias: 'skipSteps',
default: false,
describe: 'Take Screenshot of all Storybook stories with/without wrapped component',
nargs: 1,
type: 'boolean'
})
.example('$0', 'Captures screenshot for all stories using default static storybook path dist/iframe.html')
.example('$0 -url https://localhost:5555 --browsers chromium', 'Captures screenshot for all stories from given storybook url for chromium browser').argv;
import * as argv from "yargs";
import { BrowserName } from "./StoryWrightProcessor/Constants";
import { StoryWrightOptions } from "./StoryWrightProcessor/StoryWrightOptions";
import { StoryWrightProcessor } from "./StoryWrightProcessor/StoryWrightProcessor";
import { resolve } from "path";
const args = argv
.usage("Usage: $0 [options]")
.help("h")
.alias("h", "help")
.option("url", {
alias: "storybookurl",
default: "dist",
describe:
"Url to storybook. Can be relative path to folder like dist or server url http://localhost:5555",
nargs: 1,
type: "string",
})
.option("destpath", {
alias: "screenshotdestpath",
default: "dist/screenshots/storybook",
describe: "Output directory path where screenshots should be stored",
nargs: 1,
type: "string",
})
.option("browsers", {
alias: "browsers",
default: [BrowserName.Chromium, BrowserName.Firefox],
describe: "Comma seperated list of browsers to support",
nargs: 1,
type: "array",
coerce: (array) => {
return array.flatMap((v) => v.split(","));
},
choices: [BrowserName.Chromium, BrowserName.Firefox, BrowserName.Webkit],
})
.option("headless", {
alias: "headless",
default: false,
describe:
"True if browser needs to be launched in headless mode else false",
nargs: 1,
type: "boolean",
})
.option("partitionIndex", {
default: 1,
describe:
"Partition index (1 to totalPartitions) to run, used in conjunction with totalPartitions parameter to run specific partition of the stories. This can be used to distribute workloads across different machines, threads or workers.",
type: "number",
})
.option("totalPartitions", {
default: 1,
describe:
"Number of partitions, used in conjunction with partitionIndex parameter to run specific partition of the stories. This can be used to distribute workloads across different machines, threads or workers.",
type: "number",
})
.option("concurrency", {
alias: "concurrency",
default: 8,
describe: "Number of browser tabs to open in parallel",
nargs: 1,
type: "number",
})
.option("skipSteps", {
alias: "skipSteps",
default: false,
describe:
"Take Screenshot of all Storybook stories with/without wrapped component",
nargs: 1,
type: "boolean",
})
.example(
"$0",
"Captures screenshot for all stories using default static storybook path dist/iframe.html"
)
.example(
"$0 -url https://localhost:5555 --browsers chromium",
"Captures screenshot for all stories from given storybook url for chromium browser"
).argv;
// When http(s) storybook url is passed no modification required.
// When http(s) storybook url is passed no modification required.
// When file path is provided it needs to be converted to absolute path and file:/// needs to be added to support firefox browser.
const url: string = (args.url.indexOf('http') > -1) ? args.url : 'file:///' + resolve(args.url);
const url: string =
args.url.indexOf("http") > -1 ? args.url : "file:///" + resolve(args.url);
console.log(`================ StoryWright params =================`);
console.log(`Storybook url = ${url}`);
@ -76,17 +91,19 @@ console.log(`Browsers = ${args.browsers}`);
console.log(`Headless = ${args.headless}`);
console.log(`Concurrency = ${args.concurrency}`);
console.log(`SkipSteps = ${args.skipSteps}`);
console.log(`================ Starting story right execution =================`);
console.log(
`================ Starting story right execution =================`
);
const storyWrightOptions: StoryWrightOptions = {
url: url,
screenShotDestPath: args.destpath,
browsers: args.browsers,
headless: args.headless,
concurrency: args.concurrency,
skipSteps: args.skipSteps,
partitionIndex: args.partitionIndex,
totalPartitions: args.totalPartitions
url: url,
screenShotDestPath: args.destpath,
browsers: args.browsers,
headless: args.headless,
concurrency: args.concurrency,
skipSteps: args.skipSteps,
partitionIndex: args.partitionIndex,
totalPartitions: args.totalPartitions,
};
StoryWrightProcessor.process(storyWrightOptions);
StoryWrightProcessor.process(storyWrightOptions);

Просмотреть файл

@ -1,7 +1,10 @@
export const partitionArray = <T>(array: Array<T>, partitionIndex: number, totalPartitions: number): Array<T> => {
const totalElements = array.length;
const elementsInEachPartition = Math.ceil(totalElements/totalPartitions);
const startingIndex = (partitionIndex - 1) * elementsInEachPartition;
return array.slice(startingIndex, startingIndex + elementsInEachPartition);
};
export const partitionArray = <T>(
array: Array<T>,
partitionIndex: number,
totalPartitions: number
): Array<T> => {
const totalElements = array.length;
const elementsInEachPartition = Math.ceil(totalElements / totalPartitions);
const startingIndex = (partitionIndex - 1) * elementsInEachPartition;
return array.slice(startingIndex, startingIndex + elementsInEachPartition);
};