e2e tests

This commit is contained in:
Dylan Vorster
2019-07-25 00:32:56 +02:00
parent efc53eec9b
commit ea0160cbcf
47 changed files with 215 additions and 291 deletions

View File

@ -1,20 +0,0 @@
const path = require("path");
// jest.config.js
module.exports = {
"preset": "jest-puppeteer",
transform: {
".*test_loader.*": path.join(__dirname, "tests", "helpers", "storybook-loader.js" ),
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.jsx?$': 'babel-jest'
},
moduleNameMapper: {
"\\.(scss|css|png)$": path.join(__dirname, "tests", "helpers", "css-mock.js"),
"storm-react-diagrams": path.join(__dirname, "src", "main")
},
roots:[
__dirname+'/tests'
],
testMatch: [
"**/*\.test\.ts"
]
};

View File

@ -1,6 +1,8 @@
import * as React from 'react';
import * as _ from "lodash";
import { NodeModel } from '../models/NodeModel';
import { BaseWidget, BaseWidgetProps } from './BaseWidget';
import {Toolkit} from "../Toolkit";
export interface PortProps extends BaseWidgetProps {
name: string;
@ -23,6 +25,16 @@ export class PortWidget extends BaseWidget<PortProps, PortState> {
return 'port ' + super.getClassName() + (this.state.selected ? this.bem('--selected') : '');
}
getExtraProps(){
if(Toolkit.TESTING){
const links = _.keys(this.props.node.getPort(this.props.name).links).join(',');
return {
'data-links': links
}
}
return {};
}
render() {
return (
<div
@ -35,6 +47,7 @@ export class PortWidget extends BaseWidget<PortProps, PortState> {
}}
data-name={this.props.name}
data-nodeid={this.props.node.getID()}
{...this.getExtraProps()}
/>
);
}

View File

@ -1,9 +1,9 @@
import * as React from 'react';
import { storiesOf, addParameters } from '@storybook/react';
import { setOptions } from '@storybook/addon-options';
import { Toolkit } from '@projectstorm/react-diagrams-core';
import { Toolkit } from '@projectstorm/react-diagrams';
import { themes } from '@storybook/theming';
import './src/helpers/demo.scss';
import './demos/helpers/demo.scss';
Toolkit.TESTING = true;
@ -19,15 +19,15 @@ setOptions({
addonPanelInRight: true
});
import demo_simple from './src/demo-simple';
import demo_flow from './src/demo-simple-flow';
import demo_performance from './src/demo-performance';
import demo_locks from './src/demo-locks';
import demo_grid from './src/demo-grid';
import demo_limit_points from './src/demo-limit-points';
import demo_listeners from './src/demo-listeners';
import demo_zoom from './src/demo-zoom-to-fit';
import demo_labels from './src/demo-labelled-links';
import demo_simple from './demos/demo-simple';
import demo_flow from './demos/demo-simple-flow';
import demo_performance from './demos/demo-performance';
import demo_locks from './demos/demo-locks';
import demo_grid from './demos/demo-grid';
import demo_limit_points from './demos/demo-limit-points';
import demo_listeners from './demos/demo-listeners';
import demo_zoom from './demos/demo-zoom-to-fit';
import demo_labels from './demos/demo-labelled-links';
storiesOf('Simple Usage', module)
.add('Simple example', demo_simple)
@ -40,11 +40,11 @@ storiesOf('Simple Usage', module)
.add('Zoom to fit', demo_zoom)
.add('Links with labels', demo_labels);
import demo_adv_clone_selected from './src/demo-cloning';
import demo_adv_ser_des from './src/demo-serializing';
import demo_adv_prog from './src/demo-mutate-graph';
import demo_adv_dnd from './src/demo-drag-and-drop';
import demo_smart_routing from './src/demo-smart-routing';
import demo_adv_clone_selected from './demos/demo-cloning';
import demo_adv_ser_des from './demos/demo-serializing';
import demo_adv_prog from './demos/demo-mutate-graph';
import demo_adv_dnd from './demos/demo-drag-and-drop';
import demo_smart_routing from './demos/demo-smart-routing';
storiesOf('Advanced Techniques', module)
.add('Clone Selected', demo_adv_clone_selected)
@ -53,14 +53,14 @@ storiesOf('Advanced Techniques', module)
.add('Drag and drop', demo_adv_dnd)
.add('Smart routing', demo_smart_routing);
import demo_cust_nodes from './src/demo-custom-node1';
import demo_cust_links from './src/demo-custom-link1';
import demo_cust_nodes from './demos/demo-custom-node1';
import demo_cust_links from './demos/demo-custom-link1';
storiesOf('Custom Models', module)
.add('Custom diamond node', demo_cust_nodes)
.add('Custom animated links', demo_cust_links);
import demo_3rd_dagre from './src/demo-dagre';
import demo_3rd_dagre from './demos/demo-dagre';
storiesOf('3rd party libraries', module).add('Auto Distribute (Dagre)', demo_3rd_dagre);

