diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..2521cd6e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "monthly" + cooldown: + default-days: 2 + allow: + - dependency-type: "production" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + cooldown: + default-days: 2 diff --git a/examples/ui5-js-app/jsconfig.json b/examples/ui5-js-app/jsconfig.json index 3c53f59a..c915f74b 100644 --- a/examples/ui5-js-app/jsconfig.json +++ b/examples/ui5-js-app/jsconfig.json @@ -5,7 +5,7 @@ "@openui5/types", "@wdio/globals/types", "@wdio/mocha-framework", - "wdio-ui5-service/dist/cjs", + "wdio-ui5-service", "expect-webdriverio" ] } diff --git a/examples/ui5-js-app/webapp/controller/Main.controller.js b/examples/ui5-js-app/webapp/controller/Main.controller.js index 9d442ed4..70c3833a 100644 --- a/examples/ui5-js-app/webapp/controller/Main.controller.js +++ b/examples/ui5-js-app/webapp/controller/Main.controller.js @@ -49,6 +49,13 @@ sap.ui.define( onSearch(oEvent) { this.getView().byId("idSearchResult").setText(oEvent.getSource().getValue()) }, + onKeepFocusInputChange(oEvent) { + this.getView().byId("idKeepFocusResult").setText(oEvent.getSource().getValue()) + }, + onClearTextInputChange(oEvent) { + console.log(oEvent.getSource().getValue()) + this.getView().byId("idClearTextResult").setText(oEvent.getSource().getValue()) + }, onTest(oEvent) { this.onBoo(oEvent) }, diff --git a/examples/ui5-js-app/webapp/test/e2e/allControlsInteraction.test.js b/examples/ui5-js-app/webapp/test/e2e/allControlsInteraction.test.js new file mode 100644 index 00000000..49ec792a --- /dev/null +++ b/examples/ui5-js-app/webapp/test/e2e/allControlsInteraction.test.js @@ -0,0 +1,65 @@ +const Main = require("./pageObjects/Main") + +describe("allControls interaction", () => { + before(async () => { + await Main.open() + }) + + it("should press a specific button from allControls", async () => { + const selector = { + selector: { + controlType: "sap.m.Button", + viewName: "test.Sample.view.Main", + interaction: "root" + } + } + + const buttons = await browser.allControls(selector) + // Find the button with a specific ID among all buttons + + let targetButton + for (const btn of buttons) { + const id = await btn.getId() + if (id.includes("idIaSync")) { + targetButton = btn + break + } + } + + expect(targetButton).toBeDefined() + // The fix ensures that .press() uses the specific ID of this control instance + await targetButton.press() + + // Verification: we can check if the button is still there or some other state + // In Main.controller.js, onPress shows a MessageToast. + // Hard to verify MessageToast without further setup, but we can at least ensure the call doesn't fail + // and targets the right element. + expect(await targetButton.getVisible()).toBeTruthy() + }) + + it("should enter text into a specific input from allControls", async () => { + const selector = { + selector: { + controlType: "sap.m.Input", + viewName: "test.Sample.view.Main", + interaction: "root" + } + } + + const inputs = await browser.allControls(selector) + let targetInput + for (const input of inputs) { + const id = await input.getId() + if (id.includes("mainUserInput")) { + targetInput = input + break + } + } + + expect(targetInput).toBeDefined() + const testText = "Typed from allControls" + await targetInput.enterText(testText) + + expect(await targetInput.getValue()).toEqual(testText) + }) +}) diff --git a/examples/ui5-js-app/webapp/test/e2e/allControlsPress.test.js b/examples/ui5-js-app/webapp/test/e2e/allControlsPress.test.js new file mode 100644 index 00000000..9e7488f9 --- /dev/null +++ b/examples/ui5-js-app/webapp/test/e2e/allControlsPress.test.js @@ -0,0 +1,27 @@ +const Other = require("./pageObjects/Other") + +describe("ui5 basic, press all buttons", () => { + before(async () => { + await Other.open() + }) + + it("test with pressing multiple checkboxes", async () => { + const selector = { + selector: { + controlType: "sap.m.CheckBox", + viewName: "test.Sample.view.Other", + interaction: "press" + } + } + const checkboxList = await browser.allControls(selector) + + expect(checkboxList.length).toEqual(9) + + for await (let checkbox of checkboxList) { + const selectedBeforePress = await checkbox.getSelected() + await checkbox.press() + const selectedAfterPress = await checkbox.getSelected() + expect(selectedAfterPress).toEqual(!selectedBeforePress) + } + }) +}) diff --git a/examples/ui5-js-app/webapp/test/e2e/ancestor-descendant.test.js b/examples/ui5-js-app/webapp/test/e2e/ancestor-descendant.test.js new file mode 100644 index 00000000..ed147826 --- /dev/null +++ b/examples/ui5-js-app/webapp/test/e2e/ancestor-descendant.test.js @@ -0,0 +1,95 @@ +const Main = require("./pageObjects/Main") + +describe("ancestor and descendant selector resolution", () => { + before(async () => { + await Main.open() + }) + + it("should find a control using an ancestor selector (asControl)", async () => { + const selector = { + selector: { + controlType: "sap.m.Title", + ancestor: { + controlType: "sap.m.Panel", + properties: { + headerText: "Header Text" + } + } + } + } + const title = await browser.asControl(selector) + expect(await title.getText()).toBe("Custom Toolbar with a header text") + }) + + it("should find a control using a descendant selector (asControl)", async () => { + const selector = { + selector: { + controlType: "sap.m.Panel", + descendant: { + controlType: "sap.m.Title", + properties: { + text: "Custom Toolbar with a header text" + } + } + } + } + const panel = await browser.asControl(selector) + expect(await panel.getHeaderText()).toBe("Header Text") + }) + + it("should find all controls using an ancestor selector (allControls)", async () => { + const selector = { + wdio_ui5_key: "allButtonsWithAncestor", + selector: { + controlType: "sap.m.Button", + ancestor: { + id: "page", + viewName: "test.Sample.view.Main" + } + } + } + const buttons = await browser.allControls(selector) + expect(Array.isArray(buttons)).toBe(true) + expect(buttons.length).toBe(9) + }) + + it("should find all controls using a descendant selector (allControls)", async () => { + const selector = { + wdio_ui5_key: "allPanelsWithDescendant", + selector: { + controlType: "sap.m.Panel", + descendant: { + controlType: "sap.m.Title", + properties: { + text: "Custom Toolbar with a header text" + } + } + } + } + const panels = await browser.allControls(selector) + expect(Array.isArray(panels)).toBe(true) + expect(panels.length).toBe(1) + expect(await panels[0].getHeaderText()).toBe("Header Text") + }) + + it("should find a control using nested ancestor/descendant selectors", async () => { + // Find the Title inside the Panel, which is inside the Page + const selector = { + selector: { + controlType: "sap.m.Title", + properties: { + text: "Custom Toolbar with a header text" + }, + ancestor: { + controlType: "sap.m.Panel", + ancestor: { + id: "page", + viewName: "test.Sample.view.Main" + } + } + } + } + const title = await browser.asControl(selector) + expect(await title.getText()).toBe("Custom Toolbar with a header text") + }) +}) diff --git a/examples/ui5-js-app/webapp/test/e2e/interaction.test.js b/examples/ui5-js-app/webapp/test/e2e/interaction.test.js index b619e8e8..73b9d5ab 100644 --- a/examples/ui5-js-app/webapp/test/e2e/interaction.test.js +++ b/examples/ui5-js-app/webapp/test/e2e/interaction.test.js @@ -68,4 +68,158 @@ describe("interaction + binding", () => { expect(await ui5Input.getProperty("value")).toEqual(inputText) }) + + it("enterText() with pressEnterKey:true triggers search event", async () => { + const searchFieldSelector = { + selector: { + id: "idSearchfield", + viewName: viewName, + interaction: "focus" + } + } + const searchResultSelector = { + selector: { + id: "idSearchResult", + viewName: viewName + } + } + + const searchTerm = "Alice" + const searchField = await browser.asControl(searchFieldSelector) + await searchField.enterText(searchTerm, { pressEnterKey: true }) + + const searchResult = await browser.asControl(searchResultSelector) + expect(await searchResult.getProperty("text")).toEqual(searchTerm) + }) + + it("enterText() with pressEnterKey:false does not trigger search event", async () => { + const searchFieldSelector = { + selector: { + id: "idSearchfield", + viewName: viewName + } + } + const searchResultSelector = { + selector: { + id: "idSearchResult", + viewName: viewName + } + } + + // capture the current search result before typing (set by the previous test) + const searchResult = await browser.asControl(searchResultSelector) + const previousResult = await searchResult.getProperty("text") + + const searchTerm = "Charlie" + const searchField = await browser.asControl(searchFieldSelector) + // pressEnterKey:false — text is entered but Enter is not pressed, so onSearch is not fired + await searchField.enterText(searchTerm, { pressEnterKey: false, keepFocus: true }) + + // result text must remain unchanged since the search event was not triggered + expect(await searchResult.getProperty("text")).toEqual(previousResult) + }) + + it("enterText() with keepFocus:false blurs the input and triggers change event", async () => { + const inputSelector = { + selector: { + id: "idKeepFocusInput", + viewName: viewName, + interaction: "focus" + } + } + const resultSelector = { + selector: { + id: "idKeepFocusResult", + viewName: viewName + } + } + + const inputText = "keep focus off" + const input = await browser.asControl(inputSelector) + // keepFocus:false (default) — focus is removed after enterText, which fires the change event + await input.enterText(inputText, { keepFocus: false }) + + const result = await browser.asControl(resultSelector) + expect(await result.getProperty("text")).toEqual(inputText) + }) + + it("enterText() with keepFocus:true retains focus and does not trigger change event", async () => { + const inputSelector = { + selector: { + id: "idKeepFocusInput", + viewName: viewName, + interaction: "focus" + } + } + const resultSelector = { + selector: { + id: "idKeepFocusResult", + viewName: viewName + } + } + + // capture current result (set by previous test) + const result = await browser.asControl(resultSelector) + const previousResult = await result.getProperty("text") + + const inputText = "keep focus on" + const input = await browser.asControl(inputSelector) + // keepFocus:true — focus is retained after enterText, so no blur occurs and change event does not fire + await input.enterText(inputText, { keepFocus: true }) + + // result text must remain unchanged since the change event was not triggered + expect(await result.getProperty("text")).toEqual(previousResult) + }) + + it("enterText() with clearTextFirst:true replaces existing input value", async () => { + const inputSelector = { + selector: { + id: "idClearTextInput", + viewName: viewName, + interaction: "focus" + } + } + const resultSelector = { + selector: { + id: "idClearTextResult", + viewName: viewName + } + } + + const appendedText = " World" + const input = await browser.asControl(inputSelector) + // clearTextFirst:true (default) — existing "Hello" is cleared before typing, so only appended text remains + await input.enterText(appendedText, { clearTextFirst: true }) + + const result = await browser.asControl(resultSelector) + expect(await result.getProperty("text")).toEqual(appendedText) + }) + + it.skip("enterText() with clearTextFirst:false appends to existing input value", async () => { + // TODO: perhaps capture UI5 event in a browser.execute command to ensure whether this has been triggered + const inputSelector = { + selector: { + id: "idClearTextInput", + viewName: viewName, + interaction: "focus" + } + } + const resultSelector = { + selector: { + id: "idClearTextResult", + viewName: viewName + } + } + + const initialText = "Hello" + const appendedText = " World" + // restore a known value first (clearTextFirst:true so we start clean) + const input = await browser.asControl(inputSelector) + await input.enterText(initialText, { clearTextFirst: true }) + // clearTextFirst:false — existing "Hello" is kept and new text is appended after it + await input.enterText(appendedText, { clearTextFirst: false }) + + const result = await browser.asControl(resultSelector) + expect(await result.getProperty("text")).toEqual(initialText + appendedText) + }) }) diff --git a/examples/ui5-js-app/webapp/test/e2e/press.test.js b/examples/ui5-js-app/webapp/test/e2e/press.test.js index 501a5815..fa38525e 100644 --- a/examples/ui5-js-app/webapp/test/e2e/press.test.js +++ b/examples/ui5-js-app/webapp/test/e2e/press.test.js @@ -62,7 +62,8 @@ describe("custom wdi5 press event", async () => { id: "container-Sample---Main--idDateTime-icon" } }) - await element.fireEvent("press") + // await element.fireEvent("press") + await element.press() startDate.setDate(startDate.getDate() + 1) // add one day to the start date const tomorrowSuffix = startDate.toJSON().substring(0, 10).replaceAll("-", "") diff --git a/examples/ui5-js-app/webapp/test/e2e/ui5-features-available.test.js b/examples/ui5-js-app/webapp/test/e2e/ui5-features-available.test.js new file mode 100644 index 00000000..541254a3 --- /dev/null +++ b/examples/ui5-js-app/webapp/test/e2e/ui5-features-available.test.js @@ -0,0 +1,95 @@ +// only requiring the service for late inject/init +const { mock } = require("node:test") +const { default: _ui5Service } = require("wdio-ui5-service") +const ui5Service = new _ui5Service() + +/** + * All tests have the same setup and initial assertions + */ +async function setupMockInjectAssert(ui5Version) { + let ui5Features = await browser.execute(() => window.wdi5Ui5FeaturesAvailable) + expect(ui5Features).toBeUndefined() + const browserMock = mock.method(browser, "getUI5Version", () => ui5Version) + await ui5Service.injectUI5() + browserMock.mock.restore() + ui5Features = await browser.execute(() => window.wdi5Ui5FeaturesAvailable) + expect(ui5Features.version).toBe(ui5Version) + return ui5Features +} + +describe("ui5 features available", () => { + beforeEach(async () => { + // Start fresh every test + await browser.url("http://localhost:8081/index.html") + }) + + it("should show UI5 features available for the UI5 v1.71", async () => { + const ui5Features = await setupMockInjectAssert("1.71.0") + expect(ui5Features.useFetchWaiter).toBeFalsy() + expect(ui5Features.useGetComponentById).toBeFalsy() + expect(ui5Features.useUI5ElementClosestTo).toBeFalsy() + expect(ui5Features.useOldHashChanger).toBeTruthy() + expect(ui5Features.useOldDoubleLeadingSlash).toBeTruthy() + expect(ui5Features.useOldMatcherAPI).toBeTruthy() + }) + + it("should show UI5 features available for the UI5 v1.74", async () => { + const ui5Features = await setupMockInjectAssert("1.74.0") + expect(ui5Features.useFetchWaiter).toBeFalsy() + expect(ui5Features.useGetComponentById).toBeFalsy() + expect(ui5Features.useUI5ElementClosestTo).toBeFalsy() + expect(ui5Features.useOldHashChanger).toBeTruthy() + expect(ui5Features.useOldDoubleLeadingSlash).toBeTruthy() + expect(ui5Features.useOldMatcherAPI).toBeFalsy() + }) + + it("should show UI5 features available for the UI5 v1.80", async () => { + const ui5Features = await setupMockInjectAssert("1.80.0") + expect(ui5Features.useFetchWaiter).toBeFalsy() + expect(ui5Features.useGetComponentById).toBeFalsy() + expect(ui5Features.useUI5ElementClosestTo).toBeFalsy() + expect(ui5Features.useOldHashChanger).toBeFalsy() + expect(ui5Features.useOldDoubleLeadingSlash).toBeTruthy() + expect(ui5Features.useOldMatcherAPI).toBeFalsy() + }) + + it("should show UI5 features available for the UI5 v1.107", async () => { + const ui5Features = await setupMockInjectAssert("1.107.0") + expect(ui5Features.useFetchWaiter).toBeFalsy() + expect(ui5Features.useGetComponentById).toBeFalsy() + expect(ui5Features.useUI5ElementClosestTo).toBeFalsy() + expect(ui5Features.useOldHashChanger).toBeFalsy() + expect(ui5Features.useOldDoubleLeadingSlash).toBeFalsy() + expect(ui5Features.useOldMatcherAPI).toBeFalsy() + }) + + it("should show UI5 features available for the UI5 v1.113", async () => { + const ui5Features = await setupMockInjectAssert("1.113.0") + expect(ui5Features.useFetchWaiter).toBeFalsy() + expect(ui5Features.useGetComponentById).toBeFalsy() + expect(ui5Features.useUI5ElementClosestTo).toBeTruthy() + expect(ui5Features.useOldHashChanger).toBeFalsy() + expect(ui5Features.useOldDoubleLeadingSlash).toBeFalsy() + expect(ui5Features.useOldMatcherAPI).toBeFalsy() + }) + + it("should show UI5 features available for the UI5 v1.119", async () => { + const ui5Features = await setupMockInjectAssert("1.119.0") + expect(ui5Features.useFetchWaiter).toBeTruthy() + expect(ui5Features.useGetComponentById).toBeFalsy() + expect(ui5Features.useUI5ElementClosestTo).toBeTruthy() + expect(ui5Features.useOldHashChanger).toBeFalsy() + expect(ui5Features.useOldDoubleLeadingSlash).toBeFalsy() + expect(ui5Features.useOldMatcherAPI).toBeFalsy() + }) + + it("should show UI5 features available for the UI5 v1.136", async () => { + const ui5Features = await setupMockInjectAssert("1.136.0") + expect(ui5Features.useFetchWaiter).toBeTruthy() + expect(ui5Features.useGetComponentById).toBeTruthy() + expect(ui5Features.useUI5ElementClosestTo).toBeTruthy() + expect(ui5Features.useOldHashChanger).toBeFalsy() + expect(ui5Features.useOldDoubleLeadingSlash).toBeFalsy() + expect(ui5Features.useOldMatcherAPI).toBeFalsy() + }) +}) diff --git a/examples/ui5-js-app/webapp/test/e2e/ui5-late.test.js b/examples/ui5-js-app/webapp/test/e2e/ui5-late.test.js index 965c5119..b07480f7 100644 --- a/examples/ui5-js-app/webapp/test/e2e/ui5-late.test.js +++ b/examples/ui5-js-app/webapp/test/e2e/ui5-late.test.js @@ -5,11 +5,17 @@ const ui5Service = new _ui5Service() describe("ui5 basic", () => { it('should show a non UI5 page, then advance to a UI5 page and late init "wdi5"', async () => { // native wdio functionality - navigates to the wdi5 github page - await browser.$("#user-content-wdi5-").waitForExist() + const link = await $("=wdi5") + await expect(link).toHaveText("wdi5") // open local app await browser.url("http://localhost:8081/index.html") + let hasWdi5InBrowser = await browser.execute(() => !!window.wdi5) + expect(hasWdi5InBrowser).toBeFalsy() + // do the late injection await ui5Service.injectUI5() + hasWdi5InBrowser = await browser.execute(() => !!window.wdi5) + expect(hasWdi5InBrowser).toBeTruthy() }) it("should verify the caching of the wdi5 config", async () => { diff --git a/examples/ui5-js-app/webapp/test/wdio-ui5-late.conf.js b/examples/ui5-js-app/webapp/test/wdio-ui5-late.conf.js index ee2960db..b8a73cdf 100644 --- a/examples/ui5-js-app/webapp/test/wdio-ui5-late.conf.js +++ b/examples/ui5-js-app/webapp/test/wdio-ui5-late.conf.js @@ -5,7 +5,7 @@ const _config = { skipInjectUI5OnStart: true, waitForUI5Timeout: 654321 }, - specs: ["e2e/ui5-late.test.js"], + specs: ["e2e/ui5-late.test.js", "e2e/ui5-features-available.test.js"], baseUrl: "https://github.com/ui5-community/wdi5/" } diff --git a/examples/ui5-js-app/webapp/view/Main.view.xml b/examples/ui5-js-app/webapp/view/Main.view.xml index 09fb4082..84147aec 100644 --- a/examples/ui5-js-app/webapp/view/Main.view.xml +++ b/examples/ui5-js-app/webapp/view/Main.view.xml @@ -61,6 +61,14 @@ +