Files
Shrinath Rao 6650950a50 test(e2e/alpha1): fix share-link redirect origin and cloud metrics ingest auth (#13776)
## Why

The scheduled **Alpha1 (Cloud)** e2e run failed on three jobs
([o2-enterprise run
31547517666](https://github.com/openobserve/o2-enterprise/actions/runs/31547517666)):
**Logs-Features** (7), **Functions** (11), **RegressionSet** (4).
Root-caused each against a freshly-deployed alpha; two are genuine test
heals, one was pure environment skew.

## Root causes & fixes

### 1. Logs-Features — shareLink + monaco-query-prefill (7 tests) ✅
fixed
On cloud the app builds the share link from its **public** base URL
(`*.external.zinclabs.dev`), while the tests authenticate against the
ingress host (`*.internal.zinclabs.dev`). `page.goto(sharedUrl)`
navigated **cross-origin**, dropped the auth cookies, and bounced to Dex
— so every post-redirect state assertion read back `null`.

**Fix:** re-point the short URL's origin to `ZO_BASE_URL` in
`clickShareLinkAndGetUrl()` (right next to the existing HTTP→HTTPS
"maintain auth context" step). `/short/<id>` resolves identically on the
backend regardless of ingress host, so only the origin changes and the
session stays valid. Same-origin envs (localhost / matching deployed
host) are untouched.

### 2. RegressionSet — alerts Bug #9967 PromQL (1 test) ✅ fixed
`ingestMetricsData()` authenticated with `ZO_ROOT_USER_EMAIL:PASSWORD`,
but **cloud ingestion needs `email:passcode`**. The ingest silently
401'd → the `e2e_test_cpu_usage` metrics stream was never created → the
PromQL alert flow timed out selecting it.

**Fix:** build auth via `getAuthHeaders()` (passcode on cloud, root
creds on self-hosted), refresh the passcode from the live browser
session first, and poll until the stream is indexed so the wizard's
stream-select doesn't race WAL lag.

### 3. Functions — radio → segmented toggle (11 tests) — no change
needed
The page object was already migrated to the segmented toggle in #13766;
these failures were the **alpha frontend lagging the merged test code**.
Green after the redeploy with zero code change.

## Verification (against alpha, `auto_org_1`)
- `shareLink.spec.js` + `monaco-query-prefill.spec.js` → **11/11
passed**
- `alerts-regression.spec.js` (full) → **5/5 passed** (#9967, #10110,
#10899, #10872, #10472)
- `Functions/js-transform-type.spec.js` → **7/7**,
`row-expansion.spec.js` → green

## Scope
OSS-only (test files). The ENT `playwright_alpha1.yml` checks out
opensource at `main`, so merging makes the next scheduled run pick this
up. It can also be pre-verified by dispatching that workflow with
`o2_opensource_repo=test/alpha1-e2e-heals-0812`.
2026-08-12 06:35:48 +00:00

868 lines
38 KiB
JavaScript

/**
* Search Patterns Tests (Enterprise Feature)
*
* Tests for the Search Patterns feature that allows users to automatically
* extract and analyze log patterns from their log data.
*
* Note: Pattern detection requires:
* - Stream with FTS (Full-Text Search) fields configured
* - Stream with enable_log_patterns_extraction enabled
* - Sufficient log volume for pattern clustering
*
* These tests configure the stream properly and ingest pattern-rich data.
*/
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");
const patternsTestData = require("../../../test-data/patterns_test_data.json");
const { ingestCustomData, enableLogPatternsExtraction, waitForStreamData } = require('../utils/data-ingestion.js');
const { getOrgIdentifier } = require('../utils/cloud-auth.js');
// Dedicated stream for pattern tests with proper configuration.
// Unique per-run suffix so concurrent alpha runs never share the name: on the shared
// cloud org a fixed name meant one run's pre-test cleanup was deleting the stream while
// another run's beforeAll ingested into it, returning 400 "stream is being deleted" and
// leaving the stream absent for selectStream. cleanup.spec.js reclaims these via the
// `/^e2e_http_patterns/` prefix pattern.
const PATTERNS_STREAM = `e2e_http_patterns_${Date.now().toString(36)}`;
// Track if data has been ingested (for serial test execution)
let dataIngested = false;
test.describe("Search Patterns Feature", { tag: ['@enterprise', '@searchPatterns'] }, () => {
test.describe.configure({ mode: 'serial' });
let pm;
/**
* Helper function to setup patterns view and wait for patterns to load
* Reduces code duplication across all pattern tests
* @param {Page} page - Playwright page object
* @param {PageManager} pm - Page manager instance
* @param {string} timeRange - Time range selector ('1hour' | '30seconds')
* @returns {Promise<string>} - Result from waitForPatternsToLoad ('patterns' | 'statistics' | 'empty' | 'timeout')
*/
async function setupPatternsView(page, pm, timeRange = '1hour') {
await page.goto(`${logData.logsUrl}?org_identifier=${getOrgIdentifier()}`);
await pm.logsPage.selectStream(PATTERNS_STREAM);
// Switch off quick mode (required for patterns)
await pm.logsPage.clickQuickModeToggle();
// Set time range
if (timeRange === '1hour') {
await pm.logsPage.clickDateTimeButton();
await pm.logsPage.clickRelative1HourOrFallback();
} else if (timeRange === '30seconds') {
await pm.logsPage.setTimeToPast30Seconds();
}
await pm.logsPage.clickRefreshButton();
await pm.logsPage.waitForLogsTableToLoad();
await pm.logsPage.clickPatternsToggle();
let result = await pm.logsPage.waitForPatternsToLoad(60000);
// Pattern extraction is async on the backend; under alpha indexing load it can still
// be empty/timeout after the first 60s wait (the CI flake here). Re-run the query to
// re-trigger extraction and wait once more before giving up — resilience for the
// data-dependency, not an assertion mask (callers still assert the returned state).
if (result !== 'patterns' && result !== 'statistics') {
testLogger.warn(`Patterns not ready (${result}) — re-running query and retrying once`);
await pm.logsPage.clickRefreshButton();
result = await pm.logsPage.waitForPatternsToLoad(60000);
}
return result;
}
test.beforeAll(async ({ browser }) => {
// Ingest pattern-rich data once before all tests
if (dataIngested) {
testLogger.info('Pattern data already ingested, skipping...');
return;
}
testLogger.info('Setting up pattern test data (beforeAll)...');
const context = await browser.newContext();
const page = await context.newPage();
try {
// Navigate to establish session
await page.goto(`${logData.logsUrl}?org_identifier=${getOrgIdentifier()}`);
// Ingest pattern-rich data with fresh timestamps
const baseTimestamp = Date.now() * 1000; // microseconds
const freshData = patternsTestData.map((log, index) => ({
...log,
_timestamp: baseTimestamp + (index * 1000)
}));
testLogger.info(`Ingesting ${freshData.length} logs to stream: ${PATTERNS_STREAM}`);
// Retry on the transient "stream is being deleted" (400) that a concurrent run's
// cleanup can trigger — the delete completes, then the POST recreates the stream.
let ingestResponse;
for (let attempt = 1; attempt <= 3; attempt++) {
ingestResponse = await ingestCustomData(page, PATTERNS_STREAM, freshData);
if (ingestResponse.status === 200) break;
const msg = JSON.stringify(ingestResponse.data || {});
testLogger.warn(`Pattern data ingestion attempt ${attempt} returned ${ingestResponse.status}`, { data: msg.slice(0, 160) });
if (attempt < 3) await page.waitForTimeout(3000);
}
if (ingestResponse.status === 200) {
testLogger.info('Pattern data ingested successfully', { status: ingestResponse.status, stream: PATTERNS_STREAM });
} else {
testLogger.error('Pattern data ingestion failed after retries', { status: ingestResponse.status, data: ingestResponse.data });
}
// Enable log patterns extraction on the stream
testLogger.info('Enabling log patterns extraction on stream...');
const settingsResponse = await enableLogPatternsExtraction(page, PATTERNS_STREAM);
testLogger.info('Stream settings updated', { status: settingsResponse.status });
// Wait for data to be indexed by polling search API (replaces arbitrary 15s wait)
testLogger.info('Waiting for data to be indexed...');
const dataIndexed = await waitForStreamData(page, PATTERNS_STREAM, 100, 30000, 2000);
if (!dataIndexed) {
testLogger.warn('Stream data polling timed out, proceeding anyway...');
}
// Only latch the "already ingested" guard when the stream actually landed, so a
// transient ingest failure doesn't make every test in the serial block run
// against a non-existent stream.
dataIngested = ingestResponse.status === 200;
} finally {
await context.close();
}
testLogger.info('Pattern test data setup complete');
});
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
// Navigate to base URL with authentication
await navigateToBase(page);
pm = new PageManager(page);
// Post-authentication stabilization wait
testLogger.info(`Test setup completed - using ${PATTERNS_STREAM} stream`);
});
test.afterEach(async ({}, testInfo) => {
testLogger.testEnd(testInfo.title, testInfo.status);
});
// ==========================================================================
// P0 - Critical / Smoke Tests
// ==========================================================================
test("should display Patterns toggle button in Enterprise edition @P0 @smoke", async ({ page }) => {
testLogger.info('Test: Verify Patterns toggle is visible (Enterprise)');
await page.goto(`${logData.logsUrl}?org_identifier=${getOrgIdentifier()}`);
// STRONG ASSERTION: Patterns toggle must be visible in Enterprise edition
await pm.logsPage.expectPatternsToggleVisible();
testLogger.info('PASSED: Patterns toggle is visible');
});
test("should switch to Patterns view when toggle is clicked @P0 @smoke", async ({ page }) => {
testLogger.info('Test: Verify patterns view activates on toggle click');
await page.goto(`${logData.logsUrl}?org_identifier=${getOrgIdentifier()}`);
// Select stream and set time range
await pm.logsPage.selectStream(PATTERNS_STREAM);
// Switch off quick mode (required for patterns)
await pm.logsPage.clickQuickModeToggle();
await pm.logsPage.clickDateTimeButton();
await pm.logsPage.clickRelative1HourOrFallback();
await pm.logsPage.clickRefreshButton();
// Wait for logs to load first
await pm.logsPage.waitForLogsTableToLoad();
// Click patterns toggle
await pm.logsPage.clickPatternsToggle();
// STRONG ASSERTION: Patterns toggle should be in selected state
await pm.logsPage.expectPatternsToggleSelected();
// Wait for patterns to load (could be patterns, statistics, or empty)
const result = await pm.logsPage.waitForPatternsToLoad(60000);
testLogger.info(`Patterns loading result: ${result}`);
// ASSERTION: Should show one of the valid states (not timeout)
expect(['statistics', 'patterns', 'empty']).toContain(result);
testLogger.info('PASSED: Patterns view activated successfully');
});
test("should display pattern statistics or empty state after loading @P0 @functional", async ({ page }) => {
testLogger.info('Test: Verify pattern view shows valid state');
await page.goto(`${logData.logsUrl}?org_identifier=${getOrgIdentifier()}`);
await pm.logsPage.selectStream(PATTERNS_STREAM);
// Switch off quick mode (required for patterns)
await pm.logsPage.clickQuickModeToggle();
await pm.logsPage.clickDateTimeButton();
await pm.logsPage.clickRelative1HourOrFallback();
await pm.logsPage.clickRefreshButton();
// Wait for logs to load first
await pm.logsPage.waitForLogsTableToLoad();
// Switch to patterns view
await pm.logsPage.clickPatternsToggle();
// Wait for patterns to load
const result = await pm.logsPage.waitForPatternsToLoad(60000);
if (result === 'statistics') {
// The old "N patterns found" summary was removed in the patterns UI
// redesign; the severity filter row now stands in for it and only
// renders once patterns exist, so assert on that contract instead of
// on summary copy that no longer exists.
await pm.logsPage.expectPatternStatisticsVisible();
const statsText = await pm.logsPage.getPatternStatisticsText();
expect(statsText.trim().length).toBeGreaterThan(0);
testLogger.info(`Severity filter: ${statsText}`);
} else if (result === 'patterns') {
// Pattern cards are visible (statistics element may not exist in UI)
await pm.logsPage.expectPatternCardsVisible();
const patternCount = await pm.logsPage.getPatternCardCount();
expect(patternCount).toBeGreaterThan(0);
testLogger.info(`Found ${patternCount} pattern cards`);
} else if (result === 'empty') {
// Empty state - verify empty message is shown
await pm.logsPage.expectPatternEmptyStateVisible();
testLogger.info('Empty state displayed - no patterns found for this data');
}
testLogger.info('PASSED: Pattern view shows valid state');
});
// ==========================================================================
// P1 - Functional Tests (only run if patterns are detected)
// ==========================================================================
test("should display pattern cards with template, frequency and percentage when patterns exist @P1 @functional", async ({ page }) => {
testLogger.info('Test: Verify pattern cards display correct information');
await page.goto(`${logData.logsUrl}?org_identifier=${getOrgIdentifier()}`);
await pm.logsPage.selectStream(PATTERNS_STREAM);
// Switch off quick mode (required for patterns)
await pm.logsPage.clickQuickModeToggle();
await pm.logsPage.clickDateTimeButton();
await pm.logsPage.clickRelative1HourOrFallback();
await pm.logsPage.clickRefreshButton();
// Wait for logs to load first
await pm.logsPage.waitForLogsTableToLoad();
await pm.logsPage.clickPatternsToggle();
const result = await pm.logsPage.waitForPatternsToLoad(60000);
if (result === 'patterns' || result === 'statistics') {
const cardCount = await pm.logsPage.getPatternCardCount();
if (cardCount > 0) {
// STRONG ASSERTION: First pattern card should have template text
const templateText = await pm.logsPage.getPatternCardTemplateText(0);
expect(templateText.length).toBeGreaterThan(0);
// STRONG ASSERTION: First pattern card should have frequency
const frequency = await pm.logsPage.getPatternCardFrequency(0);
expect(frequency).toBeTruthy();
// STRONG ASSERTION: First pattern card should have percentage
const percentage = await pm.logsPage.getPatternCardPercentage(0);
expect(percentage).toContain('%');
testLogger.info(`Pattern 0: template="${templateText.substring(0, 50)}...", frequency=${frequency}, percentage=${percentage}`);
} else {
testLogger.info('Statistics shown but no pattern cards visible yet');
}
} else {
testLogger.info('No patterns found - skipping card verification');
}
testLogger.info('PASSED: Pattern card display verification complete');
});
test("should open pattern details dialog when clicking pattern card @P1 @functional", async ({ page }) => {
testLogger.info('Test: Verify pattern details dialog opens on card click');
await page.goto(`${logData.logsUrl}?org_identifier=${getOrgIdentifier()}`);
await pm.logsPage.selectStream(PATTERNS_STREAM);
// Switch off quick mode (required for patterns)
await pm.logsPage.clickQuickModeToggle();
await pm.logsPage.clickDateTimeButton();
await pm.logsPage.clickRelative1HourOrFallback();
await pm.logsPage.clickRefreshButton();
// Wait for logs to load first
await pm.logsPage.waitForLogsTableToLoad();
await pm.logsPage.clickPatternsToggle();
const result = await pm.logsPage.waitForPatternsToLoad(60000);
if (result === 'patterns' || result === 'statistics') {
const cardCount = await pm.logsPage.getPatternCardCount();
if (cardCount > 0) {
// Click on first pattern card's details icon
await pm.logsPage.clickPatternDetailsIcon(0);
// STRONG ASSERTION: Details dialog should be open
await pm.logsPage.expectPatternDetailsDialogOpen();
// Close dialog
await pm.logsPage.closePatternDetailsDialog();
testLogger.info('Pattern details dialog opened and closed successfully');
} else {
testLogger.info('No pattern cards to click - skipping dialog test');
}
} else {
testLogger.info('No patterns found - skipping dialog test');
}
testLogger.info('PASSED: Pattern details dialog test complete');
});
test("should navigate between patterns using Previous/Next buttons @P1 @functional", async ({ page }) => {
testLogger.info('Test: Verify Previous/Next navigation in pattern details');
await page.goto(`${logData.logsUrl}?org_identifier=${getOrgIdentifier()}`);
await pm.logsPage.selectStream(PATTERNS_STREAM);
// Switch off quick mode (required for patterns)
await pm.logsPage.clickQuickModeToggle();
await pm.logsPage.clickDateTimeButton();
await pm.logsPage.clickRelative1HourOrFallback();
await pm.logsPage.clickRefreshButton();
// Wait for logs to load first
await pm.logsPage.waitForLogsTableToLoad();
await pm.logsPage.clickPatternsToggle();
const result = await pm.logsPage.waitForPatternsToLoad(60000);
if (result === 'patterns' || result === 'statistics') {
const cardCount = await pm.logsPage.getPatternCardCount();
if (cardCount >= 2) {
const absoluteIndex = await pm.logsPage.clickPatternDetailsIcon(0);
await pm.logsPage.expectPatternDetailsDialogOpen();
if (absoluteIndex === 0) {
await pm.logsPage.expectPatternDetailPreviousBtnDisabled();
await pm.logsPage.expectPatternDetailNextBtnEnabled();
await pm.logsPage.clickPatternDetailNextBtn();
await pm.logsPage.waitForPatternDetailIndex(2);
await pm.logsPage.expectPatternDetailPreviousBtnEnabled();
await pm.logsPage.clickPatternDetailPreviousBtn();
await pm.logsPage.waitForPatternDetailIndex(1);
} else {
await pm.logsPage.expectPatternDetailPreviousBtnEnabled();
await pm.logsPage.clickPatternDetailPreviousBtn();
await pm.logsPage.waitForPatternDetailIndex(absoluteIndex);
await pm.logsPage.expectPatternDetailNextBtnEnabled();
await pm.logsPage.clickPatternDetailNextBtn();
await pm.logsPage.waitForPatternDetailIndex(absoluteIndex + 1);
}
await pm.logsPage.closePatternDetailsDialog();
testLogger.info('Navigation between patterns works correctly');
} else {
testLogger.info('Not enough patterns for navigation test (need at least 2)');
}
} else {
testLogger.info('No patterns found - skipping navigation test');
}
testLogger.info('PASSED: Pattern navigation test complete');
});
test("should add pattern to search using Include button @P1 @functional", async ({ page }) => {
testLogger.info('Test: Verify Include button functionality');
await page.goto(`${logData.logsUrl}?org_identifier=${getOrgIdentifier()}`);
await pm.logsPage.selectStream(PATTERNS_STREAM);
// Switch off quick mode (required for patterns)
await pm.logsPage.clickQuickModeToggle();
await pm.logsPage.clickDateTimeButton();
await pm.logsPage.clickRelative1HourOrFallback();
await pm.logsPage.clickRefreshButton();
// Wait for logs to load first
await pm.logsPage.waitForLogsTableToLoad();
await pm.logsPage.clickPatternsToggle();
const result = await pm.logsPage.waitForPatternsToLoad(60000);
if (result === 'patterns' || result === 'statistics') {
const cardCount = await pm.logsPage.getPatternCardCount();
if (cardCount > 0) {
const templateText = await pm.logsPage.getPatternCardTemplateText(0);
// Click include button
await pm.logsPage.clickPatternIncludeBtn(0);
// ASSERTION: After clicking Include, page should navigate away from patterns view
// Verify logs table is visible (pattern view is exited)
await pm.logsPage.expectLogsTableVisible();
testLogger.info(`Clicked include on pattern: ${templateText.substring(0, 50)}...`);
} else {
testLogger.info('No patterns to include');
}
} else {
testLogger.info('No patterns found - skipping include test');
}
testLogger.info('PASSED: Include button test complete');
});
test("should add pattern to search using Exclude button @P1 @functional", async ({ page }) => {
testLogger.info('Test: Verify Exclude button functionality');
await page.goto(`${logData.logsUrl}?org_identifier=${getOrgIdentifier()}`);
await pm.logsPage.selectStream(PATTERNS_STREAM);
// Switch off quick mode (required for patterns)
await pm.logsPage.clickQuickModeToggle();
await pm.logsPage.clickDateTimeButton();
await pm.logsPage.clickRelative1HourOrFallback();
await pm.logsPage.clickRefreshButton();
// Wait for logs to load first
await pm.logsPage.waitForLogsTableToLoad();
await pm.logsPage.clickPatternsToggle();
const result = await pm.logsPage.waitForPatternsToLoad(60000);
if (result === 'patterns' || result === 'statistics') {
const cardCount = await pm.logsPage.getPatternCardCount();
if (cardCount > 0) {
const templateText = await pm.logsPage.getPatternCardTemplateText(0);
// Click exclude button
await pm.logsPage.clickPatternExcludeBtn(0);
// ASSERTION: After clicking Exclude, page should navigate away from patterns view
// Verify logs table is visible (pattern view is exited)
await pm.logsPage.expectLogsTableVisible();
testLogger.info(`Clicked exclude on pattern: ${templateText.substring(0, 50)}...`);
} else {
testLogger.info('No patterns to exclude');
}
} else {
testLogger.info('No patterns found - skipping exclude test');
}
testLogger.info('PASSED: Exclude button test complete');
});
// ==========================================================================
// P2 - Edge Case Tests
// ==========================================================================
test("should handle empty state gracefully @P2 @edgeCase", async ({ page }) => {
testLogger.info('Test: Verify empty state handling');
await page.goto(`${logData.logsUrl}?org_identifier=${getOrgIdentifier()}`);
// Use a short time range that's likely to have no patterns
await pm.logsPage.selectStream(PATTERNS_STREAM);
// Switch off quick mode (required for patterns)
await pm.logsPage.clickQuickModeToggle();
await pm.logsPage.setTimeToPast30Seconds();
await pm.logsPage.clickRefreshButton();
await pm.logsPage.clickPatternsToggle();
const result = await pm.logsPage.waitForPatternsToLoad(60000);
// Either empty state or patterns is valid
expect(['statistics', 'patterns', 'empty']).toContain(result);
if (result === 'empty') {
await pm.logsPage.expectPatternEmptyStateVisible();
testLogger.info('Empty state displayed correctly');
} else {
testLogger.info(`Got ${result} - patterns found even in short time range`);
}
testLogger.info('PASSED: Empty state test complete');
});
test("should open details dialog via details icon @P2 @functional", async ({ page }) => {
testLogger.info('Test: Verify details icon opens pattern details dialog');
await page.goto(`${logData.logsUrl}?org_identifier=${getOrgIdentifier()}`);
await pm.logsPage.selectStream(PATTERNS_STREAM);
// Switch off quick mode (required for patterns)
await pm.logsPage.clickQuickModeToggle();
await pm.logsPage.clickDateTimeButton();
await pm.logsPage.clickRelative1HourOrFallback();
await pm.logsPage.clickRefreshButton();
// Wait for logs to load first
await pm.logsPage.waitForLogsTableToLoad();
await pm.logsPage.clickPatternsToggle();
const result = await pm.logsPage.waitForPatternsToLoad(60000);
if (result === 'patterns' || result === 'statistics') {
const cardCount = await pm.logsPage.getPatternCardCount();
if (cardCount > 0) {
// Click details icon
await pm.logsPage.clickPatternDetailsIcon(0);
// STRONG ASSERTION: Details dialog should be open
await pm.logsPage.expectPatternDetailsDialogOpen();
await pm.logsPage.closePatternDetailsDialog();
testLogger.info('Details icon opens dialog correctly');
} else {
testLogger.info('No pattern cards found');
}
} else {
testLogger.info('No patterns found - skipping details icon test');
}
testLogger.info('PASSED: Details icon test complete');
});
// ==========================================================================
// PR #10949 & #10965 - Duplicate Patterns Fix & Tokenized Chips Tests
// ==========================================================================
test("should not display duplicate patterns @P0 @smoke", async ({ page }) => {
testLogger.info('Test: Verify patterns are unique (no duplicates)');
const result = await setupPatternsView(page, pm);
// CRITICAL: P0 test must have pattern data to be meaningful
expect(['patterns', 'statistics']).toContain(result);
const cardCount = await pm.logsPage.getPatternCardCount();
// STRONG ASSERTION: Must have patterns for duplicate check (P0 test)
expect(cardCount).toBeGreaterThan(0);
// Get all pattern templates
const patternTemplates = [];
for (let i = 0; i < cardCount; i++) {
const template = await pm.logsPage.getPatternCardTemplateText(i);
patternTemplates.push(template);
}
testLogger.info(`Total patterns found: ${cardCount}`);
testLogger.info(`Pattern templates: ${patternTemplates.map(t => t.substring(0, 50)).join(', ')}`);
// Check for duplicates
const uniqueTemplates = [...new Set(patternTemplates)];
const duplicateCount = cardCount - uniqueTemplates.length;
testLogger.info(`Unique patterns: ${uniqueTemplates.length}`);
testLogger.info(`Duplicate patterns: ${duplicateCount}`);
// STRONG ASSERTION: No duplicates should exist
expect(duplicateCount).toBe(0);
expect(uniqueTemplates.length).toBe(cardCount);
await page.screenshot({ path: 'playwright-results/pattern-no-duplicates.png', fullPage: true });
testLogger.info('✅ VERIFIED: All patterns are unique, no duplicates found');
testLogger.info('PASSED: No duplicate patterns test complete');
});
test("should display tokenized wildcard chips in pattern cards @P1 @functional", async ({ page }) => {
testLogger.info('Test: Verify tokenized wildcard chips are displayed');
const result = await setupPatternsView(page, pm);
// ASSERTION: Patterns must be available for this test
expect(['patterns', 'statistics']).toContain(result);
const cardCount = await pm.logsPage.getPatternCardCount();
// ASSERTION: Must have patterns to test tokenization
expect(cardCount).toBeGreaterThan(0);
// Check multiple patterns for wildcards (not just index 0)
// At least one pattern should contain wildcard indicators
let patternsWithWildcards = 0;
const patternSamples = [];
for (let i = 0; i < Math.min(cardCount, 5); i++) {
const patternText = await pm.logsPage.getPatternCardTemplateText(i);
// Check for wildcard chip DOM elements (rendered as .wildcard-chip)
// textContent returns short labels like "str"/"num" rather than the
// raw template format like "<:STR>"/"<:NUM>"
const chipCount = await pm.logsPage.getPatternCardWildcardChipCount(i);
const hasWildcards = chipCount > 0;
if (hasWildcards) {
patternsWithWildcards++;
}
patternSamples.push({
index: i,
sample: patternText.substring(0, 80),
hasWildcards
});
}
testLogger.info(`Patterns checked: ${patternSamples.length}`);
testLogger.info(`Patterns with wildcards: ${patternsWithWildcards}`);
testLogger.info(`Pattern samples: ${JSON.stringify(patternSamples, null, 2)}`);
// Take screenshot for manual verification
await page.screenshot({ path: 'playwright-results/pattern-wildcard-chips.png', fullPage: true });
// ASSERTION: At least one pattern must contain wildcard indicators
expect(patternsWithWildcards).toBeGreaterThan(0);
testLogger.info('✅ Wildcard tokenization verified');
testLogger.info('PASSED: Tokenized wildcard chips test complete');
});
test("should display wildcard chip tooltips with sample values on hover @P1 @functional", async ({ page }) => {
testLogger.info('Test: Verify wildcard chip hover tooltips show sample values');
const result = await setupPatternsView(page, pm);
// ASSERTION: Patterns must be available for this test
expect(['patterns', 'statistics']).toContain(result);
const cardCount = await pm.logsPage.getPatternCardCount();
// ASSERTION: Must have patterns to test tooltips
expect(cardCount).toBeGreaterThan(0);
// Find wildcard chips - they have class 'wildcard-chip' in PatternCard.vue
// Tooltips only appear when tok.sampleValues.length > 0 (see PatternCard.vue:48)
const chipCount = await pm.logsPage.getWildcardChipCount();
testLogger.info(`Found ${chipCount} wildcard chips on page`);
// ASSERTION: Must have wildcard chips to test
expect(chipCount).toBeGreaterThan(0);
// Try hovering over multiple chips to find one with a tooltip
// Tooltips only appear when pattern has sampleValues (data-dependent)
let tooltipFound = false;
let tooltipContent = '';
const chipsToTry = Math.min(chipCount, 5);
for (let i = 0; i < chipsToTry && !tooltipFound; i++) {
const isVisible = await pm.logsPage.isWildcardChipVisible(i);
if (!isVisible) continue;
const chipText = await pm.logsPage.getWildcardChipText(i);
testLogger.info(`Trying chip ${i}: ${chipText}`);
// Hover over the chip
await pm.logsPage.hoverWildcardChip(i);
// Wait for tooltip (delay is 300ms + render time)
await page.waitForTimeout(500);
// Check if tooltip appeared
const tooltipVisible = await pm.logsPage.isTooltipVisible(1000);
if (tooltipVisible) {
tooltipFound = true;
tooltipContent = await pm.logsPage.getTooltipText(1000) || '';
testLogger.info(`✅ Tooltip found on chip ${i}: ${tooltipContent.substring(0, 100)}`);
// ASSERTION: Tooltip content must not be empty
expect(tooltipContent.length).toBeGreaterThan(0);
break;
}
// Move mouse away to reset for next chip
await page.mouse.move(0, 0);
await page.waitForTimeout(100);
}
await page.screenshot({ path: 'playwright-results/pattern-wildcard-tooltip.png', fullPage: true });
// If no tooltip found on any chip, this is data-dependent (no sampleValues)
// The test should still verify chips render correctly
if (tooltipFound) {
testLogger.info('✅ Wildcard chip tooltip with sample values verified');
} else {
// No tooltips found - verify this is due to missing sampleValues, not broken feature
// PatternCard.vue only shows tooltip when tok.sampleValues.length > 0
testLogger.warn('⚠️ No tooltips found on any wildcard chip - pattern data may not include sampleValues');
testLogger.info('This is data-dependent: tooltips only appear when sampleValues are populated');
// ASSERTION: At minimum, chips must render with proper text
// wildcardLabel() maps <:STR>→"str", <:NUM>→"num", <*>→"<*>" etc.
// chips display short labels, not raw angle-bracket tokens
const firstChipText = await pm.logsPage.getWildcardChipText(0);
expect(firstChipText.length).toBeGreaterThan(0);
// Match wildcard chip labels: <*> literal or short type label (str, num, ip, ts, etc.)
expect(firstChipText).toMatch(/^[a-z0-9]+$|^<\*>$/);
testLogger.info('✅ Wildcard chips render correctly (no sampleValues in data for tooltips)');
}
testLogger.info('PASSED: Wildcard chip tooltip test complete');
});
test("should render anomaly badges inline with pattern cards @P1 @functional", async ({ page }) => {
testLogger.info('Test: Verify anomaly detection badges render inline with patterns');
const result = await setupPatternsView(page, pm);
// ASSERTION: Patterns must be available for this test
expect(['patterns', 'statistics']).toContain(result);
const cardCount = await pm.logsPage.getPatternCardCount();
// ASSERTION: Must have patterns to test anomaly badges
expect(cardCount).toBeGreaterThan(0);
// Anomaly badges are INLINE with pattern cards, not in a separate column
// Look for anomaly badges using data-test attribute: pattern-card-{index}-anomaly-badge
let patternsWithAnomalyBadge = 0;
const anomalyBadgeInfo = [];
for (let i = 0; i < Math.min(cardCount, 10); i++) {
const isVisible = await pm.logsPage.isPatternAnomaly(i);
if (isVisible) {
patternsWithAnomalyBadge++;
const badgeText = await pm.logsPage.getPatternAnomalyBadgeText(i);
anomalyBadgeInfo.push({ index: i, text: badgeText });
}
}
testLogger.info(`Patterns with anomaly badges: ${patternsWithAnomalyBadge} out of ${Math.min(cardCount, 10)} checked`);
if (anomalyBadgeInfo.length > 0) {
testLogger.info(`Anomaly badge samples: ${JSON.stringify(anomalyBadgeInfo.slice(0, 3))}`);
}
await page.screenshot({ path: 'playwright-results/pattern-anomaly-badges.png', fullPage: true });
// Anomaly detection is data-dependent - badges only appear for statistically rare patterns
// The test verifies the feature is functional, not that anomalies must exist
if (patternsWithAnomalyBadge > 0) {
testLogger.info(`✅ Anomaly detection active: ${patternsWithAnomalyBadge} badges rendered`);
// Verify badge contains expected text (e.g., "Rare Pattern" or warning emoji)
const firstBadge = anomalyBadgeInfo[0];
expect(firstBadge.text.length).toBeGreaterThan(0);
testLogger.info(`Anomaly badge text verified: "${firstBadge.text}"`);
} else {
// No anomalies in current data - this is acceptable
testLogger.info('No anomaly badges visible (data-dependent - patterns may not be statistically rare)');
// Verify pattern cards themselves are rendered correctly
const firstPatternTemplate = await pm.logsPage.getPatternCardTemplateText(0);
expect(firstPatternTemplate.length).toBeGreaterThan(0);
testLogger.info('✅ Pattern cards render correctly, no anomalies detected in current data');
}
testLogger.info('PASSED: Anomaly detection badge test complete');
});
test("should display pattern statistics in details dialog @P1 @functional", async ({ page }) => {
testLogger.info('Test: Verify pattern statistics (z_score, avg_frequency) in details dialog');
const result = await setupPatternsView(page, pm);
// ASSERTION: Patterns must be available for this test
expect(['patterns', 'statistics']).toContain(result);
const cardCount = await pm.logsPage.getPatternCardCount();
// ASSERTION: Must have patterns to test statistics
expect(cardCount).toBeGreaterThan(0);
// Open pattern details dialog
await pm.logsPage.clickPatternDetailsIcon(0);
await pm.logsPage.expectPatternDetailsDialogOpen();
// Check for statistical information in the dialog
const dialogContent = await pm.logsPage.getPatternDetailsDialogContent();
// Look for statistical fields (PR #10965 adds z_score, avg_frequency, wildcard_values)
const hasFrequency = dialogContent.toLowerCase().includes('frequency') ||
dialogContent.toLowerCase().includes('occurrences') ||
dialogContent.toLowerCase().includes('count');
const hasPercentage = dialogContent.includes('%');
const hasZScore = dialogContent.toLowerCase().includes('z-score') ||
dialogContent.toLowerCase().includes('z_score') ||
dialogContent.toLowerCase().includes('zscore');
testLogger.info(`Dialog statistics - Frequency: ${hasFrequency}, Percentage: ${hasPercentage}, Z-Score: ${hasZScore}`);
testLogger.info(`Dialog content preview: ${dialogContent.substring(0, 200)}`);
await page.screenshot({ path: 'playwright-results/pattern-details-statistics.png', fullPage: true });
// ASSERTION: Dialog must contain both frequency/count AND percentage
// These are core pattern statistics that should always be displayed
expect(hasFrequency).toBeTruthy();
expect(hasPercentage).toBeTruthy();
// INFO: Z-Score may be conditionally displayed (for anomalous patterns)
if (hasZScore) {
testLogger.info('✅ Z-Score statistic also present in dialog');
}
await pm.logsPage.closePatternDetailsDialog();
testLogger.info('✅ Pattern statistics (frequency and percentage) verified in details dialog');
testLogger.info('PASSED: Pattern statistics test complete');
});
});