diff --git a/pr.codecept.js b/pr.codecept.js index 0f6a27add..272bc5850 100644 --- a/pr.codecept.js +++ b/pr.codecept.js @@ -1,5 +1,6 @@ const { Agent } = require('https'); const { pageObjects, getChunks } = require('./codeceptConfigHelper'); +const bootstrapHook = require('./tests/helper/hooks.js'); require('dotenv').config(); @@ -16,6 +17,8 @@ exports.config = { url: pmmUrl.replace(/\/(?!.*\/)$/gm, ''), restart: true, show: false, + trace: true, + keepTraceForPassedTests: false, browser: 'chromium', windowSize: '1920x1080', timeout: 20000, @@ -137,7 +140,7 @@ exports.config = { }, }, }, - bootstrap: false, + bootstrap: bootstrapHook, teardown: null, hooks: [], gherkin: {}, diff --git a/tests/QAN/details_test.js b/tests/QAN/details_test.js index da96bc065..4b168b812 100644 --- a/tests/QAN/details_test.js +++ b/tests/QAN/details_test.js @@ -53,7 +53,7 @@ if (isJenkinsGssapiJob) { } else { databaseEnvironments = [ { serviceName: 'ps_', queryTypes: ['SELECT s.first_name', 'INSERT INTO classes', 'DELETE FROM students', 'CREATE TABLE classes'], cluster: 'ps-single-dev-cluster' }, - { serviceName: 'pdpgsql_', queryTypes: ['SELECT s.first_name', 'INSERT INTO classes', 'DELETE FROM', 'CREATE TABLE classes '], cluster: '' }, + { serviceName: 'pdpgsql_pmm_17', queryTypes: ['SELECT s.first_name', 'INSERT INTO classes', 'DELETE FROM', 'CREATE TABLE classes '], cluster: '' }, { serviceName: 'rs101', queryTypes: ['db.students', 'db.runCommand', 'db.test'], cluster: 'replicaset' }, ]; } diff --git a/tests/QAN/filters_test.js b/tests/QAN/filters_test.js index 3701e3b57..32963a3ac 100644 --- a/tests/QAN/filters_test.js +++ b/tests/QAN/filters_test.js @@ -198,9 +198,7 @@ Scenario( queryAnalyticsPage.filters.selectContainFilter(serviceName); queryAnalyticsPage.data.waitForNewItemsCount(count); queryAnalyticsPage.filters.selectFilterInGroup(db2, section); - await within(queryAnalyticsPage.data.root, () => { - I.waitForText('No queries available for this combination of filters', 30); - }); + I.waitForText('No queries available for this combination of filters', 30); }, ).retry(2); diff --git a/tests/QAN/timerange_test.js b/tests/QAN/timerange_test.js index 820073f3c..498d27580 100644 --- a/tests/QAN/timerange_test.js +++ b/tests/QAN/timerange_test.js @@ -4,7 +4,11 @@ const assert = require('assert'); Feature('QAN timerange').retry(1); Before(async ({ I, queryAnalyticsPage, codeceptjsConfig }) => { - I.restartBrowser({ permissions: ['clipboard-read', 'clipboard-write'], origin: codeceptjsConfig.config.helpers.Playwright.url }); + await I.usePlaywrightTo('Grant Permissions', async ({ browserContext }) => { + await browserContext.grantPermissions(['clipboard-read', 'clipboard-write'], { + origin: codeceptjsConfig.config.helpers.Playwright.url, + }); + }); await I.usePlaywrightTo('Mock BE Responses', async ({ page }) => { await page.route('**/v1/users/me', (route) => route.fulfill({ status: 200, diff --git a/tests/configuration/permissions_test.js b/tests/configuration/permissions_test.js index 3219cafa7..5d86a788b 100644 --- a/tests/configuration/permissions_test.js +++ b/tests/configuration/permissions_test.js @@ -183,7 +183,6 @@ Data(ptSummaryRoleCheck).Scenario( await I.Authorize(username, password); I.amOnPage(homePage.url); I.waitForVisible(homePage.fields.checksPanelSelector, 30); - I.waitForVisible(homePage.fields.pmmCustomMenu, 30); I.waitForVisible(dashboardPage.graphsLocator('Monitored Nodes'), 30); I.waitForVisible(dashboardPage.graphsLocator('Monitored DB Services'), 30); diff --git a/tests/dashboards/verifyHomeDashboards_test.js b/tests/dashboards/verifyHomeDashboards_test.js index 8caa066d8..0f8955e8d 100644 --- a/tests/dashboards/verifyHomeDashboards_test.js +++ b/tests/dashboards/verifyHomeDashboards_test.js @@ -52,10 +52,6 @@ Data(panels).Scenario( // Wait for tab to open I.wait(2); I.switchToNextTab(); - // need to skip PMM tour modal window due to new tab opening - await dashboardPage.clickUpgradeModal(); - await dashboardPage.clickSkipPmmTour(); - I.waitForElement(dashboardPage.fields.dashboardTitle(dashboardName), 60); I.seeInCurrentUrl(expectedDashboard.clearUrl); diff --git a/tests/dashboards/verifyMysqlDashboards_test.js b/tests/dashboards/verifyMysqlDashboards_test.js index 261b4e0ce..ac2eb0d1d 100644 --- a/tests/dashboards/verifyMysqlDashboards_test.js +++ b/tests/dashboards/verifyMysqlDashboards_test.js @@ -153,7 +153,7 @@ Scenario.skip( Scenario( 'PMM-T324 - Verify MySQL - MySQL User Details dashboard @nightly @dashboards', async ({ I, dashboardPage }) => { - const serviceName = serviceList.find((service) => service.name.includes('ps_pmm')); + const serviceName = serviceList.find((service) => service.name.includes('ps_pmm')).name; const url = I.buildUrlWithParams(dashboardPage.mysqlUserDetailsDashboard.clearUrl, { service_name: serviceName, from: 'now-5m' }); I.amOnPage(url); diff --git a/tests/helper/grafana_helper.js b/tests/helper/grafana_helper.js index c93479125..1884cd6be 100644 --- a/tests/helper/grafana_helper.js +++ b/tests/helper/grafana_helper.js @@ -295,7 +295,7 @@ class Grafana extends Helper { await Playwright.page.locator(dropdownLocator).first().waitFor({ state: 'attached', timeout: 5000 }); await Playwright.page.locator(dropdownLocator).first().click(); - await Playwright.page.waitForTimeout(500); + await Playwright.wait(0.5); const optionLocator = Playwright.page.locator('div[role="option"] span'); @@ -310,10 +310,11 @@ class Grafana extends Helper { async isElementDisplayed(locator, timeoutInSeconds = 60) { const { Playwright } = this.helpers; - const elementLocator = Playwright.page.locator(locate(locator).toXPath()); + const context = Playwright.context || Playwright.page; + const elementLocator = context.locator(locate(locator).toXPath()); for (let i = 0; i < timeoutInSeconds; i++) { - await Playwright.page.waitForTimeout(1000); + await Playwright.wait(1); if (await elementLocator.first().isVisible()) { return true; diff --git a/tests/helper/hooks.js b/tests/helper/hooks.js new file mode 100644 index 000000000..b2407fc1b --- /dev/null +++ b/tests/helper/hooks.js @@ -0,0 +1,199 @@ +/* eslint-disable func-names */ +/* eslint-disable no-underscore-dangle */ +const { event, container } = require('codeceptjs'); + +function getSelector(locator) { + let selector = locator; + + if (!locator) return null; + + if (typeof locator === 'object') { + if (locator.xpath) { + selector = `xpath=${locator.xpath}`; + } else if (locator.css) { + selector = locator.css; + } else if (locator.value && locator.type) { + if (locator.type === 'xpath') { + selector = `xpath=${locator.value}`; + } else if (locator.type === 'css') { + selector = locator.value; + } else { + selector = locator.value; + } + } else { + selector = locator.toString(); + } + } + + if (typeof selector === 'string') { + if (selector.startsWith('{xpath:') && selector.endsWith('}')) { + return `xpath=${selector.substring(7, selector.length - 1).trim()}`; + } + + if (selector.startsWith('{css:') && selector.endsWith('}')) { + return selector.substring(5, selector.length - 1).trim(); + } + + if (selector.startsWith('$')) { + return `[data-testid="${selector.substring(1)}"]`; + } + } + + return selector; +} + +async function switchToGrafana(helper) { + const grafanaIframe = '#grafana-iframe'; + + await helper.switchTo(); + if (helper.page) await helper.page.waitForLoadState('domcontentloaded'); + + await helper.waitForVisible(grafanaIframe, 60); + await helper.switchTo(grafanaIframe); + + if (helper.page) helper.context = helper.page.frameLocator(grafanaIframe); +} + +async function resetContext(helper) { + if (helper.browserContext) { + const pages = helper.browserContext.pages(); + + if (pages.length > 0) [helper.page] = pages; + } + + await helper.switchTo(); + helper.context = null; +} + +function applyOverride(helper, methodName, wrapperFunction) { + const originalMethod = helper[methodName]; + + helper[methodName] = async function pmmMethodWrapper(...args) { + return wrapperFunction.apply(this, [originalMethod, ...args]); + }; +} + +function applyContextOverride(helper, methodName, contextAction) { + applyOverride(helper, methodName, async function (original, ...args) { + if (helper.context) return contextAction.apply(this, args); + + return original.apply(this, args); + }); +} + +module.exports = function pmmGrafanaIframeHook() { + const helper = container.helpers('Playwright'); + const navigationMethods = ['amOnPage', 'refreshPage', 'openNewTab', 'switchToNextTab', 'switchToPreviousTab']; + const noIframeMethods = ['openNewTab']; + const noIframeUrls = ['login', 'help', 'updates']; + + navigationMethods.forEach((methodName) => { + applyOverride(helper, methodName, async function (original, ...args) { + await resetContext(helper); + await original.apply(this, args); + + if (methodName === 'amOnPage' && noIframeUrls.some((url) => args[0].includes(url))) return; + + if (noIframeMethods.includes(methodName)) return; + + await switchToGrafana(helper); + }); + }); + applyOverride(helper, 'pressKey', async function (original, key) { + function getPage() { + if (helper.page && helper.page.keyboard) return helper.page; + + if (helper.browserContext) { + const pages = helper.browserContext.pages(); + + if (pages.length > 0) { + const [firstPage] = pages; + + return firstPage; + } + } + + return helper.page; + } + + const page = getPage(); + + if (helper.context && page && page.keyboard) { + const modifiers = ['Control', 'Command', 'Alt', 'Shift', 'Meta']; + + if (Array.isArray(key) && key.length === 2 && modifiers.includes(key[0])) { + await page.keyboard.down(key[0]); + await page.keyboard.press(key[1]); + await page.keyboard.up(key[0]); + } else if (Array.isArray(key)) { + for (const keyItem of key) { + await helper.pressKey(keyItem); + } + } else { + await page.keyboard.press(key); + } + + return; + } + + await original.call(this, key); + }); + applyContextOverride(helper, 'grabTextFrom', async (locator) => helper.context.locator(getSelector(locator)).first().textContent()); + applyContextOverride(helper, 'grabTextFromAll', async (locator) => helper.context.locator(getSelector(locator)).allTextContents()); + applyContextOverride(helper, 'waitForText', async (text, seconds = null, context = null) => { + await helper.context.locator(getSelector(context) || 'body').filter({ hasText: text }).first().waitFor({ + state: 'visible', + timeout: seconds ? seconds * 1000 : helper.options.waitForTimeout, + }); + }); + applyContextOverride(helper, 'waitForDetached', async (locator, seconds = null) => { + await helper.context + .locator(getSelector(locator)) + .first() + .waitFor({ state: 'detached', timeout: seconds ? seconds * 1000 : helper.options.waitForTimeout }); + }); + applyContextOverride(helper, 'waitForValue', async (field, value, seconds = null) => { + const waitTimeout = seconds ? seconds * 1000 : helper.options.waitForTimeout; + const locator = helper.context.locator(getSelector(field)).first(); + const startTime = Date.now(); + + while (Date.now() < startTime + waitTimeout) { + const inputValue = await locator.inputValue().catch(() => ''); + + if (inputValue.includes(value)) return; + + await new Promise((resolve) => { setTimeout(resolve, 100); }); + } + throw new Error(`Wait for value "${value}" failed for field ${field}`); + }); + applyContextOverride(helper, 'moveCursorTo', async (locator, offsetX = 0, offsetY = 0) => { + const element = helper.context.locator(getSelector(locator)).first(); + + await element.evaluate((elementInstance) => { + elementInstance.scrollIntoView({ block: 'center', inline: 'center' }); + }); + await element.hover({ position: { x: offsetX, y: offsetY }, force: true }); + }); + applyOverride(helper, 'usePlaywrightTo', async function (original, description, callback) { + return original.call(this, description, async (args) => { + if (helper.context) { + args.page = helper.context; + + if (!args.page.evaluate) { + Object.defineProperty(args.page, 'evaluate', { + async value(functionToExecute, argument) { + return args.page.locator('body').evaluate(functionToExecute, argument); + }, + writable: true, + configurable: true, + }); + } + } + + return callback(args); + }); + }); + + event.dispatcher.on(event.test.before, resetContext.bind(null, helper)); + event.dispatcher.on(event.test.after, resetContext.bind(null, helper)); +}; diff --git a/tests/leftNavigation_test.js b/tests/leftNavigation_test.js index 29e21ab60..dabdaefe3 100644 --- a/tests/leftNavigation_test.js +++ b/tests/leftNavigation_test.js @@ -25,6 +25,17 @@ Feature('Left Navigation menu tests').retry(1); Before(async ({ I }) => { await I.Authorize(); + await I.usePlaywrightTo('Mock Updates for Help Menu', async ({ page }) => { + await page.route('**/v1/server/updates?force=**', (route) => route.fulfill({ + status: 200, + body: JSON.stringify({ + last_check: new Date().toISOString(), + installed: { timestamp: new Date().toISOString() }, + latest: { timestamp: new Date().toISOString() }, + update_available: false, + }), + })); + }); }); /** Data(sidebar).Scenario( @@ -59,9 +70,12 @@ Scenario( async ({ I, homePage, serverApi }) => { await homePage.open(); + I.switchTo(); I.waitForVisible(homePage.buttons.pmmHelp); + I.wait(2); I.click(homePage.buttons.pmmHelp); + I.waitForVisible(homePage.buttons.pmmLogs, 5); const path = await I.downloadFile(homePage.buttons.pmmLogs); await I.seeEntriesInZip(path, ['pmm-agent.yaml', 'pmm-managed.log', 'pmm-agent.log']); diff --git a/tests/pages/components/queryAnalytics/queryAnalyticsFilters.js b/tests/pages/components/queryAnalytics/queryAnalyticsFilters.js index ec34a3470..6c5109e4b 100644 --- a/tests/pages/components/queryAnalytics/queryAnalyticsFilters.js +++ b/tests/pages/components/queryAnalytics/queryAnalyticsFilters.js @@ -50,7 +50,7 @@ class QueryAnalyticsFilters { await locator.waitFor({ state: 'attached' }); await locator.type(filterName); - await page.waitForTimeout(200); + await new Promise((resolve) => setTimeout(resolve, 200)); }); } @@ -111,15 +111,9 @@ class QueryAnalyticsFilters { selectContainFilter(filterName) { I.waitForVisible(this.fields.groupHeaders, 30); - I.click(this.fields.groupHeaders); I.fillField(this.fields.filterBy, filterName); I.waitForVisible(this.fields.filterByName(filterName)); - I.usePlaywrightTo('Select QAN Filter', async ({ page }) => { - const locator = await page.locator(this.fields.filterByName(filterName).value); - - await locator.first().waitFor({ state: 'attached' }); - await locator.first().click(); - }); + I.click(this.fields.filterByName(filterName)); queryAnalyticsPage.waitForLoaded(); I.click(this.fields.filterBy); adminPage.customClearField(this.fields.filterBy); diff --git a/tests/pages/dashboardPage.js b/tests/pages/dashboardPage.js index 63350f2f2..627e41f1b 100644 --- a/tests/pages/dashboardPage.js +++ b/tests/pages/dashboardPage.js @@ -1147,7 +1147,7 @@ module.exports = { clickablePanel: (name) => locate('$header-container').withText(name).find('a'), dashboardTitle: (name) => locate('span').withText(name), metricPanelNa: (name) => `//section[@aria-label="${name}"]//span[text()="N/A"]`, - loadingElement: locate('//div[@aria-label="Panel loading bar"]'), + loadingElement: '[aria-label="Panel loading bar"]', multiSelect: (filterName) => locate(`//label[contains(text(), "${filterName}")]/following-sibling::div//div[contains(@class,"grafana-select-multi-value-container")]`), }, @@ -1341,28 +1341,24 @@ module.exports = { }, async expandEachDashboardRow() { - await I.usePlaywrightTo('expanding collapsed rows', async ({ page }) => { - const getCollapsedRowsLocators = async () => await page.locator(this.fields.collapsedDashboardRow).all(); - let collapsedRowsLocators = await getCollapsedRowsLocators(); + let collapsedRows = await I.grabNumberOfVisibleElements(this.fields.collapsedDashboardRow); + let maxTries = 20; - while (collapsedRowsLocators.length > 0) { - await page.keyboard.press('End'); - await collapsedRowsLocators[0].scrollIntoViewIfNeeded(); - await collapsedRowsLocators[0].click(); - collapsedRowsLocators.shift(); - - collapsedRowsLocators = await getCollapsedRowsLocators(); - } - }); + while (collapsedRows > 0 && maxTries > 0) { + I.pressKey('End'); + I.click(locate(this.fields.collapsedDashboardRow).first()); + I.wait(1); + collapsedRows = await I.grabNumberOfVisibleElements(this.fields.collapsedDashboardRow); + // eslint-disable-next-line no-plusplus + maxTries--; + } }, async expandDashboardRow(rowName) { - await I.usePlaywrightTo('Expand collapsed row', async ({ page }) => { - const rowLocator = await page.locator(this.fields.collapsedDashboardRowByName(rowName)); + const rowLocator = this.fields.collapsedDashboardRowByName(rowName); - await rowLocator.scrollIntoViewIfNeeded(); - await rowLocator.click(); - }); + I.scrollTo(rowLocator); + I.click(rowLocator); }, waitForDashboardOpened() { diff --git a/tests/pages/homePage.js b/tests/pages/homePage.js index d887ea3d3..2a7034b7c 100644 --- a/tests/pages/homePage.js +++ b/tests/pages/homePage.js @@ -34,6 +34,7 @@ module.exports = { failedChecksPanelInfo: '[aria-label="Advisors check panel"] i', newsPanelTitleSelector: dashboardPage.graphsLocator('Percona News'), pmmCustomMenu: locate('[data-toggle="dropdown"]').withText('PMM'), + metricTitle: '$header-container', servicesButton: locate('span').withText('Services'), newsPanelContentSelector: locate('.panel-content').inside('[aria-label="Percona News panel"]'), @@ -83,7 +84,7 @@ module.exports = { }, }, buttons: { - pmmHelp: locate('button').withAttr({ 'aria-label': 'Help' }), + pmmHelp: '$navitem-help-list-item', pmmLogs: locate('//a[@href="/logs.zip"]'), }, upgradeMilestones: [ @@ -102,7 +103,7 @@ module.exports = { async open() { I.amOnPage(this.url); - I.waitForElement(this.fields.dashboardHeaderLocator, 60); + I.waitForElement(this.fields.metricTitle, 60); }, async openLeftMenu() { diff --git a/tests/pages/loginPage.js b/tests/pages/loginPage.js index 09a120936..f23835a8e 100644 --- a/tests/pages/loginPage.js +++ b/tests/pages/loginPage.js @@ -36,6 +36,8 @@ module.exports = { I.click(this.fields.skipButton); } + I.amOnPage(homePage.url); + I.waitInUrl(homePage.landingUrl); },