From d644defe63691a13727871c009c691fd3d17b035 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Thu, 26 Oct 2023 03:04:08 +0300 Subject: [PATCH 1/4] test: get rid of chrome-pwt and add e2e tests for tinder --- .../components/controls/control-button.tsx | 8 +- .../controls/strict-match-filter-input.jsx | 1 + .../controls/test-name-filter-input.jsx | 1 + .../modals/screenshot-accepter/body.jsx | 2 +- .../modals/screenshot-accepter/header.jsx | 5 +- lib/static/components/progress-bar/index.jsx | 4 +- lib/static/components/section/body/index.jsx | 1 + lib/static/components/state/index.jsx | 1 + test/func/tests/.hermione.conf.js | 12 +- test/func/tests/common-gui/index.hermione.js | 61 ++------ .../tests/common-tinder/index.hermione.js | 134 ++++++++++++++++++ .../tests/common/tests-details.hermione.js | 2 +- test/func/tests/local.hermione.conf.js | 34 +++++ test/func/tests/package.json | 10 +- .../screens/1bb949f/chrome/retry-switcher.png | Bin 0 -> 517 bytes .../screens/bdf4a21/chrome/retry-switcher.png | Bin 0 -> 493 bytes .../c0db305/chrome/details summary.png | Bin 6496 -> 6396 bytes test/func/tests/utils.js | 60 +++++++- 18 files changed, 263 insertions(+), 73 deletions(-) create mode 100644 test/func/tests/common-tinder/index.hermione.js create mode 100644 test/func/tests/local.hermione.conf.js create mode 100644 test/func/tests/screens/1bb949f/chrome/retry-switcher.png create mode 100644 test/func/tests/screens/bdf4a21/chrome/retry-switcher.png diff --git a/lib/static/components/controls/control-button.tsx b/lib/static/components/controls/control-button.tsx index b089750e7..39fa189f1 100644 --- a/lib/static/components/controls/control-button.tsx +++ b/lib/static/components/controls/control-button.tsx @@ -13,6 +13,7 @@ interface ControlButtonProps { isDisabled?: boolean; isRunning?: boolean; extendClassNames?: string | string[]; + dataTestId?: string | number; } export default class ControlButton extends Component { @@ -26,7 +27,8 @@ export default class ControlButton extends Component { isSuiteControl: PropTypes.bool, isControlGroup: PropTypes.bool, isRunning: PropTypes.bool, - extendClassNames: PropTypes.oneOfType([PropTypes.array, PropTypes.string]) + extendClassNames: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), + dataTestId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) }; render(): JSX.Element { @@ -40,7 +42,8 @@ export default class ControlButton extends Component { isControlGroup, isDisabled = false, isRunning = false, - extendClassNames + extendClassNames, + dataTestId } = this.props; const className = classNames( @@ -58,6 +61,7 @@ export default class ControlButton extends Component { onClick={handler} className={className} disabled={isDisabled} + data-test-id={dataTestId} > {label} ; diff --git a/lib/static/components/controls/strict-match-filter-input.jsx b/lib/static/components/controls/strict-match-filter-input.jsx index bf785041b..14dd226fd 100644 --- a/lib/static/components/controls/strict-match-filter-input.jsx +++ b/lib/static/components/controls/strict-match-filter-input.jsx @@ -23,6 +23,7 @@ const StrictMatchFilterInput = ({strictMatchFilter, actions}) => { label="Strict match" onChange={onChange} checked={checked} + data-test-id="header-strict-match" /> ); diff --git a/lib/static/components/controls/test-name-filter-input.jsx b/lib/static/components/controls/test-name-filter-input.jsx index 33ce6f076..f11facaa4 100644 --- a/lib/static/components/controls/test-name-filter-input.jsx +++ b/lib/static/components/controls/test-name-filter-input.jsx @@ -27,6 +27,7 @@ const TestNameFilterInput = ({actions, testNameFilter: testNameFilterProp}) => { value={testNameFilter} placeholder="filter by test name" onChange={onChange} + data-test-id="header-test-name-filter" /> ); }; diff --git a/lib/static/components/modals/screenshot-accepter/body.jsx b/lib/static/components/modals/screenshot-accepter/body.jsx index 76f0a0602..430ec4f45 100644 --- a/lib/static/components/modals/screenshot-accepter/body.jsx +++ b/lib/static/components/modals/screenshot-accepter/body.jsx @@ -51,7 +51,7 @@ class ScreenshotAccepterBody extends Component { return (
- {testName} + {testName} {'/'} {browserName} {'/'} diff --git a/lib/static/components/modals/screenshot-accepter/header.jsx b/lib/static/components/modals/screenshot-accepter/header.jsx index 8d05ff6ca..d9008265c 100644 --- a/lib/static/components/modals/screenshot-accepter/header.jsx +++ b/lib/static/components/modals/screenshot-accepter/header.jsx @@ -143,6 +143,7 @@ export default class ScreenshotAccepterHeader extends Component { isDisabled={images.length === 0} extendClassNames="screenshot-accepter__accept-btn" handler={this.handleScreenshotAccept} + dataTestId="screenshot-accepter-accept" /> - +
diff --git a/lib/static/components/progress-bar/index.jsx b/lib/static/components/progress-bar/index.jsx index d2c171c08..aa0e9ff40 100644 --- a/lib/static/components/progress-bar/index.jsx +++ b/lib/static/components/progress-bar/index.jsx @@ -2,11 +2,11 @@ import React from 'react'; import './index.styl'; -export default ({done, total}) => { +export default ({done, total, dataTestId}) => { const percent = (done / total).toFixed(2) * 100; return ( - + ); diff --git a/lib/static/components/section/body/index.jsx b/lib/static/components/section/body/index.jsx index 3c33e3136..f34e4b437 100644 --- a/lib/static/components/section/body/index.jsx +++ b/lib/static/components/section/body/index.jsx @@ -67,6 +67,7 @@ class Body extends Component { isSuiteControl={true} isDisabled={running} handler={this.onTestRetry} + dataTestId="test-retry" /> ) diff --git a/lib/static/components/state/index.jsx b/lib/static/components/state/index.jsx index dc8b64164..384d9cc6b 100644 --- a/lib/static/components/state/index.jsx +++ b/lib/static/components/state/index.jsx @@ -101,6 +101,7 @@ class State extends Component { isDisabled={isScreenshotAccepterDisabled} extendClassNames="screenshot-accepter__arrows-open-btn" handler={() => this.toggleModal()} + data-test-id="test-switch-accept-mode" /> ); diff --git a/test/func/tests/.hermione.conf.js b/test/func/tests/.hermione.conf.js index 5d72a170a..21ee6c65e 100644 --- a/test/func/tests/.hermione.conf.js +++ b/test/func/tests/.hermione.conf.js @@ -24,26 +24,20 @@ const commonConfig = getCommonConfig(__dirname); const config = _.merge(commonConfig, { baseUrl: `http://${serverHost}:${serverPort}/fixtures/${projectUnderTest}/report/index.html`, - browsers: { - // TODO: this is a hack to be able to have 2 sets of screenshots, for hermione-based report and pwt-based report - // currently, those have weird tiny diffs. Would be nice to figure out the cause and have common screenshots. - 'chrome-pwt': {...commonConfig.browsers.chrome} - }, - sets: { common: { files: 'common/**/*.hermione.js' }, 'common-gui': { - browsers: ['chrome'], files: 'common-gui/**/*.hermione.js' }, + 'common-tinder': { + files: 'common-tinder/**/*.hermione.js' + }, eye: { - browsers: ['chrome'], files: 'eye/**/*.hermione.js', }, plugins: { - browsers: ['chrome'], files: 'plugins/**/*.hermione.js' } }, diff --git a/test/func/tests/common-gui/index.hermione.js b/test/func/tests/common-gui/index.hermione.js index aee49712e..4bc2dfc99 100644 --- a/test/func/tests/common-gui/index.hermione.js +++ b/test/func/tests/common-gui/index.hermione.js @@ -6,7 +6,15 @@ const {promisify} = require('util'); const treeKill = promisify(require('tree-kill')); const {PORTS} = require('../../utils/constants'); -const {getTestSectionByNameSelector, getSpoilerByNameSelector, getElementWithTextSelector, hideScreenshots} = require('../utils'); +const { + getTestSectionByNameSelector, + getSpoilerByNameSelector, + getElementWithTextSelector, + hideScreenshots, + runGui, + waitForFsChanges, + getFsDiffFromVcs +} = require('../utils'); const serverHost = process.env.SERVER_HOST ?? 'host.docker.internal'; @@ -18,57 +26,6 @@ const reportDir = path.join(projectDir, 'report'); const reportBackupDir = path.join(projectDir, 'report-backup'); const screensDir = path.join(projectDir, 'screens'); -const runGui = async () => { - return new Promise((resolve, reject) => { - const child = childProcess.spawn('npm', ['run', 'gui'], {cwd: projectDir}); - - let processKillTimeoutId = setTimeout(() => { - treeKill(child.pid).then(() => { - reject(new Error('Couldn\'t start GUI: timed out')); - }); - }, 3000); - - child.stdout.on('data', (data) => { - if (data.toString().includes('GUI is running at')) { - clearTimeout(processKillTimeoutId); - resolve(child); - } - }); - - child.stderr.on('data', (data) => { - console.error(`stderr: ${data}`); - }); - - child.on('close', (code) => { - if (code !== 0) { - reject(new Error(`GUI process exited with code ${code}`)); - } - }); - }); -}; - -const getFsDiffFromVcs = (directory) => childProcess.execSync('git status . --porcelain=v2', {cwd: directory}); - -const waitForFsChanges = async (dirPath, condition = (output) => output.length > 0, {timeout = 1000, interval = 50} = {}) => { - let isTimedOut = false; - - const timeoutId = setTimeout(() => { - isTimedOut = true; - throw new Error(`Timed out while waiting for fs changes in ${dirPath} for ${timeout}ms`); - }, timeout); - - while (!isTimedOut) { - const output = getFsDiffFromVcs(dirPath); - - if (condition(output)) { - clearTimeout(timeoutId); - return; - } - - await new Promise(resolve => setTimeout(resolve, interval)); - } -}; - // These tests should not be launched in parallel describe('GUI mode', () => { let guiProcess; diff --git a/test/func/tests/common-tinder/index.hermione.js b/test/func/tests/common-tinder/index.hermione.js new file mode 100644 index 000000000..28046b91c --- /dev/null +++ b/test/func/tests/common-tinder/index.hermione.js @@ -0,0 +1,134 @@ +const childProcess = require('child_process'); +const fs = require('fs/promises'); +const path = require('path'); +const treeKill = require('tree-kill'); + +const {PORTS} = require('../../utils/constants'); +const {hideScreenshots, runGui, waitForFsChanges, getFsDiffFromVcs} = require('../utils'); + +const serverHost = process.env.SERVER_HOST ?? 'host.docker.internal'; + +const projectName = process.env.PROJECT_UNDER_TEST; +const projectDir = path.resolve(__dirname, '../../fixtures', projectName); +const guiUrl = `http://${serverHost}:${PORTS[projectName].gui}`; + +const reportDir = path.join(projectDir, 'report'); +const reportBackupDir = path.join(projectDir, 'report-backup'); +const screensDir = path.join(projectDir, 'screens'); + +// These tests should not be launched in parallel +describe('Tinder mode', () => { + let guiProcess; + + beforeEach(async ({browser}) => { + await fs.cp(reportDir, reportBackupDir, {recursive: true}); + + guiProcess = await runGui(); + + await browser.url(guiUrl); + await browser.$('button*=Expand all').click(); + + await browser.$('button*=Switch accept mode').click(); + }); + + afterEach(async () => { + await treeKill(guiProcess.pid); + + await fs.rm(reportDir, {recursive: true, force: true, maxRetries: 3}); + await fs.rename(reportBackupDir, reportDir); + + childProcess.execSync('git restore .', {cwd: screensDir}); + childProcess.execSync('git clean -dfx .', {cwd: screensDir}); + }); + + describe(`accepting screenshot`, () => { + beforeEach(async ({browser}) => { + const testFullName = await browser.$('span[data-test-id="screenshot-accepter-test-name"]').getText(); + + const acceptButton = await browser.$('button[data-test-id="screenshot-accepter-accept"]'); + await acceptButton.click(); + + await browser.waitUntil(async () => { + const progress = await browser.$('span[data-test-id="screenshot-accepter-progress-bar"]').getAttribute('data-content'); + + return progress === '1/2'; + }, {interval: 100}); + + const switchAcceptModeButton = await browser.$('button[data-test-id="screenshot-accepter-switch-accept-mode"]'); + await switchAcceptModeButton.click(); + + const testNameFilterInput = await browser.$('input[data-test-id="header-test-name-filter"]'); + + await testNameFilterInput.setValue(testFullName); + await browser.$('div[data-test-id="header-strict-match"]').click(); + + await waitForFsChanges(screensDir); + }); + + it('should create a successful retry', async ({browser}) => { + const retrySwitcher = await browser.$(`(//button[@data-test-id="retry-switcher"])[last()]`); + await hideScreenshots(browser); + + await retrySwitcher.assertView('retry-switcher'); + }); + + it('should make the test pass on next run', async ({browser}) => { + const retryButton = await browser.$('button[data-test-id="test-retry"]'); + + // TODO: find a correct sign to wait for. Issue is that retry button is totally clickable, but doesn't + // work right away after switch accept mode and applying filtering for some reason. + await browser.pause(500); + await retryButton.click(); + + await retryButton.waitForClickable({reverse: true, timeout: 10000}); + await retryButton.waitForClickable({timeout: 10000}); + + const retrySwitcher = await browser.$(`(//button[@data-test-id="retry-switcher"])[last()]`); + await hideScreenshots(browser); + + await retrySwitcher.assertView('retry-switcher'); + }); + }); + + describe(`undo accepting screenshot`, () => { + it('should leave project files intact', async ({browser}) => { + const acceptButton = await browser.$('button[data-test-id="screenshot-accepter-accept"]'); + await acceptButton.click(); + + await browser.waitUntil(async () => { + const progress = await browser.$('span[data-test-id="screenshot-accepter-progress-bar"]').getAttribute('data-content'); + + return progress === '1/2'; + }, {interval: 100}); + + await waitForFsChanges(screensDir); + const fsDiffBeforeUndo = getFsDiffFromVcs(screensDir); + + const undoButton = await browser.$('button[data-test-id="screenshot-accepter-undo"]'); + await undoButton.click(); + + await waitForFsChanges(screensDir, (output) => output.length === 0); + + const fsDiffAfterUndo = getFsDiffFromVcs(screensDir); + + expect(fsDiffBeforeUndo.length > 0).toBeTruthy(); + expect(fsDiffAfterUndo.length === 0).toBeTruthy(); + }); + }); + + it('should show success screen after accepting all screenshots', async ({browser}) => { + const acceptButton = await browser.$('button[data-test-id="screenshot-accepter-accept"]'); + + for (let i = 1; i <= 2; i++) { + await acceptButton.click(); + + await browser.waitUntil(async () => { + const progress = await browser.$('span[data-test-id="screenshot-accepter-progress-bar"]').getAttribute('data-content'); + + return progress === `${i}/2`; + }, {interval: 100}); + } + + await expect(await browser.$('div*=All screenshots are accepted')).toBeDisplayed(); + }); +}); diff --git a/test/func/tests/common/tests-details.hermione.js b/test/func/tests/common/tests-details.hermione.js index 60832fa0a..bf75f8cc9 100644 --- a/test/func/tests/common/tests-details.hermione.js +++ b/test/func/tests/common/tests-details.hermione.js @@ -21,7 +21,7 @@ describe('Test details', function() { it('should prevent details summary overflow', async ({browser}) => { const selector = getTestSectionByNameSelector('test with long error message') + - `//summary[.${getElementWithTextSelector('span', 'message')}/..]`; + `//summary[.${getElementWithTextSelector('span', 'stack')}/..]`; await browser.$(selector).waitForDisplayed(); await browser.assertView('details summary', selector); diff --git a/test/func/tests/local.hermione.conf.js b/test/func/tests/local.hermione.conf.js new file mode 100644 index 000000000..fdd79fbf5 --- /dev/null +++ b/test/func/tests/local.hermione.conf.js @@ -0,0 +1,34 @@ +/* +This hermione config may be useful for running tests on a local, non-headless Chromium browser while debugging. + +Use it as follows: +npm run gui:hermione-common -- -c local.hermione.conf.js +*/ + +process.env.SERVER_HOST = 'localhost'; + +const _ = require('lodash'); + +const mainConfig = require('./.hermione.conf.js'); + +// Make sure to adjust chromium binary path to a desired Chromium location. +const CHROME_BINARY_PATH = '/Applications/Chromium.app/Contents/MacOS/Chromium'; + +const config = _.merge(mainConfig, { + // Default chromedriver host and port. Adjust to your needs. + gridUrl: 'http://localhost:9515/', + + browsers: { + chrome: { + desiredCapabilities: { + 'goog:chromeOptions': { + args: ['no-sandbox', 'hide-scrollbars'], + binary: CHROME_BINARY_PATH + } + }, + waitTimeout: 3000 + } + } +}); + +module.exports = config; diff --git a/test/func/tests/package.json b/test/func/tests/package.json index 0d4765bea..c55294af5 100644 --- a/test/func/tests/package.json +++ b/test/func/tests/package.json @@ -3,16 +3,18 @@ "version": "0.0.0", "private": true, "scripts": { - "gui:hermione-common": "PROJECT_UNDER_TEST=hermione npx hermione --set common -b chrome gui", + "gui:hermione-common": "PROJECT_UNDER_TEST=hermione npx hermione --set common gui", "gui:hermione-eye": "PROJECT_UNDER_TEST=hermione-eye npx hermione --no --set eye gui", "gui:hermione-gui": "PROJECT_UNDER_TEST=hermione-gui npx hermione --no --set common-gui gui", - "gui:playwright": "PROJECT_UNDER_TEST=playwright npx hermione --set common -b chrome-pwt gui", + "gui:playwright": "PROJECT_UNDER_TEST=playwright npx hermione --set common gui", "gui:plugins": "PROJECT_UNDER_TEST=plugins SERVER_PORT=8084 npx hermione --set plugins gui", - "hermione:hermione-common": "PROJECT_UNDER_TEST=hermione SERVER_PORT=8061 npx hermione --set common -b chrome", + "gui:hermione-tinder": "PROJECT_UNDER_TEST=hermione-gui SERVER_PORT=8084 npx hermione --set common-tinder gui", + "hermione:hermione-common": "PROJECT_UNDER_TEST=hermione SERVER_PORT=8061 npx hermione --set common", "hermione:hermione-eye": "PROJECT_UNDER_TEST=hermione-eye SERVER_PORT=8062 npx hermione --set eye", "hermione:hermione-gui": "PROJECT_UNDER_TEST=hermione-gui SERVER_PORT=8063 npx hermione --no --set common-gui", - "hermione:playwright": "PROJECT_UNDER_TEST=playwright SERVER_PORT=8065 npx hermione --set common -b chrome-pwt", + "hermione:playwright": "PROJECT_UNDER_TEST=playwright SERVER_PORT=8065 npx hermione --set common", "hermione:plugins": "PROJECT_UNDER_TEST=plugins SERVER_PORT=8064 npx hermione --set plugins", + "hermione:hermione-tinder": "PROJECT_UNDER_TEST=hermione-gui SERVER_PORT=8084 npx hermione --set common-tinder", "test": "run-s hermione:*" } } diff --git a/test/func/tests/screens/1bb949f/chrome/retry-switcher.png b/test/func/tests/screens/1bb949f/chrome/retry-switcher.png new file mode 100644 index 0000000000000000000000000000000000000000..996698da73745c2a148612dec1b3ad1645fcad81 GIT binary patch literal 517 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 R^p^>lj^=%h0yUeU{{bTGOCbON diff --git a/test/func/tests/utils.js b/test/func/tests/utils.js index 5d045f93e..980516b61 100644 --- a/test/func/tests/utils.js +++ b/test/func/tests/utils.js @@ -1,3 +1,7 @@ +const childProcess = require('child_process'); +const {promisify} = require('util'); +const treeKill = promisify(require('tree-kill')); + /** Returns a div, which wraps the whole test section with specified name */ const getTestSectionByNameSelector = (testName) => `//div[contains(text(),'${testName}')]/..`; @@ -28,6 +32,57 @@ const hideScreenshots = async (browser) => { }); }; +const runGui = async (projectDir) => { + return new Promise((resolve, reject) => { + const child = childProcess.spawn('npm', ['run', 'gui'], {cwd: projectDir}); + + let processKillTimeoutId = setTimeout(() => { + treeKill(child.pid).then(() => { + reject(new Error('Couldn\'t start GUI: timed out')); + }); + }, 3000); + + child.stdout.on('data', (data) => { + if (data.toString().includes('GUI is running at')) { + clearTimeout(processKillTimeoutId); + resolve(child); + } + }); + + child.stderr.on('data', (data) => { + console.error(`stderr: ${data}`); + }); + + child.on('close', (code) => { + if (code !== 0) { + reject(new Error(`GUI process exited with code ${code}`)); + } + }); + }); +}; + +const getFsDiffFromVcs = (directory) => childProcess.execSync('git status . --porcelain=v2', {cwd: directory}); + +const waitForFsChanges = async (dirPath, condition = (output) => output.length > 0, {timeout = 1000, interval = 50} = {}) => { + let isTimedOut = false; + + const timeoutId = setTimeout(() => { + isTimedOut = true; + throw new Error(`Timed out while waiting for fs changes in ${dirPath} for ${timeout}ms`); + }, timeout); + + while (!isTimedOut) { + const output = getFsDiffFromVcs(dirPath); + + if (condition(output)) { + clearTimeout(timeoutId); + return; + } + + await new Promise(resolve => setTimeout(resolve, interval)); + } +}; + module.exports = { getTestSectionByNameSelector, getTestStateByNameSelector, @@ -35,5 +90,8 @@ module.exports = { getElementWithTextSelector, getSpoilerByNameSelector, hideHeader, - hideScreenshots + hideScreenshots, + runGui, + getFsDiffFromVcs, + waitForFsChanges }; From 6e4733aacca5bbbd61c7dc175b43aa438aa04503 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Mon, 30 Oct 2023 03:37:26 +0300 Subject: [PATCH 2/4] fix: fix gui tests and update hermione local config --- test/func/tests/common-gui/index.hermione.js | 2 +- test/func/tests/common-tinder/index.hermione.js | 2 +- test/func/tests/local.hermione.conf.js | 11 ++++------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/test/func/tests/common-gui/index.hermione.js b/test/func/tests/common-gui/index.hermione.js index 4bc2dfc99..ddc87b890 100644 --- a/test/func/tests/common-gui/index.hermione.js +++ b/test/func/tests/common-gui/index.hermione.js @@ -33,7 +33,7 @@ describe('GUI mode', () => { beforeEach(async ({browser}) => { await fs.cp(reportDir, reportBackupDir, {recursive: true}); - guiProcess = await runGui(); + guiProcess = await runGui(projectDir); await browser.url(guiUrl); await browser.$('button*=Expand all').click(); diff --git a/test/func/tests/common-tinder/index.hermione.js b/test/func/tests/common-tinder/index.hermione.js index 28046b91c..e17a33afe 100644 --- a/test/func/tests/common-tinder/index.hermione.js +++ b/test/func/tests/common-tinder/index.hermione.js @@ -23,7 +23,7 @@ describe('Tinder mode', () => { beforeEach(async ({browser}) => { await fs.cp(reportDir, reportBackupDir, {recursive: true}); - guiProcess = await runGui(); + guiProcess = await runGui(projectDir); await browser.url(guiUrl); await browser.$('button*=Expand all').click(); diff --git a/test/func/tests/local.hermione.conf.js b/test/func/tests/local.hermione.conf.js index fdd79fbf5..2a29299c4 100644 --- a/test/func/tests/local.hermione.conf.js +++ b/test/func/tests/local.hermione.conf.js @@ -11,19 +11,13 @@ const _ = require('lodash'); const mainConfig = require('./.hermione.conf.js'); -// Make sure to adjust chromium binary path to a desired Chromium location. -const CHROME_BINARY_PATH = '/Applications/Chromium.app/Contents/MacOS/Chromium'; - const config = _.merge(mainConfig, { - // Default chromedriver host and port. Adjust to your needs. - gridUrl: 'http://localhost:9515/', - browsers: { chrome: { + automationProtocol: 'devtools', desiredCapabilities: { 'goog:chromeOptions': { args: ['no-sandbox', 'hide-scrollbars'], - binary: CHROME_BINARY_PATH } }, waitTimeout: 3000 @@ -31,4 +25,7 @@ const config = _.merge(mainConfig, { } }); +delete config.gridUrl; +delete config.browsers.chrome.desiredCapabilities['goog:chromeOptions'].binary; + module.exports = config; From 24294d6f7fe4e6b9a7574e53746e311d22158b61 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Mon, 30 Oct 2023 03:40:02 +0300 Subject: [PATCH 3/4] fix: fix eslint issues --- test/func/tests/local.hermione.conf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/func/tests/local.hermione.conf.js b/test/func/tests/local.hermione.conf.js index 2a29299c4..f1de1cd60 100644 --- a/test/func/tests/local.hermione.conf.js +++ b/test/func/tests/local.hermione.conf.js @@ -17,7 +17,7 @@ const config = _.merge(mainConfig, { automationProtocol: 'devtools', desiredCapabilities: { 'goog:chromeOptions': { - args: ['no-sandbox', 'hide-scrollbars'], + args: ['no-sandbox', 'hide-scrollbars'] } }, waitTimeout: 3000 From e1ae1ef84ea0e40a361944c296270ef6726947d3 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Mon, 30 Oct 2023 04:09:56 +0300 Subject: [PATCH 4/4] fix: fix pwt screenshot --- .../chromium/header-success.png | Bin 3961 -> 3554 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/func/fixtures/playwright/tests/screens/failed-describe-test-with-successful-assertView-and-error/chromium/header-success.png b/test/func/fixtures/playwright/tests/screens/failed-describe-test-with-successful-assertView-and-error/chromium/header-success.png index 2baf53cb579da69edfda76e39ffc7629b1dfd9d8..3a5f9d5891c29ecdae3cde2ce7606d77fe706210 100644 GIT binary patch literal 3554 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