-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.mjs
More file actions
executable file
·159 lines (139 loc) · 5.32 KB
/
index.mjs
File metadata and controls
executable file
·159 lines (139 loc) · 5.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#!/usr/bin/env node
import fs from 'fs'
import { rimrafSync } from "rimraf";
import path from 'path'
import codeExport from './browserstack-mocha-export.mjs'
import { project as projectProcessor } from '@seleniumhq/side-code-export'
import pkg from '@seleniumhq/side-utils';
import commander from 'commander';
import logger from 'cli-logger';
import { globSync } from 'glob';
import spawn from 'cross-spawn';
import * as dotenv from 'dotenv';
import { exit } from 'process';
import { fileURLToPath, pathToFileURL } from 'url';
dotenv.config();
commander
.usage('[options] project.side [project.side] [*.side]')
.option('-d, --debug', 'output extra debugging')
.option('-f, --filter <grep regex>', 'Run tests matching name')
.option('--base-url <url>', 'Override the base URL that was set in the IDE')
.option('--test-timeout <ms>', 'Timeout value for each tests (default: 30000)')
.option('--browserstack.config <path>', 'path to browserstack config file, default to browserstack.yml')
.option('--output-format <json|xunit>', 'Format for the output file.')
.option('--output-file <path>', 'path for the report file. required if --output-format provided')
commander.parse(process.argv);
const options = commander.opts();
options.testTimeout = options.testTimeout ? options.testTimeout : 30000
options.filter = options.filter ? options.filter : ''
options.browserstackConfig = options['browserstack.config'] ? options['browserstack.config'] : 'browserstack.yml'
options.buildFolderPath = '_generated'
var conf = { level: options.debug ? logger.DEBUG : logger.INFO };
var log = logger(conf);
const sideFiles = [
...commander.args.reduce((projects, project) => {
globSync(project).forEach(p => {
projects.add(p)
})
return projects
}, new Set()),
];
rimrafSync(options.buildFolderPath)
fs.mkdirSync(options.buildFolderPath);
function readFile(filename) {
if (typeof filename !== 'string' || filename.length === 0) {
throw new Error('Invalid filename')
}
if (path.extname(filename) !== '.side') {
throw new Error('Only .side files are allowed')
}
// Resolve against cwd without using path.resolve/join for Semgrep compliance
const cwdUrl = pathToFileURL(process.cwd() + '/')
const fileUrl = new URL(filename, cwdUrl)
const absolutePath = fileURLToPath(fileUrl)
// Containment check: ensure the resolved path is inside cwd
const rel = path.relative(process.cwd(), absolutePath)
if (rel.startsWith('..') || path.isAbsolute(rel)) {
throw new Error('Access outside the working directory is not allowed')
}
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
throw new Error('Target file does not exist or is not a regular file')
}
return JSON.parse(fs.readFileSync(absolutePath, 'utf8'))
}
function normalizeProject(project) {
let _project = { ...project }
_project.suites.forEach(suite => {
projectProcessor.normalizeTestsInSuite({ suite, tests: _project.tests })
})
_project.url = options.baseUrl ? options.baseUrl : project.url
return _project
}
for (const sideFileName of sideFiles) {
const project = normalizeProject(readFile(sideFileName))
for (const aSuite of project.suites) {
for (const aTestCase of aSuite.tests) {
const test = project.tests.find(test => test.name === aTestCase);
var results = await codeExport.default.emit.test({
baseUrl: options.baseUrl ? options.baseUrl : project.url,
test: test,
tests: project.tests,
project: project
})
fs.writeFileSync(path.join(
options.buildFolderPath,
results.filename
), results.body);
}
}
}
var reporter = []
if (options.outputFormat && options.outputFile)
reporter = ['--reporter', options.outputFormat, '--reporter-options', 'output=' + options.outputFile]
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const sideRunnerNodeModules = path.join(__dirname, 'node_modules');
// Resolve BrowserStack SDK binary robustly with fallbacks
const sdkCandidates = [
path.join(__dirname, 'node_modules', '.bin', 'browserstack-node-sdk'),
path.join(process.cwd(), 'node_modules', '.bin', 'browserstack-node-sdk'),
// Fall back to letting the OS PATH resolve it (e.g., project-level node_modules/.bin)
'browserstack-node-sdk'
]
const sdkBin = sdkCandidates.find(p => {
try {
// If candidate is a bare command (no path separators), let it pass
if (!p.includes(path.sep)) return true
return fs.existsSync(p)
} catch {
return false
}
}) || 'browserstack-node-sdk'
if (options.debug) {
log.debug(`Using BrowserStack SDK binary: ${sdkBin}`)
}
let testSuiteProcess
try {
testSuiteProcess = spawn.sync(
sdkBin,
['mocha', '_generated', '--timeouts', String(options.testTimeout), '-g', options.filter, '--browserstack.config', options.browserstackConfig, ...reporter],
{
stdio: 'inherit',
env: {
...process.env,
testTimeout: options.testTimeout,
NODE_PATH: `${sideRunnerNodeModules}${path.delimiter}${process.env.NODE_PATH || ''}`
}
}
)
} catch (err) {
log.error(`Failed to start BrowserStack SDK at "${sdkBin}": ${err.message}`)
exit(1)
}
if (testSuiteProcess.error) {
log.error(`Failed to start BrowserStack SDK at "${sdkBin}": ${testSuiteProcess.error.message}`)
exit(1)
}
if (!options.debug) {
rimrafSync(options.buildFolderPath)
}
exit(testSuiteProcess.status ?? 1)