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 @@
+
+
+
+
+
+
+
+