diff --git a/.travis.yml b/.travis.yml index 888fa4110..b144196e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,6 +39,11 @@ install: - npm config set prefix=$HOME/.node_modules - npm install - npm run build + - google-chrome-stable --headless --disable-gpu --remote-debugging-port=9222 index.html & + - BROWSER_PID=$! + - node start.js test + - node ./bin/test-chrome.js + - kill $BROWSER_PID - popd - rm -rf tools/idea/idea-artifacts/dependencies diff --git a/tests/src/test/js/client.js b/tests/src/test/js/client.js index 78219a5e9..ced3ced9b 100644 --- a/tests/src/test/js/client.js +++ b/tests/src/test/js/client.js @@ -20,7 +20,7 @@ function tryConnect() { let ws = new WebSocket("ws://localhost:9090"); ws.onopen = () => { - console.log("Connected established"); + console.log("Connection established"); listen(ws); }; @@ -30,6 +30,10 @@ function tryConnect() { tryConnect(); }, 500); }; + + ws.onerror = err => { + console.log("Could not connect WebSocket", err); + } } function listen(ws) { @@ -58,12 +62,14 @@ function runTests(ws, suiteId, tests, index) { function runSingleTest(test, callback) { console.log("Running test " + test.name + " consisting of " + test.files); - let iframe = document.getElementById("test"); + let iframe = document.createElement("iframe"); + document.body.appendChild(iframe); let handshakeListener = () => { window.removeEventListener("message", handshakeListener); let listener = event => { window.removeEventListener("message", listener); + document.body.removeChild(iframe); callback(event.data); }; window.addEventListener("message", listener); diff --git a/tests/src/test/js/index.html b/tests/src/test/js/index.html index 36c250b29..580bc3699 100644 --- a/tests/src/test/js/index.html +++ b/tests/src/test/js/index.html @@ -21,6 +21,5 @@ - \ No newline at end of file diff --git a/tests/src/test/js/src/cdp.js b/tests/src/test/js/src/cdp.js new file mode 100644 index 000000000..22667b1a5 --- /dev/null +++ b/tests/src/test/js/src/cdp.js @@ -0,0 +1,118 @@ +/* + * Copyright 2019 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +"use strict"; + +import * as http from "http"; +import { client as WebSocketClient } from "websocket"; + +function httpGetJson(url) { + return new Promise((resolve, reject) => { + http.request(url, resp => { + if (resp.statusCode !== 200) { + reject(new Error(`HTTP status: ${resp.statusCode}`)); + return; + } + let data = ""; + resp.on('data', (chunk) => { + data += chunk; + }); + resp.on('end', () => { + resolve(JSON.parse(data)); + }); + }).on("error", err => { + reject(new Error(`HTTP error: ${err.message}`)); + }).end(); + }); +} + +function connectWs(url) { + return new Promise((resolve, reject) => { + const client = new WebSocketClient(); + client.on("connectFailed", error => reject(new Error('Connect Error: ' + error))); + client.on("connect", resolve); + client.connect(url); + }); +} + +class CDP { + constructor(conn) { + this.idGenerator = 1; + this.pendingCalls = Object.create(null); + this.eventHandlers = Object.create(null); + this.conn = conn; + } + + start() { + this.conn.on("message", message => { + if (message.type === 'utf8') { + const messageObj = JSON.parse(message.utf8Data); + if (messageObj.id !== void 0) { + const pendingCall = this.pendingCalls[messageObj.id]; + delete this.pendingCalls[messageObj.id]; + if (messageObj.error) { + pendingCall.reject(new Error(`Error calling CDP method ${messageObj.error}`)); + } else { + pendingCall.resolve(messageObj.result); + } + } else { + const handlers = this.eventHandlers[messageObj.method]; + if (handlers) { + for (const handler of handlers) { + handler(messageObj.params); + } + } + } + } + }); + this.conn.on("close", () => { + for (const key in Object.getOwnPropertyNames(this.pendingCalls)) { + this.pendingCalls[key].reject(new Error("Connection closed before result received")); + } + }); + this.conn.on("error", err => { + console.error("WS error: %j", err); + }); + } + + call(method, params = undefined) { + return new Promise((resolve, reject) => { + const id = this.idGenerator++; + this.pendingCalls[id] = { resolve, reject }; + this.conn.send(JSON.stringify({ id, method, params })); + }); + } + + async on(eventName, handler) { + let handlers = this.eventHandlers[eventName]; + if (handlers === void 0) { + handlers = []; + this.eventHandlers[eventName] = handlers; + } + handlers.push(handler); + } + + static async connect(url) { + const targets = await httpGetJson(url + "/json/list"); + const wsUrl = targets.find(target => target.type === "page").webSocketDebuggerUrl; + console.log("Connected to Chrome"); + const wsConn = await connectWs(wsUrl); + console.log(`Connected to WS endpoint: ${wsUrl}`); + return wsConn; + } +} + +export { CDP }; \ No newline at end of file diff --git a/tests/src/test/js/src/log-chrome.js b/tests/src/test/js/src/log-chrome.js new file mode 100644 index 000000000..e78717973 --- /dev/null +++ b/tests/src/test/js/src/log-chrome.js @@ -0,0 +1,48 @@ +/* + * Copyright 2019 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +"use strict"; + +import { CDP } from "./cdp.js"; + +async function run() { + const wsConn = await CDP.connect("http://localhost:9222"); + const cdp = new CDP(wsConn); + const waitForLog = new Promise(resolve => { + let timeout = setTimeout(resolve, 500); + cdp.on("Runtime.consoleAPICalled", event => { + const value = event.args.filter(arg => arg.type === "string").map(arg => arg.value).join(""); + if (value !== "") { + console.log("[LOG] " + new Date(event.timestamp) + ": " + value); + } + clearTimeout(timeout); + timeout = setTimeout(resolve, 500); + }); + }); + + cdp.start(); + + try { + await cdp.call("Runtime.enable"); + await waitForLog; + } finally { + wsConn.close(); + } +} + +run().catch(e => { + console.error("Error", e); +}); diff --git a/tests/src/test/js/src/run-tests.js b/tests/src/test/js/src/run-tests.js index d118674b5..c7a210831 100644 --- a/tests/src/test/js/src/run-tests.js +++ b/tests/src/test/js/src/run-tests.js @@ -192,7 +192,7 @@ class TestRunner { const resultPromises = []; - this.timeout = createRefreshableTimeoutPromise(10000); + this.timeout = createRefreshableTimeoutPromise(20000); for (let i = 0; i < suite.testCases.length; ++i) { resultPromises.push(new Promise(resolve => { this.pendingRequests[request.id + "-" + i] = resolve; diff --git a/tests/src/test/js/src/test-chrome.js b/tests/src/test/js/src/test-chrome.js new file mode 100644 index 000000000..c5a38b513 --- /dev/null +++ b/tests/src/test/js/src/test-chrome.js @@ -0,0 +1,95 @@ +/* + * Copyright 2019 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +"use strict"; + +import { CDP } from "./cdp.js"; + +function waitForTest(cdp) { + return new Promise(resolve => { + let stage = 'initial'; + let requestId = null; + cdp.on("Runtime.consoleAPICalled", event => { + const value = event.args.filter(arg => arg.type === "string").map(arg => arg.value).join(""); + switch (stage) { + case 'initial': + if (value === 'Connection established') { + stage = 'connected'; + } + break; + case 'connected': { + const result = /Request #([0-9]+) received/.exec(value); + if (result) { + requestId = result[1]; + stage = 'sent'; + } + break; + } + case 'sent': + if (value === "Running test only simple consisting of http://localhost:9090//only/test.js") { + stage = 'ran'; + } + break; + case 'ran': + if (value === "Sending response #" + requestId) { + stage = 'received'; + resolve(); + } + break; + } + }); + }); +} + +function timeout(time) { + let timer; + return { + promise: new Promise((resolve, reject) => { + timer = setTimeout(() => { reject(new Error("Timeout expired")); }, time); + }), + cancel: () => clearTimeout(timer) + } +} + +async function run() { + const wsConn = await CDP.connect("http://localhost:9222"); + const cdp = new CDP(wsConn); + cdp.on("Runtime.consoleAPICalled", event => { + const value = event.args.filter(arg => arg.type === "string").map(arg => arg.value).join(""); + if (value !== "") { + console.log("[LOG] " + new Date(event.timestamp) + ": " + value); + } + }); + + const wait = waitForTest(cdp); + let timer; + cdp.start(); + + try { + await cdp.call("Runtime.enable"); + timer = timeout(10000); + await Promise.race([wait, timer.promise]); + } finally { + if (timer) { + timer.cancel(); + } + wsConn.close(); + } +} + +run().catch(e => { + console.error("Error", e); +}); diff --git a/tests/src/test/js/test/only/test.js b/tests/src/test/js/test/only/test.js new file mode 100644 index 000000000..98d9a437b --- /dev/null +++ b/tests/src/test/js/test/only/test.js @@ -0,0 +1,19 @@ +/* + * Copyright 2019 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +function main(args, callback) { + callback(); +} \ No newline at end of file