/** * 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([]); } ); } );