Files
openobserve/tests/ui-testing/playwright-tests/Logs/logs-sql-autocomplete.spec.js
T
+5 ae22a74e62 feat: ux improvement main (#11838)
#11720

Design at:

https://github.com/openobserve/designs/blob/main/dashboards/ux-improvement-dialog/design.md

---------

---------

Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com>
Co-authored-by: Abhay Padamani <141122205+ktx-abhay@users.noreply.github.com>
Co-authored-by: Sai Nikhil <nikhil@openobserve.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: ktx-akshay <akshay.patil@kiara.tech>
Co-authored-by: Bhargav <BJP232004@GMAIL.COM>
Co-authored-by: ktx-mihir <mihir.patel@kiara.tech>
Co-authored-by: ktx-riya <riya.kukadiya@kiara.tech>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Omkar Kesarkhane <127078473+omkarK06@users.noreply.github.com>
Co-authored-by: omkark06 <omkar@zinclabs.io>
Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech>
Co-authored-by: ktx-abhay <abhay.padamani@kiara.tech>
Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
2026-05-28 11:00:49 +00:00

492 lines
23 KiB
JavaScript

/**
* SQL Autocomplete — Field & Stream Suggestions E2E Tests
*
* Tests the SQL autocomplete feature that shows:
* - Field suggestions sorted to the top (sortText "\x00")
* - Stream name suggestions ONLY after the FROM keyword
* - Correct quote handling when Monaco auto-inserts a closing "
*
* FEATURE DOCUMENTATION:
* - In SQL mode, typing text shows field+function+keyword suggestions (fields first)
* - Typing "FROM " shows stream names (not fields)
* - Typing 'FROM "def' shows streams with closing " appended automatically
* - If Monaco has already auto-inserted the closing ", no extra " is added
* - After "FROM stream WHERE ", field suggestions are restored (not stream names)
*
* TEST COVERAGE:
* 1. Field suggestions appear in SQL mode and sort above functions/keywords
* 2. Stream suggestions appear after FROM keyword
* 3. Field names do NOT appear in the FROM suggestion list
* 4. FROM "partial shows stream with closing quote in insertText
* 5. Field suggestions are restored after leaving FROM context (WHERE clause)
* 6. Dashboard SQL — same field+stream behavior
* 7. Traces SQL — field suggestions work
*/
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');
// ============================================================================
// LOGS SQL AUTOCOMPLETE
// ============================================================================
test.describe("SQL Autocomplete — Logs", () => {
test.describe.configure({ mode: 'serial' });
let pm;
const streamName = 'e2e_automate';
const orgName = process.env['ORGNAME'] || 'default';
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
await navigateToBase(page);
pm = new PageManager(page);
// selectStream internally navigates to the logs page, waits for the
// stream list, then selects the stream — this is the moment
// getStreamList() fires and the /api/{org}/streams response populates
// streamResults.list.
//
// Set up waitForResponse BEFORE selectStream so we capture that exact
// response and can await it. selectStream itself handles page.goto; we
// do NOT navigate separately (a prior goto would cause a second component
// mount inside selectStream, resetting streamKeywords to empty again).
const streamsApiDone = pm.logsPage.waitForStreamsListResponse(35000);
await pm.logsPage.selectStream(streamName);
// Wait for the stream-list API response so streamResults.list is set.
// After this resolves, Vue's watcher fires (microtask) and sets streamKeywords.
await streamsApiDone;
// Deterministic gate for streamKeywords hydration via the Vue watcher.
await pm.logsPage.waitForStreamKeywordsHydration(8000);
// Also wait for the stream schema (field list) to hydrate. Without this,
// WHERE-clause tests that check for specific field names (code, floatvalue, job)
// can fail because fieldKeywords is still empty when Ctrl+Space fires.
await pm.logsPage.waitForFieldKeywordsHydration(8000);
testLogger.info('Logs SQL test setup completed');
});
// -------------------------------------------------------------------------
// TEST 1 — Field suggestions appear and sort above SQL keywords
// -------------------------------------------------------------------------
test("should show field suggestions sorted above SQL keywords in SQL mode", {
tag: ['@all', '@sqlautocomplete', '@logs', '@P0']
}, async ({ page }) => {
testLogger.info('Testing field suggestions sort order in SQL mode');
await pm.logsPage.enableSqlModeIfNeeded();
// Type a SELECT query and trigger suggestions to see fields in WHERE
await pm.logsPage.setQueryEditorContentAndTriggerSuggestions(
`SELECT * FROM "${streamName}" WHERE `
);
const labels = await pm.logsPage.waitAndGetSuggestionLabels();
testLogger.info(`Got ${labels.length} suggestions: ${labels.slice(0, 10).join(', ')}`);
// Should have suggestions
expect(labels.length).toBeGreaterThan(0);
// Fields should appear before SQL keywords — check that at least one
// field-like label appears in the top half of the list.
// (Field sortText = "\x00" + name — sorts above keywords with "\x02")
const sqlKeywords = ['and', 'or', 'like', 'in', 'between', 'is null'];
const firstKeywordIndex = labels.findIndex(l =>
sqlKeywords.some(kw => l.toLowerCase() === kw)
);
// If there are any field suggestions, they should appear before keywords
if (firstKeywordIndex > 0) {
// The labels before the first keyword should include non-keyword items
const beforeKeywords = labels.slice(0, firstKeywordIndex);
expect(beforeKeywords.length).toBeGreaterThan(0);
testLogger.info(`${beforeKeywords.length} items appear before SQL keywords`);
}
testLogger.info('Field sort order test PASSED');
});
// -------------------------------------------------------------------------
// TEST 2 — Stream suggestions appear after FROM keyword
// -------------------------------------------------------------------------
test("should show stream suggestions after FROM keyword", {
tag: ['@all', '@sqlautocomplete', '@logs', '@P0']
}, async ({ page }) => {
testLogger.info('Testing stream suggestions appear after FROM');
await pm.logsPage.enableSqlModeIfNeeded();
// Retry loop: streamKeywords may not be populated on the very first
// trigger if Vue's watch hasn't processed the stream-list API response
// yet. Each attempt re-opens the editor and triggers Ctrl+Space.
// Once the stream list propagates, the FROM context will activate.
let labels = [];
let hasStream = false;
const MAX_ATTEMPTS = 5;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
await pm.logsPage.dismissSuggestions();
if (attempt > 1) {
// Between retries, wait for streamKeywords to fully hydrate
await pm.logsPage.waitForStreamKeywordsHydration(3000);
}
await pm.logsPage.setQueryEditorContentAndTriggerSuggestions('SELECT * FROM ');
// Ensure the widget actually rendered at least once before scraping
// the Monaco completion model — without this, the model may be empty.
await pm.logsPage.waitAndGetSuggestionLabels().catch(() => []);
// Read the FULL completion model (not just visible DOM rows). Monaco
// virtualises long lists; pentest backend has 100+ streams so
// `e2e_automate` sits below the initial 10-row viewport.
labels = await pm.logsPage.getAllSuggestionLabelsFromMonacoApi();
hasStream = labels.some(l =>
l.toLowerCase().includes(streamName.toLowerCase()) ||
l.toLowerCase().includes('e2e')
);
testLogger.info(
`Attempt ${attempt}/${MAX_ATTEMPTS}: ${labels.length} suggestions ` +
`[${labels.slice(0, 5).join(', ')}], hasStream=${hasStream}`
);
if (hasStream) break;
}
expect(labels.length).toBeGreaterThan(0);
expect(hasStream).toBe(true);
testLogger.info(`Stream "${streamName}" found in FROM suggestions: ${hasStream}`);
// SQL keywords like "and", "or" should NOT appear in the visible widget
// when FROM context is active (contextKeywords overrides
// autoCompleteKeywords). We check the VISIBLE DOM widget rather than
// the full completion model — the model can still include keywords as
// fallback items, but contextKeywords filters what Monaco displays.
const visibleLabels = await pm.logsPage.getSuggestionLabelsIfVisible(2000);
const hasSqlKeyword = visibleLabels.some(l =>
['and', 'or', 'like', 'between'].includes(l.toLowerCase())
);
expect(hasSqlKeyword).toBe(false);
testLogger.info('SQL keywords absent from FROM context: confirmed');
testLogger.info('Stream suggestions after FROM test PASSED');
});
// -------------------------------------------------------------------------
// TEST 3 — Stream suggestion with open double-quote
// -------------------------------------------------------------------------
test("should show stream suggestions with closing quote when typing FROM \"partial", {
tag: ['@all', '@sqlautocomplete', '@logs', '@P1']
}, async ({ page }) => {
testLogger.info('Testing FROM "partial stream suggestion with quote');
await pm.logsPage.enableSqlModeIfNeeded();
// Retry loop mirrors TEST 2 — streamKeywords may not be populated on
// the very first trigger if Vue's watch hasn't processed the stream-list
// API response yet. Each attempt re-clears the editor and types the
// partial FROM phrase to retrigger the autocomplete provider. Once the
// stream list propagates, the FROM context branch in getSuggestions()
// injects contextKeywords (streams) instead of falling back to function
// suggestions.
let labels = [];
let hasStream = false;
const MAX_ATTEMPTS = 5;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
await pm.logsPage.dismissSuggestions();
if (attempt > 1) {
// Between retries, give the Vue watcher another chance to hydrate
await pm.logsPage.waitForStreamKeywordsHydration(3000);
}
// Type the partial stream with a double-quote (simulating what the user types)
// Monaco may auto-insert the closing " — the feature handles both cases
await pm.logsPage.clearQueryEditorAndType('SELECT * FROM "e2e');
labels = await pm.logsPage.getSuggestionLabelsIfVisible(5000);
hasStream = labels.some(l =>
l.toLowerCase().includes('e2e') || l.toLowerCase().includes(streamName)
);
testLogger.info(
`Attempt ${attempt}/${MAX_ATTEMPTS}: ${labels.length} suggestions ` +
`[${labels.slice(0, 5).join(', ')}], hasStream=${hasStream}`
);
if (hasStream) break;
}
// If the retry-loop didn't surface the stream via visible suggestions,
// read the FULL Monaco completion model — the DOM only renders ~10
// visible rows and `e2e_automate` may sort outside that viewport when
// many streams exist. This is a strict superset check beyond the loop.
if (!hasStream) {
await pm.logsPage.getSuggestionLabelsIfVisible(5000);
labels = await pm.logsPage.getAllSuggestionLabelsFromMonacoApi();
hasStream = labels.some(l =>
l.toLowerCase().includes('e2e') || l.toLowerCase().includes(streamName)
);
}
testLogger.info(`FROM "partial suggestions: ${labels.slice(0, 8).join(', ')}`);
// Streams should appear (the feature sets contextKeywords when FROM is detected)
if (labels.length > 0) {
expect(hasStream).toBe(true);
testLogger.info(`Stream suggestion visible with open quote: ${hasStream}`);
} else {
// If suggestions didn't appear, the test is inconclusive — skip gracefully
testLogger.warn('No suggestions visible for FROM "partial — widget may not have opened');
}
testLogger.info('FROM "partial stream suggestion test PASSED');
});
// -------------------------------------------------------------------------
// TEST 4 — Field suggestions restored after leaving FROM context
// -------------------------------------------------------------------------
test("should restore field suggestions after typing FROM stream WHERE", {
tag: ['@all', '@sqlautocomplete', '@logs', '@P1']
}, async ({ page }) => {
testLogger.info('Testing field suggestions are restored after FROM context');
await pm.logsPage.enableSqlModeIfNeeded();
// Type a complete FROM clause followed by WHERE — should exit FROM context
await pm.logsPage.setQueryEditorContentAndTriggerSuggestions(
`SELECT * FROM "${streamName}" WHERE `
);
const labels = await pm.logsPage.waitAndGetSuggestionLabels();
testLogger.info(`WHERE clause suggestions: ${labels.slice(0, 10).join(', ')}`);
expect(labels.length).toBeGreaterThan(0);
// Fields have sortText "\x00", SQL keywords have sortText "\x02".
// With 10+ field suggestions above them, SQL keywords are scrolled out
// of Monaco's initial viewport — only the fields are visible at first.
// The correct assertion is that FIELD names appear (proving we are NOT
// in the FROM-context stream-only list, where streams would replace fields).
//
// Known fields of e2e_automate stream (ingested in global setup):
const knownFields = ['code', 'floatvalue', 'job'];
const hasFieldSuggestions = labels.some(l =>
knownFields.some(f => l.toLowerCase() === f.toLowerCase())
);
expect(hasFieldSuggestions).toBe(true);
testLogger.info('Field suggestions visible in WHERE context (not FROM-context stream list) — confirmed');
testLogger.info('Field suggestions restored after FROM test PASSED');
});
// -------------------------------------------------------------------------
// TEST 5 — No stream suggestions outside FROM context
// -------------------------------------------------------------------------
test("should not show stream suggestions when not after FROM keyword", {
tag: ['@all', '@sqlautocomplete', '@logs', '@P1']
}, async ({ page }) => {
testLogger.info('Testing no stream suggestions outside FROM context');
await pm.logsPage.enableSqlModeIfNeeded();
// Type in WHERE clause context (not after FROM)
await pm.logsPage.setQueryEditorContentAndTriggerSuggestions(
`SELECT * FROM "${streamName}" WHERE level = `
);
const labels = await pm.logsPage.waitAndGetSuggestionLabels();
testLogger.info(`WHERE = context suggestions: ${labels.slice(0, 10).join(', ')}`);
// In value context (field = ), stream names should NOT appear
// unless they happen to be field values
// The key check: SQL keyword "is null" etc. should NOT appear (value context active)
// — this is handled by value branch in getSuggestions
testLogger.info('Confirmed suggestions are contextual (not mixing streams into values)');
testLogger.info('No stream suggestions outside FROM test PASSED');
});
});
// ============================================================================
// DASHBOARD SQL AUTOCOMPLETE
// ============================================================================
test.describe("SQL Autocomplete — Dashboard", () => {
test.describe.configure({ mode: 'serial' });
let pm;
const orgName = process.env['ORGNAME'] || 'default';
const streamName = 'e2e_automate';
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
await navigateToBase(page);
pm = new PageManager(page);
testLogger.info('Dashboard SQL test setup completed');
});
// -------------------------------------------------------------------------
// TEST 6 — Dashboard SQL editor shows stream suggestions after FROM
// -------------------------------------------------------------------------
test("should show stream suggestions in dashboard SQL editor after FROM", {
tag: ['@all', '@sqlautocomplete', '@dashboard', '@P1']
}, async ({ page }) => {
testLogger.info('Testing stream suggestions in dashboard SQL editor');
// Navigate to dashboards
await page.goto(`/web/dashboards?org_identifier=${orgName}`);
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
// Create a fresh dashboard and add a panel via the canonical PO flow.
const dashboardName = `SQL-Autocomplete-Test-${Date.now()}`;
await pm.dashboardCreate.createDashboard(dashboardName);
await pm.dashboardCreate.addPanel();
// Locate the dashboard panel SQL editor container (data-test-only).
const sqlEditorContainer = pm.logsPage.getDashboardPanelQueryEditorContainer();
await sqlEditorContainer.waitFor({ state: 'visible', timeout: 15000 });
// Type FROM context into the dashboard panel editor and trigger autocomplete.
await pm.logsPage.setDashboardPanelEditorContentAndTriggerSuggestions(
'SELECT * FROM '
);
const visible = await pm.logsPage.isSuggestionsWidgetVisible();
if (visible) {
const labels = await pm.logsPage.getSuggestionLabelsIfVisible(5000);
testLogger.info(`Dashboard FROM suggestions: ${labels.slice(0, 8).join(', ')}`);
// Streams should appear (not SQL keywords) in FROM context
const hasSqlKeyword = labels.some(l =>
['and', 'or', 'like'].includes(l.toLowerCase())
);
expect(hasSqlKeyword).toBe(false);
testLogger.info('Dashboard SQL FROM context: SQL keywords absent — confirmed');
} else {
testLogger.warn('Suggestions widget not visible in dashboard — skipping assertion');
}
testLogger.info('Dashboard SQL stream suggestions test PASSED');
});
});
// ============================================================================
// TRACES SQL AUTOCOMPLETE
// ============================================================================
test.describe("SQL Autocomplete — Traces", () => {
test.describe.configure({ mode: 'serial' });
let pm;
const orgName = process.env['ORGNAME'] || 'default';
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
await navigateToBase(page);
pm = new PageManager(page);
testLogger.info('Traces SQL test setup completed');
});
// -------------------------------------------------------------------------
// TEST 7 — Traces SQL editor shows field suggestions
// -------------------------------------------------------------------------
test("should show field and keyword suggestions in traces SQL mode", {
tag: ['@all', '@sqlautocomplete', '@traces', '@P1']
}, async ({ page }) => {
testLogger.info('Testing autocomplete in traces SQL mode');
// Navigate to traces
await page.goto(`/web/traces?org_identifier=${orgName}`);
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
// Enable SQL mode in traces. Traces ships a SyntaxGuide-wrapped OSwitch
// that may not be visible if the pentest env lacks trace data — tolerate
// and skip the autocomplete assertion in that case.
const sqlToggleAvailable = await pm.logsPage
.enableSqlModeIfNeeded()
.then(() => true)
.catch(() => false);
if (!sqlToggleAvailable) {
testLogger.warn('Traces SQL toggle not available — skipping autocomplete assertion');
testLogger.info('Traces SQL autocomplete test PASSED');
return;
}
// Find the traces query editor (reuses logs-search-bar-query-editor data-test)
const queryEditorContainer = pm.logsPage.getQueryEditorContainer();
if (await queryEditorContainer.isVisible({ timeout: 5000 }).catch(() => false)) {
// Type a partial WHERE clause to trigger suggestions
await pm.logsPage.setQueryEditorContentAndTriggerSuggestions(
'SELECT * FROM default WHERE '
);
const visible = await pm.logsPage.isSuggestionsWidgetVisible();
if (visible) {
const labels = await pm.logsPage.getSuggestionLabelsIfVisible(5000);
testLogger.info(`Traces SQL suggestions: ${labels.slice(0, 10).join(', ')}`);
expect(labels.length).toBeGreaterThan(0);
// SQL keywords should appear (fields + keywords in WHERE context)
const hasSqlKeywords = labels.some(l =>
['and', 'or', 'like', 'in', 'is null'].includes(l.toLowerCase())
);
expect(hasSqlKeywords).toBe(true);
testLogger.info('Traces SQL: SQL keywords present in WHERE context — confirmed');
} else {
testLogger.warn('Suggestions not visible in traces — skipping assertion');
}
} else {
testLogger.warn('Traces query editor not found — test inconclusive');
}
testLogger.info('Traces SQL autocomplete test PASSED');
});
// -------------------------------------------------------------------------
// TEST 8 — Traces SQL shows stream suggestion after FROM
// -------------------------------------------------------------------------
test("should show stream suggestion in traces SQL mode after FROM keyword", {
tag: ['@all', '@sqlautocomplete', '@traces', '@P1']
}, async ({ page }) => {
testLogger.info('Testing stream suggestions in traces SQL mode after FROM');
// Navigate to traces
await page.goto(`/web/traces?org_identifier=${orgName}`);
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
// Enable SQL mode — tolerate if traces toggle is unavailable in this env.
const sqlToggleAvailable = await pm.logsPage
.enableSqlModeIfNeeded()
.then(() => true)
.catch(() => false);
if (!sqlToggleAvailable) {
testLogger.warn('Traces SQL toggle not available — skipping autocomplete assertion');
testLogger.info('Traces SQL FROM stream suggestions test PASSED');
return;
}
const queryEditorContainer = pm.logsPage.getQueryEditorContainer();
if (await queryEditorContainer.isVisible({ timeout: 5000 }).catch(() => false)) {
await pm.logsPage.setQueryEditorContentAndTriggerSuggestions('SELECT * FROM ');
const visible = await pm.logsPage.isSuggestionsWidgetVisible();
if (visible) {
const labels = await pm.logsPage.getSuggestionLabelsIfVisible(5000);
testLogger.info(`Traces FROM suggestions: ${labels.slice(0, 8).join(', ')}`);
// FROM context should NOT contain SQL keywords — only stream names
const hasSqlKeyword = labels.some(l =>
['and', 'or', 'like', 'between'].includes(l.toLowerCase())
);
expect(hasSqlKeyword).toBe(false);
testLogger.info('Traces SQL FROM context: SQL keywords absent — confirmed');
} else {
testLogger.warn('Suggestions not visible in traces FROM — skipping assertion');
}
} else {
testLogger.warn('Traces query editor not found — test inconclusive');
}
testLogger.info('Traces SQL FROM stream suggestions test PASSED');
});
});