Files
openobserve/tests/ui-testing/playwright-tests/Workflows/workflows-negative.spec.js
T
4f02b20d4a fix: workflow ux improvements - 2 (#14027)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ashish Kolhe <ashish.j.kolhe@gmail.com>
2026-09-03 10:16:24 +00:00

266 lines
13 KiB
JavaScript

/**
* Workflows v1 — Negative / validation sweep (CT-18) + NFR sweep (CT-19)
*
* Enterprise-only feature: event(alert_fired) -> action(remote/pipeline destination).
* Plan: tests/ui-testing/MD_Files/features/workflows/06-consolidated-suite.md (CT-18, CT-19).
* Findings this spec asserts against (04-execution-log.md):
* F-05 (NEG-02) ✅ — trigger-only save is BLOCKED: "Add At Least One Step After The Trigger".
* F-01 (K10) — GET /workflows list is slow (~16-20s live). We time it and soft-assert < 60s.
* F-02 (K9) — Save button tooltip intercepts pointer events; the page object bypasses it
* with a JS .click() (clickSave()). We assert that JS-click path works and
* document (as fixme) that a plain .click() is intercepted.
*
* Self-cleaning: every artifact is namespaced `wf_auto_neg_*`; cleanup.spec.js sweeps by prefix.
* No ingestion needed here (UI validation + list timing + console/network hygiene only).
*
* Known quirks handled by the page object: K9 (Save tooltip intercept -> JS click), K10 (~18s list load).
*/
const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
const testLogger = require('../utils/test-logger.js');
const PageManager = require('../../pages/page-manager.js');
const uniq = () => `${Date.now().toString(36).slice(-5)}${Math.random().toString(36).slice(2, 5)}`;
test.describe.configure({ mode: 'parallel' });
test.describe(
'Workflows negative & NFR sweep',
{ tag: ['@workflows', '@workflowsNegative', '@enterprise', '@all'] },
() => {
let pm;
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
await navigateToBase(page);
pm = new PageManager(page);
// Enterprise-only feature: these specs run ONLY in the ENT playwright matrix (never wired into
// OSS), where Workflows is enabled by default. No runtime availability skip — if the feature is
// missing where this runs, the test must fail loudly rather than silently pass as skipped.
await pm.workflowsPage.assertEnabled();
});
// =====================================================================
// CT-18 — Negative & validation sweep (UI)
// Covers NEG-01 (empty name), NEG-02 (no-sink), NEG-04 (long/unicode/emoji names),
// NEG-05 (invalid function code -> graceful compile error) and NEG-06 (dead destination
// URL -> per-node send error, via a Test-run).
// =====================================================================
// NEG-02 — CONFIRMED (F-05): a trigger-only workflow cannot be saved; the app requires
// at least one downstream step. This is a real, working validation -> hard assertion.
test(
'CT-18 NEG-02: rejects saving a trigger-only workflow (no sink)',
{ tag: ['@workflowsNegative'] },
async () => {
await pm.workflowsPage.goToAdd();
await pm.workflowsPage.setName(`wf_auto_neg_${uniq()}`);
const msg = await pm.workflowsPage.saveAndCaptureResult();
testLogger.info('trigger-only save result', { msg });
// F-05: exact confirmed copy is "Add At Least One Step After The Trigger".
expect(msg.toLowerCase()).toContain('at least one step');
}
);
// NEG-01 — empty name must block save (either name-required or the no-step validation fires;
// either way, save must NOT silently succeed and a message must be surfaced).
test(
'CT-18 NEG-01: empty name is blocked on save',
{ tag: ['@workflowsNegative'] },
async () => {
await pm.workflowsPage.goToAdd();
// Leave the name empty; attempt to save.
const msg = await pm.workflowsPage.saveAndCaptureResult();
testLogger.info('empty-name save result', { msg });
// A validation message must appear; the editor must stay on the add page (no navigation away).
expect(msg.length).toBeGreaterThan(0);
await pm.workflowsPage.expectEditorVisible();
}
);
// NEG-04 — very long / unicode / emoji names must be handled without a crash.
// We type each into the name field and assert the app does not blow up: the editor stays
// rendered, the value round-trips into the field, and a save attempt returns a graceful
// message (rather than an uncaught error / blank page).
for (const variant of [
{ label: 'very-long', value: `wf_auto_neg_${uniq()}_${'a'.repeat(300)}` },
{ label: 'unicode', value: `wf_auto_neg_${uniq()}_テステ」キ_数据_Ω≈ç√` },
{ label: 'emoji', value: `wf_auto_neg_${uniq()}_🚀🔥✅🧪` },
]) {
test(
`CT-18 NEG-04: handles ${variant.label} name without crashing`,
{ tag: ['@workflowsNegative'] },
async () => {
await pm.workflowsPage.goToAdd();
await pm.workflowsPage.setName(variant.value);
// Value round-trips into the field (no truncation crash / input rejection blow-up).
await pm.workflowsPage.expectNameValue(variant.value);
// Attempt save; app must respond gracefully (a message toast, no uncaught crash).
const msg = await pm.workflowsPage.saveAndCaptureResult();
testLogger.info(`${variant.label} name save result`, { msg });
// Editor must still be rendered (app did not crash to a blank/error page).
await pm.workflowsPage.expectEditorVisible();
}
);
}
// NEG-05 — invalid function code is rejected gracefully. Workflow functions run on QuickJS
// (JavaScript); feeding the inline function editor code that fails to compile must surface a
// graceful "JS compilation failed" message and keep the editor rendered — never an uncaught
// app crash. (This is the deterministic guard; runtime-abort surfacing is covered separately.)
test(
'CT-18 NEG-05: invalid function code surfaces a graceful error',
{ tag: ['@workflowsNegative'] },
async () => {
const id = uniq();
await pm.workflowsPage.goToAdd();
await pm.workflowsPage.setName(`wf_auto_neg_${id}`);
await pm.workflowsPage.addNodeFromPalette('function');
const msg = await pm.workflowsPage.attemptCreateFunction({
name: `wf_auto_fn_${id}`,
code: 'function ((( this is not valid javascript', // fails QuickJS compilation
});
testLogger.info('invalid function save result', { msg });
// A graceful compilation-error message is surfaced (no uncaught crash).
expect(msg.toLowerCase()).toMatch(/compil|exception|fail|error/);
// The editor is still rendered — the app degraded gracefully.
await pm.workflowsPage.expectEditorVisible();
}
);
// NEG-06 — dead / invalid destination URL surfaces a per-node send error. Build
// Trigger -> Destination pointed at an unreachable URL, save, then Test-run the graph and
// assert the destination node paints an error badge while the trigger paints ✓.
test(
'CT-18 NEG-06: dead destination URL surfaces a per-node send error',
{ tag: ['@workflowsNegative'] },
async () => {
const id = uniq();
const name = `wf_auto_neg_${id}`;
await pm.workflowsPage.buildTriggerToDestinationAndSave({
name,
destName: `wf_auto_dest_${id}`,
url: 'http://127.0.0.1:1/dead', // closed port -> immediate connection-refused (fast, not a hang)
});
await pm.workflowsPage.goToList();
await pm.workflowsPage.openEdit(name);
await pm.workflowsPage.testRunFromEditor({ liveSend: true });
// The destination step fails to send; the trigger step still succeeds.
await pm.workflowsPage.expectNodeTestError('destination');
await pm.workflowsPage.expectNodeTestPassed('workflow_trigger');
}
);
// =====================================================================
// CT-19 — NFR sweep (Mixed)
// Covers REG-06/K10 (list latency), REG-05/K9 (Save-button interception),
// NFR-03 (console/network hygiene).
// =====================================================================
// K10 (F-01) — quantify the workflows list load time. We wrap goToList() in a timer,
// assert the list eventually renders, log the ms, and soft-assert it stays under 60s.
// (Live it is ~16-20s; this documents the perf risk without being CI-brittle.)
test(
'CT-19 K10: measures workflows list load latency',
{ tag: ['@workflowsNegative'] },
async () => {
const start = Date.now();
await pm.workflowsPage.goToList();
const loadMs = Date.now() - start;
testLogger.info('workflows list load latency', { loadMs, note: 'K10 perf risk (~16-20s live)' });
// It must render at all.
await pm.workflowsPage.expectListVisible();
// Soft upper bound — flags a hang/regression, not the known ~18s slowness.
expect(loadMs).toBeLessThan(60000);
}
);
// K9 (F-02) — the editor Save button's tooltip overlay intercepts pointer events.
// The page object's clickSave() bypasses it with a JS .click(). Here we assert that
// JS-click path actually triggers the app's save/validation flow (a toast comes back).
test(
'CT-19 K9: Save works via the JS-click path (tooltip-intercept workaround)',
{ tag: ['@workflowsNegative'] },
async () => {
await pm.workflowsPage.goToAdd();
await pm.workflowsPage.setName(`wf_auto_neg_${uniq()}`);
// clickSave() uses evaluate()+.click() to dodge the o-tooltip-content overlay (K9).
const msg = await pm.workflowsPage.saveAndCaptureResult();
testLogger.info('K9 JS-click save produced a response', { msg });
// The JS-click reached the app: it ran validation (trigger-only) and surfaced a message.
expect(msg.length).toBeGreaterThan(0);
}
);
// K9 — the page object saves via a JS .click() because a stray tooltip overlay can cover
// Save and swallow a normal Playwright .click(). Save has no tooltip of its own
// (WorkflowEditor.vue), so that intercepting overlay is transient/adjacent and does NOT
// reproduce deterministically in headless CI — a hard assert on interception flakes (it
// passed on slow runs, failed on fast ones). So the interception is logged as an
// informational canary, and the real assertion is the stable one: the JS-click save
// still reaches the app. If K9 is ever fixed the canary log shows `intercepted:false`
// consistently — the signal to drop the workaround from the page object.
test(
'CT-19 K9: JS-click Save workaround reaches the app (tooltip-overlay canary)',
{ tag: ['@workflowsNegative'] },
async () => {
await pm.workflowsPage.goToAdd();
await pm.workflowsPage.setName(`wf_auto_neg_${uniq()}`);
// Informational only — the intercepting overlay is non-deterministic in CI, so this
// is logged, not asserted (see comment above).
const intercepted = await pm.workflowsPage.normalSaveClickIsIntercepted(4000);
testLogger.info('K9 canary: normal Save click intercepted by overlay?', { intercepted });
// The JS-click workaround still reaches the app (validation toast comes back).
const msg = await pm.workflowsPage.saveAndCaptureResult();
expect(msg.length).toBeGreaterThan(0);
}
);
// NFR-03 — console / network hygiene on a create+list flow: no uncaught JS errors,
// no 5xx responses. (4xx from expected validation is tolerated; we only fail on 5xx.)
test(
'CT-19 NFR-03: no uncaught JS errors or 5xx during a create+list flow',
{ tag: ['@workflowsNegative'] },
async ({ page }) => {
const consoleErrors = [];
const serverErrors = [];
page.on('console', (msg) => {
if (msg.type() !== 'error') return;
const text = msg.text();
// Chrome logs a "Failed to load resource: … status of 4xx" for every
// 4xx asset fetch. The test's contract (see header) is that 4xx from
// expected validation is tolerated; only real JS errors and 5xx
// matter. Drop resource-load 4xx noise; keep everything else.
if (/Failed to load resource: the server responded with a status of 4\d\d/.test(text)) return;
consoleErrors.push(text);
});
page.on('pageerror', (err) => {
consoleErrors.push(`pageerror: ${err.message}`);
});
page.on('response', (resp) => {
const status = resp.status();
if (status >= 500) serverErrors.push(`${status} ${resp.url()}`);
});
// Exercise the editor (create screen) then the list — the two heaviest screens.
await pm.workflowsPage.goToAdd();
await pm.workflowsPage.setName(`wf_auto_neg_${uniq()}`);
await pm.workflowsPage.goToList();
testLogger.info('console/network hygiene', {
consoleErrorCount: consoleErrors.length,
serverErrorCount: serverErrors.length,
consoleErrors: consoleErrors.slice(0, 10),
serverErrors: serverErrors.slice(0, 10),
});
// No server-side failures on these screens.
expect(serverErrors, `5xx responses: ${serverErrors.join(', ')}`).toEqual([]);
// No uncaught JS errors / pageerrors.
expect(consoleErrors, `console errors: ${consoleErrors.join(' | ')}`).toEqual([]);
}
);
}
);