diff --git a/tests/ui-testing/pages/generalPages/ingestionConfigPage.js b/tests/ui-testing/pages/generalPages/ingestionConfigPage.js index 978b7dca5d..08b816b1a0 100644 --- a/tests/ui-testing/pages/generalPages/ingestionConfigPage.js +++ b/tests/ui-testing/pages/generalPages/ingestionConfigPage.js @@ -1,4 +1,5 @@ import { expect } from '@playwright/test'; +const testLogger = require('../../playwright-tests/utils/test-logger.js'); export class IngestionConfigPage { constructor(page) { @@ -77,6 +78,7 @@ export class IngestionConfigPage { this.rumContentText = this.legacyCodeBlockPre.first(); // Shared by every link in the test so 429 waits stay well under the test timeout this.rateLimitRetryBudgetMs = 60000; + this.networkErrorRetryBudgetMs = 60000; } // ==================== Navigation ==================== @@ -460,8 +462,7 @@ export class IngestionConfigPage { } async headWithRateLimitRetry(href, maxRetries = 2) { - // 5 second timeout to prevent hanging - let response = await this.page.request.head(href, { timeout: 5000 }); + let response = await this.headWithNetworkErrorRetry(href); for (let attempt = 0; attempt < maxRetries && response.status() === 429; attempt++) { const retryAfter = Number(response.headers()['retry-after']); const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 2000; @@ -478,4 +479,20 @@ export class IngestionConfigPage { } return response; } + + async headWithNetworkErrorRetry(href, maxRetries = 2) { + for (let attempt = 0; ; attempt++) { + try { + // 5 second timeout to prevent hanging + return await this.page.request.head(href, { timeout: 5000 }); + } catch (error) { + const retryCostMs = 1000 * (attempt + 1) + 5000; + // A spent budget would push the whole test past its timeout, so the real error is reported instead + if (attempt >= maxRetries || retryCostMs > this.networkErrorRetryBudgetMs) throw error; + testLogger.warn(`[doc-links] HEAD failed (${error.message}), retry ${attempt + 1}/${maxRetries}: ${href}`); + this.networkErrorRetryBudgetMs -= retryCostMs; + await this.page.waitForTimeout(1000 * (attempt + 1)); + } + } + } } diff --git a/tests/ui-testing/pages/logsPages/jobScheduler.js b/tests/ui-testing/pages/logsPages/jobScheduler.js index 66e0960c38..9f32e21e7a 100644 --- a/tests/ui-testing/pages/logsPages/jobScheduler.js +++ b/tests/ui-testing/pages/logsPages/jobScheduler.js @@ -2,6 +2,10 @@ import { expect } from '@playwright/test'; import testLogger from '../../playwright-tests/utils/test-logger.js'; const { getAuthHeaders, getOrgIdentifier } = require('../../playwright-tests/utils/cloud-auth.js'); +// The search-job worker starts one queued job per 10 s tick, and parallel tests queue several jobs. +const SEARCH_JOB_START_TIMEOUT_MS = 120000; +const JOB_STATUS = { PENDING: 0, RUNNING: 1, FINISHED: 2 }; + export class JobSchedulerPage { constructor(page) { this.page = page; @@ -96,6 +100,16 @@ export class JobSchedulerPage { return jobs.findIndex(job => job.trace_id === trace_id); } + async _getJobStatus(trace_id) { + const orgId = getOrgIdentifier(); + const url = `${process.env["ZO_BASE_URL"]}/api/${orgId}/search_jobs?type=logs&search_type=UI&use_cache=true`; + const response = await this.page.request.get(url, { headers: getAuthHeaders() }); + expect(response.status(), `GET search_jobs failed: ${await response.text()}`).toBe(200); + const job = (await response.json()).find(j => j.trace_id === trace_id); + expect(job, `Job with trace ID ${trace_id} not found in search_jobs`).toBeTruthy(); + return job.status; + } + async deleteJobSearch(trace_id) { const rowIndex = await this._getJobRowIndex(trace_id); @@ -165,33 +179,27 @@ async cancelJobSearch(trace_id) { } async exploreJob(trace_id) { - // The explore button is disabled while the job is pending (status=0) or failed (status=3). - // Retry: each attempt re-fetches the list and checks whether the button is enabled. - const maxRetries = 5; - let clicked = false; + // Explore is enabled only once the job has left pending (0) and was not cancelled (3). + let status; + await expect.poll(async () => { + status = await this._getJobStatus(trace_id); + return status; + }, { + message: `Search job ${trace_id} never left the pending queue`, + timeout: SEARCH_JOB_START_TIMEOUT_MS, + intervals: [2000, 5000], + }).not.toBe(JOB_STATUS.PENDING); + expect([JOB_STATUS.RUNNING, JOB_STATUS.FINISHED], `Search job ${trace_id} ended in status ${status}`).toContain(status); - for (let attempt = 0; attempt < maxRetries; attempt++) { - const rowIndex = await this._getJobRowIndex(trace_id, 15000); - if (rowIndex === -1) throw new Error(`Job with trace ID ${trace_id} not found in scheduler list`); + const rowIndex = await this._getJobRowIndex(trace_id, 15000); + if (rowIndex === -1) throw new Error(`Job with trace ID ${trace_id} not found in scheduler list`); - const row = this.page.locator(`[data-test="search-scheduler-table"] [data-test="o2-table-row-${rowIndex}"]`); - await row.waitFor({ state: 'visible', timeout: 10000 }); + const row = this.page.locator(`[data-test="search-scheduler-table"] [data-test="o2-table-row-${rowIndex}"]`); + await row.waitFor({ state: 'visible', timeout: 10000 }); - const exploreButton = row.locator('[data-test="search-scheduler-explore-btn"]'); - const isEnabled = await exploreButton.isEnabled(); - if (isEnabled) { - await exploreButton.click(); - clicked = true; - break; - } - - testLogger.warn(`Explore button for trace ID ${trace_id} is not enabled (attempt ${attempt + 1}/${maxRetries}).`); - if (attempt < maxRetries - 1) await this.page.waitForTimeout(5000); - } - - if (!clicked) { - throw new Error(`Explore button for trace ID ${trace_id} remained disabled after ${maxRetries} attempts.`); - } + const exploreButton = row.locator('[data-test="search-scheduler-explore-btn"]'); + await expect(exploreButton, `Explore button for trace ID ${trace_id} is disabled`).toBeEnabled({ timeout: 10000 }); + await exploreButton.click(); await expect( this.page.locator('[data-test="o-toast-message"]').filter({ hasText: 'Search Job have been applied successfully' }).first() diff --git a/tests/ui-testing/pages/logsPages/logsPage.js b/tests/ui-testing/pages/logsPages/logsPage.js index aef687db06..5aff5cec76 100644 --- a/tests/ui-testing/pages/logsPages/logsPage.js +++ b/tests/ui-testing/pages/logsPages/logsPage.js @@ -4819,11 +4819,7 @@ export class LogsPage { // When the show-query toggle is clicked, it switches to results view // Check if we're in results view by looking for the logs table const logsTable = this.page.locator('[data-test="logs-search-result-logs-table"]'); - const isResultsVisible = await logsTable.isVisible(); - - if (!isResultsVisible) { - throw new Error('Expected results view to be visible after toggling query editor off'); - } + await expect(logsTable, 'Expected results view to be visible after toggling query editor off').toBeVisible({ timeout: 15000 }); testLogger.info('Results view is visible - query editor toggled off successfully'); return true; diff --git a/tests/ui-testing/playwright-tests/Alerts/alerts-from-panel.spec.js b/tests/ui-testing/playwright-tests/Alerts/alerts-from-panel.spec.js index c57b67da8a..8b0498dbd6 100644 --- a/tests/ui-testing/playwright-tests/Alerts/alerts-from-panel.spec.js +++ b/tests/ui-testing/playwright-tests/Alerts/alerts-from-panel.spec.js @@ -118,7 +118,8 @@ test.describe("Create Alert from Dashboard Panel testcases", () => { // is a span, not an , so read the committed value via the helper. const alertNameValue = pm.alertsPage.getAlertNameValueLocator(); await expect(alertNameValue).toBeVisible({ timeout: 30000 }); - expect((await alertNameValue.textContent()) ?? "").toContain("Alert_from_"); + // The temporary auto-name shows first until the prefill resetForm applies the panel name + await expect(alertNameValue).toContainText("Alert_from_", { timeout: 30000 }); testLogger.info("Landed on the alert form pre-filled from the panel"); // Navigate back to the dashboard folder and clean up. diff --git a/tests/ui-testing/playwright-tests/Logs/logs-jk-navigation.spec.js b/tests/ui-testing/playwright-tests/Logs/logs-jk-navigation.spec.js index c2005dff61..834949a3c2 100644 --- a/tests/ui-testing/playwright-tests/Logs/logs-jk-navigation.spec.js +++ b/tests/ui-testing/playwright-tests/Logs/logs-jk-navigation.spec.js @@ -107,6 +107,8 @@ async function openResults(page) { ).toHaveCount(0, { timeout: 60000, }); + // Stale rows stay on screen during the re-run and the app ignores J/K until the hits settle. + expect(await logs.getResultHitsCount()).toBe(50); await page.evaluate( () => document.activeElement instanceof HTMLElement && diff --git a/tests/ui-testing/playwright-tests/Metrics/metrics-aggregations.spec.js b/tests/ui-testing/playwright-tests/Metrics/metrics-aggregations.spec.js index 37cad4e4af..49ad941c83 100644 --- a/tests/ui-testing/playwright-tests/Metrics/metrics-aggregations.spec.js +++ b/tests/ui-testing/playwright-tests/Metrics/metrics-aggregations.spec.js @@ -2,11 +2,16 @@ const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures const testLogger = require('../utils/test-logger.js'); const PageManager = require('../../pages/page-manager.js'); const { ensureMetricsIngested } = require('../utils/shared-metrics-setup.js'); +const metricsIngestion = require('../utils/metrics-ingestion.js'); const { verifyDataOnUIWithValues: verifyDataOnUI } = require('../utils/metrics-assertions.js'); test.describe("Metrics Aggregation and Grouping Tests", () => { test.beforeAll(async () => { await ensureMetricsIngested(); + // The random seed puts ~1 point per series, and rate() needs 2+ points in one series. + const seeded = await metricsIngestion.ingestFixedRequestCountSeries(); + expect(seeded.success, `fixed request_count seed failed: ${seeded.error}`).toBe(true); + expect(await metricsIngestion.waitForMetricQueryable('sum by (service) (rate(request_count[5m]))'), 'rate(request_count[5m]) never became queryable').toBe(true); }); /** Create fresh PageManager per test — avoids data races in parallel workers. */ diff --git a/tests/ui-testing/playwright-tests/Streams/ingestion-config.spec.js b/tests/ui-testing/playwright-tests/Streams/ingestion-config.spec.js index a672f7a7ae..332f9a8fc9 100644 --- a/tests/ui-testing/playwright-tests/Streams/ingestion-config.spec.js +++ b/tests/ui-testing/playwright-tests/Streams/ingestion-config.spec.js @@ -328,7 +328,7 @@ test.describe("Ingestion Configuration Tests", () => { testLogger.info(` Path: ${item.path}`); for (const link of item.brokenLinks) { testLogger.info(` ✗ ${link.url}`); - testLogger.info(` Status: ${link.status || link.error}`); + testLogger.info(` Status: ${link.error ? `${link.status} (${link.error})` : link.status}`); } } testLogger.info('\n' + '='.repeat(80)); diff --git a/tests/ui-testing/playwright-tests/utils/metrics-ingestion.js b/tests/ui-testing/playwright-tests/utils/metrics-ingestion.js index 5eab069c5d..376b694847 100644 --- a/tests/ui-testing/playwright-tests/utils/metrics-ingestion.js +++ b/tests/ui-testing/playwright-tests/utils/metrics-ingestion.js @@ -585,6 +585,49 @@ class MetricsIngestion { }; } + /** + * Send several request_count points into one fixed series so rate() over it is never empty. + */ + async ingestFixedRequestCountSeries(points = 5, spacingMs = 15000) { + const now = Date.now(); + const dataPoints = []; + for (let i = 0; i < points; i++) { + dataPoints.push({ + timeUnixNano: ((now - (points - 1 - i) * spacingMs) * 1000000).toString(), + asDouble: 100 + i * 10, + attributes: [ + { key: "node", value: { stringValue: "server-01" } }, + { key: "service", value: { stringValue: "api-gateway" } }, + { key: "region", value: { stringValue: "us-east-1" } } + ] + }); + } + const metricsData = { + resourceMetrics: [{ + resource: { + attributes: [ + { key: "service.name", value: { stringValue: "api-gateway" } }, + { key: "service.version", value: { stringValue: "1.0.0" } }, + { key: "environment", value: { stringValue: "production" } }, + { key: "host.name", value: { stringValue: "server-01" } }, + { key: "region", value: { stringValue: "us-east-1" } } + ] + }, + scopeMetrics: [{ + scope: { name: "opentelemetry-instrumentation", version: "1.0.0" }, + metrics: [{ name: "request_count", unit: "count", gauge: { dataPoints } }] + }] + }] + }; + const result = await this.sendMetrics(metricsData); + if (result.success) { + testLogger.info('Sent fixed request_count series', { points }); + } else { + testLogger.error('Failed to send fixed request_count series', { error: result.error }); + } + return result; + } + /** * Continuous metrics ingestion * Uses while loop with await to prevent overlapping executions diff --git a/web/src/plugins/logs/Index.permalink.spec.ts b/web/src/plugins/logs/Index.permalink.spec.ts index 4d22be8619..7f45cf47ad 100644 --- a/web/src/plugins/logs/Index.permalink.spec.ts +++ b/web/src/plugins/logs/Index.permalink.spec.ts @@ -216,7 +216,7 @@ describe("Logs Index — opening a line link (4c C5)", { timeout: 30000 }, () => } as any); gridRuns.length = 0; await land(linkQuery(), {}, { realExecutor: true }); - await vi.waitFor(() => expect(gridRuns.length).toBeGreaterThan(0)); + expect(gridRuns.length).toBeGreaterThan(0); expect(gridRuns[0][2]).toMatch(/^permalink-init:/); }); @@ -239,11 +239,7 @@ describe("Logs Index — opening a line link (4c C5)", { timeout: 30000 }, () => await vi.waitFor(() => expect(document.querySelector('[data-test="logs-permalink-detail-dialog"]')).not.toBeNull(), ); - await vi.waitFor(() => - expect( - document.querySelector('[data-test="log-detail-json-content"]')?.textContent, - ).toContain("payment declined"), - ); + await vi.waitFor(() => expect(sharedLineRecord.value).toEqual(line)); logs.mockClear(); wrapper.vm.onGuardRunAnyway(); expect(logs).toHaveBeenCalledTimes(1);