View File

@ -0,0 +1,18 @@
const path = require("path");
module.exports = {
"preset": "jest-puppeteer",
transform: {
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.jsx?$': 'babel-jest'
},
moduleNameMapper: {
"\\.(scss|css|png)$": path.join(__dirname, "tests-e2e", "helpers", "css-mock.js"),
},
roots:[
path.join(__dirname, 'tests-e2e'),
path.join(__dirname, 'tests-snapshots')
],
testMatch: [
"**/*\.test\.ts"
]
};

View File

@ -7,7 +7,11 @@
"url": "https://github.com/projectstorm/react-diagrams.git"
},
"scripts": {
"start": "../node_modules/.bin/start-storybook"
"start": "../node_modules/.bin/start-storybook",
"build": "../node_modules/.bin/build-storybook -c .storybook -o .out",
"github": "../node_modules/.bin/storybook-to-ghpages",
"test:ci": "rm -rf ./dist && node ./tests-e2e/e2e/generate-e2e.js && jest --runInBand --no-cache",
"test": "../node_modules/.bin/jest --no-cache"
},
"keywords": [
"web",

View File

@ -1,139 +0,0 @@
import { ElementHandle, Page } from 'puppeteer';
import * as _ from 'lodash';
export class E2EElement {
helper: E2EHelper;
page: Page;
element: ElementHandle;
id: string;
constructor(helper: E2EHelper, page: Page, element: ElementHandle, id: string) {
this.page = page;
this.element = element;
this.id = id;
this.helper = helper;
}
}
export class E2ENode extends E2EElement {
async port(id: string): Promise<E2EPort> {
return new E2EPort(this.helper, this.page, await this.element.$(`div[data-name="${id}"]`), id, this);
}
async model(): Promise<any> {
return await this.page.evaluate(id => {
return window['diagram_instance']
.getDiagramModel()
.getNode(id)
.serialize();
}, this.id);
}
}
export class E2EPort extends E2EElement {
parent: E2ENode;
constructor(helper: E2EHelper, page: Page, element: ElementHandle, id: string, parent: E2ENode) {
super(helper, page, element, id);
this.parent = parent;
}
async link(port: E2EPort): Promise<E2ELink> {
let currentLinks = _.flatMap((await this.parent.model()).ports, 'links');
let bounds = await this.element.boundingBox();
// click on this port
this.page.mouse.move(bounds.x, bounds.y);
this.page.mouse.down();
//
let bounds2 = await port.element.boundingBox();
// drag to other port
this.page.mouse.move(bounds2.x, bounds2.y);
this.page.mouse.up();
// get the parent to get the link
return await this.helper.link(_.difference(_.flatMap((await this.parent.model()).ports, 'links'), currentLinks)[0]);
}
async linkToPoint(x: number, y: number): Promise<E2ELink> {
let currentLinks = _.flatMap((await this.parent.model()).ports, 'links');
let bounds = await this.element.boundingBox();
// click on this port
this.page.mouse.move(bounds.x, bounds.y);
this.page.mouse.down();
// drag to point
this.page.mouse.move(x, y);
this.page.mouse.up();
const link = _.difference(_.flatMap((await this.parent.model()).ports, 'links'), currentLinks)[0];
if (!link) {
return null;
}
// get the parent to get the link
return await this.helper.link(link);
}
}
export class E2ELink extends E2EElement {
async model(): Promise<any> {
return await this.page.evaluate(id => {
return window['diagram_instance']
.getDiagramModel()
.getLink(id)
.serialize();
}, this.id);
}
async exists(): Promise<boolean> {
return await this.page.evaluate(id => {
return !!document.querySelector(`path[data-linkid="${id}"]`);
}, this.id);
}
async select(): Promise<any> {
const point = await this.page.evaluate(id => {
const path = document.querySelector(`path[data-linkid="${id}"]`) as SVGPathElement;
const point = path.getPointAtLength(path.getTotalLength() / 2);
return {
x: point.x,
y: point.y
};
}, this.id);
await this.page.keyboard.down('Shift');
await this.page.mouse.move(point.x, point.y);
await this.page.mouse.down();
await this.page.keyboard.up('Shift');
}
}
export class E2EHelper {
page: Page;
constructor(page: Page) {
this.page = page;
}
async link(id): Promise<E2ELink> {
if (!id) {
throw 'Link ID must be valid';
}
let selector = await this.page.waitForSelector(`path[data-linkid="${id}"]`);
return new E2ELink(this, this.page, selector, id);
}
async node(id): Promise<E2ENode> {
if (!id) {
if (!id) {
throw 'Node ID must be valid';
}
}
let selector = await this.page.waitForSelector(`div[data-nodeid="${id}"]`);
return new E2ENode(this, this.page, selector, id);
}
}

View File

@ -1,21 +0,0 @@
function answer(options, a, b) {
return {
code:
`
var demo = require("` +
options.entry +
`");
var ReactDOM = require("react-dom");
var srd = require("../../src/main.ts");
srd.Toolkit.TESTING = true;
var styles = require("` +
(__dirname + '/../../demos/.helpers/demo.scss') +
`");
window.onload = function(){
ReactDOM.render(demo.default(),document.querySelector("#application"));
};
`
};
}
module.exports = answer;

View File

@ -1,45 +0,0 @@
let glob = require('glob');
let webpack = require('webpack');
let path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
glob.glob(__dirname + '/../../demos/demo-*/index.tsx', {}, (err, files) => {
let config = require('../../webpack.config');
let entry = {};
let copy = [];
files.forEach(entryFile => {
entry[path.basename(path.dirname(entryFile))] = 'val-loader?entry=' + entryFile + '!' + __dirname + '/entry.js';
copy.push({ to: path.basename(path.dirname(entryFile)), from: __dirname + '/index.html' });
});
config = {
entry: entry,
plugins: [new CopyWebpackPlugin(copy)],
output: {
filename: '[name]/main.js',
path: __dirname + '/../../dist/e2e'
},
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
].concat(config.module.rules)
},
resolve: {
...config.resolve,
alias: {
'storm-react-diagrams': path.join(__dirname, '..', '..', 'src', 'main')
}
}
};
webpack(config, (err, stats) => {
if (err || stats.hasErrors()) {
// Handle errors here
return;
}
});
});

