├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── nightly_release.yml ├── .gitignore ├── .husky └── commit-msg ├── .vscode ├── launch.json └── settings.json ├── .yo-rc.json ├── BUILD_SIZE_REDUCTION.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── docs ├── Contribution.md ├── JourneyRecorder.mp4 ├── img │ ├── ExtensionMenu.png │ └── LoadUnpacked.png ├── img_1.png ├── img_2.png ├── img_3.png ├── img_4.png ├── img_5.png ├── img_6.png ├── img_7.png ├── img_8.png └── journeyRecorder.gif ├── karma-ci-cov.conf.js ├── karma-ci.conf.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── tsconfig.json ├── ui5-dist.yaml ├── ui5.yaml ├── utils └── deployBuild.js └── webapp ├── Component.ts ├── assets ├── .gitkeep ├── icons │ ├── icon │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 32.png │ │ ├── 48.png │ │ ├── 64.png │ │ ├── 80.png │ │ └── 96.png │ ├── icon_old.png │ ├── icon_old.svg │ ├── ui5_journey_recorder.png │ └── ui5_journey_recorder_wb.png └── scripts │ ├── communication_inject.js │ ├── content_inject.js │ ├── page_inject.js │ └── starter.js ├── control ├── CodeViewer.gen.d.ts ├── CodeViewer.ts ├── CodeViewerRenderer.ts ├── EditableText.gen.d.ts ├── EditableText.ts └── EditableTextRenderer.ts ├── controller ├── App.controller.ts ├── BaseController.ts ├── JourneyPage.controller.ts ├── Main.controller.ts ├── StepPage.controller.ts └── dialogs │ ├── BaseDialogController.ts │ └── Settings.controller.ts ├── css ├── control │ └── editableText.css ├── hljs-github-theme.css ├── journeyPage.css ├── main.css └── stepPage.css ├── favicon.ico ├── fragment ├── RecordingDialog.fragment.xml ├── ReplayStartDialog.fragment.xml ├── SettingsDialog.fragment.xml ├── StepTypeMenu.fragment.xml └── TestFrameworkMenu.fragment.xml ├── i18n ├── i18n.properties ├── i18n_de.properties └── i18n_en.properties ├── index.html ├── manifest.json ├── model ├── class │ ├── Journey.class.ts │ ├── RequestBuilder.class.ts │ ├── Step.class.ts │ ├── Utils.class.ts │ └── generator │ │ ├── common │ │ ├── Generator.class.ts │ │ ├── JourneyGenerator.class.ts │ │ └── PageGenerator.class.ts │ │ ├── opa5 │ │ ├── OPA5Journey.class.ts │ │ ├── OPA5Page.class.ts │ │ └── OPA5Templates.ts │ │ └── wdi5 │ │ ├── Wdi5Journey.class.ts │ │ ├── Wdi5Page.class.ts │ │ └── Wdi5Templates.ts ├── enum │ ├── ConnectionStatus.ts │ ├── StepType.ts │ ├── TestFrameworks.ts │ └── Themes.ts ├── formatter.ts └── models.ts ├── service ├── ChromeExtension.service.ts ├── CodeGeneration.service.ts ├── JourneyStorage.service.ts └── SettingsStorage.service.ts ├── test ├── e2e │ ├── sample.test.ts │ ├── tsconfig.json │ └── wdio.conf.ts ├── integration │ ├── HelloJourney.ts │ ├── opaTests.qunit.html │ ├── opaTests.qunit.ts │ └── pages │ │ └── MainPage.ts ├── testsuite.qunit.html ├── testsuite.qunit.ts └── unit │ ├── class │ ├── Journey.class.qunit.ts │ └── Step.class.qunit.ts │ ├── controller │ └── Main.qunit.ts │ ├── service │ └── JourneyStorage.service.qunit.ts │ ├── unitTests.qunit.html │ └── unitTests.qunit.ts └── view ├── App.view.xml ├── JourneyPage.view.xml ├── Main.view.xml ├── StepPage.view.xml └── dialogs └── Settings.view.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | # We recommend you to keep these unchanged 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | # Change these settings to your own preference 15 | indent_style = tab 16 | indent_size = 2 17 | 18 | [*.{yaml,yml}] 19 | indent_style = space 20 | 21 | [*.md] 22 | indent_style = unset 23 | trim_trailing_whitespace = false 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es6: true, 6 | node: true, 7 | }, 8 | extends: [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 12 | ], 13 | parser: "@typescript-eslint/parser", 14 | parserOptions: { 15 | tsconfigRootDir: __dirname, 16 | project: ["./tsconfig.json"], 17 | sourceType: "module", 18 | }, 19 | plugins: ["@typescript-eslint"], 20 | ignorePatterns: [".eslintrc.js"], 21 | ignores: ["blub/**"] 22 | }; 23 | -------------------------------------------------------------------------------- /.github/workflows/nightly_release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Create Nightly Release 5 | 6 | on: 7 | push: 8 | branches: 9 | - develop 10 | paths: 11 | - 'webapp/**' 12 | jobs: 13 | build-and-release: 14 | if: "!startsWith(github.event.head_commit.message, 'chore') && !startsWith(github.event.head_commit.message, 'docs') && !startsWith(github.event.head_commit.message, 'ci')" 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: "20.x" 22 | cache: "npm" 23 | - name: Install dependencies 24 | run: npm ci 25 | - name: Configure committer 26 | run: | 27 | git config user.name "UI5 journey recorder bot" 28 | git config user.email "ui5-journey-recorder-bot@users.noreply.github.com" 29 | - name: Create changelog and increase version 30 | run: npm run changelog -- --prerelease nightly 31 | - name: Build the extension for deploy 32 | run: npm run deployBuild -- --pre 33 | working-directory: ${{ github.workspace }} 34 | - name: Extract versions 35 | id: version_extract 36 | run: | 37 | echo "VERSION=$(cat package.json | grep -sw '"\bversion\b"' | cut -d '"' -f 4)" >> "$GITHUB_ENV" 38 | echo "FILE_VERSION=$(cat package.json | grep -sw '"\bversion\b"' | cut -d '"' -f 4 | sed "s/\./\-/g")" >> "$GITHUB_ENV" 39 | - name: Push version and changlog 40 | run: git push --follow-tags origin develop 41 | - name: Create Github Release 42 | id: create_release 43 | uses: actions/create-release@v1 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 46 | with: 47 | tag_name: ${{ env.VERSION }}_nightly 48 | release_name: Nightly-Release $env.version 49 | body: | 50 | Automatic generated nightly release with the latest features and bugfixes 51 | draft: false 52 | prerelease: true 53 | - name: Upload release artifact 54 | id: upload_release_artifact 55 | uses: actions/upload-release-asset@v1 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | with: 59 | upload_url: ${{ steps.create_release.outputs.upload_url }} 60 | asset_path: ./deployments/journey_recorder_nightly_${{ env.FILE_VERSION }}.zip 61 | asset_name: journey_recorder_nightly_${{ env.FILE_VERSION }}.zip 62 | asset_content_type: application/zip 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build results 2 | dist 3 | coverage 4 | deploySelection 5 | deployments 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Dependency directories 15 | node_modules/ 16 | 17 | .DS_Store 18 | .env 19 | blub 20 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx commitlint --edit 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "attach", 10 | "name": "Attach Karma Chrome", 11 | "address": "localhost", 12 | "port": 9333, 13 | "pathMapping": { 14 | "/": "${workspaceRoot}/", 15 | "/base/": "${workspaceRoot}/" 16 | } 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit", 4 | "source.fixAll.stylelint": "explicit" 5 | }, 6 | "eslint.validate": [ 7 | "javascript", 8 | "javascriptreact", 9 | "typescript", 10 | "typescriptreact" 11 | ], 12 | "html.format.wrapAttributes": "force-aligned", 13 | "java.configuration.updateBuildConfiguration": "interactive", 14 | "scss.validate": false, 15 | "stylelint.validate": [ 16 | "css", 17 | "less", 18 | "postcss", 19 | "scss" 20 | ], 21 | "angular.enable-strict-mode-prompt": false, 22 | "typescript.tsdk": "node_modules\\typescript\\lib" 23 | } -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-ui5-ts-app": { 3 | "namespace": "com.ui5.journeyrecorder", 4 | "framework": "OpenUI5", 5 | "frameworkVersion": "1.120.6", 6 | "author": "Adrian Marten", 7 | "initrepo": false, 8 | "tstypes": "@openui5/types", 9 | "tstypesVersion": "1.120.6", 10 | "appId": "com.ui5.journeyrecorder", 11 | "appURI": "com/ui5/journeyrecorder", 12 | "cdnDomain": "sdk.openui5.org", 13 | "defaultTheme": "sap_horizon", 14 | "gte1_98_0": true, 15 | "gte1_104_0": true, 16 | "gte1_115_0": true, 17 | "setupCompleted": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BUILD_SIZE_REDUCTION.md: -------------------------------------------------------------------------------- 1 | - removing layout library -> 208.89 MB 2 | - "--exclude-task=createDebugFiles" -> didn't work 3 | - using UI5 Webcomponents -> 244.87 MB 4 | - replacing all components with WebComp -> breaks 5 | - removeFromBuild util-script -> (208.89MB) 92.69MB 6 | - remove types.ts from assets/scripts -> 208.89 MB 7 | - ui5-minify-xml task -> 208.84 MB 8 | - ui5-task-minifier -> 208.84 MB 9 | - add removing messagebundles for unnecessary languages to the removeFromBuildScript -> 92.48 MB -> 87.6 MB 10 | - removing less files from themes -> 80.91 MB 11 | - creating a selective bundling script only for copying necessary stuff -> 43.7 MB 12 | - rebuild code viewer with highlight.js (removing core.ui.codeeditor) -> 178.14 (208.84) -> removeFromBuild.js 71.94 13 | - merge dark and bright css -> 178.13MB 14 | - deploy script -> 43.16MB 15 | - using webcomponents instead of sap.m -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.3.5](https://ui5-community///compare/v0.3.4...v0.3.5) (2025-01-21) 6 | 7 | 8 | ### Features 9 | 10 | * **no-record:** automatic nav to main if no steps were recorded ([b433e54](https://ui5-community///commit/b433e54c47cef4aa758b907bdd13407d8653f02d)) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * **chrome-130:** fixed the issue with dynamically loaded scripts ([0a1c18b](https://ui5-community///commit/0a1c18b73f719d7ee49b1fab9e166520a456a3af)) 16 | * **code-gen:** fixed wrong indentation within generated sources ([446f72c](https://ui5-community///commit/446f72c6b2c56b93cf56b38fc303c74c786f2bf7)) 17 | * **content-inject:** fixed src type within content-inject ([76107ca](https://ui5-community///commit/76107cad4514df1ec3e494b878f6a8977cb26f71)) 18 | * **settings:** changed structure of settings dialog ([d5d0c6f](https://ui5-community///commit/d5d0c6f59c0327e70ae2d305b0c61d049e0643c3)) 19 | * **ui5:** fixed Typos and misspellings after version upgrade ([5f25d78](https://ui5-community///commit/5f25d788833c2dc6c3099f320824c679febf9d06)) 20 | * **unnecessary:** removed unnecessary type from style ([9afb553](https://ui5-community///commit/9afb553d3f5919ddaf5c3d70f777040ec853a9e9)) 21 | 22 | ### [0.3.4](https://ui5-community///compare/v0.3.1...v0.3.4) (2024-12-20) 23 | 24 | 25 | ### Features 26 | 27 | * **comm:** added comments as generation part ([1cca09d](https://ui5-community///commit/1cca09d0d9d374cccf60bf966423a7ab9a4efe11)) 28 | * **generation:** started rebuild of code generation ([f0a10b6](https://ui5-community///commit/f0a10b6535444af1e274a4c25694df4c790d0566)) 29 | * **generator:** added final steps for new code generation setup ([aead91b](https://ui5-community///commit/aead91b0d7458819c1ebb6e89cf56895af840eba)) 30 | * **journey:** added dnd for reordering journey steps ([319630f](https://ui5-community///commit/319630ffddfb02534e2c07fb72fe7f9164a5093e)) 31 | * **templ:** added template demo approach ([62bf137](https://ui5-community///commit/62bf1377c29d45af832cbd9efb5262f2b83627cd)) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * **project:** dependency update ([0c4715e](https://ui5-community///commit/0c4715e80198fd0ebe98a5a428e4fab964990550)) 37 | * **small:** fixed different smaller issues ([fc5295a](https://ui5-community///commit/fc5295ae49d23aaa7e6cb841b6f7d5b01a60690d)) 38 | * **StepConversion:** fixed the conversion to input steps ([71413a4](https://ui5-community///commit/71413a4b8b90fa2e3305acf013688c6032229c56)) 39 | * **test-gen:** fixed some test-code generation bugs ([379d43f](https://ui5-community///commit/379d43fc5254d30301731f0caa787e03bb08fbd0)) 40 | 41 | ### [0.3.3](https://github.com/ui5-community/ui5-journey-recorder/compare/v0.3.2...v0.3.3) (2024-06-05) 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * **project:** dependency update ([f261ac2](https://github.com/ui5-community/ui5-journey-recorder/commit/f261ac2abcb5d0bf59b18a9d7f7203064af0c878)) 47 | * **StepConversion:** fixed the conversion to input steps ([ac1224a](https://github.com/ui5-community/ui5-journey-recorder/commit/ac1224aacf5267bdd58b7d61c1bea3c158321ca8)) 48 | 49 | ### [0.3.2](https://github.com/ui5-community/ui5-journey-recorder/compare/v0.3.1...v0.3.2) (2024-03-05) 50 | 51 | 52 | ### Features 53 | 54 | * **journey:** added dnd for reordering journey steps ([319630f](https://github.com/ui5-community/ui5-journey-recorder/commit/319630ffddfb02534e2c07fb72fe7f9164a5093e)) 55 | 56 | ### 0.3.1 (2024-02-28) 57 | 58 | ## 0.3.0 (2024-02-26) 59 | 60 | 61 | ### Features 62 | 63 | * Refactoring of the code page to contain a folder-like left side drawer ([4f74f3d](https://github.com/ui5-community/ui5-journey-recorder/commit/4f74f3d9d0e75c51e924708ee2d638fe58bb6ba2)) 64 | 65 | 66 | ### Bug Fixes 67 | 68 | * bad open method string ([5eb7976](https://github.com/ui5-community/ui5-journey-recorder/commit/5eb7976e9e35b0e137fcb82c2712b1a73f3c17c1)) 69 | 70 | ## 0.2.0 (2022-10-17) 71 | 72 | 73 | ### Features 74 | 75 | * wdi5 code generator ([#17](https://github.com/ui5-community/ui5-journey-recorder/issues/17)) ([51c83ea](https://github.com/ui5-community/ui5-journey-recorder/commit/51c83ea0b6ba6bcced382a827be5250c879aaebd)) 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | 204 | /* 205 | * ---------------------------------------------------------------------------- 206 | * "THE DERIVED BEER-WARE LICENSE" (Revision 2): 207 | * You can do whatever you want with this stuff. When you like it, just buy 208 | * Adrian Marten (@tabris87) or 209 | * any contributor https://github.com/ui5-community/ui5-journey-recorder/graphs/contributors of your 210 | * choice a beer when you see them. 211 | * 212 | * Inspired by the official: https://fedoraproject.org/wiki/Licensing/Beerware 213 | * 214 | * "THE BEER-WARE LICENSE" (Revision 42): 215 | * wrote this file. As long as you retain this notice you 216 | * can do whatever you want with this stuff. If we meet some day, and you think 217 | * this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp 218 | * ---------------------------------------------------------------------------- 219 | */ 220 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UI5 Journey Recorder 2 | 3 | The _UI5 Journey Recorder_ is a [Chrome](https://www.google.com/chrome/)-Extension. 4 | It is used to record and generate test journeys for [UI5](https://ui5.sap.com). 5 | 6 | The recorded journeys can be used to generate [OPA5](https://ui5.sap.com/#/topic/7cdee404cac441888539ed7bfe076e57) or [wdi5](https://github.com/ui5-community/wdi5) test code. 7 | 8 |

