Files
openobserve/tests/ui-testing/playwright-tests/Logs/shareLink.spec.js
T
Shrinath Rao 82d14e8458 test(sentinel): POM compliance across Playwright suite (non-Dashboards) (#13727)
## What & why

Sentinel (Council of Agents) audit-driven **Page Object Model
compliance** across the Playwright suite. Many specs (a lot added
recently, some AI-generated) had raw `page.locator(...)` /
`page.getBy*(...)` selectors directly in spec files — including inside
`expect(...)` — which the Sentinel flags as CRITICAL. This PR relocates
those selectors into their page objects and calls page-object
methods/getters instead.

A full audit of **all 256 specs** was generated first (stored locally
under `tests/ui-testing/MD_Files/sentinel-audit-2026-08-10/`, which is
gitignored). Original totals: **1,609** raw selectors, **190** inside
assertions, plus console/testLogger items.

## Scope of THIS PR (non-Dashboards)

Every non-Dashboards spec is now **100% raw-selector clean** (verified:
0 occurrences). Shards covered:

- **GeneralTests, Logs (Core/Builder-Advanced/Features/Regression),
Traces (+Reg), Streams (+Reg), Metrics (+Reg), Pipelines, Reports
(+Reg), Anomaly, Login/auth, auth-warm, and the unmatched Alerts
specs.**
- `console.*` → `testLogger` where it was Node-context (kept `console.*`
**inside `page.evaluate()`** — that's browser context; `testLogger`
doesn't exist there).
- `testLogger` import/use added where missing.
- **`waitForTimeout` intentionally left as-is** (logged in the audit,
not rewritten — per direction).

## Deferred to a follow-up PR

The **heavy Dashboards shards** (Variables 693, Config-Settings 255,
Tables 127, Charts 81, Core 79, Visualize 65, Settings 52,
Panel-DateTime 55, Dashboard-Regression 52 raw selectors) are **not** in
this PR — they're the bulk of the work and will be a separate, focused
pass. (dashboard-streaming console cleanup is included here.)

## Verification

Fixes were applied and executed **shard by shard** against a local env,
gated green before commit where the env allowed:

| Group | Local result |
|-------|--------------|
| GeneralTests (edited specs) | 52 passed / 11 skipped / 0 failed |
| Logs (shareLink, logsQuickPick, indexquery) | 21 passed |
| Traces / Streams / Metrics | 49 passed / 2 skipped / 0 failed |
| Pipelines | 56 passed / 17 skipped / 0 failed |
| logs-bugs :548, dashboard-streaming | verified not regressed
(isolation) |

**Alerts create-wizard specs** (Alerts shard, Alerts-Regression,
unmatched Alerts) could not be gated locally — the alert-creation
wizard's stream popover times out on this env **on unmodified code too**
(reproduced on original, test `:363`), so those refactors are 1:1
selector→method mappings, syntax-clean (`node --check`), and rely on CI
to gate.

Every changed `.js` passes `node --check`. `waitForTimeout` unchanged;
no behavior changes intended.

## Stats
51 files changed. Selectors moved into page objects, not deleted.
2026-08-10 15:30:21 +00:00

451 lines
17 KiB
JavaScript

/**
* Share Link End-to-End Tests (Consolidated)
*
* Tests for the share link feature on the logs page including:
* - Share link button visibility and functionality
* - State preservation across redirects (stream, time range, SQL mode, histogram, org)
* - Short URL redirect functionality
* - Edge cases (no stream, multiple clicks, SQL queries)
*
* Optimized to avoid duplicate coverage while maintaining comprehensive testing.
*/
const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
const testLogger = require('../utils/test-logger.js');
const PageManager = require('../../pages/page-manager.js');
const logData = require('../../fixtures/log.json');
test.describe("Share Link Test Cases", () => {
// Tests are independent - each has its own beforeEach and fresh page context
// Running in parallel with 5 workers for faster execution
let pm;
const TEST_STREAM = 'e2e_automate';
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
await navigateToBase(page);
pm = new PageManager(page);
// Navigate to logs page
const logsUrl = `${logData.logsUrl}?org_identifier=${process.env["ORGNAME"]}`;
testLogger.navigation('Navigating to logs page', { url: logsUrl });
await page.goto(logsUrl);
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
testLogger.info('Test setup completed');
});
// =====================================================
// P0 - SMOKE TESTS (Critical Functionality)
// =====================================================
test("P0: Share link button visibility and success notification", {
tag: ['@shareLink', '@smoke', '@P0']
}, async ({ page }) => {
testLogger.info('Testing share link button visibility and success notification');
// Verify share link button is visible
await pm.logsPage.expectShareLinkButtonVisible();
// Select a stream and execute search
await pm.logsPage.selectStream(TEST_STREAM);
await page.waitForTimeout(2000);
await pm.logsPage.clickRefresh();
await page.waitForTimeout(3000);
// Click share link and verify SUCCESS notification appears
const success = await pm.logsPage.clickShareLinkAndExpectSuccess();
expect(success).toBe(true);
testLogger.info('Share link visibility and success notification test completed');
});
test("P0: Share link preserves stream and time range after redirect", {
tag: ['@shareLink', '@statePreservation', '@smoke', '@P0']
}, async ({ page }) => {
testLogger.info('Testing stream and time range preservation via share link redirect');
// Step 1: Select stream
await pm.logsPage.selectStream(TEST_STREAM);
await page.waitForTimeout(2000);
// Step 2: Set a specific time range (1 hour)
await pm.logsPage.setRelativeTimeRange('1-h');
// Step 3: Click refresh
await pm.logsPage.clickRefresh();
await page.waitForTimeout(3000);
// Step 4: Capture original state
const originalState = await pm.logsPage.captureCurrentState();
testLogger.info('Original state captured', { stream: originalState.stream, period: originalState.period });
// Step 5: Click share link and get URL
const sharedUrl = await pm.logsPage.clickShareLinkAndGetUrl();
testLogger.info('Shared URL captured', { url: sharedUrl });
// Step 6: Navigate to shared URL
await page.goto(sharedUrl);
await pm.logsPage.waitForRedirectComplete();
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
// Step 7: Capture and verify state
const redirectedState = await pm.logsPage.captureCurrentState();
testLogger.info('Redirected state', { stream: redirectedState.stream, from: redirectedState.from, to: redirectedState.to });
// Verify stream is preserved
expect(redirectedState.stream).toBe(originalState.stream);
// Verify time range: either period=1h OR from/to with ~1 hour duration
if (redirectedState.period === '1h') {
expect(redirectedState.period).toBe('1h');
} else if (redirectedState.from && redirectedState.to) {
const fromTime = parseInt(redirectedState.from);
const toTime = parseInt(redirectedState.to);
const durationHours = (toTime - fromTime) / (1000000 * 60 * 60);
testLogger.info('Time range duration', { durationHours });
expect(durationHours).toBeGreaterThan(0.9);
expect(durationHours).toBeLessThan(1.1);
} else {
throw new Error('No time range found in redirected state');
}
testLogger.info('Stream and time range preservation test completed');
});
// =====================================================
// P1 - FUNCTIONAL TESTS (Feature Validation)
// =====================================================
test("P1: Share link preserves SQL mode toggle state after redirect", {
tag: ['@shareLink', '@statePreservation', '@functional', '@P1']
}, async ({ page }) => {
testLogger.info('Testing SQL mode preservation via share link redirect');
// Step 1: Select stream
await pm.logsPage.selectStream(TEST_STREAM);
await page.waitForTimeout(2000);
// Step 2-4: Deterministically enable SQL mode, set the query, run it (without cancelling
// the auto-search), and confirm sql_mode committed to the URL — retries once then
// strict-asserts, so the generated short URL reliably captures sql_mode=true + the query
// (previously enableSqlModeIfNeeded didn't stick on CI, leaving sql_mode=false).
await pm.logsPage.setupSqlQueryForShare(`SELECT * FROM "${TEST_STREAM}"`);
testLogger.info('SQL mode enabled before sharing');
// Step 5: Click share link and get URL
const sharedUrl = await pm.logsPage.clickShareLinkAndGetUrl();
// Step 6: Navigate to shared URL
await page.goto(sharedUrl);
await pm.logsPage.waitForRedirectComplete();
// Step 7: SQL mode is auto-detected from the query editor content, which lazy-loads
// and re-hydrates AFTER the redirect. Wait for the restored SELECT query to land in
// the editor before asserting (self-heals with one reload if the lazy editor mounts
// empty after the short-URL hop) — a fixed 2s wait raced the pre-fill on CI.
await pm.logsPage.waitForRedirectedQueryEditorContent('SELECT');
const redirectedState = await pm.logsPage.captureCurrentState();
testLogger.info('Redirected state', redirectedState);
// Verify SQL mode is preserved after redirect
const sqlModeAfterRedirect = await pm.logsPage.isSqlModeEnabled();
testLogger.info('SQL mode after redirect', { enabled: sqlModeAfterRedirect });
expect(sqlModeAfterRedirect).toBe(true);
testLogger.info('SQL mode preservation test completed');
});
test("P1: Share link preserves histogram toggle state after redirect", {
tag: ['@shareLink', '@statePreservation', '@functional', '@P1']
}, async ({ page }) => {
testLogger.info('Testing histogram toggle preservation via share link redirect');
// Step 1: Select stream
await pm.logsPage.selectStream(TEST_STREAM);
await page.waitForTimeout(2000);
// Step 2: Toggle histogram (now inside utilities hamburger menu)
await pm.logsPage.toggleHistogram();
await page.waitForTimeout(1000);
// Step 3: Click refresh
await pm.logsPage.clickRefresh();
// Step 4: Capture original state. The histogram toggle writes show_histogram to the
// URL asynchronously, so wait for the param to be present before reading it —
// otherwise the "original" value itself is captured mid-write on a loaded runner.
await pm.logsPage.waitForUrlParam('show_histogram');
const originalState = await pm.logsPage.captureCurrentState();
testLogger.info('Original histogram state', { showHistogram: originalState.showHistogram });
// Step 5: Click share link and get URL
const sharedUrl = await pm.logsPage.clickShareLinkAndGetUrl();
// Step 6: Navigate to shared URL
await page.goto(sharedUrl);
await pm.logsPage.waitForRedirectComplete();
// Step 7: show_histogram is one of the last params the SPA appends when re-hydrating
// from the short URL. Wait for it to appear (was read as null / a transitional value
// on CI when captured mid-rewrite) before comparing.
await pm.logsPage.waitForUrlParam('show_histogram', originalState.showHistogram);
const redirectedState = await pm.logsPage.captureCurrentState();
testLogger.info('Redirected histogram state', { showHistogram: redirectedState.showHistogram });
expect(redirectedState.showHistogram).toBe(originalState.showHistogram);
testLogger.info('Histogram toggle preservation test completed');
});
test("P1: Share link preserves complete search state (stream + time + mode + histogram)", {
tag: ['@shareLink', '@statePreservation', '@functional', '@P1']
}, async ({ page }) => {
testLogger.info('Testing complete search state preservation via share link redirect');
// Step 1: Setup complete search state
await pm.logsPage.selectStream(TEST_STREAM);
await page.waitForTimeout(2000);
// Set time range to 30 minutes
await pm.logsPage.setRelativeTimeRange('30-m');
// Step 2: Click refresh
await pm.logsPage.clickRefresh();
// Step 3: Capture complete original state. Wait for the last-written toggle param
// (show_histogram) so the full search state is in the URL before we snapshot it.
await pm.logsPage.waitForUrlParam('show_histogram');
const originalState = await pm.logsPage.captureCurrentState();
testLogger.info('Complete original state captured', originalState);
// Step 4: Click share link and get URL
const sharedUrl = await pm.logsPage.clickShareLinkAndGetUrl();
testLogger.info('Shared URL', { url: sharedUrl });
// Step 5: Navigate to shared URL
await page.goto(sharedUrl);
await pm.logsPage.waitForRedirectComplete();
// Step 6: Wait for the SPA to finish re-hydrating the toggle params from the short
// URL (show_histogram is written last) before capturing, then compare.
await pm.logsPage.waitForUrlParam('show_histogram', originalState.showHistogram);
const redirectedState = await pm.logsPage.captureCurrentState();
testLogger.info('Redirected state captured', redirectedState);
// Step 7: Verify key states are preserved
const comparison = pm.logsPage.compareStates(originalState, redirectedState, [
'stream',
'streamType',
'showHistogram',
'quickMode'
]);
expect(comparison.isMatch).toBe(true);
if (!comparison.isMatch) {
testLogger.error('State mismatch detected', comparison.differences);
}
// Additionally verify the time range duration is approximately 30 minutes
if (redirectedState.from && redirectedState.to) {
const durationMinutes = (parseInt(redirectedState.to) - parseInt(redirectedState.from)) / (1000000 * 60);
testLogger.info('Time range duration', { durationMinutes });
expect(durationMinutes).toBeGreaterThan(25);
expect(durationMinutes).toBeLessThan(35);
}
testLogger.info('Complete state preservation test completed successfully');
});
test("P1: Share link button shows loading state while generating", {
tag: ['@shareLink', '@functional', '@P1']
}, async ({ page }) => {
testLogger.info('Testing share link loading state');
// Select stream and refresh
await pm.logsPage.selectStream(TEST_STREAM);
await pm.logsPage.clickRefresh();
await page.waitForTimeout(3000);
// Get the share button
const shareButton = pm.logsPage.getShareLinkButtonLocator();
// Click and wait for success notification
await shareButton.click();
await pm.logsPage.expectShareLinkSuccessNotification();
testLogger.info('Loading state test completed');
});
// =====================================================
// P2 - EDGE CASE TESTS
// =====================================================
test("P2: Share link without stream selected", {
tag: ['@shareLink', '@edge', '@P2']
}, async ({ page }) => {
testLogger.info('Testing share link without stream selected');
// Don't select any stream, just wait for page to load
await page.waitForTimeout(2000);
// Share link button should still be visible
await pm.logsPage.expectShareLinkButtonVisible();
// Click share link and verify notification appears (success or error)
const result = await pm.logsPage.clickShareLinkAndExpectNotification();
expect(result.appeared).toBe(true);
testLogger.info('Share link notification received', { text: result.text });
testLogger.info('No stream share link test completed');
});
test("P2: Multiple share link clicks work correctly", {
tag: ['@shareLink', '@edge', '@P2']
}, async ({ page }) => {
testLogger.info('Testing multiple share link clicks');
// Select stream and refresh
await pm.logsPage.selectStream(TEST_STREAM);
await pm.logsPage.clickRefresh();
await page.waitForTimeout(3000);
// Click share link multiple times
for (let i = 0; i < 3; i++) {
await pm.logsPage.clickShareLinkButton();
await page.waitForTimeout(2000);
testLogger.info(`Share link click ${i + 1} completed`);
}
// Last click should still show success
await pm.logsPage.expectShareLinkSuccessNotification();
testLogger.info('Multiple clicks share link test completed');
});
test("P2: Share link with SQL query preserves query content after redirect", {
tag: ['@shareLink', '@statePreservation', '@edge', '@P2']
}, async ({ page }) => {
testLogger.info('Testing SQL query preservation via share link redirect');
// Step 1: Select stream
await pm.logsPage.selectStream(TEST_STREAM);
await page.waitForTimeout(2000);
// Step 2: Enable SQL mode
await pm.logsPage.enableSqlModeIfNeeded();
await page.waitForTimeout(1000);
// Step 3: Enter a SQL query
const queryEditor = pm.logsPage.getQueryEditorLocator();
await queryEditor.click();
await page.keyboard.type(`SELECT * FROM "${TEST_STREAM}" LIMIT 50`);
await page.waitForTimeout(1000);
// Step 4: Click refresh
await pm.logsPage.clickRefresh();
await page.waitForTimeout(3000);
// Step 5: Capture original state
const originalUrl = await pm.logsPage.getCurrentUrl();
testLogger.info('Original URL with query', { url: originalUrl });
// Verify original URL contains the stream and SQL mode
expect(originalUrl).toContain(TEST_STREAM);
expect(originalUrl).toContain('sql_mode=true');
// Step 6: Click share link and get URL
const sharedUrl = await pm.logsPage.clickShareLinkAndGetUrl();
testLogger.info('Shared URL', { url: sharedUrl });
// Step 7: Navigate to shared URL
await page.goto(sharedUrl);
await pm.logsPage.waitForRedirectComplete();
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
// Step 8: Verify we're back on logs page with state
const redirectedUrl = await pm.logsPage.getCurrentUrl();
expect(redirectedUrl).toContain('logs');
expect(redirectedUrl).toContain('stream');
testLogger.info('SQL query preservation test completed');
});
test("P2: Shared URL redirect, org context, and multiple access consistency", {
tag: ['@shareLink', '@statePreservation', '@edge', '@P2']
}, async ({ page }) => {
testLogger.info('Testing shared URL redirect, org context, and multiple access consistency');
// Step 1: Setup and share
await pm.logsPage.selectStream(TEST_STREAM);
await page.waitForTimeout(2000);
await pm.logsPage.setRelativeTimeRange('1-h');
await pm.logsPage.clickRefresh();
await page.waitForTimeout(3000);
// Capture original org
const originalState = await pm.logsPage.captureCurrentState();
const originalOrg = originalState.orgIdentifier;
const sharedUrl = await pm.logsPage.clickShareLinkAndGetUrl();
testLogger.info('Captured shared URL', { url: sharedUrl });
// Step 2: Verify it's a short URL
const isShortUrl = sharedUrl.includes('/short/');
testLogger.info('URL type', { isShortUrl });
// Step 3: Access the URL first time
await page.goto(sharedUrl);
await pm.logsPage.waitForRedirectComplete();
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
const firstAccessState = await pm.logsPage.captureCurrentState();
testLogger.info('First access state', firstAccessState);
// Verify redirect completed (not on short URL anymore)
const firstUrl = await pm.logsPage.getCurrentUrl();
expect(firstUrl).toContain('logs');
expect(firstUrl).not.toContain('/short/');
// Verify org is preserved
expect(firstAccessState.orgIdentifier).toBe(originalOrg);
// Step 4: Navigate away
await page.goto(`${process.env["ZO_BASE_URL"]}/web/?org_identifier=${process.env["ORGNAME"]}`);
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
// Step 5: Access the URL second time
await page.goto(sharedUrl);
await pm.logsPage.waitForRedirectComplete();
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
const secondAccessState = await pm.logsPage.captureCurrentState();
testLogger.info('Second access state', secondAccessState);
// Step 6: Verify states match
const comparison = pm.logsPage.compareStates(firstAccessState, secondAccessState, [
'stream',
'orgIdentifier'
]);
expect(comparison.isMatch).toBe(true);
testLogger.info('Shared URL redirect, org context, and multiple access test completed');
});
// Bug #9788 test should be moved to RegressionSet/logs-regression.spec.js or similar
});