mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 00:08:55 +00:00
test: add E2E tests for report folders functionality (#11985)
## Summary
- **Add `reportFoldersPage.js`** page object with 30+ methods for folder
CRUD, report move, search, and cross-folder toggle
- **Add `reportFolders.spec.js`** with 12 tests covering:
- **P0 Smoke**: Sidebar renders with default folder, create new folder
- **P1 Functional**: Create report via API (creates temp dashboard from
scratch), move report between folders, cross-folder search, folder
sidebar search filter
- **P2 Edge Cases**: Move button disabled when source=destination, save
disabled for empty name, cancel closes dialog, default folder has no
edit/delete options, special characters allowed in folder names
- **Cleanup**: Deletes all test reports, folders, and leaves no
resources behind
- **Add `createDashboardViaApi`** to apiCleanup.js — creates a minimal
v8 dashboard from scratch, removing dependency on pre-existing
dashboards
- **Add `createReportViaApi`**, `fetchReportFolders`,
`deleteReportFolder`, `cleanupReportFolders` methods to apiCleanup.js
- Register ReportFoldersPage in PageManager
## Approach
- Everything created from scratch — unique report via API, unique
folders via UI, all cleaned up afterward
- No dependency on pre-existing dashboards or reports
- Zero `waitForTimeout` calls — all waits use `waitForLoadState`,
`waitFor({ state: 'visible/hidden' })`, or role-based waits
- Serial execution (tests build on each other: create folder → create
report → move report → cleanup)
## Test Results
```
12 passed (2.1m)
```
## CI Integration
- Added to `.github/workflows/playwright.yml` under `Reports` testfolder
alongside existing report tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
6b2742ded2
commit
ab86391649
@@ -297,8 +297,9 @@ jobs:
|
||||
- testfolder: "Reports"
|
||||
browser: "chrome"
|
||||
run_files: [
|
||||
"reportsScheduleNow.spec.js",
|
||||
"reportsScheduleLater.spec.js"
|
||||
"reportsScheduleNow.spec.js",
|
||||
"reportsScheduleLater.spec.js",
|
||||
"reportFolders.spec.js"
|
||||
]
|
||||
- testfolder: "Streams"
|
||||
browser: "chrome"
|
||||
|
||||
@@ -349,6 +349,7 @@ class APICleanup {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch all pipelines
|
||||
* @returns {Promise<Array>} Array of pipeline objects
|
||||
@@ -832,6 +833,109 @@ class APICleanup {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all report folders (type=reports)
|
||||
* @returns {Promise<Array>} Array of folder objects with folderId and name
|
||||
*/
|
||||
async fetchReportFolders() {
|
||||
try {
|
||||
const response = await this._fetch(`${this.baseUrl}/api/v2/${this.org}/folders/reports`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': this.authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
testLogger.error('Failed to fetch report folders', { status: response.status });
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.list || [];
|
||||
} catch (error) {
|
||||
testLogger.error('Failed to fetch report folders', { error: error.message });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a report folder by ID
|
||||
* @param {string} folderId - The folder ID to delete
|
||||
* @returns {Promise<Object>} Deletion result
|
||||
*/
|
||||
async deleteReportFolder(folderId) {
|
||||
try {
|
||||
const response = await this._fetch(`${this.baseUrl}/api/v2/${this.org}/folders/reports/${folderId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': this.authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
const jsonResult = JSON.parse(text);
|
||||
return { code: response.ok ? 200 : response.status, message: jsonResult.message || text };
|
||||
} catch {
|
||||
return { code: response.status, message: text };
|
||||
}
|
||||
} catch (error) {
|
||||
testLogger.error('Failed to delete report folder', { folderId, error: error.message });
|
||||
return { code: 500, message: error.message, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up report folders matching specified name prefixes
|
||||
* @param {Array<string>} namePrefixes - Array of folder name prefixes to match (e.g., ['test_folder_', 'test_special_'])
|
||||
*/
|
||||
async cleanupReportFolders(namePrefixes = []) {
|
||||
testLogger.info('Starting report folders cleanup', { prefixes: namePrefixes });
|
||||
|
||||
try {
|
||||
const folders = await this.fetchReportFolders();
|
||||
testLogger.info('Fetched report folders', { total: folders.length });
|
||||
|
||||
const matchingFolders = folders.filter(f =>
|
||||
namePrefixes.some(prefix => f.name.startsWith(prefix))
|
||||
);
|
||||
testLogger.info('Found report folders matching prefixes', { count: matchingFolders.length });
|
||||
|
||||
if (matchingFolders.length === 0) {
|
||||
testLogger.info('No report folders to clean up');
|
||||
return;
|
||||
}
|
||||
|
||||
let deletedCount = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
for (const folder of matchingFolders) {
|
||||
const result = await this.deleteReportFolder(folder.folderId);
|
||||
|
||||
if (result.code === 200) {
|
||||
deletedCount++;
|
||||
testLogger.info('Deleted report folder', { name: folder.name, folderId: folder.folderId });
|
||||
} else {
|
||||
failedCount++;
|
||||
testLogger.warn('Failed to delete report folder', { name: folder.name, folderId: folder.folderId, result });
|
||||
}
|
||||
}
|
||||
|
||||
testLogger.info('Report folders cleanup completed', { deletedCount, failedCount });
|
||||
|
||||
if (failedCount > 0) {
|
||||
throw new Error(`Failed to delete ${failedCount} report folder(s)`);
|
||||
}
|
||||
} catch (error) {
|
||||
testLogger.error('Report folders cleanup failed', { error: error.message });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all dashboards owned by the automation test user
|
||||
* Deletes dashboards from the 'default' folder where owner matches ZO_ROOT_USER_EMAIL
|
||||
|
||||
@@ -185,3 +185,64 @@ export async function deleteDashboard(page, dashboardName) {
|
||||
// Ensure the dashboard row is removed from the table
|
||||
// await expect(dashboardRow).not.toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a minimal v8 dashboard via API for use as a report source.
|
||||
* Uses the apiCleanup instance for authenticated requests.
|
||||
* @param {Object} api - apiCleanup instance
|
||||
* @param {string} dashboardName - Unique name for the dashboard
|
||||
* @param {string} folderId - Folder ID (defaults to "default")
|
||||
* @returns {Promise<Object>} { success, dashboard, error }
|
||||
*/
|
||||
export async function createDashboardViaApi(api, dashboardName, folderId = 'default') {
|
||||
testLogger.info('Creating dashboard via API', { dashboardName, folderId });
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
version: 8,
|
||||
title: dashboardName,
|
||||
description: 'Auto-created for E2E test',
|
||||
tabs: [{
|
||||
tabId: 'default',
|
||||
name: 'Default',
|
||||
panels: []
|
||||
}]
|
||||
};
|
||||
|
||||
const response = await api._fetch(
|
||||
`${api.baseUrl}/api/${api.org}/dashboards?folder=${encodeURIComponent(folderId)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': api.authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
testLogger.error('Failed to create dashboard via API', { dashboardName, status: response.status, body: errorBody });
|
||||
return { success: false, error: `HTTP ${response.status}: ${errorBody}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
// MetaDashboard nests the actual data under the version key (e.g., "v8")
|
||||
const inner = result[`v${result.version}`] || result;
|
||||
const tabs = (inner.tabs || [{ tabId: 'default', name: 'Default' }]).map(t => ({
|
||||
tab_id: t.tabId || t.tab_id || 'default'
|
||||
}));
|
||||
const dashboard = {
|
||||
dashboard_id: inner.dashboardId || inner.dashboard_id,
|
||||
title: inner.title,
|
||||
tabs
|
||||
};
|
||||
|
||||
testLogger.info('Dashboard created via API', { dashboardName, dashboardId: dashboard.dashboard_id });
|
||||
return { success: true, dashboard };
|
||||
} catch (error) {
|
||||
testLogger.error('Failed to create dashboard via API', { dashboardName, error: error.message });
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import { ServiceGraphPage } from "./tracesPages/serviceGraphPage.js";
|
||||
import { ServicesCatalogPage } from "./tracesPages/servicesCatalogPage.js";
|
||||
import { RumPage } from "./logsPages/rumPage.js";
|
||||
import { ReportsPage } from "./reportsPages/reportsPage.js";
|
||||
import { ReportFoldersPage } from "./reportsPages/reportFoldersPage.js";
|
||||
import { DataPage } from "./generalPages/dataPage.js";
|
||||
import { IamPage } from "./iamPages/iamPage.js";
|
||||
import { ManagementPage } from "./generalPages/managementPage.js";
|
||||
@@ -133,6 +134,7 @@ class PageManager {
|
||||
this.servicesCatalogPage = new ServicesCatalogPage(page);
|
||||
this.rumPage = new RumPage(page);
|
||||
this.reportsPage = new ReportsPage(page);
|
||||
this.reportFoldersPage = new ReportFoldersPage(page);
|
||||
this.dataPage = new DataPage(page);
|
||||
this.iamPage = new IamPage(page);
|
||||
this.managementPage = new ManagementPage(page);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
const testLogger = require('../../playwright-tests/utils/test-logger.js');
|
||||
|
||||
/**
|
||||
* Create a report via API for testing purposes.
|
||||
* Uses the apiCleanup instance for authenticated requests.
|
||||
* @param {Object} api - apiCleanup instance
|
||||
* @param {string} reportName - Unique name for the report
|
||||
* @param {string} folderId - Folder ID (defaults to "default")
|
||||
* @returns {Promise<Object>} { success, reportName, error }
|
||||
*/
|
||||
export async function createReportViaApi(api, reportName, folderId = 'default') {
|
||||
testLogger.info('Creating report via API', { reportName, folderId });
|
||||
|
||||
try {
|
||||
const dashboards = await api.fetchDashboardsInFolder(folderId);
|
||||
if (dashboards.length === 0) {
|
||||
testLogger.error('No dashboards found in folder, cannot create report', { folderId });
|
||||
return { success: false, error: 'No dashboards available' };
|
||||
}
|
||||
|
||||
const dashboard = dashboards[0];
|
||||
const inner = dashboard[`v${dashboard.version}`] || dashboard;
|
||||
if (!inner.dashboardId && !inner.dashboard_id) {
|
||||
testLogger.error('Dashboard missing required fields', { dashboard });
|
||||
return { success: false, error: 'Dashboard missing dashboard_id' };
|
||||
}
|
||||
|
||||
const dashboardId = inner.dashboardId || inner.dashboard_id;
|
||||
const dashboardTabs = inner.tabs || [{ tabId: 'default' }];
|
||||
testLogger.info('Using dashboard for report', { dashboardId, title: inner.title });
|
||||
|
||||
const payload = {
|
||||
name: reportName,
|
||||
description: '',
|
||||
dashboards: [{
|
||||
folder: folderId,
|
||||
dashboard: dashboardId,
|
||||
tabs: [dashboardTabs[0]?.tabId || dashboardTabs[0]?.tab_id || 'default'],
|
||||
variables: [],
|
||||
timerange: {
|
||||
type: 'relative',
|
||||
period: '30m',
|
||||
from: 0,
|
||||
to: 0
|
||||
},
|
||||
report_type: 'pdf',
|
||||
email_attachment_type: 'standard'
|
||||
}],
|
||||
destinations: [{ email: api.email }],
|
||||
enabled: true,
|
||||
imagePreview: false,
|
||||
title: `Test Report ${reportName}`,
|
||||
message: '',
|
||||
orgId: api.org,
|
||||
frequency: { interval: 1, type: 'once', cron: '' },
|
||||
timezone: 'UTC',
|
||||
timezoneOffset: 0,
|
||||
lastTriggeredAt: null,
|
||||
owner: api.email,
|
||||
lastEditedBy: api.email
|
||||
};
|
||||
|
||||
const response = await api._fetch(
|
||||
`${api.baseUrl}/api/v2/${api.org}/reports?folder=${encodeURIComponent(folderId)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': api.authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
testLogger.error('Failed to create report via API', { reportName, status: response.status, body: errorBody });
|
||||
return { success: false, error: `HTTP ${response.status}: ${errorBody}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
testLogger.info('Report created via API', { reportName, result });
|
||||
return { success: true, reportName };
|
||||
} catch (error) {
|
||||
testLogger.error('Failed to create report via API', { reportName, error: error.message });
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
export class ReportFoldersPage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
|
||||
// Navigation
|
||||
this.reportsMenu = '[data-test="menu-link-\\/reports-item"]';
|
||||
|
||||
// Folder sidebar
|
||||
this.addFolderBtn = '[data-test="dashboard-new-folder-btn"]';
|
||||
this.folderSearchInput = '[data-test="folder-search"]';
|
||||
this.folderTabsContainer = '[data-test="dashboards-folder-tabs"]';
|
||||
this.moreIcon = '[data-test="dashboard-more-icon"]';
|
||||
this.editFolderIcon = '[data-test="dashboard-edit-folder-icon"]';
|
||||
this.deleteFolderIcon = '[data-test="dashboard-delete-folder-icon"]';
|
||||
|
||||
// Add/Edit folder dialog
|
||||
this.folderDialog = '[data-test="dashboard-folder-dialog"]';
|
||||
this.folderNameInput = '[data-test="dashboard-folder-add-name"]';
|
||||
this.folderDescInput = '[data-test="dashboard-folder-add-description"]';
|
||||
this.folderSaveBtn = '[data-test="dashboard-folder-add-save"]';
|
||||
this.folderCancelBtn = '[data-test="dashboard-folder-add-cancel"]';
|
||||
|
||||
// Delete confirmation dialog
|
||||
this.confirmDeleteDialog = '[data-test="dashboard-confirm-delete-folder-dialog"]';
|
||||
this.confirmButton = '[data-test="confirm-button"]';
|
||||
|
||||
// Move dialog
|
||||
this.moveDialogHeader = '[data-test="reports-folder-move-header"]';
|
||||
this.moveFolderSelect = '[data-test="reports-index-dropdown-stream_type"]';
|
||||
this.moveSubmitBtn = '[data-test="reports-folder-move"]';
|
||||
this.moveCancelBtn = '[data-test="reports-folder-move-cancel"]';
|
||||
|
||||
// Report list
|
||||
this.reportSearchInput = '[data-test="report-list-search-input"]';
|
||||
this.allFoldersToggle = '[data-test="report-list-search-across-folders-toggle"]';
|
||||
this.moveReportBtn = (name) => `[data-test="report-list-${name}-move-report"]`;
|
||||
|
||||
// Page title
|
||||
this.pageTitle = '[data-test="report-list-title"]';
|
||||
}
|
||||
|
||||
getTabByName(folderName) {
|
||||
return this.page.locator(`${this.folderTabsContainer} [role="tab"]`).filter({ hasText: folderName });
|
||||
}
|
||||
|
||||
async navigateToReports() {
|
||||
await this.page.goto(
|
||||
`${process.env["ZO_BASE_URL"]}/web/reports?org_identifier=${process.env["ORGNAME"]}`,
|
||||
{ waitUntil: 'domcontentloaded' }
|
||||
);
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {
|
||||
console.warn('navigateToReports: networkidle timed out, continuing');
|
||||
});
|
||||
await expect(this.page.locator(this.pageTitle)).toContainText('Reports');
|
||||
await this.page.locator(this.folderTabsContainer).waitFor({ state: 'visible', timeout: 15000 }).catch(() => {
|
||||
console.warn('navigateToReports: folderTabsContainer not visible, continuing');
|
||||
});
|
||||
}
|
||||
|
||||
async clickAddFolder() {
|
||||
await this.page.locator(this.addFolderBtn).click();
|
||||
await expect(this.page.locator(this.folderNameInput)).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
|
||||
async fillFolderName(name) {
|
||||
await this.page.locator(this.folderNameInput).fill(name);
|
||||
}
|
||||
|
||||
async fillFolderDescription(desc) {
|
||||
await this.page.locator(this.folderDescInput).fill(desc);
|
||||
}
|
||||
|
||||
async clickSaveFolder() {
|
||||
await this.page.locator(this.folderSaveBtn).click();
|
||||
}
|
||||
|
||||
async clickCancelFolder() {
|
||||
await this.page.locator(this.folderCancelBtn).click();
|
||||
}
|
||||
|
||||
async createFolder(name, description = '') {
|
||||
await this.clickAddFolder();
|
||||
await this.fillFolderName(name);
|
||||
if (description) {
|
||||
await this.fillFolderDescription(description);
|
||||
}
|
||||
await this.clickSaveFolder();
|
||||
// Wait for the dialog to close after save
|
||||
await this.page.locator(this.folderDialog).waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
|
||||
}
|
||||
|
||||
async expectFolderTabVisible(folderName) {
|
||||
await expect(this.getTabByName(folderName)).toBeVisible({ timeout: 10000 });
|
||||
}
|
||||
|
||||
async expectFolderTabNotVisible(folderName) {
|
||||
await expect(this.getTabByName(folderName)).not.toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
|
||||
async clickFolderTab(folderName) {
|
||||
await this.getTabByName(folderName).click();
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
||||
}
|
||||
|
||||
async clickMoreIcon(folderName) {
|
||||
const tab = this.getTabByName(folderName);
|
||||
await tab.hover();
|
||||
const moreBtn = tab.locator(this.moreIcon);
|
||||
await moreBtn.waitFor({ state: 'visible', timeout: 3000 });
|
||||
await moreBtn.click();
|
||||
}
|
||||
|
||||
async clickEditFolder(folderName) {
|
||||
await this.clickMoreIcon(folderName);
|
||||
await this.page.locator(this.editFolderIcon).click({ force: true });
|
||||
await expect(this.page.locator(this.folderNameInput)).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
|
||||
async clickDeleteFolder(folderName) {
|
||||
await this.clickMoreIcon(folderName);
|
||||
await this.page.locator(this.deleteFolderIcon).click({ force: true });
|
||||
await expect(this.page.locator(this.confirmDeleteDialog)).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
|
||||
async confirmDelete() {
|
||||
await this.page.locator(this.confirmButton).click();
|
||||
await this.page.locator(this.confirmDeleteDialog).waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
|
||||
}
|
||||
|
||||
async searchFolders(query) {
|
||||
await this.page.locator(this.folderSearchInput).fill(query);
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
||||
}
|
||||
|
||||
async clearFolderSearch() {
|
||||
await this.page.locator(this.folderSearchInput).clear();
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
||||
}
|
||||
|
||||
async expectFolderSaveDisabled() {
|
||||
await expect(this.page.locator(this.folderSaveBtn)).toBeDisabled();
|
||||
}
|
||||
|
||||
async expectFolderSaveEnabled() {
|
||||
await expect(this.page.locator(this.folderSaveBtn)).toBeEnabled();
|
||||
}
|
||||
|
||||
async openMoveDialog(reportName) {
|
||||
await this.page.locator(this.moveReportBtn(reportName)).click({ force: true });
|
||||
await expect(this.page.locator(this.moveDialogHeader)).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
|
||||
async expectMoveButtonDisabled() {
|
||||
await expect(this.page.locator(this.moveSubmitBtn)).toBeDisabled();
|
||||
}
|
||||
|
||||
async expectMoveButtonEnabled() {
|
||||
await expect(this.page.locator(this.moveSubmitBtn)).toBeEnabled();
|
||||
}
|
||||
|
||||
async selectMoveDestination(folderName) {
|
||||
await this.page.locator(this.moveFolderSelect).click();
|
||||
// Quasar q-select popup — wait for the target option to appear
|
||||
await this.page.getByRole('option', { name: folderName }).waitFor({ state: 'visible', timeout: 5000 });
|
||||
await this.page.getByRole('option', { name: folderName }).click();
|
||||
// Wait for the q-select popup to close
|
||||
await this.page.getByRole('listbox').waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
|
||||
}
|
||||
|
||||
async clickMove() {
|
||||
await this.page.locator(this.moveSubmitBtn).click();
|
||||
await this.page.locator(this.moveDialogHeader).waitFor({ state: 'hidden', timeout: 10000 }).catch(() => {});
|
||||
}
|
||||
|
||||
async cancelMove() {
|
||||
await this.page.locator(this.moveCancelBtn).first().click();
|
||||
await this.page.locator(this.moveDialogHeader).waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
|
||||
}
|
||||
|
||||
async expectDefaultFolderExists() {
|
||||
await expect(this.getTabByName('default')).toBeVisible({ timeout: 10000 });
|
||||
}
|
||||
|
||||
async expectMoreIconNotVisible(folderName) {
|
||||
const tab = this.getTabByName(folderName);
|
||||
await tab.hover();
|
||||
await expect(tab.locator(this.moreIcon)).not.toBeVisible({ timeout: 3000 });
|
||||
}
|
||||
|
||||
async expectMoreIconVisible(folderName) {
|
||||
const tab = this.getTabByName(folderName);
|
||||
await tab.hover();
|
||||
await expect(tab.locator(this.moreIcon)).toBeVisible({ timeout: 3000 });
|
||||
}
|
||||
|
||||
async toggleAllFolders() {
|
||||
await this.page.locator(this.allFoldersToggle).click();
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
||||
}
|
||||
|
||||
async searchReports(query) {
|
||||
await this.page.locator(this.reportSearchInput).fill(query);
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
||||
}
|
||||
|
||||
async clearReportSearch() {
|
||||
await this.page.locator(this.reportSearchInput).clear();
|
||||
await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
||||
}
|
||||
|
||||
async expectReportVisibleInTable(reportName) {
|
||||
await expect(
|
||||
this.page.locator(`[data-test="report-list-${reportName}-pause-start-report"]`)
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
}
|
||||
|
||||
async getFolderCount() {
|
||||
return await this.page.locator(`${this.folderTabsContainer} [role="tab"]`).count();
|
||||
}
|
||||
|
||||
async getFirstReportWithMoveButton() {
|
||||
const btn = this.page.locator('[data-test*="-move-report"]').first();
|
||||
const testId = await btn.getAttribute('data-test');
|
||||
if (!testId) return null;
|
||||
return testId.replace('report-list-', '').replace('-move-report', '');
|
||||
}
|
||||
|
||||
async expectAllFoldersToggleVisible() {
|
||||
await expect(this.page.locator(this.allFoldersToggle)).toBeVisible();
|
||||
}
|
||||
|
||||
async isFolderTabPresent(folderName) {
|
||||
return await this.getTabByName(folderName).isVisible({ timeout: 2000 }).catch(() => false);
|
||||
}
|
||||
|
||||
async deleteFolderIfExists(folderName) {
|
||||
if (await this.isFolderTabPresent(folderName)) {
|
||||
await this.clickDeleteFolder(folderName);
|
||||
await this.confirmDelete();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
|
||||
const testLogger = require('../utils/test-logger.js');
|
||||
const PageManager = require('../../pages/page-manager.js');
|
||||
const { createDashboardViaApi } = require('../../pages/dashboardPages/dashCreation.js');
|
||||
const { createReportViaApi } = require('../../pages/reportsPages/reportCreation.js');
|
||||
|
||||
const timestamp = Date.now();
|
||||
const FOLDER_A = `test_folder_a_${timestamp}`;
|
||||
const FOLDER_B = `test_folder_b_${timestamp}`;
|
||||
const FOLDER_SPECIAL = `test_special_chars_${timestamp}`;
|
||||
const REPORT_A = `test_report_${timestamp}`;
|
||||
|
||||
test.describe("Report Folders", () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
let pm;
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
testLogger.testStart(testInfo.title, testInfo.file);
|
||||
await navigateToBase(page);
|
||||
pm = new PageManager(page);
|
||||
await pm.reportFoldersPage.navigateToReports();
|
||||
testLogger.info('Test setup completed');
|
||||
});
|
||||
|
||||
// ===== P0: SMOKE TESTS =====
|
||||
|
||||
test("P0: Folder sidebar renders with default folder", {
|
||||
tag: ['@reportFolders', '@smoke', '@P0']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Verifying folder sidebar renders with default folder');
|
||||
await pm.reportFoldersPage.expectDefaultFolderExists();
|
||||
testLogger.info('Default folder is visible in sidebar');
|
||||
});
|
||||
|
||||
test("P0: Create a new folder via Add Folder dialog", {
|
||||
tag: ['@reportFolders', '@smoke', '@P0']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Creating a new folder');
|
||||
await pm.reportFoldersPage.createFolder(FOLDER_A, 'Test folder A description');
|
||||
await pm.reportFoldersPage.expectFolderTabVisible(FOLDER_A);
|
||||
testLogger.info(`Folder "${FOLDER_A}" created and visible in sidebar`);
|
||||
});
|
||||
|
||||
// ===== P1: FUNCTIONAL TESTS =====
|
||||
|
||||
test("P1: Create a report from scratch via API and verify it is visible", {
|
||||
tag: ['@reportFolders', '@functional', '@P1']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Creating dashboard via API for report');
|
||||
|
||||
// Create a dashboard via API helper (from dashCreation.js)
|
||||
const dashResult = await createDashboardViaApi(
|
||||
pm.apiCleanup,
|
||||
`dash_${Date.now()}`
|
||||
);
|
||||
if (!dashResult.success) {
|
||||
throw new Error(`Failed to create dashboard: ${dashResult.error}`);
|
||||
}
|
||||
testLogger.info(`Dashboard "${dashResult.dashboard.dashboard_id}" created via API`);
|
||||
|
||||
// Navigate to reports
|
||||
await pm.reportFoldersPage.navigateToReports();
|
||||
|
||||
testLogger.info('Creating test report via API');
|
||||
const result = await createReportViaApi(pm.apiCleanup, REPORT_A);
|
||||
if (!result.success) {
|
||||
throw new Error(`Failed to create test report: ${result.error}`);
|
||||
}
|
||||
|
||||
testLogger.info(`Test report "${REPORT_A}" created via API`);
|
||||
|
||||
// Refresh the page so the reports table picks up the newly created report
|
||||
await pm.reportFoldersPage.navigateToReports();
|
||||
await pm.reportFoldersPage.expectReportVisibleInTable(REPORT_A);
|
||||
|
||||
testLogger.info('Test report visible in default folder');
|
||||
});
|
||||
|
||||
test("P1: Move a report to another folder", {
|
||||
tag: ['@reportFolders', '@functional', '@P1']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing move report between folders');
|
||||
|
||||
await pm.reportFoldersPage.createFolder(FOLDER_B);
|
||||
await pm.reportFoldersPage.expectFolderTabVisible(FOLDER_B);
|
||||
|
||||
// Navigate to default folder where REPORT_A lives
|
||||
await pm.reportFoldersPage.clickFolderTab('default');
|
||||
|
||||
testLogger.info(`Moving report "${REPORT_A}" to folder "${FOLDER_B}"`);
|
||||
|
||||
// Open move dialog and move to destination folder
|
||||
await pm.reportFoldersPage.openMoveDialog(REPORT_A);
|
||||
await pm.reportFoldersPage.selectMoveDestination(FOLDER_B);
|
||||
await pm.reportFoldersPage.expectMoveButtonEnabled();
|
||||
await pm.reportFoldersPage.clickMove();
|
||||
|
||||
// Verify report is now in destination folder
|
||||
await pm.reportFoldersPage.clickFolderTab(FOLDER_B);
|
||||
await pm.reportFoldersPage.expectReportVisibleInTable(REPORT_A);
|
||||
|
||||
// Move it back to default
|
||||
await pm.reportFoldersPage.openMoveDialog(REPORT_A);
|
||||
await pm.reportFoldersPage.selectMoveDestination('default');
|
||||
await pm.reportFoldersPage.clickMove();
|
||||
|
||||
testLogger.info('Report moved to another folder and back successfully');
|
||||
});
|
||||
|
||||
test("P1: Cross-folder search with All Folders toggle", {
|
||||
tag: ['@reportFolders', '@functional', '@P1']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing cross-folder search');
|
||||
|
||||
await pm.reportFoldersPage.toggleAllFolders();
|
||||
await pm.reportFoldersPage.searchReports(REPORT_A.substring(0, 8)); // search by prefix
|
||||
await pm.reportFoldersPage.expectAllFoldersToggleVisible();
|
||||
|
||||
testLogger.info('Cross-folder search toggled successfully');
|
||||
});
|
||||
|
||||
test("P1: Filter folders via sidebar search", {
|
||||
tag: ['@reportFolders', '@functional', '@P1']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing folder search filter');
|
||||
|
||||
// Search for a known folder
|
||||
await pm.reportFoldersPage.searchFolders('default');
|
||||
await pm.reportFoldersPage.expectFolderTabVisible('default');
|
||||
|
||||
// Search for non-existent folder - default should disappear
|
||||
await pm.reportFoldersPage.searchFolders('zzz_nonexistent_xyz');
|
||||
await pm.reportFoldersPage.expectFolderTabNotVisible('default');
|
||||
|
||||
// Clear search - folders should reappear
|
||||
await pm.reportFoldersPage.clearFolderSearch();
|
||||
await pm.reportFoldersPage.expectFolderTabVisible('default');
|
||||
|
||||
testLogger.info('Folder search filter works correctly');
|
||||
});
|
||||
|
||||
// ===== P2: EDGE CASE TESTS =====
|
||||
|
||||
test("P2: Move button disabled when source equals destination", {
|
||||
tag: ['@reportFolders', '@edge', '@P2']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing move button disabled for same folder');
|
||||
|
||||
await pm.reportFoldersPage.clickFolderTab('default');
|
||||
|
||||
await pm.reportFoldersPage.openMoveDialog(REPORT_A);
|
||||
await pm.reportFoldersPage.expectMoveButtonDisabled();
|
||||
await pm.reportFoldersPage.cancelMove();
|
||||
|
||||
testLogger.info('Move button correctly disabled when source = destination');
|
||||
});
|
||||
|
||||
test("P2: Save button disabled when folder name is empty", {
|
||||
tag: ['@reportFolders', '@edge', '@P2']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing save button disabled for empty name');
|
||||
|
||||
await pm.reportFoldersPage.clickAddFolder();
|
||||
await pm.reportFoldersPage.expectFolderSaveDisabled();
|
||||
|
||||
await pm.reportFoldersPage.fillFolderName('Temporary');
|
||||
await pm.reportFoldersPage.expectFolderSaveEnabled();
|
||||
|
||||
await pm.reportFoldersPage.fillFolderName('');
|
||||
await pm.reportFoldersPage.expectFolderSaveDisabled();
|
||||
|
||||
await pm.reportFoldersPage.clickCancelFolder();
|
||||
|
||||
testLogger.info('Save button correctly disabled when name is empty');
|
||||
});
|
||||
|
||||
test("P2: Cancel button closes Add Folder dialog", {
|
||||
tag: ['@reportFolders', '@edge', '@P2']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing cancel closes dialog');
|
||||
|
||||
await pm.reportFoldersPage.clickAddFolder();
|
||||
await pm.reportFoldersPage.fillFolderName('Should Not Save');
|
||||
await pm.reportFoldersPage.clickCancelFolder();
|
||||
|
||||
await pm.reportFoldersPage.expectFolderTabNotVisible('Should Not Save');
|
||||
|
||||
testLogger.info('Cancel correctly closes dialog without creating folder');
|
||||
});
|
||||
|
||||
test("P2: Default folder has no edit/delete options", {
|
||||
tag: ['@reportFolders', '@edge', '@P2']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing default folder has no more_vert menu');
|
||||
|
||||
await pm.reportFoldersPage.expectMoreIconNotVisible('default');
|
||||
|
||||
testLogger.info('Default folder correctly has no edit/delete options');
|
||||
});
|
||||
|
||||
test("P2: Special characters allowed in folder names", {
|
||||
tag: ['@reportFolders', '@edge', '@P2']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing special characters in folder names');
|
||||
|
||||
const specialName = `${FOLDER_SPECIAL}/?#`;
|
||||
await pm.reportFoldersPage.createFolder(specialName);
|
||||
await pm.reportFoldersPage.expectFolderTabVisible(specialName);
|
||||
|
||||
testLogger.info('Special characters are allowed in folder names (known behavior)');
|
||||
});
|
||||
|
||||
// ===== CLEANUP via API =====
|
||||
|
||||
test("Cleanup: Remove all test resources via API", {
|
||||
tag: ['@reportFolders', '@cleanup']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Cleaning up all test resources via API');
|
||||
|
||||
// Delete all test reports matching prefix (catches leftovers from failed runs too)
|
||||
const reports = await pm.apiCleanup.fetchReports();
|
||||
expect(Array.isArray(reports)).toBeTruthy();
|
||||
const testReports = reports.filter(r => r.name && r.name.startsWith('test_report_'));
|
||||
for (const report of testReports) {
|
||||
const result = await pm.apiCleanup.deleteReport(report.name);
|
||||
expect(result.code).toBe(200);
|
||||
testLogger.info(`Deleted test report: ${report.name}`);
|
||||
}
|
||||
|
||||
// Delete all test folders
|
||||
const foldersBefore = await pm.apiCleanup.fetchReportFolders();
|
||||
expect(Array.isArray(foldersBefore)).toBeTruthy();
|
||||
await pm.apiCleanup.cleanupReportFolders(['test_folder_', 'test_special_']);
|
||||
|
||||
// Delete dashboards created during tests
|
||||
await pm.apiCleanup.cleanupDashboards();
|
||||
|
||||
testLogger.info('Cleanup completed');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user