Skip to content

Commit

Permalink
[DEVX-1176] Set browser proxy via env var (#111)
Browse files Browse the repository at this point in the history
* Consolidate config

Drive all playwright config with a single config file. It sets defaults
and merges with customer defined playwright.config.js file (if it
exists).

* update tests

* oops, was supposed to be list reporter

* Define junit.xml path once

* Define assets path once

* Explain why we have to set runCfg.path

* style

* treat playwright config as src

* Don't merge configs just yet.

Will need some more exploration to understand more of the side effects.

* setup mock cwd and test against it more clearly

* config is in src now

* Use a more compatible way to set proxy via browser context

* also ignore https errors if proxy is set

* set proxy on context and launch

* update readme instructions

* 2 spaces for everything

* little note on setting the second proxy
  • Loading branch information
mhan83 authored Aug 27, 2021
1 parent 8a91808 commit da9a359
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 62 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ root = true
[*]

indent_style = space
indent_size = 4
indent_size = 2

end_of_line = lf
charset = utf-8
Expand Down
28 changes: 17 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,28 @@ To work on code the following dependencies are required:
You can pull the latest version of this image via:

```sh
$ docker pull saucelabs/stt-playwright-jest-node:latest
$ docker pull saucelabs/stt-playwright-node:latest
```

## Run

In order to test your changes, just build the image and run a test with an example file:
In order to test your changes, just build the image, configure saucectl to run against that image, run saucectl.


```sh
# build image
$ docker build -t saucelabs/stt-playwright-jest-node:latest --cache-from saucelabs/stt-playwright-jest-node:latest .
# start container
$ docker run --env SAUCE_USERNAME --env SAUCE_ACCESS_KEY -d --name=testrunner saucelabs/stt-playwright-jest-node:latest
# push file into container
$ docker cp ./path/to/testfile.test.js testrunner:/home/seluser/tests
# run test
$ docker exec testrunner saucectl run /home/seluser/tests
# stop container
$ docker stop testrunner
$ docker build -t saucelabs/stt-playwright-node:local --cache-from saucelabs/stt-playwright-node:latest .
```

Define `docker.image` in your saucectl config:

```yaml
docker:
image: saucelabs/stt-playwright-node:local
```
Run a saucectl suite in docker mode
```
$ saucectl run --select-suite "some suite configured for docker mode"
```
5 changes: 0 additions & 5 deletions playwright.config.js

This file was deleted.

3 changes: 1 addition & 2 deletions scripts/bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export PLAYWRIGHT_BROWSERS_PATH=$PWD/bundle/Cache/
echo $PLAYWRIGHT_BROWSERS_PATH
cp -r ./src/ ./bundle/src/
cp -r bin/ bundle/bin/
cp -r playwright.config.js bundle/
cp package.json bundle/package.json
cp package-lock.json bundle/package-lock.json
cp "$(which node)" bundle/
Expand All @@ -24,4 +23,4 @@ popd
# The upgrade to playwright 1.8.0 does not fix the missing
# DLL issue. As a workaround, we decided to ship it within
# the bundle to avoid modifiying the system image.
cp ./libs/vcruntime140_1.dll ${PLAYWRIGHT_BROWSERS_PATH}/firefox-*/firefox/
cp ./libs/vcruntime140_1.dll ${PLAYWRIGHT_BROWSERS_PATH}/firefox-*/firefox/
63 changes: 28 additions & 35 deletions src/playwright-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const { updateExportedValue } = require('sauce-testrunner-utils').saucectl;
const SauceLabs = require('saucelabs').default;
const { LOG_FILES } = require('./constants');
const fs = require('fs');
const fsExtra = require('fs-extra');
const glob = require('glob');
const convert = require('xml-js');

Expand All @@ -18,7 +17,7 @@ const { getAbsolutePath, getArgs, exec } = utils;
// Path has to match the value of the Dockerfile label com.saucelabs.job-info !
const SAUCECTL_OUTPUT_FILE = '/tmp/output.json';

async function createJob (suiteName, hasPassed, startTime, endTime, args, playwright, metrics, region, metadata, saucectlVersion) {
async function createJob (suiteName, hasPassed, startTime, endTime, args, playwright, metrics, region, metadata, saucectlVersion, assetsDir) {
const tld = region === 'staging' ? 'net' : 'com';
const api = new SauceLabs({
user: process.env.SAUCE_USERNAME,
Expand All @@ -45,10 +44,10 @@ async function createJob (suiteName, hasPassed, startTime, endTime, args, playwr

// Take the 1st webm video we find and translate it video.mp4
// TODO: We need to translate all .webm to .mp4 and combine them into one video.mp4
const webmFiles = glob.sync(path.join(cwd, '__assets__', '**', '*.webm'));
const webmFiles = glob.sync(path.join(assetsDir, '**', '*.webm'));
let videoLocation;
if (webmFiles.length > 0) {
videoLocation = path.join(cwd, '__assets__', 'video.mp4');
videoLocation = path.join(assetsDir, 'video.mp4');
try {
await exec(`ffmpeg -i ${webmFiles[0]} ${videoLocation}`, {suppressLogs: true});
} catch (e) {
Expand All @@ -59,7 +58,7 @@ async function createJob (suiteName, hasPassed, startTime, endTime, args, playwr

let files = [
path.join(cwd, 'console.log'),
path.join(cwd, '__assets__', 'junit.xml'), // TOOD: Should add junit.xml.json as well
path.join(assetsDir, 'junit.xml'),
...containerLogFiles
];

Expand All @@ -68,7 +67,7 @@ async function createJob (suiteName, hasPassed, startTime, endTime, args, playwr
if (_.isEmpty(mt.data)) {
continue;
}
let mtFile = path.join(cwd, '__assets__', mt.name);
let mtFile = path.join(assetsDir, mt.name);
fs.writeFileSync(mtFile, JSON.stringify(mt.data, ' ', 2));
files.push(mtFile);
}
Expand Down Expand Up @@ -98,15 +97,14 @@ async function createJob (suiteName, hasPassed, startTime, endTime, args, playwr
return sessionId;
}

function generateJunitfile (cwd, suiteName, browserName, platformName) {
const junitPath = path.join(cwd, '__assets__', `junit.xml`);
if (!fs.existsSync(junitPath)) {
function generateJunitfile (sourceFile, suiteName, browserName, platformName) {
if (!fs.existsSync(sourceFile)) {
return;
}
let result;
let opts = {compact: true, spaces: 4};
try {
const xmlData = fs.readFileSync(junitPath, 'utf8');
const xmlData = fs.readFileSync(sourceFile, 'utf8');
if (!xmlData) {
return;
}
Expand Down Expand Up @@ -190,17 +188,17 @@ function generateJunitfile (cwd, suiteName, browserName, platformName) {
try {
opts.textFn = escapeXML;
let xmlResult = convert.js2xml(result, opts);
fs.writeFileSync(path.join(cwd, '__assets__', 'junit.xml'), xmlResult);
fs.writeFileSync(sourceFile, xmlResult);
} catch (err) {
console.error(err);
}
}


async function runReporter ({ suiteName, hasPassed, startTime, endTime, args, playwright, metrics, region, metadata, saucectlVersion }) {
async function runReporter ({ suiteName, hasPassed, startTime, endTime, args, playwright, metrics, region, metadata, saucectlVersion, assetsDir }) {
let jobDetailsUrl, reportingSucceeded = false;
try {
let sessionId = await createJob(suiteName, hasPassed, startTime, endTime, args, playwright, metrics, region, metadata, saucectlVersion);
let sessionId = await createJob(suiteName, hasPassed, startTime, endTime, args, playwright, metrics, region, metadata, saucectlVersion, assetsDir);
let domain;
const tld = region === 'staging' ? 'net' : 'com';
switch (region) {
Expand All @@ -222,34 +220,32 @@ async function runReporter ({ suiteName, hasPassed, startTime, endTime, args, pl
}

async function run (nodeBin, runCfgPath, suiteName) {
const assetsDir = path.join(process.cwd(), '__assets__');
const junitFile = path.join(assetsDir, 'junit.xml');

runCfgPath = getAbsolutePath(runCfgPath);
const runCfg = await loadRunConfig(runCfgPath);
runCfg.path = runCfgPath;
const cwd = process.cwd();

const suite = _.find(runCfg.suites, ({name}) => name === suiteName);
if (!suite) {
throw new Error(`Could not find suite named '${suiteName}'`);
}

const projectPath = path.dirname(runCfg.path);
const projectPath = path.dirname(runCfgPath);
if (!fs.existsSync(projectPath)) {
throw new Error(`Could not find projectPath directory: '${projectPath}'`);
}

// Copy our runner's playwright config to a custom location in order to
// preserve the customer's config which we may want to load in the future
const configFile = path.join(projectPath, 'custom.config.js');
fs.copyFileSync(path.join(__dirname, 'playwright.config.js'), configFile);

const defaultArgs = {
headed: process.env.SAUCE_VM ? true : false,
output: path.join(cwd, '__assets__'),
reporter: 'junit,list',
output: assetsDir,
config: configFile,
};

if (!process.env.SAUCE_VM) {
// Copy our own playwright configuration to the project folder (to enable video recording),
// as we currently don't support having a user provided playwright configuration yet.
fs.copyFileSync(path.join(__dirname, '..', 'playwright.config.js'), path.join(projectPath, 'playwright.config.js'));
defaultArgs.config = path.join(projectPath, 'playwright.config.js');
}

const playwrightBin = path.join(__dirname, '..', 'node_modules', '@playwright', 'test', 'lib', 'cli', 'cli.js');
const procArgs = [
playwrightBin, 'test'
Expand Down Expand Up @@ -281,12 +277,15 @@ async function run (nodeBin, runCfgPath, suiteName) {
let env = {
...process.env,
...suite.env,
PLAYWRIGHT_JUNIT_OUTPUT_NAME: path.join(cwd, '__assets__', 'junit.xml'),
PLAYWRIGHT_JUNIT_OUTPUT_NAME: junitFile,
FORCE_COLOR: 0,
};

// Install NPM dependencies
let metrics = [];

// runCfg.path must be set for prepareNpmEnv to find node_modules. :(
runCfg.path = runCfgPath;
let npmMetrics = await prepareNpmEnv(runCfg);
metrics.push(npmMetrics);

Expand All @@ -308,13 +307,7 @@ async function run (nodeBin, runCfgPath, suiteName) {
console.error(`Could not complete job. Reason: ${e}`);
}

// Move to __assets__
const files = glob.sync(path.join(projectPath, 'test-results', '*')) || [];
for (const file of files) {
fsExtra.moveSync(file, path.join(cwd, '__assets__', path.basename(file)));
}

generateJunitfile(cwd, suiteName, args.param.browser, args.platformName);
generateJunitfile(junitFile, suiteName, args.param.browser, args.platformName);

// If it's a VM, don't try to upload the assets
if (process.env.SAUCE_VM) {
Expand All @@ -328,7 +321,7 @@ async function run (nodeBin, runCfgPath, suiteName) {

const saucectlVersion = process.env.SAUCE_SAUCECTL_VERSION;
const region = (runCfg.sauce && runCfg.sauce.region) || 'us-west-1';
await runReporter({ suiteName, hasPassed, startTime, endTime, args, playwright: runCfg.playwright, metrics, region, metadata: runCfg.sauce.metadata, saucectlVersion});
await runReporter({ suiteName, hasPassed, startTime, endTime, args, playwright: runCfg.playwright, metrics, region, metadata: runCfg.sauce.metadata, saucectlVersion, assetsDir});
return hasPassed;
}

Expand Down
26 changes: 26 additions & 0 deletions src/playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const process = require('process');

const defaults = {
use: {
headed: process.env.SAUCE_VM ? true : false,
video: process.env.SAUCE_VM ? 'off' : 'on',
},
reporter: [
['list'],
// outputFile is set by playwright-runner.js as an env variable. The runner needs to process it
// so better for it to set the output path
['junit'],
],
};

if ('HTTP_PROXY' in process.env && process.env.HTTP_PROXY !== '') {
const proxy = {
server: process.env.HTTP_PROXY,
};

defaults.use.contextOptions = { proxy, ignoreHTTPSErrors: true };
// Need to set the browser launch option as well, it is a hard requirement when testing chromium + windows.
defaults.use.launchOptions = { proxy, ignoreHTTPSErrors: true };
}

module.exports = defaults;
17 changes: 9 additions & 8 deletions tests/unit/src/playwright-runner.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const fs = require('fs');
const glob = require('glob');
const testRunnerUtils = require('sauce-testrunner-utils');

const MOCK_CWD = '/fake/runner';

describe('playwright-runner', function () {
const baseRunCfg = {
playwright: {
Expand Down Expand Up @@ -54,7 +56,7 @@ describe('playwright-runner', function () {
return playwrightProc;
});
fsExistsMock.mockImplementation((url) => url.startsWith('/bad/path') ? false : true);
cwdMock.mockReturnValue('/fake/runner');
cwdMock.mockReturnValue(MOCK_CWD);
process.env = {
SAUCE_TAGS: 'tag-one,tag-two',
HELLO: 'world',
Expand All @@ -66,32 +68,31 @@ describe('playwright-runner', function () {
it('should run playwright test as a spawn command in VM', async function () {
process.env.SAUCE_VM = 'truthy';
testRunnerUtils.loadRunConfig.mockReturnValue({...baseRunCfg});
await run('/fake/path/to/node', '/fake/runner/path', 'basic-js');
await run('/fake/path/to/node', path.join(MOCK_CWD, 'sauce-runner.json'), 'basic-js');
glob.sync.mockReturnValueOnce([]);
const [[nodeBin, procArgs, spawnArgs]] = spawnMock.mock.calls;
procArgs[0] = path.basename(procArgs[0]);
spawnArgs.cwd = path.basename(spawnArgs.cwd);
spawnArgs.env.PLAYWRIGHT_JUNIT_OUTPUT_NAME = path.basename(spawnArgs.env.PLAYWRIGHT_JUNIT_OUTPUT_NAME);
expect(nodeBin).toMatch('/fake/path/to/node');
expect(procArgs).toMatchObject([
'cli.js',
'test',
'--headed',
'--output',
'/fake/runner/__assets__',
'--reporter',
'junit,list',
path.join(MOCK_CWD, '__assets__'),
'--config',
path.join(MOCK_CWD, 'custom.config.js'),
'--browser',
'chromium',
'--headed',
'**/*.spec.js',
'**/*.test.js',
]);
expect(spawnArgs).toMatchObject({
'cwd': 'runner',
'env': {
'HELLO': 'world',
'PLAYWRIGHT_JUNIT_OUTPUT_NAME': 'junit.xml',
'SAUCE_TAGS': 'tag-one,tag-two',
'PLAYWRIGHT_JUNIT_OUTPUT_NAME': path.join(MOCK_CWD, '__assets__', 'junit.xml'),
},
'stdio': 'inherit',
});
Expand Down

0 comments on commit da9a359

Please sign in to comment.