From da9a35996e9cb0c4b8420fa373ad3f864f2dd845 Mon Sep 17 00:00:00 2001 From: Mike Han <56001373+mhan83@users.noreply.github.com> Date: Thu, 26 Aug 2021 19:41:10 -0600 Subject: [PATCH] [DEVX-1176] Set browser proxy via env var (#111) * 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 --- .editorconfig | 2 +- README.md | 28 ++++++----- playwright.config.js | 5 -- scripts/bundle.sh | 3 +- src/playwright-runner.js | 63 +++++++++++------------- src/playwright.config.js | 26 ++++++++++ tests/unit/src/playwright-runner.spec.js | 17 ++++--- 7 files changed, 82 insertions(+), 62 deletions(-) delete mode 100644 playwright.config.js create mode 100644 src/playwright.config.js diff --git a/.editorconfig b/.editorconfig index ed314e6d..08f65f71 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,7 +7,7 @@ root = true [*] indent_style = space -indent_size = 4 +indent_size = 2 end_of_line = lf charset = utf-8 diff --git a/README.md b/README.md index 0ecb78ff..5e820db2 100644 --- a/README.md +++ b/README.md @@ -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" ``` diff --git a/playwright.config.js b/playwright.config.js deleted file mode 100644 index 9072a523..00000000 --- a/playwright.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - use: { - video: 'on', - }, -}; \ No newline at end of file diff --git a/scripts/bundle.sh b/scripts/bundle.sh index 05a1380a..44f12a0a 100644 --- a/scripts/bundle.sh +++ b/scripts/bundle.sh @@ -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/ @@ -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/ \ No newline at end of file +cp ./libs/vcruntime140_1.dll ${PLAYWRIGHT_BROWSERS_PATH}/firefox-*/firefox/ diff --git a/src/playwright-runner.js b/src/playwright-runner.js index 9511608d..d7fbfce3 100644 --- a/src/playwright-runner.js +++ b/src/playwright-runner.js @@ -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'); @@ -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, @@ -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) { @@ -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 ]; @@ -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); } @@ -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; } @@ -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) { @@ -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' @@ -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); @@ -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) { @@ -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; } diff --git a/src/playwright.config.js b/src/playwright.config.js new file mode 100644 index 00000000..2b141fdc --- /dev/null +++ b/src/playwright.config.js @@ -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; diff --git a/tests/unit/src/playwright-runner.spec.js b/tests/unit/src/playwright-runner.spec.js index 8e27d765..b480ad29 100644 --- a/tests/unit/src/playwright-runner.spec.js +++ b/tests/unit/src/playwright-runner.spec.js @@ -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: { @@ -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', @@ -66,23 +68,22 @@ 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', ]); @@ -90,8 +91,8 @@ describe('playwright-runner', function () { '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', });