Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use chrome for testing #2726

Merged
merged 24 commits into from
May 24, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -30,7 +30,10 @@
],
"rules": {
"prettier/prettier": [
"error"
"error",
{
"endOfLine": "auto"
}
],
"no-control-regex": 0,
"no-instanceof/no-instanceof": [
@@ -61,4 +64,4 @@
"curly": 2,
"require-atomic-updates": 0
}
}
}
28 changes: 14 additions & 14 deletions .github/workflows/taiko.yml
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node_version: ['16']
node_version: ['20']
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v2
@@ -50,8 +50,11 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node_version: ['16']
os: [ubuntu-latest, windows-latest]
node_version: ['20']
os: [ubuntu-latest]
include:
- os: windows-latest
node_version: '16'
steps:
- uses: actions/checkout@v2
- name: Use NodeJS ${{ matrix.node_version }}
@@ -97,10 +100,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use NodeJS 16
- name: Use NodeJS 20
uses: actions/setup-node@v1
with:
node-version: 16
node-version: 20
- name: install browser dependencies
run: |
sudo apt-get update
@@ -115,7 +118,7 @@ jobs:
libgtk-3-0 \
libdrm-dev \
libgbm-dev \
libasound-dev
libasound-dev
- name: install xvfb
run: sudo apt install xvfb
- name: functional-tests-headful
@@ -128,7 +131,7 @@ jobs:
if: failure()
with:
name: ft-reports-linux-headful
path: test/functional-tests/reports/html-report
path: test/functional-tests/reports/html-report
- name: Upload logs
uses: actions/upload-artifact@v1
if: failure()
@@ -139,16 +142,13 @@ jobs:
docs-tests:
needs: unit-tests
name: Docs tests - ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use NodeJS 16
- name: Use NodeJS 20
uses: actions/setup-node@v1
with:
node-version: '16'
node-version: '20'
- name: install
run: npm install
- name: install browser dependencies
@@ -174,7 +174,7 @@ jobs:
if: failure()
with:
name: docs-tests-reports-${{ matrix.os }}
path: test/docs-tests/gauge/reports/html-report
path: test/docs-tests/gauge/reports/html-report
- name: Upload logs
uses: actions/upload-artifact@v1
if: failure()
5 changes: 2 additions & 3 deletions .github/workflows/update_chromium.yml
Original file line number Diff line number Diff line change
@@ -2,11 +2,10 @@ name: update-chromium
on:
schedule:
# cron job that will run 1st and 15th of every month by 12am
- cron: '0 12 1,15 * *'
- cron: '0 12 1,15 * *'

jobs:
update-to-latest-chromium:

runs-on: ubuntu-latest

steps:
@@ -16,7 +15,7 @@ jobs:
- name: Use NodeJS 16
uses: actions/setup-node@v1
with:
node-version: '16'
node-version: '20'

- name: Setup
run: |
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -68,10 +68,12 @@ docs/layout/partials/content.html
#local chromium
.local-chromium

#asdf tool-version
.tool-versions

