diff --git a/tests/ui-testing/pages/functionsPages/functionsFormValidationPage.js b/tests/ui-testing/pages/functionsPages/functionsFormValidationPage.js index c8c2ded1a3..3cf6b95a5a 100644 --- a/tests/ui-testing/pages/functionsPages/functionsFormValidationPage.js +++ b/tests/ui-testing/pages/functionsPages/functionsFormValidationPage.js @@ -63,9 +63,9 @@ class FunctionsFormValidationPage { // ==================== FunctionsToolbar locators ==================== - // VRL / JS transform type radios - this.vrlRadio = page.locator('[data-test="function-transform-type-vrl-radio"]'); - this.jsRadio = page.locator('[data-test="function-transform-type-js-radio"]'); + // VRL / JS language toggle (OToggleGroup items) + this.vrlOption = page.locator('[data-test="function-transform-type-vrl-option"]'); + this.jsOption = page.locator('[data-test="function-transform-type-js-option"]'); // ==================== StreamRouting locators ==================== @@ -253,14 +253,11 @@ class FunctionsFormValidationPage { // ==================== FunctionsToolbar assertion helpers ==================== async assertVrlRadioSelected() { - testLogger.info('Asserting VRL radio is checked'); - await this.vrlRadio.waitFor({ state: 'visible', timeout: 10000 }); - // ORadio renders a native input — check aria-checked or checked attribute - const checked = await this.vrlRadio.evaluate(el => { - const input = el.querySelector('input[type="radio"]') || el.closest('[role="radio"]') || el; - return input.checked || el.getAttribute('aria-checked') === 'true' || el.getAttribute('data-state') === 'checked'; - }); - expect(checked).toBe(true); + testLogger.info('Asserting the VRL language option is selected'); + await this.vrlOption.waitFor({ state: 'visible', timeout: 10000 }); + // OToggleGroupItem exposes selection via Reka-UI's `data-state="on|off"` + // (and mirrors it on aria-pressed). + await expect(this.vrlOption).toHaveAttribute('data-state', 'on'); } // ==================== StreamRouting actions ==================== diff --git a/tests/ui-testing/pages/functionsPages/functionsPage.js b/tests/ui-testing/pages/functionsPages/functionsPage.js index 41b143377e..37cf2250ee 100644 --- a/tests/ui-testing/pages/functionsPages/functionsPage.js +++ b/tests/ui-testing/pages/functionsPages/functionsPage.js @@ -20,12 +20,11 @@ class FunctionsPage { this.rowEditButton = this.page.locator(this.rowEditButtonSelector); this.rowDeleteButton = this.page.locator(this.rowDeleteButtonSelector); - // Function type selection (Reka-UI radios — wrapper divs carry the data-test). - // The inner RadioGroupItem renders data-state="checked|unchecked" — we read it - // via page.evaluate scoped from the wrapper's data-test locator (no element-tag - // or class selectors per AGENT_RULES §2). - this.vrlRadio = this.page.locator('[data-test="function-transform-type-vrl-radio"]'); - this.jsRadio = this.page.locator('[data-test="function-transform-type-js-radio"]'); + // Language selection — a segmented toggle (OToggleGroup). The data-test lands + // on the ToggleGroupItem button itself, which Reka-UI marks with + // data-state="on|off" (NOT the radio group's "checked|unchecked"). + this.vrlOption = this.page.locator('[data-test="function-transform-type-vrl-option"]'); + this.jsOption = this.page.locator('[data-test="function-transform-type-js-option"]'); // Function form elements. The name is an inline-edited title (OFormInlineEdit): // a display trigger swaps to an input on click. @@ -126,18 +125,18 @@ class FunctionsPage { } async selectJavaScriptType() { - // ORadio forwards the consumer's data-test onto the inner RadioGroupItem - // (the ARIA-focusable radio button) — `this.jsRadio` IS the clickable item. - await expect(this.jsRadio).toBeVisible(); - await this.jsRadio.click(); + // OToggleGroupItem forwards the consumer's data-test onto the item button — + // `this.jsOption` IS the clickable item. + await expect(this.jsOption).toBeVisible(); + await this.jsOption.click(); // Wait for CodeQueryEditor to remount Monaco for the new language // (editor-id is re-keyed on currentLanguage in QueryEditor.vue) await this.page.waitForTimeout(1500); } async selectVRLType() { - await expect(this.vrlRadio).toBeVisible(); - await this.vrlRadio.click(); + await expect(this.vrlOption).toBeVisible(); + await this.vrlOption.click(); await this.page.waitForTimeout(1500); } @@ -316,23 +315,23 @@ class FunctionsPage { } async expectVrlRadioVisible() { - await expect(this.vrlRadio).toBeVisible(); + await expect(this.vrlOption).toBeVisible(); } async expectJsRadioVisible() { - await expect(this.jsRadio).toBeVisible(); + await expect(this.jsOption).toBeVisible(); } async expectJsRadioSelected() { - // ORadio forwards the consumer's data-test onto the inner RadioGroupItem, - // which Reka-UI exposes via `data-state="checked|unchecked"`. - await this.jsRadio.waitFor({ state: 'visible' }); - await expect(this.jsRadio).toHaveAttribute('data-state', 'checked'); + // OToggleGroupItem forwards the consumer's data-test onto the item button, + // which Reka-UI exposes via `data-state="on|off"`. + await this.jsOption.waitFor({ state: 'visible' }); + await expect(this.jsOption).toHaveAttribute('data-state', 'on'); } async expectVrlRadioSelected() { - await this.vrlRadio.waitFor({ state: 'visible' }); - await expect(this.vrlRadio).toHaveAttribute('data-state', 'checked'); + await this.vrlOption.waitFor({ state: 'visible' }); + await expect(this.vrlOption).toHaveAttribute('data-state', 'on'); } async expectTestOutputContains(text) { @@ -377,11 +376,11 @@ class FunctionsPage { } async isJsRadioVisible() { - return await this.jsRadio.isVisible().catch(() => false); + return await this.jsOption.isVisible().catch(() => false); } async expectJsRadioHidden() { - await expect(this.jsRadio).not.toBeVisible(); + await expect(this.jsOption).not.toBeVisible(); } // ==================== Complex Workflows ==================== @@ -479,16 +478,16 @@ class FunctionsPage { await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {}); await this.page.waitForTimeout(2000); - // Wait for radio buttons to be visible - await this.jsRadio.waitFor({ state: 'visible', timeout: 10000 }); + // Wait for the language toggle to be visible + await this.jsOption.waitFor({ state: 'visible', timeout: 10000 }); - // ORadio forwards the consumer's data-test onto the inner RadioGroupItem, - // which Reka-UI exposes via `data-state="checked|unchecked"`. - const jsState = await this.jsRadio.getAttribute('data-state'); - const vrlState = await this.vrlRadio.getAttribute('data-state'); + // OToggleGroupItem forwards the consumer's data-test onto the item button, + // which Reka-UI exposes via `data-state="on|off"`. + const jsState = await this.jsOption.getAttribute('data-state'); + const vrlState = await this.vrlOption.getAttribute('data-state'); - if (jsState === 'checked') return 'js'; - if (vrlState === 'checked') return 'vrl'; + if (jsState === 'on') return 'js'; + if (vrlState === 'on') return 'vrl'; return null; } diff --git a/web/src/components/functions/FunctionsToolbar.spec.ts b/web/src/components/functions/FunctionsToolbar.spec.ts index db1b399844..69cbc80f04 100644 --- a/web/src/components/functions/FunctionsToolbar.spec.ts +++ b/web/src/components/functions/FunctionsToolbar.spec.ts @@ -125,28 +125,73 @@ describe("FunctionsToolbar", () => { expect(wrapper.find('[data-test="add-function-name-input-value"]').exists()).toBe(true); }); - it("should render VRL radio button", () => { + it("should render the VRL language option", () => { const wrapper = mountToolbar({ transformTypeOptions: [{ label: "VRL", value: "0" }], }); - expect(wrapper.find('[data-test="function-transform-type-vrl-radio"]').exists()).toBe(true); + expect(wrapper.find('[data-test="function-transform-type-vrl-option"]').exists()).toBe(true); }); - it("should render JavaScript radio button when option is provided", () => { + it("should render the JavaScript language option when it is provided", () => { const wrapper = mountToolbar({ transformTypeOptions: [ { label: "VRL", value: "0" }, { label: "JavaScript", value: "1" }, ], }); - expect(wrapper.find('[data-test="function-transform-type-js-radio"]').exists()).toBe(true); + expect(wrapper.find('[data-test="function-transform-type-js-option"]').exists()).toBe(true); }); - it("should NOT render the JavaScript radio when only one option is provided", () => { + it("should NOT render the JavaScript option when only one option is provided", () => { const wrapper = mountToolbar({ transformTypeOptions: [{ label: "VRL", value: "0" }], }); - expect(wrapper.find('[data-test="function-transform-type-js-radio"]').exists()).toBe(false); + expect(wrapper.find('[data-test="function-transform-type-js-option"]').exists()).toBe(false); + }); + + it("marks the option matching the form's transType as selected", () => { + const wrapper = mountToolbar( + { + transformTypeOptions: [ + { label: "VRL", value: "0" }, + { label: "JavaScript", value: "1" }, + ], + }, + { name: "fn", transType: "1" }, + ); + + expect( + wrapper.find('[data-test="function-transform-type-js-option"]').attributes("data-state"), + ).toBe("on"); + expect( + wrapper.find('[data-test="function-transform-type-vrl-option"]').attributes("data-state"), + ).toBe("off"); + }); + + it("writes the picked language to the form when an option is clicked", async () => { + const wrapper = mountToolbar({ + transformTypeOptions: [ + { label: "VRL", value: "0" }, + { label: "JavaScript", value: "1" }, + ], + }); + + await wrapper.find('[data-test="function-transform-type-js-option"]').trigger("click"); + + expect(getForm(wrapper).state.values.transType).toBe("1"); + }); + + it("offers an info tip for BOTH languages without selecting either", () => { + const wrapper = mountToolbar({ + transformTypeOptions: [ + { label: "VRL", value: "0" }, + { label: "JavaScript", value: "1" }, + ], + }); + + // transType defaults to "0" (VRL) — the JS tip must still be reachable. + expect(wrapper.find('[data-test="function-transform-type-vrl-info"]').exists()).toBe(true); + expect(wrapper.find('[data-test="function-transform-type-js-info"]').exists()).toBe(true); }); it("hides the language toggle entirely when hideTransType is set", () => { @@ -164,8 +209,10 @@ describe("FunctionsToolbar", () => { }, }); - expect(wrapper.find('[data-test="function-transform-type-vrl-radio"]').exists()).toBe(false); - expect(wrapper.find('[data-test="function-transform-type-js-radio"]').exists()).toBe(false); + expect(wrapper.find('[data-test="function-transform-type-vrl-option"]').exists()).toBe(false); + expect(wrapper.find('[data-test="function-transform-type-js-option"]').exists()).toBe(false); + // The locked language's tip stays reachable. + expect(wrapper.find('[data-test="function-transform-type-info"]').exists()).toBe(true); }); it("should emit test event when test button is clicked", async () => { diff --git a/web/src/components/functions/FunctionsToolbar.vue b/web/src/components/functions/FunctionsToolbar.vue index 8fae0942bd..d1ea6bcff1 100644 --- a/web/src/components/functions/FunctionsToolbar.vue +++ b/web/src/components/functions/FunctionsToolbar.vue @@ -1,7 +1,7 @@ @@ -26,38 +26,107 @@ /> - - + + + + - - + - - - {{ - transformTypeOptions[0]?.label - }} - + + + + {{ raw("V") }} + + {{ transformTypeOptions[0]?.label }} + + + + + + + + {{ t("function.vrl") }} {{ t("function.tipLabel") }} + + {{ t("function.vrlFunctionHint") }} + + + + + + + + + + - - - {{ - transformTypeOptions[1]?.label - }} - - - - + + + {{ raw("JS") }} + + {{ transformTypeOptions[1]?.label }} + + + + + + + {{ t("function.javascript") }} {{ t("function.tipLabel") }} + + {{ t("function.jsFunctionHint") }} + + + + + + + + + + - {{ transTypeValue === "1" ? t("function.javascript") : t("function.vrl") }} @@ -144,7 +213,7 @@