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:
Neha P
2026-05-20 07:53:49 +00:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 6b2742ded2
commit ab86391649
7 changed files with 744 additions and 2 deletions
+3 -2
View File
@@ -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"
+104
View File
@@ -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 };
}
}
+2
View File
@@ -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');
});
});