diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index d0d8ed10c6..452f648fb8 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -122,6 +122,7 @@ jobs: "reportsUpdated2.spec.js", "dashboards/dashboard-filter.spec.js", "dashboards/dashboard-folder.spec.js", + "dashboards/dashboard-multi-y-axis.spec.js", "join.spec.js", "changeOrg.spec.js", "serviceAccount.spec.js", diff --git a/tests/ui-testing/pages/dashboardPages/dashboard-chart.js b/tests/ui-testing/pages/dashboardPages/dashboard-chart.js new file mode 100644 index 0000000000..9b6b6da077 --- /dev/null +++ b/tests/ui-testing/pages/dashboardPages/dashboard-chart.js @@ -0,0 +1,141 @@ +import { expect } from "@playwright/test"; + +// pages/chartTypeSelector.js +// Methods : selectChartType, selectStreamType, searchAndAddField, selectStream + +export default class ChartTypeSelector { + constructor(page) { + this.page = page; + } + + // Chart Type select + async selectChartType(chartType) { + const chartOption = this.page.locator( + `[data-test="selected-chart-${chartType}-item"]` + ); + await chartOption.waitFor({ state: "visible", timeout: 5000 }); + await chartOption.click(); + } + + // Stream Type select + async selectStreamType(type) { + // Click the dropdown + await this.page.locator('[data-test="index-dropdown-stream_type"]').click(); + + await this.page + .getByRole("option", { name: type }) + .locator("div") + .nth(2) + .click(); + } + + // Stream select + async selectStream(streamName) { + const streamInput = this.page.locator( + '[data-test="index-dropdown-stream"]' + ); + await streamInput.click(); + await streamInput.press("Control+a"); + await streamInput.fill(streamName); + + const streamOption = this.page + .getByRole("option", { name: streamName, exact: true }) + .locator("div") + .nth(2); + + await streamOption.waitFor({ state: "visible", timeout: 5000 }); + await streamOption.click(); + } + + // Search field and added for X, Y,Breakdown etc. + async searchAndAddField(fieldName, target) { + const searchInput = this.page.locator( + '[data-test="index-field-search-input"]' + ); + await searchInput.click(); + await searchInput.fill(fieldName); + + const buttonSelectors = { + x: '[data-test="dashboard-add-x-data"]', + y: '[data-test="dashboard-add-y-data"]', + b: '[data-test="dashboard-add-b-data"]', + filter: '[data-test="dashboard-add-filter-data"]', + latitude: '[data-test="dashboard-add-latitude-data"]', + longitude: '[data-test="dashboard-add-longitude-data"]', + weight: '[data-test="dashboard-add-weight-data"]', + z: '[data-test="dashboard-add-z-data"]', + }; + + const buttonSelector = buttonSelectors[target]; + + if (!buttonSelector) { + throw new Error(`Invalid target type: ${target}`); + } + + await this.page.locator(buttonSelector).click(); + } + + // Add filter condition + async addFilterCondition(initialFieldName, newFieldName, operator, value) { + // Step 1: Click the existing field if provided + if (initialFieldName) { + await this.page + .locator( + `[data-test="dashboard-add-condition-label-0-${initialFieldName}"]` + ) + .click(); + } + + // Step 2: Change field if newFieldName is provided + if (newFieldName) { + const fieldDropdown = this.page.locator( + '[data-test="dashboard-add-condition-column-0\\}"]' + ); + + await fieldDropdown.click(); + await fieldDropdown.fill(newFieldName); + + await this.page + .getByRole("option", { name: newFieldName, exact: true }) + .first() + .click(); + } + + const selectedField = newFieldName || initialFieldName; + + // Step 3: Open the condition configuration if operator or value is being handled + if (operator || value) { + await this.page + .locator('[data-test="dashboard-add-condition-list-0"]') + .click(); + await this.page + .locator('[data-test="dashboard-add-condition-condition-0"]') + .click(); + } + + // Step 4: Select operator if provided + if (operator) { + await this.page + .locator('[data-test="dashboard-add-condition-operator"]') + .click(); + await this.page.getByRole("option", { name: operator }).click(); + } + + // Step 5: Enter value if provided + if (value) { + const valueInput = this.page.locator( + '[data-test="common-auto-complete"]' + ); + await valueInput.click(); + await valueInput.fill(value); + } else if (operator && selectedField) { + // Step 6: Assert dynamic error message for missing value + const expectedError = `Filter: ${selectedField}: Condition value required`; + const errorMessageLocator = this.page + .locator("div") + .filter({ hasText: expectedError }) + .first(); + await expect(errorMessageLocator).toBeVisible(); + } + } +} diff --git a/tests/ui-testing/pages/dashboardPages/dashboard-create.js b/tests/ui-testing/pages/dashboardPages/dashboard-create.js new file mode 100644 index 0000000000..623a85548b --- /dev/null +++ b/tests/ui-testing/pages/dashboardPages/dashboard-create.js @@ -0,0 +1,46 @@ +import { expect } from "@playwright/test"; + +// pages/chartTypeSelector.js +// methods: createDashboard, searchDashboard, AddPanel, applyButton + +export default class DashboardCreate { + constructor(page) { + this.page = page; + this.dashCreateBtn = page.locator('[data-test="dashboard-add"]'); + this.dashName = page.locator('[data-test="add-dashboard-name"]'); + this.submitBtn = page.locator('[data-test="dashboard-add-submit"]'); + this.deleteIcon = page.locator('[data-test="dashboard-delete"]'); + this.confirmDelete = page.locator('[data-test="confirm-button"]'); + this.searchDash = page.locator('[data-test="dashboard-search"]'); + this.addPanelIfEmptyBtn = page.locator( + '[data-test="dashboard-if-no-panel-add-panel-btn"]' + ); + this.applyQueryBtn = page.locator('[data-test="dashboard-apply"]'); + } + async createDashboard(dashboardName) { + await this.dashCreateBtn.click(); + await this.dashName.click(); + await this.dashName.fill(dashboardName); + await this.submitBtn.click(); + } + + //Search Folder + async searchDashboard(dashboardName) { + await this.page + .locator('[data-test="dashboard-folder-tab-default"]') + .waitFor({ state: "visible" }); + + await this.searchDash.click(); + await this.searchDash.fill(dashboardName); + } + + //Add Panel to dashboard + async addPanel() { + await expect(this.addPanelIfEmptyBtn).toBeVisible({ timeout: 5000 }); + await this.addPanelIfEmptyBtn.click(); + } + + async applyButton() { + await this.applyQueryBtn.click(); + } +} diff --git a/tests/ui-testing/pages/dashboardPages/dashboard-panel-actions.js b/tests/ui-testing/pages/dashboardPages/dashboard-panel-actions.js new file mode 100644 index 0000000000..ced38d9280 --- /dev/null +++ b/tests/ui-testing/pages/dashboardPages/dashboard-panel-actions.js @@ -0,0 +1,38 @@ +// Dashboard actions page +// Methods : AddPanelName, SavePanel, ApplyDashboardBtn + +export default class DashboardactionPage { + constructor(page) { + this.page = page; + + this.panelNameInput = page.locator('[data-test="dashboard-panel-name"]'); + this.panelSaveBtn = page.locator('[data-test="dashboard-panel-save"]'); + this.applyDashboard = page.locator('[data-test="dashboard-apply"]'); + } + + // Add panel name + async addPanelName(panelName) { + await this.panelNameInput.click(); + await this.panelNameInput.fill(panelName); + } + + // Save panel button + async savePanel() { + await this.panelSaveBtn.click(); + } + + //Apply dashboard button + async applyDashboardBtn() { + await this.applyDashboard.click(); + } + + // Wait for chart to render + + async waitForChartToRender() { + // Wait for it to go back to bg-secondary (render complete) + await this.page.waitForFunction(() => { + const btn = document.querySelector('[data-test="dashboard-apply"]'); + return btn && btn.classList.contains("bg-secondary"); + }); + } +} diff --git a/tests/ui-testing/pages/dashboardPages/dashboard-time.js b/tests/ui-testing/pages/dashboardPages/dashboard-time.js new file mode 100644 index 0000000000..f09dfb374c --- /dev/null +++ b/tests/ui-testing/pages/dashboardPages/dashboard-time.js @@ -0,0 +1,22 @@ +import { expect } from "@playwright/test"; + +//methods : setRelativeTimeRange + +export default class DateTimeHelper { + constructor(page) { + this.page = page; + this.applyQueryBtn = page.locator('[data-test="dashboard-apply"]'); + this.timePickerBtn = page.locator('[data-test="date-time-btn"]'); + this.applyTimeBtn = page.locator('[data-test="date-time-apply-btn"]'); + } + // set relative time range + async setRelativeTimeRange(rangeCode) { + // Minutes= m Hours= h Days= d Weeks= w Months= M + await this.applyQueryBtn.click(); + await this.timePickerBtn.click(); + await this.page + .locator(`[data-test="date-time-relative-${rangeCode}-btn"]`) + .click(); + await this.applyTimeBtn.click(); + } +} diff --git a/tests/ui-testing/playwright-tests/dashboards/dashboard-multi-y-axis.spec.js b/tests/ui-testing/playwright-tests/dashboards/dashboard-multi-y-axis.spec.js new file mode 100644 index 0000000000..78fbad4f65 --- /dev/null +++ b/tests/ui-testing/playwright-tests/dashboards/dashboard-multi-y-axis.spec.js @@ -0,0 +1,91 @@ +import { test, expect } from "../baseFixtures.js"; +import logData from "../../cypress/fixtures/log.json"; +import logsdata from "../../../test-data/logs_data.json"; +import { login } from "../utils/dashLogin.js"; +import { ingestion } from "../utils/dashIngestion.js"; +import { + waitForDashboardPage, + deleteDashboard, +} from "../utils/dashCreation.js"; + +import ChartTypeSelector from "../../pages/dashboardPages/dashboard-chart.js"; +import DashboardListPage from "../../pages/dashboardPages/dashboard-list.js"; +import DashboardCreate from "../../pages/dashboardPages/dashboard-create.js"; +import DateTimeHelper from "../../pages/dashboardPages/dashboard-time.js"; +import DashboardactionPage from "../../pages/dashboardPages/dashboard-panel-actions.js"; + +const randomDashboardName = + "Dashboard_" + Math.random().toString(36).substr(2, 9); + +test.describe.configure({ mode: "parallel" }); + +test.describe("dashboard multi y axis testcases", () => { + test.beforeEach(async ({ page }) => { + console.log("running before each"); + await login(page); + await page.waitForTimeout(1000); + await ingestion(page); + await page.waitForTimeout(2000); + + const orgNavigation = page.goto( + `${logData.logsUrl}?org_identifier=${process.env["ORGNAME"]}` + ); + await orgNavigation; + }); + + test("Should correctly add multiple Y-axes to the stacked chart type.", async ({ + page, + }) => { + const chartTypeSelector = new ChartTypeSelector(page); + const dashboardPage = new DashboardListPage(page); + const dashboardCreate = new DashboardCreate(page); + const dateTimeHelper = new DateTimeHelper(page); + const dashboardPageActions = new DashboardactionPage(page); + + await dashboardPage.menuItem("dashboards-item"); + + await waitForDashboardPage(page); + + await dashboardCreate.createDashboard(randomDashboardName); + + await dashboardCreate.addPanel(); + + await chartTypeSelector.selectChartType("stacked"); + + await chartTypeSelector.selectStreamType("logs"); + + await chartTypeSelector.selectStream("e2e_automate"); + + await chartTypeSelector.searchAndAddField("kubernetes_namespace_name", "y"); + + await chartTypeSelector.searchAndAddField("kubernetes_container_name", "y"); + + await chartTypeSelector.searchAndAddField("kubernetes_labels_name", "b"); + + await dateTimeHelper.setRelativeTimeRange("6-w"); + + await dashboardPageActions.waitForChartToRender(); + + await page + .locator('[data-test="dashboard-panel-data-view-query-inspector-btn"]') + .click(); + + await expect( + page + .getByRole("cell", { + name: 'SELECT histogram(_timestamp) as "x_axis_1", count(kubernetes_namespace_name) as "y_axis_1", count(kubernetes_container_name) as "y_axis_2", kubernetes_labels_name as "breakdown_1" FROM "e2e_automate" GROUP BY x_axis_1, breakdown_1 ORDER BY x_axis_1 ASC', + }) + .first() + ).toBeVisible(); + + await page.locator('[data-test="query-inspector-close-btn"]').click(); + + await dashboardPageActions.addPanelName(randomDashboardName); + + await dashboardPageActions.savePanel(); + + await page.locator('[data-test="dashboard-back-btn"]').click(); + + await deleteDashboard(page, randomDashboardName); + }); +});