View File

@ -0,0 +1,137 @@
import {ElementHandle, FrameBase} from 'puppeteer';
import * as _ from 'lodash';
export abstract class E2EElement {
id: string;
constructor(id: string) {
this.id = id;
}
async getSelector(): Promise<FrameBase> {
return page;
}
async getElement(): Promise<ElementHandle | null>{
if(!await this.getSelector()){
return null;
}
return (await this.getSelector()).$(this.selector())
};
async waitForElement(): Promise<ElementHandle | null>{
return (await this.getSelector()).waitForSelector(this.selector(), {
timeout: 1000
})
};
protected abstract selector(): string;
}
export class E2ENode extends E2EElement {
async port(id: string): Promise<E2EPort> {
return new E2EPort(id, this);
}
protected selector(): string {
return `div[data-nodeid="${this.id}"]`;
}
}
export class E2EPort extends E2EElement {
parent: E2ENode;
constructor(id: string, parent: E2ENode) {
super(id);
this.parent = parent;
}
async getLinks(): Promise<E2ELink[]> {
const element = await this.getElement();
const attribute = await page.evaluate( (obj) => {
return obj.getAttribute('data-links');
}, element);
if(attribute.trim() === ""){
return [];
}
return _.map(attribute.split(','), (id) => {
return new E2ELink(id);
});
}
async link(port: E2EPort): Promise<E2ELink> {
let currentLinks = _.map(await this.getLinks(), 'id');
let bounds = await (await this.getElement()).boundingBox();
// click on this port
page.mouse.move(bounds.x, bounds.y);
page.mouse.down();
//
let bounds2 = await (await port.getElement()).boundingBox();
// drag to other port
page.mouse.move(bounds2.x, bounds2.y);
page.mouse.up();
let newLinks = _.map(await this.getLinks(), 'id');
// get the parent to get the link
return new E2ELink(_.difference(newLinks, currentLinks)[0]);
}
async linkToPoint(x: number, y: number): Promise<E2ELink> {
let currentLinks = _.map(await this.getLinks(), 'id');
let bounds = await (await this.getElement()).boundingBox();
// click on this port
page.mouse.move(bounds.x, bounds.y);
page.mouse.down();
// drag to point
page.mouse.move(x, y);
page.mouse.up();
let newLinks = _.map(await this.getLinks(), 'id');
const link = _.difference(newLinks, currentLinks)[0];
if (!link) {
return null;
}
// get the parent to get the link
return new E2ELink(link);
}
async getSelector(): Promise<FrameBase> {
return (await this.parent.getElement()) as any;
}
protected selector(): string {
return `div[data-name="${this.id}"]`;
}
}
export class E2ELink extends E2EElement {
async select(): Promise<any> {
const point = await page.evaluate(id => {
const path = document.querySelector(`path[data-linkid="${id}"]`) as SVGPathElement;
const point = path.getPointAtLength(path.getTotalLength() / 2);
return {
x: point.x,
y: point.y
};
}, this.id);
await page.keyboard.down('Shift');
await page.mouse.move(point.x, point.y);
await page.mouse.down();
await page.keyboard.up('Shift');
}
protected selector(): string {
return `path[data-linkid="${this.id}"]`;
}
}

