diff --git a/packages/xod-client-browser/package.json b/packages/xod-client-browser/package.json index 656f2ee4c..d7477e43e 100644 --- a/packages/xod-client-browser/package.json +++ b/packages/xod-client-browser/package.json @@ -32,6 +32,7 @@ "devDependencies": { "chai": "^4.1.2", "node-static": "^0.7.10", + "expose-loader": "^1.0.3", "why-did-you-update": "^0.1.0", "xod-fs": "^0.36.0" }, diff --git a/packages/xod-client-browser/test-func/bootstrap.js b/packages/xod-client-browser/test-func/bootstrap.js index cb979b962..9c5f99ecb 100644 --- a/packages/xod-client-browser/test-func/bootstrap.js +++ b/packages/xod-client-browser/test-func/bootstrap.js @@ -1,17 +1,38 @@ /* global browser */ -// eslint-disable-next-line import/no-extraneous-dependencies +/* eslint-disable import/no-extraneous-dependencies */ import puppeteer from 'puppeteer'; +import webpack from 'webpack'; +import WebpackDevServer from 'webpack-dev-server'; +/* eslint-enable import/no-extraneous-dependencies */ + import { assert } from 'chai'; import R from 'ramda'; +import config from '../webpack.config.test'; -const { startServer, stopServer } = require('../tools/staticServer'); +import { PORT } from './server.config'; const globalVariables = R.pick(['browser', 'assert'], global); -before(async () => { - await startServer(); +const startServer = () => + new Promise((resolve, reject) => { + const compiler = webpack(config); + const server = new WebpackDevServer(compiler); + // Replace the next line with `compiler.hooks.done.tap('onDone', ...` + // after upgrading webpack to version >4 + compiler.plugin('done', () => { + resolve(server); + }); + server.listen(PORT, 'localhost', err => { + if (err) { + console.error(err); // eslint-disable-line no-console + reject(err); + } + }); + }); +before(async () => { + global.server = await startServer(); global.assert = assert; global.browser = await puppeteer.launch({ args: [ @@ -25,7 +46,7 @@ before(async () => { after(() => { browser.close(); - stopServer(); + global.server.close(() => {}); global.browser = globalVariables.browser; global.assert = globalVariables.assert; diff --git a/packages/xod-client-browser/test-func/mocha.opts b/packages/xod-client-browser/test-func/mocha.opts index 1d4110543..522d54ff7 100644 --- a/packages/xod-client-browser/test-func/mocha.opts +++ b/packages/xod-client-browser/test-func/mocha.opts @@ -1,4 +1,4 @@ --require babel-register --colors ---timeout=60000 +--timeout=90000 --bail diff --git a/packages/xod-client-browser/test-func/pageObjects/IdeCrashReport.js b/packages/xod-client-browser/test-func/pageObjects/IdeCrashReport.js new file mode 100644 index 000000000..6848b84fe --- /dev/null +++ b/packages/xod-client-browser/test-func/pageObjects/IdeCrashReport.js @@ -0,0 +1,36 @@ +import BasePageObject from './BasePageObject'; + +const SELECTOR = '.IdeCrashReport'; + +class IdeCrashReport extends BasePageObject { + async getErrorReport() { + const textAreaElementHandle = await this.elementHandle.$( + '.Message textarea' + ); + + const errorReport = await this.page.evaluate( + el => el.value, + textAreaElementHandle + ); + + return errorReport; + } + + async clickClose() { + const [button] = await this.elementHandle.$x('//button'); + await button.click(); + } +} + +IdeCrashReport.findOnPage = async page => { + const elementHandle = await page.$(SELECTOR); + if (!elementHandle) return null; + return new IdeCrashReport(page, elementHandle); +}; + +IdeCrashReport.waitOnPage = async page => { + await page.waitFor(SELECTOR, { timeout: 20000 }); + return IdeCrashReport.findOnPage(page); +}; + +export default IdeCrashReport; diff --git a/packages/xod-client-browser/test-func/pageObjects/Node.js b/packages/xod-client-browser/test-func/pageObjects/Node.js index 087a61b50..e7fd3ef35 100644 --- a/packages/xod-client-browser/test-func/pageObjects/Node.js +++ b/packages/xod-client-browser/test-func/pageObjects/Node.js @@ -70,6 +70,11 @@ export const getAllNodes = async page => { return elementHandles.map(eh => new Node(page, eh)); }; +export const getAllNodeIds = async page => { + const nodes = await getAllNodes(page); + return Promise.all(nodes.map(node => node.getId())); +}; + export const getSelectedNodes = async page => { const elementHandles = await page.$$('.Node.is-selected'); return elementHandles.map(eh => new Node(page, eh)); diff --git a/packages/xod-client-browser/test-func/recovering.spec.js b/packages/xod-client-browser/test-func/recovering.spec.js new file mode 100644 index 000000000..aa5f34c60 --- /dev/null +++ b/packages/xod-client-browser/test-func/recovering.spec.js @@ -0,0 +1,90 @@ +/* global browser:false, assert:false */ + +import getPage from './utils/getPage'; + +import ProjectBrowser from './pageObjects/ProjectBrowser'; +import PromptPopup from './pageObjects/PromptPopup'; +import IdeCrashReport from './pageObjects/IdeCrashReport'; +import { getAllNodeIds, getSelectedNodes } from './pageObjects/Node'; + +it('Recovers on IDE crash', async () => { + const page = await getPage(browser); + + // Recovering of the IDE recreates all components, + // so we have to find this element again after recovering finished + let projectBrowser = await ProjectBrowser.findOnPage(page); + + // Create a new patch + projectBrowser.clickCreatePatch(); + + const popup = await PromptPopup.waitOnPage(page); + await popup.typeText('test-recover'); + await popup.clickConfirm(); + + // Mock the `render` method of Link to emulate a React Error + await page.evaluate(() => { + window.Components.Link.prototype.render = function erroredRender() { + throw new Error('CATCH ME'); + }; + }); + + // Add first node + await projectBrowser.addNodeViaContextMenu('xod/core', 'clock'); + const [clockNode] = await getSelectedNodes(page); + await clockNode.drag(150, 150); + + // Add second node + await projectBrowser.addNodeViaContextMenu('xod/core', 'flip-flop'); + const [flipFlopNode] = await getSelectedNodes(page); + await flipFlopNode.drag(150, 250); + + // Begin linking: click on first pin + const clockTickPin = await clockNode.findPinByName('TICK'); + await clockTickPin.click(); + // It will create a Link, which render method are broken for the test + // So the IDE should catch the Error and recover to the previous state + + // Test that state recovered and error has been shown + const crashReport = await IdeCrashReport.waitOnPage(page); + const report = await crashReport.getErrorReport(); + + // Report contains a lot of data, so we'll check only first two rows + const expectedFirstRowsOfReport = [ + '# ERROR', + "Error: A cross-origin error was thrown. React doesn't have access to the actual error object in development. See https://fb.me/react-crossorigin-error for more information.", + ].join('\n'); + assert.equal(report.split('\n', 2).join('\n'), expectedFirstRowsOfReport); + + // Test that report can be closed + await crashReport.clickClose(); + assert.isNull( + await IdeCrashReport.findOnPage(page), + 'Crash report element is closed' + ); + + // Test that state was recovered correctly (the patch has placed nodes) + const expectedNodesOnPatch = [ + await clockNode.getId(), + await flipFlopNode.getId(), + ]; + assert.sameMembers( + await getAllNodeIds(page), + expectedNodesOnPatch, + 'The patch should contain the same nodes after recovering' + ); + + // Renew the `projectBrowser` page object + projectBrowser = await ProjectBrowser.findOnPage(page); + + // Test that IDE still works (add one more node) + await projectBrowser.addNodeViaContextMenu('xod/gpio', 'digital-write'); + const [digitalWrite] = await getSelectedNodes(page); + await digitalWrite.drag(150, 350); + + // Test that the third node was added successfully + assert.sameMembers( + await getAllNodeIds(page), + [...expectedNodesOnPatch, await digitalWrite.getId()], + 'The patch should contain the same nodes after recovering' + ); +}); diff --git a/packages/xod-client-browser/test-func/server.config.js b/packages/xod-client-browser/test-func/server.config.js new file mode 100644 index 000000000..9715ce50f --- /dev/null +++ b/packages/xod-client-browser/test-func/server.config.js @@ -0,0 +1,2 @@ +export const PORT = process.env.STATIC_SERVER_PORT || 8081; +export const SERVER_URL = `http://localhost:${PORT}`; diff --git a/packages/xod-client-browser/test-func/utils/getPage.js b/packages/xod-client-browser/test-func/utils/getPage.js index f70dcd0fc..9d7c7b86b 100644 --- a/packages/xod-client-browser/test-func/utils/getPage.js +++ b/packages/xod-client-browser/test-func/utils/getPage.js @@ -1,4 +1,4 @@ -import { SERVER_URL } from '../../tools/staticServer'; +import { SERVER_URL } from '../server.config'; export default async function getPage(browser) { const page = await browser.newPage(); diff --git a/packages/xod-client-browser/webpack.config.test.js b/packages/xod-client-browser/webpack.config.test.js new file mode 100644 index 000000000..fc8a8fa30 --- /dev/null +++ b/packages/xod-client-browser/webpack.config.test.js @@ -0,0 +1,63 @@ +const path = require('path'); +/* eslint-disable import/no-extraneous-dependencies */ +const merge = require('webpack-merge'); +/* eslint-enable import/no-extraneous-dependencies */ +const baseConfig = require('./webpack.config.js'); + +const pkgpath = subpath => path.join(__dirname, subpath); + +const babelLoader = { + loader: 'babel-loader', + options: { + presets: ['react', 'es2015'], + plugins: ['transform-object-rest-spread'], + }, +}; + +module.exports = merge.smart(baseConfig, { + devtool: 'eval-source-map', + output: { + publicPath: 'http://localhost:8080/', + }, + devServer: { + hot: false, + host: 'localhost', + port: 8080, + contentBase: pkgpath('dist'), + compress: true, + }, + module: { + rules: [ + { + test: /xod-client\/.+(components|containers)\/.+\.js$/, + use: [ + { + loader: 'expose-loader', + options: { + exposes: { + globalName: 'Components.[name]', + moduleLocalName: 'default', + }, + }, + }, + ], + }, + { + test: /\.jsx$/, + use: [ + { + loader: 'expose-loader', + options: { + exposes: { + globalName: 'Components.[name]', + moduleLocalName: 'default', + override: true, + }, + }, + }, + babelLoader, + ], + }, + ], + }, +}); diff --git a/yarn.lock b/yarn.lock index bc8a75acd..acb57e085 100644 --- a/yarn.lock +++ b/yarn.lock @@ -903,6 +903,11 @@ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.0.tgz#2cc2ca41051498382b43157c8227fea60363f94a" integrity sha512-ohkhb9LehJy+PA40rDtGAji61NCgdtKLAlFoYp4cnuuQEswwdK3vz9SOIkkyc3wrk8dzjphQApNs56yyXLStaQ== +"@types/json-schema@^7.0.6": + version "7.0.6" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" + integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== + "@types/lodash@^4.14.116": version "4.14.117" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.117.tgz#695a7f514182771a1e0f4345d189052ee33c8778" @@ -1089,6 +1094,11 @@ ajv-keywords@^3.4.0: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.0.tgz#4b831e7b531415a7cc518cd404e73f6193c6349d" integrity sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw== +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + ajv@^4.7.0, ajv@^4.9.1: version "4.11.8" resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" @@ -1127,6 +1137,16 @@ ajv@^6.1.0: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ajv@^6.5.5: version "6.5.5" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.5.tgz#cf97cdade71c6399a92c6d6c4177381291b781a1" @@ -4425,7 +4445,12 @@ colors@1.0.3: resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= -colors@>=0.6.0, colors@~1.1.2: +colors@>=0.6.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +colors@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM= @@ -6130,6 +6155,11 @@ emojis-list@^2.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + encode-3986@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/encode-3986/-/encode-3986-1.0.0.tgz#940d51498f8741ade184b75ad1439b317c0c7a60" @@ -6737,6 +6767,14 @@ exports-loader@^0.6.3: loader-utils "^1.0.2" source-map "0.5.x" +expose-loader@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/expose-loader/-/expose-loader-1.0.3.tgz#5686d3b78cac8831c4af11c3dc361563deb8a9c0" + integrity sha512-gP6hs3vYeWIqyoVfsApGQcgCEpbcI1xe+celwI31zeDhXz2q03ycBC1+75IlQUGaYvj6rAloFIe/NIBnEElLsQ== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + express@^4.16.2: version "4.16.2" resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" @@ -6920,6 +6958,11 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + fast-diff@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154" @@ -9896,6 +9939,13 @@ json5@^2.1.0: dependencies: minimist "^1.2.0" +json5@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + dependencies: + minimist "^1.2.5" + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" @@ -10273,6 +10323,15 @@ loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: emojis-list "^2.0.0" json5 "^0.5.0" +loader-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" + integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -11074,6 +11133,11 @@ minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" @@ -11670,9 +11734,9 @@ node-sass@^4.9.2: "true-case-path" "^1.0.2" node-static@^0.7.10: - version "0.7.10" - resolved "https://registry.yarnpkg.com/node-static/-/node-static-0.7.10.tgz#a1ddb72027c7f67179fb33487807b57e8bc7d2e7" - integrity sha512-bd7zO5hvCWzdglgwz9t82T4mYTEUzEG5pXnSqEzitvmEacusbhl8/VwuCbMaYR9g2PNK5191yBtAEQLJEmQh1A== + version "0.7.11" + resolved "https://registry.yarnpkg.com/node-static/-/node-static-0.7.11.tgz#60120d349f3cef533e4e820670057eb631882e7f" + integrity sha512-zfWC/gICcqb74D9ndyvxZWaI1jzcoHmf4UTHWQchBNuNMxdBLJMDiUgZ1tjGLEIe/BMhj2DxKD8HOuc2062pDQ== dependencies: colors ">=0.6.0" mime "^1.2.9" @@ -14905,6 +14969,15 @@ schema-utils@^0.4.0: ajv "^6.1.0" ajv-keywords "^3.1.0" +schema-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef" + integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA== + dependencies: + "@types/json-schema" "^7.0.6" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + scroll-into-view-if-needed@2.2.20: version "2.2.20" resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.20.tgz#3a46847a72233a3af9770e55df450f2a7f2e2a0e"