#test report
test/functional-tests/reports/
test/functional-tests/.gauge/
test/functional-tests/package-lock.json
test/unit-tests/data/*.html

examples/package-lock.json
1 change: 0 additions & 1 deletion .tool-versions

This file was deleted.

19 changes: 9 additions & 10 deletions docs/configuring_taiko.md
Original file line number Diff line number Diff line change
@@ -9,13 +9,12 @@ for specific uses cases

Taiko lets you specify certain Environment variables to customize its behaviour

* `TAIKO_ENABLE_ACTION_OUTPUT` - set to `true` to print output of each action. It's set to `true` by default with Taiko runner and in REPL mode.
* `TAIKO_SKIP_CHROMIUM_DOWNLOAD` - set to `true` to skip downloading chromium
* `TAIKO_HIGHLIGHT_ON_ACTION` - set to `false` to turn off highlighting the element on action
* `TAIKO_CHROMIUM_URL` - set to host url of mirror to download chromium
* `TAIKO_BROWSER_PATH` - set to launch browser from different location
* `TAIKO_BROWSER_ARGS` - set ',' separated browser command line switches to launch browser with extra args
* `TAIKO_EMULATE_DEVICE` - set to device model to emulate device view port
* `TAIKO_EMULATE_NETWORK` - set to the network type for Taiko to simulate. Available options are GPRS, Regular2G, Good2G, Regular3G, Good3G, Regular4G, DSL, WiFi, Offline
* `TAIKO_PLUGIN` - set to the plugin which you want Taiko to load. Takes comma separated values.
* `TAIKO_SKIP_DOCUMENTATION` - set to skip API documentation generation during install.
- `TAIKO_ENABLE_ACTION_OUTPUT` - set to `true` to print output of each action. It's set to `true` by default with Taiko runner and in REPL mode.
- `TAIKO_SKIP_CHROMIUM_DOWNLOAD` - set to `true` to skip downloading chromium
- `TAIKO_HIGHLIGHT_ON_ACTION` - set to `false` to turn off highlighting the element on action
- `TAIKO_BROWSER_PATH` - set to launch browser from different location
- `TAIKO_BROWSER_ARGS` - set ',' separated browser command line switches to launch browser with extra args
- `TAIKO_EMULATE_DEVICE` - set to device model to emulate device view port
- `TAIKO_EMULATE_NETWORK` - set to the network type for Taiko to simulate. Available options are GPRS, Regular2G, Good2G, Regular3G, Good3G, Regular4G, DSL, WiFi, Offline
- `TAIKO_PLUGIN` - set to the plugin which you want Taiko to load. Takes comma separated values.
- `TAIKO_SKIP_DOCUMENTATION` - set to skip API documentation generation during install.
2 changes: 1 addition & 1 deletion examples/01-file_upload.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { goto, fileField, button, above, attach, click, text } = require('taiko'),
path = require('path'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browserLauncher'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browser/launcher'),
expect = require('chai').expect;

(async () => {
2 changes: 1 addition & 1 deletion examples/02-file_download.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { goto, client, click } = require('taiko'),
path = require('path'),
fs = require('fs'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browserLauncher'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browser/launcher'),
expect = require('chai').expect;

(async () => {
2 changes: 1 addition & 1 deletion examples/03-work-with-frames.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { goto, text } = require('taiko'),
path = require('path'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browserLauncher'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browser/launcher'),
expect = require('chai').expect;

(async () => {
2 changes: 1 addition & 1 deletion examples/04-windows-tabs.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { goto, click, title, closeTab, currentURL, text } = require('taiko'),
path = require('path'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browserLauncher'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browser/launcher'),
expect = require('chai').expect;

(async () => {
2 changes: 1 addition & 1 deletion examples/05-dropdown.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { goto, dropDown } = require('taiko'),
path = require('path'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browserLauncher'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browser/launcher'),
expect = require('chai').expect;

(async () => {
2 changes: 1 addition & 1 deletion examples/06-basic-auth.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { goto, text } = require('taiko'),
path = require('path'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browserLauncher'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browser/launcher'),
expect = require('chai').expect;

(async () => {
2 changes: 1 addition & 1 deletion examples/07-dynamic-loading.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { goto, click, text } = require('taiko'),
path = require('path'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browserLauncher'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browser/launcher'),
expect = require('chai').expect;

(async () => {
2 changes: 1 addition & 1 deletion examples/08-contenteditable.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { goto, below, textBox, write, into } = require('taiko'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browserLauncher'),
{ openBrowserAndStartScreencast, closeBrowserAndStopScreencast } = require('./browser/launcher'),
path = require('path'),
expect = require('chai').expect;
const cwd = process.cwd();
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
@@ -40,7 +40,7 @@ This folder contains samples inspired by some of selenium tips published by [ele

`npm test` runs the examples under a headless chromium.

> **Note:** The examples use `openBrowserAndStartScreencast` and `closeBrowserAndStopScreencast` from [`browserLauncher`](browserLauncher.js) instead of `openBrowser` and `closeBrowser` directly. This is to capture screen recordings. The `browserLauncher` methods can be substituted with `openBrowser` and `closeBrowser` if there is no need for screencast capture.
> **Note:** The examples use `openBrowserAndStartScreencast` and `closeBrowserAndStopScreencast` from [`browserLauncher`](browser/launcher.js) instead of `openBrowser` and `closeBrowser` directly. This is to capture screen recordings. The `browserLauncher` methods can be substituted with `openBrowser` and `closeBrowser` if there is no need for screencast capture.
### Run Options

2 changes: 1 addition & 1 deletion examples/runner.js
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ var server = app.listen(3000, async () => {
var examples = glob
.sync('*.js')
.filter(
(f) => __filename !== path.resolve(f) && 'browserLauncher.js' !== f && f.startsWith(prefix),
(f) => __filename !== path.resolve(f) && 'browser/launcher.js' !== f && f.startsWith(prefix),
)
.map((f) => {
return { file: f, task: () => run(f) };
223 changes: 223 additions & 0 deletions lib/browser/browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/**
* Copyright 2018 Thoughtworks Inc. All rights reserved.
*
* 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.
*/
/**
* This module is imported from Puppeteer(https://github.com/GoogleChrome/puppeteer)
* Few modifications are done on the file.
*/

const fs = require('fs-extra');
const { readdir, access, removeSync, existsSync } = fs;
const { join, basename } = require('path');
const { promisify } = require('util');
const { helper, assert } = require('../helper');
const { createInterface } = require('readline');
const { parse } = require('url');
const BrowserMetadata = require('./metadata');
const metadata = new BrowserMetadata();

const supportedPlatforms = ['mac-arm64', 'mac-x64', 'linux', 'win32', 'win64'];

const readdirAsync = promisify(readdir.bind(fs));

