feat(bazel): protractor_web_test_suite for release (#24787)
PR Close #24787
This commit is contained in:
parent
71e0df039c
commit
161ff5c79d
|
@ -35,6 +35,9 @@ node_modules_filegroup(
|
|||
"@types",
|
||||
"@webcomponents/custom-elements",
|
||||
],
|
||||
patterns = [
|
||||
"node_modules/protractor/**",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
|
|
13
WORKSPACE
13
WORKSPACE
|
@ -13,15 +13,18 @@ http_archive(
|
|||
|
||||
http_archive(
|
||||
name = "bazel_gazelle",
|
||||
sha256 = "d03625db67e9fb0905bbd206fa97e32ae9da894fe234a493e7517fd25faec914",
|
||||
url = "https://github.com/bazelbuild/bazel-gazelle/releases/download/0.10.1/bazel-gazelle-0.10.1.tar.gz",
|
||||
urls = [
|
||||
"https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/0.12.0/bazel-gazelle-0.12.0.tar.gz",
|
||||
"https://github.com/bazelbuild/bazel-gazelle/releases/download/0.12.0/bazel-gazelle-0.12.0.tar.gz",
|
||||
],
|
||||
sha256 = "ddedc7aaeb61f2654d7d7d4fd7940052ea992ccdb031b8f9797ed143ac7e8d43",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "io_bazel_rules_webtesting",
|
||||
url = "https://github.com/bazelbuild/rules_webtesting/archive/v0.2.0.zip",
|
||||
strip_prefix = "rules_webtesting-0.2.0",
|
||||
sha256 = "cecc12f07e95740750a40d38e8b14b76fefa1551bef9332cb432d564d693723c",
|
||||
url = "https://github.com/bazelbuild/rules_webtesting/archive/7ffe970bbf380891754487f66c3d680c087d67f2.zip",
|
||||
strip_prefix = "rules_webtesting-7ffe970bbf380891754487f66c3d680c087d67f2",
|
||||
sha256 = "4fb0dca8c9a90547891b7ef486592775a523330fc4555c88cd8f09270055c2ce",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
exports_files([
|
||||
"protractor.conf.js",
|
||||
])
|
||||
|
||||
filegroup(
|
||||
name = "node_modules",
|
||||
srcs = glob(
|
||||
|
|
|
@ -11,11 +11,20 @@ http_archive(
|
|||
sha256 = "634206524d90dc03c52392fa3f19a16637d2bcf154910436fe1d669a0d9d7b9c",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "bazel_gazelle",
|
||||
urls = [
|
||||
"https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/0.12.0/bazel-gazelle-0.12.0.tar.gz",
|
||||
"https://github.com/bazelbuild/bazel-gazelle/releases/download/0.12.0/bazel-gazelle-0.12.0.tar.gz",
|
||||
],
|
||||
sha256 = "ddedc7aaeb61f2654d7d7d4fd7940052ea992ccdb031b8f9797ed143ac7e8d43",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "io_bazel_rules_webtesting",
|
||||
url = "https://github.com/bazelbuild/rules_webtesting/archive/v0.2.0.zip",
|
||||
strip_prefix = "rules_webtesting-0.2.0",
|
||||
sha256 = "cecc12f07e95740750a40d38e8b14b76fefa1551bef9332cb432d564d693723c",
|
||||
url = "https://github.com/bazelbuild/rules_webtesting/archive/8fd9ce0fd9254bde251da0bc373d6cd08e811434.zip",
|
||||
strip_prefix = "rules_webtesting-8fd9ce0fd9254bde251da0bc373d6cd08e811434",
|
||||
sha256 = "4baee95fcfadfbaf868707af8accfd1cb98c5d13f808908e0152468bfb47f0f7",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
|
|
|
@ -18,17 +18,13 @@
|
|||
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
|
||||
"@types/jasmine": "2.8.7",
|
||||
"@types/source-map": "0.5.1",
|
||||
"concurrently": "3.5.1",
|
||||
"http-server": "0.11.1",
|
||||
"protractor": "file:../../node_modules/protractor",
|
||||
"tsutils": "file:../../node_modules/tsutils",
|
||||
"typescript": "file:../../node_modules/typescript"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "concurrently \"webdriver-manager update $CHROMEDRIVER_VERSION_ARG\" \"ngc -p angular.tsconfig.json\"",
|
||||
"test": "bazel build //... --noshow_progress && yarn e2e",
|
||||
"pree2e": "bazel build test/...",
|
||||
"e2e": "yarn e2e-prodserver && yarn e2e-devserver",
|
||||
"e2e-prodserver": "concurrently \"bazel run //src:prodserver\" \"while ! nc -z 127.0.0.1 5432; do sleep 1; done && protractor\" --kill-others --success first",
|
||||
"e2e-devserver": "concurrently \"bazel run //src:devserver\" \"while ! nc -z 127.0.0.1 5432; do sleep 1; done && protractor\" --kill-others --success first"
|
||||
"postinstall": "ngc -p angular.tsconfig.json",
|
||||
"test": "bazel build ... --noshow_progress && bazel test ..."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
exports.config = {
|
||||
specs: ['bazel-bin/test/e2e/*.spec.js'],
|
||||
capabilities: {browserName: 'chrome', chromeOptions: {args: ['--no-sandbox']}},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:5432/',
|
||||
framework: 'jasmine',
|
||||
getPageTimeout: 60 * 1000,
|
||||
allScriptsTimeout: 60 * 1000,
|
||||
};
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load("@angular//:index.bzl", "ng_module")
|
||||
|
||||
# Allow targets under sub-packages to reference the tsconfig.json file
|
||||
|
@ -46,15 +48,11 @@ genrule(
|
|||
|
||||
nodejs_binary(
|
||||
name = "prodserver",
|
||||
args = [
|
||||
"./src",
|
||||
"-p",
|
||||
"5432",
|
||||
],
|
||||
data = [
|
||||
"index.html",
|
||||
":bundle",
|
||||
":zone.js",
|
||||
],
|
||||
entry_point = "http-server/bin/http-server",
|
||||
templated_args = ["src"],
|
||||
)
|
||||
|
|
|
@ -1,7 +1,34 @@
|
|||
load("@build_bazel_rules_typescript//:defs.bzl", "ts_library")
|
||||
load("@angular//:index.bzl", "protractor_web_test_suite")
|
||||
|
||||
ts_library(
|
||||
name = "e2e",
|
||||
testonly = 1,
|
||||
srcs = glob(["*.ts"]),
|
||||
srcs = ["app.spec.ts"],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "ts_on_prepare",
|
||||
testonly = 1,
|
||||
srcs = ["on-prepare.ts"],
|
||||
tsconfig = ":tsconfig.json",
|
||||
deps = ["@angular//src/protractor/utils"],
|
||||
)
|
||||
|
||||
protractor_web_test_suite(
|
||||
name = "devserver_test",
|
||||
configuration = "//:protractor.conf.js",
|
||||
data = ["@angular//src/protractor/utils"],
|
||||
on_prepare = ":ts_on_prepare",
|
||||
server = "//src:devserver",
|
||||
deps = [":e2e"],
|
||||
)
|
||||
|
||||
protractor_web_test_suite(
|
||||
name = "prodserver_test",
|
||||
configuration = "//:protractor.conf.js",
|
||||
data = ["@angular//src/protractor/utils"],
|
||||
on_prepare = ":ts_on_prepare",
|
||||
server = "//src:prodserver",
|
||||
deps = [":e2e"],
|
||||
)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {browser, by, element, ExpectedConditions} from 'protractor';
|
||||
import {browser, by, element} from 'protractor';
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000;
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import { browser } from 'protractor';
|
||||
import {OnPrepareConfig, runServer} from '@angular/bazel/protractor-utils';
|
||||
|
||||
export = function(config: OnPrepareConfig) {
|
||||
const portFlag = config.server.endsWith('prodserver') ? '-p' : '-port';
|
||||
return runServer(config.workspace, config.server, portFlag, [])
|
||||
.then(serverSpec => {
|
||||
const serverUrl = `http://localhost:${serverSpec.port}`;
|
||||
console.log(`Server has been started, starting tests against ${serverUrl}`);
|
||||
browser.baseUrl = serverUrl;
|
||||
});
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2015"]
|
||||
}
|
||||
}
|
|
@ -3,12 +3,12 @@
|
|||
|
||||
|
||||
"@angular/animations@file:../../dist/packages-dist/animations":
|
||||
version "6.0.0-rc.5"
|
||||
version "6.1.0-beta.1"
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
"@angular/bazel@file:../../dist/packages-dist/bazel":
|
||||
version "6.0.0-rc.5"
|
||||
version "6.1.0-beta.1"
|
||||
dependencies:
|
||||
"@bazel/typescript" "^0.15.0"
|
||||
"@types/node" "6.0.84"
|
||||
|
@ -17,12 +17,12 @@
|
|||
shelljs "0.7.8"
|
||||
|
||||
"@angular/common@file:../../dist/packages-dist/common":
|
||||
version "6.0.0-rc.5"
|
||||
version "6.1.0-beta.1"
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
"@angular/compiler-cli@file:../../dist/packages-dist/compiler-cli":
|
||||
version "6.0.0-rc.5"
|
||||
version "6.1.0-beta.1"
|
||||
dependencies:
|
||||
chokidar "^1.4.2"
|
||||
minimist "^1.2.0"
|
||||
|
@ -30,22 +30,22 @@
|
|||
tsickle "^0.29.0"
|
||||
|
||||
"@angular/compiler@file:../../dist/packages-dist/compiler":
|
||||
version "6.0.0-rc.5"
|
||||
version "6.1.0-beta.1"
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
"@angular/core@file:../../dist/packages-dist/core":
|
||||
version "6.0.0-rc.5"
|
||||
version "6.1.0-beta.1"
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
"@angular/platform-browser-dynamic@file:../../dist/packages-dist/platform-browser-dynamic":
|
||||
version "6.0.0-rc.5"
|
||||
version "6.1.0-beta.1"
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
"@angular/platform-browser@file:../../dist/packages-dist/platform-browser":
|
||||
version "6.0.0-rc.5"
|
||||
version "6.1.0-beta.1"
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
|
@ -74,16 +74,16 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||
|
||||
"@types/node@*":
|
||||
version "10.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.1.4.tgz#606651d3f8a8bec08b8cb262161aab9209f4a29d"
|
||||
version "10.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.3.4.tgz#c74e8aec19e555df44609b8057311052a2c84d9e"
|
||||
|
||||
"@types/node@6.0.84":
|
||||
version "6.0.84"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.84.tgz#193ffe5a9f42864d425ffd9739d95b753c6a1eab"
|
||||
|
||||
"@types/node@^6.0.46":
|
||||
version "6.0.111"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.111.tgz#85f880a1bab78d395a5de9bcb5319e73a0c31400"
|
||||
version "6.0.113"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.113.tgz#4b41f38ad03e4b41f9dc259b3b58aecb22c9aebc"
|
||||
|
||||
"@types/q@^0.0.32":
|
||||
version "0.0.32"
|
||||
|
@ -132,10 +132,6 @@ ajv@^5.1.0:
|
|||
fast-json-stable-stringify "^2.0.0"
|
||||
json-schema-traverse "^0.3.0"
|
||||
|
||||
ansi-regex@^0.2.0, ansi-regex@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9"
|
||||
|
||||
ansi-regex@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
|
||||
|
@ -144,10 +140,6 @@ ansi-regex@^3.0.0:
|
|||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
|
||||
|
||||
ansi-styles@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de"
|
||||
|
||||
ansi-styles@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
|
||||
|
@ -286,16 +278,6 @@ caseless@~0.12.0:
|
|||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
||||
|
||||
chalk@0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174"
|
||||
dependencies:
|
||||
ansi-styles "^1.1.0"
|
||||
escape-string-regexp "^1.0.0"
|
||||
has-ansi "^0.1.0"
|
||||
strip-ansi "^0.3.0"
|
||||
supports-color "^0.2.0"
|
||||
|
||||
chalk@^1.1.1, chalk@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
|
||||
|
@ -355,27 +337,10 @@ combined-stream@1.0.6, combined-stream@~1.0.5:
|
|||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
commander@2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d"
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
|
||||
concurrently@3.5.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-3.5.1.tgz#ee8b60018bbe86b02df13e5249453c6ececd2521"
|
||||
dependencies:
|
||||
chalk "0.5.1"
|
||||
commander "2.6.0"
|
||||
date-fns "^1.23.0"
|
||||
lodash "^4.5.1"
|
||||
rx "2.3.24"
|
||||
spawn-command "^0.0.2-1"
|
||||
supports-color "^3.2.3"
|
||||
tree-kill "^1.1.0"
|
||||
|
||||
console-control-strings@^1.0.0, console-control-strings@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
||||
|
@ -394,10 +359,6 @@ dashdash@^1.12.0:
|
|||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
date-fns@^1.23.0:
|
||||
version "1.29.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6"
|
||||
|
||||
debug@2, debug@^2.1.2, debug@^2.2.0:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
|
@ -457,7 +418,7 @@ ecstatic@^3.0.0:
|
|||
minimist "^1.1.0"
|
||||
url-join "^2.0.2"
|
||||
|
||||
escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2:
|
||||
escape-string-regexp@^1.0.2:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
|
||||
|
@ -645,22 +606,12 @@ har-validator@~5.0.3:
|
|||
ajv "^5.1.0"
|
||||
har-schema "^2.0.0"
|
||||
|
||||
has-ansi@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e"
|
||||
dependencies:
|
||||
ansi-regex "^0.2.0"
|
||||
|
||||
has-ansi@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
|
||||
dependencies:
|
||||
ansi-regex "^2.0.0"
|
||||
|
||||
has-flag@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
|
||||
|
||||
has-unicode@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
|
||||
|
@ -894,10 +845,6 @@ lcid@^1.0.0:
|
|||
dependencies:
|
||||
invert-kv "^1.0.0"
|
||||
|
||||
lodash@^4.5.1:
|
||||
version "4.17.10"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
|
||||
|
||||
long@~3:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b"
|
||||
|
@ -1303,8 +1250,8 @@ requires-port@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
|
||||
resolve@^1.1.6:
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3"
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26"
|
||||
dependencies:
|
||||
path-parse "^1.0.5"
|
||||
|
||||
|
@ -1314,10 +1261,6 @@ rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1:
|
|||
dependencies:
|
||||
glob "^7.0.5"
|
||||
|
||||
rx@2.3.24:
|
||||
version "2.3.24"
|
||||
resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7"
|
||||
|
||||
"rxjs@file:../../node_modules/rxjs":
|
||||
version "6.0.0"
|
||||
dependencies:
|
||||
|
@ -1327,7 +1270,7 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0,
|
|||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3":
|
||||
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
|
||||
|
@ -1413,18 +1356,15 @@ source-map@^0.6.0:
|
|||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
|
||||
spawn-command@^0.0.2-1:
|
||||
version "0.0.2-1"
|
||||
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
|
||||
|
||||
sshpk@^1.7.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.1.tgz#130f5975eddad963f1d56f92b9ac6c51fa9f83eb"
|
||||
version "1.14.2"
|
||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98"
|
||||
dependencies:
|
||||
asn1 "~0.2.3"
|
||||
assert-plus "^1.0.0"
|
||||
dashdash "^1.12.0"
|
||||
getpass "^0.1.1"
|
||||
safer-buffer "^2.0.2"
|
||||
optionalDependencies:
|
||||
bcrypt-pbkdf "^1.0.0"
|
||||
ecc-jsbn "~0.1.1"
|
||||
|
@ -1452,12 +1392,6 @@ string_decoder@~1.1.1:
|
|||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
strip-ansi@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.3.0.tgz#25f48ea22ca79187f3174a4db8759347bb126220"
|
||||
dependencies:
|
||||
ansi-regex "^0.2.1"
|
||||
|
||||
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
|
||||
|
@ -1474,20 +1408,10 @@ strip-json-comments@~2.0.1:
|
|||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
|
||||
supports-color@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a"
|
||||
|
||||
supports-color@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
|
||||
|
||||
supports-color@^3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
|
||||
dependencies:
|
||||
has-flag "^1.0.0"
|
||||
|
||||
tar@^4:
|
||||
version "4.4.4"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.4.tgz#ec8409fae9f665a4355cc3b4087d0820232bb8cd"
|
||||
|
@ -1516,10 +1440,6 @@ tough-cookie@~2.3.3:
|
|||
dependencies:
|
||||
punycode "^1.4.1"
|
||||
|
||||
tree-kill@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.0.tgz#5846786237b4239014f05db156b643212d4c6f36"
|
||||
|
||||
tsickle@^0.29.0:
|
||||
version "0.29.0"
|
||||
resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.29.0.tgz#812806554bb46c1aa16eb0fe2a051da95ca8f5a4"
|
||||
|
@ -1529,10 +1449,15 @@ tsickle@^0.29.0:
|
|||
source-map "^0.6.0"
|
||||
source-map-support "^0.5.0"
|
||||
|
||||
tslib@^1.9.0:
|
||||
tslib@^1.8.1, tslib@^1.9.0:
|
||||
version "1.9.2"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.2.tgz#8be0cc9a1f6dc7727c38deb16c2ebd1a2892988e"
|
||||
|
||||
"tsutils@file:../../node_modules/tsutils":
|
||||
version "2.20.0"
|
||||
dependencies:
|
||||
tslib "^1.8.1"
|
||||
|
||||
tunnel-agent@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
|
||||
|
@ -1544,7 +1469,7 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
|||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||
|
||||
"typescript@file:../../node_modules/typescript":
|
||||
version "2.7.2"
|
||||
version "2.8.3"
|
||||
|
||||
ultron@1.0.x:
|
||||
version "1.0.2"
|
||||
|
|
|
@ -9,8 +9,11 @@ Users should not load files under "/src"
|
|||
|
||||
load("//packages/bazel/src:ng_module.bzl", _ng_module = "ng_module")
|
||||
load("//packages/bazel/src/ng_package:ng_package.bzl", _ng_package = "ng_package")
|
||||
load("//packages/bazel/src/protractor:protractor_web_test_suite.bzl", _protractor_web_test_suite = "protractor_web_test_suite")
|
||||
load("//packages/bazel/src/protractor:protractor_web_test.bzl",
|
||||
_protractor_web_test = "protractor_web_test",
|
||||
_protractor_web_test_suite = "protractor_web_test_suite")
|
||||
|
||||
ng_module = _ng_module
|
||||
ng_package = _ng_package
|
||||
protractor_web_test = _protractor_web_test
|
||||
protractor_web_test_suite = _protractor_web_test_suite
|
||||
|
|
|
@ -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__"],
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
])
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
};
|
|
@ -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;
|
|
@ -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));
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
)
|
|
@ -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",
|
||||
)
|
|
@ -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}; });
|
||||
});
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": true,
|
||||
"lib": ["es2015"]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
load("//packages/bazel:index.bzl", "protractor_web_test_suite")
|
||||
load("@build_bazel_rules_typescript//:defs.bzl", "ts_devserver", "ts_library")
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary", "rollup_bundle")
|
||||
load("//tools/http-server:http_server.bzl", "http_server")
|
||||
|
||||
ts_library(
|
||||
name = "app",
|
||||
srcs = ["app.ts"],
|
||||
)
|
||||
|
||||
ts_devserver(
|
||||
name = "devserver",
|
||||
serving_path = "/bundle.min.js",
|
||||
static_files = ["index.html"],
|
||||
deps = [":app"],
|
||||
)
|
||||
|
||||
rollup_bundle(
|
||||
name = "bundle",
|
||||
entry_point = "packages/bazel/test/protractor-2/app",
|
||||
deps = [":app"],
|
||||
)
|
||||
|
||||
http_server(
|
||||
name = "prodserver",
|
||||
data = [
|
||||
"index.html",
|
||||
":bundle",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "ts_spec",
|
||||
testonly = True,
|
||||
srcs = ["test.spec.ts"],
|
||||
)
|
||||
|
||||
protractor_web_test_suite(
|
||||
name = "prodserver_test",
|
||||
configuration = ":conf.js",
|
||||
data = ["//packages/bazel/src/protractor/utils"],
|
||||
on_prepare = ":on-prepare.js",
|
||||
server = ":prodserver",
|
||||
deps = [":ts_spec"],
|
||||
)
|
||||
|
||||
protractor_web_test_suite(
|
||||
name = "devserver_test",
|
||||
configuration = ":conf.js",
|
||||
data = ["//packages/bazel/src/protractor/utils"],
|
||||
on_prepare = ":on-prepare.js",
|
||||
server = ":prodserver",
|
||||
deps = [":ts_spec"],
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* @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 el: HTMLDivElement = document.createElement('div');
|
||||
el.innerText = 'Hello, Protractor';
|
||||
el.className = 'ts1';
|
||||
document.body.appendChild(el);
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
exports.config = {
|
||||
onPrepare: function() { global.userOnPrepareGotCalled = true; }
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>protractor test</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="/bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* @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 protractorUtils = require('@angular/bazel/protractor-utils');
|
||||
const protractor = require('protractor');
|
||||
|
||||
module.exports = function(config) {
|
||||
if (!global.userOnPrepareGotCalled) {
|
||||
throw new Error('Expecting user configuration onPrepare to have been called');
|
||||
}
|
||||
const portFlag = config.server.endsWith('prodserver') ? '-p' : '-port';
|
||||
return protractorUtils.runServer(config.workspace, config.server, portFlag, [])
|
||||
.then(serverSpec => {
|
||||
const serverUrl = `http://localhost:${serverSpec.port}`;
|
||||
protractor.browser.baseUrl = serverUrl;
|
||||
});
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* @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 small demo of how to run a protractor test.
|
||||
*/
|
||||
|
||||
import {ExpectedConditions, browser, by, element} from 'protractor';
|
||||
|
||||
|
||||
// This test uses Protractor without Angular, so disable Angular features
|
||||
browser.waitForAngularEnabled(false);
|
||||
|
||||
describe('app', () => {
|
||||
beforeAll(() => {
|
||||
browser.get('');
|
||||
browser.wait(ExpectedConditions.presenceOf(element(by.css('div.ts1'))), 100000);
|
||||
});
|
||||
|
||||
it('should display: Hello, Protractor', (done) => {
|
||||
const div = element(by.css('div.ts1'));
|
||||
div.getText().then(t => expect(t).toEqual(`Hello, Protractor`));
|
||||
done();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2015"]
|
||||
}
|
||||
}
|
|
@ -7,8 +7,17 @@ ts_library(
|
|||
srcs = ["test.spec.ts"],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "ts_conf",
|
||||
testonly = True,
|
||||
srcs = ["conf.ts"],
|
||||
tsconfig = ":tsconfig.json",
|
||||
deps = ["//packages/bazel/src/protractor/utils"],
|
||||
)
|
||||
|
||||
protractor_web_test_suite(
|
||||
name = "demo",
|
||||
conf = "conf.js",
|
||||
name = "test",
|
||||
configuration = ":ts_conf",
|
||||
data = ["//packages/bazel/src/protractor/utils"],
|
||||
deps = [":ts_spec"],
|
||||
)
|
||||
|
|
|
@ -1,29 +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 demo of what a user's conf.js file might look like.
|
||||
*/
|
||||
const http = require('http');
|
||||
|
||||
const PORT = 3000;
|
||||
|
||||
exports.config = {
|
||||
baseUrl: `http://localhost:${PORT}`,
|
||||
onPrepare() {
|
||||
const app = new http.Server();
|
||||
|
||||
app.on('request', (req, res) => {
|
||||
res.writeHead(200, {'Content-Type': 'text/plain'});
|
||||
res.write('Hello World');
|
||||
res.end('\n');
|
||||
});
|
||||
|
||||
return new Promise(resolve => { app.listen(PORT, () => { resolve(); }); });
|
||||
}
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* @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 protractorUtils from '@angular/bazel/protractor-utils';
|
||||
import {browser} from 'protractor';
|
||||
|
||||
const http = require('http');
|
||||
|
||||
exports.config = {
|
||||
onPrepare() {
|
||||
return protractorUtils.findFreeTcpPort().then(port => {
|
||||
const app = new http.Server();
|
||||
|
||||
app.on('request', (req, res) => {
|
||||
res.writeHead(200, {'Content-Type': 'text/plain'});
|
||||
res.write('Hello World');
|
||||
res.end('\n');
|
||||
});
|
||||
|
||||
browser.baseUrl = `http://localhost:${port}`;
|
||||
|
||||
return new Promise(resolve => { app.listen(port, () => { resolve(); }); });
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2015"]
|
||||
}
|
||||
}
|
|
@ -4,15 +4,15 @@ See https://www.npmjs.com/package/http-server
|
|||
"""
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary")
|
||||
|
||||
def http_server(args = [], **kwargs):
|
||||
def http_server(templated_args = [], **kwargs):
|
||||
# By default, we pass an argument pointing the http server to the
|
||||
# package of the caller.
|
||||
# This assumes there is an index.html in the package directory.
|
||||
if not args:
|
||||
args = [native.package_name()]
|
||||
if not templated_args:
|
||||
templated_args = [native.package_name()]
|
||||
|
||||
nodejs_binary(
|
||||
node_modules = "@http-server_runtime_deps//:node_modules",
|
||||
entry_point = "http-server/bin/http-server",
|
||||
args = args,
|
||||
templated_args = templated_args,
|
||||
**kwargs)
|
||||
|
|
Loading…
Reference in New Issue