From 7dd73df6b08cddb0ab5d07f1c5d72766bc5a67c7 Mon Sep 17 00:00:00 2001 From: Mohab Mohie Date: Sat, 27 Jan 2024 17:32:35 +0200 Subject: [PATCH] init - does not compile --- .github/FUNDING.yml | 14 + .github/dependabot.yml | 18 + .github/workflows/codeql-analysis.yml | 58 +++ .github/workflows/dependency_review.yml | 16 + .github/workflows/mavenCentral_cd.yml | 55 +++ pom.xml | 213 ++++++++++ .../java/com/shaft/driver/DriverFactory.java | 34 ++ src/main/java/com/shaft/driver/SHAFT.java | 34 ++ .../internal/FluentWebDriverAction.java | 19 + .../com/shaft/gui/element/SikuliActions.java | 375 ++++++++++++++++++ .../internal/ElementActionsHelper.java | 123 ++++++ .../gui/internal/image/ScreenshotManager.java | 91 +++++ src/test/java/testPackage/SikulixTests.java | 60 +++ .../testDataFiles/sikulixElements/youtube.png | Bin 0 -> 1532 bytes 14 files changed, 1110 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/dependency_review.yml create mode 100644 .github/workflows/mavenCentral_cd.yml create mode 100644 pom.xml create mode 100644 src/main/java/com/shaft/driver/DriverFactory.java create mode 100644 src/main/java/com/shaft/driver/SHAFT.java create mode 100644 src/main/java/com/shaft/driver/internal/FluentWebDriverAction.java create mode 100644 src/main/java/com/shaft/gui/element/SikuliActions.java create mode 100644 src/main/java/com/shaft/gui/element/internal/ElementActionsHelper.java create mode 100644 src/main/java/com/shaft/gui/internal/image/ScreenshotManager.java create mode 100644 src/test/java/testPackage/SikulixTests.java create mode 100644 src/test/resources/testDataFiles/sikulixElements/youtube.png diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..f57f01f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,14 @@ +# These are supported funding model platforms + +github: [MohabMohie] +# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +# patreon: # Replace with a single Patreon username +# open_collective: # Replace with a single Open Collective username +# ko_fi: # Replace with a single Ko-fi username +# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +# liberapay: # Replace with a single Liberapay username +# issuehunt: # Replace with a single IssueHunt username +# otechie: # Replace with a single Otechie username +# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..40afc9d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + time: "00:00" + timezone: "Africa/Cairo" + + # Maintain dependencies for maven + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "daily" + time: "00:00" + timezone: "Africa/Cairo" \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..cab2677 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,58 @@ +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'zulu' + check-latest: true + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }} + + - name: Setup Prerequisites + continue-on-error: true + run: | + git config --global user.signingKey ${{ secrets.GPG_KEYNAME }} + git config --global commit.gpgsign true + gpg --version + gpgconf --kill gpg-agent + gpg -K --keyid-format SHORT + export GPG_TTY=$(tty) + + - name: Build SHAFT_Engine + env: + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + run: mvn -B clean package --file pom.xml -DskipTests + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 \ No newline at end of file diff --git a/.github/workflows/dependency_review.yml b/.github/workflows/dependency_review.yml new file mode 100644 index 0000000..ce0c15f --- /dev/null +++ b/.github/workflows/dependency_review.yml @@ -0,0 +1,16 @@ +name: 'Dependency Review' +on: + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-22.04 + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v4 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v3 \ No newline at end of file diff --git a/.github/workflows/mavenCentral_cd.yml b/.github/workflows/mavenCentral_cd.yml new file mode 100644 index 0000000..dafac97 --- /dev/null +++ b/.github/workflows/mavenCentral_cd.yml @@ -0,0 +1,55 @@ +name: Maven Central Continuous Delivery +# Executed automatically when a new PR is merged to master, if the release number already exists this job will fail +# This pipeline will build from main, upload the artifacts, and create the GitHub release + +on: + push: + branches: [ main ] + workflow_dispatch: + +jobs: + build_release_and_deliver: + runs-on: ubuntu-22.04 + steps: + - name: Checkout Code + uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'zulu' + check-latest: true + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }} + # Captures the engine version from the pom.xml + - name: Set Release Version Number + run: | + echo "RELEASE_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV + # Attempt to create a GitHub release using the version in the pom.xml, if this release already exists, this job will fail fast + - name: Create GitHub Release + id: create_release + uses: ncipollo/release-action@v1.13.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + allowUpdates: false + generateReleaseNotes: true + name: ${{ env.RELEASE_VERSION }} + tag: ${{ env.RELEASE_VERSION }} + - name: Setup Prerequisites + run: | + git config --global user.signingKey ${{ secrets.GPG_KEYNAME }} + git config --global commit.gpgsign true + gpg --version + gpgconf --kill gpg-agent + gpg -K --keyid-format SHORT + export GPG_TTY=$(tty) + - name: Deploy SHAFT to maven central + continue-on-error: true + env: + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + run: mvn --batch-mode deploy "-DskipTests" "-Dgpg.keyname=${{secrets.GPG_KEYNAME}}" "-Dgpg.passphrase=${{secrets.GPG_PASSPHRASE}}" \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..177e13e --- /dev/null +++ b/pom.xml @@ -0,0 +1,213 @@ + + + 4.0.0 + + io.github.shafthq + SHAFT_SIKULIX + 1.0.0 + + + + 8.1.20240127 + 1.9.21 + 3.12.1 + 3.3.1 + 3.2.5 + 3.2.5 + 5.10.1 + + + + io.github.shafthq + SHAFT_ENGINE + ${shaft_engine.version} + + + com.sikulix + sikulixapi + 2.0.5 + + + io.netty + netty-handler + + + org.openpnp + opencv + + + org.ow2.asm + asm-commons + + + org.ow2.asm + asm-tree + + + org.ow2.asm + asm + + + org.slf4j + slf4j-nop + + + org.slf4j + jul-to-slf4j + + + org.slf4j + jcl-over-slf4j + + + org.slf4j + log4j-over-slf4j + + + log4j + log4j + + + org.slf4j + slf4j-api + + + org.apache.pdfbox + pdfbox + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + 21 + 21 + UTF-8 + 10240m + 1024m + + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + UTF-8 + target/classes + + + + + + + testng + + true + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.aspectj + aspectjweaver + ${aspectjweaver.version} + + + org.apache.maven.surefire + surefire-testng + ${surefire-testng.version} + + + + + true + + false + false + false + false + UTF-8 + + -javaagent:${user.home}/.m2/repository/org/aspectj/aspectjweaver/${aspectjweaver.version}/aspectjweaver-${aspectjweaver.version}.jar + + + + usedefaultlisteners + false + + + listener + com.shaft.listeners.TestNGListener + + + + + + + + + junit + + + src/test/resources/META-INF/services/org.testng.ITestNGListener + src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.aspectj + aspectjweaver + ${aspectjweaver.version} + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter-engine.version} + + + + + true + + false + false + false + false + UTF-8 + + -javaagent:${user.home}/.m2/repository/org/aspectj/aspectjweaver/${aspectjweaver.version}/aspectjweaver-${aspectjweaver.version}.jar + + + + usedefaultlisteners + false + + + listener + com.shaft.listeners.JunitListener + + + + + + + + + diff --git a/src/main/java/com/shaft/driver/DriverFactory.java b/src/main/java/com/shaft/driver/DriverFactory.java new file mode 100644 index 0000000..8f725ea --- /dev/null +++ b/src/main/java/com/shaft/driver/DriverFactory.java @@ -0,0 +1,34 @@ +package com.shaft.driver; + +import com.shaft.tools.io.ReportManager; +import lombok.Setter; +import org.sikuli.script.App; + +@Setter +@SuppressWarnings({"UnusedReturnValue", "unused"}) +public class DriverFactory { + /** + * Attaches your SikuliActions to a specific Application instance + * + * @param applicationName the name or partial name of the currently opened application window that you want to attach to + * @return a sikuli App instance that can be used to perform SikuliActions + */ + public static App getSikuliApp(String applicationName) { +// DriverFactoryHelper.initializeSystemProperties(); + var myapp = new App(applicationName); + myapp.waitForWindow(SHAFT.Properties.timeouts.browserNavigationTimeout()); + myapp.focus(); + ReportManager.log("Opened app: [" + myapp.getName() + "]..."); + return myapp; + } + + /** + * Terminates the desired sikuli app instance + * + * @param application a sikuli App instance that can be used to perform SikuliActions + */ + public static void closeSikuliApp(App application) { + ReportManager.log("Closing app: [" + application.getName() + "]..."); + application.close(); + } +} \ No newline at end of file diff --git a/src/main/java/com/shaft/driver/SHAFT.java b/src/main/java/com/shaft/driver/SHAFT.java new file mode 100644 index 0000000..e5773bf --- /dev/null +++ b/src/main/java/com/shaft/driver/SHAFT.java @@ -0,0 +1,34 @@ +package com.shaft.driver; + +import com.shaft.gui.element.SikuliActions; +import org.sikuli.script.App; + +@SuppressWarnings("unused") +public class SHAFT { + public static class GUI { + + public static class SikuliDriver { + private final App sikuliApp; + + public SikuliDriver(String applicationName) { + sikuliApp = DriverFactory.getSikuliApp(applicationName); + } + + public static SikuliDriver getInstance(String applicationName) { + return new SikuliDriver(applicationName); + } + + public void quit() { + DriverFactory.closeSikuliApp(sikuliApp); + } + + public SikuliActions element() { + return new SikuliActions(sikuliApp); + } + + public App getDriver(String applicationName) { + return sikuliApp; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/shaft/driver/internal/FluentWebDriverAction.java b/src/main/java/com/shaft/driver/internal/FluentWebDriverAction.java new file mode 100644 index 0000000..b9bbadd --- /dev/null +++ b/src/main/java/com/shaft/driver/internal/FluentWebDriverAction.java @@ -0,0 +1,19 @@ +package com.shaft.driver.internal; + +import com.shaft.gui.element.SikuliActions; +import org.sikuli.script.App; + +public class FluentWebDriverAction { + public SikuliActions performSikuliAction() { + return new SikuliActions(); + } + public SikuliActions performSikuliAction(App applicationWindow) { + return new SikuliActions(applicationWindow); + } + public SikuliActions sikulix() { + return new SikuliActions(); + } + public SikuliActions sikulix(App applicationWindow) { + return new SikuliActions(applicationWindow); + } +} \ No newline at end of file diff --git a/src/main/java/com/shaft/gui/element/SikuliActions.java b/src/main/java/com/shaft/gui/element/SikuliActions.java new file mode 100644 index 0000000..3a3a42a --- /dev/null +++ b/src/main/java/com/shaft/gui/element/SikuliActions.java @@ -0,0 +1,375 @@ +package com.shaft.gui.element; + +import com.shaft.driver.SHAFT; +import com.shaft.gui.element.internal.ElementActionsHelper; +import com.shaft.gui.internal.image.ScreenshotManager; +import com.shaft.gui.internal.video.RecordManager; +import com.shaft.tools.io.internal.ReportManagerHelper; +import org.apache.commons.io.IOUtils; +import org.sikuli.basics.Settings; +import org.sikuli.script.*; + +import javax.imageio.ImageIO; +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +@SuppressWarnings("unused") +public class SikuliActions { + private Screen screen; + private App applicationWindow; + + public SikuliActions() { + initializeSikuliEngineForCurrentScreen(); + } + + public SikuliActions(App applicationWindow) { + initializeSikuliEngineForCurrentScreen(); + this.applicationWindow = applicationWindow; + } + + public static List prepareElementScreenshotAttachment(Screen screen, App applicationWindow, Pattern element, String actionName, boolean passFailStatus) { + return ScreenshotManager.takeScreenshotUsingSikuliX(screen, applicationWindow, element, actionName, passFailStatus); + } + + /** + * Checks if there is any text in an element, clears it, then types the required + * string into the target element. + * + * @param pathToTargetElementImage relative path to the desired element image following this example "src/test/resources/DynamicObjectRepository/" + "sikuli_googleHome_searchBox_text.PNG" + * @param text the target text that needs to be typed into the target + * element + * @return a self-reference to be used to chain actions + */ + public SikuliActions type(String pathToTargetElementImage, String text) { + return type(readImageFromFile(pathToTargetElementImage), text); + } + + /** + * Checks if there is any text in an element, clears it, then types the required + * string into the target element. + * + * @param targetElement the image of the desired element in the form of a byte[] + * @param text the target text that needs to be typed into the target + * element + * @return a self-reference to be used to chain actions + */ + public SikuliActions type(byte[] targetElement, String text) { + Pattern element = null; + try { + element = prepareElementPattern(targetElement); + clearAndType(element, text); + } catch (IOException | FindFailed rootCauseException) { + ElementActionsHelper.failAction(screen, applicationWindow, element, formatTextForReport(text), rootCauseException); + } + ElementActionsHelper.passAction(screen, applicationWindow, element, formatTextForReport(text)); + return this; + } + + /** + * ValidationEnums the required string into the target element. + * + * @param pathToTargetElementImage relative path to the desired element image following this example "src/test/resources/DynamicObjectRepository/" + "sikuli_googleHome_searchBox_text.PNG" + * @param text the target text that needs to be typed into the target + * element + * @return a self-reference to be used to chain actions + */ + public SikuliActions typeAppend(String pathToTargetElementImage, String text) { + return typeAppend(readImageFromFile(pathToTargetElementImage), text); + } + + /** + * ValidationEnums the required string into the target element. + * + * @param targetElement the image of the desired element in the form of a byte[] + * @param text the target text that needs to be typed into the target + * element + * @return a self-reference to be used to chain actions + */ + public SikuliActions typeAppend(byte[] targetElement, String text) { + Pattern element = null; + try { + element = prepareElementPattern(targetElement); + screen.wait(element).type(text); + } catch (IOException | FindFailed rootCauseException) { + ElementActionsHelper.failAction(screen, applicationWindow, element, formatTextForReport(text), rootCauseException); + } + ElementActionsHelper.passAction(screen, applicationWindow, element, formatTextForReport(text)); + return this; + } + + /** + * Checks if there is any text in an element, clears it, then types the required + * string into the target element. + * + * @param pathToTargetElementImage relative path to the desired element image following this example "src/test/resources/DynamicObjectRepository/" + "sikuli_googleHome_searchBox_text.PNG" + * @param text the target text that needs to be typed into the target + * element + * @return a self-reference to be used to chain actions + */ + public SikuliActions typeSecure(String pathToTargetElementImage, String text) { + return typeSecure(readImageFromFile(pathToTargetElementImage), text); + } + + /** + * Checks if there is any text in an element, clears it, then types the required + * string into the target element. + * + * @param targetElement the image of the desired element in the form of a byte[] + * @param text the target text that needs to be typed into the target + * element + * @return a self-reference to be used to chain actions + */ + public SikuliActions typeSecure(byte[] targetElement, String text) { + Pattern element = null; + try { + element = prepareElementPattern(targetElement); + clearAndType(element, text); + } catch (IOException | FindFailed rootCauseException) { + ElementActionsHelper.failAction(screen, applicationWindow, element, formatTextForReport(text), rootCauseException); + } + //noinspection SuspiciousRegexArgument + ElementActionsHelper.passAction(screen, applicationWindow, element, formatTextForReport(text).replaceAll(".", "•")); + return this; + } + + /** + * Clicks on a certain element using SikuliX + * + * @param pathToTargetElementImage relative path to the desired element image following this example "src/test/resources/DynamicObjectRepository/" + "sikuli_googleHome_searchBox_text.PNG" + * @return a self-reference to be used to chain actions + */ + public SikuliActions click(String pathToTargetElementImage) { + return click(readImageFromFile(pathToTargetElementImage)); + } + + /** + * Clicks on a certain element using SikuliX + * + * @param targetElement the image of the desired element in the form of a byte[] + * @return a self-reference to be used to chain actions + */ + public SikuliActions click(byte[] targetElement) { + Pattern element = null; + String elementText = null; + try { + element = prepareElementPattern(targetElement); + elementText = screen.wait(element).getText(); + screen.wait(element).click(); + } catch (IOException | FindFailed rootCauseException) { + ElementActionsHelper.failAction(screen, applicationWindow, element, elementText, rootCauseException); + } + ElementActionsHelper.passAction(screen, applicationWindow, element, formatTextForReport(elementText)); + return this; + } + + /** + * Retrieves text from the target element and returns it as a string value. + * + * @param pathToTargetElementImage relative path to the desired element image following this example "src/test/resources/DynamicObjectRepository/" + "sikuli_googleHome_searchBox_text.PNG" + * @return the text value of the target element + */ + public String getText(String pathToTargetElementImage) { + return getText(readImageFromFile(pathToTargetElementImage)); + } + + /** + * Retrieves text from the target element and returns it as a string value. + * + * @param targetElement the image of the desired element in the form of a byte[] + * @return the text value of the target element + */ + public String getText(byte[] targetElement) { + Pattern element = null; + String elementText = null; + try { + element = prepareElementPattern(targetElement); + elementText = screen.wait(element).getText().replace("\n", "").trim(); + } catch (IOException | FindFailed rootCauseException) { + ElementActionsHelper.failAction(screen, applicationWindow, element, null, rootCauseException); + } + ElementActionsHelper.passAction(screen, applicationWindow, element, formatTextForReport(elementText)); + return elementText; + } + + /** + * Hovers over target element. + * + * @param pathToTargetElementImage relative path to the desired element image following this example "src/test/resources/DynamicObjectRepository/" + "sikuli_googleHome_searchBox_text.PNG" + * @return a self-reference to be used to chain actions + */ + public SikuliActions hover(String pathToTargetElementImage) { + return hover(readImageFromFile(pathToTargetElementImage)); + } + + /** + * Hovers over target element. + * + * @param targetElement the image of the desired element in the form of a byte[] + * @return a self-reference to be used to chain actions + */ + public SikuliActions hover(byte[] targetElement) { + Pattern element = null; + String elementText = null; + try { + element = prepareElementPattern(targetElement); + elementText = screen.wait(element).getText().replace("\n", "").trim(); + screen.wait(element).hover(element); + } catch (IOException | FindFailed rootCauseException) { + ElementActionsHelper.failAction(screen, applicationWindow, element, elementText, rootCauseException); + } + ElementActionsHelper.passAction(screen, applicationWindow, element, formatTextForReport(elementText)); + return this; + } + + /** + * Double-clicks on an element using SikuliX + * + * @param pathToTargetElementImage relative path to the desired element image following this example "src/test/resources/DynamicObjectRepository/" + "sikuli_googleHome_searchBox_text.PNG" + * @return a self-reference to be used to chain actions + */ + public SikuliActions doubleClick(String pathToTargetElementImage) { + return doubleClick(readImageFromFile(pathToTargetElementImage)); + } + + /** + * Double-clicks on an element using SikuliX + * + * @param targetElement the image of the desired element in the form of a byte[] + * @return a self-reference to be used to chain actions + */ + public SikuliActions doubleClick(byte[] targetElement) { + Pattern element = null; + String elementText = null; + try { + element = prepareElementPattern(targetElement); + elementText = screen.wait(element).getText().replace("\n", "").trim(); + screen.wait(element).doubleClick(element); + } catch (IOException | FindFailed rootCauseException) { + ElementActionsHelper.failAction(screen, applicationWindow, element, elementText, rootCauseException); + } + ElementActionsHelper.passAction(screen, applicationWindow, element, formatTextForReport(elementText)); + return this; + } + + /** + * Right-clicks on an element to trigger the context menu + * + * @param pathToTargetElementImage relative path to the desired element image following this example "src/test/resources/DynamicObjectRepository/" + "sikuli_googleHome_searchBox_text.PNG" + * @return a self-reference to be used to chain actions + */ + public SikuliActions rightClick(String pathToTargetElementImage) { + return rightClick(readImageFromFile(pathToTargetElementImage)); + } + + /** + * Right-clicks on an element to trigger the context menu + * + * @param targetElement the image of the desired element in the form of a byte[] + * @return a self-reference to be used to chain actions + */ + public SikuliActions rightClick(byte[] targetElement) { + Pattern element = null; + String elementText = null; + try { + element = prepareElementPattern(targetElement); + elementText = screen.wait(element).getText().replace("\n", "").trim(); + screen.wait(element).rightClick(element); + } catch (IOException | FindFailed rootCauseException) { + ElementActionsHelper.failAction(screen, applicationWindow, element, elementText, rootCauseException); + } + ElementActionsHelper.passAction(screen, applicationWindow, element, formatTextForReport(elementText)); + return this; + } + + /** + * Drags the draggable element and drops it onto the target element + * + * @param pathToDraggableElementImage relative path to the desired element image following this example "src/test/resources/DynamicObjectRepository/" + "sikuli_googleHome_searchBox_text.PNG" + * @param pathToTargetElementImage relative path to the desired element image following this example "src/test/resources/DynamicObjectRepository/" + "sikuli_googleHome_searchBox_text.PNG" + * @return a self-reference to be used to chain actions + */ + public SikuliActions dragAndDrop(String pathToDraggableElementImage, String pathToTargetElementImage) { + return dragAndDrop(readImageFromFile(pathToDraggableElementImage), readImageFromFile(pathToTargetElementImage)); + } + + /** + * Drags the draggable element and drops it onto the target element + * + * @param draggableElement the image of the desired element in the form of a byte[] + * @param targetElement the image of the desired element in the form of a byte[] + * @return a self-reference to be used to chain actions + */ + public SikuliActions dragAndDrop(byte[] draggableElement, byte[] targetElement) { + Pattern draggableElementPattern = null; + Pattern targetElementPattern; + String elementText = null; + try { + draggableElementPattern = prepareElementPattern(draggableElement); + targetElementPattern = prepareElementPattern(targetElement); + elementText = screen.wait(draggableElementPattern).getText().replace("\n", "").trim(); + screen.wait(draggableElementPattern).dragDrop(draggableElementPattern, targetElementPattern); + } catch (IOException | FindFailed rootCauseException) { + ElementActionsHelper.failAction(screen, applicationWindow, draggableElementPattern, elementText, rootCauseException); + } + ElementActionsHelper.passAction(screen, applicationWindow, draggableElementPattern, elementText); + return this; + } + + private void clearAndType(Pattern element, String text) throws FindFailed { + String elementText = screen.wait(element).getText().replace("\n", "").trim(); + if (!elementText.isEmpty()) { + //clear + Collections.singletonList(elementText.toCharArray()).forEach(character -> { + try { + screen.wait(element).type(element, Key.BACKSPACE); + } catch (FindFailed findFailed) { + ReportManagerHelper.logDiscrete(findFailed); + } + }); + } + screen.wait(element).type(text); + } + + private byte[] readImageFromFile(String pathToTargetElementImage) { + try { + return IOUtils.toByteArray(new FileInputStream(pathToTargetElementImage)); + } catch (IOException rootCauseException) { + ElementActionsHelper.failAction(null, "Failed to initialize SikuliAction; couldn't read the target Element Image", null, rootCauseException); + return new byte[0]; + } + } + + private Pattern prepareElementPattern(byte[] targetElement) throws IOException { + if (applicationWindow != null) { + applicationWindow.waitForWindow(SHAFT.Properties.timeouts.browserNavigationTimeout()); + applicationWindow.focus(); + } + Pattern elementPattern = new Pattern(); + ByteArrayInputStream targetElementImage = new ByteArrayInputStream(targetElement); + elementPattern.setBImage(ImageIO.read(targetElementImage)); + return elementPattern; + } + + private void initializeSikuliEngineForCurrentScreen() { + Settings.setShowActions(false); + Settings.ActionLogs = true; + Settings.InfoLogs = true; + Settings.DebugLogs = true; + Settings.LogTime = true; + screen = new Screen(); + screen.setAutoWaitTimeout(SHAFT.Properties.timeouts.defaultElementIdentificationTimeout()); + RecordManager.startVideoRecording(); + } + + private String formatTextForReport(String text) { + if (text != null) { + return text.replace("\n", "").trim(); + } else { + return "NULL"; + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/shaft/gui/element/internal/ElementActionsHelper.java b/src/main/java/com/shaft/gui/element/internal/ElementActionsHelper.java new file mode 100644 index 0000000..5576481 --- /dev/null +++ b/src/main/java/com/shaft/gui/element/internal/ElementActionsHelper.java @@ -0,0 +1,123 @@ +package com.shaft.gui.element.internal; + +import com.google.common.base.Throwables; +import com.shaft.cli.FileActions; +import com.shaft.driver.SHAFT; +import com.shaft.driver.internal.DriverFactory.DriverFactoryHelper; +import com.shaft.enums.internal.ClipboardAction; +import com.shaft.enums.internal.ElementAction; +import com.shaft.gui.browser.internal.BrowserActionsHelper; +import com.shaft.gui.element.ElementActions; +import com.shaft.gui.element.SikuliActions; +import com.shaft.gui.internal.exceptions.MultipleElementsFoundException; +import com.shaft.gui.internal.image.ImageProcessingActions; +import com.shaft.gui.internal.image.ScreenshotManager; +import com.shaft.gui.internal.locator.LocatorBuilder; +import com.shaft.gui.internal.locator.ShadowLocatorBuilder; +import com.shaft.tools.internal.support.JavaHelper; +import com.shaft.tools.internal.support.JavaScriptHelper; +import com.shaft.tools.io.ReportManager; +import com.shaft.tools.io.internal.FailureReporter; +import com.shaft.tools.io.internal.ReportHelper; +import com.shaft.tools.io.internal.ReportManagerHelper; +import com.shaft.validation.internal.ValidationsHelper; +import io.appium.java_client.AppiumDriver; +import lombok.Getter; +import org.apache.logging.log4j.Level; +import org.jsoup.Jsoup; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.*; +import org.openqa.selenium.interactions.Actions; +import org.openqa.selenium.interactions.Locatable; +import org.openqa.selenium.remote.Browser; +import org.openqa.selenium.support.locators.RelativeLocator; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.FluentWait; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.sikuli.script.App; +import org.sikuli.script.Pattern; +import org.sikuli.script.Screen; +import org.testng.Assert; + +import java.awt.*; +import java.time.Duration; +import java.util.List; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@SuppressWarnings({"UnusedReturnValue", "unused"}) +public class ElementActionsHelper { + public static final String OBFUSCATED_STRING = "•"; + public static final int ELEMENT_IDENTIFICATION_TIMEOUT_INTEGER = (int) SHAFT.Properties.timeouts.defaultElementIdentificationTimeout(); + private static final boolean GET_ELEMENT_HTML = true; //TODO: expose parameter + private static final boolean FORCE_CHECK_FOR_ELEMENT_VISIBILITY = SHAFT.Properties.flags.forceCheckForElementVisibility(); + private static final int ELEMENT_IDENTIFICATION_POLLING_DELAY = 100; // milliseconds + private static final String WHEN_TO_TAKE_PAGE_SOURCE_SNAPSHOT = SHAFT.Properties.visuals.whenToTakePageSourceSnapshot(); + + public static void passAction(Screen screen, App applicationWindow, Pattern element, String testData) { + String actionName = Thread.currentThread().getStackTrace()[2].getMethodName(); + List> attachments = new LinkedList<>(); + attachments.add(SikuliActions.prepareElementScreenshotAttachment(screen, applicationWindow, element, actionName, true)); + passAction(null, null, actionName, testData, attachments, null); + } + public static void passAction(WebDriver driver, By elementLocator, String actionName, String testData, List> screenshots, String elementName) { + com.shaft.gui.element.internal.ElementActionsHelper.reportActionResult(driver, actionName, testData, elementLocator, screenshots, elementName, true); + } + + public static void failAction(Screen screen, App applicationWindow, Pattern element, String testData, Throwable... rootCauseException) { + String actionName = Thread.currentThread().getStackTrace()[2].getMethodName(); + List> attachments = new LinkedList<>(); + attachments.add(SikuliActions.prepareElementScreenshotAttachment(screen, applicationWindow, element, actionName, false)); + failAction(null, actionName, testData, null, attachments, rootCauseException); + } + + public static void failAction(WebDriver driver, String actionName, String testData, By elementLocator, List> screenshots, Throwable... rootCauseException) { + //TODO: merge all fail actions, make all methods call this one, get elementName where applicable instead of reporting null + //this condition works if this is the first level of failure, but the first level is usually caught by the calling method + +// boolean skipPageScreenshot = rootCauseException.length >= 1 && ( +// TimeoutException.class.getName().equals(rootCauseException[0].getClass().getName()) //works to capture fluent wait failure +// || ( +// rootCauseException[0].getMessage().contains("Identify unique element") +// && isFoundInStacktrace(ValidationsHelper.class, rootCauseException[0]) +// )//works to capture calling elementAction failure in case this is an assertion +// ); + + //don't take a second screenshot in case of validation failure because the original element action will have always failed first + boolean skipPageScreenshot = rootCauseException.length >= 1 && (com.shaft.gui.element.internal.ElementActionsHelper.isFoundInStacktrace(ValidationsHelper.class, rootCauseException[0]) && isFoundInStacktrace(ElementActionsHelper.class, rootCauseException[0])); + + String elementName = elementLocator != null ? com.shaft.gui.element.internal.ElementActionsHelper.formatLocatorToString(elementLocator) : ""; + if (elementLocator != null && (rootCauseException.length >= 1 && Throwables.getRootCause(rootCauseException[0]).getClass() != MultipleElementsFoundException.class && Throwables.getRootCause(rootCauseException[0]).getClass() != NoSuchElementException.class && Throwables.getRootCause(rootCauseException[0]).getClass() != InvalidSelectorException.class)) { + try { + var accessibleName = ((WebElement) com.shaft.gui.element.internal.ElementActionsHelper.identifyUniqueElement(driver, elementLocator).get(1)).getAccessibleName(); + if (accessibleName != null && !accessibleName.isBlank()) { + elementName = accessibleName; + } + } catch (WebDriverException e) { + //happens on some elements that show unhandled inspector error + //this exception is thrown on some older selenium grid instances, I saw it with firefox running over selenoid + //ignore + } + } + + String message; + if (skipPageScreenshot) { + //don't try to take a screenshot again and set element locator to null in case element was not found by timeout or by nested element actions call + message = com.shaft.gui.element.internal.ElementActionsHelper.createReportMessage(actionName, testData, elementName, false); + ReportManager.logDiscrete(message); + } else { + if (rootCauseException.length >= 1) { + message = com.shaft.gui.element.internal.ElementActionsHelper.reportActionResult(driver, actionName, testData, elementLocator, screenshots, elementName, false, rootCauseException[0]); + } else { + message = com.shaft.gui.element.internal.ElementActionsHelper.reportActionResult(driver, actionName, testData, null, screenshots, elementName, false); + } + } + if (rootCauseException.length >= 1) { + Assert.fail(message, rootCauseException[0]); + } else { + Assert.fail(message); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/shaft/gui/internal/image/ScreenshotManager.java b/src/main/java/com/shaft/gui/internal/image/ScreenshotManager.java new file mode 100644 index 0000000..e6f0017 --- /dev/null +++ b/src/main/java/com/shaft/gui/internal/image/ScreenshotManager.java @@ -0,0 +1,91 @@ +package com.shaft.gui.internal.image; + +import com.epam.healenium.SelfHealingDriver; +import com.shaft.driver.SHAFT; +import com.shaft.driver.internal.DriverFactory.DriverFactoryHelper; +import com.shaft.enums.internal.Screenshots; +import com.shaft.gui.browser.internal.JavaScriptWaitManager; +import com.shaft.gui.element.internal.ElementActionsHelper; +import com.shaft.gui.element.internal.ElementInformation; +import com.shaft.tools.io.ReportManager; +import com.shaft.tools.io.internal.ReportManagerHelper; +import lombok.SneakyThrows; +import org.openqa.selenium.Rectangle; +import org.openqa.selenium.*; +import org.openqa.selenium.support.locators.RelativeLocator; +import org.sikuli.script.App; +import org.sikuli.script.FindFailed; +import org.sikuli.script.Pattern; +import org.sikuli.script.Screen; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class ScreenshotManager { + + public static List takeScreenshotUsingSikuliX(Screen screen, App applicationWindow, Pattern element, String actionName, + boolean passFailStatus) { + com.shaft.gui.internal.image.ScreenshotManager.globalPassFailStatus = passFailStatus; + if (passFailStatus) { + com.shaft.gui.internal.image.ScreenshotManager.globalPassFailAppendedText = "passed"; + } else { + com.shaft.gui.internal.image.ScreenshotManager.globalPassFailAppendedText = "failed"; + } + boolean takeScreenshot = "Always".equals(SHAFT.Properties.visuals.screenshotParamsWhenToTakeAScreenshot()) + || ("ValidationPointsOnly".equals(SHAFT.Properties.visuals.screenshotParamsWhenToTakeAScreenshot()) + && (actionName.toLowerCase().contains("assert") + || actionName.toLowerCase().contains("verify"))) + || !passFailStatus; + if (takeScreenshot || (SHAFT.Properties.visuals.createAnimatedGif() && (AnimatedGifManager.DETAILED_GIF || actionName.matches(AnimatedGifManager.DETAILED_GIF_REGEX)))) { + /* + * Take the screenshot and store it as a file + */ + byte[] src = null; + try { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + switch (Screenshots.getType()) { + case ELEMENT: + if (element != null) { + try { + ImageIO.write(screen.capture(screen.wait(element).getRect()).getImage(), "png", byteArrayOutputStream); + src = byteArrayOutputStream.toByteArray(); + break; + } catch (FindFailed e) { + //do nothing and fall into the next type of screenshot + } + } + case VIEWPORT: + if (applicationWindow != null) { + ImageIO.write(screen.capture(applicationWindow.waitForWindow()).getImage(), "png", byteArrayOutputStream); + src = byteArrayOutputStream.toByteArray(); + break; + } + case FULL: + ImageIO.write(screen.capture().getImage(), "png", byteArrayOutputStream); + src = byteArrayOutputStream.toByteArray(); + break; + default: + break; + } + } catch (IOException e) { + ReportManager.logDiscrete("Failed to create attachment."); + ReportManagerHelper.logDiscrete(e); + } + + AnimatedGifManager.startOrAppendToAnimatedGif(src); + if (takeScreenshot) { + return com.shaft.gui.internal.image.ScreenshotManager.prepareImageForReport(src, actionName); + } else { + return null; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/test/java/testPackage/SikulixTests.java b/src/test/java/testPackage/SikulixTests.java new file mode 100644 index 0000000..ec8cdca --- /dev/null +++ b/src/test/java/testPackage/SikulixTests.java @@ -0,0 +1,60 @@ +package testPackage; + +import com.shaft.cli.TerminalActions; +import com.shaft.driver.DriverFactory; +import com.shaft.driver.SHAFT; +import com.shaft.gui.browser.BrowserActions; +import com.shaft.gui.element.ElementActions; +import com.shaft.gui.element.SikuliActions; +import com.shaft.validation.Validations; +import org.openqa.selenium.WebDriver; +import org.sikuli.script.App; +import org.sikuli.script.Key; +import org.testng.annotations.Test; + +public class SikulixTests { + App calculator; + String pathToCalculatorElementsFolder = "src/test/resources/DynamicObjectRepository/calculator/"; + + //@Test + @SuppressWarnings("CommentedOutCode") + public void sampleWithSeleniumWebDriver() { + new BrowserActions().navigateToURL("https://www.google.com/ncr", "https://www.google.com"); +// byte[] searchTextBox = ScreenshotManager.takeElementScreenshot(driver, By.xpath("//input[@name='q']")); +// ElementActions.performSikuliAction(searchTextBox).type("SHAFT_Engine trial using SikuliX1" + Key.ENTER); + String pathToTargetElementImage = "src/test/resources/DynamicObjectRepository/" + "sikuli_googleHome_searchBox_text.PNG"; + new SikuliActions().click(pathToTargetElementImage).type(pathToTargetElementImage, "SHAFT_Engine trial using SikuliX1" + Key.ENTER); + new BrowserActions().closeCurrentWindow(); + } + + @Test + public void sampleWithSeleniumAndYoutube() { + WebDriver driver = new DriverFactory().getDriver(); + new BrowserActions(driver).navigateToURL("https://www.youtube.com/watch?v=6FbpNgZ8fZ8&t=2s"); + String pathToTargetElementImage = SHAFT.Properties.paths.testData() + "sikulixElements/youtube.png"; + new ElementActions(driver).performSikuliAction().click(pathToTargetElementImage); + Validations.assertThat().browser(driver).url().isEqualTo("https://www.youtube.com/").perform(); + } + + @Test + public void sampleWithDesktopApplication() { + String result = new SikuliActions(calculator).click(pathToCalculatorElementsFolder + "1.png") + .click(pathToCalculatorElementsFolder + "+.png") + .click(pathToCalculatorElementsFolder + "3.png") + .click(pathToCalculatorElementsFolder + "=.png") + .getText(pathToCalculatorElementsFolder + "result.png"); + Validations.assertThat().object(result).isEqualTo("4").perform(); + } + + //@BeforeClass + public void openApplication() { + new TerminalActions().performTerminalCommand("calc"); + calculator = DriverFactory.getSikuliApp("Calculator"); + } + + //@AfterClass(alwaysRun = true) + public void closeApplication() { + DriverFactory.closeSikuliApp(calculator); + } + +} diff --git a/src/test/resources/testDataFiles/sikulixElements/youtube.png b/src/test/resources/testDataFiles/sikulixElements/youtube.png new file mode 100644 index 0000000000000000000000000000000000000000..c5b979828d384ea19edb553a5f9f936139ae3c40 GIT binary patch literal 1532 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1(8WaK~#8N?U_wT zRZSSjpZmH97nPB?$%VR7!B7-+A!uMhAQ&j5ZBhe41#u(p(iC@!9}9yZa-#?)nFyjT z3JJ^)`dBC;Y6-e%W18UysPmrxoSDmcug81Oxp{9o@BCmmGtbPNGo1fK^4RKS?=&K(bunL{`+?`xgjcwC^gm0$Hfs97MiyI0c&e()YjHU`}_OC zLR3>zL+R=1G&wm*BO@cUx3@uu_7ZQg_US|d71k9`nWwNCWbzI_&^H_3shNINf8kdqLoV^ zs9y>Tp?GLgR1{-Be~Je&*3r?y;^X64PEL-nFg7?i$ZbeFJ3GSa4%y!>O;1mYR;NLo zlXihvU0r4E?d{C)LSZp6L}n8c6Qab}@$oVH{rfk=FU?>HyoSDgD>5NqdzDskxJnxp;+FG7)4}cj!+Jf=;U|3mMq3Y^tDladm*49?< z^n2sR4Ss$8{5d^&@`TSO2L}h-78F8Qdp>`*_pI|wjg8`ACbw_j<{mLVKF-Vc@89R; zmoH!P@}D4QfQJts3X6Zj;h|$=W1_^^*47s5>FGJu&JF%O6dvRR1Uq?oqVQ*AWo7ZJ z^vtEDC4QCb7uMPO`g$rYEu~klUeT{#zqlP9-PP5_lk{$U?I>d{v;%h82iV=i3hsyJ z8q=h(n4sL!(sF7{fk;V7A>%`#_4RdLM!%z@Bc2Z$A*?;`-r0MVZ_3Lv+rN3kSX`Wa zo3j$uXbW=9gcnH%#62PD8U$}rlv&6^ll)%8)K67zGK36#F$F? z_+`q-$l#GTwl25_Y{myhX=!PqS8#;pV4zDl*GhDo|jV_M8*r+-QD$; zlarHq9{5gyp#TA!b!<{l1aJUjvIbaAr`_`=bSFjF_noo#3xw560bb_cdO^NeBki-+ zX5gmr3~f9uQncqqif@gRaCw?~BXeD@M!747NMr!+7y;7#)AGTitV z)M=#p<%|1>_>v;ma)@c?h0V>)d_NkF$lDJ8f`-F;96rOkt|H*DzA)7Jt3L~Cq#US^ z+=|G94%ycl^+N1kBW^(3iyP(QH&>{*e%;kNZEU{zwY$5U3JMB%9xqgii8ERt>+|#T zdHF1?g5s|V_JUF%U&hDBz0bUQ^{RJmuc)ZtS3EOSe4N%Xg`0>b1Ch4}1H*Ls