function existsAsync(filePath) {
let fulfill = null;
const promise = new Promise((x) => (fulfill = x));
access(filePath, (err) => fulfill(!err));
return promise;
}

class Browser {
/**
* @param {!Browser.Options=} options
*/
constructor() {
this._downloadsFolder = join(helper.projectRoot(), '.local-chromium');
this._platform = metadata.platform();
this.revisionInfo = metadata.revisionInfo();
}

/**
* @return {string}
*/
platform() {
return this._platform;
}

/**
* @return {!Promise<!Array<string>>}
*/
async localRevisions() {
if (!(await existsAsync(this._downloadsFolder))) {
return [];
}
const fileNames = await readdirAsync(this._downloadsFolder);
return fileNames
.map((fileName) => parseFolderPath(fileName))
.filter((entry) => entry && entry.platform === this._platform)
.map((entry) => entry.revision);
}

/**
* @param {string} revision
* @return {!Promise}
*/
async remove(revision) {
const folderPath = this._getFolderPath(revision);
assert(
await existsAsync(folderPath),
`Failed to remove: revision ${revision} is not downloaded`,
);
removeSync(folderPath);
}

_resolveExecutablePath() {
const executablePath = process.env['TAIKO_BROWSER_PATH'];
if (executablePath) {
const missingText = !existsSync(executablePath)
? 'Tried to use TAIKO_BROWSER_PATH env variable to launch browser but did not find any executable at: ' +
executablePath
: null;
return { executablePath, missingText };
}

let metadata = require(join(helper.projectRoot(), 'package.json'));

if (!metadata.taiko) {
return {
executablePath,
missingText:
'Cannot find browser executable information in package.json, please set TAIKO_BROWSER_PATH env variable',
};
}

const missingText = !this.revisionInfo.local
? 'Chromium revision is not downloaded. Provide TAIKO_BROWSER_PATH or Install taiko again to download bundled chromium.'
: null;
return {
executablePath: this.revisionInfo.executablePath,
missingText,
};
}

getExecutablePath() {
const { missingText, executablePath } = this._resolveExecutablePath();
if (missingText) {
throw new Error(missingText);
}
return executablePath;
}

waitForWSEndpoint(browserProcess, timeout) {
return new Promise((resolve, reject) => {
const rl = createInterface({
input: browserProcess.stderr,
});
let stderr = '';
const listeners = [
helper.addEventListener(rl, 'line', onLine),
helper.addEventListener(rl, 'close', () => onClose()),
helper.addEventListener(browserProcess, 'exit', () => onClose()),
helper.addEventListener(browserProcess, 'error', (error) => onClose(error)),
];
const timeoutId = timeout ? setTimeout(onTimeout, timeout) : 0;

/**
* @param {!Error=} error
*/
function onClose(error) {
cleanup();
reject(
new Error(
'Failed to launch browser!' + (error ? ' ' + error.message : '') + '\n' + stderr,
),
);
}

function onTimeout() {
cleanup();
reject(new Error(`Timed out after ${timeout} ms while trying to connect to Browser!`));
}

/**
* @param {string} line
*/
function onLine(line) {
stderr += line + '\n';
const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
if (!match) {
return;
}
cleanup();
const endpoint = {
host: parse(match[1]).hostname,
port: parse(match[1]).port,
browser: parse(match[1]).href,
};
resolve(endpoint);
}

function cleanup() {
if (timeoutId) {
clearTimeout(timeoutId);
}
helper.removeEventListeners(listeners);
}
});
}

/**
* @param {string} revision
* @return {string}
*/
_getFolderPath(revision) {
return join(this._downloadsFolder, this._platform + '-' + revision);
}
}

module.exports = Browser;

/**
* @param {string} folderPath
* @return {?{platform: string, revision: string}}
*/
function parseFolderPath(folderPath) {
const name = basename(folderPath);
const splits = name.split('-');
if (splits.length !== 2) {
return null;
}
const [platform, revision] = splits;
if (!supportedPlatforms.includes(platform)) {
return null;
}
return { platform, revision };
}

/**
* @typedef {Object} BrowserFetcher.Options
* @property {string=} platform
* @property {string=} path
* @property {string=} host
*/

/**
* @typedef {Object} BrowserFetcher.RevisionInfo
* @property {string} folderPath
* @property {string} executablePath
* @property {string} url
* @property {boolean} local
* @property {string} revision
*/
205 changes: 205 additions & 0 deletions lib/browser/fetcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/**
* Copyright 2018 Thoughtworks Inc. All rights reserved.
*
* 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.
*/
/**
* This module is imported from Puppeteer(https://github.com/GoogleChrome/puppeteer)
* Few modifications are done on the file.
*/

const fs = require('fs-extra');
const path = require('path');
const extract = require('extract-zip');
const util = require('util');
const URL = require('url');
const { helper, assert } = require('../helper');
const ProxyAgent = require('https-proxy-agent');
const getProxyForUrl = require('proxy-from-env').getProxyForUrl;

