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

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

@ -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.

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

@ -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
@ -18,13 +18,13 @@ You should receive a response within 24 hours. If for some reason you do not, pl
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.

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

@ -6,7 +6,7 @@
- **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.*
_Then remove this first heading from this SUPPORT.MD file before publishing your repo._
# Support

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,6 +1,6 @@
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.
@ -15,13 +15,16 @@ export class BrowserExecutor {
}
for (const step of steps) {
const testName = step.name;
switch (step['type']) {
switch (step["type"]) {
case StepType.SaveScreenshot: {
await TestExecutorActions.makeScreenshot(testName);
break;
}
case StepType.CropScreenshot: {
await TestExecutorActions.elementScreenshot(step.locator.value, testName);
await TestExecutorActions.elementScreenshot(
step.locator.value,
testName
);
break;
}
case StepType.WaitForElementPresent: {
@ -41,7 +44,10 @@ export class BrowserExecutor {
break;
}
case StepType.SetElementText: {
await TestExecutorActions.setElementText(step.locator.value, step.text);
await TestExecutorActions.setElementText(
step.locator.value,
step.text
);
break;
}
case StepType.SendKeys: {
@ -52,10 +58,13 @@ export class BrowserExecutor {
keyFound = true;
}
});
if (step.keys === '') {
if (step.keys === "") {
await TestExecutorActions.focus(step.locator.value);
} else if (!keyFound) {
await TestExecutorActions.setElementText(step.locator.value, step.keys);
await TestExecutorActions.setElementText(
step.locator.value,
step.keys
);
} else {
await TestExecutorActions.pressKey(step.locator.value, step.keys);
}

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

@ -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;

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

@ -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
*/
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,5 +1,5 @@
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
@ -7,7 +7,10 @@ export class BrowserUtils {
* @param headless Whether to start in headless mode
* @returns Playwright browser instance
*/
public static async getBrowserInstance(browserName: string, headless: boolean) {
public static async getBrowserInstance(
browserName: string,
headless: boolean
) {
switch (browserName) {
case BrowserName.Chromium:
return await playwright.chromium.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,78 +128,75 @@ 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;

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

@ -1,10 +1,10 @@
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
@ -16,29 +16,41 @@ export class StoryWrightProcessor {
*/
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,45 +78,55 @@ 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',
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'
type: "string",
})
.option('destpath', {
alias: 'screenshotdestpath',
default: 'dist/screenshots/storybook',
describe: 'Output directory path where screenshots should be stored',
.option("destpath", {
alias: "screenshotdestpath",
default: "dist/screenshots/storybook",
describe: "Output directory path where screenshots should be stored",
nargs: 1,
type: 'string'
type: "string",
})
.option('browsers', {
alias: 'browsers',
.option("browsers", {
alias: "browsers",
default: [BrowserName.Chromium, BrowserName.Firefox],
describe: 'Comma seperated list of browsers to support',
describe: "Comma seperated list of browsers to support",
nargs: 1,
type: 'array',
coerce: array => {
return array.flatMap(v => v.split(','))
type: "array",
coerce: (array) => {
return array.flatMap((v) => v.split(","));
},
choices: [BrowserName.Chromium, BrowserName.Firefox, BrowserName.Webkit]
choices: [BrowserName.Chromium, BrowserName.Firefox, BrowserName.Webkit],
})
.option('headless', {
alias: 'headless',
.option("headless", {
alias: "headless",
default: false,
describe: 'True if browser needs to be launched in headless mode else false',
describe:
"True if browser needs to be launched in headless mode else false",
nargs: 1,
type: 'boolean'
type: "boolean",
})
.option('partitionIndex', {
.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"
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', {
.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"
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',
.option("concurrency", {
alias: "concurrency",
default: 8,
describe: 'Number of browser tabs to open in parallel',
describe: "Number of browser tabs to open in parallel",
nargs: 1,
type: 'number'
type: "number",
})
.option('skipSteps', {
alias: 'skipSteps',
.option("skipSteps", {
alias: "skipSteps",
default: false,
describe: 'Take Screenshot of all Storybook stories with/without wrapped component',
describe:
"Take Screenshot of all Storybook stories with/without wrapped component",
nargs: 1,
type: 'boolean'
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;
.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 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,7 +91,9 @@ 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,
@ -86,7 +103,7 @@ const storyWrightOptions: StoryWrightOptions = {
concurrency: args.concurrency,
skipSteps: args.skipSteps,
partitionIndex: args.partitionIndex,
totalPartitions: args.totalPartitions
totalPartitions: args.totalPartitions,
};
StoryWrightProcessor.process(storyWrightOptions);

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

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