diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 739200e258..50911cfad1 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -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" diff --git a/tests/ui-testing/pages/apiCleanup.js b/tests/ui-testing/pages/apiCleanup.js index ae264249a7..ccc0e1b0b5 100644 --- a/tests/ui-testing/pages/apiCleanup.js +++ b/tests/ui-testing/pages/apiCleanup.js @@ -349,6 +349,7 @@ class APICleanup { } } + /** * Fetch all pipelines * @returns {Promise} Array of pipeline objects @@ -832,6 +833,109 @@ class APICleanup { } } + /** + * Fetch all report folders (type=reports) + * @returns {Promise} 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} 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} 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 diff --git a/tests/ui-testing/pages/dashboardPages/dashCreation.js b/tests/ui-testing/pages/dashboardPages/dashCreation.js index 0b1815a8f7..72b8a7fd62 100644 --- a/tests/ui-testing/pages/dashboardPages/dashCreation.js +++ b/tests/ui-testing/pages/dashboardPages/dashCreation.js @@ -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} { 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 }; + } +} diff --git a/tests/ui-testing/pages/page-manager.js b/tests/ui-testing/pages/page-manager.js index 92df20156a..42e287dd1c 100644 --- a/tests/ui-testing/pages/page-manager.js +++ b/tests/ui-testing/pages/page-manager.js @@ -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); diff --git a/tests/ui-testing/pages/reportsPages/reportCreation.js b/tests/ui-testing/pages/reportsPages/reportCreation.js new file mode 100644 index 0000000000..66e309648f --- /dev/null +++ b/tests/ui-testing/pages/reportsPages/reportCreation.js @@ -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} { 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 }; + } +} diff --git a/tests/ui-testing/pages/reportsPages/reportFoldersPage.js b/tests/ui-testing/pages/reportsPages/reportFoldersPage.js new file mode 100644 index 0000000000..edb869dcb0 --- /dev/null +++ b/tests/ui-testing/pages/reportsPages/reportFoldersPage.js @@ -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; + } +} diff --git a/tests/ui-testing/playwright-tests/Reports/reportFolders.spec.js b/tests/ui-testing/playwright-tests/Reports/reportFolders.spec.js new file mode 100644 index 0000000000..a6c70dc238 --- /dev/null +++ b/tests/ui-testing/playwright-tests/Reports/reportFolders.spec.js @@ -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'); + }); +});