feat(bazel): protractor_web_test_suite for release (#24787)

PR Close #24787
This commit is contained in:
Greg Magolan
2018-07-06 14:51:37 -07:00
committed by Matias Niemelä
parent 71e0df039c
commit 161ff5c79d
35 changed files with 924 additions and 311 deletions

View File

@ -5,6 +5,7 @@ filegroup(
srcs = glob(["*"]) + [
"//packages/bazel/src/ng_package:package_assets",
"//packages/bazel/src/ngc-wrapped:package_assets",
"//packages/bazel/src/protractor:package_assets",
],
visibility = ["//packages/bazel:__subpackages__"],
)

View File

@ -1,4 +1,13 @@
package(default_visibility = ["//visibility:public"])
filegroup(
name = "package_assets",
srcs = glob(["*"]) + [
"//packages/bazel/src/protractor/utils:package_assets",
],
visibility = ["//packages/bazel:__subpackages__"],
)
exports_files([
"conf.js.tmpl",
"protractor_runner.js",
"protractor.conf.js",
])

View File

@ -1,14 +0,0 @@
const baseConf = require("BASE_CONF_IMPORT_PATH");
exports.config = {
...baseConf.config,
framework: "jasmine2",
seleniumAddress: process.env.WEB_TEST_HTTP_SERVER.trim() + "/wd/hub",
specs: [
SPEC_FILE_IMPORT_PATHS
].map(specPath => require.resolve(specPath)),
// TODO: Allow users to specifify other browsers.
capabilities: {
browserName: "chrome"
},
};

View File

@ -0,0 +1,154 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const path = require('path');
const DEBUG = false;
const configPath = 'TMPL_config';
const onPreparePath = 'TMPL_on_prepare';
const workspace = 'TMPL_workspace';
const server = 'TMPL_server';
if (DEBUG)
console.info(`Protractor test starting with:
cwd: ${process.cwd()}
configPath: ${configPath}
onPreparePath: ${onPreparePath}
workspace: ${workspace}
server: ${server}`);
// Helper function to warn when a user specified value is being overwritten
function setConf(conf, name, value, msg) {
if (conf[name] && conf[name] !== value) {
console.warn(
`Your protractor configuration specifies an option which is overwritten by Bazel: '${name}' ${msg}`);
}
conf[name] = value;
}
let conf = {};
// Import the user's base protractor configuration if specified
if (configPath) {
const baseConf = require(configPath);
if (!baseConf.config) {
throw new Error('Invalid base protractor configration. Expected config to be exported.');
}
conf = baseConf.config;
}
// Import the user's on prepare function if specified
if (onPreparePath) {
const onPrepare = require(onPreparePath);
if (typeof onPrepare === 'function') {
const original = conf.onPrepare;
conf.onPrepare = function() {
return Promise.resolve(original ? original() : null)
.then(() => Promise.resolve(onPrepare({workspace, server})));
};
} else {
throw new Error(
'Invalid protractor on_prepare script. Expected a function as the default export.');
}
}
// Override the user's base protractor configuration as appropriate based on the
// ts_web_test_suite & rules_webtesting WEB_TEST_METADATA attributes
setConf(conf, 'framework', 'jasmine2', 'is set to jasmine2');
const specs = [TMPL_specs]
.map(s => require.resolve(s))
.filter(s => s.endsWith('.spec.js') || s.endsWith('.test.js'));
setConf(conf, 'specs', specs, 'are determined by the srcs and deps attribute');
// WEB_TEST_METADATA is configured in rules_webtesting based on value
// of the browsers attribute passed to ts_web_test_suite
// We setup the protractor configuration based on the values in this object
if (process.env['WEB_TEST_METADATA']) {
const webTestMetadata = require(process.env['WEB_TEST_METADATA']);
if (DEBUG) console.info(`WEB_TEST_METADATA: ${JSON.stringify(webTestMetadata, null, 2)}`);
if (webTestMetadata['environment'] === 'sauce') {
// If a sauce labs browser is chosen for the test such as
// "@io_bazel_rules_webtesting//browsers/sauce:chrome-win10"
// than the 'environment' will equal 'sauce'.
// We expect that a SAUCE_USERNAME and SAUCE_ACCESS_KEY is available
// from the environment for this test to run
// TODO(gmagolan): implement sauce labs support for protractor
throw new Error('Saucelabs not yet support by protractor_web_test_suite.');
// if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) {
// console.error('Make sure the SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are
// set.');
// process.exit(1);
// }
// setConf(conf, 'sauceUser', process.env.SAUCE_USERNAME, 'is determined by the SAUCE_USERNAME
// environment variable');
// setConf(conf, 'sauceKey', process.env.SAUCE_ACCESS_KEY, 'is determined by the
// SAUCE_ACCESS_KEY environment variable');
} else if (webTestMetadata['environment'] === 'local') {
// When a local chrome or firefox browser is chosen such as
// "@io_bazel_rules_webtesting//browsers:chromium-local" or
// "@io_bazel_rules_webtesting//browsers:firefox-local"
// then the 'environment' will equal 'local' and
// 'webTestFiles' will contain the path to the binary to use
const webTestNamedFiles = webTestMetadata['webTestFiles'][0]['namedFiles'];
const headless = !process.env['DISPLAY'];
if (webTestNamedFiles['CHROMIUM']) {
const chromeBin = path.join(process.cwd(), 'external', webTestNamedFiles['CHROMIUM']);
const args = [];
if (headless) {
args.push('--headless');
args.push('--disable-gpu');
}
setConf(conf, 'directConnect', true, 'is set to true for chrome');
setConf(
conf, 'chromeDriver',
path.join(process.cwd(), 'external', webTestNamedFiles['CHROMEDRIVER']),
'is determined by the browsers attribute');
setConf(
conf, 'capabilities', {
browserName: 'chrome',
chromeOptions: {
binary: chromeBin,
args: args,
}
},
'is determined by the browsers attribute');
}
if (webTestNamedFiles['FIREFOX']) {
// TODO(gmagolan): implement firefox support for protractor
throw new Error('Firefox not yet support by protractor_web_test_suite');
// const firefoxBin = path.join('external', webTestNamedFiles['FIREFOX']);
// const args = [];
// if (headless) {
// args.push("--headless")
// args.push("--marionette")
// }
// setConf(conf, 'seleniumAddress', process.env.WEB_TEST_HTTP_SERVER.trim() + "/wd/hub", 'is
// configured by Bazel for firefox browser')
// setConf(conf, 'capabilities', {
// browserName: "firefox",
// 'moz:firefoxOptions': {
// binary: firefoxBin,
// args: args,
// }
// }, 'is determined by the browsers attribute');
}
} else {
console.warn(`Unknown WEB_TEST_METADATA environment '${webTestMetadata['environment']}'`);
}
}
// Export the complete protractor configuration
if (DEBUG) console.info(`Protractor configuration: ${JSON.stringify(conf, null, 2)}`);
exports.config = conf;

View File

@ -1,23 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*
* @fileoverview A wrapper around the protractor cli for bazel compatibility.
*/
const launcher = require('protractor/built/launcher');
function main(args) {
if (!args.length) {
throw new Error('Config file argument missing');
}
const config = require.resolve(args[0]);
launcher.init(config);
}
if (require.main === module) {
process.exitCode = main(process.argv.slice(2));
}

View File

@ -0,0 +1,341 @@
# Copyright Google Inc. All Rights Reserved.
#
# Use of this source code is governed by an MIT-style license that can be
# found in the LICENSE file at https://angular.io/license
"""Implementation of the protractor_web_test and protractor_web_test_suite rules.
"""
load("@build_bazel_rules_nodejs//internal:node.bzl",
"sources_aspect",
"expand_path_into_runfiles",
)
load("@io_bazel_rules_webtesting//web:web.bzl", "web_test_suite")
load("@io_bazel_rules_webtesting//web/internal:constants.bzl", "DEFAULT_WRAPPED_TEST_TAGS")
load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary")
_CONF_TMPL = "//packages/bazel/src/protractor:protractor.conf.js"
def _protractor_web_test_impl(ctx):
configuration = ctx.actions.declare_file(
"%s.conf.js" % ctx.label.name,
sibling=ctx.outputs.executable)
files = depset(ctx.files.srcs)
for d in ctx.attr.deps:
if hasattr(d, "node_sources"):
files = depset(transitive = [files, d.node_sources])
elif hasattr(d, "files"):
files = depset(transitive = [files, d.files])
specs = [
expand_path_into_runfiles(ctx, f.short_path)
for f in files
]
configuration_sources = []
if ctx.file.configuration:
configuration_sources = [ctx.file.configuration]
if hasattr(ctx.attr.configuration, "node_sources"):
configuration_sources = ctx.attr.configuration.node_sources.to_list()
configuration_file = ctx.file.configuration
if hasattr(ctx.attr.configuration, "typescript"):
configuration_file = ctx.attr.configuration.typescript.es5_sources.to_list()[0]
on_prepare_sources = []
if ctx.file.on_prepare:
on_prepare_sources = [ctx.file.on_prepare]
if hasattr(ctx.attr.on_prepare, "node_sources"):
on_prepare_sources = ctx.attr.on_prepare.node_sources.to_list()
on_prepare_file = ctx.file.on_prepare
if hasattr(ctx.attr.on_prepare, "typescript"):
on_prepare_file = ctx.attr.on_prepare.typescript.es5_sources.to_list()[0]
protractor_executable_path = ctx.executable.protractor.short_path
if protractor_executable_path.startswith('..'):
protractor_executable_path = "external" + protractor_executable_path[2:]
server_executable_path = ''
if ctx.executable.server:
server_executable_path = ctx.executable.server.short_path
if server_executable_path.startswith('..'):
server_executable_path = "external" + protractor_executable_path[2:]
ctx.actions.expand_template(
output = configuration,
template = ctx.file._conf_tmpl,
substitutions = {
"TMPL_config": expand_path_into_runfiles(ctx, configuration_file.short_path) if configuration_file else "",
"TMPL_on_prepare": expand_path_into_runfiles(ctx, on_prepare_file.short_path) if on_prepare_file else "",
"TMPL_workspace": ctx.workspace_name,
"TMPL_server": server_executable_path,
"TMPL_specs": "\n".join([" '%s'," % e for e in specs]),
})
runfiles = [configuration] + configuration_sources + on_prepare_sources
ctx.actions.write(
output = ctx.outputs.executable,
is_executable = True,
content = """#!/usr/bin/env bash
if [ -e "$RUNFILE_MANIFEST_FILE" ]; then
while read line; do
declare -a PARTS=($line)
if [ "${{PARTS[0]}}" == "angular/{TMPL_protractor}" ]; then
readonly PROTRACTOR=${{PARTS[1]}}
elif [ "${{PARTS[0]}}" == "angular/{TMPL_conf}" ]; then
readonly CONF=${{PARTS[1]}}
fi
done < $RUNFILE_MANIFEST_FILE
else
readonly PROTRACTOR={TMPL_protractor}
readonly CONF={TMPL_conf}
fi
export HOME=$(mktemp -d)
# Print the protractor version in the test log
PROTRACTOR_VERSION=$($PROTRACTOR --version)
echo "Protractor $PROTRACTOR_VERSION"
# Run the protractor binary
$PROTRACTOR $CONF
""".format(TMPL_protractor = protractor_executable_path,
TMPL_conf = configuration.short_path))
return [DefaultInfo(
files = depset([ctx.outputs.executable]),
runfiles = ctx.runfiles(
files = runfiles,
transitive_files = files,
# Propagate protractor_bin and its runfiles
collect_data = True,
collect_default = True,
),
executable = ctx.outputs.executable,
)]
_protractor_web_test = rule(
implementation = _protractor_web_test_impl,
test = True,
executable = True,
attrs = {
"configuration": attr.label(
doc = "Protractor configuration file",
allow_single_file = True,
cfg = "data",
aspects = [sources_aspect]),
"srcs": attr.label_list(
doc = "A list of JavaScript test files",
allow_files = [".js"]),
"on_prepare": attr.label(
doc = """A file with a node.js script to run once before all tests run.
If the script exports a function which returns a promise, protractor
will wait for the promise to resolve before beginning tests.""",
allow_single_file = True,
cfg = "data",
aspects = [sources_aspect]),
"deps": attr.label_list(
doc = "Other targets which produce JavaScript such as `ts_library`",
allow_files = True,
aspects = [sources_aspect]),
"data": attr.label_list(
doc = "Runtime dependencies",
cfg = "data"),
"server": attr.label(
doc = "Optional server executable target",
executable = True,
cfg = "data",
single_file = False,
allow_files = True),
"protractor": attr.label(
doc = "Protractor executable target (set by protractor_web_test macro)",
executable = True,
cfg = "data",
single_file = False,
allow_files = True),
"_conf_tmpl": attr.label(
default = Label(_CONF_TMPL),
allow_single_file = True,
),
},
)
def protractor_web_test(
name,
configuration = None,
on_prepare = None,
srcs = [],
deps = [],
data = [],
server = None,
tags = [],
**kwargs):
"""Runs a protractor test in a browser.
Args:
name: The name of the test
configuration: Protractor configuration file.
on_prepare: A file with a node.js script to run once before all tests run.
If the script exports a function which returns a promise, protractor
will wait for the promise to resolve before beginning tests.
srcs: JavaScript source files
deps: Other targets which produce JavaScript such as `ts_library`
data: Runtime dependencies
server: Optional server executable target
tags: Standard Bazel tags, this macro adds one for ibazel
**kwargs: passed through to `_protractor_web_test`
"""
protractor_bin_name = name + "_protractor_bin"
nodejs_binary(
name = protractor_bin_name,
entry_point = "protractor/bin/protractor",
data = srcs + deps + data,
node_modules = "@//:node_modules",
testonly = 1,
visibility = ["//visibility:private"],
)
# Our binary dependency must be in data[] for collect_data to pick it up
# FIXME: maybe we can just ask :protractor_bin_name for its runfiles attr
web_test_data = data + [":" + protractor_bin_name]
if server:
web_test_data += [server]
_protractor_web_test(
name = name,
configuration = configuration,
on_prepare=on_prepare,
srcs = srcs,
deps = deps,
data = web_test_data,
server = server,
protractor = protractor_bin_name,
tags = tags + [
# Users don't need to know that this tag is required to run under ibazel
"ibazel_notify_changes",
],
**kwargs)
def protractor_web_test_suite(
name,
configuration = None,
on_prepare = None,
srcs = [],
deps = [],
data = [],
server = None,
browsers=["@io_bazel_rules_webtesting//browsers:chromium-local"],
args=None,
browser_overrides=None,
config=None,
flaky=None,
local=None,
shard_count=None,
size=None,
tags = [],
test_suite_tags=None,
timeout=None,
visibility=None,
web_test_data=[],
wrapped_test_tags=None,
**remaining_keyword_args):
"""Defines a test_suite of web_test targets that wrap a protractor_web_test target.
Args:
name: The base name of the test.
configuration: Protractor configuration file.
on_prepare: A file with a node.js script to run once before all tests run.
If the script exports a function which returns a promise, protractor
will wait for the promise to resolve before beginning tests.
srcs: JavaScript source files
deps: Other targets which produce JavaScript such as `ts_library`
data: Runtime dependencies
server: Optional server executable target
browsers: A sequence of labels specifying the browsers to use.
args: Args for web_test targets generated by this extension.
browser_overrides: Dictionary; optional; default is an empty dictionary. A
dictionary mapping from browser names to browser-specific web_test
attributes, such as shard_count, flakiness, timeout, etc. For example:
{'//browsers:chrome-native': {'shard_count': 3, 'flaky': 1}
'//browsers:firefox-native': {'shard_count': 1, 'timeout': 100}}.
config: Label; optional; Configuration of web test features.
flaky: A boolean specifying that the test is flaky. If set, the test will
be retried up to 3 times (default: 0)
local: boolean; optional.
shard_count: The number of test shards to use per browser. (default: 1)
size: A string specifying the test size. (default: 'large')
tags: A list of test tag strings to apply to each generated web_test target.
This macro adds a couple for ibazel.
test_suite_tags: A list of tag strings for the generated test_suite.
timeout: A string specifying the test timeout (default: computed from size)
visibility: List of labels; optional.
web_test_data: Data dependencies for the web_test.
wrapped_test_tags: A list of test tag strings to use for the wrapped test
**remaining_keyword_args: Arguments for the wrapped test target.
"""
# Check explicitly for None so that users can set this to the empty list
if wrapped_test_tags == None:
wrapped_test_tags = DEFAULT_WRAPPED_TEST_TAGS
size = size or "large"
wrapped_test_name = name + "_wrapped_test"
protractor_bin_name = name + "_protractor_bin"
# Users don't need to know that this tag is required to run under ibazel
tags = tags + ["ibazel_notify_changes"]
nodejs_binary(
name = protractor_bin_name,
entry_point = "protractor/bin/protractor",
data = srcs + deps + data,
node_modules = "@//:node_modules",
testonly = 1,
visibility = ["//visibility:private"],
)
# Our binary dependency must be in data[] for collect_data to pick it up
# FIXME: maybe we can just ask the :protractor_bin_name for its runfiles attr
web_test_data = web_test_data + [":" + protractor_bin_name]
if server:
web_test_data += [server]
_protractor_web_test(
name=wrapped_test_name,
configuration=configuration,
on_prepare=on_prepare,
srcs=srcs,
deps=deps,
data=web_test_data,
server=server,
protractor=protractor_bin_name,
args=args,
flaky=flaky,
local=local,
shard_count=shard_count,
size=size,
tags=wrapped_test_tags,
timeout=timeout,
visibility=["//visibility:private"],
**remaining_keyword_args)
web_test_suite(
name=name,
launcher=":"+wrapped_test_name,
args=args,
browsers=browsers,
browser_overrides=browser_overrides,
config=config,
data=web_test_data,
flaky=flaky,
local=local,
shard_count=shard_count,
size=size,
tags=tags,
test=wrapped_test_name,
test_suite_tags=test_suite_tags,
timeout=timeout,
visibility=visibility)

View File

@ -1,107 +0,0 @@
# Copyright Google Inc. All Rights Reserved.
#
# Use of this source code is governed by an MIT-style license that can be
# found in the LICENSE file at https://angular.io/license
"""Implementation of the protractor_web_test_suite rule.
"""
load("@io_bazel_rules_webtesting//web:web.bzl", "web_test_suite")
load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary")
load("@build_bazel_rules_nodejs//internal/common:sources_aspect.bzl", "sources_aspect")
load("@build_bazel_rules_nodejs//internal/common:module_mappings.bzl", "module_mappings_runtime_aspect")
def _modify_tsconfig_impl(ctx):
spec_file_import_paths = []
for dep in ctx.attr.deps:
# For each transitive ES5 dependency, grab the short path
for f in dep.node_sources.to_list():
spec_file_import_paths.append("\"{workspace}/{path}\"".format(workspace=ctx.workspace_name, path=f.short_path))
ctx.actions.expand_template(
template = ctx.file._conf_templ,
output = ctx.outputs._modified_conf,
substitutions = {
"BASE_CONF_IMPORT_PATH": "{workspace}/{path}".format(workspace=ctx.workspace_name, path=ctx.file.base_conf.short_path),
"SPEC_FILE_IMPORT_PATHS": ',\n '.join(spec_file_import_paths),
})
_modify_conf = rule(
attrs = {
"base_conf": attr.label(
doc = """conf.js file used to configure protractor.""",
allow_single_file = True,
cfg = "data",
aspects = [
sources_aspect,
module_mappings_runtime_aspect,
],
),
"deps": attr.label_list(
doc = """Spec and page files used for testing.""",
allow_files = True,
cfg = "data",
aspects = [
sources_aspect,
module_mappings_runtime_aspect,
],
),
"_conf_templ": attr.label(
allow_files = True,
single_file = True,
default = Label("@angular//packages/bazel/src/protractor:conf.js.tmpl"),
),
},
outputs = {
"_modified_conf": "%{name}.conf.js",
},
implementation = _modify_tsconfig_impl,
)
def protractor_web_test_suite(name, conf, deps, data = [], **kwargs):
"""Runs protractor using the passed conf.js file and tests listed within deps.
Args:
name: The name of the web_test_suite rule the macro expands into.
conf: A conf.js file to be used as a base template. The following fields of the base
config are overridden:
framework
seleniumAddress
specs
capabilities
deps: A list of dependencies containing the test files to run within protractor.
data: Any runtime files which are needed to run the test suite.
**kwargs: Any other arguements are passed directory to the expanded web_test_suite rule.
Returns:
This macro expands into a web_test_suite rule which runs the protractor tests.
"""
_modify_conf_name = "%s_modify_conf" % name
_modify_conf(
name = _modify_conf_name,
base_conf = conf,
deps = deps,
testonly = True,
)
_modified_conf = "%s.conf.js" % _modify_conf_name
_protractor_runner_name = name + "_protractor_runner"
nodejs_binary(
name = _protractor_runner_name,
entry_point = "angular/packages/bazel/src/protractor/protractor_runner.js",
data = data + deps + [_modified_conf, "@angular//packages/bazel/src/protractor:protractor_runner.js"],
templated_args = ["$(location :%s)" % _modified_conf],
tags = ["manual"],
testonly = True,
)
web_test_suite(
name = name,
# TODO: Allow users to specify more browsers. Currently conf.js is hardcoded for chrome.
browsers = ["@io_bazel_rules_webtesting//browsers:chromium-local"],
data = data + deps + [conf, _modified_conf],
test = ":%s_bin" % _protractor_runner_name,
testonly = True,
**kwargs
)

View File

@ -0,0 +1,16 @@
package(default_visibility = ["//visibility:public"])
filegroup(
name = "package_assets",
srcs = glob(["*"]),
visibility = ["//packages/bazel:__subpackages__"],
)
load("@build_bazel_rules_typescript//:defs.bzl", "ts_library")
ts_library(
name = "utils",
srcs = ["index.ts"],
module_name = "@angular/bazel/protractor-utils",
tsconfig = ":tsconfig.json",
)

View File

@ -0,0 +1,94 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as child_process from 'child_process';
import * as net from 'net';
import * as path from 'path';
export function isTcpPortFree(port: number): Promise<boolean> {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.on('error', (e) => { resolve(false); });
server.on('close', () => { resolve(true); });
server.listen(port, () => { server.close(); });
});
}
export function isTcpPortBound(port: number): Promise<boolean> {
return new Promise((resolve, reject) => {
const client = new net.Socket();
client.once('connect', () => { resolve(true); });
client.once('error', (e) => { resolve(false); });
client.connect(port);
});
}
export async function findFreeTcpPort(): Promise<number> {
const range = {
min: 32768,
max: 60000,
};
for (let i = 0; i < 100; i++) {
let port = Math.floor(Math.random() * (range.max - range.min) + range.min);
if (await isTcpPortFree(port)) {
return port;
}
}
throw new Error('Unable to find a free port');
}
// Interface for config parameter of the protractor_web_test_suite onPrepare function
export interface OnPrepareConfig {
// The workspace name
workspace: string;
// The server binary to run
server: string;
}
export function waitForServer(port: number, timeout: number): Promise<boolean> {
return isTcpPortBound(port).then(isBound => {
if (!isBound) {
if (timeout <= 0) {
throw new Error('Timeout waiting for server to start');
}
const wait = Math.min(timeout, 500);
return new Promise((res, rej) => setTimeout(res, wait))
.then(() => waitForServer(port, timeout - wait));
}
return true;
});
}
// Return type from runServer function
export interface ServerSpec {
// Port number that the server is running on
port: number;
}
export function runServer(
workspace: string, binary: string, portFlag: string, args: string[],
timeout = 5000): Promise<ServerSpec> {
return findFreeTcpPort().then(function(port) {
const runfiles_path = process.env.TEST_SRCDIR;
const cmd = path.join(runfiles_path, workspace, binary);
args = args.concat([portFlag, port.toString()]);
const child = child_process.spawn(
cmd, args, {cwd: path.join(runfiles_path, workspace), stdio: 'inherit'});
child.on('exit', function(code) {
if (code != 0) {
throw new Error(`non-zero exit code ${code} from server`);
}
});
return waitForServer(port, timeout).then(() => { return {port}; });
});
}

View File

@ -0,0 +1,6 @@
{
"compilerOptions": {
"noImplicitAny": true,
"lib": ["es2015"]
}
}