mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-06 08:09:24 +00:00
670 lines
32 KiB
JavaScript
670 lines
32 KiB
JavaScript
import { expect } from '@playwright/test';
|
|
import { confirmDraftSignOut } from '../generalPages/draftSignOut.js';
|
|
import {
|
|
dateTimeButtonLocator, relative30SecondsButtonLocator, absoluteTabLocator,
|
|
Past30SecondsValue, openNavFlyoutChild
|
|
} from '../commonActions.js';
|
|
|
|
export class ReportsPage {
|
|
constructor(page) {
|
|
this.page = page;
|
|
|
|
// Navigation locators (data-test only). The MainLayout side-nav builds
|
|
// each entry's data-test as `menu-link-${link}-item`, so the Home link
|
|
// (`link: "/"`) becomes `menu-link-/-item` (escaped CSS: `menu-link-\\/-item`).
|
|
this.reportListTitle = page.locator('[data-test="report-list-title"]');
|
|
this.reportListTable = page.locator('[data-test="report-list-table"]');
|
|
this.scheduledTab = page.locator('[data-test="tab-shared"]');
|
|
|
|
// Create-report flow locators
|
|
this.addReportButton = page.locator('[data-test="report-list-add-report-btn"]');
|
|
this.reportNameInputField = page.locator('[data-test="add-report-name-input-field"]');
|
|
this.descriptionInputField = page.locator('[data-test="add-report-description-input-field"]');
|
|
|
|
// Step-1 dashboard pickers (OSelect triggers use data-test-selected-value attribute)
|
|
this.dashboardFolderSelect = page.locator('[data-test="add-report-dashboard-folder-select"]');
|
|
this.dashboardFolderTrigger = page.locator('[data-test="add-report-dashboard-folder-select"] [data-test-selected-value]');
|
|
this.dashboardFolderPopover = page.locator('[data-test="add-report-dashboard-folder-select-popover"]');
|
|
this.dashboardFolderOption = (value) => page.locator(`[data-test="add-report-dashboard-folder-select-option"][data-test-value="${value}"]`);
|
|
|
|
this.dashboardNameSelect = page.locator('[data-test="add-report-dashboard-name-select"]');
|
|
this.dashboardNameTrigger = page.locator('[data-test="add-report-dashboard-name-select"] [data-test-selected-value]');
|
|
this.dashboardNamePopover = page.locator('[data-test="add-report-dashboard-name-select-popover"]');
|
|
this.dashboardNameOptions = page.locator('[data-test="add-report-dashboard-name-select-option"]');
|
|
|
|
this.dashboardTabSelect = page.locator('[data-test="add-report-dashboard-tab-select"]');
|
|
this.dashboardTabTrigger = page.locator('[data-test="add-report-dashboard-tab-select"] [data-test-selected-value]');
|
|
this.dashboardTabPopover = page.locator('[data-test="add-report-dashboard-tab-select-popover"]');
|
|
this.dashboardTabOption = (value) => page.locator(`[data-test="add-report-dashboard-tab-select-option"][data-test-value="${value}"]`);
|
|
|
|
// Step navigation buttons
|
|
this.continueButtonStep1 = page.locator('[data-test="add-report-step1-continue-btn"]');
|
|
this.continueButtonStep2 = page.locator('[data-test="add-report-step2-continue-btn"]');
|
|
|
|
// Step-2 frequency buttons
|
|
this.frequencyOnceBtn = page.locator('[data-test="add-report-schedule-frequency-once-btn"]');
|
|
this.frequencyHoursBtn = page.locator('[data-test="add-report-schedule-frequency-hours-btn"]');
|
|
this.frequencyDaysBtn = page.locator('[data-test="add-report-schedule-frequency-days-btn"]');
|
|
this.frequencyWeeksBtn = page.locator('[data-test="add-report-schedule-frequency-weeks-btn"]');
|
|
this.frequencyMonthsBtn = page.locator('[data-test="add-report-schedule-frequency-months-btn"]');
|
|
this.frequencyCustomBtn = page.locator('[data-test="add-report-schedule-frequency-custom-btn"]');
|
|
this.frequencyCronBtn = page.locator('[data-test="add-report-schedule-frequency-cron-btn"]');
|
|
this.scheduleNowBtn = page.locator('[data-test="add-report-schedule-scheduleNow-btn"]');
|
|
this.scheduleLaterBtn = page.locator('[data-test="add-report-schedule-scheduleLater-btn"]');
|
|
|
|
// Schedule-later inputs — ODate forwards consumer data-test to its outer wrapper <div>
|
|
// via v-bind="$attrs"; the interactable role="group" segments container carries
|
|
// `${parentDataTest}-group` (added to ODate.vue). OTime places data-test directly
|
|
// on its role="group" element (compound selector ok).
|
|
this.scheduleStartDateField = page.locator('[data-test="add-report-schedule-start-date-field-group"]');
|
|
this.scheduleStartTimeField = page.locator('[data-test="add-report-schedule-start-time-field"][role="group"]');
|
|
this.scheduleTimezoneSelect = page.locator('[data-test="add-report-schedule-start-timezone-select"]');
|
|
this.scheduleTimezoneTrigger = page.locator('[data-test="add-report-schedule-start-timezone-select"] [data-test-selected-value]');
|
|
this.scheduleTimezonePopover = page.locator('[data-test="add-report-schedule-start-timezone-select-popover"]');
|
|
this.scheduleTimezoneOption = (value) => page.locator(`[data-test="add-report-schedule-start-timezone-select-option"][data-test-value="${value}"]`);
|
|
|
|
// Share step
|
|
this.titleInputField = page.locator('[data-test="add-report-share-title-input-field"]');
|
|
this.recipientsInputField = page.locator('[data-test="add-report-share-recipients-input-field"]');
|
|
|
|
// Save / cancel
|
|
this.saveButton = page.locator('[data-test="add-report-save-btn"]');
|
|
this.cancelButton = page.locator('[data-test="add-report-cancel-btn"]');
|
|
|
|
// ── Report Format section ──────────────────────────────────────────────
|
|
this.reportFormatSection = page.locator('[data-test="add-report-format-section"]');
|
|
|
|
// Report Type OSelect (trigger + popover + per-option)
|
|
this.reportTypeSelect = page.locator('[data-test="add-report-type-select"]');
|
|
this.reportTypeTrigger = page.locator('[data-test="add-report-type-select"] [data-test-selected-value]');
|
|
this.reportTypePopover = page.locator('[data-test="add-report-type-select-popover"]');
|
|
// OSelect always stamps data-test-value on options (unconditional), but
|
|
// data-test-concatenated attrs are only emitted when OSelect receives a
|
|
// direct data-test prop. This instance lacks it — match by value only.
|
|
this.reportTypeOption = (value) => page.locator(`[data-test-value="${value}"]`).first();
|
|
|
|
// Attachment Type OSelect (hidden when CSV is selected)
|
|
this.attachmentTypeSelect = page.locator('[data-test="add-report-attachment-type-select"]');
|
|
|
|
// Custom Dimensions expandable section (hidden when CSV is selected)
|
|
this.customDimensionsSection = page.locator('[data-test="add-report-custom-dimensions-section"]');
|
|
|
|
// PNG warning banner (visible only when PNG is selected)
|
|
this.pngNoteBanner = page.locator('[data-test="add-report-png-note"]');
|
|
|
|
// Toasts (OToast variants)
|
|
this.toastSuccess = page.locator('[data-test-variant="success"]');
|
|
this.toastError = page.locator('[data-test-variant="error"]');
|
|
this.toastMessage = page.locator('[data-test="o-toast-message"]');
|
|
|
|
// Common time-range locators (used by Logs/Reports/Dashboards screens, exported as data-test)
|
|
this.dateTimeButton = dateTimeButtonLocator;
|
|
this.relative30SecondsButton = page.locator(relative30SecondsButtonLocator);
|
|
this.absoluteTab = absoluteTabLocator;
|
|
|
|
// Header / profile / logout
|
|
this.profileButton = page.locator('[data-test="header-my-account-profile-icon"]');
|
|
this.signOutButton = page.locator('[data-test="menu-link-logout-item"]');
|
|
|
|
// Report list — search + row actions
|
|
this.reportSearchInput = page.locator('[data-test="report-list-search-input"]');
|
|
this.reportSearchInputField = page.locator('[data-test="report-list-search-input-field"]');
|
|
// Scope-based row resolution: find the per-name cell (added in ReportList.vue
|
|
// §6 source edit), walk up to the OTable row, then locate the action button
|
|
// within. More robust than relying on a standalone embedded-name data-test
|
|
// which can race the rendering of the row.
|
|
this.reportRow = (reportName) => page.locator(`[data-test="report-list-name-cell-${reportName}"]`)
|
|
.locator('xpath=ancestor::*[starts-with(@data-test,"o2-table-row-")]').first();
|
|
this.pauseStartReportBtn = (reportName) => this.reportRow(reportName).locator('[data-test$="-pause-start-report"]');
|
|
this.editReportBtn = (reportName) => this.reportRow(reportName).locator('[data-test$="-edit-report"]');
|
|
this.deleteReportBtn = (reportName) => this.reportRow(reportName).locator('[data-test$="-delete-report"]');
|
|
|
|
// Confirm dialog (delete)
|
|
this.confirmDialog = page.locator('[data-test="confirm-dialog"]');
|
|
this.confirmPrimaryBtn = page.locator('[data-test="confirm-dialog"] [data-test="o-dialog-primary-btn"]');
|
|
this.confirmSecondaryBtn = page.locator('[data-test="confirm-dialog"] [data-test="o-dialog-secondary-btn"]');
|
|
|
|
// Multi-org / organization switcher (used by reportsPageDefaultMultiOrg)
|
|
this.orgSelectTrigger = page.locator('[data-test="navbar-organizations-select"] [data-test-selected-value]');
|
|
this.orgSearchInputField = page.locator('[data-test="organization-search-input-field"]');
|
|
this.orgMenuItemLabel = page.locator('[data-test="organization-menu-item-label-item-label"]');
|
|
|
|
// Date/Time locators wrapped as Locator (the constants are CSS selector strings)
|
|
this.dateTimeButtonLocator = page.locator(dateTimeButtonLocator);
|
|
this.absoluteTabLocator = page.locator(absoluteTabLocator);
|
|
this.absoluteStartTimeField = page.locator('[data-test="date-time-absolute-start-time-field"]');
|
|
this.absoluteEndTimeField = page.locator('[data-test="date-time-absolute-end-time-field"]');
|
|
}
|
|
|
|
// Returns the search-input locator preferring the inner -field variant when present
|
|
reportSearchField() {
|
|
return this.reportSearchInputField;
|
|
}
|
|
|
|
// OTime wraps a (visually hidden) native <input type="time"> inside the
|
|
// schedule start-time role="group" field. Returns that inner input so specs
|
|
// can fill/read the 24h HH:MM value directly (force:true needed to fill).
|
|
scheduleStartTimeInput() {
|
|
return this.scheduleStartTimeField.locator('input[type="time"]');
|
|
}
|
|
|
|
async navigateToReports() {
|
|
await openNavFlyoutChild(this.page, 'reports');
|
|
await expect(this.reportListTitle).toContainText('Reports');
|
|
await this.scheduledTab.click({ force: true });
|
|
}
|
|
|
|
async goToReports() {
|
|
await openNavFlyoutChild(this.page, 'reports');
|
|
await expect(this.reportListTitle).toContainText('Reports');
|
|
}
|
|
|
|
async reportsPageDefaultMultiOrg() {
|
|
await this.orgSelectTrigger.click({ force: true });
|
|
await this.page.waitForTimeout(2000);
|
|
|
|
// Search for the organization
|
|
await this.orgSearchInputField.fill('defaulttestmulti');
|
|
await this.page.waitForTimeout(2000);
|
|
|
|
// Click the organization from search results
|
|
await this.orgMenuItemLabel.first().click({ force: true });
|
|
}
|
|
|
|
async reportsPageURLValidation() {
|
|
// TODO: fix this test
|
|
// await expect(this.page).not.toHaveURL(/default/);
|
|
}
|
|
|
|
async reportsURLValidation() {
|
|
await expect(this.page).toHaveURL(/report/);
|
|
}
|
|
|
|
async createReportAddReportButton() {
|
|
await this.addReportButton.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.addReportButton.click({ force: true });
|
|
}
|
|
|
|
async createReportReportNameInput(TEST_REPORT_NAME) {
|
|
await this.reportNameInputField.waitFor({ state: 'visible', timeout: 10000 });
|
|
// onBeforeMount seeds the form via an async form.reset() that can land after this fill and wipe name to "" — re-fill until it survives, else the submit is silently blocked by "Name is required".
|
|
await expect(async () => {
|
|
await this.reportNameInputField.fill(TEST_REPORT_NAME);
|
|
await this.page.waitForTimeout(1000);
|
|
// Short timeout: the default expect timeout outlives the toPass budget, so a wiped value was never re-filled.
|
|
await expect(this.reportNameInputField).toHaveValue(TEST_REPORT_NAME, { timeout: 2000 });
|
|
}).toPass({ timeout: 20000, intervals: [1000, 2000, 3000] });
|
|
}
|
|
|
|
async createReportFolderInput() {
|
|
// Open folder popover (PopoverTrigger inside the OSelect wrapper)
|
|
await this.dashboardFolderTrigger.click({ force: true });
|
|
await this.dashboardFolderPopover.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
// Folder value === "default"
|
|
const opt = this.dashboardFolderOption('default');
|
|
await opt.waitFor({ state: 'visible', timeout: 10000 });
|
|
await opt.click({ force: true });
|
|
await this.dashboardFolderPopover.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
|
|
}
|
|
|
|
async createReportDashboardInput(dashboardName) {
|
|
// Open dashboard popover, find option whose label matches dashboardName, click it
|
|
await this.dashboardNameTrigger.click({ force: true });
|
|
await this.dashboardNamePopover.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
|
|
// Wait until at least one option is rendered
|
|
await this.dashboardNameOptions.first().waitFor({ state: 'visible', timeout: 15000 });
|
|
|
|
// Pick option by its text content (scoped to data-test list), via evaluate using data-test attrs only
|
|
const clicked = await this.page.evaluate((name) => {
|
|
const opts = Array.from(
|
|
document.querySelectorAll('[data-test="add-report-dashboard-name-select-option"]')
|
|
);
|
|
for (const el of opts) {
|
|
const text = (el.textContent || '').trim();
|
|
if (text === name || text.includes(name)) {
|
|
el.scrollIntoView({ block: 'center' });
|
|
(el).dispatchEvent(new MouseEvent('pointerdown', { bubbles: true }));
|
|
(el).click();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}, dashboardName);
|
|
|
|
if (!clicked) {
|
|
// Fallback: click the first option
|
|
await this.dashboardNameOptions.first().click({ force: true });
|
|
}
|
|
await this.dashboardNamePopover.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
|
|
await this.page.waitForTimeout(500);
|
|
}
|
|
|
|
async createReportDashboardTabInput() {
|
|
await this.dashboardTabTrigger.click({ force: true });
|
|
await this.dashboardTabPopover.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
const opt = this.dashboardTabOption('default');
|
|
await opt.waitFor({ state: 'visible', timeout: 10000 });
|
|
await opt.click({ force: true });
|
|
await this.dashboardTabPopover.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
|
|
}
|
|
|
|
async createReportContinueButtonStep1() {
|
|
// Advance step 1 (dashboard) -> step 2 (schedule). On the EDIT flow the
|
|
// step-1 fields (folder/dashboard/tabs) are prefilled asynchronously via
|
|
// form.reset() after the report GET resolves; goToStep() validates those
|
|
// fields before advancing, so a single Continue click that lands before the
|
|
// prefill is applied fails validation and silently stays on step 1 (the
|
|
// schedule toggle never renders — the create/edit flow then times out looking
|
|
// for it). Retry the click until the schedule step is actually reached.
|
|
await expect(async () => {
|
|
if (await this.continueButtonStep1.isVisible().catch(() => false)) {
|
|
await this.continueButtonStep1.click({ force: true });
|
|
}
|
|
await expect(this.scheduleLaterBtn).toBeVisible({ timeout: 3000 });
|
|
}).toPass({ timeout: 20000, intervals: [500, 1000, 2000] });
|
|
// Preserve the original post-advance settle: downstream step-2 interactions
|
|
// (the scheduleLater toggle, the step-2 Continue) can be swallowed if driven
|
|
// before the freshly-mounted step has settled.
|
|
await this.page.waitForTimeout(3000);
|
|
}
|
|
|
|
async createReportOnce() {
|
|
await this.frequencyOnceBtn.click({ force: true });
|
|
}
|
|
|
|
async createReportHours() {
|
|
await this.frequencyHoursBtn.click({ force: true });
|
|
}
|
|
|
|
async createReportDays() {
|
|
await this.frequencyDaysBtn.click({ force: true });
|
|
}
|
|
|
|
async createReportWeeks() {
|
|
await this.frequencyWeeksBtn.click({ force: true });
|
|
}
|
|
|
|
async createReportMonths() {
|
|
await this.frequencyMonthsBtn.click({ force: true });
|
|
}
|
|
|
|
async createReportCustom() {
|
|
await this.frequencyCustomBtn.click({ force: true });
|
|
}
|
|
|
|
async createReportCron() {
|
|
await this.frequencyCronBtn.click({ force: true });
|
|
}
|
|
|
|
async createReportDateTime() {
|
|
// Start Date + Start Time are REQUIRED + format-checked on the non-cron
|
|
// "Schedule Later" tab (CreateReport.schema date/time rule), so both must be
|
|
// entered with well-formed values or the save is blocked.
|
|
|
|
// ODate is a Reka segmented field (no native <input>). reka-ui defaults its
|
|
// locale to "en", which ICU resolves to en-US → segment order is
|
|
// MONTH / DAY / YEAR (deterministic; independent of the runner's OS locale).
|
|
// Click the field, walk to the leftmost (month) segment with ArrowLeft, then
|
|
// type the segments in M/D/Y order so Reka auto-advances and emits ISO
|
|
// YYYY-MM-DD (here: 2027-12-29). Typing the day (29) into the month segment
|
|
// first misplaces the digits and leaves `date` empty/invalid, which blocks
|
|
// the step-2 -> step-3 advance (goToStep validates date/time).
|
|
await this.scheduleStartDateField.click({ force: true });
|
|
await this.page.keyboard.press('ArrowLeft');
|
|
await this.page.keyboard.press('ArrowLeft'); // 3 segments -> 2 lefts reaches month from any start
|
|
await this.page.keyboard.type('12'); // month
|
|
await this.page.keyboard.type('29'); // day
|
|
await this.page.keyboard.type('2027'); // year
|
|
await this.page.keyboard.press('Escape');
|
|
|
|
// OTime wraps a (visually hidden) native <input type="time"> inside its field
|
|
// group — fill it directly with a 24h HH:MM value (matches the schema's
|
|
// reportTimeRegex). force:true because the native input is visually hidden.
|
|
await this.scheduleStartTimeField.waitFor({ state: 'visible', timeout: 5000 });
|
|
const timeInput = this.scheduleStartTimeField.locator('input[type="time"]');
|
|
await timeInput.fill('10:30', { force: true });
|
|
}
|
|
|
|
async createReportZone() {
|
|
await this.scheduleTimezoneTrigger.click({ force: true });
|
|
await this.scheduleTimezonePopover.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
const opt = this.scheduleTimezoneOption('UTC');
|
|
await opt.waitFor({ state: 'visible', timeout: 10000 });
|
|
await opt.click({ force: true });
|
|
await this.scheduleTimezonePopover.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
|
|
}
|
|
|
|
async setTimeZone(zone) {
|
|
await this.scheduleTimezoneTrigger.click({ force: true });
|
|
await this.scheduleTimezonePopover.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.scheduleTimezoneOption(zone).click({ force: true });
|
|
}
|
|
|
|
async setTimeIST() {
|
|
await this.scheduleTimezoneTrigger.click({ force: true });
|
|
await this.scheduleTimezonePopover.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.scheduleTimezoneOption('Asia/Calcutta').click({ force: true });
|
|
}
|
|
|
|
async createReportScheduleLater() {
|
|
await this.scheduleLaterBtn.click({ force: true });
|
|
}
|
|
|
|
async createReportContinueButtonStep2() {
|
|
await this.continueButtonStep2.click({ force: true });
|
|
}
|
|
|
|
async createReportFillDetail() {
|
|
await this.titleInputField.fill('reporterTest');
|
|
await this.recipientsInputField.fill(process.env["ZO_ROOT_USER_EMAIL"]);
|
|
}
|
|
|
|
async createReportSaveButton() {
|
|
const [response] = await Promise.all([
|
|
this.page.waitForResponse(
|
|
(resp) => {
|
|
const pathname = new URL(resp.url()).pathname;
|
|
return /^\/api\/(?:v2\/)?[^/]+\/reports\/?$/.test(pathname) &&
|
|
resp.request().method() === 'POST';
|
|
},
|
|
{ timeout: 30000 }
|
|
),
|
|
this.saveButton.click({ force: true }),
|
|
]);
|
|
|
|
if (!response.ok()) {
|
|
const responseBody = await response.text().catch(() => '<response body unavailable>');
|
|
throw new Error(
|
|
`Report creation failed with HTTP ${response.status()}: ${responseBody}`
|
|
);
|
|
}
|
|
}
|
|
|
|
async verifyReportCreated(reportName) {
|
|
await expect(this.page).toHaveURL(/\/web\/reports(?:\?|$)/, { timeout: 30000 });
|
|
|
|
// Reload the reports list and wait for its collection request. Both v1 and
|
|
// v2 are accepted so this helper works across mixed-version deployments.
|
|
const reportsApiPromise = this.page.waitForResponse(
|
|
(resp) => {
|
|
const pathname = new URL(resp.url()).pathname;
|
|
return /^\/api\/(?:v2\/)?[^/]+\/reports\/?$/.test(pathname) &&
|
|
resp.request().method() === 'GET' &&
|
|
resp.ok();
|
|
},
|
|
{ timeout: 30000 }
|
|
);
|
|
await this.page.goto(process.env["ZO_BASE_URL"] + "/web/reports?org_identifier=" + process.env["ORGNAME"]);
|
|
await reportsApiPromise;
|
|
|
|
// Search for the report and verify the action button is present
|
|
await this.reportSearchInputField.fill(reportName);
|
|
await expect(this.pauseStartReportBtn(reportName)).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
async createReport(dashboardName) {
|
|
await this.createReportAddReportButton();
|
|
await this.createReportReportNameInput('rreport1');
|
|
await this.createReportFolderInput();
|
|
await this.createReportDashboardInput(dashboardName);
|
|
await this.createReportDashboardTabInput();
|
|
await this.continueButtonStep1.click({ force: true });
|
|
await this.continueButtonStep2.click({ force: true });
|
|
await this.titleInputField.fill('reporterTest');
|
|
await this.recipientsInputField.fill(process.env["ZO_ROOT_USER_EMAIL"]);
|
|
await this.saveButton.click({ force: true });
|
|
}
|
|
|
|
async verifyReportSaved() {
|
|
await expect(this.toastSuccess.first()).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
async deleteReport(reportName) {
|
|
await this.reportSearchInputField.fill(reportName);
|
|
await this.deleteReportBtn(reportName).click({ force: true });
|
|
await this.confirmPrimaryBtn.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.confirmPrimaryBtn.click();
|
|
}
|
|
|
|
// Switching tabs invalidates the folder cache and reloads — the path #7280's stale list came from.
|
|
/**
|
|
* Switch between the Scheduled ("shared") and Cached report tabs.
|
|
* @param {'shared'|'cached'} tabValue
|
|
*/
|
|
async selectReportTab(tabValue) {
|
|
const tab = this.page.locator(`[data-test="tab-${tabValue}"]`);
|
|
await tab.waitFor({ state: 'visible', timeout: 15000 });
|
|
await tab.click();
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
}
|
|
|
|
async expectReportListed(reportName) {
|
|
await this.reportSearchInputField.fill(reportName);
|
|
await expect(this.reportRow(reportName)).toBeVisible({ timeout: 20000 });
|
|
}
|
|
|
|
async expectReportNotListed(reportName) {
|
|
await this.reportSearchInputField.fill(reportName);
|
|
await expect(this.reportRow(reportName)).toHaveCount(0, { timeout: 20000 });
|
|
}
|
|
|
|
async setTimeToPast30Seconds() {
|
|
// Set the time filter to the last 30 seconds
|
|
await this.dateTimeButtonLocator.click({ force: true });
|
|
await this.relative30SecondsButton.click({ force: true });
|
|
}
|
|
|
|
async verifyTimeSetTo30Seconds() {
|
|
// Verify that the time filter displays "Past 30 Seconds"
|
|
await expect(this.dateTimeButtonLocator).toContainText(Past30SecondsValue);
|
|
}
|
|
|
|
async setDateTime() {
|
|
await expect(this.dateTimeButtonLocator).toBeVisible();
|
|
await this.dateTimeButtonLocator.click({ force: true });
|
|
await this.absoluteTabLocator.click({ force: true });
|
|
await this.page.waitForTimeout(1000);
|
|
}
|
|
|
|
async fillTimeRange(startTime, endTime) {
|
|
// Absolute-tab calendar — first day cell, then access_time input (left untouched: this method isn't used by reportsScheduleNow)
|
|
await this.absoluteTabLocator.click({ force: true });
|
|
await this.absoluteStartTimeField.fill(startTime);
|
|
await this.absoluteEndTimeField.fill(endTime);
|
|
}
|
|
|
|
async verifyDateTime(startTime, endTime) {
|
|
await expect(this.dateTimeButtonLocator).toContainText(`${startTime} - ${endTime}`);
|
|
}
|
|
|
|
async signOut() {
|
|
// A forced click on the still-opening profile menu can miss, leaving the session alive; retry until /login.
|
|
await expect(async () => {
|
|
if (/\/login/.test(this.page.url())) return;
|
|
if (!(await this.signOutButton.isVisible())) await this.profileButton.click();
|
|
await this.signOutButton.waitFor({ state: 'visible', timeout: 5000 });
|
|
await this.signOutButton.click();
|
|
await confirmDraftSignOut(this.page);
|
|
await this.page.waitForURL(/\/login/, { timeout: 5000 });
|
|
}).toPass({ timeout: 45000, intervals: [1000, 2000, 5000] });
|
|
}
|
|
|
|
async pauseReport(reportName) {
|
|
const btn = this.pauseStartReportBtn(reportName);
|
|
await this.reportSearchInputField.fill(reportName);
|
|
const visible = await btn.isVisible().catch(() => false);
|
|
|
|
// Cross-cluster (super-cluster / SC) propagation race:
|
|
// After SC login the active org may differ from ORGNAME (the org the report
|
|
// was created in). Use page.goto with an explicit org_identifier rather than
|
|
// page.reload so every retry lands on the correct org. Also wait for
|
|
// networkidle so the reports API response is fully loaded before we search.
|
|
if (!visible) {
|
|
const origin = new URL(this.page.url()).origin;
|
|
const reportsUrl = `${origin}/web/reports?org_identifier=${process.env["ORGNAME"]}`;
|
|
await expect.poll(async () => {
|
|
await this.page.goto(reportsUrl, { waitUntil: 'domcontentloaded' });
|
|
await this.page.waitForLoadState('networkidle', { timeout: 20000 }).catch(() => {});
|
|
await this.reportListTable.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.page.waitForFunction(() => {
|
|
const t = document.querySelector('[data-test="report-list-table"]');
|
|
if (!t) return false;
|
|
const text = t.textContent || '';
|
|
return /Showing \d+ - \d+/.test(text) || !!document.querySelector('[data-test="o2-empty-state"]');
|
|
}, undefined, { timeout: 10000 });
|
|
await this.reportSearchInputField.fill(reportName);
|
|
return await btn.waitFor({ state: 'visible', timeout: 3000 }).then(() => true).catch(() => false);
|
|
}, {
|
|
intervals: [3000, 5000, 5000, 10000, 10000, 15000],
|
|
timeout: 120000,
|
|
}).toBe(true);
|
|
}
|
|
|
|
await btn.click();
|
|
// Wait for stopped/success toast
|
|
await this.toastSuccess.first().waitFor({ state: 'visible', timeout: 10000 });
|
|
await expect(this.toastSuccess.first()).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async updateReport(reportName) {
|
|
// Search for the report
|
|
await this.reportSearchInputField.fill(reportName);
|
|
await this.page.waitForTimeout(1000);
|
|
|
|
// Click edit button
|
|
await this.editReportBtn(reportName).click();
|
|
|
|
// Wait for edit form to fully load
|
|
await this.page.waitForTimeout(3000);
|
|
|
|
// Update description (inner -field of the OInput wrapper)
|
|
await this.descriptionInputField.click();
|
|
await this.descriptionInputField.fill('Report Updated');
|
|
|
|
// Click save button
|
|
await this.saveButton.click();
|
|
|
|
// Wait for save to process — gate on the reports list page returning to
|
|
// a state where the search input is visible (proves navigation back from
|
|
// the edit form completed and ReportList re-mounted). Replaces the legacy
|
|
// 2s waitForTimeout which raced the navigation under CI load.
|
|
await this.reportSearchInputField.waitFor({ state: 'visible', timeout: 30000 });
|
|
|
|
// Search for the report to filter and verify the update appears
|
|
await this.reportSearchInputField.fill(reportName);
|
|
|
|
// Verify the updated description appears in the report row description
|
|
// cell. Use OTable's column-scoped cell data-test (`o2-table-cell-<id>`)
|
|
// and assert table contents via toContainText — no hasText filters.
|
|
await this.reportListTable.waitFor({ state: 'visible', timeout: 15000 });
|
|
await expect(this.reportListTable).toContainText('Report Updated', { timeout: 15000 });
|
|
}
|
|
|
|
// async logedOut() {
|
|
// await this.page.locator('[data-test="header-my-account-profile-icon"]').click({ force: true });
|
|
// await this.page.waitForSelector('[data-test="menu-link-logout-item"]');
|
|
// await this.page.locator('[data-test="menu-link-logout-item"]').click();
|
|
// }
|
|
|
|
async loggedOut() {
|
|
// Click on the profile icon
|
|
await this.profileButton.click();
|
|
|
|
// Wait for the logout menu item to be attached to the DOM
|
|
await this.signOutButton.waitFor({ state: 'attached', timeout: 10000 });
|
|
// Wait for element to be visible instead of hard wait
|
|
await this.signOutButton.waitFor({ state: 'visible', timeout: 5000 });
|
|
// Now click the logout item
|
|
await this.signOutButton.click({ force: true });
|
|
await confirmDraftSignOut(this.page);
|
|
}
|
|
|
|
async notAvailableReport(reportName) {
|
|
await this.reportSearchInputField.fill(reportName);
|
|
await this.reportListTable.waitFor({ state: 'visible', timeout: 10000 });
|
|
await expect(this.page.locator('[data-test="o2-empty-state"]')).toBeVisible();
|
|
}
|
|
|
|
// ── Report Format section helpers ───────────────────────────────────────
|
|
|
|
async selectReportType(value) {
|
|
// Open the report type dropdown by clicking the trigger.
|
|
// NOTE: This OSelect lacks a direct `data-test` prop (the attribute is on
|
|
// the parent <div> instead), so the popover and option children do NOT have
|
|
// data-test-concatenated attributes. We locate options by data-test-value
|
|
// instead, which OSelect always renders unconditionally.
|
|
await this.reportTypeTrigger.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.reportTypeTrigger.click();
|
|
// Click the desired option — gating on its visibility confirms the popover opened
|
|
const opt = this.reportTypeOption(value);
|
|
await opt.waitFor({ state: 'visible', timeout: 10000 });
|
|
await opt.click();
|
|
// Wait for popover to close after selection (gate on option disappearing)
|
|
await opt.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
|
|
// Allow Vue reactivity to settle
|
|
await this.page.waitForTimeout(300);
|
|
}
|
|
|
|
async selectDashboardDefaults(dashboardName) {
|
|
await this.createReportFolderInput();
|
|
await this.createReportDashboardInput(dashboardName);
|
|
await this.createReportDashboardTabInput();
|
|
}
|
|
|
|
// ── Visibility assertion helpers (no raw selectors leak into specs) ─────
|
|
|
|
async expectReportFormatSectionVisible() {
|
|
await expect(this.reportFormatSection).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
async expectAttachmentTypeVisible() {
|
|
await expect(this.attachmentTypeSelect).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async expectAttachmentTypeHidden() {
|
|
await expect(this.attachmentTypeSelect).not.toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async expectCustomDimensionsVisible() {
|
|
await expect(this.customDimensionsSection).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async expectCustomDimensionsHidden() {
|
|
await expect(this.customDimensionsSection).not.toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async expectPngNoteVisible() {
|
|
await expect(this.pngNoteBanner).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async expectPngNoteHidden() {
|
|
await expect(this.pngNoteBanner).not.toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async expectReportTypeOptionVisible(value) {
|
|
// Open the popover, assert the option is visible, then close it.
|
|
// Same note as selectReportType: popover/option data-test attrs are missing
|
|
// because OSelect lacks a direct data-test prop. Use data-test-value instead.
|
|
await this.reportTypeTrigger.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.reportTypeTrigger.click();
|
|
const opt = this.reportTypeOption(value);
|
|
await expect(opt).toBeVisible({ timeout: 5000 });
|
|
// Close popover by pressing Escape
|
|
await this.page.keyboard.press('Escape');
|
|
await opt.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
|
|
}
|
|
|
|
async expectReportTypeOptionSelected(value) {
|
|
// Verify the selected value display shows the expected label
|
|
const labelMap = { csv: 'CSV (Data)', pdf: 'PDF (default)', png: 'PNG (Image)' };
|
|
const expectedLabel = labelMap[value] || value;
|
|
await expect(this.reportTypeTrigger).toContainText(expectedLabel, { timeout: 10000 });
|
|
}
|
|
|
|
}
|