const mkdirAsync = util.promisify(fs.mkdir.bind(fs));
const unlinkAsync = util.promisify(fs.unlink.bind(fs));
const chmodAsync = util.promisify(fs.chmod.bind(fs));
const BrowserMetadata = require('./metadata');
const metadata = new BrowserMetadata();

function existsAsync(filePath) {
let fulfill = null;
const promise = new Promise((x) => (fulfill = x));
fs.access(filePath, (err) => fulfill(!err));
return promise;
}

class BrowserFetcher {
constructor(options = {}) {
this._downloadsFolder = options.path || path.join(helper.projectRoot(), '.local-chromium');
this._platform = options.platform || metadata.platform();
this.downloadURL = metadata.downloadURL;
this.revisionInfo = metadata.revisionInfo();
}

/**
* @return {string}
*/
platform() {
return this._platform;
}

/**
* @return {!Promise<boolean>}
*/
canDownload() {
let resolve;
const promise = new Promise((x) => (resolve = x));
const request = httpRequest(this.downloadURL, 'HEAD', (response) => {
resolve(response.statusCode === 200);
});
request.on('error', (error) => {
console.error(error);
resolve(false);
});
return promise;
}

/**
* @param {string} revision
* @param {?function(number, number)} progressCallback
* @return {!Promise<!BrowserMetadata.RevisionInfo>}
*/
async download(revision, progressCallback) {
const zipPath = path.join(this._downloadsFolder, `download-${this._platform}-${revision}.zip`);
const folderPath = this._getFolderPath(revision);
if (await existsAsync(folderPath)) {
return this.revisionInfo;
}
if (!(await existsAsync(this._downloadsFolder))) {
await mkdirAsync(this._downloadsFolder);
}
try {
await downloadFile(this.downloadURL, zipPath, progressCallback);
await extractZip(zipPath, folderPath);
} finally {
if (await existsAsync(zipPath)) {
await unlinkAsync(zipPath);
}
}
const revisionInfo = this.revisionInfo;
if (revisionInfo) {
await chmodAsync(revisionInfo.executablePath, 0o755);
}
return revisionInfo;
}

/**
* @param {string} revision
* @return {!Promise}
*/
async remove(revision) {
const folderPath = this._getFolderPath(revision);
assert(
await existsAsync(folderPath),
`Failed to remove: revision ${revision} is not downloaded`,
);
fs.removeSync(folderPath);
}

/**
* @param {string} revision
* @return {string}
*/
_getFolderPath(revision) {
return path.join(this._downloadsFolder, this._platform + '-' + revision);
}
}

module.exports = BrowserFetcher;

/**
* @param {string} url
* @param {string} destinationPath
* @param {?function(number, number)} progressCallback
* @return {!Promise}
*/
function downloadFile(url, destinationPath, progressCallback) {
let fulfill, reject;
let downloadedBytes = 0;
let totalBytes = 0;

const promise = new Promise((x, y) => {
fulfill = x;
reject = y;
});

const request = httpRequest(url, 'GET', (response) => {
if (response.statusCode !== 200) {
const error = new Error(
`Download failed: server returned code ${response.statusCode}. URL: ${url}`,
);
// consume response data to free up memory
response.resume();
reject(error);
return;
}
const file = fs.createWriteStream(destinationPath);
file.on('finish', () => fulfill());
file.on('error', (error) => reject(error));
response.pipe(file);
totalBytes = parseInt(/** @type {string} */ (response.headers['content-length']), 10);
if (progressCallback) {
response.on('data', onData);
}
});
request.on('error', (error) => reject(error));
return promise;

function onData(chunk) {
downloadedBytes += chunk.length;
progressCallback(downloadedBytes, totalBytes);
}
}

/**
* @param {string} zipPath
* @param {string} folderPath
* @return {!Promise<?Error>}
*/
function extractZip(zipPath, folderPath) {
return new Promise((fulfill) => extract(zipPath, { dir: folderPath }, fulfill));
}