9 | 10 |

11 | 12 | # Installation 13 | 14 | The _UI5 Journey Recorder_ can be installed via the [Chrome WebStore](https://chrome.google.com/webstore/detail/ui5-journey-recorder/clhcepeibbgcdmhalaaomdpofecmgimf). 15 | After that the _UI5 Journey Recorder_ can be accessed via the Extension Menu at the top right of **Chrome**. 16 | Another way to install the _UI5 Journey Recorder_ is via the setup of the [development environment](./docs/Contribution.md).
17 | In the end you can open the extension via the extension menu at the top right of Chrome: 18 | ![Extension Menu](./docs/img/ExtensionMenu.png) 19 | 20 | Another way to install the _UI5 Journey Recorder_ is to download the zip of a release from the release section of github and unpack it where you want. 21 | Then go to the extension site of chrome ([chrome://extensions](chrome://extensions)) and load the unpacked extension by using the button at the left top corner ![Load unpacked](./docs/img/LoadUnpacked.png). 22 | 23 | ## License 24 | 25 | This work is dual-licensed under Apache 2.0 and the Derived Beer-ware 🍺 License. The official license will be Apache 2.0 but finally you can choose between one of them if you use this work. 26 | 27 | Thus, when you like this stuff, buy [any (or all 😆) of the contributors](https://github.com/ui5-community/ui5-journey-recorder/graphs/contributors) a beer when you see them. 28 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | "body-leading-blank": [0] 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /docs/Contribution.md: -------------------------------------------------------------------------------- 1 | # Contribution 2 | 3 | Because the _UI5 Journey Recorder_ is a Chrome Extension the Chrome-Webbrowser is required. 4 | 5 | ## Prerequisites 6 | 7 | - UI5 app running in the browser, accessible via `http(s)://host.ext:port`. 8 | Recommended tooling for this is either the official [UI5 tooling](https://github.com/SAP/ui5-tooling) (`ui5 serve`) or some standalone http server like [`soerver`](https://github.com/vobu/soerver) or [`http-server`](https://www.npmjs.com/package/http-server). 9 | - UI5 app using UI5 >= `1.60` (because the [`RecordReplay` API](https://ui5.sap.com/sdk/#/api/sap.ui.test.RecordReplay) is used extensively which is only available from UI5 `1.60`+) 10 | - Node.js version >= `14` (`lts/fermium`) 11 | - Angular CLI 14 (because the extension is based on Angular 14) (`npm install -g @angular/cli`) 12 | 13 | ## set up the dev environment 14 | 1. Clone the repo from [Github](https://github.com/ui5-community/ui5-journey-recorder.git) 15 | 2. Navigate to the app folder of the repository and install the dependencies `npm install` 16 | 3. Execute the build:dev `npm run build:dev` to build 17 | 3.1. OR Execute the build:watch `npm run build:watch` 18 | 5. Open Chrome and the [Chrome-Extension Page chrome://extensions](chrome://extensions) 19 | 6. Choose the "Load unpacked" Button in the top left 20 | ![Load unpacked](./img/LoadUnpacked.png) 21 | 7. Choose the dist-folder `\dist` 22 | 8. Now open the *UI5 Journey Recorder* from the extension menu at the top right of Chrome 23 |
(Use the pin to make the extension always accessible) 24 | ![Extension Menu](./img/ExtensionMenu.png) 25 | 26 | ## extension documentation 27 | 28 | The documentation for the code is done via [Compodoc](https://www.npmjs.com/package/@compodoc/compodoc). 29 | Therefore you just have to execute the *doc, doc:serve or docu:coverage* NPM-Script by `npm run doc`. 30 | After this you can open the documentation from `app\documentation\index.html`. 31 | 32 | ## work on the *UI5 Journey Recorder* 33 | The *UI5 Journey Recorder* consists of two parts: 34 | - the injected content 35 | - the popup content 36 | 37 | ### Injected content 38 | This is the part which is injected into the page where the journey should be recorded. Here you can find the: 39 | - page_inject 40 | - communication_inject 41 | - content_inject 42 | 43 | #### page_inject 44 | The page_inject contains all necessary functionality to detect ui5 controls, replay actions gather control informations 45 | 46 | #### communication_inject 47 | The communication_inject contains a REST-Like interface to communicate with the popup-Part of the Extension. 48 | This should enable an easier extension and maintenance of the extension-page-communication. 49 | 50 | #### content_inject 51 | The content_inject is the bridge between the page and the extension-popup it contains the load-up for the page_inject and communication_inject.
52 | Additonally it creates a "bridge"/"passthrough" for the communication-events. 53 | 54 | ## debugging 55 | All parts of the extension can be debugged with the default Chrome-Debbuging tools. 56 | 57 | ## commiting changes 58 | Changes are only allowed via Pull-Requests. 59 | We have a setup of eslint for eslint please be aware of these. 60 | 61 | "More to be comming" 62 | -------------------------------------------------------------------------------- /docs/JourneyRecorder.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/docs/JourneyRecorder.mp4 -------------------------------------------------------------------------------- /docs/img/ExtensionMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/docs/img/ExtensionMenu.png -------------------------------------------------------------------------------- /docs/img/LoadUnpacked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/docs/img/LoadUnpacked.png -------------------------------------------------------------------------------- /docs/img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/docs/img_1.png -------------------------------------------------------------------------------- /docs/img_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/docs/img_2.png -------------------------------------------------------------------------------- /docs/img_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/docs/img_3.png -------------------------------------------------------------------------------- /docs/img_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/docs/img_4.png -------------------------------------------------------------------------------- /docs/img_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/docs/img_5.png -------------------------------------------------------------------------------- /docs/img_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/docs/img_6.png -------------------------------------------------------------------------------- /docs/img_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/docs/img_7.png -------------------------------------------------------------------------------- /docs/img_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/docs/img_8.png -------------------------------------------------------------------------------- /docs/journeyRecorder.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/docs/journeyRecorder.gif -------------------------------------------------------------------------------- /karma-ci-cov.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | require("./karma-ci.conf")(config); 3 | config.set({ 4 | reporters: ["progress", "coverage"], 5 | preprocessors: { 6 | "webapp/**/*.ts": ["ui5-transpile"] 7 | }, 8 | coverageReporter: { 9 | dir: "coverage", 10 | reporters: [ 11 | { type: "html", subdir: "report-html" }, 12 | { type: "cobertura", subdir: ".", file: "cobertura.txt" }, 13 | { type: "lcovonly", subdir: ".", file: "report-lcovonly.txt" }, 14 | { type: "text-summary" } 15 | ] 16 | } 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /karma-ci.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | require("./karma.conf")(config); 3 | config.set({ 4 | browsers: ["ChromeHeadless"], 5 | singleRun: true 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // karma-ui5 usage: https://github.com/SAP/karma-ui5 2 | module.exports = function (config) { 3 | config.set({ 4 | frameworks: ["ui5"], 5 | browsers: ["Chrome"], 6 | ui5: { 7 | testpage: "webapp/test/testsuite.qunit.html" 8 | } 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.ui5.journeyrecorder", 3 | "version": "0.3.5", 4 | "description": "UI5 Application: com.ui5.journeyrecorder", 5 | "author": "Adrian Marten", 6 | "license": "Apache-2.0", 7 | "scripts": { 8 | "clean": "rimraf dist coverage", 9 | "build": "ui5 build self-contained -a --clean-dest", 10 | "ts-typecheck": "tsc --noEmit", 11 | "lint": "eslint webapp", 12 | "karma": "karma start", 13 | "karma-ci": "karma start karma-ci.conf.js", 14 | "karma-ci-cov": "karma start karma-ci-cov.conf.js", 15 | "test": "npm run lint && npm run karma-ci-cov", 16 | "wdi5": "wdio run ./webapp/test/e2e/\\wdio.conf.ts", 17 | "deployBuild": "node ./utils/deployBuild.js", 18 | "deployBuild-keep": "node ./utils/deployBuild.js --keep", 19 | "prepare": "husky", 20 | "commit": "npx commit", 21 | "changelog": "standard-version" 22 | }, 23 | "devDependencies": { 24 | "@commitlint/cli": "^19.6.1", 25 | "@commitlint/config-conventional": "^19.6.0", 26 | "@commitlint/prompt-cli": "^19.6.1", 27 | "@openui5/types": "^1.131.1", 28 | "@types/chrome": "^0.0.287", 29 | "@types/node": "^22.10.2", 30 | "@typescript-eslint/eslint-plugin": "^8.18.1", 31 | "@typescript-eslint/parser": "^8.18.1", 32 | "@ui5/builder": "^4.0.5", 33 | "@ui5/cli": "^4.0.12", 34 | "@ui5/ts-interface-generator": "^0.9.0", 35 | "@wdio/cli": "^9.4.5", 36 | "@wdio/local-runner": "^9.4.5", 37 | "@wdio/mocha-framework": "^9.4.4", 38 | "@wdio/spec-reporter": "^9.4.4", 39 | "eslint": "^9.17.0", 40 | "glob": "^11.0.0", 41 | "http-server": "^14.1.1", 42 | "husky": "^9.1.7", 43 | "karma": "^6.4.4", 44 | "karma-chrome-launcher": "^3.2.0", 45 | "karma-coverage": "^2.2.1", 46 | "karma-ui5": "^3.0.4", 47 | "karma-ui5-transpile": "^3.5.1", 48 | "rimraf": "^6.0.1", 49 | "standard-version": "^9.5.0", 50 | "ts-node": "^10.9.2", 51 | "typescript": "^5.7.2", 52 | "ui5-middleware-livereload": "^3.1.0", 53 | "ui5-task-minifier": "^2.0.0", 54 | "ui5-task-minify-xml": "^3.1.0", 55 | "ui5-task-zipper": "^3.3.1", 56 | "ui5-tooling-modules": "^3.19.0", 57 | "ui5-tooling-transpile": "^3.5.1", 58 | "wdio-ui5-service": "^1.5.6", 59 | "zip-a-folder": "^3.1.8" 60 | }, 61 | "dependencies": { 62 | "client-zip": "^2.4.6", 63 | "highlight.js": "^11.11.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "es2022", 5 | "moduleResolution": "node", 6 | "skipLibCheck": true, 7 | "allowJs": true, 8 | "strict": true, 9 | "strictNullChecks": false, 10 | "strictPropertyInitialization": false, 11 | "rootDir": "./webapp", 12 | "types": [ 13 | "@openui5/types", 14 | "@types/qunit", 15 | "@types/chrome", 16 | "@types/node", 17 | "chrome" 18 | ], 19 | "paths": { 20 | "com/ui5/journeyrecorder/*": [ 21 | "./webapp/*" 22 | ], 23 | "unit/*": [ 24 | "./webapp/test/unit/*" 25 | ], 26 | "integration/*": [ 27 | "./webapp/test/integration/*" 28 | ] 29 | } 30 | }, 31 | "include": [ 32 | "./webapp/**/*" 33 | ], 34 | "exclude": [ 35 | "./webapp/assets/scripts", 36 | "./utils" 37 | ] 38 | } -------------------------------------------------------------------------------- /ui5-dist.yaml: -------------------------------------------------------------------------------- 1 | specVersion: "3.2" 2 | metadata: 3 | name: com.ui5.journeyrecorder 4 | type: application 5 | resources: 6 | configuration: 7 | paths: 8 | webapp: dist 9 | framework: 10 | name: OpenUI5 11 | version: "1.120.6" 12 | libraries: 13 | - name: sap.m 14 | - name: sap.uxap 15 | - name: sap.ui.core 16 | - name: themelib_sap_horizon 17 | - name: themelib_sap_fiori_3 18 | -------------------------------------------------------------------------------- /ui5.yaml: -------------------------------------------------------------------------------- 1 | specVersion: "3.2" 2 | metadata: 3 | name: com.ui5.journeyrecorder 4 | type: application 5 | framework: 6 | name: OpenUI5 7 | version: "1.131.1" 8 | libraries: 9 | - name: sap.m 10 | - name: sap.ui.core 11 | - name: sap.uxap 12 | - name: themelib_sap_fiori_3 13 | - name: themelib_sap_horizon 14 | builder: 15 | customTasks: 16 | #- name: task-usage-search 17 | # afterTask: replaceVersion 18 | - name: ui5-tooling-modules-task 19 | afterTask: replaceVersion 20 | configuration: 21 | addToNamespace: true 22 | - name: ui5-tooling-transpile-task 23 | afterTask: replaceVersion 24 | - name: ui5-task-minify-xml 25 | afterTask: replaceVersion 26 | configuration: 27 | minifyOptions: 28 | removeComments: true 29 | collapseEmptyElements: true 30 | collapseWhitespaceInAttributeValues: true 31 | fileExtensions: 32 | - "xml" 33 | - name: ui5-task-minifier 34 | afterTask: minify 35 | configuration: 36 | html: true 37 | css: true 38 | json: true 39 | server: 40 | customMiddleware: 41 | - name: ui5-tooling-modules-middleware 42 | afterMiddleware: compression 43 | - name: ui5-tooling-transpile-middleware 44 | afterMiddleware: compression 45 | - name: ui5-middleware-livereload 46 | afterMiddleware: compression 47 | -------------------------------------------------------------------------------- /utils/deployBuild.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const fs = require("fs"); 3 | const glob = require("glob"); 4 | const path = require("path"); 5 | const { zip } = require('zip-a-folder'); 6 | const { version } = require('../package.json'); 7 | const { argv } = require('process'); 8 | 9 | const WORK_DIR = process.cwd(); 10 | 11 | const CONFIG = { 12 | DEPLOY_BUILD: "deploySelection", 13 | PROJECT_FOLDER: "webapp", 14 | BUILD_FOLDER: "dist", 15 | DEPLOY_VERSIONS: "deployments", 16 | UNUSEDFILE_ENDINGS: [ 17 | '/**/*-dbg.js', 18 | '/**/*-dbg.*.js', 19 | '/**/*.js.map', 20 | '/**/*.type.js', 21 | '/**/*.ts', 22 | '/**/*.gitkeep', 23 | '/**/*.less', 24 | ], 25 | RECURSIVE_FOLDERS: [ 26 | 'assets', 27 | 'css', 28 | 'resources/sap/ui/core/themes', 29 | 'resources/sap/ui/layout/themes', 30 | 'resources/sap/m/themes', 31 | 'resources/sap/f/themes', 32 | 'resources/sap/uxap/themes', 33 | 'resources/sap/ui/unified/themes' 34 | ], 35 | FILES_TO_COPY: [ 36 | "index.html", 37 | "favicon.ico", 38 | "manifest.json", 39 | "resources/sap-ui-custom.js", 40 | "resources/sap/m/CheckBox.js", 41 | "resources/sap/m/CheckBoxRenderer.js", 42 | "resources/sap/m/VBox.js", 43 | "resources/sap/m/VBoxRenderer.js", 44 | "resources/sap/ui/core/messagebundle.properties", 45 | "resources/sap/ui/core/messagebundle_de.properties", 46 | "resources/sap/ui/core/messagebundle_en_GB.properties", 47 | "resources/sap/ui/core/messagebundle_en_US_sappsd.properties", 48 | "resources/sap/ui/core/messagebundle_en_US_saprigi.properties", 49 | "resources/sap/ui/core/messagebundle_en_US_saptrc.properties", 50 | "resources/sap/ui/core/messagebundle_en.properties", 51 | "resources/sap/ui/core/ComponentSupport.js", 52 | "resources/sap/ui/core/date/Gregorian.js", 53 | "resources/sap/ui/core/cldr/en.json", 54 | "resources/sap/ui/core/boot/FieldHelpEndpoint.js", 55 | "resources/sap/ui/layout/library-preload-lazy.js", 56 | "resources/sap/ui/unified/library-preload-lazy.js", 57 | "resources/sap/m/messagebundle.properties", 58 | "resources/sap/m/messagebundle_de.properties", 59 | "resources/sap/m/messagebundle_en_GB.properties", 60 | "resources/sap/m/messagebundle_en_US_sappsd.properties", 61 | "resources/sap/m/messagebundle_en_US_saprigi.properties", 62 | "resources/sap/m/messagebundle_en.properties", 63 | "resources/sap/f/messagebundle.properties", 64 | "resources/sap/f/messagebundle_de.properties", 65 | "resources/sap/f/messagebundle_en_GB.properties", 66 | "resources/sap/f/messagebundle_en_US_sappsd.properties", 67 | "resources/sap/f/messagebundle_en_US_saprigi.properties", 68 | "resources/sap/f/messagebundle_en.properties", 69 | "resources/sap/uxap/messagebundle.properties", 70 | "resources/sap/uxap/messagebundle_de.properties", 71 | "resources/sap/uxap/messagebundle_en_GB.properties", 72 | "resources/sap/uxap/messagebundle_en_US_sappsd.properties", 73 | "resources/sap/uxap/messagebundle_en_US_saprigi.properties", 74 | "resources/sap/uxap/messagebundle_en_US_saptrc.properties", 75 | "resources/sap/uxap/messagebundle_en.properties", 76 | "resources/sap/ui/layout/messagebundle.properties", 77 | "resources/sap/ui/layout/messagebundle_de.properties", 78 | "resources/sap/ui/layout/messagebundle_en_GB.properties", 79 | "resources/sap/ui/layout/messagebundle_en_US_saprigi.properties", 80 | "resources/sap/ui/layout/messagebundle_en_US_saptrc.properties", 81 | "resources/sap/ui/layout/messagebundle_en.properties", 82 | ] 83 | } 84 | 85 | function cleanup() { 86 | console.log("Remove old deploy build"); 87 | fs.rmSync(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD), { recursive: true, force: true }); 88 | } 89 | 90 | async function buildExtension(projectPath, destinationPath) { 91 | const { graphFromPackageDependencies } = await import("@ui5/project/graph"); 92 | const graph = await graphFromPackageDependencies({ cwd: projectPath }); 93 | return graph.build({ 94 | destPath: destinationPath, 95 | selfContained: true, 96 | includedDependencies: ["*"], 97 | cleanDest: true 98 | }); 99 | } 100 | 101 | async function _removeFiles(aFilesToRemoveGlobs) { 102 | const files = (await Promise.all(aFilesToRemoveGlobs.map(tg => glob.glob(tg)))).flat(); 103 | return Promise.all(files.map(async (f) => { 104 | if (fs.existsSync(f)) { 105 | fs.unlinkSync(f) 106 | } 107 | })); 108 | } 109 | 110 | function cleanupTheBuildStuff() { 111 | console.log('Removing unnecessary files for recursive copy'); 112 | return _removeFiles(CONFIG.UNUSEDFILE_ENDINGS.map(ending => path.join(WORK_DIR, CONFIG.BUILD_FOLDER, ending))); 113 | } 114 | 115 | async function buildFolderStructure() { 116 | console.log("Recreate deploy build"); 117 | // create necessary subfolders 118 | console.log("Create folderstructure"); 119 | await fs.promises.mkdir(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD)); 120 | await fs.promises.mkdir(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD, '/resources')); 121 | await fs.promises.mkdir(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD, '/resources/sap')); 122 | await fs.promises.mkdir(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD, '/resources/sap/ui')); 123 | await Promise.all([ 124 | fs.promises.mkdir(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD, "/resources/sap/m")), 125 | fs.promises.mkdir(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD, "/resources/sap/uxap")), 126 | fs.promises.mkdir(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD, "/resources/sap/f")) 127 | ]); 128 | await Promise.all([ 129 | fs.promises.mkdir(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD, "/resources/sap/ui/core")).then(() => { 130 | return Promise.all([ 131 | fs.promises.mkdir(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD, "/resources/sap/ui/core/date")), 132 | fs.promises.mkdir(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD, "/resources/sap/ui/core/cldr")), 133 | fs.promises.mkdir(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD, "/resources/sap/ui/core/boot")) 134 | ]) 135 | }), 136 | fs.promises.mkdir(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD, "/resources/sap/ui/layout")), 137 | fs.promises.mkdir(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD, "/resources/sap/ui/unified")) 138 | ]); 139 | } 140 | 141 | function recursiveFolderCopy() { 142 | //recursive copy necessary folder 143 | console.log("Copy folders"); 144 | return Promise.all(CONFIG.RECURSIVE_FOLDERS.map(rc => fs.promises.cp(path.join(WORK_DIR, CONFIG.BUILD_FOLDER, rc), path.join(WORK_DIR, CONFIG.DEPLOY_BUILD, rc), { recursive: true }))); 145 | } 146 | 147 | function selectiveFileCopy() { 148 | //copy single files 149 | console.log("copy files"); 150 | return Promise.all(CONFIG.FILES_TO_COPY.map(rc => fs.promises.cp(path.join(WORK_DIR, CONFIG.BUILD_FOLDER, rc), path.join(WORK_DIR, CONFIG.DEPLOY_BUILD, rc)))); 151 | } 152 | 153 | function createDeployZip(bPreVersion) { 154 | console.log("Creating deploy ZIP"); 155 | let suffix = ''; 156 | if (bPreVersion) { 157 | suffix = 'nightly_' 158 | } 159 | if (!fs.existsSync(path.join(WORK_DIR, CONFIG.DEPLOY_VERSIONS))) { 160 | fs.mkdirSync(path.join(WORK_DIR, CONFIG.DEPLOY_VERSIONS)); 161 | } 162 | return zip(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD), path.join(WORK_DIR, CONFIG.DEPLOY_VERSIONS, `journey_recorder_${suffix}${version.replace(/\./gm, '-')}.zip`)); 163 | } 164 | 165 | function _byteSize(iSize) { 166 | const size_map = { 167 | 0: 'B', 168 | 1: 'kB', 169 | 2: 'MB', 170 | 3: 'GB' 171 | }; 172 | let divided = 0; 173 | while (iSize > 1024) { 174 | divided++ 175 | iSize = iSize / 1024.0; 176 | } 177 | return `${Math.floor(iSize * 100) / 100.0} ${size_map[divided]}`; 178 | } 179 | 180 | async function getDirSize(sDirName) { 181 | const files = await glob.glob(`./${sDirName}/**/*.*`); 182 | return _byteSize(files.map(f => { 183 | try { 184 | return fs.statSync(f).size; 185 | } catch (_) { 186 | return 0; 187 | } 188 | }).reduce((a, b) => { return a + b; }, 0)); 189 | } 190 | 191 | (async () => { 192 | cleanup(); 193 | await buildExtension(path.join(WORK_DIR, CONFIG.PROJECT_FOLDER), path.join(WORK_DIR, CONFIG.BUILD_FOLDER)); 194 | if (argv.includes('--size')) { 195 | console.log('Build-Size, ui5 tooling: ', (await getDirSize(path.join(WORK_DIR, CONFIG.BUILD_FOLDER)))); 196 | } 197 | await cleanupTheBuildStuff(); 198 | 199 | if (argv.includes('--size')) { 200 | console.log('Build-Size, cleanup: ', (await getDirSize(path.join(WORK_DIR, CONFIG.BUILD_FOLDER)))); 201 | } 202 | await buildFolderStructure(); 203 | await recursiveFolderCopy(); 204 | await selectiveFileCopy(); 205 | 206 | if (argv.includes('--size')) { 207 | console.log('Build-Size, deploy: ', (await getDirSize(path.join(WORK_DIR, CONFIG.DEPLOY_BUILD)))); 208 | } 209 | await createDeployZip(argv.includes('--pre')); 210 | 211 | if (!(argv.includes('--keep'))) { 212 | cleanup(); 213 | } 214 | })(); 215 | -------------------------------------------------------------------------------- /webapp/Component.ts: -------------------------------------------------------------------------------- 1 | import UIComponent from "sap/ui/core/UIComponent"; 2 | import models from "./model/models"; 3 | import Device from "sap/ui/Device"; 4 | import JSONModel from "sap/ui/model/json/JSONModel"; 5 | import SettingsStorageService, { AppSettings } from "./service/SettingsStorage.service"; 6 | import Theming from "sap/ui/core/Theming"; 7 | import { ConnectionStatus } from "./model/enum/ConnectionStatus"; 8 | 9 | /** 10 | * @namespace com.ui5.journeyrecorder 11 | */ 12 | export default class Component extends UIComponent { 13 | public static metadata = { 14 | manifest: "json" 15 | }; 16 | 17 | private contentDensityClass: string; 18 | 19 | public init(): void { 20 | // call the base component's init function 21 | super.init(); 22 | // create the application wide default model, only for setups 23 | this.setModel(new JSONModel({ connectionStatus: ConnectionStatus.DISCONNECTED })); 24 | 25 | const version = (this.getManifestObject().getJson() as Record).version as string; 26 | (this.getModel() as JSONModel).setProperty('/appVersion', version); 27 | 28 | // create the app settings model 29 | void SettingsStorageService.getSettings().then((settings: AppSettings) => { 30 | this.setModel(new JSONModel(settings), 'settings'); 31 | Theming.setTheme(settings.theme); 32 | }); 33 | 34 | // create the device model 35 | this.setModel(models.createDeviceModel(), "device"); 36 | 37 | // create the views based on the url/hash 38 | this.getRouter().initialize(); 39 | } 40 | 41 | /** 42 | * This method can be called to determine whether the sapUiSizeCompact or sapUiSizeCozy 43 | * design mode class should be set, which influences the size appearance of some controls. 44 | * @public 45 | * @returns css class, either 'sapUiSizeCompact' or 'sapUiSizeCozy' - or an empty string if no css class should be set 46 | */ 47 | public getContentDensityClass(): string { 48 | if (this.contentDensityClass === undefined) { 49 | // check whether FLP has already set the content density class; do nothing in this case 50 | if (document.body.classList.contains("sapUiSizeCozy") || document.body.classList.contains("sapUiSizeCompact")) { 51 | this.contentDensityClass = ""; 52 | } else if (!Device.support.touch) { 53 | // apply "compact" mode if touch is not supported 54 | this.contentDensityClass = "sapUiSizeCompact"; 55 | } else { 56 | // "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table 57 | this.contentDensityClass = "sapUiSizeCozy"; 58 | } 59 | } 60 | return this.contentDensityClass; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /webapp/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/webapp/assets/.gitkeep -------------------------------------------------------------------------------- /webapp/assets/icons/icon/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/webapp/assets/icons/icon/128.png -------------------------------------------------------------------------------- /webapp/assets/icons/icon/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/webapp/assets/icons/icon/16.png -------------------------------------------------------------------------------- /webapp/assets/icons/icon/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/webapp/assets/icons/icon/32.png -------------------------------------------------------------------------------- /webapp/assets/icons/icon/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/webapp/assets/icons/icon/48.png -------------------------------------------------------------------------------- /webapp/assets/icons/icon/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/webapp/assets/icons/icon/64.png -------------------------------------------------------------------------------- /webapp/assets/icons/icon/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/webapp/assets/icons/icon/80.png -------------------------------------------------------------------------------- /webapp/assets/icons/icon/96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/webapp/assets/icons/icon/96.png -------------------------------------------------------------------------------- /webapp/assets/icons/icon_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/webapp/assets/icons/icon_old.png -------------------------------------------------------------------------------- /webapp/assets/icons/ui5_journey_recorder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/webapp/assets/icons/ui5_journey_recorder.png -------------------------------------------------------------------------------- /webapp/assets/icons/ui5_journey_recorder_wb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/webapp/assets/icons/ui5_journey_recorder_wb.png -------------------------------------------------------------------------------- /webapp/assets/scripts/communication_inject.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | class API { 3 | static own_id = "ui5_tr_handler"; 4 | static key_ident = `(\\(\\S+\\))?`; 5 | static nav_prop = `(\\/\\S+)?`; 6 | 7 | #root = ""; 8 | #getter_expr = []; 9 | #post_expr = []; 10 | #getter_routes = []; 11 | #post_routes = []; 12 | 13 | webSocket; 14 | 15 | /** 16 | * Constructor of the API 17 | * 18 | * @param {string} [sRoot] optional root path, used as prefix bevor every route 19 | */ 20 | contructor(sRoot) { 21 | this.#root = sRoot || ""; 22 | } 23 | 24 | //#region public 25 | /** 26 | * Set a getter route for the api 27 | * 28 | * @param {string} sRoute which should be handled 29 | * @param {function(req,res)=>void} callback function handling calls to the route 30 | */ 31 | get(sRoute, callback) { 32 | const map_id = this.#getter_expr.length; 33 | const { route, pars } = this.#prepareRoute(sRoute); 34 | 35 | const prefix = this.#root + route; 36 | const regexp = new RegExp(`((${prefix})((\\?\\S+)$)?)$`, 'gm'); 37 | 38 | this.#getter_expr.push({ regex: regexp, id: map_id, paramList: pars }); 39 | this.#getter_routes[map_id] = callback; 40 | } 41 | 42 | /** 43 | * Set a post route for the api 44 | * 45 | * @param {string} sRoute 46 | * @param {function(req,res)=>void} callback function handling calls to the route 47 | */ 48 | post(sRoute, callback) { 49 | const prefix = this.#root + sRoute; 50 | const regexp = new RegExp(`(${prefix})${API.key_ident}${API.nav_prop}`, 'gm'); 51 | const map_id = this.#post_expr.length; 52 | this.#post_expr.push({ regex: regexp, id: map_id }); 53 | this.#post_routes[map_id] = callback; 54 | } 55 | 56 | /** 57 | * Start the api and use the listener id as filter for all message events 58 | * 59 | * @param {string} sListenerId for the "message" events to listen on {event.data.origin} 60 | */ 61 | listen(sListenerId) { 62 | const page_origin = new URL(location.href).origin; 63 | 64 | window.addEventListener("message", (oEvent) => { 65 | if (oEvent.origin === page_origin && oEvent.data.origin === sListenerId) { 66 | this.#handleEvent(oEvent, sListenerId); 67 | } 68 | }); 69 | 70 | this.webSocket = { 71 | send_record_step: (step) => { 72 | window.postMessage({ 73 | origin: `${sListenerId}_${API.own_id}`, 74 | message: { 75 | instantType: 'record-token', 76 | content: step 77 | } 78 | }); 79 | }, 80 | send_instant_message: (sMsg) => { 81 | window.postMessage({ 82 | origin: `${sListenerId}_${API.own_id}`, message: { 83 | instantType: 'instant', 84 | content: sMsg 85 | } 86 | }) 87 | } 88 | } 89 | } 90 | //#endregion public 91 | //#region private 92 | #prepareRoute(sRoute) { 93 | const pathParamIdentifier = /\<([a-zA-Z0-9])+\>/gm; 94 | const pathParams = []; 95 | const matches = sRoute.match(pathParamIdentifier); 96 | let enclosingEnd = ''; 97 | let enclosingFront = ''; 98 | if (matches) { 99 | matches.forEach((par, i) => { 100 | const paramName = par.replace('<', '').replace('>', ''); 101 | sRoute = sRoute.replace(par, '%*'); 102 | const front = sRoute.indexOf('%'); 103 | const end = sRoute.indexOf('*'); 104 | const len = sRoute.length; 105 | if (end - 1 === len) { 106 | sRoute += "$"; 107 | } else { 108 | enclosingEnd = sRoute[end + 1]; 109 | sRoute = sRoute.substring(0, end + 1) + '\\' + sRoute.substring(end + 1); 110 | } 111 | if (front === 0) { 112 | sRoute = '^' + sRoute; 113 | } else { 114 | enclosingFront = sRoute[front - 1]; 115 | sRoute = sRoute.substring(0, front - 1) + '\\' + sRoute.substring(front - 1) 116 | } 117 | 118 | pathParams.push({ index: i, name: paramName, encFront: enclosingFront, encEnd: enclosingEnd }); 119 | 120 | sRoute = sRoute.replaceAll('%*', "([a-zA-Z0-9'-])+"); 121 | }); 122 | return { route: sRoute, pars: pathParams }; 123 | } else { 124 | return { route: sRoute, pars: pathParams }; 125 | } 126 | } 127 | 128 | #handleEvent(oEvent, sExt_id) { 129 | const answer_origin = `${sExt_id}_${API.own_id}`; 130 | const event_id = oEvent?.data?.message_id; 131 | const req_type = oEvent?.data?.method; 132 | 133 | switch (req_type) { 134 | case 'GET': 135 | this.#handleGet(oEvent?.data, this.#provideResponseCallback(event_id, answer_origin)); 136 | break; 137 | case 'POST': 138 | this.#handlePost(oEvent?.data, this.#provideResponseCallback(event_id, answer_origin)) 139 | break; 140 | default: 141 | this.#provideResponseCallback(event_id, answer_origin)({ status: 501, error: `Request type (${req_type}) not provided` }); 142 | } 143 | } 144 | 145 | #provideResponseCallback(event_id, answer_origin) { 146 | return (oResponseData) => { 147 | window.postMessage({ origin: answer_origin, response: { message_id: event_id, ...oResponseData } }); 148 | }; 149 | } 150 | 151 | #handleGet(oEventData, res) { 152 | const url = oEventData.url; 153 | if (!url) { 154 | res({ status: 400, error: `Bad Request no url provided!` }); 155 | return; 156 | } 157 | const url_obj = new URL(`http://${API.own_id}/${url}`); 158 | 159 | const handler = this.#getter_expr.find(r => r.regex.test(decodeURIComponent(url_obj.pathname))); 160 | if (!handler) { 161 | res({ status: 404, error: `The requested ressource does not exist!` }); 162 | return; 163 | } 164 | //reset the regex because of the global 'g' modifier 165 | this.#getter_expr.forEach(r => r.regex.lastIndex = 0); 166 | const req = {} 167 | req.pathParams = this.#retrievePathParameters(url, handler); 168 | 169 | req.searchParams = {}; 170 | for (var spkey of url_obj.searchParams.keys()) { 171 | req.searchParams[spkey] = url_obj.searchParams.get(spkey); 172 | } 173 | 174 | req.url = url; 175 | 176 | this.#getter_routes[handler.id](req, res); 177 | } 178 | 179 | #handlePost(oEventData, res) { 180 | const url = oEventData.url; 181 | if (!url) { 182 | res({ status: 400, error: `Bad Request no url provided!` }); 183 | return; 184 | } 185 | const url_obj = new URL(`http://${API.own_id}/${url}`); 186 | 187 | const handler = this.#post_expr.find(r => r.regex.test(decodeURIComponent(url_obj.pathname))); 188 | if (!handler) { 189 | res({ status: 404, error: `The requested ressource does not exist!` }); 190 | return; 191 | } 192 | //reset the regex because of the global 'g' modifier 193 | this.#post_expr.forEach(r => r.regex.lastIndex = 0); 194 | const req = {} 195 | var parts = [decodeURIComponent(url.pathname).matchAll(handler.regex)][0]; 196 | const key = parts[2]; 197 | const navigation = parts[3]; 198 | 199 | req.pathParam = {}; 200 | req.pathParam['key'] = key; 201 | req.pathParam['navigation'] = navigation; 202 | 203 | req.searchParams = {}; 204 | for (var spkey of url_obj.searchParams.keys()) { 205 | req.searchParams[spkey] = url_obj.searchParams.get(spkey); 206 | } 207 | 208 | req.url = url; 209 | req.body = oEventData.body; 210 | 211 | this.#post_routes[handler.id](req, res); 212 | } 213 | 214 | #retrievePathParameters(sUrl, handler) { 215 | const paramMap = {}; 216 | handler.paramList.forEach(param => { 217 | const parRegex = new RegExp('\\' + param.encFront + "([a-zA-Z0-9'-])+" + "\\" + param.encEnd); 218 | const res = sUrl.match(parRegex); 219 | if (res) { 220 | paramMap[param.name] = res[0].replace(param.encFront, '').replace(param.encEnd, ''); 221 | } else { 222 | paramMap[param.name] = undefined; 223 | } 224 | sUrl = sUrl.replace(parRegex, '[done]'); 225 | }) 226 | return paramMap; 227 | 228 | //#endregion private 229 | } 230 | } 231 | 232 | //#region setup com 233 | const communicationService = new API(location.href.origin + '/page'); 234 | const ext_id = document.getElementById('UI5TR-communication-js').getAttribute('data-id'); 235 | 236 | communicationService.get('/controls', (req, res) => { 237 | if (req.searchParams.count === '') { 238 | const controlType = req.searchParams.control_type; 239 | const attributes = decodeURIComponent(req.searchParams.attributes); 240 | const recorderInstance = window.ui5TestRecorder?.recorder; 241 | if (recorderInstance) { 242 | let elements = recorderInstance.getElementsByAttributes(controlType, JSON.parse(attributes)); 243 | res({ status: 200, message: elements.length }); 244 | } else { 245 | res({ status: 500, message: 'No recorder inject found!' }); 246 | } 247 | } 248 | }); 249 | 250 | communicationService.get("/controls()", (req, res) => { 251 | const recorderInstance = window.ui5TestRecorder?.recorder; 252 | if (recorderInstance) { 253 | if (req.searchParams.count === '') { 254 | let elements = []; 255 | if (req.pathParams.id.indexOf("'") > -1) { 256 | elements = recorderInstance.getElementsForId(req.pathParams.id.replaceAll("'", '')) 257 | } else { 258 | elements = recorderInstance.getElementsForId(req.pathParams.id); 259 | } 260 | res({ status: 200, message: elements.length }); 261 | } 262 | } else { 263 | res({ status: 500, message: 'No recorder inject found!' }); 264 | } 265 | }); 266 | 267 | communicationService.post('/controls/action', (req, res) => { 268 | const recorderInstance = window.ui5TestRecorder?.recorder; 269 | if (recorderInstance) { 270 | const body = req.body; 271 | recorderInstance.executeAction({ step: body.step, useSelectors: body.useManualSelection }).then(() => { 272 | res({ status: 200, message: 'executed' }); 273 | }).catch((e) => { 274 | res({ status: 500, message: e }); 275 | }); 276 | } else { 277 | res({ status: 500, message: 'No recorder inject found!' }); 278 | } 279 | }); 280 | 281 | communicationService.post('/pageInfo/disconnected', (_, __) => { 282 | if (ui5TestRecorder.recorder) { 283 | ui5TestRecorder.recorder.showToast('UI5 Journey Recorder disconnected', { 284 | duration: 2000, 285 | autoClose: true 286 | }) 287 | } 288 | }); 289 | 290 | communicationService.get('/pageInfo/connected', (_, res) => { 291 | res({ status: 200, message: 'Connected' }); 292 | }); 293 | 294 | communicationService.get('/pageInfo/version', (_, res) => { 295 | const version = ui5TestRecorder?.recorder?.getUI5Version(); 296 | if (version) { 297 | res({ status: 200, message: version }); 298 | } else { 299 | res({ status: 400, message: '' }); 300 | } 301 | }); 302 | 303 | communicationService.post('/disableRecordListener', (_, res) => { 304 | const recorderInstance = window.ui5TestRecorder?.recorder; 305 | if (recorderInstance) { 306 | recorderInstance.disableRecording(); 307 | res({ status: 200, message: 'executed' }); 308 | } else { 309 | res({ status: 500, message: 'No recorder inject found!' }); 310 | } 311 | }); 312 | 313 | communicationService.post('/enableRecordListener', (_, res) => { 314 | const recorderInstance = window.ui5TestRecorder?.recorder; 315 | if (recorderInstance) { 316 | recorderInstance.enableRecording(); 317 | res({ status: 200, message: 'executed' }); 318 | } else { 319 | res({ status: 500, message: 'No recorder inject found!' }); 320 | } 321 | }) 322 | 323 | communicationService.listen(ext_id); 324 | 325 | window.ui5TestRecorder = { 326 | ...window.ui5TestRecorder, 327 | ... { 328 | communication: communicationService 329 | } 330 | } 331 | //#endregion setup com 332 | })(); 333 | -------------------------------------------------------------------------------- /webapp/assets/scripts/content_inject.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | //DOM-node tags 5 | const TAG_ID_PREFIX = "UI5TR-" 6 | const EXT_ID = chrome.runtime.id; 7 | const page_origin = new URL(location.href).origin; 8 | let port; 9 | 10 | console.log('injected'); 11 | 12 | function injectJS() { 13 | console.log('--- Inject UI5 Journey Recorder JS ---'); 14 | let script = document.createElement('script'); 15 | script.id = `${TAG_ID_PREFIX}-js`; 16 | script.src = chrome.runtime.getURL('/assets/scripts/page_inject.js'); 17 | script.defer = "defer"; 18 | document.head.prepend(script); 19 | } 20 | 21 | function commJS() { 22 | console.log('--- Inject Communication JS ---'); 23 | let script = document.createElement('script'); 24 | script.id = `${TAG_ID_PREFIX}communication-js`; 25 | script.src = chrome.runtime.getURL('/assets/scripts/communication_inject.js'); 26 | script.setAttribute('data-id', EXT_ID); 27 | script.defer = "defer"; 28 | document.head.prepend(script); 29 | } 30 | 31 | function setupInjectCSS() { 32 | let css = '.injectClass { box-shadow: 0 0 2px 2px red, inset 0 0 2px 2px red; }'; 33 | let head = document.head || document.getElementsByTagName('head')[0]; 34 | let style = document.createElement('style'); 35 | 36 | style.id = "UI5TR--css"; 37 | style.appendChild(document.createTextNode(css)); 38 | 39 | head.prepend(style); 40 | } 41 | 42 | function setup_port_passthrough() { 43 | port = chrome.runtime.connect({ name: "ui5_tr" }); 44 | 45 | port.onMessage.addListener((msg) => { 46 | window.postMessage({ 47 | origin: EXT_ID, 48 | ...msg 49 | }) 50 | }); 51 | 52 | port.onDisconnect.addListener(() => { 53 | 54 | window.postMessage({ 55 | origin: EXT_ID, 56 | ...{ 57 | url: '/pageInfo/disconnected', 58 | method: 'POST', 59 | message_id: -1 60 | } 61 | }); 62 | document.getElementById(`${TAG_ID_PREFIX}communication-js`).remove(); 63 | document.getElementById(`${TAG_ID_PREFIX}-js`).remove(); 64 | document.getElementById(`${TAG_ID_PREFIX}-css`).remove(); 65 | }); 66 | 67 | const page_id = EXT_ID + '_ui5_tr_handler'; 68 | window.addEventListener("message", (event) => { 69 | 70 | if (event.data.origin === page_id && event.origin === page_origin) { 71 | port.postMessage({ data: event.data.message || event.data.response }); 72 | } 73 | }) 74 | 75 | window.addEventListener("beforeunload", () => { 76 | port.disconnect(); 77 | }) 78 | } 79 | 80 | injectJS(); 81 | commJS(); 82 | setupInjectCSS(); 83 | setup_port_passthrough(); 84 | }()); 85 | -------------------------------------------------------------------------------- /webapp/assets/scripts/starter.js: -------------------------------------------------------------------------------- 1 | let color = '#3aa757'; 2 | 3 | /* chrome.runtime.onInstalled.addListener(() => { 4 | chrome.storage.sync.set({ color }); 5 | console.log('Default background color set to %cgreen', `color: ${color}`); 6 | }); */ 7 | 8 | chrome.action.onClicked.addListener((/* tab */) => {/* 9 | chrome.scripting.executeScript({ 10 | target: { tabId: tab.id }, 11 | func: (name) => { alert(`"${name}" executed`); }, 12 | args: ['action'] 13 | }); */ 14 | 15 | chrome.windows.create({ 16 | url: chrome.runtime.getURL('../index.html'), 17 | type: 'popup', 18 | focused: true 19 | }, (fnWindow) => {/* 20 | console.log(`loaded extension with id: ${fnWindow.id}`); */ 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /webapp/control/CodeViewer.gen.d.ts: -------------------------------------------------------------------------------- 1 | import { PropertyBindingInfo } from "sap/ui/base/ManagedObject"; 2 | import { $ControlSettings } from "sap/ui/core/Control"; 3 | 4 | declare module "./CodeViewer" { 5 | 6 | /** 7 | * Interface defining the settings object used in constructor calls 8 | */ 9 | interface $CodeViewerSettings extends $ControlSettings { 10 | height?: string | PropertyBindingInfo; 11 | language?: string | PropertyBindingInfo; 12 | code?: string | PropertyBindingInfo; 13 | } 14 | 15 | export default interface CodeViewer { 16 | 17 | // property: height 18 | getHeight(): string; 19 | setHeight(height: string): this; 20 | 21 | // property: language 22 | getLanguage(): string; 23 | setLanguage(language: string): this; 24 | bindLanguage(bindingInfo: PropertyBindingInfo): this; 25 | unbindLanguage(): this; 26 | 27 | // property: code 28 | getCode(): string; 29 | setCode(code: string): this; 30 | bindCode(bindingInfo: PropertyBindingInfo): this; 31 | unbindCode(): this; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webapp/control/CodeViewer.ts: -------------------------------------------------------------------------------- 1 | import Control from "sap/ui/core/Control"; 2 | import { MetadataOptions } from "sap/ui/core/Element"; 3 | import CodeViewerRenderer from "./CodeViewerRenderer"; 4 | import { Themes } from "../model/enum/Themes"; 5 | import Theming from "sap/ui/core/Theming"; 6 | 7 | /*! 8 | * ${copyright} 9 | */ 10 | 11 | /** 12 | * @namespace com.ui5.journeyrecorder 13 | * 14 | * @extends Control 15 | * @author Adrian Marten 16 | * @version ${version} 17 | * 18 | * @constructor 19 | * @public 20 | * @name com.ui5.journeyrecorder.control.CodeViewer 21 | */ 22 | export default class CodeViewer extends Control { 23 | // The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures 24 | constructor(idOrSettings?: string | $CodeViewerSettings); 25 | constructor(id?: string, settings?: $CodeViewerSettings); 26 | constructor(id?: string, settings?: $CodeViewerSettings) { 27 | super(id, settings); 28 | Theming.attachApplied(() => { 29 | this.invalidate(); 30 | }); 31 | } 32 | 33 | static readonly metadata: MetadataOptions = { 34 | properties: { 35 | height: { type: "string", defaultValue: '100%' }, 36 | language: { type: "string", defaultValue: '', bindable: true }, 37 | code: { type: "string", defaultValue: '', bindable: true }, 38 | } 39 | } 40 | 41 | static renderer: typeof CodeViewerRenderer = CodeViewerRenderer; 42 | } -------------------------------------------------------------------------------- /webapp/control/CodeViewerRenderer.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * ${copyright} 3 | */ 4 | 5 | import RenderManager from "sap/ui/core/RenderManager"; 6 | import CodeViewer from "./CodeViewer"; 7 | import hljs from "highlight.js/lib/core"; 8 | import javascript from "highlight.js/lib/languages/javascript"; 9 | import Theming from "sap/ui/core/Theming"; 10 | import { Themes } from "../model/enum/Themes"; 11 | 12 | /** 13 | * @namespace com.ui5.journeyrecorder 14 | * 15 | * @author Adrian Marten 16 | * @version ${version} 17 | * 18 | * @constructors 19 | * @public 20 | * @name com.ui5.journeyrecorder.control.CodeViewerRenderer 21 | */ 22 | export default class CodeViewerRenderer { 23 | apiVersion: 2; 24 | 25 | constructor() { } 26 | 27 | public static render(rm: RenderManager, control: CodeViewer) { 28 | const th = Theming.getTheme() as Themes; 29 | hljs.registerLanguage('javascript', javascript) 30 | rm.openStart("div", control); 31 | rm.class("code-viewer") 32 | rm.style("height", control.getHeight()); 33 | rm.style("overflow", "auto"); 34 | rm.openEnd(); 35 | rm.openStart("pre") 36 | switch (th) { 37 | case Themes.QUARTZ_DARK: 38 | case Themes.EVENING_HORIZON: 39 | rm.class("dark"); 40 | break; 41 | default: 42 | rm.class("bright"); 43 | } 44 | rm.openEnd(); 45 | rm.openStart("code").class("hljs").class("internal").openEnd(); 46 | const highlightedCode = control.getCode().split('\n').map((cp) => hljs.highlight(cp, { language: control.getLanguage() }).value); 47 | const lineCount = highlightedCode.length; 48 | const indicatorWith = `${('' + lineCount).length * 0.5}rem`; 49 | highlightedCode.forEach((hc, index) => { 50 | rm.openStart("div").class("row").openEnd(); 51 | rm.openStart("div").class("line-indicator").style("width", indicatorWith).openEnd(); 52 | rm.text('' + index); 53 | rm.close("div"); 54 | rm.openStart("div").class("code").openEnd() 55 | rm.unsafeHtml(hc); 56 | rm.close("div") 57 | rm.close("div") 58 | }) 59 | 60 | rm.close("code"); 61 | rm.close("pre"); 62 | 63 | // get the pre and code section with speed-highlight package 64 | rm.close("div"); 65 | } 66 | } -------------------------------------------------------------------------------- /webapp/control/EditableText.gen.d.ts: -------------------------------------------------------------------------------- 1 | import Event from "sap/ui/base/Event"; 2 | import { PropertyBindingInfo } from "sap/ui/base/ManagedObject"; 3 | import { $ControlSettings } from "sap/ui/core/Control"; 4 | 5 | declare module "./EditableText" { 6 | 7 | /** 8 | * Interface defining the settings object used in constructor calls 9 | */ 10 | interface $EditableTextSettings extends $ControlSettings { 11 | prefix?: string | PropertyBindingInfo; 12 | text?: string | PropertyBindingInfo; 13 | useAsTitle?: boolean | PropertyBindingInfo | `{${string}}`; 14 | change?: (event: EditableText$ChangeEvent) => void; 15 | } 16 | 17 | export default interface EditableText { 18 | 19 | // property: prefix 20 | getPrefix(): string; 21 | setPrefix(prefix: string): this; 22 | bindPrefix(bindingInfo: PropertyBindingInfo): this; 23 | unbindPrefix(): this; 24 | 25 | // property: text 26 | getText(): string; 27 | setText(text: string): this; 28 | bindText(bindingInfo: PropertyBindingInfo): this; 29 | unbindText(): this; 30 | 31 | // property: useAsTitle 32 | getUseAsTitle(): boolean; 33 | setUseAsTitle(useAsTitle: boolean): this; 34 | bindUseAsTitle(bindingInfo: PropertyBindingInfo): this; 35 | unbindUseAsTitle(): this; 36 | 37 | // event: change 38 | attachChange(fn: (event: EditableText$ChangeEvent) => void, listener?: object): this; 39 | attachChange(data: CustomDataType, fn: (event: EditableText$ChangeEvent, data: CustomDataType) => void, listener?: object): this; 40 | detachChange(fn: (event: EditableText$ChangeEvent) => void, listener?: object): this; 41 | fireChange(parameters?: EditableText$ChangeEventParameters): this; 42 | } 43 | 44 | /** 45 | * Interface describing the parameters of EditableText's 'change' event. 46 | */ 47 | export interface EditableText$ChangeEventParameters { 48 | value?: string; 49 | } 50 | 51 | /** 52 | * Type describing the EditableText's 'change' event. 53 | */ 54 | export type EditableText$ChangeEvent = Event; 55 | } 56 | -------------------------------------------------------------------------------- /webapp/control/EditableText.ts: -------------------------------------------------------------------------------- 1 | import Control from "sap/ui/core/Control"; 2 | import { MetadataOptions } from "sap/ui/core/Element"; 3 | import EditableTextRenderer from "./EditableTextRenderer"; 4 | import Button from "sap/m/Button"; 5 | import { ButtonType } from "sap/m/library"; 6 | import Label from "sap/m/Label"; 7 | import { LabelDesign } from "sap/m/library"; 8 | import Input from "sap/m/Input"; 9 | import { InputBase$ChangeEvent } from "sap/m/InputBase"; 10 | 11 | /*! 12 | * ${copyright} 13 | */ 14 | 15 | /** 16 | * @namespace com.ui5.journeyrecorder 17 | * 18 | * @extends Control 19 | * @author Adrian Marten 20 | * @version ${version} 21 | * 22 | * @constructor 23 | * @public 24 | * @name com.ui5.journeyrecorder.control.EditableText 25 | */ 26 | export default class EditableText extends Control { 27 | // The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures 28 | constructor(idOrSettings?: string | $EditableTextSettings); 29 | constructor(id?: string, settings?: $EditableTextSettings); 30 | constructor(id?: string, settings?: $EditableTextSettings) { super(id, settings); } 31 | 32 | static readonly metadata: MetadataOptions = { 33 | properties: { 34 | prefix: { type: "string", defaultValue: '', bindable: true }, 35 | text: { type: "string", defaultValue: '', bindable: true }, 36 | useAsTitle: { type: "boolean", defaultValue: false, bindable: true } 37 | }, 38 | aggregations: { 39 | _prefix: { type: "sap.m.Label", multiple: false, visibility: "hidden" }, 40 | _text: { type: "sap.m.Label", multiple: false, visibility: "hidden" }, 41 | _input: { type: "sap.m.Input", multiple: false, visibility: "hidden" }, 42 | _toEdit: { type: "sap.m.Button", multiple: false, visibility: "hidden" }, 43 | _toShow: { type: "sap.m.Button", multiple: false, visibility: "hidden" } 44 | }, 45 | events: { 46 | change: { 47 | parameters: { 48 | value: { type: "string" } 49 | } 50 | } 51 | } 52 | } 53 | 54 | init() { 55 | this.setAggregation("_toEdit", 56 | new Button({ 57 | id: this.getId() + "-editSwitch", 58 | icon: "sap-icon://edit", 59 | type: ButtonType.Transparent, 60 | visible: true, 61 | press: this._toEdit.bind(this) 62 | })); 63 | this.setAggregation("_toShow", 64 | new Button({ 65 | id: this.getId() + "-viewSwitch", 66 | icon: "sap-icon://decline", 67 | type: ButtonType.Transparent, 68 | visible: false, 69 | press: this._toShow.bind(this) 70 | })); 71 | 72 | 73 | this.setAggregation("_prefix", 74 | new Label({ 75 | id: this.getId() + "-prefix", 76 | text: this.getPrefix(), 77 | visible: true, 78 | design: (this.getUseAsTitle() as boolean) ? LabelDesign.Bold : LabelDesign.Standard 79 | })); 80 | 81 | const titleInput = new Label({ 82 | id: this.getId() + "-text", 83 | text: this.getText(), 84 | visible: true, 85 | design: (this.getUseAsTitle() as boolean) ? LabelDesign.Bold : LabelDesign.Standard 86 | }); 87 | this.setAggregation("_text", titleInput); 88 | 89 | this.setAggregation("_input", 90 | new Input({ 91 | value: this.getText(), 92 | visible: false, 93 | change: this._textChanged.bind(this) 94 | }) 95 | ); 96 | } 97 | 98 | onBeforeRendering(): void { 99 | (this.getAggregation('_prefix') as Label).setText(this.getPrefix()); 100 | (this.getAggregation('_prefix') as Label).setDesign((this.getUseAsTitle() as boolean) ? LabelDesign.Bold : LabelDesign.Standard); 101 | (this.getAggregation('_text') as Label).setText(this.getText()); 102 | (this.getAggregation('_text') as Label).setDesign((this.getUseAsTitle() as boolean) ? LabelDesign.Bold : LabelDesign.Standard); 103 | (this.getAggregation('_input') as Input).setValue(this.getText()); 104 | } 105 | 106 | private _toEdit() { 107 | (this.getAggregation("_toEdit") as Button).setVisible(false); 108 | (this.getAggregation("_toShow") as Button).setVisible(true); 109 | (this.getAggregation('_prefix') as Label).setVisible(false); 110 | (this.getAggregation('_text') as Label).setVisible(false); 111 | (this.getAggregation("_input") as Input).setVisible(true); 112 | 113 | } 114 | 115 | private _toShow() { 116 | (this.getAggregation("_toEdit") as Button).setVisible(true); 117 | (this.getAggregation("_toShow") as Button).setVisible(false); 118 | (this.getAggregation('_prefix') as Label).setVisible(true); 119 | (this.getAggregation('_text') as Label).setVisible(true); 120 | (this.getAggregation("_input") as Input).setVisible(false); 121 | } 122 | 123 | private _textChanged(oEvent: InputBase$ChangeEvent) { 124 | const newText: string = oEvent.getParameter('value'); 125 | this.setText(newText); 126 | (this.getAggregation("_input") as Input).setValue(newText); 127 | (this.getAggregation('_text') as Label).setText(newText); 128 | 129 | this.fireEvent("change", { 130 | value: this.getText() 131 | }); 132 | } 133 | 134 | static renderer: typeof EditableTextRenderer = EditableTextRenderer; 135 | } -------------------------------------------------------------------------------- /webapp/control/EditableTextRenderer.ts: -------------------------------------------------------------------------------- 1 | import RenderManager from "sap/ui/core/RenderManager"; 2 | import EditableText from "./EditableText"; 3 | import Label from "sap/m/Title"; 4 | import Input from "sap/m/Input"; 5 | import Button from "sap/m/Button"; 6 | 7 | /*! 8 | * ${copyright} 9 | */ 10 | 11 | /** 12 | * EditableText renderer. 13 | * @namespace com.ui5.journeyrecorder 14 | */ 15 | export default { 16 | apiVersion: 2, 17 | 18 | render: function (rm: RenderManager, control: EditableText) { 19 | rm.openStart("div", control); 20 | rm.class("editable-text") 21 | rm.openEnd(); 22 | 23 | const isTitleStyle = control.getUseAsTitle(); 24 | const prefix = control.getAggregation('_prefix') as Label; 25 | if(isTitleStyle) { 26 | prefix.addStyleClass('title-use') 27 | } 28 | rm.renderControl(prefix); 29 | 30 | if ((control.getAggregation('_prefix') as Label).getText() !== '') { 31 | rm.openStart("span"); 32 | rm.openEnd(); 33 | rm.text("\u00a0"); 34 | rm.close("span"); 35 | } 36 | 37 | const text = control.getAggregation('_text') as Label; 38 | if(isTitleStyle) { 39 | text.addStyleClass('title-use') 40 | } 41 | rm.renderControl(text); 42 | 43 | rm.renderControl(control.getAggregation('_input') as Input); 44 | rm.renderControl(control.getAggregation('_toEdit') as Button) 45 | rm.renderControl(control.getAggregation('_toShow') as Button) 46 | 47 | rm.close("div"); 48 | } 49 | } -------------------------------------------------------------------------------- /webapp/controller/App.controller.ts: -------------------------------------------------------------------------------- 1 | import BaseController from "./BaseController"; 2 | 3 | /** 4 | * @namespace com.ui5.journeyrecorder.controller 5 | */ 6 | export default class App extends BaseController { 7 | public onInit(): void { 8 | // apply content density mode to root view 9 | this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /webapp/controller/BaseController.ts: -------------------------------------------------------------------------------- 1 | import Controller from "sap/ui/core/mvc/Controller"; 2 | import UIComponent from "sap/ui/core/UIComponent"; 3 | import AppComponent from "../Component"; 4 | import Model from "sap/ui/model/Model"; 5 | import ResourceModel from "sap/ui/model/resource/ResourceModel"; 6 | import ResourceBundle from "sap/base/i18n/ResourceBundle"; 7 | import Router from "sap/ui/core/routing/Router"; 8 | import History from "sap/ui/core/routing/History"; 9 | import UI5Element from "sap/ui/core/Element"; 10 | import Dialog from "sap/m/Dialog"; 11 | import Fragment from "sap/ui/core/Fragment"; 12 | import Event from "sap/ui/base/Event"; 13 | import JSONModel from "sap/ui/model/json/JSONModel"; 14 | import SettingsStorageService, { AppSettings } from "../service/SettingsStorage.service"; 15 | import { TestFrameworks } from "../model/enum/TestFrameworks"; 16 | import { ConnectionStatus } from "../model/enum/ConnectionStatus"; 17 | import { IconColor, ValueState } from "sap/ui/core/library"; 18 | import { ButtonType, DialogType } from "sap/m/library"; 19 | import Button from "sap/m/Button"; 20 | import Text from "sap/m/Text"; 21 | import BusyIndicator from "sap/ui/core/BusyIndicator"; 22 | import { ChromeExtensionService } from "../service/ChromeExtension.service"; 23 | import MessageToast from "sap/m/MessageToast"; 24 | import XMLView from "sap/ui/core/mvc/XMLView"; 25 | 26 | /** 27 | * @namespace com.ui5.journeyrecorder.controller 28 | */ 29 | export default abstract class BaseController extends Controller { 30 | protected settingsDialog: UI5Element; 31 | protected _unsafeDialog: Dialog; 32 | 33 | private _dialogs: Record; 38 | 39 | private _fragments: Record; 40 | 41 | /** 42 | * Convenience method for accessing the component of the controller's view. 43 | * @returns The component of the controller's view 44 | */ 45 | public getOwnerComponent(): AppComponent { 46 | return super.getOwnerComponent() as AppComponent; 47 | } 48 | 49 | /** 50 | * Convenience method to get the components' router instance. 51 | * @returns The router instance 52 | */ 53 | public getRouter(): Router { 54 | return UIComponent.getRouterFor(this); 55 | } 56 | 57 | /** 58 | * Convenience method for getting the i18n resource bundle of the component. 59 | * @returns The i18n resource bundle of the component 60 | */ 61 | public getResourceBundle(): ResourceBundle | Promise { 62 | const oModel = this.getOwnerComponent().getModel("i18n") as ResourceModel; 63 | return oModel.getResourceBundle(); 64 | } 65 | 66 | /** 67 | * Convenience method for getting the view model by name in every controller of the application. 68 | * @param [sName] The model name 69 | * @returns The model instance 70 | */ 71 | public getModel(sName?: string): Model { 72 | return this.getView().getModel(sName); 73 | } 74 | 75 | /** 76 | * Convenience method for setting the view model in every controller of the application. 77 | * @param oModel The model instance 78 | * @param [sName] The model name 79 | * @returns The current base controller instance 80 | */ 81 | public setModel(oModel: Model, sName?: string): BaseController { 82 | this.getView().setModel(oModel, sName); 83 | return this; 84 | } 85 | 86 | /** 87 | * Convenience method for triggering the navigation to a specific target. 88 | * @public 89 | * @param sName Target name 90 | * @param [oParameters] Navigation parameters 91 | * @param [bReplace] Defines if the hash should be replaced (no browser history entry) or set (browser history entry) 92 | */ 93 | public navTo(sName: string, oParameters?: object, bReplace?: boolean): void { 94 | this.getRouter().navTo(sName, oParameters, undefined, bReplace); 95 | } 96 | 97 | /** 98 | * Convenience event handler for navigating back. 99 | * It there is a history entry we go one step back in the browser history 100 | * If not, it will replace the current entry of the browser history with the main route. 101 | */ 102 | public onNavBack(): void { 103 | const sPreviousHash = History.getInstance().getPreviousHash(); 104 | if (sPreviousHash !== undefined) { 105 | window.history.go(-1); 106 | } else { 107 | this.getRouter().navTo("main", {}, undefined, true); 108 | } 109 | } 110 | 111 | async onOpenSettingsDialog() { 112 | await this.openDialog("Settings"); 113 | } 114 | 115 | setConnecting() { 116 | (this.getModel() as JSONModel).setProperty('/connectionStatus', ConnectionStatus.CONNECTING); 117 | } 118 | 119 | setConnected() { 120 | (this.getModel() as JSONModel).setProperty('/connectionStatus', ConnectionStatus.CONNECTED); 121 | } 122 | 123 | setDisconnected() { 124 | (this.getModel() as JSONModel).setProperty('/connectionStatus', ConnectionStatus.DISCONNECTED); 125 | } 126 | 127 | connectionIcon(connectionStatus: ConnectionStatus) { 128 | switch (connectionStatus) { 129 | case ConnectionStatus.CONNECTED: 130 | return 'sap-icon://connected'; 131 | case ConnectionStatus.DISCONNECTED: 132 | return 'sap-icon://disconnected'; 133 | case ConnectionStatus.CONNECTING: 134 | return 'sap-icon://along-stacked-chart'; 135 | default: 136 | return ''; 137 | } 138 | } 139 | 140 | connectionColor(connectionStatus: ConnectionStatus) { 141 | switch (connectionStatus) { 142 | case ConnectionStatus.CONNECTED: 143 | return IconColor.Positive; 144 | case ConnectionStatus.DISCONNECTED: 145 | return IconColor.Negative; 146 | case ConnectionStatus.CONNECTING: 147 | return IconColor.Neutral; 148 | default: 149 | return IconColor.Default; 150 | } 151 | } 152 | 153 | protected _openUnsafedDialog(callbacks: { success?: () => void | Promise; error?: () => void | Promise }) { 154 | if (!this._unsafeDialog) { 155 | this._unsafeDialog = new Dialog({ 156 | type: DialogType.Message, 157 | state: ValueState.Warning, 158 | title: 'Unsafed Changes!', 159 | content: new Text({ text: "You have unsafed changes, proceed?" }), 160 | beginButton: new Button({ 161 | type: ButtonType.Attention, 162 | text: 'Proceed', 163 | press: () => { 164 | if (callbacks.success) { 165 | void callbacks.success(); 166 | } 167 | this._unsafeDialog.close(); 168 | } 169 | }), 170 | endButton: new Button({ 171 | text: 'Cancel', 172 | press: () => { 173 | if (callbacks.error) { 174 | void callbacks.error(); 175 | } 176 | this._unsafeDialog.close(); 177 | } 178 | }) 179 | }) 180 | } 181 | this._unsafeDialog.open(); 182 | } 183 | 184 | protected async onConnect(url: string) { 185 | BusyIndicator.show(); 186 | this.setConnecting(); 187 | await ChromeExtensionService.getInstance().reconnectToPage(url); 188 | BusyIndicator.hide(); 189 | this.setConnected(); 190 | MessageToast.show('Connected', { duration: 500 }); 191 | } 192 | 193 | protected async onDisconnect() { 194 | try { 195 | await ChromeExtensionService.getInstance().disconnect(); 196 | this.setDisconnected(); 197 | MessageToast.show('Disconnected', { duration: 500 }); 198 | } catch (e) { 199 | console.error(e); 200 | this.setDisconnected(); 201 | ChromeExtensionService.getInstance().setCurrentTab(); 202 | MessageToast.show('Disconnected', { duration: 500 }); 203 | } 204 | } 205 | 206 | protected openDialog(sDialogName: string, oData?: Record): Promise | void> { 207 | if (!this._dialogs) { 208 | this._dialogs = {}; 209 | } 210 | 211 | return new Promise(async (resolve, reject) => { 212 | if (!this._dialogs[sDialogName]) { 213 | const oDialog = new Dialog({ 214 | showHeader: false 215 | }); 216 | this.getView().addDependent(oDialog); 217 | const oView = await this.getOwnerComponent().runAsOwner(async () => { 218 | return await XMLView.create({ 219 | viewName: `com.ui5.journeyrecorder.view.dialogs.${sDialogName}` 220 | }); 221 | }); 222 | const oController = oView.getController(); 223 | oDialog.addContent(oView); 224 | 225 | this._dialogs[sDialogName] = { 226 | dialog: oDialog, 227 | view: oView, 228 | controller: oController 229 | } 230 | } 231 | const oDialogCompound = this._dialogs[sDialogName]; 232 | if (oData) { 233 | oDialogCompound.view.setModel(new JSONModel(oData), "importData"); 234 | } 235 | 236 | if (oDialogCompound.controller.settings.initialHeight) { 237 | oDialogCompound.dialog.setContentHeight(oDialogCompound.controller.settings.initialHeight); 238 | } 239 | 240 | if (oDialogCompound.controller.settings.initialWidth) { 241 | oDialogCompound.dialog.setContentWidth(oDialogCompound.controller.settings.initialWidth); 242 | } 243 | 244 | const beforeClose = (oEvent: Event) => { 245 | oDialogCompound.dialog.detachBeforeClose(beforeClose); 246 | const pars = oEvent.getParameters() as Record; 247 | oDialogCompound.dialog.close(); 248 | 249 | if (pars.status === "Success") { 250 | if (pars.data) { 251 | resolve(pars.data as Record); 252 | } else { 253 | resolve(); 254 | } 255 | } else { 256 | reject(); 257 | } 258 | }; 259 | 260 | oDialogCompound.dialog.attachBeforeClose(beforeClose); 261 | 262 | oDialogCompound.dialog.open(); 263 | }) 264 | } 265 | 266 | protected async openFragment(sFragmentName: string, sFragmentId?: string): Promise { 267 | if (!sFragmentName) { 268 | throw new Error("At least the Fragment-Name is needed!"); 269 | } 270 | 271 | if (!this._fragments) { 272 | this._fragments = {}; 273 | } 274 | 275 | if (!this._fragments[sFragmentName]) { 276 | const oFragmentDialog = await Fragment.load({ 277 | id: sFragmentId || `${sFragmentName}_id`, 278 | name: `com.ui5.journeyrecorder.view.dialogs.${sFragmentName}`, 279 | controller: this 280 | }) 281 | this.getView().addDependent(oFragmentDialog as UI5Element); 282 | } 283 | 284 | this._fragments[sFragmentName].open(); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /webapp/controller/Main.controller.ts: -------------------------------------------------------------------------------- 1 | import BaseController from "./BaseController"; 2 | import JSONModel from "sap/ui/model/json/JSONModel"; 3 | import JourneyStorageService from "../service/JourneyStorage.service"; 4 | import Journey from "../model/class/Journey.class"; 5 | import BusyIndicator from "sap/ui/core/BusyIndicator"; 6 | import Dialog from "sap/m/Dialog"; 7 | import Text from "sap/m/Text"; 8 | import { ButtonType, DialogType } from "sap/m/library"; 9 | import MessageToast from "sap/m/MessageToast"; 10 | import Button from "sap/m/Button"; 11 | import { ChromeExtensionService } from "../service/ChromeExtension.service"; 12 | import UI5Element from "sap/ui/core/Element"; 13 | import Event from "sap/ui/base/Event"; 14 | import List from "sap/m/List"; 15 | import SearchField from "sap/m/SearchField"; 16 | import Filter from "sap/ui/model/Filter"; 17 | import FilterOperator from "sap/ui/model/FilterOperator"; 18 | import { Tab } from "../service/ChromeExtension.service"; 19 | import SettingsStorageService, { AppSettings } from "../service/SettingsStorage.service"; 20 | 21 | /** 22 | * @namespace com.ui5.journeyrecorder.controller 23 | */ 24 | export default class Main extends BaseController { 25 | private _approveUploadDialog: Dialog; 26 | private _removeJourneyDialog: Dialog; 27 | private _timerIndex: number; 28 | onInit() { 29 | const model = new JSONModel({}); 30 | this.setModel(model, 'main'); 31 | this.getRouter().getRoute("main").attachPatternMatched(async () => { 32 | await this._loadTabs(); 33 | //the periodic load 34 | this._timerIndex = setInterval(async () => { await this._loadTabs(); }, 5000); 35 | }, this); 36 | // eslint-disable-next-line @typescript-eslint/unbound-method 37 | this.getRouter().getRoute("main").attachPatternMatched(this._loadJourneys, this); 38 | this.getView().addEventDelegate({ 39 | onBeforeHide: () => { 40 | clearInterval(this._timerIndex); 41 | } 42 | }, this) 43 | } 44 | 45 | onUploadJourney() { 46 | BusyIndicator.show(); 47 | const input: HTMLInputElement = document.createElement("input"); 48 | input.type = "file"; 49 | input.accept = ".json"; 50 | input.addEventListener('change', ($event: Event) => { 51 | const files = ($event.currentTarget as HTMLInputElement).files; 52 | const reader = new FileReader(); 53 | reader.onload = (input) => { 54 | const content = input.target?.result as string; 55 | if (content) { 56 | void this._importFinished(content); 57 | } 58 | }; 59 | if (files) { 60 | reader.readAsText(files[0]); 61 | } 62 | }); 63 | input.click(); 64 | input.remove(); 65 | } 66 | 67 | onSearch(oEvent: sap.ui.base.Event): void { 68 | // add filter for search 69 | const aFilters = []; 70 | const sourceId: string = (oEvent.getSource() as SearchField).getId(); 71 | const sQuery = (oEvent.getSource() as SearchField).getValue(); 72 | let oList: List; 73 | 74 | if (sourceId.indexOf('-tabs') > -1) { 75 | oList = this.byId("tabsList"); 76 | if (sQuery && sQuery.length > 0) { 77 | const filter = new Filter("title", FilterOperator.Contains, sQuery); 78 | aFilters.push(filter); 79 | } 80 | } else { 81 | oList = this.byId("journeysList"); 82 | if (sQuery && sQuery.length > 0) { 83 | const filter = new Filter("name", FilterOperator.Contains, sQuery); 84 | aFilters.push(filter); 85 | } 86 | } 87 | // update list binding 88 | const oBinding = oList.getBinding("items"); 89 | oBinding.filter(aFilters); 90 | } 91 | 92 | navigateToJourney($event: sap.ui.base.Event) { 93 | const source: UI5Element = $event.getSource(); 94 | const bindingCtx = source.getBindingContext('main'); 95 | const id: string = bindingCtx.getProperty('id') as string; 96 | 97 | this.getRouter().navTo('journey', { id: id }); 98 | } 99 | 100 | connectToTab($event: sap.ui.base.Event) { 101 | const source: UI5Element = $event.getSource() as UI5Element; 102 | const bindingCtx = source.getBindingContext('main'); 103 | const tab: Tab = bindingCtx.getObject() as Tab; 104 | 105 | if (tab) { 106 | this.getRouter().navTo("recording", { tabId: tab.id }); 107 | } 108 | } 109 | 110 | private async _loadTabs(): Promise { 111 | //the initial load 112 | let settings = (this.getModel('settings') as JSONModel)?.getData() as AppSettings; 113 | if (!settings) { 114 | settings = await SettingsStorageService.getSettings(); 115 | } 116 | void ChromeExtensionService.getAllTabs(settings.showUI5only).then((aTabs) => { 117 | (this.getModel('main') as JSONModel).setProperty("/tabs", aTabs); 118 | }); 119 | } 120 | 121 | private _loadJourneys(): void { 122 | void JourneyStorageService.getInstance().getAll().then((journeys: Journey[]) => { 123 | (this.getModel('main') as JSONModel).setProperty("/journeys", journeys); 124 | }); 125 | } 126 | 127 | private async _importFinished(jsonString: string): Promise { 128 | const jour = Journey.fromJSON(jsonString); 129 | const exists = await JourneyStorageService.getInstance().getById(jour.id); 130 | if (exists) { 131 | BusyIndicator.hide(); 132 | if (!this._approveUploadDialog) { 133 | this._approveUploadDialog = new Dialog({ 134 | type: DialogType.Message, 135 | title: 'Journey already exists!', 136 | content: new Text({ text: "A journey with the same id already exists, override?" }), 137 | beginButton: new Button({ 138 | type: ButtonType.Critical, 139 | text: "Override!", 140 | press: () => { 141 | BusyIndicator.show(); 142 | void JourneyStorageService.getInstance().save(jour).then(() => { 143 | BusyIndicator.hide(); 144 | this._approveUploadDialog.close() 145 | MessageToast.show('Journey imported!'); 146 | this._loadJourneys(); 147 | }); 148 | } 149 | }), 150 | endButton: new Button({ 151 | text: "Cancel", 152 | press: () => { 153 | this._approveUploadDialog.close() 154 | } 155 | }) 156 | }); 157 | } 158 | this._approveUploadDialog.open(); 159 | } else { 160 | await JourneyStorageService.getInstance().save(jour); 161 | BusyIndicator.hide(); 162 | MessageToast.show('Journey imported!'); 163 | this._loadJourneys(); 164 | } 165 | } 166 | async removeJourney($event: sap.ui.base.Event): Promise { 167 | const source: UI5Element = $event.getSource(); 168 | const bindingCtx = source.getBindingContext('main'); 169 | const jour = await JourneyStorageService.getInstance().getById(bindingCtx.getProperty('id') as string); 170 | if (!this._removeJourneyDialog) { 171 | this._removeJourneyDialog = new Dialog({ 172 | type: DialogType.Message, 173 | title: 'Delete Journey!', 174 | content: new Text({ text: `Do you really want to delete the journey "${bindingCtx.getProperty('name')}"?` }), 175 | beginButton: new Button({ 176 | type: ButtonType.Critical, 177 | text: "Delete!", 178 | press: () => { 179 | BusyIndicator.show(); 180 | void JourneyStorageService.getInstance().deleteJourney(jour).then(() => { 181 | BusyIndicator.hide(); 182 | this._removeJourneyDialog.close() 183 | MessageToast.show('Journey removed!'); 184 | this._loadJourneys(); 185 | }); 186 | } 187 | }), 188 | endButton: new Button({ 189 | text: "Cancel", 190 | press: () => { 191 | this._removeJourneyDialog.close() 192 | } 193 | }) 194 | }); 195 | } 196 | this._removeJourneyDialog.open(); 197 | } 198 | } -------------------------------------------------------------------------------- /webapp/controller/StepPage.controller.ts: -------------------------------------------------------------------------------- 1 | import JSONModel from "sap/ui/model/json/JSONModel"; 2 | import BaseController from "./BaseController"; 3 | import JourneyStorageService from "../service/JourneyStorage.service"; 4 | import Event from "sap/ui/base/Event"; 5 | import Fragment from "sap/ui/core/Fragment"; 6 | import Menu from "sap/m/Menu"; 7 | import Button from "sap/m/Button"; 8 | import MenuItem from "sap/m/MenuItem"; 9 | import { RecordEvent, Step } from "../model/class/Step.class"; 10 | import { AppSettings } from "../service/SettingsStorage.service"; 11 | import { CodeStyles, TestFrameworks } from "../model/enum/TestFrameworks"; 12 | import MessageToast from "sap/m/MessageToast"; 13 | import CodeGenerationService from "../service/CodeGeneration.service"; 14 | import { Route$MatchedEvent } from "sap/ui/core/routing/Route"; 15 | import BusyIndicator from "sap/ui/core/BusyIndicator"; 16 | import { ChromeExtensionService } from "../service/ChromeExtension.service"; 17 | import Utils from "../model/class/Utils.class"; 18 | import Dialog from "sap/m/Dialog"; 19 | import Text from "sap/m/Text"; 20 | import { DialogType } from "sap/m/library"; 21 | import { ValueState } from "sap/ui/core/library"; 22 | 23 | /** 24 | * @namespace com.ui5.journeyrecorder.controller 25 | */ 26 | export default class StepPage extends BaseController { 27 | private model: JSONModel; 28 | private setupModel: JSONModel; 29 | private frameworkMenu: Menu; 30 | private stepMenu: Menu; 31 | 32 | onInit() { 33 | this.model = new JSONModel({}); 34 | this.setModel(this.model, 'step'); 35 | const settingsModel = (this.getOwnerComponent().getModel('settings') as JSONModel).getData() as AppSettings; 36 | this.setupModel = new JSONModel({ 37 | codeStyle: 'javascript', 38 | code: `module.exports = function (config) { 39 | "use strict"; 40 | 41 | config.set({ 42 | frameworks: ["ui5"], 43 | browsers: ["Chrome"] 44 | }); 45 | };`, 46 | paged: settingsModel.pagedDefault, 47 | framework: settingsModel.framework, 48 | propertyChanged: false 49 | }); 50 | this.setModel(this.setupModel, 'stepSetup'); 51 | this.getRouter().getRoute("step").attachMatched((oEvent: Route$MatchedEvent) => { 52 | void this._loadStep(oEvent); 53 | }); 54 | this.getRouter().getRoute("step-define").attachMatched((oEvent: Route$MatchedEvent) => { 55 | void this._startStepDefinition(oEvent); 56 | }); 57 | } 58 | 59 | async onSave() { 60 | const journey = await JourneyStorageService.getInstance().getById((this.getModel('stepSetup') as JSONModel).getProperty('/journeyId') as string); 61 | const step = Step.fromObject((this.getModel("step") as JSONModel).getData() as Partial); 62 | journey.updateStep(step); 63 | await JourneyStorageService.getInstance().save(journey); 64 | MessageToast.show('Step saved!'); 65 | } 66 | 67 | async typeChange($event: Event) { 68 | const button: Button = $event.getSource(); 69 | if (!this.stepMenu) { 70 | this.stepMenu = await Fragment.load({ 71 | id: this.getView().getId(), 72 | name: "com.ui5.journeyrecorder.fragment.StepTypeMenu", 73 | controller: this 74 | }) as Menu; 75 | this.getView().addDependent(this.stepMenu); 76 | } 77 | this.stepMenu.openBy(button, false); 78 | } 79 | 80 | async frameworkChange($event: Event) { 81 | const button: Button = $event.getSource(); 82 | if (!this.frameworkMenu) { 83 | this.frameworkMenu = await Fragment.load({ 84 | id: this.getView().getId(), 85 | name: "com.ui5.journeyrecorder.fragment.TestFrameworkMenu", 86 | controller: this 87 | }) as Menu; 88 | this.getView().addDependent(this.frameworkMenu); 89 | } 90 | this.frameworkMenu.openBy(button, false); 91 | } 92 | 93 | async onStepRemove() { 94 | const jId = this.getModel('stepSetup').getProperty('/journeyId') as string; 95 | const id = this.model.getProperty('/id') as string; 96 | const journey = await JourneyStorageService.getInstance().getById(jId); 97 | if (journey) { 98 | const index = journey.steps.findIndex(step => step.id === id); 99 | journey.steps.splice(index, 1); 100 | this.navTo("journey", { id: journey.id }); 101 | } 102 | } 103 | 104 | onStepTypeChange(oEvent: Event) { 105 | const oItem = oEvent.getParameter("item" as never) as MenuItem; 106 | (this.getModel('step') as JSONModel).setProperty('/actionType', oItem.getKey()); 107 | } 108 | 109 | onCodeCriteriaChanged() { 110 | this._generateStepCode(); 111 | } 112 | 113 | getAttributeCount(property: unknown[]): number { 114 | return property?.length || 0; 115 | } 116 | 117 | getAttributeVisibility(property: unknown[]): boolean { 118 | return property?.length > 0; 119 | } 120 | 121 | async onCopyCode() { 122 | await navigator.clipboard.writeText((this.getModel('stepSetup') as JSONModel).getProperty('/code') as string); 123 | MessageToast.show("Code copied"); 124 | } 125 | 126 | async onReselect() { 127 | await this._startRedefinition(); 128 | } 129 | 130 | private async _loadStep(oEvent: Event) { 131 | const oArgs: { id: string; stepId: string } = oEvent.getParameter("arguments" as never); 132 | const step = await JourneyStorageService.getInstance().getStepById({ journeyId: oArgs.id, stepId: oArgs.stepId }); 133 | if (!step) { 134 | this.onNavBack(); 135 | return; 136 | } 137 | (this.getModel('stepSetup') as JSONModel).setProperty('/journeyId', oArgs.id); 138 | this.model.setData(step); 139 | this._generateStepCode(); 140 | } 141 | 142 | private async _startStepDefinition(oEvent: Event) { 143 | const oArgs: { id: string; stepId: string } = oEvent.getParameter("arguments" as never); 144 | const step = await JourneyStorageService.getInstance().getStepById({ journeyId: oArgs.id, stepId: oArgs.stepId }); 145 | if (!step) { 146 | this.onNavBack(); 147 | return; 148 | } 149 | (this.getModel('stepSetup') as JSONModel).setProperty('/journeyId', oArgs.id); 150 | this.model.setData(step); 151 | 152 | await this._startRedefinition(); 153 | } 154 | 155 | private async _startRedefinition() { 156 | BusyIndicator.show(0); 157 | // 1. get all steps 158 | const jour = await JourneyStorageService.getInstance().getById((this.getModel('stepSetup') as JSONModel).getProperty('/journeyId') as string); 159 | const steps = jour.steps; 160 | const selfIndex = steps.findIndex((s: Step) => s.id === (this.model.getData() as Step).id); 161 | const settings = (this.getModel('settings') as JSONModel).getData() as AppSettings; 162 | await this.onConnect(jour.startUrl); 163 | BusyIndicator.show(0); 164 | for (let index = 0; index < steps.length; index++) { 165 | await Utils.delay(1000 * settings.replayDelay) 166 | 167 | if (index === selfIndex) { 168 | //set "backend" to record mode 169 | const selectElementDialog = new Dialog({ 170 | state: ValueState.Information, 171 | type: DialogType.Message, 172 | title: 'Waiting for element select...', 173 | content: new Text({ text: "Please select an element at your UI5 application to redefine this step!" }) 174 | }); 175 | 176 | const onStepRerecord = (_1: string, _2: string, recordData: object) => { 177 | const newStep = Step.recordEventToStep(recordData as RecordEvent); 178 | jour.steps[selfIndex] = newStep; 179 | this.model.setData(newStep); 180 | ChromeExtensionService.getInstance().unregisterRecordingWebsocket(onStepRerecord, this); 181 | ChromeExtensionService.getInstance().disableRecording().then(() => { }).catch(() => { }).finally(() => { 182 | selectElementDialog.close(); 183 | selectElementDialog.destroy(); 184 | BusyIndicator.hide(); 185 | this.onDisconnect().then(() => { }).catch(() => { }); 186 | }) 187 | 188 | //assume the journey is call by reference it should work 189 | }; 190 | 191 | ChromeExtensionService.getInstance().registerRecordingWebsocket(onStepRerecord, this); 192 | await ChromeExtensionService.getInstance().enableRecording(); 193 | selectElementDialog.open(); 194 | break; 195 | } else { 196 | const curStep = steps[index]; 197 | try { 198 | await ChromeExtensionService.getInstance().performAction(curStep, settings.useRRSelector); 199 | } catch (e) { 200 | await this.onDisconnect(); 201 | MessageToast.show('An Error happened during replay former steps', { duration: 3000 }); 202 | BusyIndicator.hide(); 203 | return; 204 | } 205 | } 206 | } 207 | // replay the steps before this step by connecting to the page. 208 | // after the first click take the found element and action as new step setting 209 | // store and setup the step accordingly 210 | } 211 | 212 | private _generateStepCode(): void { 213 | const oViewModel = this.getModel('stepSetup'); 214 | const step = this.model.getData() as Step; 215 | const framework = oViewModel.getProperty('/framework') as TestFrameworks; 216 | const style = oViewModel.getProperty('/style') as CodeStyles; 217 | CodeGenerationService.generateStepCode(step, { framework, style }).then((generatedCode) => { 218 | (oViewModel as JSONModel).setProperty('/code', generatedCode); 219 | }); 220 | } 221 | } -------------------------------------------------------------------------------- /webapp/controller/dialogs/BaseDialogController.ts: -------------------------------------------------------------------------------- 1 | import Event from "sap/ui/base/Event"; 2 | import BaseController from "../BaseController"; 3 | import Dialog from "sap/m/Dialog"; 4 | 5 | /** 6 | * @namespace com.ui5.journeyrecorder.controller.dialogs 7 | */ 8 | export default class BaseDialogController extends BaseController { 9 | closeBySuccess(oPressEvent: Event, data?: Record) { 10 | const oResult: { 11 | origin: unknown, 12 | result: "Confirm" | "Abort", 13 | data?: Record 14 | } = { 15 | origin: oPressEvent.getSource(), 16 | result: "Confirm", 17 | }; 18 | if (data) { 19 | oResult.data = data; 20 | } 21 | (this.getView().getParent() as Dialog).fireBeforeClose(oResult) 22 | } 23 | 24 | closeByAbort(oPressEvent: Event) { 25 | (this.getView().getParent() as Dialog).fireBeforeClose({ 26 | origin: oPressEvent.getSource(), 27 | result: "Abort" 28 | }) 29 | } 30 | } -------------------------------------------------------------------------------- /webapp/controller/dialogs/Settings.controller.ts: -------------------------------------------------------------------------------- 1 | import BaseDialogController from "./BaseDialogController"; 2 | import { Themes } from "../../model/enum/Themes"; 3 | import Theming from "sap/ui/core/Theming"; 4 | import JSONModel from "sap/ui/model/json/JSONModel"; 5 | import SettingsStorageService, { AppSettings } from "../../service/SettingsStorage.service"; 6 | import ListItem from "sap/ui/core/ListItem"; 7 | import Event from "sap/ui/base/Event"; 8 | import Dialog from "sap/m/Dialog"; 9 | 10 | /** 11 | * @namespace com.ui5.journeyrecorder.controller.dialogs 12 | */ 13 | export default class Settings extends BaseDialogController { 14 | settings = { 15 | initialHeight: '91vh', 16 | initialWidth: '30rem' 17 | } 18 | 19 | onThemeSelect(oEvent: Event) { 20 | const oItem = oEvent.getParameter("selectedItem" as never) as ListItem; 21 | const oModel = this.getModel("settings") as JSONModel; 22 | oModel.setProperty('/theme', oItem.getKey()); 23 | Theming.setTheme(oItem.getKey()); 24 | } 25 | 26 | onCloseDialog(oEvent: Event, bSave: boolean) { 27 | if (bSave) { 28 | void SettingsStorageService.save((this.getModel("settings") as JSONModel).getData() as AppSettings); 29 | } 30 | (this.getView().getParent() as Dialog).fireBeforeClose({ 31 | origin: oEvent.getSource(), 32 | result: bSave ? 'Confirm' : 'Reject' 33 | }); 34 | } 35 | } -------------------------------------------------------------------------------- /webapp/css/control/editableText.css: -------------------------------------------------------------------------------- 1 | .editable-text{ 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: flex-start; 5 | align-items: center; 6 | } 7 | 8 | .editable-text > .title-use { 9 | font-size: 1.1rem; 10 | } -------------------------------------------------------------------------------- /webapp/css/hljs-github-theme.css: -------------------------------------------------------------------------------- 1 | pre code.hljs { 2 | overflow-x: auto; 3 | padding: 1em 4 | } 5 | code.hljs { 6 | padding: 3px 5px 7 | } 8 | /*! 9 | Theme: GitHub & Github Dark mixed 10 | Description: Dark theme as seen on github.com 11 | Author: github.com 12 | Maintainer: @Hirse 13 | Updated: 2021-05-15 14 | 15 | Outdated base version: https://github.com/primer/github-syntax-dark 16 | Current colors taken from GitHub's CSS 17 | */ 18 | /** bright theme **/ 19 | pre.bright .hljs { 20 | color: #24292e; 21 | background: #ffffff 22 | } 23 | pre.bright .hljs-doctag, 24 | pre.bright .hljs-keyword, 25 | pre.bright .hljs-meta .hljs-keyword, 26 | pre.bright .hljs-template-tag, 27 | pre.bright .hljs-template-variable, 28 | pre.bright .hljs-type, 29 | pre.bright .hljs-variable.language_ { 30 | /* prettylights-syntax-keyword */ 31 | color: #d73a49 32 | } 33 | 34 | pre.bright .hljs-title, 35 | pre.bright .hljs-title.class_, 36 | pre.bright .hljs-title.class_.inherited__, 37 | pre.bright .hljs-title.function_ { 38 | /* prettylights-syntax-entity */ 39 | color: #6f42c1 40 | } 41 | 42 | pre.bright .hljs-attr, 43 | pre.bright .hljs-attribute, 44 | pre.bright .hljs-literal, 45 | pre.bright .hljs-meta, 46 | pre.bright .hljs-number, 47 | pre.bright .hljs-operator, 48 | pre.bright .hljs-variable, 49 | pre.bright .hljs-selector-attr, 50 | pre.bright .hljs-selector-class, 51 | pre.bright .hljs-selector-id { 52 | /* prettylights-syntax-constant */ 53 | color: #005cc5 54 | } 55 | 56 | pre.bright .hljs-regexp, 57 | pre.bright .hljs-string, 58 | pre.bright .hljs-meta .hljs-string { 59 | /* prettylights-syntax-string */ 60 | color: #032f62 61 | } 62 | 63 | pre.bright .hljs-built_in, 64 | pre.bright .hljs-symbol { 65 | /* prettylights-syntax-variable */ 66 | color: #e36209 67 | } 68 | 69 | pre.bright .hljs-comment, 70 | pre.bright .hljs-code, 71 | pre.bright .hljs-formula { 72 | /* prettylights-syntax-comment */ 73 | color: #6a737d 74 | } 75 | 76 | pre.bright .hljs-name, 77 | pre.bright .hljs-quote, 78 | pre.bright .hljs-selector-tag, 79 | pre.bright .hljs-selector-pseudo { 80 | /* prettylights-syntax-entity-tag */ 81 | color: #22863a 82 | } 83 | 84 | pre.bright .hljs-subst { 85 | /* prettylights-syntax-storage-modifier-import */ 86 | color: #24292e 87 | } 88 | 89 | pre.bright .hljs-section { 90 | /* prettylights-syntax-markup-heading */ 91 | color: #005cc5; 92 | font-weight: bold 93 | } 94 | 95 | pre.bright .hljs-bullet { 96 | /* prettylights-syntax-markup-list */ 97 | color: #735c0f 98 | } 99 | 100 | pre.bright .hljs-emphasis { 101 | /* prettylights-syntax-markup-italic */ 102 | color: #24292e; 103 | font-style: italic 104 | } 105 | 106 | pre.bright .hljs-strong { 107 | /* prettylights-syntax-markup-bold */ 108 | color: #24292e; 109 | font-weight: bold 110 | } 111 | 112 | pre.bright .hljs-addition { 113 | /* prettylights-syntax-markup-inserted */ 114 | color: #22863a; 115 | background-color: #f0fff4 116 | } 117 | 118 | pre.bright .hljs-deletion { 119 | /* prettylights-syntax-markup-deleted */ 120 | color: #b31d28; 121 | background-color: #ffeef0 122 | } 123 | 124 | /** dark theme **/ 125 | pre.dark .hljs { 126 | color: #c9d1d9; 127 | background: #0d1117 128 | } 129 | 130 | pre.dark .hljs-doctag, 131 | pre.dark .hljs-keyword, 132 | pre.dark .hljs-meta .hljs-keyword, 133 | pre.dark .hljs-template-tag, 134 | pre.dark .hljs-template-variable, 135 | pre.dark .hljs-type, 136 | pre.dark .hljs-variable.language_ { 137 | /* prettylights-syntax-keyword */ 138 | color: #ff7b72 139 | } 140 | 141 | pre.dark .hljs-title, 142 | pre.dark .hljs-title.class_, 143 | pre.dark .hljs-title.class_.inherited__, 144 | pre.dark .hljs-title.function_ { 145 | /* prettylights-syntax-entity */ 146 | color: #d2a8ff 147 | } 148 | 149 | pre.dark .hljs-attr, 150 | pre.dark .hljs-attribute, 151 | pre.dark .hljs-literal, 152 | pre.dark .hljs-meta, 153 | pre.dark .hljs-number, 154 | pre.dark .hljs-operator, 155 | pre.dark .hljs-variable, 156 | pre.dark .hljs-selector-attr, 157 | pre.dark .hljs-selector-class, 158 | pre.dark .hljs-selector-id { 159 | /* prettylights-syntax-constant */ 160 | color: #79c0ff 161 | } 162 | 163 | pre.dark .hljs-regexp, 164 | pre.dark .hljs-string, 165 | pre.dark .hljs-meta .hljs-string { 166 | /* prettylights-syntax-string */ 167 | color: #a5d6ff 168 | } 169 | 170 | pre.dark .hljs-built_in, 171 | pre.dark .hljs-symbol { 172 | /* prettylights-syntax-variable */ 173 | color: #ffa657 174 | } 175 | 176 | pre.dark .hljs-comment, 177 | pre.dark .hljs-code, 178 | pre.dark .hljs-formula { 179 | /* prettylights-syntax-comment */ 180 | color: #8b949e 181 | } 182 | 183 | pre.dark .hljs-name, 184 | pre.dark .hljs-quote, 185 | pre.dark .hljs-selector-tag, 186 | pre.dark .hljs-selector-pseudo { 187 | /* prettylights-syntax-entity-tag */ 188 | color: #7ee787 189 | } 190 | 191 | pre.dark .hljs-subst { 192 | /* prettylights-syntax-storage-modifier-import */ 193 | color: #c9d1d9 194 | } 195 | 196 | pre.dark .hljs-section { 197 | /* prettylights-syntax-markup-heading */ 198 | color: #1f6feb; 199 | font-weight: bold 200 | } 201 | 202 | pre.dark .hljs-bullet { 203 | /* prettylights-syntax-markup-list */ 204 | color: #f2cc60 205 | } 206 | 207 | pre.dark .hljs-emphasis { 208 | /* prettylights-syntax-markup-italic */ 209 | color: #c9d1d9; 210 | font-style: italic 211 | } 212 | 213 | pre.dark .hljs-strong { 214 | /* prettylights-syntax-markup-bold */ 215 | color: #c9d1d9; 216 | font-weight: bold 217 | } 218 | 219 | pre.dark .hljs-addition { 220 | /* prettylights-syntax-markup-inserted */ 221 | color: #aff5b4; 222 | background-color: #033a16 223 | } 224 | 225 | pre.dark .hljs-deletion { 226 | /* prettylights-syntax-markup-deleted */ 227 | color: #ffdcd7; 228 | background-color: #67060c 229 | } -------------------------------------------------------------------------------- /webapp/css/journeyPage.css: -------------------------------------------------------------------------------- 1 | .step-list .step-item { 2 | padding: 0.5rem; 3 | } 4 | 5 | .space-even { 6 | justify-content: space-evenly; 7 | } 8 | 9 | .journey-page-header .sapFDynamicPageTitleMain>.sapFDynamicPageTitleMainInner .sapFDynamicPageTitleMainHeading .sapFDynamicPageTitleMainHeadingInner .sapMTitle:not(.sapUICompVarMngmtTitle){ 10 | font-size: 1rem; 11 | } 12 | 13 | pre code.hljs.internal, pre.dark code.hljs.internal { 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: flex-start; 17 | align-items: flex-start; 18 | } 19 | 20 | pre code.hljs div.row, pre.dark code.hljs div.row { 21 | display: flex; 22 | flex-direction: row; 23 | } 24 | 25 | pre code.hljs div.row .line-indicator, pre.dark code.hljs div.row .line-indicator { 26 | min-width: 1rem; 27 | padding: 0 0.5rem; 28 | text-align: end; 29 | border-right: 1px solid; 30 | } 31 | 32 | pre code.hljs div.row .code, pre.dark code.hljs div.row .code { 33 | margin-left: 0.5rem; 34 | } -------------------------------------------------------------------------------- /webapp/css/main.css: -------------------------------------------------------------------------------- 1 | .journey-line { 2 | margin-left: auto; 3 | } -------------------------------------------------------------------------------- /webapp/css/stepPage.css: -------------------------------------------------------------------------------- 1 | .sapUxAPObjectPageHeaderTitle .step-page-header h2.sapUxAPObjectPageHeaderIdentifierTitle, .sapUxAPObjectPageHeaderTitle.sapUxAPObjectPageHeaderStickied .step-page-header h2.sapUxAPObjectPageHeaderIdentifierTitle{ 2 | font-size: 1rem; 3 | } 4 | 5 | 6 | 7 | .step-page-header .sapFDynamicPageTitleMain>.sapFDynamicPageTitleMainInner .sapFDynamicPageTitleMainHeading .sapFDynamicPageTitleMainHeadingInner .sapMTitle:not(.sapUICompVarMngmtTitle){ 8 | font-size: 1rem; 9 | } 10 | 11 | /*for code styling look at the journey css*/ -------------------------------------------------------------------------------- /webapp/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui5-community/ui5-journey-recorder/57204a751cb31a178c50f94214e9f5806a1d93d4/webapp/favicon.ico -------------------------------------------------------------------------------- /webapp/fragment/RecordingDialog.fragment.xml: -------------------------------------------------------------------------------- 1 | 4 |

5 | 6 | 10 | 11 | 15 | 16 |