From 71f6702deea4b9493ef979ea0fbfec2acd2f8703 Mon Sep 17 00:00:00 2001 From: Shrinath Rao Date: Fri, 4 Sep 2026 04:36:25 +0000 Subject: [PATCH] test(autoe2e): generated E2E tests for #13978 (condition-null-empty-operators) (#14008) Co-authored-by: Shrinath-O2 <214651540+Shrinath-O2@users.noreply.github.com> --- .../pages/pipelinesPages/pipelinesPage.js | 64 ++++++++- .../Pipelines/pipeline-conditions.spec.js | 129 ++++++++++++++++-- 2 files changed, 184 insertions(+), 9 deletions(-) diff --git a/tests/ui-testing/pages/pipelinesPages/pipelinesPage.js b/tests/ui-testing/pages/pipelinesPages/pipelinesPage.js index a7d4f4f97f..1b526fee01 100644 --- a/tests/ui-testing/pages/pipelinesPages/pipelinesPage.js +++ b/tests/ui-testing/pages/pipelinesPages/pipelinesPage.js @@ -280,7 +280,7 @@ export class PipelinesPage { this.addConditionDeleteBtn = page.locator('[data-test="add-condition-drawer"] [data-test="o-drawer-neutral-btn"]'); this.scheduledAlertTabs = page.locator('[data-test="scheduled-alert-tabs"]'); this.nestedGroups = page.locator('.el-border'); - this.operatorLabels = page.locator('span.tw\\:lowercase'); + this.operatorLabels = page.locator('[data-test="alert-conditions-operator-label"]'); this.firstConditionLabel = page.locator('[data-test="add-condition-section"]').getByText('if', { exact: true }).first(); this.noteContainer = page.locator('[data-test="add-condition-note-container"]'); this.noteHeading = page.locator('[data-test="add-condition-note-heading"]'); @@ -1625,6 +1625,30 @@ export class PipelinesPage { await this.page.waitForTimeout(300); } + async fillUnaryCondition(columnName, operator, index = 0) { + // Select the column (mirror fillCondition's column step) then the unary + // operator. Unary operators (is_null / is_not_null / is_empty / + // is_not_empty) render no value input, so never fill a value here. + const triggers = this.columnSelectTrigger; + const wrappers = this.columnSelect; + const trigCount = await triggers.count(); + if (trigCount > 0) { + await triggers.nth(index).click(); + } else { + await wrappers.nth(index).click(); + } + await this.columnSelectPopover.first().waitFor({ state: 'visible', timeout: 5000 }).catch(() => {}); + const searchCount = await this.columnSelectSearch.count(); + if (searchCount > 0) { + await this.columnSelectSearch.first().fill(columnName); + } + await this.columnOptionByName(columnName).waitFor({ state: 'visible', timeout: 10000 }); + await this.columnOptionByName(columnName).click(); + await this.page.waitForTimeout(300); + + await this.selectOperatorFromMenu(operator); + } + async addNewCondition() { await this.addConditionButton.first().click(); await this.page.waitForTimeout(500); @@ -1921,6 +1945,44 @@ export class PipelinesPage { await this.conditionRequiredToast.first().waitFor({ state: 'visible', timeout: 10000 }); } + async verifyNoConditionError() { + // A successful save surfaces no inline schema error. Used to prove a + // unary condition (value-exempt) saved without the "fill all fields" error. + await expect(this.conditionRequiredToast).toHaveCount(0); + } + + async verifyValueInputCount(expectedCount) { + // Count value inputs (v-if-removed for unary operators, present for binary). + await expect(this.valueInput).toHaveCount(expectedCount); + } + + async verifyOperatorSelected(operator, index = 0) { + // The operator OFormSelect renders in reka-Select mode (non-listbox), so + // its trigger carries no `data-test-selected-value`; it shows the selected + // label as text. Unary operators map wire value -> display label. + const labelByOperator = { + is_null: 'Is Null', + is_not_null: 'Is Not Null', + is_empty: 'Is Empty', + is_not_empty: 'Is Not Empty', + }; + const expectedLabel = labelByOperator[operator] ?? operator; + await expect(this.operatorSelectTrigger.nth(index)).toContainText(expectedLabel); + } + + async verifyUnaryOperatorsOffered() { + // Open the operator menu and assert each unary option is offered by its + // snake_case `data-test-value` (the OSelect option value, not the label). + await this.operatorSelectTrigger.first().waitFor({ state: 'visible', timeout: 15000 }); + await this.operatorSelectTrigger.first().click(); + for (const operator of ["is_null", "is_not_null", "is_empty", "is_not_empty"]) { + const option = this.page.locator( + `[data-test="alert-conditions-operator-select-option"][data-test-value="${operator}"]`, + ).first(); + await expect(option).toBeVisible(); + } + } + async fillPartialCondition(columnName) { // OSelect: click trigger to open, then type in the search input await this.columnSelectTrigger.first().click(); diff --git a/tests/ui-testing/playwright-tests/Pipelines/pipeline-conditions.spec.js b/tests/ui-testing/playwright-tests/Pipelines/pipeline-conditions.spec.js index c5b79ac929..f5d6fdc598 100644 --- a/tests/ui-testing/playwright-tests/Pipelines/pipeline-conditions.spec.js +++ b/tests/ui-testing/playwright-tests/Pipelines/pipeline-conditions.spec.js @@ -274,6 +274,10 @@ test.describe("Pipeline Conditions - Comprehensive Tests", () => { await page.waitForTimeout(300); await pageManager.pipelinesPage.fillCondition("kubernetes_host", "=", "value-B", 1); + // Guard: the OR toggle took effect on condition B's join before we rely on + // the A OR B combination below. + await pageManager.pipelinesPage.verifyOperatorLabel("OR", 0); + // Condition C with AND (default) await pageManager.pipelinesPage.addNewCondition(); await pageManager.pipelinesPage.fillCondition("kubernetes_pod_name", "=", "value-C", 2); @@ -281,11 +285,10 @@ test.describe("Pipeline Conditions - Comprehensive Tests", () => { // Verify 3 conditions exist await pageManager.pipelinesPage.verifyConditionCount(3); - // Verify operators are set correctly - const operatorLabels = pageManager.pipelinesPage.operatorLabels; - if (await operatorLabels.count() >= 2) { - await pageManager.pipelinesPage.verifyOperatorLabel("OR", 0); - } + // Verify BOTH join operators unconditionally: A OR B AND C. The first join + // (B) is OR, the second (C) is AND — no `count() >= 2` guard, both must hold. + await pageManager.pipelinesPage.verifyOperatorLabel("OR", 0); + await pageManager.pipelinesPage.verifyOperatorLabel("AND", 1); await pageManager.pipelinesPage.saveConditionAndCompletePipeline(pipelineName); @@ -389,12 +392,23 @@ test.describe("Pipeline Conditions - Comprehensive Tests", () => { await pageManager.pipelinesPage.addNewCondition(); await pageManager.pipelinesPage.fillCondition("kubernetes_host", "!=", "excluded", 1); + // Add Contains operator condition + await pageManager.pipelinesPage.addNewCondition(); + await pageManager.pipelinesPage.fillCondition("kubernetes_pod_name", "Contains", "include", 2); + // Add NotContains operator condition await pageManager.pipelinesPage.addNewCondition(); - await pageManager.pipelinesPage.fillCondition("kubernetes_pod_name", "NotContains", "ignore", 2); + await pageManager.pipelinesPage.fillCondition("kubernetes_namespace_name", "NotContains", "ignore", 3); - // Verify all 3 conditions exist - await pageManager.pipelinesPage.verifyConditionCount(3); + // Verify all 4 conditions exist + await pageManager.pipelinesPage.verifyConditionCount(4); + + // Assert each row's operator actually took effect — the count alone would + // let a wrong/failed operator selection pass. + await pageManager.pipelinesPage.verifyOperatorSelected(">=", 0); + await pageManager.pipelinesPage.verifyOperatorSelected("!=", 1); + await pageManager.pipelinesPage.verifyOperatorSelected("Contains", 2); + await pageManager.pipelinesPage.verifyOperatorSelected("NotContains", 3); await pageManager.pipelinesPage.saveConditionAndCompletePipeline(pipelineName); @@ -403,4 +417,103 @@ test.describe("Pipeline Conditions - Comprehensive Tests", () => { await pageManager.pipelinesPage.openPipelineMenu(); await pageManager.pipelinesPage.deletePipelineByName(pipelineName); }); + + test("should hide the value input for a unary is_null operator and save without error", { + tag: ['@all', '@pipelines', '@pipelinesConditions', '@condition-null-empty-operators'] + }, async ({ page }) => { + await pageManager.pipelinesPage.createPipelineWithCondition(`e2e_conditions_operators${streamSuffix}`); + await pageManager.pipelinesPage.verifyConditionDialogOpen(); + + // Select column + unary operator (no value input for unary operators). + await pageManager.pipelinesPage.fillUnaryCondition("kubernetes_container_name", "is_null", 0); + + // The value input is v-if-removed for a unary operator. + await pageManager.pipelinesPage.verifyValueInputCount(0); + + // Saving a unary condition is complete without a value: no inline error and + // the drawer closes (the condition node appears on the canvas). + await pageManager.pipelinesPage.saveCondition(); + await pageManager.pipelinesPage.verifyNoConditionError(); + await pageManager.pipelinesPage.verifyConditionDialogClosed(); + }); + + test("should round-trip a saved unary operator on edit and restore the value input for a binary operator", { + tag: ['@all', '@pipelines', '@pipelinesConditions', '@condition-null-empty-operators'] + }, async ({ page }) => { + const pipelineName = `pipeline-null-empty-roundtrip-${Math.random().toString(36).substring(7)}`; + currentPipelineName = pipelineName; + + await pageManager.pipelinesPage.createPipelineWithCondition(`e2e_conditions_operators${streamSuffix}`); + await pageManager.pipelinesPage.fillUnaryCondition("kubernetes_container_name", "is_null", 0); + await pageManager.pipelinesPage.verifyValueInputCount(0); + await pageManager.pipelinesPage.saveConditionAndCompletePipeline(pipelineName); + + // Reopen the saved pipeline and edit its condition node. + await pageManager.pipelinesPage.openPipelineMenu(); + await page.waitForTimeout(1000); + await pageManager.pipelinesPage.openPipelineForEdit(pipelineName); + await pageManager.pipelinesPage.clickConditionNode(); + await pageManager.pipelinesPage.verifyConditionDialogOpen(); + + // The unary operator round-trips and the value input stays hidden on load. + await pageManager.pipelinesPage.verifyOperatorSelected("is_null", 0); + await pageManager.pipelinesPage.verifyValueInputCount(0); + + // Switching to a binary operator restores the value input for that row. + await pageManager.pipelinesPage.selectOperatorFromMenu("="); + await pageManager.pipelinesPage.verifyValueInputCount(1); + + // Cleanup + await pageManager.pipelinesPage.openPipelineMenu(); + await page.waitForTimeout(1000); + await pageManager.pipelinesPage.deletePipelineByName(pipelineName); + }); + + test("should offer all four unary operators in the condition operator dropdown", { + tag: ['@all', '@pipelines', '@pipelinesConditions', '@condition-null-empty-operators'] + }, async ({ page }) => { + await pageManager.pipelinesPage.createPipelineWithCondition(`e2e_conditions_operators${streamSuffix}`); + await pageManager.pipelinesPage.verifyConditionDialogOpen(); + + // Select a column so the operator menu is in its interactive state. + await pageManager.pipelinesPage.fillPartialCondition("kubernetes_container_name"); + + // All four unary operators are offered. + await pageManager.pipelinesPage.verifyUnaryOperatorsOffered(); + }); + + test("should still require a value for a binary operator after unary operators are added", { + tag: ['@all', '@pipelines', '@pipelinesConditions', '@pipelinesValidation', '@condition-null-empty-operators'] + }, async ({ page }) => { + await pageManager.pipelinesPage.createPipelineWithCondition(`e2e_conditions_operators${streamSuffix}`); + await pageManager.pipelinesPage.fillPartialCondition("kubernetes_container_name"); + await pageManager.pipelinesPage.selectOperatorFromMenu("="); + + // Guard: the binary operator actually took effect before asserting its error. + await pageManager.pipelinesPage.verifyOperatorSelected("=", 0); + + // A binary operator still requires a value: saving with an empty value + // surfaces the inline schema error (unary exemption did not weaken this). + await pageManager.pipelinesPage.tryToSaveWithoutValidConditions(); + await pageManager.pipelinesPage.verifyNotificationVisible(); + }); + + test("should clear a previously entered value when switching to a unary operator", { + tag: ['@all', '@pipelines', '@pipelinesConditions', '@condition-null-empty-operators'] + }, async ({ page }) => { + await pageManager.pipelinesPage.createPipelineWithCondition(`e2e_conditions_operators${streamSuffix}`); + + // Enter a binary condition with a stale value. + await pageManager.pipelinesPage.fillCondition("kubernetes_container_name", "=", "stale-value", 0); + + // Switching to a unary operator drops the stale value and hides the input. + await pageManager.pipelinesPage.selectOperatorFromMenu("is_null"); + await pageManager.pipelinesPage.verifyValueInputCount(0); + + // Switching back to a binary operator restores the input, now empty — + // proving the stale value did not leak into the form. + await pageManager.pipelinesPage.selectOperatorFromMenu("="); + await pageManager.pipelinesPage.verifyValueInputCount(1); + await pageManager.pipelinesPage.verifyConditionValueInputValue("", 0); + }); });