function httpRequest(url, method, response) {
/** @type {Object} */
const options = URL.parse(url);
options.method = method;

const proxyURL = getProxyForUrl(url);
if (proxyURL) {
/** @type {Object} */
const parsedProxyURL = URL.parse(proxyURL);
parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:';

options.agent = new ProxyAgent(parsedProxyURL);
options.rejectUnauthorized = false;
}

const driver = options.protocol === 'https:' ? 'https' : 'http';
const request = require(driver).request(options, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
httpRequest(res.headers.location, method, response);
} else {
response(res);
}
});
request.end();
return request;
}
15 changes: 6 additions & 9 deletions lib/browserLauncher.js → lib/browser/launcher.js
Original file line number Diff line number Diff line change
@@ -2,8 +2,8 @@ const path = require('path');
const fs = require('fs-extra');
const os = require('os');
const childProcess = require('child_process');
const { setBrowserOptions, defaultConfig } = require('./config');
const { eventHandler } = require('./eventBus');
const { setBrowserOptions, defaultConfig } = require('../config');
const { eventHandler } = require('../eventBus');
const util = require('util');
const mkdtempAsync = util.promisify(fs.mkdtemp);
const writeFileAsync = util.promisify(fs.writeFile);
@@ -322,9 +322,9 @@ const launchBrowser = async (options) => {
if (browserProcess && !browserProcess.killed) {
throw new Error('openBrowser cannot be called again as there is a browser instance open.');
}
const BrowserFetcher = require('./browserFetcher');
const browserFetcher = new BrowserFetcher();
const browserExecutable = browserFetcher.getExecutablePath();
const Browser = require('./browser');
const browser = new Browser();
const browserExecutable = browser.getExecutablePath();
options = setBrowserOptions(options);
let args = await setBrowserArgs(options);
browserProcess = await childProcess.spawn(browserExecutable, args);
@@ -333,10 +333,7 @@ const launchBrowser = async (options) => {
browserProcess.stdout.pipe(process.stdout);
}
browserProcess.once('exit', browserExitEventHandler);
const endpoint = await browserFetcher.waitForWSEndpoint(
browserProcess,
defaultConfig.navigationTimeout,
);
const endpoint = await browser.waitForWSEndpoint(browserProcess, defaultConfig.navigationTimeout);
return {
currentHost: endpoint.host,
currentPort: endpoint.port,
160 changes: 160 additions & 0 deletions lib/browser/metadata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* Copyright 2018 Thoughtworks Inc. All rights reserved.
*
* 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.
*/
/**
* This module is imported from Puppeteer(https://github.com/GoogleChrome/puppeteer)
* Few modifications are done on the file.
*/

const os = require('os');
const fs = require('fs-extra');
const path = require('path');
const util = require('util');
const { helper, assert } = require('../helper');
const supportedPlatforms = ['mac-arm64', 'mac-x64', 'linux64', 'win32', 'win64'];

const readdirAsync = util.promisify(fs.readdir.bind(fs));
const browser = require('../../package.json').taiko.browser;

function existsAsync(filePath) {
let fulfill = null;
const promise = new Promise((x) => (fulfill = x));
fs.access(filePath, (err) => fulfill(!err));
return promise;
}

class BrowserMetadata {
constructor() {
this._downloadsFolder = path.join(helper.projectRoot(), '.local-chromium');
const platform = os.platform();
if (platform === 'darwin') {
this._platform = os.arch() === 'arm64' ? 'mac-arm64' : 'mac-x64';
} else if (platform === 'linux') {
this._platform = 'linux64';
} else if (platform === 'win32') {
this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
}
assert(this._platform, 'Unsupported platform: ' + os.platform());
assert(supportedPlatforms.includes(this._platform), 'Unsupported platform: ' + this._platform);
const download = browser.downloads.chrome.find(
(download) => download.platform === this._platform,
);
this.downloadURL = download.url;
this.revision = browser.revision;
}

/**
* Returns the platform of the browser.
* @return {string} The platform.
*/
platform() {
return this._platform;
}

/**
* Determines the archive name based on the platform and revision number.
* @return {string} The archive name.
*/
archiveName() {
if (this._platform === 'win32' || this.platform === 'win64') {
// Windows archive name changed at r591479.
return parseInt(this.revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
} else {
return `chrome-${this._platform}`;
}
}

/**
* @return {!Promise<!Array<string>>}
*/
async localRevisions() {
if (!(await existsAsync(this._downloadsFolder))) {
return [];
}
const fileNames = await readdirAsync(this._downloadsFolder);
return fileNames
.map((fileName) => parseFolderPath(fileName))
.filter((entry) => entry && entry.platform === this._platform)
.map((entry) => entry.revision);
}

/**
* @return {!BrowserMetadata.RevisionInfo}
*/
revisionInfo() {
const folderPath = this._getFolderPath(this.revision);
let executablePath = '';
if (this._platform.includes('mac')) {
executablePath = path.join(
folderPath,
this.archiveName(),
'Google Chrome for Testing.app',
'Contents',
'MacOS',
'Google Chrome for Testing',
);
} else if (this._platform === 'linux64') {
executablePath = path.join(folderPath, this.archiveName(), 'chrome');
} else if (this._platform === 'win32' || this._platform === 'win64') {
executablePath = path.join(folderPath, this.archiveName(), 'chrome.exe');
} else {
throw 'Unsupported platform: ' + this._platform;
}
const local = fs.existsSync(folderPath);
return {
revision: this.revision,
executablePath,
folderPath,
local,
url: this.downloadURL,
};
}

/**
* @param {string} revision
* @return {string}
*/
_getFolderPath() {
return path.join(this._downloadsFolder, this._platform + '-' + this.revision);
}
}

module.exports = BrowserMetadata;

/**
* @param {string} folderPath
* @return {?{platform: string, revision: string}}
*/
function parseFolderPath(folderPath) {
const name = path.basename(folderPath);
const splits = name.split('-');
if (splits.length !== 2) {
return null;
}
const [platform, revision] = splits;
if (!supportedPlatforms.includes(platform)) {
return null;
}
return { platform, revision };
}

/**
* @typedef {Object} BrowserMetadata.RevisionInfo
* @property {string} folderPath
* @property {string} executablePath
* @property {string} url
* @property {boolean} local
* @property {string} revision
*/
424 changes: 0 additions & 424 deletions lib/browserFetcher.js

This file was deleted.

5 changes: 2 additions & 3 deletions lib/connection.js
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ const { defaultConfig } = require('./config');
const { eventHandler } = require('./eventBus');
const { isPromise } = require('./helper');
const { logEvent } = require('./logger');
const { errorMessageForBrowserProcessCrash, closeBrowser } = require('./browserLauncher');
const { errorMessageForBrowserProcessCrash, closeBrowser } = require('./browser/launcher');
const targetHandler = require('./handlers/targetHandler');
const pageHandler = require('./handlers/pageHandler');
const emulationHandler = require('./handlers/emulationHandler');
@@ -131,8 +131,7 @@ const closeConnection = async (promisesToBeResolvedBeforeCloseBrowser) => {
// remove listeners other than JS dialog for beforeUnload on client first to stop executing them when closing
await _client.removeAllListeners();
pageHandler.addJavascriptDialogOpeningListener();
//TODO: Remove check once fixed https://bugs.chromium.org/p/chromium/issues/detail?id=1147809
if (!defaultConfig.firefox && !(process.platform == 'win32' && defaultConfig.headful)) {
if (!defaultConfig.firefox) {
await pageHandler.closePage();

await new Promise((resolve) => {
16 changes: 9 additions & 7 deletions lib/install.js
Original file line number Diff line number Diff line change
@@ -17,11 +17,11 @@
* This module is imported from Puppeteer(https://github.com/GoogleChrome/puppeteer)
* Few modifications are done on the file.
*/
const BrowserFetcher = require('./browserFetcher');
const revision = require('../package.json').taiko.chromium_revision;
const downloadHost = process.env.TAIKO_CHROMIUM_URL;
const browserFetcher = new BrowserFetcher({ host: downloadHost });
const revisionInfo = browserFetcher.revisionInfo(revision);
const BrowserFetcher = require('./browser/fetcher');
const BrowserMetadata = require('./browser/metadata');
const browser = require('../package.json').taiko.browser;
const browserFetcher = new BrowserFetcher(browser);
const revisionInfo = new BrowserMetadata().revisionInfo();

let progressBar = null;
let lastDownloadedBytes = 0;
@@ -43,7 +43,7 @@ function onSuccess(localRevisions) {
*/
function onError(error) {
console.error(
`ERROR: Failed to download Chromium r${revision}! Set "TAIKO_SKIP_CHROMIUM_DOWNLOAD" env variable to skip download.`,
`ERROR: Failed to download Chromium r${browser.revision}! Set "TAIKO_SKIP_CHROMIUM_DOWNLOAD" env variable to skip download.`,
);
console.error(error);
process.exit(1);
@@ -53,7 +53,9 @@ function onProgress(downloadedBytes, totalBytes) {
if (!progressBar) {
const ProgressBar = require('progress');
progressBar = new ProgressBar(
`Downloading Chromium r${revision} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `,
`Downloading Chromium r${browser.revision} - ${toMegabytes(
totalBytes,
)} [:bar] :percent :etas `,
{
complete: '=',
incomplete: ' ',
2 changes: 1 addition & 1 deletion lib/taiko.js
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ const childProcess = require('child_process');
const crypto = require('crypto');
const { eventHandler, eventRegexMap } = require('./eventBus');
const { highlightElement } = require('./elements/elementHelper');
const { launchBrowser } = require('./browserLauncher');
const { launchBrowser } = require('./browser/launcher');
const {
connect_to_cri,
closeConnection,
30,928 changes: 10,938 additions & 19,990 deletions package-lock.json

Large diffs are not rendered by default.

40 changes: 33 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "http://json.schemastore.org/package",
"name": "taiko",
"version": "1.3.10",
"version": "1.4.0",
"description": "Taiko is a Node.js library for automating Chromium based browsers",
"main": "bin/taiko.js",
"bin": {
@@ -17,8 +17,8 @@
"doc:api": "node lib/documentation.js",
"test:api": "node test/unit-tests/taiko-test.js",
"examples": "cd examples && npm install && npm test",
"test:unit:silent": "mocha 'test/unit-tests/**/*.test.js' --timeout 6000 -R dot --trace-warnings",
"test:unit": "mocha 'test/unit-tests/**/*.test.js' --timeout 6000 --trace-warnings",
"test:unit:silent": "mocha 'test/unit-tests/**/*.test.js' --timeout 9000 -R dot --trace-warnings --exit",
"test:unit": "mocha 'test/unit-tests/**/*.test.js' --timeout 9000 --trace-warnings --exit",
"test": "npm run test:api && npm run test:unit:silent",
"test-functional": "npm install && cd test/functional-tests && npm install && npm test",
"test-docs": "cd test/docs-tests && node prepare.js && eleventy && cd ./gauge && npm install && npm test",
@@ -43,8 +43,34 @@
]
},
"taiko": {
"chromium_revision": "1083080",
"chromium_version": "110.0.5478.0"
"browser": {
"version": "126.0.6468.0",
"revision": "1298436",
"downloads": {
"chrome": [
{
"platform": "linux64",
"url": "https://storage.googleapis.com/chrome-for-testing-public/126.0.6468.0/linux64/chrome-linux64.zip"
},
{
"platform": "mac-arm64",
"url": "https://storage.googleapis.com/chrome-for-testing-public/126.0.6468.0/mac-arm64/chrome-mac-arm64.zip"
},
{
"platform": "mac-x64",
"url": "https://storage.googleapis.com/chrome-for-testing-public/126.0.6468.0/mac-x64/chrome-mac-x64.zip"
},
{
"platform": "win32",
"url": "https://storage.googleapis.com/chrome-for-testing-public/126.0.6468.0/win32/chrome-win32.zip"
},
{
"platform": "win64",
"url": "https://storage.googleapis.com/chrome-for-testing-public/126.0.6468.0/win64/chrome-win64.zip"
}
]
}
}
},
"husky": {
"hooks": {
@@ -56,7 +82,7 @@
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.20.7",
"chrome-remote-interface": "^0.31.3",
"chrome-remote-interface": "^0.33.0",
"commander": "^9.5.0",
"debug": "^4.3.4",
"devtools-protocol": "0.0.1082910",
@@ -93,7 +119,7 @@
"lint-staged": "^13.1.0",
"markdown-it": "^13.0.1",
"markdown-it-anchor": "^8.6.6",
"mocha": "^10.2.0",
"mocha": "^10.4.0",
"ncp": "^2.0.0",
"prettier": "^2.8.2",
"rewire": "^6.0.0",
79 changes: 0 additions & 79 deletions scripts/updateChromium.js

This file was deleted.

88 changes: 88 additions & 0 deletions scripts/updateChromium.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { writeFileSync, readFileSync } from 'fs';
import { execSync } from 'child_process';
import path from 'path';

const PACKAGE_JSON_PATH = path.join(__dirname, '..', 'package.json');

interface ChromeDownload {
platform: string;
url: string;
}

interface Downloads {
chrome: ChromeDownload[];
}

interface ChromeReleaseInfo {
version: string;
revision: string;
downloads: Downloads;
}

class ChromeUpdater {
private static readPackageJSON(): any {
return JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf-8'));
}

private static writePackageJSON(content: any): void {
writeFileSync(PACKAGE_JSON_PATH, `${JSON.stringify(content, null, 2)}\n`);
}

public static async updateChromeVersion(): Promise<void> {
try {
const latestVersion = await this.fetchLatestChromeVersion();
const currentVersion = this.getCurrentChromeVersion();

if (!currentVersion) {
this.updatePackageJSON(latestVersion);
return;
}

if (currentVersion.revision < latestVersion.revision) {
console.log(`Updating to latest chrome version: ${latestVersion.version}`);
this.updatePackageJSON(latestVersion);
execSync(`node ${__dirname}/../lib/install.js`);
} else {
console.log(
`Current chrome version (${currentVersion.revision}) is up to date with the latest version (${latestVersion.revision}).`,
);
}
} catch (error) {
console.error(`Error updating chrome version: ${error}`);
}
}

private static getCurrentChromeVersion(): ChromeReleaseInfo {
const packageJSON = this.readPackageJSON();
return packageJSON.taiko.browser as ChromeReleaseInfo;
}

private static updatePackageJSON(releaseInfo: ChromeReleaseInfo): void {
const packageJSON = this.readPackageJSON();
const filteredReleaseInfo: ChromeReleaseInfo = {
version: releaseInfo.version,
revision: releaseInfo.revision,
downloads: { chrome: releaseInfo.downloads.chrome },
};
packageJSON.taiko.browser = filteredReleaseInfo;
this.writePackageJSON(packageJSON);
}

private static async fetchLatestChromeVersion(): Promise<ChromeReleaseInfo> {
const response = await fetch(
'https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json',
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const versions = data.versions as ChromeReleaseInfo[];
return versions.slice(-1)[0] as ChromeReleaseInfo;
}
}

async function main() {
await ChromeUpdater.updateChromeVersion();
}

main();
2 changes: 1 addition & 1 deletion test/docs-tests/gauge/package.json
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
},
"dependencies": {
"@getgauge/cli": "latest",
"gauge-ts": "0.1.0",
"gauge-ts": "0.1.1",
"taiko": "file:../../.."
},
"devDependencies": {
42 changes: 20 additions & 22 deletions test/docs-tests/gauge/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@

{
"compilerOptions": {
/* Basic Options */
"target": "es6",
/* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
"module": "commonjs",
/* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": ["es2016", "DOM"], /* Specify library files to be included in the compilation: */
/* Module Resolution Options */
"moduleResolution": "node",
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* add experimental taiko TypeScript Type Definition folder to the project */
"typeRoots": ["node_modules/@types", "node_modules/taiko/types"],
/* use taiko types in the project */
"types": ["node", "taiko"]
},
"exclude": [
"node_modules"
]
}
"compilerOptions": {
"baseUrl": "./",
/* Basic Options */
"target": "es6",
/* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
"module": "commonjs",
/* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": ["es2016", "DOM"] /* Specify library files to be included in the compilation: */,
/* Module Resolution Options */
"moduleResolution": "node",
/* Experimental Options */
"experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
"emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
/* add experimental taiko TypeScript Type Definition folder to the project */
"typeRoots": ["node_modules/@types", "node_modules/taiko/types"],
/* use taiko types in the project */
"types": ["node", "taiko"]
},
"exclude": ["node_modules"]
}
2 changes: 1 addition & 1 deletion test/functional-tests/Dockerfile
Original file line number Diff line number Diff line change
@@ -2,4 +2,4 @@ FROM getgauge/taiko

ADD . /gauge

CMD npm install && npm test
CMD npm ci && npm test
1,939 changes: 1,939 additions & 0 deletions test/functional-tests/package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions test/functional-tests/package.json
Original file line number Diff line number Diff line change
@@ -13,8 +13,10 @@
},
"dependencies": {
"@getgauge/cli": "latest",
"gauge-ts": "0.1.0",
"taiko": "file:../.."
"gauge-ts": "0.1.1",
"taiko": "file:../..",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
},
"devDependencies": {
"@types/node": "latest",
42 changes: 20 additions & 22 deletions test/functional-tests/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@

{
"compilerOptions": {
/* Basic Options */
"target": "es6",
/* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
"module": "commonjs",
/* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": ["es2016", "DOM"], /* Specify library files to be included in the compilation: */
/* Module Resolution Options */
"moduleResolution": "node",
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* add experimental taiko TypeScript Type Definition folder to the project */
"typeRoots": ["node_modules/@types", "node_modules/taiko/types"],
/* use taiko types in the project */
"types": ["node", "taiko"]
},
"exclude": [
"node_modules"
]
}
"compilerOptions": {
"baseUrl": ".",
/* Basic Options */
"target": "es6",
/* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
"module": "commonjs",
/* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": ["es2016", "DOM"] /* Specify library files to be included in the compilation: */,
/* Module Resolution Options */
"moduleResolution": "node",
/* Experimental Options */
"experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
"emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
/* add experimental taiko TypeScript Type Definition folder to the project */
"typeRoots": ["node_modules/@types", "node_modules/taiko/types"],
/* use taiko types in the project */
"types": ["node", "taiko"]
},
"exclude": ["node_modules"]
}
4 changes: 3 additions & 1 deletion test/unit-tests/beforeunload.test.js
Original file line number Diff line number Diff line change
@@ -29,7 +29,9 @@ describe(test_name, () => {
window.addEventListener('beforeunload', function (e) {
let newValue = document.getElementById('name').value;
if( oldValue != newValue) {
e.returnValue = '';
event.preventDefault();
// Legacy support for older browsers.
return (event.returnValue = true);
}
});
</script>
6 changes: 3 additions & 3 deletions test/unit-tests/browserLauncher.test.js
Original file line number Diff line number Diff line change
@@ -8,10 +8,10 @@ const { eventHandler } = require('../../lib/eventBus');
describe('OpenBrowser', () => {
let browserLauncher;
before(() => {
browserLauncher = rewire('../../lib/browserLauncher');
browserLauncher = rewire('../../lib/browser/launcher');
});
after(() => {
browserLauncher = rewire('../../lib/browserLauncher');
browserLauncher = rewire('../../lib/browser/launcher');
});

describe('should set args', async () => {
@@ -40,7 +40,7 @@ describe('OpenBrowser', () => {
});

afterEach(() => {
browserLauncher = rewire('../../lib/browserLauncher');
browserLauncher = rewire('../../lib/browser/launcher');
});

it('should emit browserCrashed event when chrome process crashes', async () => {
3 changes: 0 additions & 3 deletions test/unit-tests/closeBrowser.test.js
Original file line number Diff line number Diff line change
@@ -9,9 +9,6 @@ xdescribe('close browser successfully', () => {
});

it("closeBrowser should return 'Browser closed' message", async () => {
// emitter.on('success', (desc) => {
// expect(desc).to.equal('Browser closed');
// });
await closeBrowser().then((data) => {
expect(data).to.equal(undefined);
});