diff --git a/grafana-plugin/e2e-tests/.eslintrc b/grafana-plugin/e2e-tests/.eslintrc new file mode 100644 index 0000000000..f515a0eea2 --- /dev/null +++ b/grafana-plugin/e2e-tests/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "rulesdir/no-relative-import-paths": "off", + "no-console": "off" + } +} diff --git a/grafana-plugin/e2e-tests/alerts/directPaging.test.ts b/grafana-plugin/e2e-tests/alerts/directPaging.test.ts index 572c94897c..7086cdd121 100644 --- a/grafana-plugin/e2e-tests/alerts/directPaging.test.ts +++ b/grafana-plugin/e2e-tests/alerts/directPaging.test.ts @@ -13,8 +13,8 @@ test('we can directly page a user', async ({ adminRolePage }) => { const { page } = adminRolePage; await goToOnCallPage(page, 'alert-groups'); + await page.waitForTimeout(1000); await clickButton({ page, buttonText: 'Escalation' }); - await fillInInput(page, 'textarea[name="message"]', message); await clickButton({ page, buttonText: 'Invite' }); @@ -23,8 +23,14 @@ test('we can directly page a user', async ({ adminRolePage }) => { await addRespondersPopup.getByText('Users').click(); await addRespondersPopup.getByText(adminRolePage.userName).click(); - await clickButton({ page, buttonText: 'Create' }); + // If user is not on call, confirm invitation + await page.waitForTimeout(1000); + const isConfirmationModalShown = await page.getByText('Confirm Participant Invitation').isVisible(); + if (isConfirmationModalShown) { + await page.getByTestId('confirm-non-oncall').click(); + } + await clickButton({ page, buttonText: 'Create' }); // Check we are redirected to the alert group page await page.waitForURL('**/alert-groups/I*'); // Alert group IDs always start with "I" await expect(page.getByTestId('incident-message')).toContainText(message); diff --git a/grafana-plugin/e2e-tests/escalationChains/escalationPolicy.test.ts b/grafana-plugin/e2e-tests/escalationChains/escalationPolicy.test.ts index 44679b1182..cf9126ba01 100644 --- a/grafana-plugin/e2e-tests/escalationChains/escalationPolicy.test.ts +++ b/grafana-plugin/e2e-tests/escalationChains/escalationPolicy.test.ts @@ -1,6 +1,6 @@ import {expect, test} from "../fixtures"; -import {generateRandomValue} from "../utils/forms"; import {createEscalationChain, EscalationStep, selectEscalationStepValue} from "../utils/escalationChain"; +import {generateRandomValue} from "../utils/forms"; test('escalation policy does not go back to "Default" after adding users to notify', async ({ adminRolePage }) => { const { page, userName } = adminRolePage; diff --git a/grafana-plugin/e2e-tests/escalationChains/searching.test.ts b/grafana-plugin/e2e-tests/escalationChains/searching.test.ts index 4d61ae489a..f814448f31 100644 --- a/grafana-plugin/e2e-tests/escalationChains/searching.test.ts +++ b/grafana-plugin/e2e-tests/escalationChains/searching.test.ts @@ -1,6 +1,6 @@ import { test, expect, Page } from '../fixtures'; -import { generateRandomValue } from '../utils/forms'; import { createEscalationChain } from '../utils/escalationChain'; +import { generateRandomValue } from '../utils/forms'; const assertEscalationChainSearchWorks = async ( page: Page, diff --git a/grafana-plugin/e2e-tests/fixtures.ts b/grafana-plugin/e2e-tests/fixtures.ts index fca7739a5b..12087944c0 100644 --- a/grafana-plugin/e2e-tests/fixtures.ts +++ b/grafana-plugin/e2e-tests/fixtures.ts @@ -1,10 +1,14 @@ -import * as fs from 'fs'; -import * as path from 'path'; import { test as base, Browser, Page, TestInfo } from '@playwright/test'; -import { GRAFANA_ADMIN_USERNAME, GRAFANA_EDITOR_USERNAME, GRAFANA_VIEWER_USERNAME } from './utils/constants'; import { VIEWER_USER_STORAGE_STATE, EDITOR_USER_STORAGE_STATE, ADMIN_USER_STORAGE_STATE } from '../playwright.config'; +import { GRAFANA_ADMIN_USERNAME, GRAFANA_EDITOR_USERNAME, GRAFANA_VIEWER_USERNAME } from './utils/constants'; + +import * as fs from 'fs'; +import * as path from 'path'; + + + export class BaseRolePage { page: Page; userName: string; diff --git a/grafana-plugin/e2e-tests/globalSetup.ts b/grafana-plugin/e2e-tests/globalSetup.ts index c7c4227a0a..835fc24776 100644 --- a/grafana-plugin/e2e-tests/globalSetup.ts +++ b/grafana-plugin/e2e-tests/globalSetup.ts @@ -1,5 +1,8 @@ +import { OrgRole } from '@grafana/data'; import { test as setup, chromium, expect, Page, BrowserContext, FullConfig, APIRequestContext } from '@playwright/test'; +import { VIEWER_USER_STORAGE_STATE, EDITOR_USER_STORAGE_STATE, ADMIN_USER_STORAGE_STATE } from '../playwright.config'; + import GrafanaAPIClient from './utils/clients/grafana'; import { GRAFANA_ADMIN_PASSWORD, @@ -14,8 +17,6 @@ import { } from './utils/constants'; import { clickButton, getInputByName } from './utils/forms'; import { goToGrafanaPage } from './utils/navigation'; -import { VIEWER_USER_STORAGE_STATE, EDITOR_USER_STORAGE_STATE, ADMIN_USER_STORAGE_STATE } from '../playwright.config'; -import { OrgRole } from '@grafana/data'; const grafanaApiClient = new GrafanaAPIClient(GRAFANA_ADMIN_USERNAME, GRAFANA_ADMIN_PASSWORD); diff --git a/grafana-plugin/e2e-tests/integrations/heartbeat.test.ts b/grafana-plugin/e2e-tests/integrations/heartbeat.test.ts index 202f1832e3..374ca6aec4 100644 --- a/grafana-plugin/e2e-tests/integrations/heartbeat.test.ts +++ b/grafana-plugin/e2e-tests/integrations/heartbeat.test.ts @@ -1,5 +1,4 @@ import { test, Page, expect } from '../fixtures'; - import { generateRandomValue, selectDropdownValue } from '../utils/forms'; import { createIntegration } from '../utils/integrations'; @@ -7,10 +6,7 @@ const HEARTBEAT_SETTINGS_FORM_TEST_ID = 'heartbeat-settings-form'; test.describe("updating an integration's heartbeat interval works", async () => { const _openHeartbeatSettingsForm = async (page: Page) => { - const integrationSettingsPopupElement = page.getByTestId('integration-settings-context-menu'); - await integrationSettingsPopupElement.waitFor({ state: 'visible' }); - await integrationSettingsPopupElement.click(); - + await page.getByTestId('integration-settings-context-menu-wrapper').getByRole('img').click(); await page.getByTestId('integration-heartbeat-settings').click(); }; @@ -60,6 +56,6 @@ test.describe("updating an integration's heartbeat interval works", async () => */ await page.request.get(endpoint); await page.reload({ waitUntil: 'networkidle' }); - await page.getByTestId('heartbeat-badge').waitFor({ state: 'visible' }); + await page.getByTestId('heartbeat-badge').waitFor(); }); }); diff --git a/grafana-plugin/e2e-tests/integrations/integrationsTable.test.ts b/grafana-plugin/e2e-tests/integrations/integrationsTable.test.ts index b2ad52ad75..85408fbdaa 100644 --- a/grafana-plugin/e2e-tests/integrations/integrationsTable.test.ts +++ b/grafana-plugin/e2e-tests/integrations/integrationsTable.test.ts @@ -1,29 +1,31 @@ -import { test, expect } from '../fixtures'; +import { test } from '../fixtures'; import { generateRandomValue } from '../utils/forms'; -import { createIntegration } from '../utils/integrations'; +import { createIntegration, searchIntegrationAndAssertItsPresence } from '../utils/integrations'; test('Integrations table shows data in Connections and Direct Paging tabs', async ({ adminRolePage: { page } }) => { - // // Create 2 integrations that are not Direct Paging const ID = generateRandomValue(); const WEBHOOK_INTEGRATION_NAME = `Webhook-${ID}`; const ALERTMANAGER_INTEGRATION_NAME = `Alertmanager-${ID}`; - const DIRECT_PAGING_INTEGRATION_NAME = `Direct paging`; + const DIRECT_PAGING_INTEGRATION_NAME = `Direct paging integration name`; + // Create 2 integrations that are not Direct Paging await createIntegration({ page, integrationSearchText: 'Webhook', integrationName: WEBHOOK_INTEGRATION_NAME }); + await page.waitForTimeout(1000); await page.getByRole('tab', { name: 'Tab Integrations' }).click(); - await createIntegration({ page, integrationSearchText: 'Alertmanager', shouldGoToIntegrationsPage: false, integrationName: ALERTMANAGER_INTEGRATION_NAME, }); + await page.waitForTimeout(1000); await page.getByRole('tab', { name: 'Tab Integrations' }).click(); // Create 1 Direct Paging integration if it doesn't exist - const integrationsTable = page.getByTestId('integrations-table'); await page.getByRole('tab', { name: 'Tab Direct Paging' }).click(); - const isDirectPagingAlreadyCreated = await page.getByText('Direct paging').isVisible(); + const integrationsTable = page.getByTestId('integrations-table'); + await page.waitForTimeout(2000); + const isDirectPagingAlreadyCreated = (await integrationsTable.getByText('Direct paging').count()) >= 1; if (!isDirectPagingAlreadyCreated) { await createIntegration({ page, @@ -31,17 +33,41 @@ test('Integrations table shows data in Connections and Direct Paging tabs', asyn shouldGoToIntegrationsPage: false, integrationName: DIRECT_PAGING_INTEGRATION_NAME, }); + await page.waitForTimeout(1000); } await page.getByRole('tab', { name: 'Tab Integrations' }).click(); // By default Connections tab is opened and newly created integrations are visible except Direct Paging one - await expect(integrationsTable.getByText(WEBHOOK_INTEGRATION_NAME)).toBeVisible(); - await expect(integrationsTable.getByText(ALERTMANAGER_INTEGRATION_NAME)).toBeVisible(); - await expect(integrationsTable).not.toContainText(DIRECT_PAGING_INTEGRATION_NAME); + await searchIntegrationAndAssertItsPresence({ page, integrationsTable, integrationName: WEBHOOK_INTEGRATION_NAME }); + await searchIntegrationAndAssertItsPresence({ + page, + integrationsTable, + integrationName: ALERTMANAGER_INTEGRATION_NAME, + }); + await searchIntegrationAndAssertItsPresence({ + page, + integrationsTable, + integrationName: DIRECT_PAGING_INTEGRATION_NAME, + visibleExpected: false, + }); // Then after switching to Direct Paging tab only Direct Paging integration is visible await page.getByRole('tab', { name: 'Tab Direct Paging' }).click(); - await expect(integrationsTable.getByText(WEBHOOK_INTEGRATION_NAME)).not.toBeVisible(); - await expect(integrationsTable.getByText(ALERTMANAGER_INTEGRATION_NAME)).not.toBeVisible(); - await expect(integrationsTable).toContainText(DIRECT_PAGING_INTEGRATION_NAME); + await searchIntegrationAndAssertItsPresence({ + page, + integrationsTable, + integrationName: WEBHOOK_INTEGRATION_NAME, + visibleExpected: false, + }); + await searchIntegrationAndAssertItsPresence({ + page, + integrationsTable, + integrationName: ALERTMANAGER_INTEGRATION_NAME, + visibleExpected: false, + }); + await searchIntegrationAndAssertItsPresence({ + page, + integrationsTable, + integrationName: 'Direct paging', + }); }); diff --git a/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts b/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts index 88ff3c6ec7..f3471840e7 100644 --- a/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts +++ b/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts @@ -13,8 +13,6 @@ import { goToOnCallPage } from '../utils/navigation'; type MaintenanceModeType = 'Debug' | 'Maintenance'; test.describe('maintenance mode works', () => { - test.slow(); // this test is doing a good amount of work, give it time - const MAINTENANCE_DURATION = '1 hour'; const REMAINING_TIME_TEXT = '59m left'; const REMAINING_TIME_TOOLTIP_TEST_ID = 'maintenance-mode-remaining-time-tooltip'; @@ -22,27 +20,27 @@ test.describe('maintenance mode works', () => { const createRoutedText = (escalationChainName: string): string => `alert group assigned to route "default" with escalation chain "${escalationChainName}"`; - const _openIntegrationSettingsPopup = async (page: Page): Promise => { - const integrationSettingsPopupElement = page.getByTestId('integration-settings-context-menu'); - await integrationSettingsPopupElement.waitFor({ state: 'visible' }); + const _openIntegrationSettingsPopup = async (page: Page, shouldDoubleClickSettingsIcon = false): Promise => { + await page.waitForTimeout(2000); + const integrationSettingsPopupElement = page + .getByTestId('integration-settings-context-menu-wrapper') + .getByRole('img'); await integrationSettingsPopupElement.click(); - return integrationSettingsPopupElement; + /** + * sometimes we need to click twice (e.g. adding the escalation chain route + * doesn't unfocus out of the select element after selecting an option) + */ + if (shouldDoubleClickSettingsIcon) { + await integrationSettingsPopupElement.click(); + } }; const getRemainingTimeTooltip = (page: Page): Locator => page.getByTestId(REMAINING_TIME_TOOLTIP_TEST_ID); const enableMaintenanceMode = async (page: Page, mode: MaintenanceModeType): Promise => { - const integrationSettingsPopupElement = await _openIntegrationSettingsPopup(page); - /** - * we need to click twice here, because adding the escalation chain route - * doesn't unfocus out of the select element after selecting an option - */ - await integrationSettingsPopupElement.click(); - + await _openIntegrationSettingsPopup(page, true); // open the maintenance mode settings drawer + fill in the maintenance details - const startMaintenanceModeButton = page.getByTestId('integration-start-maintenance'); - await startMaintenanceModeButton.waitFor({ state: 'visible' }); - await startMaintenanceModeButton.click(); + await page.getByTestId('integration-start-maintenance').click(); // fill in the form const maintenanceModeDrawer = page.getByTestId('maintenance-mode-drawer'); @@ -77,12 +75,10 @@ test.describe('maintenance mode works', () => { await goToOnCallPage(page, 'integrations'); await filterIntegrationsTableAndGoToDetailPage(page, integrationName); - await _openIntegrationSettingsPopup(page); + await _openIntegrationSettingsPopup(page, true); // click the stop maintenance button - const stopMaintenanceModeButton = page.getByTestId('integration-stop-maintenance'); - await stopMaintenanceModeButton.waitFor({ state: 'visible' }); - await stopMaintenanceModeButton.click(); + await page.getByTestId('integration-stop-maintenance').click(); // in the modal popup, confirm that we want to stop it await clickButton({ @@ -114,6 +110,8 @@ test.describe('maintenance mode works', () => { }; test('debug mode', async ({ adminRolePage: { page, userName } }) => { + test.slow(); + const { escalationChainName, integrationName } = await createIntegrationAndEscalationChainAndEnableMaintenanceMode( page, userName, @@ -130,6 +128,7 @@ test.describe('maintenance mode works', () => { }); test('"maintenance" mode', async ({ adminRolePage: { page, userName } }) => { + test.slow(); const { integrationName } = await createIntegrationAndEscalationChainAndEnableMaintenanceMode( page, userName, diff --git a/grafana-plugin/e2e-tests/integrations/uniqueIntegrationNames.test.ts b/grafana-plugin/e2e-tests/integrations/uniqueIntegrationNames.test.ts index be27aa868e..b30ac0796a 100644 --- a/grafana-plugin/e2e-tests/integrations/uniqueIntegrationNames.test.ts +++ b/grafana-plugin/e2e-tests/integrations/uniqueIntegrationNames.test.ts @@ -1,8 +1,10 @@ import { test, expect } from '../fixtures'; import { openCreateIntegrationModal } from '../utils/integrations'; +import { goToOnCallPage } from '../utils/navigation'; test('integrations have unique names', async ({ adminRolePage }) => { const { page } = adminRolePage; + await goToOnCallPage(page, 'integrations'); await openCreateIntegrationModal(page); const integrationNames = await page.getByTestId('integration-display-name').allInnerTexts(); diff --git a/grafana-plugin/e2e-tests/schedules/addOverride.test.ts b/grafana-plugin/e2e-tests/schedules/addOverride.test.ts index 2ee994943d..bcf56e9d89 100644 --- a/grafana-plugin/e2e-tests/schedules/addOverride.test.ts +++ b/grafana-plugin/e2e-tests/schedules/addOverride.test.ts @@ -1,7 +1,8 @@ +import dayjs from 'dayjs'; + import { test, expect } from '../fixtures'; import { clickButton, generateRandomValue } from '../utils/forms'; import { createOnCallSchedule, getOverrideFormDateInputs } from '../utils/schedule'; -import dayjs from 'dayjs'; test('default dates in override creation modal are correct', async ({ adminRolePage }) => { const { page, userName } = adminRolePage; diff --git a/grafana-plugin/e2e-tests/users/usersActions.test.ts b/grafana-plugin/e2e-tests/users/usersActions.test.ts index 127d993d80..129f517bac 100644 --- a/grafana-plugin/e2e-tests/users/usersActions.test.ts +++ b/grafana-plugin/e2e-tests/users/usersActions.test.ts @@ -1,136 +1,68 @@ -import { test, expect, Page } from '../fixtures'; +import { test, expect } from '../fixtures'; import { goToOnCallPage } from '../utils/navigation'; +import { viewUsers, accessProfileTabs } from '../utils/users'; test.describe('Users screen actions', () => { - test("Admin is allowed to edit other users' profile", async ({ adminRolePage }) => { - await _testButtons(adminRolePage.page, 'button.edit-other-profile-button[disabled]'); + test("Admin is allowed to edit other users' profile", async ({ adminRolePage: { page } }) => { + await goToOnCallPage(page, 'users'); + await expect(page.getByTestId('users-table').getByRole('button', { name: 'Edit', disabled: false })).toHaveCount(3); }); - test('Admin is allowed to view the list of users', async ({ adminRolePage }) => { - await _viewUsers(adminRolePage.page); + test('Admin is allowed to view the list of users', async ({ adminRolePage: { page } }) => { + await viewUsers(page); }); - test('Viewer is not allowed to view the list of users', async ({ viewerRolePage }) => { - await _viewUsers(viewerRolePage.page, false); + test('Viewer is not allowed to view the list of users', async ({ viewerRolePage: { page } }) => { + await viewUsers(page, false); }); test('Viewer cannot access restricted tabs from View My Profile', async ({ viewerRolePage }) => { const { page } = viewerRolePage; - await _accessProfileTabs(page, ['tab-mobile-app', 'tab-phone-verification', 'tab-slack', 'tab-telegram'], false); + await accessProfileTabs(page, ['tab-mobile-app', 'tab-phone-verification', 'tab-slack', 'tab-telegram'], false); }); test('Editor is allowed to view the list of users', async ({ editorRolePage }) => { - await _viewUsers(editorRolePage.page); + await viewUsers(editorRolePage.page); }); test("Editor cannot view other users' data", async ({ editorRolePage }) => { const { page } = editorRolePage; await goToOnCallPage(page, 'users'); - await page.waitForSelector('.current-user'); - - // check if these fields are Masked or Not (******) - const fieldIds = ['users-email', 'users-phone-number']; - - for (let i = 0; i < fieldIds.length - 1; ++i) { - const currentUsername = page.locator(`.current-user [data-testid="${fieldIds[i]}"]`); - - expect((await currentUsername.all()).length).toBe(1); // match for current user - (await currentUsername.all()).forEach((val) => expect(val).not.toHaveText('******')); + await page.getByTestId('users-email').and(page.getByText('editor')).waitFor(); - const otherUsername = page.locator(`.other-user [data-testid="${fieldIds[i]}"]`); - - expect((await otherUsername.all()).length).toBeGreaterThan(1); // match for other users (>= 1) - (await otherUsername.all()).forEach((val) => expect(val).toHaveText('******')); - } + await expect(page.getByTestId('users-email').and(page.getByText('editor'))).toHaveCount(1); + await expect(page.getByTestId('users-email').and(page.getByText('******'))).toHaveCount(2); + await expect(page.getByTestId('users-phone-number').and(page.getByText('******'))).toHaveCount(2); }); test('Editor can access tabs from View My Profile', async ({ editorRolePage }) => { const { page } = editorRolePage; // the other tabs depend on Cloud, skip for now - await _accessProfileTabs(page, ['tab-slack', 'tab-telegram'], true); + await accessProfileTabs(page, ['tab-slack', 'tab-telegram'], true); }); - test("Editor is not allowed to edit other users' profile", async ({ editorRolePage }) => { - await _testButtons(editorRolePage.page, 'button.edit-other-profile-button:not([disabled])'); + test("Editor is not allowed to edit other users' profile", async ({ editorRolePage: { page } }) => { + await goToOnCallPage(page, 'users'); + await expect(page.getByTestId('users-table').getByRole('button', { name: 'Edit', disabled: false })).toHaveCount(1); + await expect(page.getByTestId('users-table').getByRole('button', { name: 'Edit', disabled: true })).toHaveCount(2); }); test('Search updates the table view', async ({ adminRolePage }) => { const { page } = adminRolePage; await goToOnCallPage(page, 'users'); - await page.waitForTimeout(1000); + await page.waitForTimeout(2000); const searchInput = page.locator(`[data-testid="search-users"]`); await searchInput.fill('oncall'); - await page.waitForTimeout(5000); + await page.waitForTimeout(2000); const result = page.locator(`[data-testid="users-username"]`); expect(await result.count()).toBe(1); }); - - /* - * Helper methods - */ - - async function _testButtons(page: Page, selector: string) { - await goToOnCallPage(page, 'users'); - - const usersTableElement = page.getByTestId('users-table'); - await usersTableElement.waitFor({ state: 'visible' }); - - const buttonsList = await page.locator(selector); - - expect(buttonsList).toHaveCount(0); - } - - async function _accessProfileTabs(page: Page, tabs: string[], hasAccess: boolean) { - await goToOnCallPage(page, 'users'); - - await page.getByTestId('users-view-my-profile').click(); - - // the next queries could or could not resolve - // therefore we wait a generic 1000ms duration and assert based on visibility - await page.waitForTimeout(1000); - - for (let i = 0; i < tabs.length - 1; ++i) { - const tab = page.getByTestId(tabs[i]); - - if (await tab.isVisible()) { - await tab.click(); - - const query = page.getByText( - 'You do not have permission to perform this action. Ask an admin to upgrade your permissions.' - ); - - if (hasAccess) { - await expect(query).toBeHidden(); - } else { - await expect(query).toBeVisible(); - } - } - } - } - - async function _viewUsers(page: Page, isAllowedToView = true): Promise { - await goToOnCallPage(page, 'users'); - - if (isAllowedToView) { - const usersTableElement = page.getByTestId('users-table'); - await usersTableElement.waitFor({ state: 'visible' }); - - const userRowsContext = await usersTableElement.locator('tbody > tr').allTextContents(); - expect(userRowsContext.length).toBeGreaterThan(0); - } else { - const missingPermissionsMessageElement = page.getByTestId('view-users-missing-permission-message'); - await missingPermissionsMessageElement.waitFor({ state: 'visible' }); - - const missingPermissionMessage = await missingPermissionsMessageElement.textContent(); - expect(missingPermissionMessage).toMatch(/You are missing the .* to be able to view OnCall users/); - } - } }); diff --git a/grafana-plugin/e2e-tests/utils/alertGroup.ts b/grafana-plugin/e2e-tests/utils/alertGroup.ts index 339aad011d..1a7d854d83 100644 --- a/grafana-plugin/e2e-tests/utils/alertGroup.ts +++ b/grafana-plugin/e2e-tests/utils/alertGroup.ts @@ -1,4 +1,5 @@ import { Locator, Page, expect } from '@playwright/test'; + import { selectDropdownValue, selectValuePickerValue } from './forms'; import { goToOnCallPage } from './navigation'; diff --git a/grafana-plugin/e2e-tests/utils/clients/grafana.ts b/grafana-plugin/e2e-tests/utils/clients/grafana.ts index 2127410050..a9a2d0fdc0 100644 --- a/grafana-plugin/e2e-tests/utils/clients/grafana.ts +++ b/grafana-plugin/e2e-tests/utils/clients/grafana.ts @@ -95,7 +95,7 @@ export default class GrafanaAPIClient { // user was just created const respJson: CreateUserResponse = await res.json(); userId = respJson.id; - } else if (responseCode == 412) { + } else if (responseCode === 412) { // user already exists, go fetch their user id userId = await this.getUserIdByUsername(request, userName); } else { diff --git a/grafana-plugin/e2e-tests/utils/forms.ts b/grafana-plugin/e2e-tests/utils/forms.ts index 0649221098..73c9734ea7 100644 --- a/grafana-plugin/e2e-tests/utils/forms.ts +++ b/grafana-plugin/e2e-tests/utils/forms.ts @@ -1,4 +1,5 @@ import type { Locator, Page } from '@playwright/test'; + import { randomUUID } from 'crypto'; type SelectorType = 'gSelect' | 'grafanaSelect'; @@ -22,9 +23,6 @@ type SelectDropdownValueArgs = { type ClickButtonArgs = { page: Page; buttonText: string; - // if provided, search for the button by data-testid - dataTestId?: string; - // if provided, use this Locator as the root of our search for the button startingLocator?: Locator; }; @@ -36,17 +34,9 @@ export const fillInInputByPlaceholderValue = (page: Page, placeholderValue: stri export const getInputByName = (page: Page, name: string): Locator => page.locator(`input[name="${name}"]`); -export const clickButton = async ({ - page, - buttonText, - startingLocator, - dataTestId, -}: ClickButtonArgs): Promise => { - const baseLocator = dataTestId ? `button[data-testid="${dataTestId}"]` : 'button'; - const button = (startingLocator || page).locator(`${baseLocator}:not([disabled]) >> text=${buttonText}`); - - await button.waitFor({ state: 'visible' }); - await button.click(); +export const clickButton = async ({ page, buttonText, startingLocator }: ClickButtonArgs): Promise => { + const baseLocator = startingLocator || page; + await baseLocator.getByRole('button', { name: buttonText, disabled: false }).click(); }; /** @@ -94,7 +84,7 @@ export const selectDropdownValue = async (args: SelectDropdownValueArgs): Promis const { page, value, pressEnterInsteadOfSelectingOption } = args; const selectElement = await openSelect(args); - await selectElement.type(value); + await selectElement.pressSequentially(value); if (pressEnterInsteadOfSelectingOption) { await page.keyboard.press('Enter'); diff --git a/grafana-plugin/e2e-tests/utils/integrations.ts b/grafana-plugin/e2e-tests/utils/integrations.ts index 4a45211f97..72ef8ec766 100644 --- a/grafana-plugin/e2e-tests/utils/integrations.ts +++ b/grafana-plugin/e2e-tests/utils/integrations.ts @@ -1,4 +1,5 @@ -import { Page } from '@playwright/test'; +import { Locator, Page, expect } from '@playwright/test'; + import { clickButton, generateRandomValue, selectDropdownValue } from './forms'; import { goToOnCallPage } from './navigation'; @@ -60,8 +61,8 @@ export const assignEscalationChainToIntegration = async (page: Page, escalationC }; export const sendDemoAlert = async (page: Page): Promise => { - await clickButton({ page, buttonText: 'Send demo alert', dataTestId: 'send-demo-alert' }); - await clickButton({ page, buttonText: 'Send Alert', dataTestId: 'submit-send-alert' }); + await clickButton({ page, buttonText: 'Send demo alert' }); + await clickButton({ page, buttonText: 'Send Alert' }); await page.getByTestId('demo-alert-sent-notification').waitFor({ state: 'visible' }); }; @@ -85,9 +86,32 @@ export const filterIntegrationsTableAndGoToDetailPage = async (page: Page, integ pressEnterInsteadOfSelectingOption: true, }); - await ( - await page.waitForSelector( - `div[data-testid="integrations-table"] table > tbody > tr > td:first-child a >> text=${integrationName}` - ) - ).click(); + await page.getByTestId('integrations-table').getByText(`${integrationName}`).click(); +}; + +export const searchIntegrationAndAssertItsPresence = async ({ + page, + integrationName, + integrationsTable, + visibleExpected = true, +}: { + page: Page; + integrationsTable: Locator; + integrationName: string; + visibleExpected?: boolean; +}) => { + await page + .locator('div') + .filter({ hasText: /^Search or filter results\.\.\.$/ }) + .nth(1) + .click(); + await page.keyboard.insertText(integrationName); + await page.keyboard.press('Enter'); + await page.waitForTimeout(2000); + const nbOfResults = await integrationsTable.getByText(integrationName).count(); + if (visibleExpected) { + expect(nbOfResults).toBeGreaterThanOrEqual(1); + } else { + expect(nbOfResults).toBe(0); + } }; diff --git a/grafana-plugin/e2e-tests/utils/navigation.ts b/grafana-plugin/e2e-tests/utils/navigation.ts index f75eb04677..b5a1e4f7d4 100644 --- a/grafana-plugin/e2e-tests/utils/navigation.ts +++ b/grafana-plugin/e2e-tests/utils/navigation.ts @@ -1,4 +1,5 @@ import type { Page, Response } from '@playwright/test'; + import { BASE_URL } from './constants'; type GrafanaPage = '/plugins/grafana-oncall-app'; diff --git a/grafana-plugin/e2e-tests/utils/schedule.ts b/grafana-plugin/e2e-tests/utils/schedule.ts index 9d8e433587..3627109ad9 100644 --- a/grafana-plugin/e2e-tests/utils/schedule.ts +++ b/grafana-plugin/e2e-tests/utils/schedule.ts @@ -1,7 +1,8 @@ import { Page } from '@playwright/test'; +import dayjs from 'dayjs'; + import { clickButton, fillInInput, selectDropdownValue } from './forms'; import { goToOnCallPage } from './navigation'; -import dayjs from 'dayjs'; export const createOnCallSchedule = async (page: Page, scheduleName: string, userName: string): Promise => { // go to the schedules page diff --git a/grafana-plugin/e2e-tests/utils/users.ts b/grafana-plugin/e2e-tests/utils/users.ts new file mode 100644 index 0000000000..e7c5d15dff --- /dev/null +++ b/grafana-plugin/e2e-tests/utils/users.ts @@ -0,0 +1,45 @@ +import { Page, expect } from '@playwright/test'; + +import { goToOnCallPage } from './navigation'; + +export async function accessProfileTabs(page: Page, tabs: string[], hasAccess: boolean) { + await goToOnCallPage(page, 'users'); + + await page.getByTestId('users-view-my-profile').click(); + + // the next queries could or could not resolve + // therefore we wait a generic 1000ms duration and assert based on visibility + await page.waitForTimeout(1000); + + for (let i = 0; i < tabs.length - 1; ++i) { + const tab = page.getByTestId(tabs[i]); + + if (await tab.isVisible()) { + await tab.click(); + + const query = page.getByText( + 'You do not have permission to perform this action. Ask an admin to upgrade your permissions.' + ); + + if (hasAccess) { + await expect(query).toBeHidden(); + } else { + await expect(query).toBeVisible(); + } + } + } +} + +export async function viewUsers(page: Page, isAllowedToView = true): Promise { + await goToOnCallPage(page, 'users'); + + if (isAllowedToView) { + const usersTable = page.getByTestId('users-table'); + await usersTable.getByRole('row').nth(1).waitFor(); + await expect(usersTable.getByRole('row')).toHaveCount(4); + } else { + await expect(page.getByTestId('view-users-missing-permission-message')).toHaveText( + /You are missing the .* to be able to view OnCall users/ + ); + } +} diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index fb8278d3c7..02cd81faef 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -3,8 +3,8 @@ "version": "dev-oss", "description": "Grafana OnCall Plugin", "scripts": { - "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 ./src", - "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 --quiet ./src", + "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 ./src ./e2e-tests", + "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 --quiet ./src ./e2e-tests", "stylelint": "stylelint ./src/**/*.{css,scss,module.css,module.scss}", "stylelint:fix": "stylelint --fix ./src/**/*.{css,scss,module.css,module.scss}", "build": "grafana-toolkit plugin:build", @@ -64,7 +64,7 @@ "@grafana/eslint-config": "^5.1.0", "@grafana/toolkit": "^9.5.2", "@jest/globals": "^27.5.1", - "@playwright/test": "^1.35.1", + "@playwright/test": "^1.39.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "12", "@testing-library/user-event": "^14.4.3", diff --git a/grafana-plugin/playwright.config.ts b/grafana-plugin/playwright.config.ts index a7af7d8679..731912e286 100644 --- a/grafana-plugin/playwright.config.ts +++ b/grafana-plugin/playwright.config.ts @@ -1,8 +1,6 @@ -import path from 'path'; - -import type { PlaywrightTestConfig } from '@playwright/test'; -import { devices } from '@playwright/test'; +import { defineConfig, devices } from '@playwright/test'; +import path from 'path'; /** * Read environment variables from file. * https://github.com/motdotla/dotenv @@ -13,10 +11,12 @@ export const VIEWER_USER_STORAGE_STATE = path.join(__dirname, 'e2e-tests/.auth/v export const EDITOR_USER_STORAGE_STATE = path.join(__dirname, 'e2e-tests/.auth/editor.json'); export const ADMIN_USER_STORAGE_STATE = path.join(__dirname, 'e2e-tests/.auth/admin.json'); +const IS_CI = !!process.env.CI; + /** * See https://playwright.dev/docs/test-configuration. */ -const config: PlaywrightTestConfig = { +export default defineConfig({ testDir: './e2e-tests', /* Maximum time all the tests can run for. */ @@ -32,16 +32,16 @@ const config: PlaywrightTestConfig = { timeout: 10000, }, /* Run tests in files in parallel */ - fullyParallel: true, + fullyParallel: false, /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, + forbidOnly: IS_CI, /** * Retry on CI only * * NOTE: until we fix this issue (https://github.com/grafana/oncall/issues/1692) which occasionally leads - * to flaky tests.. let's just retry failed tests. If the same test fails 3 times, you know something must be up + * to flaky tests.. let's allow 1 retry per test */ - retries: !!process.env.CI ? 3 : 0, + retries: IS_CI ? 1 : 0, workers: 2, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', @@ -52,10 +52,9 @@ const config: PlaywrightTestConfig = { /* Base URL to use in actions like `await page.goto('/')`. */ // baseURL: 'http://localhost:3000', - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on', video: 'on', - headless: !!process.env.CI, + headless: IS_CI, }, /* Configure projects for major browsers */ @@ -66,38 +65,28 @@ const config: PlaywrightTestConfig = { }, { name: 'chromium', - use: { - ...devices['Desktop Chrome'], - }, + use: devices['Desktop Chrome'], dependencies: ['setup'], }, { name: 'firefox', - use: { - ...devices['Desktop Firefox'], - }, + use: devices['Desktop Firefox'], dependencies: ['setup'], }, { name: 'webkit', - use: { - ...devices['Desktop Safari'], - }, + use: devices['Desktop Safari'], dependencies: ['setup'], }, /* Test against mobile viewports. */ // { // name: 'Mobile Chrome', - // use: { - // ...devices['Pixel 5'], - // }, + // use: devices['Pixel 5'], // }, // { // name: 'Mobile Safari', - // use: { - // ...devices['iPhone 12'], - // }, + // use: devices['iPhone 12'], // }, /* Test against branded browsers. */ @@ -123,6 +112,4 @@ const config: PlaywrightTestConfig = { // command: 'npm run start', // port: 3000, // }, -}; - -export default config; +}); diff --git a/grafana-plugin/src/containers/AddResponders/AddResponders.tsx b/grafana-plugin/src/containers/AddResponders/AddResponders.tsx index 515bbe7ac1..69484de57b 100644 --- a/grafana-plugin/src/containers/AddResponders/AddResponders.tsx +++ b/grafana-plugin/src/containers/AddResponders/AddResponders.tsx @@ -214,7 +214,7 @@ const AddResponders = observer( - diff --git a/grafana-plugin/src/pages/integration/Integration.tsx b/grafana-plugin/src/pages/integration/Integration.tsx index 4744137697..7fa0a84c70 100644 --- a/grafana-plugin/src/pages/integration/Integration.tsx +++ b/grafana-plugin/src/pages/integration/Integration.tsx @@ -834,164 +834,167 @@ const IntegrationActions: React.FC = ({ - ( -
-
openIntegrationSettings()}> - Integration Settings -
- - {store.hasFeature(AppFeature.Labels) && ( - -
openLabelsForm()}> - Alert group labels -
-
- )} +
+ ( +
+
openIntegrationSettings()}> + Integration Settings +
- {showHeartbeatSettings() && ( - -
setIsHeartbeatFormOpen(true)} - data-testid="integration-heartbeat-settings" - > - Heartbeat Settings -
-
- )} + {store.hasFeature(AppFeature.Labels) && ( + +
openLabelsForm()}> + Alert group labels +
+
+ )} - {!alertReceiveChannel.maintenance_till && ( - -
- Start Maintenance -
-
- )} + {showHeartbeatSettings() && ( + +
setIsHeartbeatFormOpen(true)} + data-testid="integration-heartbeat-settings" + > + Heartbeat Settings +
+
+ )} - -
- Edit Templates -
-
+ {!alertReceiveChannel.maintenance_till && ( + +
+ Start Maintenance +
+
+ )} - {alertReceiveChannel.maintenance_till && ( -
{ - setConfirmModal({ - isOpen: true, - confirmText: 'Stop', - dismissText: 'Cancel', - onConfirm: onStopMaintenance, - title: 'Stop Maintenance', - body: ( - - Are you sure you want to stop the maintenance for{' '} - ? - - ), - }); - }} - data-testid="integration-stop-maintenance" - > - Stop Maintenance +
+ Edit Templates
- )} - {isLegacyIntegration && ( - -
- setConfirmModal({ - isOpen: true, - title: 'Migrate Integration?', - body: ( - + {alertReceiveChannel.maintenance_till && ( + +
{ + setConfirmModal({ + isOpen: true, + confirmText: 'Stop', + dismissText: 'Cancel', + onConfirm: onStopMaintenance, + title: 'Stop Maintenance', + body: ( - Are you sure you want to migrate ? + Are you sure you want to stop the maintenance for{' '} + ? + ), + }); + }} + data-testid="integration-stop-maintenance" + > + Stop Maintenance +
+
+ )} - - - Integration internal behaviour will be changed - - - Integration URL will stay the same, so no need to change {getMigrationDisplayName()}{' '} - configuration - - - - Integration templates will be reset to suit the new payload + {isLegacyIntegration && ( + +
+ setConfirmModal({ + isOpen: true, + title: 'Migrate Integration?', + body: ( + + + Are you sure you want to migrate ? - - It is needed to adjust routes manually to the new payload + + + - Integration internal behaviour will be changed + + - Integration URL will stay the same, so no need to change {getMigrationDisplayName()}{' '} + configuration + + + - Integration templates will be reset to suit the new payload + + + - It is needed to adjust routes manually to the new payload + + - - ), - onConfirm: onIntegrationMigrate, - dismissText: 'Cancel', - confirmText: 'Migrate', - }) - } - > - Migrate + ), + onConfirm: onIntegrationMigrate, + dismissText: 'Cancel', + confirmText: 'Migrate', + }) + } + > + Migrate +
+
+ )} + + openNotification('Integration ID is copied')} + > +
+ + + + UID: {alertReceiveChannel.id} +
- - )} +
- openNotification('Integration ID is copied')} - > -
- - +
- UID: {alertReceiveChannel.id} - -
- - -
- - -
-
{ - setConfirmModal({ - isOpen: true, - title: 'Delete Integration?', - body: ( - - Are you sure you want to delete ? - - ), - onConfirm: deleteIntegration, - dismissText: 'Cancel', - confirmText: 'Delete', - }); - }} - className="u-width-100" - > - - - - Delete Integration - - + +
+
{ + setConfirmModal({ + isOpen: true, + title: 'Delete Integration?', + body: ( + + Are you sure you want to delete ? + + ), + onConfirm: deleteIntegration, + dismissText: 'Cancel', + confirmText: 'Delete', + }); + }} + className="u-width-100" + > + + + + Delete Integration + + +
-
- -
- )} - > - {({ openMenu }) => } - +
+
+ )} + > + {({ openMenu }) => } + +
); diff --git a/grafana-plugin/src/plugin.json b/grafana-plugin/src/plugin.json index d87367a632..9e6311624e 100644 --- a/grafana-plugin/src/plugin.json +++ b/grafana-plugin/src/plugin.json @@ -57,7 +57,7 @@ { "type": "page", "name": "Integrations", - "path": "/a/grafana-oncall-app/integrations", + "path": "/a/grafana-oncall-app/integrations?tab=connections", "role": "Viewer", "action": "grafana-oncall-app.integrations:read", "addToNav": true diff --git a/grafana-plugin/yarn.lock b/grafana-plugin/yarn.lock index f6b902f346..2b225b09fd 100644 --- a/grafana-plugin/yarn.lock +++ b/grafana-plugin/yarn.lock @@ -3193,15 +3193,12 @@ resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.6.6.tgz#641f73913a6be402b34e4bdfca98d6832ed55586" integrity sha512-3MUulwMtsdCA9lw8a/Kc0XDBJJVCkYTQ5aGd+///TbfkOMXoOGAzzoiYKwPEsLYZv7He7fKJ/mCacqKOO7REyg== -"@playwright/test@^1.35.1": - version "1.35.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.35.1.tgz#a596b61e15b980716696f149cc7a2002f003580c" - integrity sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA== +"@playwright/test@^1.39.0": + version "1.39.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.39.0.tgz#d10ba8e38e44104499e25001945f07faa9fa91cd" + integrity sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ== dependencies: - "@types/node" "*" - playwright-core "1.35.1" - optionalDependencies: - fsevents "2.3.2" + playwright "1.39.0" "@polka/url@^1.0.0-next.20": version "1.0.0-next.21" @@ -11757,10 +11754,19 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -playwright-core@1.35.1: - version "1.35.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.35.1.tgz#52c1e6ffaa6a8c29de1a5bdf8cce0ce290ffb81d" - integrity sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg== +playwright-core@1.39.0: + version "1.39.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.39.0.tgz#efeaea754af4fb170d11845b8da30b2323287c63" + integrity sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw== + +playwright@1.39.0: + version "1.39.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.39.0.tgz#184c81cd6478f8da28bcd9e60e94fcebf566e077" + integrity sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw== + dependencies: + playwright-core "1.39.0" + optionalDependencies: + fsevents "2.3.2" please-upgrade-node@^3.2.0: version "3.2.0"