selenium-side-runner 3.17.0 → 4.0.0-alpha.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -10
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +160 -0
- package/dist/bin.js.map +1 -0
- package/dist/capabilities.d.ts +7 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +49 -66
- package/dist/capabilities.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +12 -18
- package/dist/config.js.map +1 -0
- package/dist/main.test.d.ts +2 -0
- package/dist/main.test.d.ts.map +1 -0
- package/dist/main.test.js +113 -0
- package/dist/main.test.js.map +1 -0
- package/dist/proxy.d.ts +3 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +67 -64
- package/dist/proxy.js.map +1 -0
- package/dist/register.d.ts +16 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +66 -0
- package/dist/register.js.map +1 -0
- package/dist/run.d.ts +14 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +169 -0
- package/dist/run.js.map +1 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/versioner.d.ts +2 -0
- package/dist/versioner.d.ts.map +1 -0
- package/dist/versioner.js +23 -22
- package/dist/versioner.js.map +1 -0
- package/jest.config.js +13 -0
- package/package.json +30 -25
- package/src/__tests__/capabilities.spec.ts +137 -0
- package/{dist/npm.js → src/__tests__/config.spec.ts} +8 -24
- package/src/__tests__/config_1.yml +4 -0
- package/src/__tests__/config_2.yml +3 -0
- package/src/__tests__/proxy.spec.ts +138 -0
- package/src/__tests__/versioner.spec.ts +44 -0
- package/src/bin.ts +202 -0
- package/src/capabilities.ts +87 -0
- package/{dist/util.js → src/config.ts} +11 -10
- package/src/main.test.ts +129 -0
- package/src/proxy.ts +89 -0
- package/src/register.ts +79 -0
- package/src/run.ts +185 -0
- package/src/types.ts +82 -0
- package/src/versioner.ts +36 -0
- package/tsconfig.json +19 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/.babelrc +0 -12
- package/dist/child.js +0 -7
- package/dist/index.js +0 -391
- package/dist/module-path.js +0 -41
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Licensed to the Software Freedom Conservancy (SFC) under one
|
|
2
|
+
// or more contributor license agreements. See the NOTICE file
|
|
3
|
+
// distributed with this work for additional information
|
|
4
|
+
// regarding copyright ownership. The SFC licenses this file
|
|
5
|
+
// to you under the Apache License, Version 2.0 (the
|
|
6
|
+
// "License"); you may not use this file except in compliance
|
|
7
|
+
// with the License. You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing,
|
|
12
|
+
// software distributed under the License is distributed on an
|
|
13
|
+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
// KIND, either express or implied. See the License for the
|
|
15
|
+
// specific language governing permissions and limitations
|
|
16
|
+
// under the License.
|
|
17
|
+
|
|
18
|
+
import { JSON } from './types'
|
|
19
|
+
|
|
20
|
+
export function parseString(input: string): Record<string, JSON> {
|
|
21
|
+
const capabilities = {}
|
|
22
|
+
|
|
23
|
+
matchStringPairs(input).forEach(({ key, value }) => {
|
|
24
|
+
assignStringKey(key, parseStringValue(value), capabilities)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
return capabilities
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
parseString,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function matchStringPairs(input: string) {
|
|
35
|
+
const regex = /([^\s=]*)\s?=\s?(".*?"|'.*?'|\[.*\]|[^\s]*)/g
|
|
36
|
+
let result
|
|
37
|
+
const splitCapabilities = []
|
|
38
|
+
while ((result = regex.exec(input)) !== null) {
|
|
39
|
+
splitCapabilities.push({ key: result[1], value: result[2] })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return splitCapabilities
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type KeyObject = Record<string, string | Record<string, string>>
|
|
46
|
+
|
|
47
|
+
function assignStringKey(key: string, value: string, keyObject: KeyObject) {
|
|
48
|
+
keyObject = keyObject || {}
|
|
49
|
+
|
|
50
|
+
// @ts-expect-error
|
|
51
|
+
key.split('.').reduce((objectKey, currKey, index, keys) => {
|
|
52
|
+
const ref =
|
|
53
|
+
!objectKey[currKey] || typeof objectKey[currKey] !== 'object'
|
|
54
|
+
? {}
|
|
55
|
+
: objectKey[currKey]
|
|
56
|
+
objectKey[currKey] = keys.length === index + 1 ? value : ref
|
|
57
|
+
return ref
|
|
58
|
+
}, keyObject)
|
|
59
|
+
|
|
60
|
+
return keyObject
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseStringValue(value: string) {
|
|
64
|
+
// is array
|
|
65
|
+
if (/^\[.*\]$/.test(value)) {
|
|
66
|
+
const matches = value.slice(1, -2).match(/(([^,]*)*)/g) as string[]
|
|
67
|
+
return matches.filter((s) => !!s).map((s) => s.trim())
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
let parsed = JSON.parse(value)
|
|
71
|
+
if (typeof parsed === 'string') {
|
|
72
|
+
parsed = parsed.trim()
|
|
73
|
+
}
|
|
74
|
+
return parsed
|
|
75
|
+
} catch (e) {
|
|
76
|
+
// single quote is illegal in JSON
|
|
77
|
+
return trimSingleQuote(value)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function trimSingleQuote(value: string) {
|
|
82
|
+
let str = value.trim()
|
|
83
|
+
if (str[0] === str[str.length - 1] && str[0] === "'") {
|
|
84
|
+
return str.slice(1, -2)
|
|
85
|
+
}
|
|
86
|
+
return str
|
|
87
|
+
}
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.sanitizeFileName = sanitizeFileName;
|
|
7
|
-
|
|
8
1
|
// Licensed to the Software Freedom Conservancy (SFC) under one
|
|
9
2
|
// or more contributor license agreements. See the NOTICE file
|
|
10
3
|
// distributed with this work for additional information
|
|
@@ -21,6 +14,14 @@ exports.sanitizeFileName = sanitizeFileName;
|
|
|
21
14
|
// KIND, either express or implied. See the License for the
|
|
22
15
|
// specific language governing permissions and limitations
|
|
23
16
|
// under the License.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
|
|
18
|
+
import fs from 'fs'
|
|
19
|
+
import yaml from 'js-yaml'
|
|
20
|
+
|
|
21
|
+
export function load(config: string) {
|
|
22
|
+
return yaml.load(fs.readFileSync(config, 'utf8'))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default {
|
|
26
|
+
load,
|
|
27
|
+
}
|
package/src/main.test.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Licensed to the Software Freedom Conservancy (SFC) under one
|
|
2
|
+
// or more contributor license agreements. See the NOTICE file
|
|
3
|
+
// distributed with this work for additional information
|
|
4
|
+
// regarding copyright ownership. The SFC licenses this file
|
|
5
|
+
// to you under the Apache License, Version 2.0 (the
|
|
6
|
+
// "License"); you may not use this file except in compliance
|
|
7
|
+
// with the License. You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing,
|
|
12
|
+
// software distributed under the License is distributed on an
|
|
13
|
+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
// KIND, either express or implied. See the License for the
|
|
15
|
+
// specific language governing permissions and limitations
|
|
16
|
+
// under the License.
|
|
17
|
+
|
|
18
|
+
import each from 'jest-each'
|
|
19
|
+
import fs from 'fs'
|
|
20
|
+
import glob from 'glob'
|
|
21
|
+
import { createLogger, format, transports } from 'winston'
|
|
22
|
+
import { Configuration, Project } from './types'
|
|
23
|
+
import buildRegister from './register'
|
|
24
|
+
import buildRunners from './run'
|
|
25
|
+
import { SuiteShape, TestShape } from '@seleniumhq/side-model'
|
|
26
|
+
|
|
27
|
+
const metadata = require('../package.json')
|
|
28
|
+
|
|
29
|
+
process.title = metadata.name
|
|
30
|
+
|
|
31
|
+
// Capture and show unhandled exceptions
|
|
32
|
+
process.on('unhandledRejection', function handleWarning(reason) {
|
|
33
|
+
console.log('[PROCESS] Unhandled Promise Rejection')
|
|
34
|
+
console.log('- - - - - - - - - - - - - - - - - - -')
|
|
35
|
+
console.log(reason)
|
|
36
|
+
console.log('- -')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
process.on('uncaughtException', (error) => {
|
|
40
|
+
console.error('Unhandled Error', error)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const configuration: Configuration = JSON.parse(
|
|
44
|
+
process.env.SE_CONFIGURATION as string
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
// Jest timeout should be really far back, otherwise it will impede people
|
|
48
|
+
// When working right, we should close shop and detonate on our own much sooner
|
|
49
|
+
jest.setTimeout(60 * 60 * 1000) // 60 minutes before we just cut it off
|
|
50
|
+
|
|
51
|
+
const logger = createLogger({
|
|
52
|
+
level: configuration.debug ? 'debug' : 'info',
|
|
53
|
+
transports: [
|
|
54
|
+
new transports.Console({
|
|
55
|
+
format: format.combine(format.colorize(), format.simple()),
|
|
56
|
+
}),
|
|
57
|
+
],
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const projectTitle = 'Running project $name'
|
|
61
|
+
const suiteTitle = 'Running suite $name'
|
|
62
|
+
const testTitle = 'Running test $name'
|
|
63
|
+
|
|
64
|
+
const projects: Project[] = [
|
|
65
|
+
...configuration.projects.reduce((projects, project) => {
|
|
66
|
+
glob.sync(project).forEach((p) => {
|
|
67
|
+
projects.add(p)
|
|
68
|
+
})
|
|
69
|
+
return projects
|
|
70
|
+
}, new Set<string>()),
|
|
71
|
+
]
|
|
72
|
+
.map((p) => {
|
|
73
|
+
const project = JSON.parse(fs.readFileSync(p, 'utf8'))
|
|
74
|
+
project.path = p
|
|
75
|
+
return project
|
|
76
|
+
})
|
|
77
|
+
.filter((p) => Boolean(p))
|
|
78
|
+
|
|
79
|
+
if (!projects.length) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
'No matching projects found, supplied filepaths: ' +
|
|
82
|
+
configuration.projects.join(', ')
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const factoryParams = {
|
|
87
|
+
configuration,
|
|
88
|
+
logger: logger as unknown as Console,
|
|
89
|
+
}
|
|
90
|
+
const register = buildRegister(factoryParams)
|
|
91
|
+
const runners = buildRunners(factoryParams)
|
|
92
|
+
each(projects).describe(projectTitle, (project: Project) => {
|
|
93
|
+
const suites = project.suites.filter(
|
|
94
|
+
(suite) =>
|
|
95
|
+
suite.tests.length && new RegExp(configuration.filter).test(suite.name)
|
|
96
|
+
)
|
|
97
|
+
if (suites.length) {
|
|
98
|
+
each(suites).describe(suiteTitle, (suite: SuiteShape) => {
|
|
99
|
+
const isParallel = suite.parallel
|
|
100
|
+
const tests = suite.tests
|
|
101
|
+
.map((tID) => register.test(project, tID))
|
|
102
|
+
.filter((test) => new RegExp(configuration.filter).test(test.name))
|
|
103
|
+
const testExecutor = each(tests)
|
|
104
|
+
const testMethod = isParallel
|
|
105
|
+
? testExecutor.test.concurrent
|
|
106
|
+
: testExecutor.test
|
|
107
|
+
testMethod(testTitle, async (test: TestShape) => {
|
|
108
|
+
await runners.test(project, test)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
} else {
|
|
112
|
+
console.info(`
|
|
113
|
+
Project ${project.name} doesn't have any suites matching filter ${configuration.filter},
|
|
114
|
+
attempting to iterate all tests in project
|
|
115
|
+
`)
|
|
116
|
+
const tests = project.tests
|
|
117
|
+
.map((test) => register.test(project, test.id))
|
|
118
|
+
.filter((test) => new RegExp(configuration.filter).test(test.name))
|
|
119
|
+
if (!tests.length) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`No suites or tests found in project ${project.name} matching filter ${configuration.filter}, unable to test!`
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
const testExecutor = each(tests)
|
|
125
|
+
testExecutor.test(testTitle, async (test: TestShape) => {
|
|
126
|
+
await runners.test(project, test)
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
})
|
package/src/proxy.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Licensed to the Software Freedom Conservancy (SFC) under one
|
|
2
|
+
// or more contributor license agreements. See the NOTICE file
|
|
3
|
+
// distributed with this work for additional information
|
|
4
|
+
// regarding copyright ownership. The SFC licenses this file
|
|
5
|
+
// to you under the Apache License, Version 2.0 (the
|
|
6
|
+
// "License"); you may not use this file except in compliance
|
|
7
|
+
// with the License. You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing,
|
|
12
|
+
// software distributed under the License is distributed on an
|
|
13
|
+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
// KIND, either express or implied. See the License for the
|
|
15
|
+
// specific language governing permissions and limitations
|
|
16
|
+
// under the License.
|
|
17
|
+
|
|
18
|
+
import { ProxyCapabilities, ProxyInputOptions, ProxyType } from './types'
|
|
19
|
+
|
|
20
|
+
export default function ParseProxy(
|
|
21
|
+
type: ProxyType,
|
|
22
|
+
options?: ProxyInputOptions
|
|
23
|
+
): ProxyCapabilities {
|
|
24
|
+
if (type === 'direct' || type === 'system') {
|
|
25
|
+
return {
|
|
26
|
+
proxyType: type,
|
|
27
|
+
}
|
|
28
|
+
} else if (type === 'pac') {
|
|
29
|
+
if (!options || typeof options !== 'string') {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'A proxy autoconfig URL was not passed (e.g. --proxy-options="http://localhost/pac")'
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
proxyType: type,
|
|
36
|
+
proxyAutoconfigUrl: options,
|
|
37
|
+
}
|
|
38
|
+
} else if (type === 'manual') {
|
|
39
|
+
if (!options) {
|
|
40
|
+
return {
|
|
41
|
+
proxyType: type,
|
|
42
|
+
}
|
|
43
|
+
} else if (typeof options !== 'object') {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'Proxy options were not passed to manual proxy (e.g. --proxy-options="http=localhost:321 ftp=localhost:4324")'
|
|
46
|
+
)
|
|
47
|
+
} else {
|
|
48
|
+
let opts: Omit<ProxyCapabilities, 'proxyType'> = {}
|
|
49
|
+
if (options.http) opts.httpProxy = options.http as string
|
|
50
|
+
if (options.https) opts.sslProxy = options.https as string
|
|
51
|
+
if (options.ftp) opts.ftpProxy = options.ftp as string
|
|
52
|
+
if (options.bypass) opts.noProxy = options.bypass as string[]
|
|
53
|
+
return {
|
|
54
|
+
proxyType: type,
|
|
55
|
+
...opts,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} else if (type === 'socks') {
|
|
59
|
+
if (!options || typeof options === 'string' || !options.socksProxy) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
'Proxy options were not passed to socks proxy (e.g. --proxy-options="socksProxy=localhost:321")'
|
|
62
|
+
)
|
|
63
|
+
} else {
|
|
64
|
+
if (options.socksVersion) {
|
|
65
|
+
const ver = parseInt(options.socksVersion as string)
|
|
66
|
+
if (ver) {
|
|
67
|
+
return {
|
|
68
|
+
proxyType: 'manual',
|
|
69
|
+
socksProxy: options.socksProxy as string,
|
|
70
|
+
socksVersion: ver,
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
throw new Error(
|
|
74
|
+
'Proxy socks version is invalid (e.g. --proxy-options="socksProxy=localhost:321 socksVersion=5")'
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
return {
|
|
79
|
+
proxyType: 'manual',
|
|
80
|
+
socksProxy: options.socksProxy as string,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
throw new Error(
|
|
86
|
+
'An unknown proxy type was passed, use one of: direct, system, manual, socks or pac (e.g. --proxy-type="direct")'
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/register.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Licensed to the Software Freedom Conservancy (SFC) under one
|
|
2
|
+
// or more contributor license agreements. See the NOTICE file
|
|
3
|
+
// distributed with this work for additional information
|
|
4
|
+
// regarding copyright ownership. The SFC licenses this file
|
|
5
|
+
// to you under the Apache License, Version 2.0 (the
|
|
6
|
+
// "License"); you may not use this file except in compliance
|
|
7
|
+
// with the License. You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing,
|
|
12
|
+
// software distributed under the License is distributed on an
|
|
13
|
+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
// KIND, either express or implied. See the License for the
|
|
15
|
+
// specific language governing permissions and limitations
|
|
16
|
+
// under the License.
|
|
17
|
+
|
|
18
|
+
import { ProjectShape, SuiteShape, TestShape } from '@seleniumhq/side-model'
|
|
19
|
+
import Satisfies from './versioner'
|
|
20
|
+
import { Configuration, Project } from './types'
|
|
21
|
+
|
|
22
|
+
export interface HoistedThings {
|
|
23
|
+
configuration: Configuration
|
|
24
|
+
logger: Console
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SuiteArgs = [SuiteShape, TestShape[]]
|
|
28
|
+
export type ProjectArgs = [ProjectShape, SuiteArgs[]]
|
|
29
|
+
const buildRunners = ({ configuration, logger }: HoistedThings) => {
|
|
30
|
+
const registerTest = (project: Project, testID: string) =>
|
|
31
|
+
project.tests.find((t) => t.id === testID) as TestShape
|
|
32
|
+
|
|
33
|
+
const registerSuite = (project: Project, suite: SuiteShape): SuiteArgs => {
|
|
34
|
+
let tests: TestShape[] = []
|
|
35
|
+
for (let i = 0, ii = suite.tests.length; i != ii; i++) {
|
|
36
|
+
tests.push(registerTest(project, suite.tests[i]))
|
|
37
|
+
}
|
|
38
|
+
return [suite, tests]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const registerProject = (project: Project): ProjectArgs => {
|
|
42
|
+
if (!configuration.force) {
|
|
43
|
+
let warning = Satisfies(project.version, '2.0')
|
|
44
|
+
if (warning) {
|
|
45
|
+
logger.warn(warning)
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
logger.warn("--force is set, ignoring project's version")
|
|
49
|
+
}
|
|
50
|
+
if (!project.suites.length) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`The project ${project.name} has no test suites defined, create a suite using the IDE.`
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const suites: SuiteArgs[] = []
|
|
57
|
+
for (let i = 0, ii = project.suites.length; i != ii; i++) {
|
|
58
|
+
suites.push(registerSuite(project, project.suites[i]))
|
|
59
|
+
}
|
|
60
|
+
return [project, suites]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const registerAll = (projectConfigs: Project[]): ProjectArgs[] => {
|
|
64
|
+
const projects: ProjectArgs[] = []
|
|
65
|
+
for (let i = 0, ii = projectConfigs.length; i != ii; i++) {
|
|
66
|
+
projects.push(registerProject(projectConfigs[i]))
|
|
67
|
+
}
|
|
68
|
+
return projects
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
all: registerAll,
|
|
73
|
+
project: registerProject,
|
|
74
|
+
suite: registerSuite,
|
|
75
|
+
test: registerTest,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default buildRunners
|
package/src/run.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Licensed to the Software Freedom Conservancy (SFC) under one
|
|
2
|
+
// or more contributor license agreements. See the NOTICE file
|
|
3
|
+
// distributed with this work for additional information
|
|
4
|
+
// regarding copyright ownership. The SFC licenses this file
|
|
5
|
+
// to you under the Apache License, Version 2.0 (the
|
|
6
|
+
// "License"); you may not use this file except in compliance
|
|
7
|
+
// with the License. You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing,
|
|
12
|
+
// software distributed under the License is distributed on an
|
|
13
|
+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
// KIND, either express or implied. See the License for the
|
|
15
|
+
// specific language governing permissions and limitations
|
|
16
|
+
// under the License.
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
getCustomCommands,
|
|
20
|
+
loadPlugins,
|
|
21
|
+
Playback,
|
|
22
|
+
PlaybackEvents,
|
|
23
|
+
PlaybackEventShapes,
|
|
24
|
+
Variables,
|
|
25
|
+
WebDriverExecutor,
|
|
26
|
+
} from '@seleniumhq/side-runtime'
|
|
27
|
+
import { WebDriverExecutorConstructorArgs } from '@seleniumhq/side-runtime/dist/webdriver'
|
|
28
|
+
import { SuiteShape, TestShape } from '@seleniumhq/side-model'
|
|
29
|
+
import * as path from 'path'
|
|
30
|
+
import Satisfies from './versioner'
|
|
31
|
+
import { Configuration, Project } from './types'
|
|
32
|
+
|
|
33
|
+
export interface HoistedThings {
|
|
34
|
+
configuration: Configuration
|
|
35
|
+
logger: Console
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const buildRunners = ({ configuration, logger }: HoistedThings) => {
|
|
39
|
+
const runTest = async (project: Project, test: TestShape) => {
|
|
40
|
+
logger.info(`Running test ${test.name}`)
|
|
41
|
+
const shortenedProjectPath = project.path
|
|
42
|
+
.split(path.sep)
|
|
43
|
+
.slice(0, -1)
|
|
44
|
+
.join(path.sep)
|
|
45
|
+
const plugins = await loadPlugins(shortenedProjectPath, project.plugins)
|
|
46
|
+
const customCommands = getCustomCommands(plugins)
|
|
47
|
+
const driver = new WebDriverExecutor({
|
|
48
|
+
capabilities:
|
|
49
|
+
configuration.capabilities as unknown as WebDriverExecutorConstructorArgs['capabilities'],
|
|
50
|
+
customCommands,
|
|
51
|
+
hooks: {
|
|
52
|
+
onBeforePlay: async () => {
|
|
53
|
+
await Promise.all(
|
|
54
|
+
plugins.map((plugin) => {
|
|
55
|
+
const onBeforePlay = plugin.hooks.onBeforePlay
|
|
56
|
+
if (onBeforePlay) {
|
|
57
|
+
return onBeforePlay({ driver })
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
)
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
server: configuration.server,
|
|
64
|
+
})
|
|
65
|
+
await playbackUntilComplete()
|
|
66
|
+
function playbackUntilComplete() {
|
|
67
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
68
|
+
return new Promise(async (resolve, reject) => {
|
|
69
|
+
const playback = new Playback({
|
|
70
|
+
baseUrl: project.url,
|
|
71
|
+
executor: driver,
|
|
72
|
+
getTestByName: (name: string) =>
|
|
73
|
+
project.tests.find((t) => t.name === name) as TestShape,
|
|
74
|
+
logger,
|
|
75
|
+
variables: new Variables(),
|
|
76
|
+
})
|
|
77
|
+
const onComplete = (failure: any) => {
|
|
78
|
+
playback.cleanup()
|
|
79
|
+
if (failure) {
|
|
80
|
+
logger.warn('Completed with failure', failure)
|
|
81
|
+
return reject(failure)
|
|
82
|
+
} else {
|
|
83
|
+
return resolve(null)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const EE = playback['event-emitter']
|
|
87
|
+
EE.addListener(
|
|
88
|
+
PlaybackEvents.PLAYBACK_STATE_CHANGED,
|
|
89
|
+
({ state }: PlaybackEventShapes['PLAYBACK_STATE_CHANGED']) => {
|
|
90
|
+
logger.debug(`Playing state changed ${state}`)
|
|
91
|
+
switch (state) {
|
|
92
|
+
case 'aborted':
|
|
93
|
+
case 'errored':
|
|
94
|
+
case 'failed':
|
|
95
|
+
case 'finished':
|
|
96
|
+
case 'paused':
|
|
97
|
+
case 'stopped':
|
|
98
|
+
logger.info(
|
|
99
|
+
`Finished test ${test.name} ${
|
|
100
|
+
state === 'finished' ? 'Success' : 'Failure'
|
|
101
|
+
}`
|
|
102
|
+
)
|
|
103
|
+
onComplete(state !== 'finished')
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
EE.addListener(
|
|
108
|
+
PlaybackEvents.COMMAND_STATE_CHANGED,
|
|
109
|
+
({
|
|
110
|
+
command,
|
|
111
|
+
state,
|
|
112
|
+
}: PlaybackEventShapes['COMMAND_STATE_CHANGED']) => {
|
|
113
|
+
const cmd = command
|
|
114
|
+
const niceString = [cmd.command, cmd.target, cmd.value]
|
|
115
|
+
.filter((v) => !!v)
|
|
116
|
+
.join('|')
|
|
117
|
+
logger.debug(`${state} ${niceString}`)
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
try {
|
|
121
|
+
await playback.play(test, {
|
|
122
|
+
startingCommandIndex: 0,
|
|
123
|
+
})
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.error(e)
|
|
126
|
+
playback.cleanup()
|
|
127
|
+
return reject(e)
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const runSuite = async (project: Project, suite: SuiteShape) => {
|
|
134
|
+
logger.info(`Running suite ${suite.name}`)
|
|
135
|
+
if (!suite.tests.length) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`The suite ${suite.name} has no tests, add tests to the suite using the IDE.`
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
for (let i = 0, ii = suite.tests.length; i != ii; i++) {
|
|
141
|
+
await runTest(
|
|
142
|
+
project,
|
|
143
|
+
project.tests.find((t) => t.id === suite.tests[i]) as TestShape
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
logger.info(`Finished suite ${suite.name}`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const runProject = async (project: Project) => {
|
|
150
|
+
logger.info(`Running project ${project.name}`)
|
|
151
|
+
if (!configuration.force) {
|
|
152
|
+
let warning = Satisfies(project.version, '2.0')
|
|
153
|
+
if (warning) {
|
|
154
|
+
logger.warn(warning)
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
logger.warn("--force is set, ignoring project's version")
|
|
158
|
+
}
|
|
159
|
+
if (!project.suites.length) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`The project ${project.name} has no test suites defined, create a suite using the IDE.`
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (let i = 0, ii = project.suites.length; i != ii; i++) {
|
|
166
|
+
await runSuite(project, project.suites[i])
|
|
167
|
+
}
|
|
168
|
+
logger.info(`Finished project ${project.name}`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const runAll = async (projects: Project[]) => {
|
|
172
|
+
for (let i = 0, ii = projects.length; i != ii; i++) {
|
|
173
|
+
await runProject(projects[i])
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
all: runAll,
|
|
179
|
+
project: runProject,
|
|
180
|
+
suite: runSuite,
|
|
181
|
+
test: runTest,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export default buildRunners
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ProjectShape } from '@seleniumhq/side-model'
|
|
2
|
+
import { Command } from 'commander'
|
|
3
|
+
|
|
4
|
+
export type JSON =
|
|
5
|
+
| null
|
|
6
|
+
| string
|
|
7
|
+
| number
|
|
8
|
+
| boolean
|
|
9
|
+
| JSON[]
|
|
10
|
+
| { [key: string]: JSON }
|
|
11
|
+
|
|
12
|
+
export interface Project extends ProjectShape {
|
|
13
|
+
path: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ProxyType =
|
|
17
|
+
| 'autodetect'
|
|
18
|
+
| 'direct'
|
|
19
|
+
| 'manual'
|
|
20
|
+
| 'pac'
|
|
21
|
+
| 'socks'
|
|
22
|
+
| 'system'
|
|
23
|
+
|
|
24
|
+
export type ProxyInputOptions =
|
|
25
|
+
| string
|
|
26
|
+
| Record<string, string | string[] | number>
|
|
27
|
+
|
|
28
|
+
export type ProxyCapabilities = {
|
|
29
|
+
proxyType: 'autodetect' | 'direct' | 'manual' | 'pac' | 'system'
|
|
30
|
+
proxyAutoconfigUrl?: string
|
|
31
|
+
ftpProxy?: string
|
|
32
|
+
httpProxy?: string
|
|
33
|
+
noProxy?: string[]
|
|
34
|
+
sslProxy?: string
|
|
35
|
+
socksProxy?: string
|
|
36
|
+
socksVersion?: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SideRunnerCLIConfig {
|
|
40
|
+
// Override the base URL that was set in the IDE
|
|
41
|
+
baseUrl: string
|
|
42
|
+
// Webdriver capabilities
|
|
43
|
+
capabilities: string
|
|
44
|
+
// Use specified YAML file for configuration. (default: .side.yml)
|
|
45
|
+
configFile: string
|
|
46
|
+
// Print debug logs
|
|
47
|
+
debug: boolean
|
|
48
|
+
// Run suites matching name
|
|
49
|
+
filter: string
|
|
50
|
+
// Forcibly run the project, regardless of project's version
|
|
51
|
+
force: boolean
|
|
52
|
+
// Maximum amount of workers that will run your tests, defaults to number of cores
|
|
53
|
+
maxWorkers: number
|
|
54
|
+
// General parameters
|
|
55
|
+
params: string
|
|
56
|
+
// Proxy options to pass, for use with manual, pac and socks proxies
|
|
57
|
+
proxyOptions?: string
|
|
58
|
+
// Type of proxy to use (one of: direct, manual, pac, socks, system)
|
|
59
|
+
proxyType?: ProxyType
|
|
60
|
+
// Webdriver remote server
|
|
61
|
+
server: string
|
|
62
|
+
// The maximimum amount of time, in milliseconds, to spend attempting to locate
|
|
63
|
+
// an element. (default: ${DEFAULT_TIMEOUT})
|
|
64
|
+
timeout: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type SideRunnerAPI = Command & SideRunnerCLIConfig
|
|
68
|
+
|
|
69
|
+
export type Configuration = Required<
|
|
70
|
+
Pick<
|
|
71
|
+
SideRunnerCLIConfig,
|
|
72
|
+
'baseUrl' | 'filter' | 'maxWorkers' | 'server' | 'timeout'
|
|
73
|
+
>
|
|
74
|
+
> &
|
|
75
|
+
Pick<SideRunnerCLIConfig, 'debug' | 'force' | 'proxyType'> & {
|
|
76
|
+
capabilities: Record<string, JSON>
|
|
77
|
+
params: Record<string, JSON>
|
|
78
|
+
projects: string[]
|
|
79
|
+
proxyOptions: ProxyInputOptions
|
|
80
|
+
runId: string
|
|
81
|
+
path: string
|
|
82
|
+
}
|