View File

@ -1,9 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script src="main.js"></script>
</head>
<body>
<div id="application"></div>
</body>
</html>

View File

@ -1,35 +1,33 @@
import 'jest';
import { E2EHelper } from './E2EHelper';
import {E2ENode} from "./helpers/E2EHelper";
describe('simple flow test', () => {
beforeEach(async () => {
await page.goto(`file://${__dirname}/../../dist/e2e/demo-simple-flow/index.html`);
await page.goto(`file://${__dirname}/../.out/iframe.html?path=/story/simple-usage--simple-flow-example`);
});
it('drag link to port adds a link', async () => {
// create a new link
let helper = new E2EHelper(page);
let node1 = await helper.node('17');
let node2 = await helper.node('9');
let node1 = new E2ENode('18');
let node2 = new E2ENode('10');
let port1 = await node1.port('18');
let port2 = await node2.port('10');
let port1 = await node1.port('19');
let port2 = await node2.port('11');
let newlink = await port1.link(port2);
await expect(await newlink.exists()).toBeTruthy();
await expect(await newlink.waitForElement()).toBeTruthy();
});
it('drag link to node does not add a link', async () => {
// create a new link
let helper = new E2EHelper(page);
let node1 = await helper.node('17');
let node2 = await helper.node('9');
let node1 = new E2ENode('18');
let node2 = new E2ENode('10');
let port1 = await node1.port('18');
let port1 = await node1.port('19');
let node2Bounds = await node2.element.boundingBox();
let node2Bounds = await (await node2.waitForElement()).boundingBox();
let newlink = await port1.linkToPoint(node2Bounds.x, node2Bounds.y);
await expect(newlink).toBeNull();
await expect(newlink).toBeFalsy();
});
});

View File

@ -1,31 +1,30 @@
import 'jest';
import { E2EHelper } from './E2EHelper';
import {E2ELink, E2ENode} from "./helpers/E2EHelper";
describe('simple test', () => {
beforeAll(async () => {
await page.goto(`file://${__dirname}/../../dist/e2e/demo-simple/index.html`);
await page.goto(`file://${__dirname}/../.out/iframe.html?path=/story/simple-usage--simple-example`);
});
it('should delete a link and create a new one', async () => {
// get the existing link
let helper = new E2EHelper(page);
let link = await helper.link('12');
await expect(await link.exists()).toBeTruthy();
let link = new E2ELink('13');
await expect(await link.waitForElement()).toBeTruthy();
// remove it
await link.select();
await page.keyboard.press('Delete');
await expect(await link.exists()).toBeFalsy();
await expect(await link.getElement()).toBeFalsy();
// create a new link
let node1 = await helper.node('6');
let node2 = await helper.node('9');
let node1 = new E2ENode('7');
let node2 = new E2ENode('10');
let port1 = await node1.port('7');
let port2 = await node2.port('10');
let port1 = await node1.port('8');
let port2 = await node2.port('11');
let newlink = await port1.link(port2);
await expect(await newlink.exists()).toBeTruthy();
await expect(await newlink.waitForElement()).toBeTruthy();
});
});

View File

@ -1,7 +0,0 @@
import { configure } from '@storybook/react';
function loadStories() {
require('./test_loader.tsx');
}
configure(loadStories, module);

View File

@ -1,4 +0,0 @@
import * as React from 'react';
import { Toolkit } from '../../src/Toolkit';
Toolkit.TESTING = true;

View File

@ -1,7 +1,7 @@
{
"extends": "../tsconfig",
"include": [
"./src"
"./demos"
],
"exclude": [
"./dist"