From df9a274df36df4e6880b02bccb46ed70327db0bf Mon Sep 17 00:00:00 2001 From: Shrinath Rao Date: Sat, 24 Oct 2026 05:49:12 +0000 Subject: [PATCH] test(autoe2e): generated E2E tests for #15614 (first-event-status) (#15661) Co-authored-by: Shrinath-O2 <214651540+Shrinath-O2@users.noreply.github.com> --- tests/ui-testing/ci-matrix/ci_matrix.json | 3 +- .../pages/generalPages/firstEventPage.js | 8 ++ .../Streams/first-event-status.spec.js | 108 ++++++++++++++++++ .../utils/first-event-helpers.js | 56 +++++++++ 4 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 tests/ui-testing/playwright-tests/Streams/first-event-status.spec.js create mode 100644 tests/ui-testing/playwright-tests/utils/first-event-helpers.js diff --git a/tests/ui-testing/ci-matrix/ci_matrix.json b/tests/ui-testing/ci-matrix/ci_matrix.json index ccc7543a01..df8076a668 100644 --- a/tests/ui-testing/ci-matrix/ci_matrix.json +++ b/tests/ui-testing/ci-matrix/ci_matrix.json @@ -534,7 +534,8 @@ "connectDataSourcePopup.spec.js", "splunk-hec-ingestion-window.spec.js", "enrichment.spec.js", - "schema.spec.js" + "schema.spec.js", + "first-event-status.spec.js" ], "disabled": [] }, diff --git a/tests/ui-testing/pages/generalPages/firstEventPage.js b/tests/ui-testing/pages/generalPages/firstEventPage.js index 0c1439bd29..f5ed4dbcfd 100644 --- a/tests/ui-testing/pages/generalPages/firstEventPage.js +++ b/tests/ui-testing/pages/generalPages/firstEventPage.js @@ -10,6 +10,9 @@ export class FirstEventPage { this.firstRecord = page.locator('[data-test="first-event-status-first-record"]'); this.openBtn = page.locator('[data-test="first-event-status-open-btn"]'); this.troubleshootBtn = page.locator('[data-test="first-event-status-troubleshoot-btn"]'); + this.checkNowBtn = page.locator('[data-test="first-event-status-check-now-btn"]'); + this.connectSourceBtn = page.locator('[data-test="first-event-status-connect-source-btn"]'); + this.setupAnotherBtn = page.locator('[data-test="first-event-status-setup-another-btn"]'); this.diagnosis = page.locator('[data-test="first-event-diagnosis"]'); this.diagnosisFix = page.locator('[data-test="first-event-diagnosis-fix"]'); this.diagnosisCopyBtn = page.locator('[data-test="first-event-diagnosis-copy-btn"]'); @@ -17,6 +20,7 @@ export class FirstEventPage { this.diagnosisEndpoint = page.locator('[data-test="first-event-diagnosis-endpoint"]'); this.stillStuck = page.locator('[data-test="first-event-diagnosis-still-stuck"]'); this.docsLink = page.locator('[data-test="first-event-diagnosis-docs-link"]'); + this.askAiBtn = page.locator('[data-test="first-event-diagnosis-ask-ai-btn"]'); this.contactSupportLink = page.locator('[data-test="first-event-diagnosis-contact-support-link"]'); this.notice = page.locator('[data-test="first-data-notice"]'); this.noticeText = page.locator('[data-test="first-data-notice-text"]'); @@ -48,6 +52,10 @@ export class FirstEventPage { await this.troubleshootBtn.click(); } + async clickCheckNow() { + await this.checkNowBtn.click(); + } + async clickOpen() { await this.openBtn.click(); } diff --git a/tests/ui-testing/playwright-tests/Streams/first-event-status.spec.js b/tests/ui-testing/playwright-tests/Streams/first-event-status.spec.js new file mode 100644 index 0000000000..4211377983 --- /dev/null +++ b/tests/ui-testing/playwright-tests/Streams/first-event-status.spec.js @@ -0,0 +1,108 @@ +const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js'); +const testLogger = require('../utils/test-logger.js'); +const { FirstEventPage } = require('../../pages/generalPages/firstEventPage.js'); +const { + ingest, + withFreshOrg, + targetProbe, + waitForStreamWithRecords, +} = require('../utils/first-event-helpers.js'); + +test.describe('Ingestion First-Event Status testcases', () => { + test.describe.configure({ mode: 'parallel' }); + + test.beforeEach(async ({ page }, testInfo) => { + testLogger.testStart(testInfo.title, testInfo.file); + await navigateToBase(page); + testLogger.info('Test setup completed'); + }); + + test('turns green on the curl test guide with test-kind markers and "Connect your real source"', { + tag: ['@first-event-status', '@ingestion', '@all', '@P0'], + }, async ({ page }) => { + testLogger.info('Opening the curl test guide on a fresh org'); + await withFreshOrg(page, 'fe_curl', async (orgId) => { + const fe = new FirstEventPage(page); + const baseline = targetProbe(page, 'logs', 'default'); + await fe.openGuide('/ingestion/custom/logs/curl', orgId); + await fe.expectState('waiting'); + await expect(fe.bar).toHaveAttribute('data-kind', 'test'); + await baseline; + + await ingest(page, orgId, 'default', [{ level: 'info', log: 'first event e2e curl' }]); + await fe.expectState('received', 15000); + await expect(fe.bar).toHaveAttribute('data-kind', 'test'); + await expect(fe.summary).toContainText('default'); + await expect(fe.firstRecord).toContainText('first event e2e curl'); + await expect(fe.openBtn).toHaveText(/Open in Logs/); + await expect(fe.connectSourceBtn).toBeVisible(); + await expect(fe.connectSourceBtn).toContainText('Connect your real source'); + await expect(fe.setupAnotherBtn).toHaveCount(0); + testLogger.info('Curl test guide turned green with test-kind markers'); + }); + }); + + test.fixme('idle state unreachable in e2e: useFirstEventWatch.ts:241 hasRecords reads stream-list doc_num (no SQL COUNT fallback unlike confirmNewStream), which stays 0 until the 10-min memtable TTL flush', { + tag: ['@first-event-status', '@ingestion', '@all', '@P1'], + }, async ({ page }) => { + testLogger.info('Ingesting before mount so the baseline probe sees an existing stream'); + await withFreshOrg(page, 'fe_idle', async (orgId) => { + await ingest(page, orgId, 'default', [{ level: 'info', log: 'pre-existing record' }]); + expect(await waitForStreamWithRecords(page, orgId, 'default')).toBe(true); + + const fe = new FirstEventPage(page); + await fe.openGuide('/ingestion/custom/logs/curl', orgId); + await fe.expectState('idle'); + await expect(fe.checkNowBtn).toBeVisible(); + await expect(fe.troubleshootBtn).toHaveCount(0); + + await fe.clickCheckNow(); + await fe.expectState('waiting'); + await expect(fe.checkNowBtn).toHaveCount(0); + + await ingest(page, orgId, 'default', [{ level: 'info', log: 'second record after check now' }]); + await fe.expectState('received', 15000); + await expect(fe.summary).toContainText('default'); + testLogger.info('Idle held on pre-existing data, then Check now engaged and received'); + }); + }); + + test('Troubleshoot on a fresh tracked org shows no-requests immediately with org id and endpoint', { + tag: ['@first-event-status', '@ingestion', '@all', '@P1'], + }, async ({ page }) => { + testLogger.info('Opening the fluentbit guide on a fresh, data-free org'); + await withFreshOrg(page, 'fe_noreq', async (orgId) => { + const fe = new FirstEventPage(page); + await fe.openGuide('/ingestion/custom/logs/fluentbit', orgId); + await fe.expectState('waiting'); + await fe.clickTroubleshoot(); + await fe.expectState('no-requests'); + await fe.expectDiagnosis({ trigger: 'troubleshoot' }); + await expect(fe.summary).toContainText('Nothing has arrived'); + await expect(fe.diagnosisOrgId).toHaveText(orgId); + await expect(fe.diagnosisEndpoint).not.toHaveText(''); + await expect(fe.diagnosisFix).toHaveText('Check that your command uses this org id and endpoint.'); + await expect(fe.diagnosisCopyBtn).toBeVisible(); + await expect(fe.stillStuck).toContainText('Still stuck?'); + await expect(fe.docsLink).toBeVisible(); + await expect(fe.docsLink).toHaveAttribute('target', '_blank'); + await expect(fe.troubleshootBtn).toHaveCount(0); + testLogger.info('Troubleshoot showed no-requests immediately'); + }); + }); + + test.fixme('Ask AI button — not wired in OSS: FirstEventStatus.vue:360 requires enterprise + ai_enabled', { + tag: ['@first-event-status', '@all', '@P2'], + }, async ({ page }) => { + testLogger.info('Ask AI CTA in the Still stuck row (enterprise + ai_enabled only)'); + await withFreshOrg(page, 'fe_askai', async (orgId) => { + const fe = new FirstEventPage(page); + await fe.openGuide('/ingestion/custom/logs/fluentbit', orgId); + await fe.expectState('waiting'); + await fe.clickTroubleshoot(); + await fe.expectState('no-requests'); + await expect(fe.askAiBtn).toBeVisible(); + testLogger.info('Ask AI button visible after diagnosis'); + }); + }); +}); diff --git a/tests/ui-testing/playwright-tests/utils/first-event-helpers.js b/tests/ui-testing/playwright-tests/utils/first-event-helpers.js new file mode 100644 index 0000000000..75f4b80959 --- /dev/null +++ b/tests/ui-testing/playwright-tests/utils/first-event-helpers.js @@ -0,0 +1,56 @@ +const { test, expect } = require('@playwright/test'); +const { getAuthHeaders, authedRequest, isCloudEnvironment } = require('./cloud-auth.js'); + +const base = () => process.env.ZO_BASE_URL.replace(/\/$/, ''); +const ingestionBase = () => (process.env.INGESTION_URL || process.env.ZO_BASE_URL).replace(/\/$/, ''); + +async function ingest(page, orgId, stream, records) { + const res = await page.request.post(`${ingestionBase()}/api/${orgId}/${stream}/_json`, { + headers: getAuthHeaders(), + data: records, + }); + expect(res.ok()).toBeTruthy(); +} + +async function withFreshOrg(page, tag, fn) { + const created = await authedRequest(page, 'post', `${base()}/api/organizations`, { + data: { name: `e2e_${tag}_${Date.now()}` }, + }); + // Cloud creates orgs only through signup, so a non-200 there means this env cannot stage the scenario. + test.skip(isCloudEnvironment() && !created.ok(), 'Cloud creates orgs only through signup'); + expect(created.ok()).toBeTruthy(); + const body = await created.json(); + const orgId = body.identifier || (body.data && body.data.identifier); + try { + await fn(orgId); + } finally { + await authedRequest(page, 'delete', `${base()}/api/organizations/${orgId}`).catch(() => {}); + } +} + +// The bar's baseline probe for its own stream, so an ingest never lands before the watch has looked. +const targetProbe = (page, type, stream) => + page.waitForResponse( + (r) => r.url().includes(`/streams?type=${type}&offset=0&limit=20&keyword=${stream}`), + { timeout: 20000 }, + ); + +// The watcher's idle gate reads doc_num from the stream list (not search), so wait for it to be non-zero. +async function waitForStreamWithRecords(page, orgId, streamName, type = 'logs', maxWaitMs = 30000) { + const start = Date.now(); + while (Date.now() - start < maxWaitMs) { + const res = await page.request.get(`${ingestionBase()}/api/${orgId}/streams?type=${type}`, { + headers: getAuthHeaders(), + }); + if (res.ok()) { + const data = await res.json().catch(() => null); + const list = Array.isArray(data?.list) ? data.list : []; + const row = list.find((s) => s?.name === streamName); + if (row && Number(row.stats?.doc_num ?? 0) > 0) return true; + } + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + return false; +} + +module.exports = { ingest, withFreshOrg, targetProbe, waitForStreamWithRecords };