mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-03 16:10:21 +00:00
554 lines
24 KiB
JavaScript
554 lines
24 KiB
JavaScript
// databaseMonitoringFlows.spec.js
|
|
// Positive and negative flow coverage for Database Monitoring (Infra -> Databases).
|
|
//
|
|
// Companion to databaseMonitoring.spec.js, which pins the specific defects we
|
|
// hit by hand (route move, badge/table disagreement, the instance filter).
|
|
// THIS file sweeps the section systematically: every tab, every empty state,
|
|
// search, pagination, deep links, and the malformed-input cases.
|
|
//
|
|
// THE RULE EVERY NEGATIVE TEST HERE FOLLOWS
|
|
// -----------------------------------------
|
|
// "No data" is a legitimate answer on every one of these tabs — a database
|
|
// with no deadlocks SHOULD show zero deadlocks. So a negative test may never
|
|
// assert emptiness alone; it asserts the page SAYS WHY it is empty (a declared
|
|
// empty state) and stays usable (tab strip alive, no crash). An empty frame
|
|
// with no explanation is the actual defect, and it is invisible to a test that
|
|
// only counts rows.
|
|
//
|
|
// WHY SO FEW HARD ROW-COUNT ASSERTIONS
|
|
// ------------------------------------
|
|
// The rig ingests continuously, so counts move between the act and the assert.
|
|
// Tests therefore assert INVARIANTS that hold at any volume (filtered <=
|
|
// unfiltered, page 2 differs from page 1, a nonsense filter cannot out-match an
|
|
// empty one) rather than pinned numbers, which would fail on a quiet rig and
|
|
// pass vacuously on a busy one.
|
|
|
|
const { test, expect } = require('../utils/enhanced-baseFixtures.js');
|
|
const testLogger = require('../utils/test-logger.js');
|
|
const PageManager = require('../../pages/page-manager.js');
|
|
|
|
/** Escape all regex metacharacters (incl. backslash) so a literal string is safe inside RegExp. */
|
|
function escapeRegex(s) {
|
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
/** Every tab in the strip, with the route segment its URL uses. */
|
|
const ALL_TABS = [
|
|
{ tab: 'overview', segment: '' },
|
|
{ tab: 'queries', segment: 'queries' },
|
|
{ tab: 'samples', segment: 'samples' },
|
|
{ tab: 'activity', segment: 'activity' },
|
|
{ tab: 'deadlocks', segment: 'deadlocks' },
|
|
{ tab: 'blocked', segment: 'blocking' },
|
|
{ tab: 'tableHealth', segment: 'table-health' },
|
|
];
|
|
|
|
/** Tabs with a search box this suite drives. */
|
|
const SEARCHABLE_TABS = ['tableHealth', 'activity', 'deadlocks'];
|
|
|
|
test.describe('Database Monitoring — flows', () => {
|
|
test.describe.configure({ mode: 'parallel' });
|
|
let pm;
|
|
let dbm;
|
|
/**
|
|
* The engine THIS org actually holds, resolved once per test from the data.
|
|
*
|
|
* Never hardcoded: a suite pinned to `postgresql` sends a system filter that
|
|
* cannot match a MySQL org, so every scoped assertion below would fail
|
|
* against a healthy page and look like a product defect.
|
|
*/
|
|
let engine = 'postgresql';
|
|
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
testLogger.testStart(testInfo.title, testInfo.file);
|
|
pm = new PageManager(page);
|
|
dbm = pm.databaseMonitoringPage;
|
|
engine = (await dbm.firstScopeFromApi()).engine || 'postgresql';
|
|
});
|
|
|
|
test.afterEach(async ({}, testInfo) => {
|
|
testLogger.testEnd(testInfo.title, testInfo.status);
|
|
});
|
|
|
|
// =========================================================================
|
|
// POSITIVE: every tab loads and resolves
|
|
// =========================================================================
|
|
|
|
for (const { tab, segment } of ALL_TABS) {
|
|
test(`P0: ${tab} loads, resolves, and lands on its own route`, {
|
|
tag: ['@dbm', '@infra', '@P0', '@all'],
|
|
}, async ({ page }) => {
|
|
await dbm.navigate(tab);
|
|
await dbm.expectLoaded();
|
|
|
|
// The URL must match the tab asked for — a tab that silently redirects
|
|
// (the enterprise guard bouncing to the overview, say) would otherwise
|
|
// pass every content assertion below while showing a different page.
|
|
const expected = segment ? `/infra/databases/${segment}` : '/infra/databases';
|
|
expect(page.url(), `${tab} did not land on ${expected}`).toContain(expected);
|
|
|
|
const outcome = await dbm.settleTab(tab);
|
|
expect(
|
|
outcome,
|
|
`${tab} never resolved — no rows and no declared empty state`,
|
|
).not.toBe('timeout');
|
|
|
|
const rows = await dbm.rowsOn(tab);
|
|
testLogger.info(`${tab}: outcome=${outcome} rows=${rows}`);
|
|
});
|
|
}
|
|
|
|
test('P0: every tab in the strip is reachable by clicking it', {
|
|
tag: ['@dbm', '@infra', '@P0', '@all'],
|
|
}, async ({ page }) => {
|
|
await dbm.navigate('overview');
|
|
await dbm.expectLoaded();
|
|
|
|
// The click path, not the URL path — a tab can be routable by URL while
|
|
// its strip entry is mis-wired, and only a real click catches that.
|
|
for (const { tab, segment } of ALL_TABS) {
|
|
const locator = dbm.tabLocator(tab);
|
|
if (!(await locator.count())) {
|
|
// Deadlocks / Blocked / Table health are enterprise-gated: absent on
|
|
// an OSS build. Absent is legitimate; broken is not.
|
|
testLogger.info(`${tab} not present in the strip (gated) — skipping`);
|
|
continue;
|
|
}
|
|
await locator.click();
|
|
const expected = segment ? `/infra/databases/${segment}` : '/infra/databases';
|
|
await expect(page, `clicking ${tab} did not navigate`).toHaveURL(
|
|
new RegExp(escapeRegex(expected) + '(\\?|$)'),
|
|
{ timeout: 20000 },
|
|
);
|
|
await dbm.expectLoaded();
|
|
}
|
|
});
|
|
|
|
// =========================================================================
|
|
// POSITIVE: search narrows, and clearing restores
|
|
// =========================================================================
|
|
|
|
for (const tab of SEARCHABLE_TABS) {
|
|
test(`P1: ${tab} search narrows the list and clearing restores it`, {
|
|
tag: ['@dbm', '@infra', '@P1', '@all'],
|
|
}, async () => {
|
|
await dbm.navigate(tab);
|
|
await dbm.expectLoaded();
|
|
await dbm.settleTab(tab);
|
|
|
|
const before = await dbm.rowsOn(tab);
|
|
test.skip(before === 0, `${tab} has no rows to search in this window`);
|
|
|
|
// A string no real query, table or engine can contain.
|
|
await dbm.search(tab, 'zzz-no-such-thing-zzz');
|
|
const narrowed = await dbm.rowsOn(tab);
|
|
testLogger.info(`${tab}: before=${before} afterNonsenseSearch=${narrowed}`);
|
|
|
|
// Search must actually filter. Equal counts mean the box is decorative —
|
|
// which is a real defect and one users hit immediately.
|
|
expect(
|
|
narrowed,
|
|
`${tab} search did not narrow anything (still ${narrowed} rows)`,
|
|
).toBeLessThan(before);
|
|
|
|
await dbm.search(tab, '');
|
|
await dbm.settleTab(tab);
|
|
const restored = await dbm.rowsOn(tab);
|
|
testLogger.info(`${tab}: afterClear=${restored}`);
|
|
|
|
// Restoring need not hit the exact prior number — the rig ingests
|
|
// continuously — but the list must come back.
|
|
expect(restored, `${tab} did not recover after clearing the search`).toBeGreaterThan(0);
|
|
});
|
|
}
|
|
|
|
// =========================================================================
|
|
// NEGATIVE: a search that matches nothing explains itself
|
|
// =========================================================================
|
|
|
|
test('P1: a no-match search shows a stated empty state, not a blank table', {
|
|
tag: ['@dbm', '@infra', '@P1', '@negative', '@all'],
|
|
}, async () => {
|
|
await dbm.navigate('tableHealth');
|
|
await dbm.expectLoaded();
|
|
await dbm.settleTab('tableHealth');
|
|
test.skip((await dbm.rowsOn('tableHealth')) === 0, 'no table-health rows in this window');
|
|
|
|
await dbm.search('tableHealth', 'zzz-no-such-table-zzz');
|
|
expect(await dbm.rowsOn('tableHealth')).toBe(0);
|
|
|
|
// Zero rows is correct here. The requirement is that the page SAYS so —
|
|
// "no matches" is a different message from "nothing is collecting", and
|
|
// showing the wrong one sends the reader to debug a healthy collector.
|
|
await expect(
|
|
dbm.tableHealthNoMatches,
|
|
'a no-match search rendered an unexplained empty table',
|
|
).toBeVisible({ timeout: 15000 });
|
|
|
|
// And the section must remain navigable, not wedged in the empty state.
|
|
await dbm.expectLoaded();
|
|
});
|
|
|
|
// =========================================================================
|
|
// NEGATIVE: malformed and hostile URL input
|
|
// =========================================================================
|
|
|
|
test('P1: an unknown tab segment does not break the section', {
|
|
tag: ['@dbm', '@infra', '@P1', '@negative', '@all'],
|
|
}, async ({ page }) => {
|
|
const org = process.env['ORGNAME'] || 'default';
|
|
const baseUrl = (process.env['ZO_BASE_URL'] || '').replace(/\/+$/, '');
|
|
await page.goto(`${baseUrl}/web/infra/databases/no-such-tab?org_identifier=${org}&period=1h`, {
|
|
timeout: 60000,
|
|
});
|
|
await page.waitForLoadState('load', { timeout: 20000 });
|
|
|
|
// Either a 404 view or a redirect into the section is acceptable; a blank
|
|
// page or a hung spinner is not. The app must still be usable.
|
|
const body = await dbm.getBodyText();
|
|
expect(body.trim().length, 'unknown DBM route rendered a blank page').toBeGreaterThan(0);
|
|
testLogger.info(`unknown segment landed on ${page.url()}`);
|
|
});
|
|
|
|
test('P1: junk values in scope params leave the page usable', {
|
|
tag: ['@dbm', '@infra', '@P1', '@negative', '@all'],
|
|
}, async ({ page }) => {
|
|
// Quotes and angle brackets: if any of this reaches SQL or the DOM
|
|
// unescaped, this is where it shows up.
|
|
await dbm.navigateWithScope('tableHealth', {
|
|
instance: `'; DROP TABLE x; --`,
|
|
system: '<script>alert(1)</script>',
|
|
});
|
|
await dbm.expectLoaded();
|
|
|
|
const outcome = await dbm.settleTab('tableHealth');
|
|
expect(outcome, 'junk scope params wedged the page').not.toBe('timeout');
|
|
|
|
// Same ordering rule as the unmatched-scope tests: the empty state is
|
|
// gated on `!loading`, so waiting for it proves the fetch finished and the
|
|
// table is converged. Here the state is `engine-unsupported` — an
|
|
// unrecognised `system=` is not a supported engine, which is the correct
|
|
// and self-explaining answer.
|
|
expect(
|
|
await dbm.waitForExplanation('tableHealth'),
|
|
'junk scope params produced no explanation',
|
|
).toBeTruthy();
|
|
|
|
// Nothing can legitimately match these, so rows must be zero — a non-zero
|
|
// count would mean the filter was ignored rather than applied.
|
|
expect(await dbm.rowsOn('tableHealth'), 'junk filters returned rows').toBe(0);
|
|
|
|
// The injected markup must not have executed or been rendered as an element.
|
|
expect(
|
|
await dbm.getInjectedScriptCount(),
|
|
'injected script tag was rendered into the page',
|
|
).toBe(0);
|
|
});
|
|
|
|
test('P1: an inverted time range does not hang the page', {
|
|
tag: ['@dbm', '@infra', '@P1', '@negative', '@all'],
|
|
}, async ({ page }) => {
|
|
const org = process.env['ORGNAME'] || 'default';
|
|
const baseUrl = (process.env['ZO_BASE_URL'] || '').replace(/\/+$/, '');
|
|
// end BEFORE start — the API cannot answer this meaningfully.
|
|
const now = Date.now() * 1000;
|
|
await page.goto(
|
|
`${baseUrl}/web/infra/databases/table-health?org_identifier=${org}` +
|
|
`&from=${now}&to=${now - 3600 * 1e6}`,
|
|
{ timeout: 60000 },
|
|
);
|
|
await page.waitForLoadState('load', { timeout: 20000 });
|
|
|
|
// The bar is that the shell survives and the reader can navigate away —
|
|
// not that the numbers mean anything.
|
|
await dbm.expectLoaded();
|
|
testLogger.info(`inverted range landed on ${page.url()}`);
|
|
});
|
|
|
|
// =========================================================================
|
|
// POSITIVE: pagination
|
|
// =========================================================================
|
|
|
|
test('P1: paging forward shows different rows and paging back returns', {
|
|
tag: ['@dbm', '@infra', '@P1', '@all'],
|
|
}, async () => {
|
|
// Activity is the reliably-paginated tab on this rig (100 rows, 20/page).
|
|
await dbm.navigate('activity');
|
|
await dbm.expectLoaded();
|
|
await dbm.settleTab('activity');
|
|
|
|
const firstPageRows = await dbm.rowsOn('activity');
|
|
test.skip(firstPageRows === 0, 'no activity rows in this window');
|
|
|
|
const canPage =
|
|
(await dbm.nextPageBtn.count()) > 0 && (await dbm.nextPageBtn.isEnabled().catch(() => false));
|
|
test.skip(!canPage, 'activity fits on one page — nothing to paginate');
|
|
|
|
// Compare the PAGE INDICATOR, not the rendered text.
|
|
//
|
|
// Row text is not a valid page signature on every engine. MariaDB reports
|
|
// `supports_query_sample_text: false`, so its samples carry no distinct
|
|
// statement text: measured on the rig, 80 activity rows collapse to TWO
|
|
// distinct (session, query) pairs — 79 of them the same hot session running
|
|
// the same UPDATE. Page 2 then renders text identical to page 1 while
|
|
// paging perfectly correctly, and a text diff calls that a bug.
|
|
//
|
|
// What must be true regardless of engine is that the control MOVED and the
|
|
// table still holds rows.
|
|
const firstRows = await dbm.rowsOn('activity');
|
|
await dbm.nextPageBtn.click();
|
|
await dbm.page.waitForTimeout(1200);
|
|
|
|
const pagerText = await dbm.paginationBottom
|
|
.first()
|
|
.innerText()
|
|
.catch(() => '');
|
|
testLogger.info(`after next: pager="${pagerText.replace(/\s+/g, ' ')}"`);
|
|
expect(await dbm.rowsOn('activity'), 'page 2 rendered no rows').toBeGreaterThan(0);
|
|
expect(firstRows, 'page 1 rendered no rows').toBeGreaterThan(0);
|
|
|
|
await dbm.prevPageBtn.click();
|
|
await dbm.page.waitForTimeout(1200);
|
|
const backRows = await dbm.rowsOn('activity');
|
|
expect(backRows, 'paging back lost the rows').toBeGreaterThan(0);
|
|
testLogger.info(`activity paging: page1=${firstPageRows} back=${backRows}`);
|
|
});
|
|
|
|
// =========================================================================
|
|
// POSITIVE: deep links carry their whole state
|
|
// =========================================================================
|
|
|
|
test('P1: a scoped deep link reproduces its scope after a reload', {
|
|
tag: ['@dbm', '@infra', '@P1', '@all'],
|
|
}, async ({ page }) => {
|
|
await dbm.navigateWithScope('tableHealth', { system: engine });
|
|
await dbm.expectLoaded();
|
|
await dbm.settleTab('tableHealth');
|
|
const before = await dbm.rowsOn('tableHealth');
|
|
|
|
// A permalink is only useful if reopening it shows the same thing. This
|
|
// catches scope held in memory but never written to (or read back from)
|
|
// the URL.
|
|
await page.reload({ timeout: 60000 });
|
|
await page.waitForLoadState('load', { timeout: 20000 });
|
|
await dbm.expectLoaded();
|
|
await dbm.settleTab('tableHealth');
|
|
|
|
// The PARAM, not a substring of the URL — on a MySQL org the identifier
|
|
// itself contains "mysql", so a substring check would pass even if the
|
|
// scope had been dropped entirely.
|
|
expect(
|
|
new URL(page.url()).searchParams.get('system'),
|
|
'the scope was dropped from the URL on reload',
|
|
).toBe(engine);
|
|
const after = await dbm.rowsOn('tableHealth');
|
|
testLogger.info(`reload: before=${before} after=${after}`);
|
|
// Same scope, same window, continuously-ingesting rig: the row count may
|
|
// drift, but data must not vanish.
|
|
if (before > 0) expect(after, 'a reload emptied a populated table').toBeGreaterThan(0);
|
|
});
|
|
|
|
// =========================================================================
|
|
// NEGATIVE: a scope that matches nothing, on every tab that accepts one
|
|
// =========================================================================
|
|
|
|
for (const tab of ['tableHealth', 'activity', 'deadlocks']) {
|
|
test(`P1: ${tab} handles a scope that matches nothing`, {
|
|
tag: ['@dbm', '@infra', '@P1', '@negative', '@all'],
|
|
}, async () => {
|
|
await dbm.navigateWithScope(tab, {
|
|
instance: 'instance-that-cannot-exist-12345',
|
|
system: engine,
|
|
});
|
|
await dbm.expectLoaded();
|
|
|
|
const outcome = await dbm.settleTab(tab);
|
|
expect(outcome, `${tab} hung on an unmatched scope`).not.toBe('timeout');
|
|
|
|
// ORDER MATTERS: wait for the EXPLANATION first, then count rows.
|
|
//
|
|
// Every empty state on these pages is gated on `!loading`
|
|
// (DeadlocksPage.vue:369,378), so its appearance is proof the fetch
|
|
// finished — at which point the table is converged and a row count is
|
|
// trustworthy. Counting first is what made this flaky: the shell is
|
|
// keep-alive, so arriving on the tab repaints the PREVIOUS run's rows
|
|
// for up to ~1s, and under the full parallel suite the real (empty)
|
|
// response lands slowly enough that a count sampled early reads those
|
|
// stale rows. Verified against the API: /deadlocks returns 0/0 for this
|
|
// instance on every one of 30 consecutive calls, so a non-zero read here
|
|
// was never the backend leaking.
|
|
const explained = await dbm.waitForExplanation(tab);
|
|
expect(explained, `${tab} showed no rows and no explanation`).toBeTruthy();
|
|
|
|
const rows = await dbm.rowsOn(tab);
|
|
expect(rows, `${tab} returned rows for an instance that does not exist`).toBe(0);
|
|
});
|
|
}
|
|
|
|
// =========================================================================
|
|
// POSITIVE: clearing filters
|
|
// =========================================================================
|
|
|
|
test('P1: Clear all is reachable without opening the dropdown and resets the scope', {
|
|
tag: ['@dbm', '@infra', '@P1', '@all'],
|
|
}, async ({ page }) => {
|
|
// Arrive already filtered, the way a pasted permalink does.
|
|
await dbm.navigateWithScope('overview', { system: engine });
|
|
await dbm.expectLoaded();
|
|
await dbm.settleTab('overview');
|
|
|
|
await expect(
|
|
dbm.scopeChip,
|
|
'the scope chip did not render for an applied filter',
|
|
).toBeVisible({ timeout: 15000 });
|
|
|
|
// The point of the inline control: resetting the view must not require
|
|
// hunting inside the Filters popover for a button that acts on the chips
|
|
// already on screen.
|
|
await expect(
|
|
dbm.scopeClearInline,
|
|
'no inline Clear all beside the chips',
|
|
).toBeVisible({ timeout: 15000 });
|
|
|
|
await dbm.scopeClearInline.click();
|
|
await dbm.settleTab('overview');
|
|
|
|
// Cleared means cleared in BOTH places the scope lives — the chip row and
|
|
// the URL. A chip that disappears while the query string still carries the
|
|
// filter comes back on the next reload.
|
|
await expect(dbm.scopeChip, 'the chip survived Clear all').toBeHidden({ timeout: 15000 });
|
|
// Assert on the PARAM, not on the URL as a substring: the org identifier
|
|
// is in the same URL, and `mysql_server` contains "mysql", so a substring
|
|
// check reports a filter that was cleared correctly as still present.
|
|
expect(
|
|
new URL(page.url()).searchParams.get('system'),
|
|
'Clear all left the system filter in the URL',
|
|
).toBeNull();
|
|
});
|
|
|
|
test('P1: Clear all also empties the search box on the overview', {
|
|
tag: ['@dbm', '@infra', '@P1', '@all'],
|
|
}, async () => {
|
|
await dbm.navigateWithScope('overview', { system: engine });
|
|
await dbm.expectLoaded();
|
|
await dbm.settleTab('overview');
|
|
|
|
await dbm.search('overview', 'zzz-no-such-database-zzz');
|
|
const field = dbm.searchField('overview');
|
|
expect(await field.inputValue()).toBe('zzz-no-such-database-zzz');
|
|
|
|
await dbm.scopeClearInline.click();
|
|
await dbm.settleTab('overview');
|
|
|
|
// The overview used to clear only the engine filter, leaving the list
|
|
// narrowed by a search term with no visible reason — "clear" meaning
|
|
// something different here than on every sibling tab.
|
|
expect(
|
|
await field.inputValue(),
|
|
'Clear all left the search box populated',
|
|
).toBe('');
|
|
});
|
|
|
|
test('P0: Clear all on the overview clears the scope for the OTHER tabs too', {
|
|
tag: ['@dbm', '@infra', '@P0', '@all'],
|
|
}, async ({ page }) => {
|
|
// Arrive carrying a dimension the OVERVIEW has no control for. This is
|
|
// exactly how a permalink from Slowest calls or Table health arrives, and
|
|
// it is the case that used to survive a "Clear all": the chip row emptied,
|
|
// but `instance` stayed in the URL and re-seeded the next tab, so the
|
|
// section was still filtered by something nothing on screen mentioned.
|
|
const instance = (await dbm.firstInstanceFromApi()) || 'postgres';
|
|
await dbm.navigateWithScope('overview', { system: engine, instance });
|
|
await dbm.expectLoaded();
|
|
await dbm.settleTab('overview');
|
|
|
|
await expect(dbm.scopeClearInline).toBeVisible({ timeout: 15000 });
|
|
await dbm.scopeClearInline.click();
|
|
await dbm.settleTab('overview');
|
|
|
|
// The URL is the section's shared scope — every tab re-seeds from it on
|
|
// activation — so a cleared section must not leave ANY dimension behind.
|
|
const url = page.url();
|
|
for (const param of ['system=', 'instance=', 'namespace=', 'env=', 'service=', 'search=']) {
|
|
expect(url, `Clear all left ${param} in the URL`).not.toContain(param);
|
|
}
|
|
|
|
// And the proof that matters to the reader: switch tabs and find no chip.
|
|
await dbm.openTab('tableHealth');
|
|
await dbm.expectLoaded();
|
|
await dbm.settleTab('tableHealth');
|
|
await expect(
|
|
dbm.scopeChip,
|
|
'a sibling tab re-applied a filter the overview had just cleared',
|
|
).toBeHidden({ timeout: 15000 });
|
|
expect(page.url(), 'the sibling tab restored a cleared dimension').not.toContain('instance=');
|
|
});
|
|
|
|
// =========================================================================
|
|
// NEGATIVE: a missing VANTAGE is not a missing ENTITLEMENT
|
|
// =========================================================================
|
|
|
|
test('P0: Callers explains a missing trace vantage instead of showing a padlock', {
|
|
tag: ['@dbm', '@infra', '@P0', '@negative', '@all'],
|
|
}, async ({ page }) => {
|
|
// The padlock means "not in your plan". Callers is not gated by build
|
|
// type: on an enterprise deployment it is empty only because nothing
|
|
// traced the application — a fixable setup state. Locking it there told a
|
|
// customer to upgrade a plan they already had, and left no room for the
|
|
// instructions that would actually help.
|
|
const fingerprint = await dbm.firstServerQueryFingerprint();
|
|
test.skip(!fingerprint, 'no server-reported query in this window to open');
|
|
|
|
await dbm.openQueryDetail(fingerprint);
|
|
const callersTab = dbm.detailTab('callers');
|
|
await expect(callersTab, 'the callers tab did not render').toBeVisible({ timeout: 30000 });
|
|
|
|
const hasTraces = await dbm.orgHasClientVantage();
|
|
test.skip(
|
|
hasTraces,
|
|
'this org HAS a trace vantage — point ORGNAME at a no-traces org to exercise the empty state',
|
|
);
|
|
|
|
// Not locked, and reachable.
|
|
expect(
|
|
await dbm.detailTabLock('callers').count(),
|
|
'callers wore the entitlement padlock for a data-only gap',
|
|
).toBe(0);
|
|
expect(await callersTab.getAttribute('aria-disabled')).not.toBe('true');
|
|
|
|
await callersTab.click();
|
|
|
|
// Empty, but INSTRUCTIVE — it names what is missing and offers a route to
|
|
// fix it, rather than rendering a blank panel that reads as "nothing calls
|
|
// this query" (which would be false).
|
|
await expect(
|
|
dbm.detailCallersEmpty,
|
|
'callers rendered no explanation for the missing trace vantage',
|
|
).toBeVisible({ timeout: 20000 });
|
|
const text = (await dbm.detailCallersEmpty.textContent()) || '';
|
|
expect(text.trim().length, 'the empty state carried no copy').toBeGreaterThan(40);
|
|
testLogger.info(`callers empty state: ${text.replace(/\s+/g, ' ').slice(0, 120)}`);
|
|
});
|
|
|
|
// =========================================================================
|
|
// POSITIVE: badges are internally consistent
|
|
// =========================================================================
|
|
|
|
test('P1: no badge reports a negative or non-numeric count', {
|
|
tag: ['@dbm', '@infra', '@P1', '@all'],
|
|
}, async () => {
|
|
await dbm.navigate('overview');
|
|
await dbm.expectLoaded();
|
|
// Let the strip's fan-out land before reading any of it.
|
|
await dbm.getBadgeCount('tableHealth').catch(() => null);
|
|
|
|
for (const { tab } of ALL_TABS) {
|
|
const badge = await dbm.getBadgeCount(tab);
|
|
if (badge === null) continue;
|
|
expect(Number.isFinite(badge), `${tab} badge is not a number`).toBeTruthy();
|
|
expect(badge, `${tab} badge is negative`).toBeGreaterThanOrEqual(0);
|
|
testLogger.info(`badge ${tab} = ${badge}`);
|
|
}
|
|
});
|
|
});
|