├── .autorc ├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.js ├── lerna.json ├── package.json ├── packages ├── .DS_Store ├── README.md ├── extension │ ├── .gitignore │ ├── README.md │ ├── __mocks__ │ │ ├── antd.js.bak │ │ ├── dexie.js │ │ ├── fileMock.js │ │ └── styleMock.js │ ├── extension │ │ ├── logo.png │ │ ├── manifest.json │ │ ├── manifestUpdater.js │ │ ├── popup.html │ │ ├── start.html │ │ └── update.xml │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── actions │ │ │ ├── __mocks__ │ │ │ │ ├── app.js │ │ │ │ ├── editor.js │ │ │ │ ├── filesystem.js │ │ │ │ └── utilities.js │ │ │ ├── action_types.js │ │ │ ├── app.js │ │ │ ├── editor.js │ │ │ ├── filesystem.js │ │ │ ├── index.js │ │ │ ├── player.js │ │ │ ├── project.js │ │ │ ├── suites.js │ │ │ └── utilities.js │ │ ├── app.js │ │ ├── common │ │ │ ├── blocks.js │ │ │ ├── capture_screenshot.js │ │ │ ├── command_runner.js │ │ │ ├── commands.js │ │ │ ├── constant.js │ │ │ ├── drag_mock │ │ │ │ ├── DataTransfer.js │ │ │ │ ├── DragDropAction.js │ │ │ │ ├── eventFactory.js │ │ │ │ └── index.js │ │ │ ├── filesystem.js │ │ │ ├── inspector.js │ │ │ ├── ipc │ │ │ │ ├── cs_postmessage.js │ │ │ │ ├── ipc_bg_cs.js │ │ │ │ ├── ipc_cs.js │ │ │ │ └── ipc_promise.js │ │ │ ├── log.js │ │ │ ├── network.js │ │ │ ├── player.js │ │ │ ├── send_keys.js │ │ │ ├── storage │ │ │ │ ├── __mocks__ │ │ │ │ │ └── index.js │ │ │ │ ├── ext_storage.js │ │ │ │ ├── index.js │ │ │ │ └── localstorage_adapter.js │ │ │ ├── substitution_builder.js │ │ │ ├── utils.js │ │ │ └── web_extension.js │ │ ├── components │ │ │ ├── Header.jsx │ │ │ ├── Modals │ │ │ │ ├── DuplicateModal.jsx │ │ │ │ ├── NewBlockModal.jsx │ │ │ │ ├── PlayLoopModal.jsx │ │ │ │ ├── RenameModal.jsx │ │ │ │ ├── SaveModal.jsx │ │ │ │ ├── SettingModal.jsx │ │ │ │ └── ShareBlockModal.jsx │ │ │ ├── Project │ │ │ │ ├── FolderBrowser.jsx │ │ │ │ └── ProjectModal.jsx │ │ │ ├── Sidebar │ │ │ │ ├── Block.jsx │ │ │ │ ├── Sidebar.jsx │ │ │ │ ├── Suite.jsx │ │ │ │ └── Testcase.jsx │ │ │ └── dashboard │ │ │ │ ├── CommandDoc.jsx │ │ │ │ ├── CommandOptions.jsx │ │ │ │ ├── CommandTable.jsx │ │ │ │ ├── DashboardBottom.jsx │ │ │ │ ├── DashboardEditor.jsx │ │ │ │ ├── PauseButton.jsx │ │ │ │ ├── PlayButton.jsx │ │ │ │ ├── PlayMenu.jsx │ │ │ │ ├── PlaybackControl.jsx │ │ │ │ ├── ResumeButton.jsx │ │ │ │ ├── StopButton.jsx │ │ │ │ ├── SuiteEditor.jsx │ │ │ │ ├── TestcaseDropdown.jsx │ │ │ │ └── fields │ │ │ │ ├── CommandButtons.jsx │ │ │ │ ├── CommandField.jsx │ │ │ │ ├── EnvironmentField.jsx │ │ │ │ └── OneField.jsx │ │ ├── config │ │ │ └── index.js │ │ ├── containers │ │ │ ├── Header.js │ │ │ ├── Modals │ │ │ │ ├── DuplicateModal.js │ │ │ │ ├── NewBlockModal.js │ │ │ │ ├── PlayLoopModal.js │ │ │ │ ├── RenameModal.js │ │ │ │ ├── SaveModal.js │ │ │ │ ├── SaveMultiSelectModal.js │ │ │ │ ├── SettingModal.js │ │ │ │ └── ShareBlockModal.js │ │ │ ├── Project │ │ │ │ ├── FolderBrowser.js │ │ │ │ └── ProjectModal.js │ │ │ ├── Sidebar.js │ │ │ └── dashboard │ │ │ │ ├── CommandOptions.js │ │ │ │ ├── CommandTable.js │ │ │ │ ├── DashboardBottom.js │ │ │ │ ├── DashboardEditor.js │ │ │ │ ├── PlayButton.js │ │ │ │ ├── PlayMenu.js │ │ │ │ ├── SuiteEditor.js │ │ │ │ ├── TestcaseDropdown.js │ │ │ │ ├── fields │ │ │ │ ├── CommandButtons.js │ │ │ │ └── CommandField.js │ │ │ │ └── index.js │ │ ├── ext │ │ │ ├── bg.js │ │ │ ├── content_script.js │ │ │ ├── cssSelector.js │ │ │ └── inject.js │ │ ├── index.js │ │ ├── models │ │ │ ├── __mocks__ │ │ │ │ ├── block-model.js │ │ │ │ ├── project_model.js │ │ │ │ ├── suite_model.js │ │ │ │ └── test_case_model.js │ │ │ ├── block-model.js │ │ │ ├── db.js │ │ │ ├── project_model.js │ │ │ ├── suite_model.js │ │ │ └── test_case_model.js │ │ ├── reducers │ │ │ ├── app.js │ │ │ ├── dropdowns.js │ │ │ ├── editor.js │ │ │ ├── files.js │ │ │ ├── modals.js │ │ │ ├── player.js │ │ │ └── projectSetup.js │ │ ├── redux │ │ │ ├── index.js │ │ │ ├── post_logic_middleware.js │ │ │ └── promise_middleware.js │ │ └── styles │ │ │ ├── app.scss │ │ │ ├── common.scss │ │ │ ├── dashboard.scss │ │ │ ├── header.scss │ │ │ └── sidebar.scss │ ├── test │ │ ├── actions │ │ │ ├── player.test.js │ │ │ ├── project.test.js │ │ │ ├── suites.test.js │ │ │ └── utilities.test.js │ │ ├── app.test.js │ │ ├── common │ │ │ ├── block.test.js │ │ │ ├── capture_screenshot.test.js │ │ │ ├── command_runner.test.js │ │ │ ├── filesystem.test.js │ │ │ ├── ipc │ │ │ │ ├── cs_postmessage.test.js │ │ │ │ ├── ipc_bg_cs.test.js │ │ │ │ ├── ipc_cs.test.js │ │ │ │ └── ipc_promise.test.js │ │ │ ├── network.test.js │ │ │ ├── send_keys.test.js │ │ │ ├── storage │ │ │ │ ├── ext_storage.test.js │ │ │ │ └── localstorage_adapter.test.js │ │ │ ├── substitution_builder.test.js │ │ │ └── utils.test.js │ │ ├── components │ │ │ ├── Header.test.js │ │ │ ├── Modals │ │ │ │ ├── DuplicateModal.test.js │ │ │ │ ├── NewBlockModal.test.js │ │ │ │ ├── PlayLoopModal.test.js │ │ │ │ ├── RenameModal.test.js │ │ │ │ ├── SaveModal.test.js │ │ │ │ ├── SettingModal.test.js │ │ │ │ └── ShareBlockModal.test.js │ │ │ ├── Project │ │ │ │ ├── FolderBrowser.test.js │ │ │ │ └── ProjectModal.test.js │ │ │ ├── Sidebar │ │ │ │ ├── Block.test.js │ │ │ │ ├── Sidebar.test.js │ │ │ │ ├── Suite.test.js │ │ │ │ └── Testcase.test.js │ │ │ └── dashboard │ │ │ │ ├── CommandDoc.test.js │ │ │ │ ├── CommandOptions.test.js │ │ │ │ ├── CommandTable.test.js │ │ │ │ ├── DashboardBottom.test.js │ │ │ │ ├── DashboardEditor.test.js │ │ │ │ ├── PauseButton.test.js │ │ │ │ ├── PlayButton.test.js │ │ │ │ ├── PlayMenu.test.js │ │ │ │ ├── PlaybackControl.test.js │ │ │ │ ├── ResumeButton.test.js │ │ │ │ ├── StopButton.test.js │ │ │ │ ├── SuiteEditor.test.js │ │ │ │ ├── TestcaseDropdown.test.js │ │ │ │ └── fields │ │ │ │ ├── CommandButtons.test.js │ │ │ │ ├── CommandField.test.js │ │ │ │ ├── EnvironmentField.test.js │ │ │ │ └── OneField.test.js │ │ ├── containers │ │ │ ├── Header.test.js │ │ │ ├── Modals │ │ │ │ ├── DuplicateModal.test.js │ │ │ │ ├── NewBlockModal.test.js │ │ │ │ ├── PlayLoopModal.test.js │ │ │ │ ├── RenameModal.test.js │ │ │ │ ├── SaveModal.test.js │ │ │ │ ├── SaveMultiSelectModal.test.js │ │ │ │ ├── SettingModal.test.js │ │ │ │ └── ShareBlockModal.test.js │ │ │ ├── Project │ │ │ │ ├── FolderBrowser.test.js │ │ │ │ └── ProjectModal.test.js │ │ │ ├── Sidebar.test.js │ │ │ └── dashboard │ │ │ │ ├── CommandOptions.test.js │ │ │ │ ├── CommandTable.test.js │ │ │ │ ├── DashboardBottom.test.js │ │ │ │ ├── DashboardEditor.test.js │ │ │ │ ├── PlayButton.test.js │ │ │ │ ├── PlayMenu.test.js │ │ │ │ ├── SuiteEditor.test.js │ │ │ │ ├── TestcaseDropdown.test.js │ │ │ │ ├── fields.test.js │ │ │ │ ├── fields │ │ │ │ ├── CommandButtons.test.js │ │ │ │ └── CommandField.test.js │ │ │ │ └── index.test.js │ │ ├── models │ │ │ ├── block-model.test.js │ │ │ ├── project_model.test.js │ │ │ └── suite_model.test.js │ │ ├── reducers │ │ │ ├── reducers.app.test.js │ │ │ ├── reducers.dropdowns.test.js │ │ │ ├── reducers.editor.blocks.test.js │ │ │ ├── reducers.editor.commands.test.js │ │ │ ├── reducers.editor.suites.test.js │ │ │ ├── reducers.editor.test.js │ │ │ ├── reducers.editor.testcases.test.js │ │ │ ├── reducers.files.test.js │ │ │ ├── reducers.modals.test.js │ │ │ ├── reducers.player.test.js │ │ │ └── reducers.projectSetup.test.js │ │ ├── redux │ │ │ ├── index.test.js │ │ │ ├── post_logic_middleware.test.js │ │ │ └── promise_middleware.test.js │ │ └── utils.js │ └── webpack.config.js ├── host │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── __mocks__ │ │ └── simple-git │ │ │ └── promise.js │ ├── package.json │ ├── src │ │ ├── index.js │ │ ├── lib │ │ │ ├── README.md │ │ │ ├── common.js │ │ │ ├── docker.js │ │ │ ├── filesystem.js │ │ │ ├── git.js │ │ │ ├── index.js │ │ │ └── shell.js │ │ └── static │ │ │ ├── com.intuit.replayweb.json │ │ │ ├── install.sh │ │ │ ├── nativeHost.sh │ │ │ ├── nativeHostLocal.sh │ │ │ ├── setup-ssh.sh │ │ │ └── setup.sh │ ├── test │ │ ├── docker.test.js │ │ ├── filesystem.test.js │ │ ├── git.test.js │ │ ├── index.test.js │ │ ├── messageHandler.test.js │ │ └── shell.test.js │ └── webpack.config.js ├── testrunner │ ├── .gitignore │ ├── README.md │ ├── __mocks__ │ │ └── realPlugin.js │ ├── jest.config.js │ ├── package.json │ ├── replay.config.json │ ├── src │ │ ├── README.md │ │ ├── command_runner.js │ │ ├── configuration.js │ │ ├── index.js │ │ └── utilities.js │ ├── test │ │ ├── integration │ │ │ ├── json │ │ │ │ └── smokeTest.json │ │ │ └── tests │ │ │ │ └── smokeTest.js │ │ ├── unit │ │ │ ├── command_runner.runCommand.test.js │ │ │ ├── configuration.test.js │ │ │ ├── index.getDefaults.test.js │ │ │ ├── index.loadFromConfig.test.js │ │ │ ├── index.removeTemporaryFiles.test.js │ │ │ ├── index.runCommands.test.js │ │ │ ├── index.runInParallel.test.js │ │ │ ├── index.runInParallelAllRegions.test.js │ │ │ ├── plugins.test.js │ │ │ ├── utilities.elements.test.js │ │ │ ├── utilities.getExecElString.test.js │ │ │ ├── utilities.getSelector.test.js │ │ │ ├── utilities.loadPlugin.test.js │ │ │ └── utilities.log.test.js │ │ └── utilities │ │ │ ├── favicon.ico │ │ │ ├── framed.html │ │ │ └── index.html │ └── wdio.conf.js └── utils │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ ├── README.md │ └── index.js │ ├── test │ └── index.test.js │ └── webpack.config.js ├── setupEnzyme.js └── yarn.lock /.autorc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "npm", 4 | "released", 5 | ["upload-assets", ["./packages/extension/dist/replayui.crx"]] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | packages/extension/dist 3 | packages/host/dist 4 | packages/testrunner/lib 5 | packages/host/lib 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "plugin:react/recommended", 6 | "prettier", 7 | "prettier/standard" 8 | ], 9 | "plugins": ["react", "jest", "prettier", "standard"], 10 | "env": { 11 | "browser": true, 12 | "node": true, 13 | "mocha": true, 14 | "jest/globals": true 15 | }, 16 | "globals": { 17 | "browser": true 18 | }, 19 | "settings": { 20 | "react": { 21 | "version": "16" 22 | } 23 | }, 24 | "rules": { 25 | "semi": [1, "never"], 26 | "react/prop-types": 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | #This is used to automatically add reviewers to PR's. Developers can still manually add reviewers in addition to the below users. 2 | 3 | #This is the catch all. If no other pattern is matched, it will request a review from the following users 4 | * @cedgington @tclarkson @crutledge 5 | 6 | #Tim is being marked owner as this is where most community files are stored and should be involved in these changes 7 | /.github/ @tclarkson 8 | *README.md @tclarkson 9 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Open Source Code of Conduct 2 | Open source projects are “living.” Contributions in the form of issues and pull requests are welcomed and encouraged. When you contribute, you explicitly say you are part of the community and abide by its Code of Conduct. 3 | 4 | ## The Code 5 | At Intuit, we foster a kind, respectful, harassment-free cooperative community. Our open source community works to: 6 | 7 | 8 | * Be kind and respectful; 9 | * Act as a global community; 10 | * Conduct ourselves professionally. 11 | 12 | As members of this community, we will not tolerate behaviors including, but not limited to: 13 | 14 | * Violent threats or language; 15 | * Discriminatory or derogatory jokes or language; 16 | * Public or private harassment of any kind; 17 | * Other conduct considered inappropriate in a professional setting. 18 | 19 | ## Reporting Concerns 20 | If you see someone violating the Code of Conduct please email TechOpenSource@intuit.com 21 | 22 | ## Scope 23 | This code of conduct applies to: 24 | 25 | All repos and communities for Intuit-managed projects, whether or not the text is included in a Intuit-managed project’s repository; 26 | 27 | Individuals or teams representing projects in official capacity, such as via official social media channels or at in-person meetups. 28 | 29 | ## Attribution 30 | This Code of Conduct is partly inspired by and based on those of [Amazon](https://aws.github.io/code-of-conduct.html), [CocoaPods](https://github.com/CocoaPods/CocoaPods/blob/master/CODE_OF_CONDUCT.md), [Github](https://opensource.guide/code-of-conduct/), [Microsoft](https://opensource.microsoft.com/codeofconduct/), [thoughtbot](https://thoughtbot.com/open-source-code-of-conduct), and on the [Contributor Covenant version 1.4.1](https://www.contributor-covenant.org/). 31 | 32 | Please refer to the [Intuit Open Source](https://opensource.intuit.com/) website for more information. -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | We fully embrace the open source model, and if you have something to add, we would love to review it and get it merged in! 4 | 5 | ## Before You Start 6 | 7 | Ensure your development environment meets the system requirements in [Getting Started](https://github.com/intuit/replayweb#getting-started). 8 | 9 | ## Contribution Process 10 | 11 | Please note we have a [Code of Conduct](https://github.com/intuit/replayweb/blob/master/.github/CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 12 | 13 | ## Communicating with the team 14 | 15 | Before starting work, please check the [issues](https://github.com/intuit/replayweb/issues) to see what's being discussed and worked on. If there's an open, unresolved issue for what you want to work on, please comment on it stating that you would like to tackle the changes. If there's not an issue, please add one and also state that you'll work on it. 16 | 17 | ## Development flow 18 | 19 | 1. Fork the repo 20 | 1. Install the dependencies - run `yarn` 21 | 1. Build your packages - run `yarn build` 22 | 1. Optional: create a branch to work off of 23 | 1. Write the code 24 | 1. Update/write tests: 25 | 1. `yarn test` will run tests and output coverage reports 26 | 1. `yarn jest --watch` is useful for development 27 | 1. Ensure all code matches the "Code Expectations" discussed below 28 | 1. Commit and push your code to your fork 29 | 1. Open a pull request 30 | 31 | ## Code Expectations 32 | 33 | #### Code Coverage 34 | 35 | All new code will be unit tested to 90% coverage. 36 | 37 | #### Coding Style 38 | 39 | Code should be written in a functional manner when possible. This means avoiding mutability and returning copies of data rather than modifying shared variables. For example: 40 | 41 | Don't do this: 42 | 43 | ```js 44 | let someData = [1,2,3,4] 45 | let list = [] 46 | for(let i = 0; i< list.someData.length; i++) { 47 | list.push(i * 2) // This function doesn't return anything, it modifies list 48 | } 49 | console.log(list) 50 | // [2,4,6,8] 51 | ``` 52 | 53 | Instead, do this: 54 | 55 | ```js 56 | const someData = [1,2,3,4] 57 | const list = someData.map(i => i * 2) 58 | console.log(list) 59 | // [2,4,6,8] 60 | ``` 61 | 62 | ## Contact Information 63 | 64 | The best way to contact the team is through the [issues](https://github.com/intuit/replayweb/issues); we'll get back to you within 3 business days. 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Bug 2 | 3 | ### Actual Behavior 4 | 5 | Describe the problem. 6 | 7 | ### Desired Behavior 8 | 9 | Describe what the goal is. 10 | 11 | ### Steps to Reproduce 12 | 13 | Provide clear steps to reproduce the issue unless already covered. 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thank you for contributing this pull request! 2 | 3 | Please make the PR against the `master` branch, add a description of what's changing, and link any relevant issues or PRs. 4 | 5 | ## What's changing 6 | 7 | ... 8 | 9 | ## What else might be impacted? 10 | 11 | ... 12 | 13 | ## Checklist 14 | 15 | [ ] Unit tests (updated and/or added) 16 | [ ] Documentation (function/class docs, comments, etc.) 17 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current' 8 | } 9 | } 10 | ], 11 | '@babel/preset-react' 12 | ], 13 | plugins: ['@babel/plugin-proposal-class-properties'], 14 | env: { 15 | test: { 16 | plugins: ['rewire'] 17 | } 18 | }, 19 | overrides: [ 20 | { 21 | test: /.*packages\/.*plugin?\//, 22 | plugins: ['add-module-exports'] 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: ['packages/*/src/**/*.js', 'packages/*/src/**/*.jsx'], 3 | reporters: [ 4 | 'default', 5 | [ 6 | 'jest-junit', 7 | { 8 | outputDirectory: './target' 9 | } 10 | ] 11 | ], 12 | roots: ['/packages/'], 13 | projects: ['/packages/'], 14 | coverageReporters: ['lcov', 'text', 'cobertura'], 15 | testPathIgnorePatterns: ['/test/integration/'], 16 | moduleNameMapper: { 17 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 18 | '/packages/extension/__mocks__/fileMock.js', 19 | '\\.(css|less|scss)$': '/packages/extension/__mocks__/styleMock.js' 20 | }, 21 | setupFilesAfterEnv: ['/setupEnzyme.js'] 22 | } 23 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "npmClient": "yarn", 6 | "useWorkspaces": true, 7 | "version": "1.0.4" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "replayweb", 3 | "private": true, 4 | "version": "0.1.0", 5 | "description": "", 6 | "main": "index.js", 7 | "author": "Harris Borawski", 8 | "scripts": { 9 | "build": "lerna run build", 10 | "test": "jest --coverage", 11 | "test:watch": "jest --watch", 12 | "version": "git add .", 13 | "lint": "eslint '**/*.{js,jsx}' --ignore-pattern '**/lib/**/*'", 14 | "lint:fix": "eslint --fix '**/*.{js,jsx}'", 15 | "release": "auto shipit" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/intuit/ReplayWeb" 20 | }, 21 | "publishConfig": { 22 | "registry": "https://registry.npmjs.org/", 23 | "access": "public" 24 | }, 25 | "workspaces": [ 26 | "packages/*" 27 | ], 28 | "keywords": [], 29 | "license": "AGPL", 30 | "devDependencies": { 31 | "@auto-it/upload-assets": "^7.8.0", 32 | "@babel/core": "^7.4.4", 33 | "@babel/plugin-proposal-class-properties": "^7.4.4", 34 | "@babel/preset-env": "^7.4.4", 35 | "@babel/preset-react": "^7.0.0", 36 | "@testing-library/react": "^9.1.3", 37 | "auto": "^7.8.0", 38 | "babel-eslint": "^10.0.3", 39 | "babel-jest": "^24.9.0", 40 | "babel-plugin-add-module-exports": "^1.0.2", 41 | "babel-plugin-rewire": "^1.2.0", 42 | "enzyme": "^3.10.0", 43 | "enzyme-adapter-react-16": "^1.14.0", 44 | "eslint": "^6.4.0", 45 | "eslint-config-prettier": "^6.3.0", 46 | "eslint-config-standard": "^14.1.0", 47 | "eslint-plugin-chai-friendly": "^0.4.1", 48 | "eslint-plugin-html": "^6.0.0", 49 | "eslint-plugin-import": "^2.18.2", 50 | "eslint-plugin-jest": "^22.17.0", 51 | "eslint-plugin-mocha": "^6.1.1", 52 | "eslint-plugin-node": "^10.0.0", 53 | "eslint-plugin-prettier": "^3.1.1", 54 | "eslint-plugin-promise": "^4.2.1", 55 | "eslint-plugin-react": "^7.14.3", 56 | "eslint-plugin-standard": "^4.0.1", 57 | "husky": "^3.0.8", 58 | "jest": "^24.7.1", 59 | "jest-junit": "^6.3.0", 60 | "lerna": "^3.13.4", 61 | "lint-staged": "^9.4.1", 62 | "prettier": "^1.18.2", 63 | "react-dom": "^16.9.0" 64 | }, 65 | "prettier": { 66 | "singleQuote": true, 67 | "semi": false 68 | }, 69 | "husky": { 70 | "hooks": { 71 | "pre-commit": "lint-staged" 72 | } 73 | }, 74 | "lint-staged": { 75 | "*.js": [ 76 | "eslint --fix", 77 | "git add" 78 | ] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intuit/ReplayWeb/8b4cb972f86bbdbf00417674caff30a279563cac/packages/.DS_Store -------------------------------------------------------------------------------- /packages/README.md: -------------------------------------------------------------------------------- 1 | # ReplayWeb Packages 2 | 3 | ReplayWeb is comprised of the following packages: 4 | 5 | - [@replayweb/extension](extension/) - allows you to record and playback functional tests for web applications from within Chrome browser. 6 | - [@replayweb/testrunner](testrunner/) - allows you to feed recorded tests from the extension into webdriverio and run on the command line. 7 | - [@replayweb/host](host/) - a native messaging host that allows the extension to communicate with the filesystem of your local machine to save files that you can push to your source repository. 8 | -------------------------------------------------------------------------------- /packages/extension/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | public/bundle.js 5 | coverage 6 | target 7 | .idea/ 8 | replaykit-v2.iml 9 | -------------------------------------------------------------------------------- /packages/extension/__mocks__/antd.js.bak: -------------------------------------------------------------------------------- 1 | /* 2 | This file is disabled. 3 | 4 | To enable, rename to 'antd.js'. 5 | 6 | This file is disabled because it blocks the unit testing of components 7 | that use imports from the 'antd' library. It's unclear what this mock 8 | was originally solving for. If you find an instance where this mock 9 | is needed in order to get tests to work, then you're free to explore 10 | enabling this mock, but be aware that doing so without solving for other 11 | imports will break a ton of component unit tests. 12 | */ 13 | 14 | const antd = jest.genMockFromModule('antd') 15 | antd.message = {} 16 | antd.message.success = () => Promise.resolve() 17 | antd.message.error = () => Promise.resolve() 18 | export default antd 19 | -------------------------------------------------------------------------------- /packages/extension/__mocks__/dexie.js: -------------------------------------------------------------------------------- 1 | class Dexie { 2 | version() { 3 | return { 4 | stores: () => {} 5 | } 6 | } 7 | 8 | open() {} 9 | } 10 | export default Dexie 11 | -------------------------------------------------------------------------------- /packages/extension/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | // Simple mock file so that Jest doesn't try to load 2 | // SCSS files as JavaScript. 3 | // 4 | // https://jestjs.io/docs/en/webpack.html#handling-static-assets 5 | 6 | module.exports = 'test-file-stub' 7 | -------------------------------------------------------------------------------- /packages/extension/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | // Simple mock file so that Jest doesn't try to load 2 | // SCSS files as JavaScript. 3 | // 4 | // https://jestjs.io/docs/en/webpack.html#handling-static-assets 5 | 6 | module.exports = {} 7 | -------------------------------------------------------------------------------- /packages/extension/extension/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intuit/ReplayWeb/8b4cb972f86bbdbf00417674caff30a279563cac/packages/extension/extension/logo.png -------------------------------------------------------------------------------- /packages/extension/extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": { 3 | "persistent": true, 4 | "scripts": [ 5 | "background.js" 6 | ] 7 | }, 8 | "browser_action": { 9 | "default_icon": { 10 | "38": "logo.png" 11 | } 12 | }, 13 | "content_scripts": [ 14 | { 15 | "all_frames": true, 16 | "js": [ 17 | "content_script.js" 18 | ], 19 | "match_about_blank": true, 20 | "matches": [ 21 | "" 22 | ], 23 | "run_at": "document_start" 24 | } 25 | ], 26 | "content_security_policy": "script-src 'self'; object-src 'self'", 27 | "description": "Test Automation for CG", 28 | "icons": { 29 | "128": "logo.png" 30 | }, 31 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp/Qfsu01ICj2s3YBvGnGkLk2xQ2YEKu54M8RP/7zC5ZNajP2suqc4dtHIoPWXR3d+/Be46ERli2Vm0H24vy50UkK5hM51dpdyucCUtlrvyDrVBxVM82x6kdXXsmCvS4e/Bkuay1Hrmi2GGfp7TZRO0jlhBvLnPxuKHc3bhhrKIAziYNM3ukpiqpYnBC4MnrelpjWVkd/Ufb1NJRFvAj/MXb3TROSEYSuawt0VO8oUjO6JxT4923mU/eVV6iGi+XSqaqlQ56WNoNX6I6A30FetKm9wCAhdBm2hi2WfNKfrDCqYuopnyH2ZOOlchf9AIsZVzLtIxdBWsISKGZuVla8zQIDAQAB", 32 | "manifest_version": 2, 33 | "name": "Replay", 34 | "offline_enabled": true, 35 | "permissions": [ 36 | "webRequest", 37 | "webRequestBlocking", 38 | "nativeMessaging", 39 | "storage", 40 | "tabs", 41 | "notifications", 42 | "cookies", 43 | "file:///*", 44 | "http://*/*", 45 | "https://*/*", 46 | "" 47 | ], 48 | "short_name": "Replay", 49 | "version": "1.0.4", 50 | "web_accessible_resources": [ 51 | "content_script.js" 52 | ] 53 | } -------------------------------------------------------------------------------- /packages/extension/extension/manifestUpdater.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const version = process.env.npm_package_version 5 | const manifestPath = path.resolve(__dirname, 'manifest.json') 6 | if (!version) { 7 | throw new Error( 8 | 'npm_package_version not defined in environment, this script should only be run from npm' 9 | ) 10 | } else { 11 | const manifestContents = JSON.parse(fs.readFileSync(manifestPath)) 12 | fs.writeFileSync( 13 | manifestPath, 14 | JSON.stringify({ ...manifestContents, version }, null, 4) 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /packages/extension/extension/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Replay 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/extension/extension/start.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Replay 8 | 9 | 10 | 35 | 36 |
37 |
38 | 39 |
Starting Replay Test...
40 |
41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /packages/extension/extension/update.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/extension/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('autoprefixer')] 3 | } 4 | -------------------------------------------------------------------------------- /packages/extension/src/actions/__mocks__/app.js: -------------------------------------------------------------------------------- 1 | import { types as T } from '../action_types' 2 | export const changeModalState = () => ({ type: T.MODAL_STATE }) 3 | -------------------------------------------------------------------------------- /packages/extension/src/actions/__mocks__/editor.js: -------------------------------------------------------------------------------- 1 | import { types as T } from '../action_types' 2 | export const setTestCases = () => ({ type: T.SET_TEST_CASES }) 3 | 4 | export const setBlocks = () => ({ type: T.SET_BLOCKS }) 5 | 6 | export const importTestCases = () => ({ type: T.SET_TEST_CASES }) 7 | 8 | export const importBlocks = () => ({ type: T.SET_BLOCKS }) 9 | 10 | export const importSuites = () => ({ type: T.SET_SUITES }) 11 | -------------------------------------------------------------------------------- /packages/extension/src/actions/__mocks__/filesystem.js: -------------------------------------------------------------------------------- 1 | import { types as T } from '../action_types' 2 | export const selectFolder = () => ({ type: T.SELECT_FOLDER }) 3 | export const makeDirectory = () => Promise.resolve() 4 | -------------------------------------------------------------------------------- /packages/extension/src/actions/__mocks__/utilities.js: -------------------------------------------------------------------------------- 1 | let fail = false 2 | let ret = {} 3 | export function nativeMessage(msg) { 4 | return fail ? Promise.reject(new Error()) : Promise.resolve(ret[msg.type]) 5 | } 6 | 7 | export function logMessage() { 8 | return { 9 | type: 'LOG_MESSAGE' 10 | } 11 | } 12 | const __setReturn = (k, v) => { 13 | ret[k] = v 14 | } 15 | const __setFail = f => { 16 | fail = f 17 | } 18 | const __reset = () => { 19 | fail = false 20 | ret = {} 21 | } 22 | export default { 23 | __setFail, 24 | __setReturn, 25 | __reset 26 | } 27 | -------------------------------------------------------------------------------- /packages/extension/src/actions/index.js: -------------------------------------------------------------------------------- 1 | export * from './utilities' 2 | export * from './filesystem' 3 | export * from './project' 4 | export * from './player' 5 | export * from './app' 6 | export * from './editor' 7 | export * from './suites' 8 | -------------------------------------------------------------------------------- /packages/extension/src/actions/suites.js: -------------------------------------------------------------------------------- 1 | import suiteModel from '../models/suite_model' 2 | import { setSuites } from './editor' 3 | import { types as T } from './action_types' 4 | 5 | export function addTestToSuite(test) { 6 | return { 7 | type: T.ADD_SUITE_TEST, 8 | test 9 | } 10 | } 11 | 12 | export function removeTestFromSuite(test) { 13 | return { 14 | type: T.REMOVE_SUITE_TEST, 15 | test 16 | } 17 | } 18 | 19 | // Thunk Actions 20 | 21 | export function createSuite(newSuite) { 22 | return (dispatch, getState) => { 23 | const projectId = getState().editor.project.id 24 | return suiteModel 25 | .insert({ ...newSuite, projectId }) 26 | .then(() => suiteModel.listByProject(projectId)) 27 | .then(suites => dispatch(setSuites(suites))) 28 | } 29 | } 30 | 31 | export function updateSuite(suite) { 32 | return (dispatch, getState) => { 33 | const projectId = getState().editor.project.id 34 | return suiteModel 35 | .update(suite.id, suite) 36 | .then(() => suiteModel.listByProject(projectId)) 37 | .then(suites => dispatch(setSuites(suites))) 38 | } 39 | } 40 | 41 | export function removeSuite(suite) { 42 | return (dispatch, getState) => { 43 | const projectId = getState().editor.project.id 44 | return suiteModel 45 | .remove(suite.id) 46 | .then(() => suiteModel.listByProject(projectId)) 47 | .then(suites => dispatch(setSuites(suites))) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/extension/src/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Dashboard from './containers/dashboard' 4 | import Header from './containers/Header' 5 | import Sidebar from './containers/Sidebar' 6 | import 'antd/dist/antd.css' 7 | import './styles/app.scss' 8 | import { DragDropContext } from 'react-dnd' 9 | import HTML5Backend from 'react-dnd-html5-backend' 10 | import { Layout } from 'antd' 11 | import { readBlockShareConfig } from './actions/app' 12 | import { connect } from 'react-redux' 13 | 14 | export class App extends Component { 15 | render() { 16 | return ( 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | ) 25 | } 26 | 27 | componentDidMount() { 28 | console.log('READING Block Share config from file system') 29 | this.props.readBlockShareConfig() 30 | } 31 | } 32 | 33 | App.propTypes = { 34 | readBlockShareConfig: PropTypes.func.isRequired 35 | } 36 | 37 | const mapStateToProps = state => { 38 | return {} 39 | } 40 | 41 | const mapDispatchToProps = dispatch => { 42 | return { 43 | readBlockShareConfig: () => dispatch(readBlockShareConfig()) 44 | } 45 | } 46 | 47 | export default connect( 48 | mapStateToProps, 49 | mapDispatchToProps 50 | )(DragDropContext(HTML5Backend)(App)) 51 | -------------------------------------------------------------------------------- /packages/extension/src/common/blocks.js: -------------------------------------------------------------------------------- 1 | /** 2 | Collapse a Test case that has already been expanded with blocks 3 | Put in the "expandedindex" so that each item knows where the index was after it was expanded 4 | **/ 5 | export const collapseExpandedTestCase = commands => { 6 | const { resultList } = commands.reduce( 7 | (current, nextCommand, index) => { 8 | if (nextCommand.isBlock) { 9 | return { 10 | isRunningBlock: true, 11 | resultList: current.isRunningBlock 12 | ? current.resultList 13 | : [ 14 | ...current.resultList, 15 | { 16 | ...nextCommand, 17 | expandedIndex: index 18 | } 19 | ] 20 | } 21 | } 22 | return { 23 | isRunningBlock: false, 24 | resultList: [ 25 | ...current.resultList, 26 | { 27 | ...nextCommand, 28 | expandedIndex: index 29 | } 30 | ] 31 | } 32 | }, 33 | { 34 | isRunningBlock: false, 35 | resultList: [] 36 | } 37 | ) 38 | return resultList 39 | } 40 | -------------------------------------------------------------------------------- /packages/extension/src/common/capture_screenshot.js: -------------------------------------------------------------------------------- 1 | import Ext from './web_extension' 2 | import fs from './filesystem' 3 | 4 | // refer to https://stackoverflow.com/questions/12168909/blob-from-dataurl 5 | function dataURItoBlob(dataURI) { 6 | // convert base64 to raw binary data held in a string 7 | // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this 8 | var byteString = atob(dataURI.split(',')[1]) 9 | 10 | // separate out the mime component 11 | var mimeString = dataURI 12 | .split(',')[0] 13 | .split(':')[1] 14 | .split(';')[0] 15 | 16 | // write the bytes of the string to an ArrayBuffer 17 | var ab = new ArrayBuffer(byteString.length) 18 | 19 | // create a view into the buffer 20 | var ia = new Uint8Array(ab) 21 | 22 | // set the bytes of the buffer to the correct values 23 | for (var i = 0; i < byteString.length; i++) { 24 | ia[i] = byteString.charCodeAt(i) 25 | } 26 | 27 | // write the ArrayBuffer to a blob, and you're done 28 | var blob = new Blob([ab], { type: mimeString }) 29 | return blob 30 | } 31 | 32 | function getActiveTabInfo() { 33 | return Ext.windows.getLastFocused().then(win => { 34 | return Ext.tabs 35 | .query({ active: true, windowId: win.id }) 36 | .then(tabs => tabs[0]) 37 | }) 38 | } 39 | 40 | function captureScreenBlob() { 41 | return Ext.tabs.captureVisibleTab(null, { format: 'png' }).then(dataURItoBlob) 42 | } 43 | 44 | export default function saveScreen() { 45 | return Promise.all([getActiveTabInfo(), captureScreenBlob()]).then( 46 | ([tabInfo, screenBlob]) => { 47 | const fileName = `${Date.now()}_${tabInfo.title.replace(/\s/g, '_')}.png` 48 | 49 | return fs.writeFile(fileName, screenBlob).then(url => url) 50 | } 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /packages/extension/src/common/constant.js: -------------------------------------------------------------------------------- 1 | const mk = list => 2 | list.reduce((prev, key) => { 3 | prev[key] = key 4 | return prev 5 | }, {}) 6 | 7 | export const APP_STATUS = mk(['NORMAL', 'INSPECTOR', 'RECORDER', 'PLAYER']) 8 | 9 | export const INSPECTOR_STATUS = mk(['PENDING', 'INSPECTING', 'STOPPED']) 10 | 11 | export const RECORDER_STATUS = mk(['PENDING', 'RECORDING', 'STOPPED']) 12 | 13 | export const PLAYER_STATUS = mk(['PLAYING', 'PAUSED', 'STOPPED']) 14 | 15 | export const CONTENT_SCRIPT_STATUS = mk([ 16 | 'NORMAL', 17 | 'RECORDING', 18 | 'INSPECTING', 19 | 'PLAYING' 20 | ]) 21 | 22 | export const TEST_CASE_STATUS = mk(['NORMAL', 'SUCCESS', 'ERROR']) 23 | 24 | export const EDITOR_STATUS = mk(['TESTS', 'BLOCKS', 'SUITES']) 25 | -------------------------------------------------------------------------------- /packages/extension/src/common/drag_mock/DataTransfer.js: -------------------------------------------------------------------------------- 1 | function removeFromArray(array, item) { 2 | var index = array.indexOf(item) 3 | 4 | if (index >= 0) { 5 | array.splice(index, 1) 6 | } 7 | } 8 | 9 | var DataTransfer = function() { 10 | this.dataByFormat = {} 11 | 12 | this.dropEffect = 'none' 13 | this.effectAllowed = 'all' 14 | this.files = [] 15 | this.types = [] 16 | } 17 | 18 | DataTransfer.prototype.clearData = function(dataFormat) { 19 | if (dataFormat) { 20 | delete this.dataByFormat[dataFormat] 21 | removeFromArray(this.types, dataFormat) 22 | } else { 23 | this.dataByFormat = {} 24 | this.types = [] 25 | } 26 | } 27 | 28 | DataTransfer.prototype.getData = function(dataFormat) { 29 | return this.dataByFormat[dataFormat] 30 | } 31 | 32 | DataTransfer.prototype.setData = function(dataFormat, data) { 33 | this.dataByFormat[dataFormat] = data 34 | 35 | if (this.types.indexOf(dataFormat) < 0) { 36 | this.types.push(dataFormat) 37 | } 38 | 39 | return true 40 | } 41 | 42 | DataTransfer.prototype.setDragImage = function() { 43 | // don't do anything (the stub just makes sure there is no error thrown if someone tries to call the method) 44 | } 45 | 46 | module.exports = window.DataTransfer || DataTransfer 47 | -------------------------------------------------------------------------------- /packages/extension/src/common/drag_mock/eventFactory.js: -------------------------------------------------------------------------------- 1 | var DataTransfer = require('./DataTransfer') 2 | 3 | var dataTransferEvents = [ 4 | 'drag', 5 | 'dragstart', 6 | 'dragenter', 7 | 'dragover', 8 | 'dragend', 9 | 'drop', 10 | 'dragleave' 11 | ] 12 | 13 | function mergeInto(destObj, srcObj) { 14 | for (var key in srcObj) { 15 | if (!srcObj[key]) { 16 | continue 17 | } // ignore inherited properties 18 | 19 | destObj[key] = srcObj[key] 20 | } 21 | 22 | return destObj 23 | } 24 | 25 | function createModernEvent(eventName, eventType, eventProperties) { 26 | // if (eventType === 'DragEvent') { eventType = 'CustomEvent'; } // Firefox fix (since FF does not allow us to override dataTransfer) 27 | 28 | var constructor = window[eventType] 29 | var options = { view: window, bubbles: true, cancelable: true } 30 | 31 | mergeInto(options, eventProperties) 32 | 33 | var event = new constructor(eventName, options) 34 | 35 | mergeInto(event, eventProperties) 36 | 37 | return event 38 | } 39 | 40 | function createLegacyEvent(eventName, eventType, eventProperties) { 41 | var event 42 | 43 | switch (eventType) { 44 | case 'MouseEvent': 45 | event = document.createEvent('MouseEvent') 46 | event.initEvent(eventName, true, true) 47 | break 48 | 49 | default: 50 | event = document.createEvent('CustomEvent') 51 | event.initCustomEvent(eventName, true, true, 0) 52 | } 53 | 54 | // copy eventProperties into event 55 | if (eventProperties) { 56 | mergeInto(event, eventProperties) 57 | } 58 | 59 | return event 60 | } 61 | 62 | function createEvent(eventName, eventType, eventProperties) { 63 | try { 64 | return createModernEvent(eventName, eventType, eventProperties) 65 | } catch (error) { 66 | return createLegacyEvent(eventName, eventType, eventProperties) 67 | } 68 | } 69 | 70 | var EventFactory = { 71 | createEvent: function(eventName, eventProperties, dataTransfer) { 72 | var eventType = 'CustomEvent' 73 | 74 | if (eventName.match(/^mouse/)) { 75 | eventType = 'MouseEvent' 76 | } else if (eventName.match(/^(drag|drop)/)) { 77 | eventType = 'DragEvent' 78 | } 79 | 80 | if (dataTransferEvents.indexOf(eventName) > -1) { 81 | eventProperties.dataTransfer = dataTransfer || new DataTransfer() 82 | } 83 | 84 | var event = createEvent(eventName, eventType, eventProperties) 85 | 86 | return event 87 | } 88 | } 89 | 90 | module.exports = EventFactory 91 | -------------------------------------------------------------------------------- /packages/extension/src/common/drag_mock/index.js: -------------------------------------------------------------------------------- 1 | const DragDropAction = require('./DragDropAction') 2 | 3 | function call(instance, methodName, args) { 4 | return instance[methodName].apply(instance, args) 5 | } 6 | 7 | const dragMock = { 8 | dragStart(targetElement, eventProperties, configCallback) { 9 | return call(new DragDropAction(), 'dragStart', arguments) 10 | }, 11 | dragEnter(targetElement, eventProperties, configCallback) { 12 | return call(new DragDropAction(), 'dragEnter', arguments) 13 | }, 14 | dragOver(targetElement, eventProperties, configCallback) { 15 | return call(new DragDropAction(), 'dragOver', arguments) 16 | }, 17 | dragLeave(targetElement, eventProperties, configCallback) { 18 | return call(new DragDropAction(), 'dragLeave', arguments) 19 | }, 20 | drop(targetElement, eventProperties, configCallback) { 21 | return call(new DragDropAction(), 'drop', arguments) 22 | }, 23 | delay(targetElement, eventProperties, configCallback) { 24 | return call(new DragDropAction(), 'delay', arguments) 25 | }, 26 | 27 | // Just for unit testing: 28 | DataTransfer: require('./DataTransfer'), 29 | DragDropAction: require('./DragDropAction'), 30 | eventFactory: require('./eventFactory') 31 | } 32 | 33 | module.exports = dragMock 34 | -------------------------------------------------------------------------------- /packages/extension/src/common/ipc/ipc_cs.js: -------------------------------------------------------------------------------- 1 | import { csInit } from './ipc_bg_cs' 2 | 3 | const throwNotTop = () => { 4 | throw new Error( 5 | 'You are not a top window, not allowed to initialize/use csIpc' 6 | ) 7 | } 8 | 9 | // Note: csIpc is only available to top window 10 | const ipc = 11 | window.top === window 12 | ? csInit() 13 | : { 14 | ask: throwNotTop, 15 | send: throwNotTop, 16 | onAsk: throwNotTop, 17 | destroy: throwNotTop 18 | } 19 | 20 | // Note: one ipc singleton per content script 21 | export default ipc 22 | -------------------------------------------------------------------------------- /packages/extension/src/common/log.js: -------------------------------------------------------------------------------- 1 | export const logFactory = enabled => { 2 | let isEnabled = !!enabled 3 | 4 | const obj = ['log', 'info', 'warn', 'error'].reduce((prev, method) => { 5 | prev[method] = (...args) => { 6 | if (!isEnabled) return 7 | console[method](new Date().toISOString(), ' - ', ...args) 8 | } 9 | return prev 10 | }, {}) 11 | 12 | return Object.assign(obj.log, obj, { 13 | enable: () => { 14 | isEnabled = true 15 | }, 16 | disable: () => { 17 | isEnabled = false 18 | } 19 | }) 20 | } 21 | 22 | export default logFactory(process.env.NODE_ENV !== 'production') 23 | -------------------------------------------------------------------------------- /packages/extension/src/common/network.js: -------------------------------------------------------------------------------- 1 | // URL regexes to rewrite bypass CORB for 2 | const CORBWhitelist = [] 3 | 4 | export const getHeaders = (url, headers) => { 5 | return CORBWhitelist.filter(hostRegex => url.match(hostRegex)).length > 0 6 | ? headers.map(item => 7 | item.name.toLowerCase() === 'Access-Control-Allow-Origin'.toLowerCase() 8 | ? { ...item, value: '*' } 9 | : item 10 | ) 11 | : headers 12 | } 13 | -------------------------------------------------------------------------------- /packages/extension/src/common/storage/__mocks__/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | get: () => {}, 3 | set: () => {} 4 | } 5 | -------------------------------------------------------------------------------- /packages/extension/src/common/storage/ext_storage.js: -------------------------------------------------------------------------------- 1 | import Ext from '../web_extension' 2 | import localStorageAdapter from './localstorage_adapter' 3 | 4 | const local = Ext.storage.local.get ? Ext.storage.local : localStorageAdapter 5 | 6 | export default { 7 | get: key => { 8 | return local.get(key).then(obj => { 9 | console.log(`get ${key}`, obj) 10 | return obj[key] 11 | }) 12 | }, 13 | 14 | set: (key, value) => { 15 | console.log(`set ${key}`, value) 16 | return local.set({ [key]: value }).then(() => true) 17 | }, 18 | 19 | remove: key => { 20 | return local.remove(key).then(() => true) 21 | }, 22 | 23 | clear: () => { 24 | return local.clear().then(() => true) 25 | }, 26 | 27 | addListener: fn => { 28 | Ext.storage.onChanged.addListener((changes, areaName) => { 29 | const list = Object.keys(changes).map(key => ({ ...changes[key], key })) 30 | fn(list) 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/extension/src/common/storage/index.js: -------------------------------------------------------------------------------- 1 | import ExtStorage from './ext_storage' 2 | 3 | export default ExtStorage 4 | -------------------------------------------------------------------------------- /packages/extension/src/common/storage/localstorage_adapter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by nhuang on 12/13/17. 3 | */ 4 | const localStorageAdapter = { 5 | get(key) { 6 | const data = localStorage.getItem(key) 7 | const parsed = JSON.parse(data) 8 | return Promise.resolve({ [key]: parsed }) 9 | }, 10 | 11 | set(kvp) { 12 | Object.keys(kvp).forEach(key => { 13 | localStorage.setItem(key, JSON.stringify(kvp[key])) 14 | }) 15 | return Promise.resolve(true) 16 | }, 17 | 18 | remove(key) { 19 | localStorage.removeItem(key) 20 | return Promise.resolve(true) 21 | }, 22 | 23 | clear() { 24 | localStorage.clear() 25 | return Promise.resolve(true) 26 | }, 27 | 28 | addListener(fn) {} 29 | } 30 | 31 | export default localStorageAdapter 32 | -------------------------------------------------------------------------------- /packages/extension/src/components/Modals/DuplicateModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Modal, Input, message } from 'antd' 4 | 5 | class DuplicateModal extends React.Component { 6 | constructor(props) { 7 | super(props) 8 | this.state = { 9 | duplicateName: this.props.src ? `${this.props.src.name}_new` : '' 10 | } 11 | this.onChange = this.onChange.bind(this) 12 | this.onDuplicate = this.onDuplicate.bind(this) 13 | this.onCancel = this.onCancel.bind(this) 14 | } 15 | 16 | componentWillReceiveProps(nextProps) { 17 | this.setState({ 18 | duplicateName: nextProps.src ? `${nextProps.src.name}_new` : '' 19 | }) 20 | } 21 | 22 | onChange(e) { 23 | this.setState({ 24 | duplicateName: e.target.value 25 | }) 26 | } 27 | 28 | onDuplicate() { 29 | this.props 30 | .duplicate(this.state.duplicateName) 31 | .then(() => { 32 | message.success('successfully duplicated!', 1.5) 33 | this.props.closeModal() 34 | }) 35 | .catch(e => { 36 | message.error(e.message, 1.5) 37 | }) 38 | } 39 | 40 | onCancel() { 41 | this.props.closeModal() 42 | this.setState({ 43 | duplicateName: '' 44 | }) 45 | } 46 | 47 | render() { 48 | return ( 49 | 58 | { 61 | if (e.keyCode === 13) this.onDuplicate() 62 | }} 63 | onChange={this.onChange} 64 | placeholder={`${this.props.type} name`} 65 | value={this.state.duplicateName} 66 | /> 67 | 68 | ) 69 | } 70 | } 71 | 72 | DuplicateModal.propTypes = { 73 | src: PropTypes.object.isRequired, 74 | duplicate: PropTypes.func.isRequired, 75 | closeModal: PropTypes.func.isRequired, 76 | visible: PropTypes.bool.isRequired, 77 | type: PropTypes.string.isRequired 78 | } 79 | 80 | export default DuplicateModal 81 | -------------------------------------------------------------------------------- /packages/extension/src/components/Sidebar/Block.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Block = props => { 5 | const disabled = props.disabled ? 'disabled' : '' 6 | const status = props.status ? props.status.toLowerCase() : 'normal' 7 | return {props.name} 8 | } 9 | 10 | Block.propTypes = { 11 | name: PropTypes.string, 12 | disabled: PropTypes.bool, 13 | status: PropTypes.string 14 | } 15 | 16 | export default Block 17 | -------------------------------------------------------------------------------- /packages/extension/src/components/Sidebar/Suite.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Suite = props => { 4 | return ( 5 | 6 | {props.name} 7 | 8 | ) 9 | } 10 | 11 | export default Suite 12 | -------------------------------------------------------------------------------- /packages/extension/src/components/Sidebar/Testcase.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Testcase = props => { 4 | return ( 5 | 9 | {props.name} 10 | 11 | ) 12 | } 13 | 14 | export default Testcase 15 | -------------------------------------------------------------------------------- /packages/extension/src/components/dashboard/CommandDoc.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Table, Input, Icon } from 'antd' 3 | import allCommands from '../../common/commands' 4 | 5 | // export default class CommandDoc extends React.Component { 6 | const CommandDoc = props => { 7 | const [searchText, setSearchText] = useState('') 8 | const availableCommands = allCommands.slice().sort((a, b) => { 9 | if (a.name.toLowerCase() === b.name.toLowerCase()) { 10 | return 0 11 | } else if (a.name.toLowerCase() > b.name.toLowerCase()) { 12 | return 1 13 | } else { 14 | return -1 15 | } 16 | }) 17 | 18 | const columns = [ 19 | { title: 'Cmd', dataIndex: 'name', key: 'name', width: 140 }, 20 | { title: 'Description', dataIndex: 'description', key: 'description' }, 21 | { title: 'Comment', dataIndex: 'comment', key: 'comment', width: 140 } 22 | ] 23 | 24 | const addOn = ( 25 | setSearchText('')} 29 | /> 30 | ) 31 | 32 | return ( 33 |
34 |
35 | setSearchText(e.target.value)} 40 | style={{ width: 180 }} 41 | addonAfter={addOn} 42 | /> 43 |
44 | 46 | e.name.toLowerCase().includes(searchText) 47 | )} 48 | pagination={false} 49 | columns={columns} 50 | bordered 51 | size="small" 52 | /> 53 | 54 | ) 55 | } 56 | 57 | CommandDoc.propTypes = {} 58 | 59 | export default CommandDoc 60 | -------------------------------------------------------------------------------- /packages/extension/src/components/dashboard/PauseButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Icon } from 'antd' 3 | import { getPlayer } from '../../common/player' 4 | 5 | const PauseButton = props => { 6 | const pause = () => { 7 | getPlayer().pause() 8 | } 9 | return ( 10 | 17 | ) 18 | } 19 | 20 | export default PauseButton 21 | -------------------------------------------------------------------------------- /packages/extension/src/components/dashboard/PlayButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Button, Icon, Tooltip } from 'antd' 4 | import { getPlayer } from '../../common/player' 5 | 6 | const PlayButton = props => { 7 | const play = () => { 8 | const { commands } = props.editing 9 | const { src } = props.editing.meta 10 | const openTc = commands.find(tc => tc.command.toLowerCase() === 'open') 11 | props.removeSearch() 12 | props.playerPlay({ 13 | title: src && src.name && src.name.length ? src.name : 'Untitled', 14 | extra: { 15 | id: src && src.id 16 | }, 17 | mode: getPlayer().C.MODE.STRAIGHT, 18 | startIndex: 0, 19 | startUrl: openTc && openTc.parameters ? openTc.parameters.url : null, 20 | resources: commands, 21 | postDelay: props.config.playCommandInterval * 1000 22 | }) 23 | } 24 | return ( 25 | 26 | 38 | 39 | ) 40 | } 41 | 42 | PlayButton.propTypes = { 43 | editing: PropTypes.object.isRequired, 44 | removeSearch: PropTypes.func.isRequired, 45 | playerPlay: PropTypes.func.isRequired, 46 | config: PropTypes.object.isRequired 47 | } 48 | 49 | export default PlayButton 50 | -------------------------------------------------------------------------------- /packages/extension/src/components/dashboard/PlayMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Menu } from 'antd' 4 | import * as C from '../../common/constant' 5 | 6 | const PlayMenu = props => { 7 | return ( 8 | props.togglePlayLoopsModal(true)} selectable={false}> 9 | 13 | Play loop.. 14 | 15 | 16 | ) 17 | } 18 | 19 | PlayMenu.propTypes = { 20 | togglePlayLoopsModal: PropTypes.func.isRequired, 21 | status: PropTypes.string 22 | } 23 | 24 | export default PlayMenu 25 | -------------------------------------------------------------------------------- /packages/extension/src/components/dashboard/PlaybackControl.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import * as C from '../../common/constant' 4 | import PauseButton from './PauseButton' 5 | import ResumeButton from './ResumeButton' 6 | import StopButton from './StopButton' 7 | import PlayButton from '../../containers/dashboard/PlayButton' 8 | 9 | const PlaybackControl = props => { 10 | const playStopButton = 11 | props.status === C.PLAYER_STATUS.STOPPED ? ( 12 | 13 | ) : ( 14 | 15 | ) 16 | if (props.status === C.PLAYER_STATUS.PLAYING) { 17 | return ( 18 | 19 | {playStopButton} 20 | 21 | 22 | ) 23 | } else if (props.status === C.PLAYER_STATUS.PAUSED) { 24 | return ( 25 | 26 | {playStopButton} 27 | 28 | 29 | ) 30 | } else { 31 | return {playStopButton} 32 | } 33 | } 34 | 35 | PlaybackControl.propTypes = { 36 | status: PropTypes.string 37 | } 38 | 39 | export default PlaybackControl 40 | -------------------------------------------------------------------------------- /packages/extension/src/components/dashboard/ResumeButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Icon, Tooltip } from 'antd' 3 | import { getPlayer } from '../../common/player' 4 | 5 | const ResumeButton = props => { 6 | const resume = () => { 7 | getPlayer().resume() 8 | } 9 | return ( 10 | 11 | 18 | 19 | ) 20 | } 21 | 22 | export default ResumeButton 23 | -------------------------------------------------------------------------------- /packages/extension/src/components/dashboard/StopButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Button, Icon, Tooltip } from 'antd' 4 | import { getPlayer } from '../../common/player' 5 | 6 | const StopButton = props => { 7 | return ( 8 | 9 | 17 | 18 | ) 19 | } 20 | 21 | StopButton.propTypes = { 22 | stopped: PropTypes.bool 23 | } 24 | 25 | export default StopButton 26 | -------------------------------------------------------------------------------- /packages/extension/src/components/dashboard/SuiteEditor.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Table, Button } from 'antd' 4 | 5 | const { Column } = Table 6 | 7 | class SuiteEditor extends React.Component { 8 | render() { 9 | const unusedTests = 10 | this.props.tests && 11 | this.props.tests.filter(t => !this.props.selectedTests.includes(t.name)) 12 | const selectedTests = 13 | this.props.selectedTests && 14 | this.props.selectedTests.map((t, i) => ({ name: t, key: i })) 15 | return ( 16 |
17 |
18 | { 22 | return ( 23 |
24 | {record.name} 25 | {record.data.commands.length} commands 26 |
31 | ) 32 | }} 33 | /> 34 |
35 | 36 | { 40 | return ( 41 |
42 | {record.name} 43 |
52 | ) 53 | }} 54 | /> 55 |
56 |
57 | ) 58 | } 59 | } 60 | 61 | SuiteEditor.propTypes = { 62 | tests: PropTypes.array.isRequired, 63 | selectedTests: PropTypes.array.isRequired, 64 | addTestToSuite: PropTypes.func.isRequired, 65 | name: PropTypes.string.isRequired, 66 | removeTestFromSuite: PropTypes.func.isRequired 67 | } 68 | 69 | export default SuiteEditor 70 | -------------------------------------------------------------------------------- /packages/extension/src/components/dashboard/fields/CommandField.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import EnvironmentField from './EnvironmentField' 4 | import OneField from './OneField' 5 | 6 | const CommandField = props => { 7 | switch (props.selectedCmd && props.selectedCmd.command) { 8 | case 'setEnvironment': 9 | return 10 | default: 11 | return 12 | } 13 | } 14 | 15 | CommandField.propTypes = { 16 | selectedCmd: PropTypes.object.isRequired 17 | } 18 | 19 | export default CommandField 20 | -------------------------------------------------------------------------------- /packages/extension/src/components/dashboard/fields/EnvironmentField.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Input, Form } from 'antd' 4 | 5 | class EnvironmentField extends React.Component { 6 | constructor(props) { 7 | super(props) 8 | this.state = { 9 | rawValue: this.getValue(this.props.selectedCmd.parameters) 10 | } 11 | } 12 | 13 | getValue(obj) { 14 | return Object.keys(obj) 15 | .map(k => `${k}=${obj[k]}`) 16 | .join(',') 17 | } 18 | 19 | updateParameters = (e) => { 20 | this.setState({ rawValue: e.target.value }, () => { 21 | if (this.state.rawValue.includes('=')) { 22 | const pairs = this.state.rawValue.split(',').map(split => { 23 | if (split.indexOf('=') !== -1) { 24 | const pair = split.split('=') 25 | return { [pair[0]]: pair[1] } 26 | } 27 | }) 28 | if (pairs.length === pairs.filter(i => i !== undefined).length) { 29 | this.props.updateSelectedCommand( 30 | { 31 | parameters: Object.assign({}, ...pairs) 32 | }, 33 | true 34 | ) 35 | } 36 | } else if (this.state.rawValue === '') { 37 | this.props.updateSelectedCommand({ parameters: {} }, true) 38 | } 39 | }) 40 | } 41 | 42 | render() { 43 | return ( 44 |
45 | 51 | 58 | 59 |
60 | ) 61 | } 62 | } 63 | 64 | EnvironmentField.propTypes = { 65 | updateSelectedCommand: PropTypes.func.isRequired, 66 | selectedCmd: PropTypes.object.isRequired, 67 | isCmdEditable: PropTypes.bool.isRequired 68 | } 69 | 70 | export default EnvironmentField 71 | -------------------------------------------------------------------------------- /packages/extension/src/config/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | urlAfterUpgrade: 'https://github.com/intuit/replayweb', 3 | urlAfterInstall: 'https://github.com/intuit/replayweb', 4 | urlAfterUninstall: 'https://github.com/intuit/replayweb', 5 | urlHomePage: 'https://github.com/intuit/replayweb', 6 | urlReleases: 'https://github.com/intuit/replayweb', 7 | productName: 'ReplayWeb', 8 | hostVersion: '1.0' 9 | } 10 | -------------------------------------------------------------------------------- /packages/extension/src/containers/Header.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { 3 | editNewTestCase, 4 | saveEditingAsExisted, 5 | saveEditingBlockAsExisted, 6 | saveEditingSuiteAsExisted, 7 | changeModalState, 8 | stopRecording, 9 | startRecording, 10 | normalizeCommands, 11 | editNewBlock, 12 | editNewSuite, 13 | logMessage 14 | } from '../actions' 15 | import Header from '../components/Header' 16 | import * as C from '../common/constant' 17 | 18 | const mapStateToProps = state => { 19 | return { 20 | status: state.app.status, 21 | editing: state.editor.editing, 22 | editorType: state.editor.status, 23 | testCases: state.editor.testCases, 24 | player: state.player, 25 | config: state.app.config, 26 | project: state.editor.project, 27 | activeFolder: state.files.activeFolder 28 | } 29 | } 30 | 31 | const mapDispatchToProps = dispatch => { 32 | return { 33 | editNew: type => { 34 | switch (type) { 35 | case C.EDITOR_STATUS.TESTS: 36 | return dispatch(editNewTestCase()) 37 | case C.EDITOR_STATUS.BLOCKS: 38 | return dispatch(editNewBlock()) 39 | case C.EDITOR_STATUS.SUITES: 40 | return dispatch(editNewSuite()) 41 | } 42 | }, 43 | saveEditingAsExisted: type => { 44 | switch (type) { 45 | case C.EDITOR_STATUS.TESTS: 46 | return dispatch(saveEditingAsExisted()) 47 | case C.EDITOR_STATUS.BLOCKS: 48 | return dispatch(saveEditingBlockAsExisted()) 49 | case C.EDITOR_STATUS.SUITES: 50 | return dispatch(saveEditingSuiteAsExisted()) 51 | } 52 | }, 53 | changeModalState: (id, state) => dispatch(changeModalState(id, state)), 54 | stopRecording: () => dispatch(stopRecording()), 55 | normalizeCommands: () => dispatch(normalizeCommands()), 56 | startRecording: () => dispatch(startRecording()), 57 | logMessage: message => dispatch(logMessage(message)) 58 | } 59 | } 60 | 61 | export default connect( 62 | mapStateToProps, 63 | mapDispatchToProps 64 | )(Header) 65 | -------------------------------------------------------------------------------- /packages/extension/src/containers/Modals/DuplicateModal.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { changeModalState, duplicate } from '../../actions' 3 | import * as C from '../../common/constant' 4 | import DuplicateModal from '../../components/Modals/DuplicateModal' 5 | const mapStateToProps = state => { 6 | return { 7 | visible: state.modals.duplicate, 8 | src: state.editor.editing.meta.src, 9 | tests: state.editor.status === C.EDITOR_STATUS.TESTS, 10 | type: state.editor.status 11 | .toLowerCase() 12 | .slice(0, state.editor.status.length - 1) 13 | } 14 | } 15 | 16 | const mapDispatchToProps = dispatch => { 17 | return { 18 | duplicate: name => dispatch(duplicate(name)), 19 | closeModal: () => dispatch(changeModalState('duplicate', false)) 20 | } 21 | } 22 | 23 | export default connect( 24 | mapStateToProps, 25 | mapDispatchToProps 26 | )(DuplicateModal) 27 | -------------------------------------------------------------------------------- /packages/extension/src/containers/Modals/NewBlockModal.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { 3 | changeModalState, 4 | setEditorStatus, 5 | selectProject, 6 | editNext, 7 | editNewBlock, 8 | clearNext, 9 | importBlockFromLink 10 | } from '../../actions' 11 | import NewBlockModal from '../../components/Modals/NewBlockModal' 12 | import * as C from '../../common/constant' 13 | const mapStateToProps = state => { 14 | return { 15 | visible: state.modals.newBlockModal, 16 | src: state.editor.editing.meta.src, 17 | project: state.editor.project, 18 | editorStatus: state.editor.status, 19 | blockShareConfig: state.app.blockShareConfig, 20 | blockShareConfigError: state.app.blockShareConfigError, 21 | blockShareConfigErrorMessage: state.app.blockShareConfigErrorMessage 22 | } 23 | } 24 | 25 | const mapDispatchToProps = dispatch => { 26 | return { 27 | editNext: () => dispatch(editNext()), 28 | clearNext: () => dispatch(clearNext()), 29 | closeModal: () => dispatch(changeModalState('newBlockModal', false)), 30 | selectProject: project => dispatch(selectProject(project)), 31 | editNewBlock: () => { 32 | dispatch(setEditorStatus(C.EDITOR_STATUS.BLOCKS)) 33 | dispatch(editNewBlock()) 34 | dispatch(changeModalState('newBlockModal', false)) 35 | }, 36 | importBlock: link => { 37 | return dispatch(importBlockFromLink(link)) 38 | } 39 | } 40 | } 41 | 42 | export default connect( 43 | mapStateToProps, 44 | mapDispatchToProps 45 | )(NewBlockModal) 46 | -------------------------------------------------------------------------------- /packages/extension/src/containers/Modals/PlayLoopModal.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { changeModalState, playerPlay } from '../../actions' 3 | import PlayLoopModal from '../../components/Modals/PlayLoopModal' 4 | const mapStateToProps = state => { 5 | return { 6 | visible: state.modals.playLoop, 7 | status: state.player.status, 8 | editing: state.editor.editing, 9 | config: state.app.config 10 | } 11 | } 12 | 13 | const mapDispatchToProps = dispatch => { 14 | return { 15 | togglePlayLoopsModal: state => 16 | dispatch(changeModalState('playLoop', state)), 17 | playerPlay: options => dispatch(playerPlay(options)) 18 | } 19 | } 20 | 21 | export default connect( 22 | mapStateToProps, 23 | mapDispatchToProps 24 | )(PlayLoopModal) 25 | -------------------------------------------------------------------------------- /packages/extension/src/containers/Modals/RenameModal.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { 3 | changeModalState, 4 | renameTestCase, 5 | renameBlock, 6 | renameSuite 7 | } from '../../actions' 8 | import RenameModal from '../../components/Modals/RenameModal' 9 | import * as C from '../../common/constant' 10 | const mapStateToProps = state => { 11 | return { 12 | visible: state.modals.rename, 13 | src: state.editor.editing.meta.src, 14 | editorStatus: state.editor.status, 15 | isTestCase: state.editor.status === C.EDITOR_STATUS.TESTS 16 | } 17 | } 18 | 19 | const mapDispatchToProps = dispatch => { 20 | return { 21 | renameSuite: name => dispatch(renameSuite(name)), 22 | renameTestCase: name => dispatch(renameTestCase(name)), 23 | renameBlock: name => dispatch(renameBlock(name)), 24 | closeModal: () => dispatch(changeModalState('rename', false)) 25 | } 26 | } 27 | 28 | export default connect( 29 | mapStateToProps, 30 | mapDispatchToProps 31 | )(RenameModal) 32 | -------------------------------------------------------------------------------- /packages/extension/src/containers/Modals/SaveModal.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { 3 | changeModalState, 4 | saveEditingAsExisted, 5 | saveEditingSuiteAsExisted, 6 | saveEditingBlockAsExisted, 7 | saveEditingBlockAsNew, 8 | saveEditingAsNew, 9 | saveEditingSuiteAsNew, 10 | selectProject, 11 | editNext, 12 | clearNext 13 | } from '../../actions' 14 | import SaveModal from '../../components/Modals/SaveModal' 15 | const mapStateToProps = state => { 16 | return { 17 | visible: state.modals.save, 18 | src: state.editor.editing.meta.src, 19 | project: state.editor.project, 20 | editorStatus: state.editor.status, 21 | newSave: state.editor.editing.meta.isNewSave 22 | } 23 | } 24 | 25 | const mapDispatchToProps = dispatch => { 26 | return { 27 | editNext: () => dispatch(editNext()), 28 | clearNext: () => dispatch(clearNext()), 29 | saveEditingAsExisted: () => dispatch(saveEditingAsExisted()), 30 | saveEditingAsNew: name => dispatch(saveEditingAsNew(name)), 31 | saveEditingSuiteAsExisted: () => dispatch(saveEditingSuiteAsExisted()), 32 | saveEditingSuiteAsNew: name => dispatch(saveEditingSuiteAsNew(name)), 33 | saveEditingBlockAsExisted: () => dispatch(saveEditingBlockAsExisted()), 34 | saveEditingBlockAsNew: name => dispatch(saveEditingBlockAsNew(name)), 35 | closeModal: () => dispatch(changeModalState('save', false)), 36 | selectProject: project => dispatch(selectProject(project)) 37 | } 38 | } 39 | 40 | export default connect( 41 | mapStateToProps, 42 | mapDispatchToProps 43 | )(SaveModal) 44 | -------------------------------------------------------------------------------- /packages/extension/src/containers/Modals/SaveMultiSelectModal.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { changeModalState, saveMultiSelectAsNewBlock } from '../../actions' 3 | import { SaveMultiSelectModal } from '../../components/Modals/SaveModal' 4 | const mapStateToProps = state => { 5 | return { 6 | visible: state.modals.multiselect 7 | } 8 | } 9 | const mapDispatchToProps = dispatch => { 10 | return { 11 | saveMultiSelectAsNewBlock: name => 12 | dispatch(saveMultiSelectAsNewBlock(name)), 13 | closeModal: () => dispatch(changeModalState('multiselect', false)) 14 | } 15 | } 16 | 17 | export default connect( 18 | mapStateToProps, 19 | mapDispatchToProps 20 | )(SaveMultiSelectModal) 21 | -------------------------------------------------------------------------------- /packages/extension/src/containers/Modals/SettingModal.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { updateConfig, changeModalState } from '../../actions' 3 | import SettingModal from '../../components/Modals/SettingModal' 4 | const mapStateToProps = state => { 5 | return { 6 | visible: state.modals.settings, 7 | config: state.app.config 8 | } 9 | } 10 | 11 | const mapDispatchToProps = dispatch => { 12 | return { 13 | updateConfig: data => dispatch(updateConfig(data)), 14 | onCancel: () => dispatch(changeModalState('settings', false)) 15 | } 16 | } 17 | 18 | export default connect( 19 | mapStateToProps, 20 | mapDispatchToProps 21 | )(SettingModal) 22 | -------------------------------------------------------------------------------- /packages/extension/src/containers/Modals/ShareBlockModal.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { changeModalState, shareEditingBlock } from '../../actions' 3 | import ShareBlockModal from '../../components/Modals/ShareBlockModal' 4 | 5 | const mapStateToProps = state => { 6 | return { 7 | visible: state.modals.shareBlock, 8 | editing: state.editor.editing, 9 | blockShareConfig: state.app.blockShareConfig, 10 | blockShareConfigError: state.app.blockShareConfigError, 11 | blockShareConfigErrorMessage: state.app.blockShareConfigErrorMessage 12 | } 13 | } 14 | 15 | const mapDispatchToProps = dispatch => { 16 | return { 17 | closeModal: () => dispatch(changeModalState('shareBlock', false)), 18 | openDuplicate: () => dispatch(changeModalState('duplicate', true)), 19 | shareEditingBlock: () => dispatch(shareEditingBlock()) 20 | } 21 | } 22 | 23 | export default connect( 24 | mapStateToProps, 25 | mapDispatchToProps 26 | )(ShareBlockModal) 27 | -------------------------------------------------------------------------------- /packages/extension/src/containers/Project/FolderBrowser.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { 3 | listDirectory, 4 | changeModalState, 5 | selectProjectFolder 6 | } from '../../actions' 7 | import FolderBrowser from '../../components/Project/FolderBrowser.jsx' 8 | 9 | const mapStateToProps = state => { 10 | return { 11 | folder: state.files.folder, 12 | error: state.files.error, 13 | contents: state.files.contents, 14 | visible: state.modals.browser, 15 | top: state.projectSetup.projectPath 16 | ? state.files.folder === state.projectSetup.projectPath 17 | : state.files.folder === '~' 18 | } 19 | } 20 | 21 | const mapDispatchToProps = dispatch => { 22 | return { 23 | listDirectory: dir => dispatch(listDirectory(dir)), 24 | closeModal: () => dispatch(changeModalState('browser', false)), 25 | selectFolder: () => dispatch(selectProjectFolder()) 26 | } 27 | } 28 | 29 | export default connect( 30 | mapStateToProps, 31 | mapDispatchToProps 32 | )(FolderBrowser) 33 | -------------------------------------------------------------------------------- /packages/extension/src/containers/Project/ProjectModal.js: -------------------------------------------------------------------------------- 1 | import { types as T } from '../../actions/action_types' 2 | import { connect } from 'react-redux' 3 | import { 4 | updateProject, 5 | clearProjectSetup, 6 | changeModalState, 7 | setPathPurpose, 8 | createProject, 9 | checkForExistingConfig 10 | } from '../../actions' 11 | import ProjectModal from '../../components/Project/ProjectModal' 12 | 13 | const mapStateToProps = state => { 14 | return { 15 | visible: state.modals.projectSetup, 16 | testPath: state.projectSetup.testPath, 17 | blockPath: state.projectSetup.blockPath, 18 | suites: state.projectSetup.suites, 19 | projectPath: state.projectSetup.projectPath, 20 | existingConfig: state.projectSetup.existingConfig, 21 | firstTime: state.editor.projects.length === 0, 22 | id: state.projectSetup.id, 23 | name: state.projectSetup.name 24 | } 25 | } 26 | 27 | const mapDispatchToProps = dispatch => { 28 | return { 29 | createProject: project => dispatch(createProject(project)), 30 | updateProject: project => dispatch(updateProject(project)), 31 | clearProjectSetup: () => dispatch(clearProjectSetup()), 32 | closeModal: () => dispatch(changeModalState('projectSetup', false)), 33 | checkForExistingConfig: path => { 34 | dispatch({ 35 | type: T.PROJECT_PATH, 36 | path 37 | }) 38 | dispatch(checkForExistingConfig(path)) 39 | }, 40 | browseProject: () => { 41 | dispatch(setPathPurpose(T.PROJECT_PATH)) 42 | dispatch(changeModalState('browser', true)) 43 | }, 44 | browseTest: () => { 45 | dispatch(setPathPurpose(T.TEST_PATH)) 46 | dispatch(changeModalState('browser', true)) 47 | }, 48 | browseBlock: () => { 49 | dispatch(setPathPurpose(T.BLOCK_PATH)) 50 | dispatch(changeModalState('browser', true)) 51 | } 52 | } 53 | } 54 | 55 | export default connect( 56 | mapStateToProps, 57 | mapDispatchToProps 58 | )(ProjectModal) 59 | -------------------------------------------------------------------------------- /packages/extension/src/containers/Sidebar.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { 3 | editTestCase, 4 | editBlock, 5 | editSuite, 6 | playerPlay, 7 | editProject, 8 | selectProject, 9 | removeProject, 10 | setNextTest, 11 | setNextBlock, 12 | setNextSuite, 13 | changeModalState, 14 | setEditorStatus, 15 | editNewSuite, 16 | editNewTestCase, 17 | editNextTest, 18 | editNextBlock, 19 | editNextSuite 20 | } from '../actions' 21 | import * as C from '../common/constant' 22 | import Sidebar from '../components/Sidebar/Sidebar' 23 | const mapStateToProps = state => { 24 | return { 25 | status: state.app.status, 26 | testCases: state.editor.testCases, 27 | blocks: state.editor.blocks, 28 | suites: state.editor.suites, 29 | editing: state.editor.editing, 30 | player: state.player, 31 | config: state.app.config, 32 | project: state.editor.project || {}, 33 | projects: state.editor.projects || [], 34 | editorStatus: state.editor.status 35 | } 36 | } 37 | 38 | const mapDispatchToProps = dispatch => { 39 | return { 40 | editTestCase: id => dispatch(editTestCase(id)), 41 | editSuite: id => dispatch(editSuite(id)), 42 | editProject: id => dispatch(editProject(id)), 43 | editNextTest: () => dispatch(editNextTest()), 44 | editNextBlock: () => dispatch(editNextBlock()), 45 | editNextSuite: () => dispatch(editNextSuite()), 46 | editNewTestCase: () => { 47 | dispatch(setEditorStatus(C.EDITOR_STATUS.TESTS)) 48 | dispatch(editNewTestCase()) 49 | }, 50 | 51 | editNewSuite: () => { 52 | dispatch(setEditorStatus(C.EDITOR_STATUS.SUITES)) 53 | dispatch(editNewSuite()) 54 | }, 55 | setNextTest: id => dispatch(setNextTest(id)), 56 | setNextBlock: id => dispatch(setNextBlock(id)), 57 | setNextSuite: id => dispatch(setNextSuite(id)), 58 | editBlock: id => dispatch(editBlock(id)), 59 | playerPlay: options => dispatch(playerPlay(options)), 60 | changeModalState: (modal, modalState) => 61 | dispatch(changeModalState(modal, modalState)), 62 | selectProject: project => dispatch(selectProject(project)), 63 | removeProject: project => dispatch(removeProject(project)), 64 | setEditorStatus: status => dispatch(setEditorStatus(status)) 65 | } 66 | } 67 | 68 | export default connect( 69 | mapStateToProps, 70 | mapDispatchToProps 71 | )(Sidebar) 72 | -------------------------------------------------------------------------------- /packages/extension/src/containers/dashboard/CommandOptions.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { bindActionCreators } from 'redux' 3 | import * as actions from '../../actions' 4 | 5 | import CommandOptions from '../../components/dashboard/CommandOptions' 6 | 7 | export default connect( 8 | state => ({ 9 | status: state.app.status, 10 | editor: state.editor, 11 | editing: state.editor.editing, 12 | clipboard: state.editor.clipboard, 13 | player: state.player, 14 | config: state.app.config, 15 | selectedCmds: state.editor.selectedCmds, 16 | selectedCommand: state.editor.editing.meta.selectedIndex 17 | }), 18 | dispatch => bindActionCreators({ ...actions }, dispatch) 19 | )(CommandOptions) 20 | -------------------------------------------------------------------------------- /packages/extension/src/containers/dashboard/CommandTable.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { 3 | selectCommand, 4 | removeCommand, 5 | insertCommand, 6 | reorderCommand, 7 | filterCommands, 8 | multiSelect, 9 | removeSelected, 10 | groupSelect, 11 | showContextMenu 12 | } from '../../actions' 13 | import CommandTable from '../../components/dashboard/CommandTable' 14 | 15 | const mapStateToProps = state => { 16 | return { 17 | editing: state.editor.editing, 18 | player: state.player, 19 | editor: state.editor, 20 | filterCommands: state.editor.editing, 21 | searchText: state.editor.searchText, 22 | selectedCommand: state.editor.editing.meta.selectedIndex 23 | } 24 | } 25 | 26 | const mapDispatchToProps = dispatch => { 27 | return { 28 | selectCommand: index => dispatch(selectCommand(index)), 29 | removeCommand: index => dispatch(removeCommand(index)), 30 | insertCommand: (command, index) => dispatch(insertCommand(command, index)), 31 | reorderCommand: (dragIndex, hoverIndex) => 32 | dispatch(reorderCommand(dragIndex, hoverIndex)), 33 | filterCommands: searchText => dispatch(filterCommands(searchText)), 34 | removeSearchText: () => dispatch(filterCommands('')), 35 | multiSelect: index => dispatch(multiSelect(index)), 36 | groupSelect: index => dispatch(groupSelect(index)), 37 | removeSelected: () => dispatch(removeSelected()), 38 | showContextMenu: data => dispatch(showContextMenu(data)) 39 | } 40 | } 41 | 42 | export default connect( 43 | mapStateToProps, 44 | mapDispatchToProps 45 | )(CommandTable) 46 | -------------------------------------------------------------------------------- /packages/extension/src/containers/dashboard/DashboardBottom.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { bindActionCreators } from 'redux' 3 | import * as actions from '../../actions' 4 | import DashboardBottom from '../../components/dashboard/DashboardBottom' 5 | 6 | const mapStateToProps = state => { 7 | return { 8 | status: state.app.status, 9 | logs: state.app.logs, 10 | screenshots: state.app.screenshots 11 | } 12 | } 13 | 14 | export default connect( 15 | mapStateToProps, 16 | dispatch => bindActionCreators({ ...actions }, dispatch) 17 | )(DashboardBottom) 18 | -------------------------------------------------------------------------------- /packages/extension/src/containers/dashboard/DashboardEditor.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { bindActionCreators } from 'redux' 3 | import * as actions from '../../actions' 4 | 5 | import DashboardEditor from '../../components/dashboard/DashboardEditor' 6 | 7 | export default connect( 8 | state => ({ 9 | status: state.app.status, 10 | editorStatus: state.editor.status, 11 | editing: state.editor.editing, 12 | clipboard: state.editor.clipboard, 13 | player: state.player, 14 | config: state.app.config, 15 | searchText: state.editor.searchText, 16 | selectedCmds: state.editor.selectedCmds, 17 | contextMenu: state.editor.contextMenu 18 | }), 19 | dispatch => bindActionCreators({ ...actions }, dispatch) 20 | )(DashboardEditor) 21 | -------------------------------------------------------------------------------- /packages/extension/src/containers/dashboard/PlayButton.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { playerPlay, filterCommands } from '../../actions' 3 | import PlayButton from '../../components/dashboard/PlayButton' 4 | const mapStateToProps = state => { 5 | return { 6 | editing: state.editor.editing, 7 | config: state.app.config, 8 | filterCommands: state.editor.editing 9 | } 10 | } 11 | 12 | const mapDispatchToProps = dispatch => { 13 | return { 14 | playerPlay: options => dispatch(playerPlay(options)), 15 | removeSearch: () => dispatch(filterCommands('')) 16 | } 17 | } 18 | 19 | export default connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(PlayButton) 23 | -------------------------------------------------------------------------------- /packages/extension/src/containers/dashboard/PlayMenu.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { changeModalState } from '../../actions' 3 | import PlayMenu from '../../components/dashboard/PlayMenu' 4 | const mapStateToProps = state => { 5 | return { 6 | visible: state.modals.playLoop, 7 | status: state.player.status 8 | } 9 | } 10 | 11 | const mapDispatchToProps = dispatch => { 12 | return { 13 | togglePlayLoopsModal: state => dispatch(changeModalState('playLoop', state)) 14 | } 15 | } 16 | 17 | export default connect( 18 | mapStateToProps, 19 | mapDispatchToProps 20 | )(PlayMenu) 21 | -------------------------------------------------------------------------------- /packages/extension/src/containers/dashboard/SuiteEditor.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { addTestToSuite, removeTestFromSuite } from '../../actions' 3 | 4 | import SuiteEditor from '../../components/dashboard/SuiteEditor' 5 | 6 | const mapStateToProps = state => { 7 | return { 8 | tests: state.editor.testCases, 9 | name: state.editor.editing.meta.src && state.editor.editing.meta.src.name, 10 | selectedTests: state.editor.editing.tests 11 | } 12 | } 13 | 14 | const mapDispatchToProps = dispatch => { 15 | return { 16 | addTestToSuite: test => dispatch(addTestToSuite(test)), 17 | removeTestFromSuite: test => dispatch(removeTestFromSuite(test)) 18 | } 19 | } 20 | 21 | export default connect( 22 | mapStateToProps, 23 | mapDispatchToProps 24 | )(SuiteEditor) 25 | -------------------------------------------------------------------------------- /packages/extension/src/containers/dashboard/TestcaseDropdown.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { 3 | changeModalState, 4 | changeDropdownState, 5 | removeCurrentTestCase, 6 | removeCurrentBlock, 7 | removeCurrentSuite 8 | } from '../../actions' 9 | import Dropdown from '../../components/dashboard/TestcaseDropdown' 10 | const mapStateToProps = state => { 11 | return { 12 | status: state.player.status, 13 | editing: state.editor.editing, 14 | editorStatus: state.editor.status 15 | } 16 | } 17 | 18 | const mapDispatchToProps = dispatch => { 19 | return { 20 | removeCurrentTestCase: () => dispatch(removeCurrentTestCase()), 21 | removeCurrentBlock: () => dispatch(removeCurrentBlock()), 22 | removeCurrentSuite: () => dispatch(removeCurrentSuite()), 23 | changeModalState: (id, state) => dispatch(changeModalState(id, state)), 24 | closeDropdown: () => dispatch(changeDropdownState('testcase', false)) 25 | } 26 | } 27 | 28 | export default connect( 29 | mapStateToProps, 30 | mapDispatchToProps 31 | )(Dropdown) 32 | -------------------------------------------------------------------------------- /packages/extension/src/containers/dashboard/fields/CommandButtons.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { updateSelectedCommand } from '../../../actions' 3 | import CommandButtons from '../../../components/dashboard/fields/CommandButtons' 4 | import { newCommand } from '../../../common/commands' 5 | 6 | const mapStateToProps = state => { 7 | const defaultDataSource = [newCommand] 8 | const index = state.editor.editing.meta.selectedIndex 9 | const selectedCmd = 10 | state.editor.editing.filterCommands && 11 | state.editor.editing.filterCommands.length 12 | ? state.editor.editing.commands[index] 13 | : defaultDataSource[index] 14 | return { 15 | selectedCmd: selectedCmd 16 | } 17 | } 18 | 19 | const mapDispatchToProps = dispatch => { 20 | return { 21 | updateSelectedCommand: obj => dispatch(updateSelectedCommand(obj)) 22 | } 23 | } 24 | 25 | export default connect( 26 | mapStateToProps, 27 | mapDispatchToProps 28 | )(CommandButtons) 29 | -------------------------------------------------------------------------------- /packages/extension/src/containers/dashboard/fields/CommandField.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import CommandField from '../../../components/dashboard/fields/CommandField' 3 | import { 4 | updateSelectedCommand, 5 | startInspecting, 6 | stopInspecting, 7 | setInspectTarget 8 | } from '../../../actions' 9 | import * as C from '../../../common/constant' 10 | import { newCommand } from '../../../common/commands' 11 | 12 | const mapStateToProps = state => { 13 | const defaultDataSource = [newCommand] 14 | const index = state.editor.editing.meta.selectedIndex 15 | const selectedCmd = 16 | state.editor.editing.filterCommands && 17 | state.editor.editing.filterCommands.length 18 | ? state.editor.editing.filterCommands[index] 19 | : defaultDataSource[index] 20 | return { 21 | selectedCmd: selectedCmd, 22 | commands: state.editor.editing.commands, 23 | isCmdEditable: 24 | state.player.status === C.PLAYER_STATUS.STOPPED && 25 | selectedCmd !== undefined, 26 | editing: state.editor.editing, 27 | filterCommands: state.editor.editing.filterCommands, 28 | blocks: state.editor.blocks, 29 | selectedIndex: index, 30 | isInspecting: state.app.status === C.APP_STATUS.INSPECTOR, 31 | inspectTarget: state.editor.inspectTarget 32 | } 33 | } 34 | 35 | const mapDispatchToProps = dispatch => { 36 | return { 37 | updateSelectedCommand: (obj, overwrite) => 38 | dispatch(updateSelectedCommand(obj, overwrite)), 39 | startInspecting: () => dispatch(startInspecting()), 40 | stopInspecting: () => dispatch(stopInspecting()), 41 | setInspectTarget: target => dispatch(setInspectTarget(target)) 42 | } 43 | } 44 | 45 | export default connect( 46 | mapStateToProps, 47 | mapDispatchToProps 48 | )(CommandField) 49 | -------------------------------------------------------------------------------- /packages/extension/src/containers/dashboard/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActionCreators } from 'redux' 4 | import { Checkbox } from 'antd' 5 | 6 | import '../../styles/dashboard.scss' 7 | import * as actions from '../../actions' 8 | 9 | import DashboardEditor from './DashboardEditor' 10 | import DashboardBottom from './DashboardBottom' 11 | import C from '../../config' 12 | 13 | class Dashboard extends React.Component { 14 | constructor(props) { 15 | super(props) 16 | this.state = { 17 | isChecked: true 18 | } 19 | this.toggleChange = this.toggleChange.bind(this) 20 | } 21 | 22 | toggleChange() { 23 | this.setState({ 24 | isChecked: !this.state.isChecked 25 | }) 26 | } 27 | 28 | render() { 29 | const viewLog = this.state.isChecked ? : null 30 | return ( 31 |
32 | 33 |
34 | 35 | Show Log 36 | 37 | {viewLog} 38 |
39 | 48 |
49 | ) 50 | } 51 | } 52 | 53 | export default connect( 54 | state => ({}), 55 | dispatch => bindActionCreators({ ...actions }, dispatch) 56 | )(Dashboard) 57 | -------------------------------------------------------------------------------- /packages/extension/src/ext/cssSelector.js: -------------------------------------------------------------------------------- 1 | const mapDOM = (element, json) => { 2 | var treeObject = {} 3 | 4 | // If string convert to document Node 5 | if (typeof element === 'string') { 6 | const parser = new DOMParser() 7 | const docNode = parser.parseFromString(element, 'text/xml') 8 | element = docNode.firstChild 9 | } 10 | 11 | // Recursively loop through DOM elements and assign properties to object 12 | const treeHTML = (element, object) => { 13 | object.data = {} 14 | object.data.nn = element.nodeName.toLowerCase() 15 | object.data.classValue = '' 16 | object.data.classList = [] 17 | object.data.attrs = {} 18 | var nodeList = element.childNodes 19 | if (nodeList != null) { 20 | if (nodeList.length) { 21 | object.children = [] 22 | for (let i = 0; i < nodeList.length; i++) { 23 | if (nodeList[i].nodeType === 3) { 24 | const nodeValue = nodeList[i].nodeValue.trim() 25 | if (nodeValue && object.data.nn !== 'script') 26 | object.text = nodeValue 27 | } else { 28 | object.children.push({}) 29 | treeHTML(nodeList[i], object.children[object.children.length - 1]) 30 | } 31 | } 32 | } 33 | } 34 | if (element.attributes != null) { 35 | if (element.attributes.length) { 36 | for (let i = 0; i < element.attributes.length; i++) { 37 | const attrName = element.attributes[i].nodeName 38 | const attrValue = element.attributes[i].nodeValue 39 | if (attrName === 'class') { 40 | object.data.classValue = attrValue 41 | object.data.classList = attrValue 42 | .split(' ') 43 | .filter(value => !!value.trim()) 44 | } else if (attrName === 'style') { 45 | continue 46 | } else { 47 | object.data.attrs[attrName] = attrValue 48 | } 49 | } 50 | } 51 | } 52 | 53 | let sibCount = 0 54 | let sibIndex = 0 55 | for (let i = 0; i < element.parentNode.childNodes.length; i++) { 56 | const sib = element.parentNode.childNodes[i] 57 | if (sib.nodeName === element.nodeName) { 58 | if (sib === element) { 59 | sibIndex = sibCount 60 | break 61 | } 62 | sibCount++ 63 | } 64 | } 65 | object.data.sibIndex = sibIndex 66 | } 67 | 68 | treeHTML(element, treeObject) 69 | 70 | return json ? JSON.stringify(treeObject) : treeObject 71 | } 72 | 73 | export const getSelector = () => { 74 | const initElement = document.getElementsByTagName('body')[0] 75 | const json = mapDOM(initElement, true) 76 | const postData = JSON.stringify({ 77 | html: JSON.parse(json) 78 | }) 79 | return postData 80 | } 81 | -------------------------------------------------------------------------------- /packages/extension/src/ext/inject.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intuit/ReplayWeb/8b4cb972f86bbdbf00417674caff30a279563cac/packages/extension/src/ext/inject.js -------------------------------------------------------------------------------- /packages/extension/src/models/__mocks__/block-model.js: -------------------------------------------------------------------------------- 1 | const model = { 2 | removeByProject: () => Promise.resolve(), 3 | listByProject: () => Promise.resolve([]), 4 | update: jest.fn().mockImplementation(() => Promise.resolve()) 5 | } 6 | 7 | export default model 8 | -------------------------------------------------------------------------------- /packages/extension/src/models/__mocks__/project_model.js: -------------------------------------------------------------------------------- 1 | const model = { 2 | insert: () => Promise.resolve(Math.random()), 3 | get: () => 4 | Promise.resolve({ 5 | name: 'Test', 6 | testPath: '/tests', 7 | blockPath: '/blocks', 8 | projectPath: '/' 9 | }), 10 | update: () => Promise.resolve(), 11 | remove: () => Promise.resolve() 12 | } 13 | 14 | export default model 15 | -------------------------------------------------------------------------------- /packages/extension/src/models/__mocks__/suite_model.js: -------------------------------------------------------------------------------- 1 | const model = { 2 | removeByProject: () => Promise.resolve(), 3 | listByProject: () => Promise.resolve([]) 4 | } 5 | 6 | export default model 7 | -------------------------------------------------------------------------------- /packages/extension/src/models/__mocks__/test_case_model.js: -------------------------------------------------------------------------------- 1 | const model = { 2 | removeByProject: () => Promise.resolve(), 3 | listByProject: () => Promise.resolve([]), 4 | update: jest.fn().mockImplementation(() => Promise.resolve()) 5 | } 6 | 7 | export default model 8 | -------------------------------------------------------------------------------- /packages/extension/src/models/block-model.js: -------------------------------------------------------------------------------- 1 | import { uid, compose, on, map } from '../common/utils' 2 | import { normalizeCommand } from './test_case_model' 3 | import db from './db' 4 | 5 | const table = db.blocks 6 | 7 | const model = { 8 | table, 9 | list() { 10 | return table.toArray() 11 | }, 12 | listByProject(project) { 13 | return table 14 | .where('projectId') 15 | .equals(project.id) 16 | .toArray() 17 | }, 18 | removeByProject(project) { 19 | return table 20 | .where('projectId') 21 | .equals(project.id) 22 | .delete() 23 | }, 24 | insert(data) { 25 | if (!data.name) { 26 | throw new Error('Model Blocks - insert: missing name') 27 | } 28 | 29 | if (!data.data) { 30 | throw new Error('Model Blocks - insert: missing data') 31 | } 32 | 33 | data.updateTime = new Date() * 1 34 | data.id = uid() 35 | return table.add(normalizeBlock(data)) 36 | }, 37 | bulkInsert(blocks) { 38 | const list = blocks.map(data => { 39 | if (!data.name) { 40 | throw new Error('Model Blocks - insert: missing name') 41 | } 42 | 43 | if (!data.data) { 44 | throw new Error('Model Blocks - insert: missing data') 45 | } 46 | 47 | data.updateTime = new Date() * 1 48 | data.id = uid() 49 | 50 | return normalizeBlock(data) 51 | }) 52 | 53 | return table.bulkAdd(list) 54 | }, 55 | bulkUpdate(blocks) { 56 | const list = blocks.map(data => { 57 | if (!data.name) { 58 | throw new Error('Model Blocks - insert: missing name') 59 | } 60 | 61 | if (!data.data) { 62 | throw new Error('Model Blocks - insert: missing data') 63 | } 64 | 65 | data.updateTime = new Date() * 1 66 | // data.id = uid() 67 | 68 | return normalizeBlock(data) 69 | }) 70 | 71 | return table.bulkPut(list) 72 | }, 73 | update(id, data) { 74 | return table.update(id, normalizeBlock(data)) 75 | }, 76 | bulkRemove(blocks) { 77 | return blocks.reduce( 78 | (acc, cv) => acc.then(() => table.delete(cv.id)), 79 | Promise.resolve() 80 | ) 81 | }, 82 | remove(id) { 83 | return table.delete(id) 84 | }, 85 | clear() { 86 | return table.clear() 87 | } 88 | } 89 | 90 | export default model 91 | 92 | export const normalizeBlock = block => { 93 | return compose( 94 | on('data'), 95 | on('commands'), 96 | map 97 | )(normalizeCommand)(block) 98 | } 99 | -------------------------------------------------------------------------------- /packages/extension/src/models/db.js: -------------------------------------------------------------------------------- 1 | import Dexie from 'dexie' 2 | // dexie is a indexdb wrapper 3 | 4 | const db = new Dexie('replaykit-ide') 5 | 6 | db.version(1).stores({ 7 | testCases: 'id,projectId,name,updateTime', 8 | projects: 'id,name,updateTime' 9 | }) 10 | db.version(2).stores({ 11 | testCases: 'id,projectId,name,updateTime', 12 | blocks: 'id,projectId,name,updateTime', 13 | projects: 'id,name,updateTime' 14 | }) 15 | db.version(3).stores({ 16 | testCases: 'id,projectId,name,updateTime', 17 | blocks: 'id,projectId,name,updateTime', 18 | projects: 'id,name,updateTime,projectPath,testPath,blockPath,suites' 19 | }) 20 | db.version(4).stores({ 21 | testCases: 'id,projectId,name,updateTime', 22 | blocks: 'id,projectId,name,updateTime', 23 | projects: 'id,name,updateTime,projectPath,testPath,blockPath,suites', 24 | suites: 'id,name,projectId,updateTime' 25 | }) 26 | 27 | db.open() 28 | 29 | export default db 30 | -------------------------------------------------------------------------------- /packages/extension/src/models/project_model.js: -------------------------------------------------------------------------------- 1 | import db from './db' 2 | import { uid } from '../common/utils' 3 | 4 | const table = db.projects 5 | 6 | const model = { 7 | table, 8 | list() { 9 | return table.toArray() 10 | }, 11 | get(id) { 12 | return table 13 | .where('id') 14 | .equals(id) 15 | .first() 16 | }, 17 | insert(data) { 18 | if (!data.name) { 19 | throw new Error('Model Project - insert: missing name') 20 | } 21 | 22 | if (!data.projectPath) { 23 | throw new Error('Model Project - insert: missing projectPath') 24 | } 25 | 26 | if (!data.testPath) { 27 | throw new Error('Model Project - insert: missing testPath') 28 | } 29 | 30 | if (!data.blockPath) { 31 | throw new Error('Model Project - insert: missing blockPath') 32 | } 33 | 34 | data.updateTime = new Date() * 1 35 | data.id = uid() 36 | return table.add(data) 37 | }, 38 | update(id, data) { 39 | return table.update(id, data) 40 | }, 41 | remove(id) { 42 | return table.delete(id) 43 | }, 44 | clear() { 45 | return table.clear() 46 | } 47 | } 48 | 49 | export default model 50 | -------------------------------------------------------------------------------- /packages/extension/src/models/suite_model.js: -------------------------------------------------------------------------------- 1 | import db from './db' 2 | import { uid } from '../common/utils' 3 | 4 | const table = db.suites 5 | 6 | const model = { 7 | table, 8 | list() { 9 | return table.toArray() 10 | }, 11 | listByProject(project) { 12 | return table 13 | .where('projectId') 14 | .equals(project.id) 15 | .toArray() 16 | }, 17 | removeByProject(project) { 18 | return table 19 | .where('projectId') 20 | .equals(project.id) 21 | .delete() 22 | }, 23 | insert(data) { 24 | if (!data.name) { 25 | throw new Error('Model Suite - insert: missing name') 26 | } 27 | 28 | if (!data.data) { 29 | throw new Error('Model Suite - insert: missing data') 30 | } 31 | 32 | data.updateTime = new Date() * 1 33 | data.id = uid() 34 | return table.add(data) 35 | }, 36 | bulkInsert(suites) { 37 | const list = suites.map(data => { 38 | if (!data.name) { 39 | throw new Error('Model Suite - insert: missing name') 40 | } 41 | 42 | if (!data.data) { 43 | throw new Error('Model Suite - insert: missing data') 44 | } 45 | 46 | data.updateTime = new Date() * 1 47 | data.id = uid() 48 | 49 | return data 50 | }) 51 | 52 | return table.bulkAdd(list) 53 | }, 54 | bulkUpdate(suites) { 55 | const list = suites.map(data => { 56 | if (!data.name) { 57 | throw new Error('Model TestCase - insert: missing name') 58 | } 59 | 60 | if (!data.data) { 61 | throw new Error('Model TestCase - insert: missing data') 62 | } 63 | 64 | data.updateTime = new Date() * 1 65 | 66 | return data 67 | }) 68 | 69 | return table.bulkPut(list) 70 | }, 71 | update(id, data) { 72 | return table.update(id, data) 73 | }, 74 | bulkRemove(suites) { 75 | return suites.reduce( 76 | (acc, cv) => acc.then(() => table.delete(cv.id)), 77 | Promise.resolve() 78 | ) 79 | }, 80 | remove(id) { 81 | return table.delete(id) 82 | }, 83 | clear() { 84 | return table.clear() 85 | } 86 | } 87 | 88 | export default model 89 | -------------------------------------------------------------------------------- /packages/extension/src/reducers/dropdowns.js: -------------------------------------------------------------------------------- 1 | import { types } from '../actions/action_types' 2 | 3 | const T = types // so that auto complete in webstorm doesn't go crazy 4 | 5 | const initialState = { 6 | testcase: false 7 | } 8 | 9 | export { initialState } 10 | 11 | export default function reducer(state = initialState, action) { 12 | switch (action.type) { 13 | case T.DROPDOWN_STATE: 14 | return Object.assign({}, state, { [action.dropdown]: action.state }) 15 | default: 16 | return state 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/extension/src/reducers/files.js: -------------------------------------------------------------------------------- 1 | import { types } from '../actions/action_types' 2 | 3 | const T = types // so that auto complete in webstorm doesn't go crazy 4 | 5 | const initialState = { 6 | modalVisible: false, 7 | folder: '~', 8 | contents: [], 9 | activeFolder: null, 10 | activeContents: [], 11 | folderList: [], 12 | error: false 13 | } 14 | 15 | export { initialState } 16 | 17 | export default function reducer(state = initialState, action) { 18 | switch (action.type) { 19 | case T.FILE_ERROR: 20 | return { ...state, error: true } 21 | case T.SELECT_FOLDER: 22 | return Object.assign({}, state, { 23 | ...state, 24 | folder: action.folder, 25 | contents: action.contents, 26 | error: false 27 | }) 28 | case T.FOLDER_CONTENTS: 29 | return Object.assign({}, state, { 30 | ...state, 31 | activeFolder: action.folder, 32 | activeContents: action.files, 33 | error: false 34 | }) 35 | case T.FOLDER_MODAL: 36 | return Object.assign({}, state, { 37 | ...state, 38 | modalVisible: action.open, 39 | error: false 40 | }) 41 | case T.SET_FOLDERS: 42 | return Object.assign({}, state, { 43 | ...state, 44 | modalVisible: false, 45 | folderList: action.folderList, 46 | error: false 47 | }) 48 | default: 49 | return state 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/extension/src/reducers/modals.js: -------------------------------------------------------------------------------- 1 | import { types } from '../actions/action_types' 2 | 3 | const T = types // so that auto complete in webstorm doesn't go crazy 4 | 5 | const initialState = { 6 | playLoop: false, 7 | settings: false, 8 | newBlockModal: false, 9 | duplicate: false, 10 | rename: false, 11 | project: false, 12 | multiselect: false, 13 | shareBlock: false, 14 | save: false, 15 | browser: false, 16 | projectSetup: false 17 | } 18 | 19 | export { initialState } 20 | 21 | export default function reducer(state = initialState, action) { 22 | switch (action.type) { 23 | case T.MODAL_STATE: 24 | return Object.assign({}, state, { [action.modal]: action.state }) 25 | default: 26 | return state 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/extension/src/reducers/player.js: -------------------------------------------------------------------------------- 1 | import { types } from '../actions/action_types' 2 | import { setIn, updateIn } from '../common/utils' 3 | import * as C from '../common/constant' 4 | 5 | const T = types // so that auto complete in webstorm doesn't go crazy 6 | 7 | const initialState = { 8 | context: {}, 9 | status: C.PLAYER_STATUS.STOPPED, 10 | stopReason: null, 11 | currentLoop: 0, 12 | loops: 0, 13 | nextCommandIndex: null, 14 | errorCommandIndices: [], 15 | doneCommandIndices: [], 16 | playInterval: 0, 17 | timeoutStatus: { 18 | type: null, 19 | total: null, 20 | past: null 21 | } 22 | } 23 | 24 | export { initialState } 25 | 26 | export default function reducer(state = initialState, action) { 27 | switch (action.type) { 28 | case T.EDIT_TEST_CASE: { 29 | return { 30 | ...state, 31 | status: C.PLAYER_STATUS.STOPPED, 32 | stopReason: null, 33 | nextCommandIndex: null, 34 | errorCommandIndices: [], 35 | doneCommandIndices: [] 36 | } 37 | } 38 | case T.EDIT_BLOCK: { 39 | return { 40 | ...state, 41 | status: C.PLAYER_STATUS.STOPPED, 42 | stopReason: null, 43 | nextCommandIndex: null, 44 | errorCommandIndices: [], 45 | doneCommandIndices: [] 46 | } 47 | } 48 | 49 | case T.EDIT_NEW_TEST_CASE: { 50 | return { 51 | ...state, 52 | nextCommandIndex: null, 53 | errorCommandIndices: [], 54 | doneCommandIndices: [] 55 | } 56 | } 57 | 58 | case T.EDIT_NEW_BLOCK: 59 | return { 60 | ...state, 61 | nextCommandIndex: null, 62 | errorCommandIndices: [], 63 | doneCommandIndices: [] 64 | } 65 | 66 | case T.SET_PLAYER_STATE: 67 | return { 68 | ...state, 69 | ...action.data 70 | } 71 | 72 | case T.PLAYER_ADD_ERROR_COMMAND_INDEX: 73 | return updateIn( 74 | ['errorCommandIndices'], 75 | indices => [...indices, action.data], 76 | state 77 | ) 78 | case T.CLEAR_CONTEXT: 79 | return setIn(['context'], {}, state) 80 | case T.SET_CONTEXT: 81 | return setIn(['context', action.key], action.value, state) 82 | default: 83 | return state 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/extension/src/reducers/projectSetup.js: -------------------------------------------------------------------------------- 1 | import { types } from '../actions/action_types' 2 | 3 | const T = types // so that auto complete in webstorm doesn't go crazy 4 | 5 | const initialState = { 6 | projectPath: '', 7 | testPath: '', 8 | blockPath: '', 9 | suites: {}, 10 | name: '', 11 | purpose: T.PROJECT_PATH, 12 | existingConfig: null, 13 | id: null 14 | } 15 | 16 | export { initialState } 17 | 18 | export default function reducer(state = initialState, action) { 19 | switch (action.type) { 20 | case T.CLEAR_PROJECT_SETUP: 21 | return { ...initialState } 22 | case T.EDIT_PROJECT: 23 | return { ...initialState, ...action.project } 24 | case T.EXISTING_CONFIG: 25 | return { ...state, existingConfig: action.exists } 26 | case T.SET_PURPOSE: 27 | return { ...state, purpose: action.purpose } 28 | case T.EXISTING_SUITES: 29 | return { ...state, suites: action.suites } 30 | case T.TEST_PATH: 31 | return { ...state, testPath: action.path.replace(state.projectPath, '.') } 32 | case T.BLOCK_PATH: 33 | return { 34 | ...state, 35 | blockPath: action.path.replace(state.projectPath, '.') 36 | } 37 | case T.PROJECT_PATH: 38 | return { ...state, projectPath: action.path } 39 | default: 40 | return state 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/extension/src/redux/index.js: -------------------------------------------------------------------------------- 1 | import { Provider } from 'react-redux' 2 | import { createStore as oldCreateStore, applyMiddleware } from 'redux' 3 | import thunk from 'redux-thunk' 4 | import createPromiseMiddleware from './promise_middleware' 5 | import createPostLogicMiddleware from './post_logic_middleware' 6 | import { createLogger } from 'redux-logger' 7 | 8 | const createStore = applyMiddleware( 9 | thunk, 10 | createPromiseMiddleware(), 11 | createPostLogicMiddleware(), 12 | createLogger({}) 13 | )(oldCreateStore) 14 | 15 | export { Provider, createStore } 16 | -------------------------------------------------------------------------------- /packages/extension/src/redux/post_logic_middleware.js: -------------------------------------------------------------------------------- 1 | // Note: if `post` field provided, it will call `post` 2 | // after the action dispatched and state updated 3 | export default function postLogicMiddleWare(extra) { 4 | return ({ dispatch, getState }) => next => action => { 5 | const { post } = action 6 | 7 | if (post && typeof post === 'function') { 8 | setTimeout(() => { 9 | post({ dispatch, getState }, action, extra) 10 | }, 0) 11 | } 12 | 13 | return next(action) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/extension/src/redux/promise_middleware.js: -------------------------------------------------------------------------------- 1 | // Note: if a `promise` field and a `types` provided, this middleware will dispatch 2 | // 3 actions REQUEST, SUCCESS, FAILURE based on the status of the promise it returns 3 | export default function promiseMiddleWare() { 4 | return ({ dispatch, getState }) => { 5 | return next => action => { 6 | const { promise, types, ...rest } = action 7 | 8 | if (!promise) { 9 | return next(action) 10 | } 11 | 12 | const [REQUEST, SUCCESS, FAILURE] = types 13 | next({ ...rest, type: REQUEST }) 14 | return promise().then( 15 | data => next({ ...rest, data, type: SUCCESS }), 16 | error => { 17 | return next({ ...rest, err: error, type: FAILURE }) 18 | } 19 | ) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/extension/src/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import './common.scss'; 2 | 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | font-size: 16px; 7 | } 8 | 9 | * { 10 | box-sizing: border-box; 11 | } 12 | 13 | .app { 14 | position: absolute; 15 | top: 0; 16 | bottom: 0; 17 | left: 0; 18 | right: 0; 19 | display: flex; 20 | 21 | .content { 22 | @include flexcol(); 23 | 24 | flex: 3; 25 | background: #fff; 26 | overflow-y: auto; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/extension/src/styles/common.scss: -------------------------------------------------------------------------------- 1 | @mixin flexcol() { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | -------------------------------------------------------------------------------- /packages/extension/src/styles/header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | overflow: hidden; 3 | padding: 0 20px; 4 | width: 100%; 5 | height: 44px; 6 | border-bottom: 2px solid #ccc; 7 | background-color: #001529; 8 | color: #ffffff; 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | 13 | .status { 14 | float: right; 15 | line-height: 42px; 16 | font-size: 14px; 17 | 18 | h1 { 19 | margin: 0; 20 | font-size: 20px; 21 | line-height: 44px; 22 | } 23 | } 24 | 25 | .show-sidebar { 26 | display: none; 27 | } 28 | 29 | &.normal { 30 | .show-sidebar { 31 | display: inline-block; 32 | margin-left: 15px; 33 | } 34 | } 35 | 36 | .ant-checkbox-wrapper { 37 | color: #ffffff; 38 | } 39 | .status-bar { 40 | margin-left: 30px; 41 | .unsaved { 42 | color: rgb(0, 195, 255); 43 | 44 | &::after { 45 | content: '*'; 46 | margin-left: 5px; 47 | } 48 | } 49 | } 50 | 51 | .ant-dropdown-menu { 52 | max-height: 300px; 53 | overflow-y: auto; 54 | 55 | .editing { 56 | color: blue !important; 57 | } 58 | } 59 | 60 | .play-ops { 61 | flex: 1; 62 | text-align: right; 63 | margin-right: 5px; 64 | margin-left: 5px; 65 | .ant-btn { 66 | border-color: rgb(0, 21, 41) !important; 67 | } 68 | .play-ops-dark { 69 | .ant-btn { 70 | background-color: #001529 !important; 71 | color: white; 72 | } 73 | } 74 | } 75 | .content-result { 76 | .ant-btn { 77 | background-color: #001529 !important; 78 | &.danger { 79 | color: #df3731 !important; 80 | } 81 | &.primary { 82 | color: #1890ff !important; 83 | } 84 | } 85 | } 86 | } 87 | 88 | .header-items { 89 | .ant-menu-item { 90 | display: flex; 91 | justify-content: space-between; 92 | align-items: center; 93 | } 94 | .ant-menu-submenu-title { 95 | margin-left: 10px; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/extension/src/styles/sidebar.scss: -------------------------------------------------------------------------------- 1 | .side-bar { 2 | .ant-menu-item, 3 | .ant-layout-sider, 4 | .ant-menu-inline, 5 | .ant-layout-sider-trigger { 6 | /* Menu */ 7 | min-width: 300px !important; 8 | flex: 0 0 300px !important; 9 | } 10 | 11 | .ant-layout-sider, 12 | .ant-layout-sider-dark, 13 | .ant-layout-sider-has-trigger { 14 | height: 100vh !important; 15 | } 16 | .ant-layout-sider-collapsed { 17 | min-width: 90px !important; 18 | width: 90px !important; 19 | flex: 0 0 90px !important; 20 | .ant-layout-sider-trigger { 21 | min-width: 90px !important; 22 | width: 90px !important; 23 | flex: 0 0 90px !important; 24 | } 25 | } 26 | 27 | @mixin sidebar-list-item { 28 | .ant-menu-item { 29 | display: flex; 30 | } 31 | .anticon { 32 | justify-content: center; 33 | align-items: flex-end; 34 | &.success { 35 | color: #35b876; 36 | } 37 | &.error { 38 | color: #df3731; 39 | } 40 | } 41 | &.success { 42 | color: #1aec83; 43 | } 44 | &.error { 45 | color: #df3731; 46 | } 47 | &.disabled { 48 | filter: grayscale(60%); 49 | cursor: not-allowed; 50 | } 51 | } 52 | 53 | .ant-layout-sider-children { 54 | overflow: overlay; 55 | } 56 | 57 | .sidebar-test-cases { 58 | .testcase { 59 | @include sidebar-list-item; 60 | } 61 | } 62 | 63 | .sidebar-blocks { 64 | .block { 65 | @include sidebar-list-item; 66 | } 67 | } 68 | } 69 | 70 | .folder-sidebar { 71 | .ant-menu-vertical.ant-menu-sub { 72 | overflow: scroll; 73 | width: 300px !important; 74 | } 75 | .ant-menu-item { 76 | display: flex; 77 | justify-content: space-between; 78 | align-items: center; 79 | width: 300px !important; 80 | } 81 | .add-folder { 82 | border-bottom: 1px solid #b3b9bd !important; 83 | } 84 | } 85 | 86 | .test-sidebar, 87 | .block-sidebar { 88 | .ant-menu-vertical.ant-menu-sub { 89 | overflow: scroll; 90 | height: 100vh !important; 91 | width: 300px !important; 92 | } 93 | } 94 | 95 | .ant-modal { 96 | .backModal { 97 | padding: 10px; 98 | font-size: 15px; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /packages/extension/test/actions/suites.test.js: -------------------------------------------------------------------------------- 1 | import { addTestToSuite, removeTestFromSuite, createSuite, updateSuite, removeSuite } from '../../src/actions/suites' 2 | import { mockStore } from '../utils' 3 | 4 | jest.mock('../../src/models/suite_model', () => ({ 5 | insert: jest.fn().mockResolvedValue(), 6 | listByProject: jest.fn().mockResolvedValue([{id: 1}, {id: 2}]), 7 | remove: jest.fn().mockResolvedValue(), 8 | update: jest.fn().mockResolvedValue() 9 | })) 10 | 11 | jest.mock('../../src/actions/editor', () => ({ 12 | setSuites: () => ({type: 'SET_SUITE_MOCK'}) 13 | })) 14 | jest.mock('../../src/actions/action_types', () => ( 15 | { 16 | types: { 17 | ADD_SUITE_TEST: 'MOCK_ADD_SUITE_TEST', 18 | REMOVE_SUITE_TEST: 'MOCK_REMOVE_SUITE_TEST' 19 | } 20 | } 21 | )) 22 | 23 | describe('action suites utils', () => { 24 | it('addTestToSuite', () => { 25 | expect(addTestToSuite('mockTest')).toEqual({ 26 | test: 'mockTest', 27 | type: 'MOCK_ADD_SUITE_TEST' 28 | }) 29 | }) 30 | it('removeTestFromSuite', () => { 31 | expect(removeTestFromSuite('mockTest')).toEqual({ 32 | test: 'mockTest', 33 | type: 'MOCK_REMOVE_SUITE_TEST' 34 | }) 35 | }) 36 | }) 37 | 38 | describe('action suites thunk utils', () => { 39 | let store 40 | beforeEach(() => { 41 | store = mockStore({ 42 | editor: { 43 | project: { 44 | id: 1 45 | } 46 | } 47 | }) 48 | }) 49 | 50 | it('createSuite', () => { 51 | return store.dispatch(createSuite({id: 1})) 52 | .then(() => { 53 | expect(store.getActions()).toEqual([{type: 'SET_SUITE_MOCK'}]) 54 | }) 55 | }) 56 | 57 | it('updateSuite', () => { 58 | return store.dispatch(updateSuite({id: 1})) 59 | .then(() => { 60 | expect(store.getActions()).toEqual([{type: 'SET_SUITE_MOCK'}]) 61 | }) 62 | }) 63 | 64 | it('removeSuite', () => { 65 | return store.dispatch(removeSuite({id: 1})) 66 | .then(() => { 67 | expect(store.getActions()).toEqual([{type: 'SET_SUITE_MOCK'}]) 68 | }) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /packages/extension/test/app.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | 3 | import React from 'react' 4 | import { App } from '../src/app' 5 | import { shallow } from 'enzyme' 6 | 7 | jest.mock('../src/actions/app', () => ({ 8 | readBlockShareConfig: jest.fn() 9 | })) 10 | jest.mock('../src/containers/dashboard', () => () => { 11 | return

Dashboard

12 | }) 13 | jest.mock('../src/containers/Header', () => () => { 14 | return

Header

15 | }) 16 | jest.mock('../src/containers/Sidebar', () => () => { 17 | return

Sidebar

18 | }) 19 | 20 | afterEach(() => { 21 | jest.clearAllMocks() 22 | }) 23 | 24 | describe('App', () => { 25 | it('renders', () => { 26 | const mockReadBlockShareConfig = jest.fn() 27 | const wrapper = shallow( 28 | 29 | ) 30 | expect(mockReadBlockShareConfig).toHaveBeenCalledTimes(1) 31 | expect(wrapper.find('Adapter').length).not.toBeNull() 32 | expect(wrapper.find('Component').length).not.toBeNull() 33 | }) 34 | }) 35 | 36 | /* eslint-enable react/display-name */ 37 | -------------------------------------------------------------------------------- /packages/extension/test/common/block.test.js: -------------------------------------------------------------------------------- 1 | import { collapseExpandedTestCase } from '../../src/common/blocks.js' 2 | 3 | describe('collapseExpandedTestCase', () => { 4 | it('returns empty when nothing passed in', () => { 5 | const res = collapseExpandedTestCase([]) 6 | expect(res).toEqual([]) 7 | }) 8 | 9 | it('expands the data', () => { 10 | const commands = [ 11 | { isBlock: false, name: 'foo' }, 12 | { isBlock: true, name: 'bar' }, 13 | { isBlock: false, name: 'baz' } 14 | ] 15 | const expected = [ 16 | { isBlock: false, name: 'foo', expandedIndex: 0 }, 17 | { isBlock: true, name: 'bar', expandedIndex: 1 }, 18 | { isBlock: false, name: 'baz', expandedIndex: 2 } 19 | ] 20 | const res = collapseExpandedTestCase(commands) 21 | expect(res).toEqual(expected) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /packages/extension/test/common/capture_screenshot.test.js: -------------------------------------------------------------------------------- 1 | import saveScreen from '../../src/common/capture_screenshot.js' 2 | 3 | jest.mock('../../src/common/filesystem', () => ({ 4 | writeFile: jest.fn().mockImplementation(() => { 5 | return Promise.resolve() 6 | }) 7 | })) 8 | 9 | jest.mock('../../src/common/web_extension', () => ({ 10 | windows: { 11 | getLastFocused: jest.fn().mockImplementation(() => { 12 | return Promise.resolve({ id: 1 }) 13 | }) 14 | }, 15 | tabs: { 16 | captureVisibleTab: jest.fn().mockImplementation(() => { 17 | return Promise.resolve('a:b;c,YWJj') 18 | }), 19 | query: jest.fn().mockImplementation(() => { 20 | return Promise.resolve([{ title: 'a b c' }]) 21 | }) 22 | } 23 | })) 24 | 25 | afterEach(() => { 26 | jest.clearAllMocks() 27 | }) 28 | 29 | describe('capture_screenshot', () => { 30 | it('does not throw any errors', async () => { 31 | await saveScreen() 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /packages/extension/test/common/filesystem.test.js: -------------------------------------------------------------------------------- 1 | describe('fs', () => { 2 | it('throws an error when not supported', () => { 3 | try { 4 | require('../../src/common/filesystem.js') 5 | expect(true).toBe(false) 6 | } catch (err) { 7 | expect(err.toString()).toMatch(/requestFileSystem not supported/) 8 | } 9 | }) 10 | 11 | it('does not throw an error when supported', () => { 12 | window.webkitRequestFileSystem = true 13 | require('../../src/common/filesystem.js') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/extension/test/common/ipc/ipc_cs.test.js: -------------------------------------------------------------------------------- 1 | afterEach(() => { 2 | jest.clearAllMocks() 3 | }) 4 | 5 | describe('ipc_cs', () => { 6 | describe('ipc', () => { 7 | it('returns the result of csInit', () => { 8 | jest.doMock('../../../src/common/ipc/ipc_bg_cs', () => ({ 9 | csInit: () => true 10 | })) 11 | const ipc = require('../../../src/common/ipc/ipc_cs') 12 | expect(ipc.default).toBe(true) 13 | }) 14 | 15 | // TODO more tests ... 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/extension/test/common/network.test.js: -------------------------------------------------------------------------------- 1 | import { getHeaders } from '../../src/common/network' 2 | 3 | describe('network', () => { 4 | describe('getHeaders', () => { 5 | it('should return same headers for URL not in CORB whitelist', () => { 6 | const headers = [ 7 | { 8 | name: 'Authorization', 9 | value: 'apikey' 10 | }, 11 | { 12 | name: 'Access-Control-Allow-Origin', 13 | value: 'example.com' 14 | } 15 | ] 16 | expect(getHeaders('https://app.example.com', headers)).toEqual(headers) 17 | }) 18 | it("should return same headers for URL in CORB whitelist if it doesn't have the CORS header", () => { 19 | const headers = [ 20 | { 21 | name: 'Authorization', 22 | value: 'apikey' 23 | } 24 | ] 25 | expect(getHeaders('https://app.example.com', headers)).toEqual(headers) 26 | }) 27 | it('should rewrite CORS header for whitelisted domain', () => { 28 | const headers = [ 29 | { 30 | name: 'Authorization', 31 | value: 'apikey' 32 | }, 33 | { 34 | name: 'Access-Control-Allow-Origin', 35 | value: '*' 36 | } 37 | ] 38 | const expectedHeaders = [ 39 | { 40 | name: 'Authorization', 41 | value: 'apikey' 42 | }, 43 | { 44 | name: 'Access-Control-Allow-Origin', 45 | value: '*' 46 | } 47 | ] 48 | expect( 49 | getHeaders('https://another.app.example.com/api/endpoint', headers) 50 | ).toEqual(expectedHeaders) 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /packages/extension/test/common/send_keys.test.js: -------------------------------------------------------------------------------- 1 | import sendKeys, { 2 | __RewireAPI__ as keysRewire 3 | } from '../../src/common/send_keys' 4 | 5 | afterEach(() => { 6 | keysRewire.__ResetDependency__('splitStringToChars') 7 | keysRewire.__ResetDependency__('getKeyStrokeAction') 8 | }) 9 | 10 | describe('sendKeys', () => { 11 | it('handles a simple case', () => { 12 | let splitStringToCharsCounter = 0 13 | keysRewire.__Rewire__('splitStringToChars', () => { 14 | splitStringToCharsCounter++ 15 | return [] 16 | }) 17 | keysRewire.__Rewire__('getKeyStrokeAction', () => { 18 | throw new Error('should not call getKeyStrokeAction') 19 | }) 20 | const focusMock = jest.fn() 21 | const target = { 22 | focus: focusMock, 23 | value: 'abc' 24 | } 25 | const res = sendKeys(target, '') 26 | expect(res).toBe(undefined) 27 | expect(splitStringToCharsCounter).toBe(1) 28 | expect(focusMock).toHaveBeenCalledTimes(1) 29 | }) 30 | 31 | // TODO more tests ... 32 | }) 33 | -------------------------------------------------------------------------------- /packages/extension/test/common/storage/ext_storage.test.js: -------------------------------------------------------------------------------- 1 | import storage from '../../../src/common/storage/ext_storage' 2 | 3 | jest.mock('../../../src/common/web_extension', () => ({ 4 | storage: { 5 | local: { 6 | get: jest.fn().mockImplementation(key => Promise.resolve({ foo: 'bar' })), 7 | set: jest.fn().mockImplementation(() => Promise.resolve(true)), 8 | remove: jest.fn().mockImplementation(() => Promise.resolve(true)), 9 | clear: jest.fn().mockImplementation(() => Promise.resolve(true)) 10 | }, 11 | onChanged: { 12 | addListener: jest.fn() 13 | } 14 | } 15 | })) 16 | 17 | afterEach(() => { 18 | localStorage.clear() 19 | jest.clearAllMocks() 20 | }) 21 | beforeEach(() => { 22 | localStorage.clear() 23 | }) 24 | 25 | describe('storage', () => { 26 | describe('get', () => { 27 | it('returns value', async () => { 28 | const res = await storage.get('foo') 29 | expect(res).toBe('bar') 30 | }) 31 | }) 32 | 33 | describe('set', () => { 34 | it('returns value', async () => { 35 | const res = await storage.set('foo', 'bar') 36 | expect(res).toBe(true) 37 | }) 38 | }) 39 | 40 | describe('remove', () => { 41 | it('returns value', async () => { 42 | const res = await storage.remove('foo') 43 | expect(res).toBe(true) 44 | }) 45 | }) 46 | 47 | describe('clear', () => { 48 | it('returns value', async () => { 49 | const res = await storage.clear('foo') 50 | expect(res).toBe(true) 51 | }) 52 | }) 53 | 54 | describe('addListener', () => { 55 | it('sets callback', () => { 56 | storage.addListener(jest.fn()) 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /packages/extension/test/common/storage/localstorage_adapter.test.js: -------------------------------------------------------------------------------- 1 | import localStorageAdapter from '../../../src/common/storage/localstorage_adapter' 2 | 3 | afterEach(() => { 4 | localStorage.clear() 5 | }) 6 | beforeEach(() => { 7 | localStorage.clear() 8 | }) 9 | 10 | describe('localstorage_adapter', () => { 11 | describe('get', () => { 12 | it('returns value', async () => { 13 | localStorage.setItem('test', `{"foo": "bar"}`) 14 | const res = await localStorageAdapter.get('test') 15 | expect(res).toEqual({ test: { foo: 'bar' } }) 16 | }) 17 | }) 18 | 19 | describe('set', () => { 20 | it('returns value', async () => { 21 | const res = await localStorageAdapter.set({ 22 | foo: 1, 23 | bar: [1], 24 | baz: '1' 25 | }) 26 | expect(res).toBe(true) 27 | expect(localStorage.getItem('foo')).toEqual('1') 28 | expect(localStorage.getItem('bar')).toEqual('[1]') 29 | expect(localStorage.getItem('baz')).toEqual('"1"') 30 | }) 31 | }) 32 | describe('remove', () => { 33 | it('returns value', async () => { 34 | localStorage.setItem('foo', 1) 35 | await localStorageAdapter.remove('foo') 36 | expect(localStorage.getItem('foo')).toBeNull() 37 | }) 38 | }) 39 | describe('clear', () => { 40 | it('returns value', async () => { 41 | localStorage.setItem('foo', 1) 42 | await localStorageAdapter.clear('foo') 43 | expect(localStorage.getItem('foo')).toBeNull() 44 | }) 45 | }) 46 | describe('addListener', () => { 47 | it('returns value', () => { 48 | localStorageAdapter.addListener() 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/extension/test/common/utils.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | until, 3 | asyncUntil, 4 | uid, 5 | pick, 6 | filtering, 7 | removeArrayItem 8 | } from '../../src/common/utils' 9 | 10 | describe('until', () => { 11 | it.only('should return error if expired', async() => { 12 | try { 13 | await until('name', () => ({pass: true, result: ''}), 1, -1, 'test error message') 14 | expect(true).toBe(false) 15 | } catch (err) { 16 | expect(err.toString()).toMatch("test error message") 17 | } 18 | }) 19 | }) 20 | 21 | describe('asyncUntil', () => { 22 | it('should return error if expired', () => { 23 | try { 24 | asyncUntil('name', 'check', 1000, 1, 'test error message') 25 | expect(true).toBe(false) 26 | } catch (err) { 27 | expect(err.toString()).toMatch("test error message") 28 | } 29 | }) 30 | }) 31 | 32 | describe('removeArrayItem', () => { 33 | it('should immutably remove the first item from an array', () => { 34 | const array = ['a', 'b', 'c', 'd'] 35 | expect(removeArrayItem(array, 'a')).toEqual(['b', 'c', 'd']) 36 | expect(array).toEqual(['a', 'b', 'c', 'd']) 37 | }) 38 | it('should immutably remove an item from an array', () => { 39 | const array = ['a', 'b', 'c', 'd'] 40 | expect(removeArrayItem(array, 'b')).toEqual(['a', 'c', 'd']) 41 | expect(array).toEqual(['a', 'b', 'c', 'd']) 42 | }) 43 | it('should not remove an item from that doesnt exist in array', () => { 44 | const array = ['a', 'b', 'c', 'd'] 45 | expect(removeArrayItem(array, 'e')).toEqual(['a', 'b', 'c', 'd']) 46 | expect(array).toEqual(['a', 'b', 'c', 'd']) 47 | }) 48 | }) 49 | 50 | describe('pick', () => { 51 | it('should return the passed in object with only certains keys', () => { 52 | const keys = ['a', 'b', 'c'] 53 | const obj = {'a':'1', 'b':'2', 'c':'3', 'd':'4'} 54 | expect(pick(keys, obj)).toEqual({'a':'1', 'b':'2', 'c':'3'}) 55 | expect(keys).toEqual(['a', 'b', 'c']) 56 | }) 57 | it('should return same object if it has the same keys', () => { 58 | const keys = ['a', 'b', 'c'] 59 | const obj = {'a':'1', 'b':'2', 'c':'3'} 60 | expect(pick(keys, obj)).toEqual({'a':'1', 'b':'2', 'c':'3'}) 61 | expect(keys).toEqual(['a', 'b', 'c']) 62 | }) 63 | it('should return empty object if an empty object is passed in', () => { 64 | const keys = ['a', 'b', 'c'] 65 | const obj = {} 66 | expect(pick(keys, obj)).toEqual({}) 67 | expect(keys).toEqual(['a', 'b', 'c']) 68 | }) 69 | }) 70 | 71 | describe('uid', () => { 72 | it('should product a new uid', () => { 73 | expect(uid()).toContain('.') 74 | }) 75 | }) 76 | 77 | describe('filtering', () => { 78 | const commands = ['commands', 'morecommands'] 79 | it('should filter commands with regex', () => { 80 | expect(filtering(commands, '')).toBeTruthy() 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /packages/extension/test/components/Modals/NewBlockModal.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, render } from '@testing-library/react' 3 | import NewBlockModal from '../../../src/components/Modals/NewBlockModal' 4 | 5 | jest.mock('../../../src/actions/index.js', () => ({})) 6 | jest.mock('github-api', () => []) 7 | 8 | afterEach(() => { 9 | cleanup() 10 | jest.clearAllMocks() 11 | }) 12 | 13 | describe('NewBlockModal', () => { 14 | it('disables block importing on invalid blockShareConfig', () => { 15 | const { getByText } = render( 16 | 22 | ) 23 | 24 | getByText('Importing blocks from registry is disabled') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/extension/test/components/Modals/PlayLoopModal.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, render } from '@testing-library/react' 3 | import PlayLoopModal from '../../../src/components/Modals/PlayLoopModal.jsx' 4 | 5 | afterEach(() => { 6 | cleanup() 7 | jest.clearAllMocks() 8 | }) 9 | 10 | const getComponent = (props = {}) => { 11 | /* eslint-disable react/prop-types */ 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | describe('PlayLoopModal', () => { 25 | it('renders', () => { 26 | const { getByText } = render(getComponent()) 27 | getByText('How many loops to play?') 28 | getByText('Start value') 29 | getByText('Max') 30 | getByText(/The value of the loop counter is available.*/) 31 | getByText('Cancel') 32 | getByText('Play') 33 | }) 34 | 35 | // TODO more tests ... 36 | }) 37 | -------------------------------------------------------------------------------- /packages/extension/test/components/Modals/SettingModal.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, render } from '@testing-library/react' 3 | import SettingModal from '../../../src/components/Modals/SettingModal' 4 | 5 | afterEach(() => { 6 | cleanup() 7 | jest.clearAllMocks() 8 | }) 9 | 10 | describe('SettingModal', () => { 11 | it('renders', () => { 12 | const { getByText } = render( 13 | 19 | ) 20 | getByText('Replay Settings') 21 | getByText('Sidebar width') 22 | getByText('Replay Helper') 23 | getByText('Scroll elements into view during replay') 24 | getByText('Highlight elements during replay') 25 | getByText('Command Interval') 26 | getByText('Record Settings') 27 | getByText('Record notifications') 28 | getByText('Filesystem Scan Interval') 29 | getByText('Use css selector') 30 | getByText('CSS selector') 31 | getByText('Selector Ignore Patterns') 32 | getByText('Add Regex to ignore') 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /packages/extension/test/components/Project/FolderBrowser.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import { cleanup} from '@testing-library/react' 4 | import FolderBrowser from '../../../src/components/Project/FolderBrowser.jsx' 5 | 6 | beforeEach(() => { 7 | global.browser = {} 8 | }) 9 | 10 | afterEach(() => { 11 | cleanup() 12 | jest.clearAllMocks() 13 | }) 14 | 15 | const getComponent = (props = {}) => { 16 | /* eslint-disable react/prop-types */ 17 | return ( 18 | 27 | ) 28 | /* eslint-enable react/prop-types */ 29 | } 30 | 31 | describe('FolderBrowser', () => { 32 | it('renders', () => { 33 | const wrapper = shallow(getComponent()) 34 | expect(wrapper.find('Choose Folder')).not.toBeNull() 35 | expect(wrapper.find('Jump To')).not.toBeNull() 36 | expect(wrapper.find('No data')).not.toBeNull() 37 | expect(wrapper.find('Cancel')).not.toBeNull() 38 | expect(wrapper.find('Select')).not.toBeNull() 39 | expect(wrapper.find('Modal')).not.toBeNull() 40 | expect(wrapper.find('Button')).not.toBeNull() 41 | expect(wrapper.find('Icon')).not.toBeNull() 42 | expect(wrapper.find('Input')).not.toBeNull() 43 | expect(wrapper.find('Table')).not.toBeNull() 44 | }) 45 | 46 | // TODO more tests ... 47 | it('changeFolder', () => { 48 | const listDirectory = jest.fn() 49 | const folder = {} 50 | const wrapper = shallow(getComponent({folder, listDirectory})) 51 | const instance = wrapper.instance() 52 | instance.changeFolder(folder) 53 | expect(listDirectory).toHaveBeenCalledWith(folder) 54 | expect(wrapper.state('loading')).toEqual(true) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /packages/extension/test/components/Sidebar/Block.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, render } from '@testing-library/react' 3 | import { shallow } from 'enzyme' 4 | import Block from '../../../src/components/Sidebar/Block.jsx' 5 | 6 | afterEach(cleanup) 7 | 8 | describe('Block with testing-library', () => { 9 | it('renders with defaults', () => { 10 | const { container } = render() 11 | const span = container.querySelector('span') 12 | expect(span).not.toBeNull() 13 | expect(span.innerHTML).toEqual('TEST') 14 | expect(span.className).toEqual('block normal') 15 | }) 16 | 17 | it('renders with disabled', () => { 18 | const { container } = render() 19 | const span = container.querySelector('span') 20 | expect(span).not.toBeNull() 21 | expect(span.innerHTML).toEqual('TEST') 22 | expect(span.className).toEqual('block disabled normal') 23 | }) 24 | 25 | it('renders with custom status', () => { 26 | const { container } = render() 27 | const span = container.querySelector('span') 28 | expect(span).not.toBeNull() 29 | expect(span.innerHTML).toEqual('TEST') 30 | expect(span.className).toEqual('block status') 31 | }) 32 | }) 33 | 34 | describe('Block with enzyme', () => { 35 | it('renders with defaults', () => { 36 | const wrapper = shallow() 37 | const span = wrapper.find('span') 38 | expect(span).not.toBeNull() 39 | expect(span.text()).toEqual('TEST') 40 | expect(span.hasClass('block')).toBe(true) 41 | expect(span.hasClass('disabled')).toBe(false) 42 | expect(span.hasClass('normal')).toBe(true) 43 | }) 44 | 45 | it('renders with disabled', () => { 46 | const wrapper = shallow() 47 | const span = wrapper.find('span') 48 | expect(span).not.toBeNull() 49 | expect(span.text()).toEqual('TEST') 50 | expect(span.hasClass('block')).toBe(true) 51 | expect(span.hasClass('disabled')).toBe(true) 52 | expect(span.hasClass('normal')).toBe(true) 53 | }) 54 | 55 | it('renders with custom status', () => { 56 | const wrapper = shallow() 57 | const span = wrapper.find('span') 58 | expect(span).not.toBeNull() 59 | expect(span.text()).toEqual('TEST') 60 | expect(span.hasClass('block')).toBe(true) 61 | expect(span.hasClass('disabled')).toBe(false) 62 | expect(span.hasClass('normal')).toBe(false) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /packages/extension/test/components/Sidebar/Suite.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, render } from '@testing-library/react' 3 | import Suite from '../../../src/components/Sidebar/Suite.jsx' 4 | 5 | afterEach(cleanup) 6 | 7 | describe('Suite', () => { 8 | it('renders with defaults', () => { 9 | const { container } = render() 10 | const span = container.querySelector('span') 11 | expect(span).not.toBeNull() 12 | expect(span.innerHTML).toEqual('TEST') 13 | expect(span.className).toEqual('block normal') 14 | }) 15 | 16 | it('renders with disabled', () => { 17 | const { container } = render() 18 | const span = container.querySelector('span') 19 | expect(span).not.toBeNull() 20 | expect(span.innerHTML).toEqual('TEST') 21 | expect(span.className).toEqual('block disabled normal') 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /packages/extension/test/components/Sidebar/Testcase.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, render } from '@testing-library/react' 3 | import Testcase from '../../../src/components/Sidebar/Testcase.jsx' 4 | 5 | afterEach(cleanup) 6 | 7 | const trimUp = s => { 8 | return s.replace('\n', '').replace(/[ ]{2,}/g, ' ') 9 | } 10 | 11 | describe('Testcase', () => { 12 | it('renders with defaults', () => { 13 | const { container } = render() 14 | const span = container.querySelector('span') 15 | expect(span).not.toBeNull() 16 | expect(span.innerHTML).toEqual('TEST') 17 | expect(trimUp(span.className)).toEqual('testcase normal') 18 | }) 19 | 20 | it('renders with disabled', () => { 21 | const { container } = render() 22 | const span = container.querySelector('span') 23 | expect(span).not.toBeNull() 24 | expect(span.innerHTML).toEqual('TEST') 25 | expect(trimUp(span.className)).toEqual('testcase disabled normal') 26 | }) 27 | 28 | it('renders with custom status', () => { 29 | const { container } = render( 30 | 31 | ) 32 | const span = container.querySelector('span') 33 | expect(span).not.toBeNull() 34 | expect(span.innerHTML).toEqual('TEST') 35 | expect(trimUp(span.className)).toEqual('testcase disabled status') 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /packages/extension/test/components/dashboard/CommandDoc.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, fireEvent, render } from '@testing-library/react' 3 | import CommandDoc from '../../../src/components/dashboard/CommandDoc.jsx' 4 | 5 | jest.mock('../../../src/common/commands', () => { 6 | return [ 7 | { name: 'command_ccc' }, 8 | { name: 'command_aaa' }, 9 | { name: 'command_aaa' }, 10 | { name: 'command_bbb' } 11 | ] 12 | }) 13 | 14 | afterEach(() => { 15 | cleanup() 16 | jest.clearAllMocks() 17 | }) 18 | 19 | describe('CommandDoc', () => { 20 | it('renders', () => { 21 | const { getByText } = render() 22 | getByText('Cmd') 23 | getByText('Description') 24 | getByText('Comment') 25 | getByText('command_ccc') 26 | }) 27 | 28 | it('sorts commands', () => { 29 | const { queryAllByText } = render() 30 | const names = queryAllByText(/command_\w+/) 31 | expect(names.length).toBe(4) 32 | expect(names[0].innerHTML.endsWith('command_aaa')).toBe(true) 33 | expect(names[1].innerHTML.endsWith('command_aaa')).toBe(true) 34 | expect(names[2].innerHTML.endsWith('command_bbb')).toBe(true) 35 | expect(names[3].innerHTML.endsWith('command_ccc')).toBe(true) 36 | }) 37 | 38 | it.skip('allows searching for commands', async () => { 39 | const { queryAllByText, getByPlaceholderText } = render() 40 | const input = getByPlaceholderText('Search Command') 41 | const unfilteredLength = queryAllByText(/command_\w+/).length 42 | // TODO this event isn't triggering the filter 43 | fireEvent.change(input, { target: { value: 'a' } }) 44 | const filteredLength = queryAllByText(/command_\w+/).length 45 | expect(unfilteredLength > filteredLength).toBe(true) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /packages/extension/test/components/dashboard/CommandOptions.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, render } from '@testing-library/react' 3 | import CommandOptions from '../../../src/components/dashboard/CommandOptions.jsx' 4 | 5 | jest.mock('../../../src/common/ipc/ipc_cs.js', () => {}) 6 | jest.mock('../../../src/actions/index.js', () => {}) 7 | jest.mock('../../../src/containers/dashboard/fields/CommandField.js', () => { 8 | // eslint-disable-next-line react/display-name 9 | return function() { 10 | return

CommandField component

11 | } 12 | }) 13 | 14 | afterEach(() => { 15 | cleanup() 16 | jest.clearAllMocks() 17 | }) 18 | 19 | const getComponent = (props = {}) => { 20 | /* eslint-disable react/prop-types */ 21 | return ( 22 | 32 | ) 33 | /* eslint-enable react/prop-types */ 34 | } 35 | 36 | describe('CommandOptions', () => { 37 | it('renders', () => { 38 | const { getByText, container } = render(getComponent()) 39 | getByText('Command') 40 | getByText('command') 41 | getByText('CommandField component') 42 | getByText('?') 43 | expect(container.querySelector('.ant-select')).not.toBeNull() 44 | }) 45 | 46 | it('displays available commands', () => { 47 | // ... 48 | }) 49 | 50 | it('allows searching for commands', () => { 51 | // ... 52 | }) 53 | 54 | it('shows the modal if the ? is clicked', () => { 55 | // ... 56 | }) 57 | 58 | it('can close the modal', () => { 59 | // ... 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /packages/extension/test/components/dashboard/CommandTable.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, render } from '@testing-library/react' 3 | import CommandTable from '../../../src/components/dashboard/CommandTable.jsx' 4 | import { DragDropContext } from 'react-dnd' 5 | import HTML5Backend from 'react-dnd-html5-backend' 6 | 7 | jest.mock('../../../src/actions/index.js', () => ({})) 8 | jest.mock('../../../src/containers/dashboard/CommandOptions', () => { 9 | // eslint-disable-next-line react/display-name 10 | return function() { 11 | return

CommandOptions

12 | } 13 | }) 14 | 15 | afterEach(() => { 16 | cleanup() 17 | jest.clearAllMocks() 18 | }) 19 | 20 | const getComponent = (props = {}) => { 21 | /* eslint-disable react/prop-types */ 22 | const component = ( 23 | 45 | ) 46 | return DragDropContext(HTML5Backend)(component) 47 | } 48 | 49 | describe('CommandTable', () => { 50 | it('renders', () => { 51 | render(getComponent()) 52 | }) 53 | 54 | // TODO more tests ... 55 | }) 56 | -------------------------------------------------------------------------------- /packages/extension/test/components/dashboard/DashboardEditor.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, render } from '@testing-library/react' 3 | import DashboardEditor from '../../../src/components/dashboard/DashboardEditor.jsx' 4 | 5 | jest.mock('../../../src/actions/index.js', () => {}) 6 | jest.mock('../../../src/containers/dashboard/CommandTable', () => { 7 | // eslint-disable-next-line react/display-name 8 | return () => { 9 | return

CommandTable

10 | } 11 | }) 12 | jest.mock('../../../src/containers/dashboard/SuiteEditor', () => { 13 | // eslint-disable-next-line react/display-name 14 | return () => { 15 | return

SuiteEditor

16 | } 17 | }) 18 | 19 | afterEach(() => { 20 | cleanup() 21 | jest.clearAllMocks() 22 | }) 23 | 24 | const getComponent = (props = {}) => { 25 | /* eslint-disable react/prop-types */ 26 | return ( 27 | 47 | ) 48 | /* eslint-enable react/prop-types */ 49 | } 50 | 51 | describe('DashboardEditor', () => { 52 | it('renders', () => { 53 | const { container, getByText, queryByText } = render(getComponent()) 54 | expect(queryByText('SuiteEditor')).toBeNull() 55 | expect(container.querySelector('.ant-tabs')).not.toBeNull() 56 | getByText('Table View') 57 | getByText('Source View (JSON)') 58 | getByText('CommandTable') 59 | getByText('Cut') 60 | getByText('Copy') 61 | getByText('Paste') 62 | getByText('Insert new line') 63 | getByText('Execute this command') 64 | getByText('Run from here') 65 | getByText('Save As New Block') 66 | }) 67 | 68 | // TODO more tests ... 69 | }) 70 | -------------------------------------------------------------------------------- /packages/extension/test/components/dashboard/PauseButton.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, fireEvent, render } from '@testing-library/react' 3 | import PauseButton from '../../../src/components/dashboard/PauseButton.jsx' 4 | import { getPlayer } from '../../../src/common/player' 5 | 6 | jest.mock('../../../src/common/player', () => { 7 | return { 8 | getPlayer: jest.fn().mockImplementation(() => { 9 | return { 10 | pause: jest.fn() 11 | } 12 | }) 13 | } 14 | }) 15 | 16 | afterEach(() => { 17 | cleanup() 18 | jest.clearAllMocks() 19 | }) 20 | 21 | describe('PauseButton', () => { 22 | it('renders with defaults', () => { 23 | const { container } = render() 24 | expect(container.querySelector('button')).not.toBeNull() 25 | expect(container.querySelector('i.anticon.anticon-pause')).not.toBeNull() 26 | }) 27 | 28 | it('has a button that calls into the player', () => { 29 | const { container } = render() 30 | const button = container.querySelector('button') 31 | fireEvent.click(button) 32 | expect(getPlayer).toHaveBeenCalled() 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /packages/extension/test/components/dashboard/PlayMenu.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, fireEvent, render } from '@testing-library/react' 3 | import PlayMenu from '../../../src/components/dashboard/PlayMenu.jsx' 4 | import * as C from '../../../src/common/constant' 5 | 6 | afterEach(cleanup) 7 | 8 | describe('PlayMenu', () => { 9 | it('renders with defaults', () => { 10 | const { getByText } = render( {}} />) 11 | expect( 12 | getByText('Play loop..').classList.contains('ant-menu-item-disabled') 13 | ).toBe(true) 14 | }) 15 | 16 | it('can be enabled', () => { 17 | const { getByText } = render( 18 | {}} 20 | status={C.PLAYER_STATUS.STOPPED} 21 | /> 22 | ) 23 | expect( 24 | getByText('Play loop..').classList.contains('ant-menu-item-disabled') 25 | ).toBe(false) 26 | }) 27 | 28 | it('calls togglePlayLoopsModal', () => { 29 | const togglePlayLoopsModal = jest.fn() 30 | const { container } = render( 31 | 35 | ) 36 | fireEvent.click(container.querySelector('li')) 37 | expect(togglePlayLoopsModal).toHaveBeenCalled() 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /packages/extension/test/components/dashboard/PlaybackControl.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, render } from '@testing-library/react' 3 | import PlaybackControl from '../../../src/components/dashboard/PlaybackControl.jsx' 4 | import * as C from '../../../src/common/constant' 5 | 6 | jest.mock('../../../src/actions/index.js', () => ({})) 7 | jest.mock('../../../src/containers/dashboard/PlayButton', () => { 8 | // eslint-disable-next-line react/display-name 9 | return function() { 10 | return

Play Button

11 | } 12 | }) 13 | 14 | afterEach(() => { 15 | cleanup() 16 | jest.clearAllMocks() 17 | }) 18 | 19 | describe('PlaybackControl', () => { 20 | it('shows right buttons when playing', () => { 21 | // stop button with stopped=false and pause button 22 | const { container } = render( 23 | 24 | ) 25 | expect( 26 | container.querySelector('i.anticon.anticon-right-square') 27 | ).not.toBeNull() 28 | expect(container.querySelector('i.anticon.anticon-pause')).not.toBeNull() 29 | }) 30 | 31 | it('shows right buttons when paused', () => { 32 | // stop button with stopped=false and resume button 33 | const { container } = render( 34 | 35 | ) 36 | expect( 37 | container.querySelector('i.anticon.anticon-right-square') 38 | ).not.toBeNull() 39 | expect( 40 | container.querySelector('i.anticon.anticon-caret-right') 41 | ).not.toBeNull() 42 | }) 43 | 44 | it('shows right buttons otherwise, stopped', () => { 45 | const { getByText } = render( 46 | 47 | ) 48 | getByText('Play Button') 49 | }) 50 | 51 | it('shows right buttons otherwise, default', () => { 52 | const { container } = render() 53 | expect( 54 | container.querySelector('i.anticon.anticon-right-square') 55 | ).not.toBeNull() 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /packages/extension/test/components/dashboard/ResumeButton.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, fireEvent, render } from '@testing-library/react' 3 | import ResumeButton from '../../../src/components/dashboard/ResumeButton.jsx' 4 | import { getPlayer } from '../../../src/common/player' 5 | 6 | jest.mock('../../../src/actions/index.js', () => ({})) 7 | jest.mock('../../../src/common/player', () => { 8 | return { 9 | getPlayer: jest.fn().mockImplementation(() => { 10 | return { resume: jest.fn() } 11 | }) 12 | } 13 | }) 14 | 15 | afterEach(() => { 16 | cleanup() 17 | jest.clearAllMocks() 18 | }) 19 | 20 | describe('ResumeButton', () => { 21 | it('renders', () => { 22 | render() 23 | }) 24 | 25 | it('has button onClick', () => { 26 | const { container } = render() 27 | fireEvent.click(container.querySelector('button')) 28 | expect(getPlayer).toHaveBeenCalled() 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /packages/extension/test/components/dashboard/StopButton.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, fireEvent, render } from '@testing-library/react' 3 | import StopButton from '../../../src/components/dashboard/StopButton.jsx' 4 | import { getPlayer } from '../../../src/common/player' 5 | 6 | jest.mock('../../../src/common/player', () => { 7 | return { 8 | getPlayer: jest.fn().mockImplementation(() => { 9 | return { stop: jest.fn() } 10 | }) 11 | } 12 | }) 13 | 14 | afterEach(() => { 15 | cleanup() 16 | jest.clearAllMocks() 17 | }) 18 | 19 | describe('StopButton', () => { 20 | it('renders with defaults', () => { 21 | const { container } = render() 22 | const button = container.querySelector('button') 23 | expect(Object.values(button.classList).indexOf('ant-btn')).not.toBe(-1) 24 | expect(button.disabled).toBe(false) 25 | }) 26 | 27 | it('can be disabled', () => { 28 | const { container } = render() 29 | const button = container.querySelector('button') 30 | expect(button.disabled).toBe(true) 31 | }) 32 | 33 | it('has onClick listener', () => { 34 | const { container } = render() 35 | fireEvent.click(container.querySelector('button')) 36 | expect(getPlayer).toHaveBeenCalled() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/extension/test/components/dashboard/TestcaseDropdown.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, render } from '@testing-library/react' 3 | import Dropdown from '../../../src/components/dashboard/TestcaseDropdown.jsx' 4 | 5 | afterEach(() => { 6 | cleanup() 7 | jest.clearAllMocks() 8 | }) 9 | 10 | const getComponent = (props = {}) => { 11 | /* eslint-disable react/prop-types */ 12 | return ( 13 | 23 | ) 24 | } 25 | 26 | describe('Dropdown', () => { 27 | it('renders nothing if status is not set', () => { 28 | try { 29 | render(getComponent()) 30 | expect(true).toBe(false) 31 | } catch (err) { 32 | // expected 33 | } 34 | }) 35 | 36 | it('renders a testcasedropdown if set', () => { 37 | const { getByText } = render(getComponent({ editorStatus: 'TESTS' })) 38 | getByText('Duplicate..') 39 | getByText('Rename') 40 | getByText('Delete') 41 | getByText('Share') 42 | getByText('Replay settings..') 43 | }) 44 | 45 | // TODO more tests ... 46 | }) 47 | -------------------------------------------------------------------------------- /packages/extension/test/components/dashboard/fields/CommandField.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, render } from '@testing-library/react' 3 | import CommandField from '../../../../src/components/dashboard/fields/CommandField.jsx' 4 | 5 | jest.mock( 6 | '../../../../src/components/dashboard/fields/EnvironmentField', 7 | () => { 8 | // eslint-disable-next-line react/display-name 9 | return function() { 10 | return

EnvironmentField

11 | } 12 | } 13 | ) 14 | jest.mock('../../../../src/components/dashboard/fields/OneField', () => { 15 | // eslint-disable-next-line react/display-name 16 | return function() { 17 | return

OneField

18 | } 19 | }) 20 | 21 | afterEach(() => { 22 | cleanup() 23 | jest.clearAllMocks() 24 | }) 25 | 26 | describe('CommandField', () => { 27 | it('renders', () => { 28 | const { queryByText } = render() 29 | expect(queryByText('OneField')).not.toBeNull() 30 | expect(queryByText('EnvironmentField')).toBeNull() 31 | }) 32 | 33 | it('shows the environment field when appropriate', () => { 34 | const { queryByText } = render( 35 | 36 | ) 37 | expect(queryByText('OneField')).toBeNull() 38 | expect(queryByText('EnvironmentField')).not.toBeNull() 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /packages/extension/test/components/dashboard/fields/EnvironmentField.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cleanup, fireEvent, render } from '@testing-library/react' 3 | import EnvironmentField from '../../../../src/components/dashboard/fields/EnvironmentField.jsx' 4 | 5 | afterEach(() => { 6 | cleanup() 7 | jest.clearAllMocks() 8 | }) 9 | 10 | const getComponent = (props = {}) => ( 11 | 16 | ) 17 | 18 | describe('EnvironmentField', () => { 19 | it('renders', () => { 20 | const { container, getByText } = render(getComponent()) 21 | expect(getByText('Environment')).not.toBeNull() 22 | expect(container.querySelector('input')).not.toBeNull() 23 | }) 24 | 25 | it('sets input value', () => { 26 | const selectedCmd = { 27 | parameters: { 28 | key1: 'param1' 29 | } 30 | } 31 | const { container } = render(getComponent({selectedCmd})) 32 | expect(container.querySelector('input').value).toEqual('key1=param1') 33 | }) 34 | 35 | it('sets multiple input values', () => { 36 | const selectedCmd = { 37 | parameters: { 38 | key1: 'param1', 39 | key2: 'param2', 40 | key3: 'param3' 41 | } 42 | } 43 | const { container } = render(getComponent({selectedCmd})) 44 | const expectedOutput = 'key1=param1,key2=param2,key3=param3' 45 | expect(container.querySelector('input').value).toEqual(expectedOutput) 46 | }) 47 | 48 | it('is editable', () => { 49 | const { container } = render(getComponent({ isCmdEditable: true })) 50 | expect(container.querySelector('input').disabled).toEqual(false) 51 | }) 52 | 53 | it('handle input change', () => { 54 | const { getByPlaceholderText } = render(getComponent({ isCmdEditable: true })) 55 | const input = getByPlaceholderText('KEY1=VALUE,KEY2=VALUE...') 56 | fireEvent.change(input, { target: { value: 'key1=param1' } }) 57 | expect(input.value).toEqual('key1=param1') 58 | }) 59 | 60 | // TODO more tests ... 61 | }) 62 | -------------------------------------------------------------------------------- /packages/extension/test/containers/Header.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/Modals/DuplicateModal.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/Modals/NewBlockModal.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/Modals/PlayLoopModal.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/Modals/RenameModal.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/Modals/SaveModal.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/Modals/SaveMultiSelectModal.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/Modals/SettingModal.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/Modals/ShareBlockModal.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/Project/FolderBrowser.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../src/actions', () => ({ 2 | listDirectory: jest.fn().mockImplementation(() => 1), 3 | changeModalState: jest.fn().mockImplementation(() => 2), 4 | selectProjectFolder: jest.fn().mockImplementation(() => 3) 5 | })) 6 | 7 | afterEach(() => { 8 | jest.clearAllMocks() 9 | }) 10 | 11 | describe('FolderBrowser store', () => { 12 | it('operates', () => { 13 | const mockConnect = jest.fn().mockImplementation(() => component => {}) 14 | jest.doMock('react-redux', () => ({ 15 | connect: mockConnect 16 | })) 17 | require('../../../src/containers/Project/FolderBrowser') 18 | const [mapStateToProps, mapDispatchToProps] = mockConnect.mock.calls[0] 19 | 20 | const state = { 21 | files: { 22 | folder: 'a', 23 | error: null, 24 | contents: 'b' 25 | }, 26 | modals: { 27 | browser: 'c' 28 | }, 29 | projectSetup: { 30 | projectPath: 'a' 31 | } 32 | } 33 | let result = mapStateToProps(state) 34 | expect(result).toEqual({ 35 | folder: 'a', 36 | error: null, 37 | contents: 'b', 38 | visible: 'c', 39 | top: true 40 | }) 41 | 42 | const mockDispatch = jest.fn().mockImplementation(() => true) 43 | result = mapDispatchToProps(mockDispatch) 44 | result.listDirectory('d') 45 | result.closeModal() 46 | result.selectFolder() 47 | 48 | expect(mockDispatch).toHaveBeenCalledTimes(3) 49 | expect(mockDispatch).toHaveBeenNthCalledWith(1, 1) 50 | expect(mockDispatch).toHaveBeenNthCalledWith(2, 2) 51 | expect(mockDispatch).toHaveBeenNthCalledWith(3, 3) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /packages/extension/test/containers/Project/ProjectModal.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../src/actions', () => ({ 2 | updateProject: jest.fn().mockImplementation(() => 1), 3 | clearProjectSetup: jest.fn().mockImplementation(() => 2), 4 | changeModalState: jest.fn().mockImplementation(() => 3), 5 | setPathPurpose: jest.fn().mockImplementation(() => 4), 6 | createProject: jest.fn().mockImplementation(() => 5), 7 | checkForExistingConfig: jest.fn().mockImplementation(() => 6) 8 | })) 9 | 10 | afterEach(() => { 11 | jest.clearAllMocks() 12 | }) 13 | 14 | describe('ProjectModal store', () => { 15 | it('operates', () => { 16 | const mockConnect = jest.fn().mockImplementation(() => component => {}) 17 | jest.doMock('react-redux', () => ({ 18 | connect: mockConnect 19 | })) 20 | require('../../../src/containers/Project/ProjectModal') 21 | const [mapStateToProps, mapDispatchToProps] = mockConnect.mock.calls[0] 22 | 23 | const state = { 24 | modals: { 25 | projectSetup: 'a' 26 | }, 27 | projectSetup: { 28 | testPath: 'b', 29 | blockPath: 'c', 30 | suites: ['d'], 31 | projectPath: 'e', 32 | existingConfig: true, 33 | id: 'f', 34 | name: 'g' 35 | }, 36 | editor: { 37 | projects: ['h'] 38 | } 39 | } 40 | let result = mapStateToProps(state) 41 | expect(result).toEqual({ 42 | visible: 'a', 43 | testPath: 'b', 44 | blockPath: 'c', 45 | suites: ['d'], 46 | projectPath: 'e', 47 | existingConfig: true, 48 | firstTime: false, 49 | id: 'f', 50 | name: 'g' 51 | }) 52 | 53 | const mockDispatch = jest.fn().mockImplementation(() => true) 54 | result = mapDispatchToProps(mockDispatch) 55 | result.createProject() 56 | result.updateProject() 57 | result.clearProjectSetup() 58 | result.closeModal() 59 | result.checkForExistingConfig() 60 | result.browseProject() 61 | result.browseTest() 62 | result.browseBlock() 63 | 64 | expect(mockDispatch).toHaveBeenCalledTimes(12) 65 | expect(mockDispatch).toHaveBeenNthCalledWith(1, 5) 66 | expect(mockDispatch).toHaveBeenNthCalledWith(2, 1) 67 | expect(mockDispatch).toHaveBeenNthCalledWith(3, 2) 68 | expect(mockDispatch).toHaveBeenNthCalledWith(4, 3) 69 | expect(mockDispatch).toHaveBeenNthCalledWith(5, { 70 | type: 'PROJECT_PATH', 71 | path: undefined 72 | }) 73 | expect(mockDispatch).toHaveBeenNthCalledWith(6, 6) 74 | expect(mockDispatch).toHaveBeenNthCalledWith(7, 4) 75 | expect(mockDispatch).toHaveBeenNthCalledWith(8, 3) 76 | expect(mockDispatch).toHaveBeenNthCalledWith(9, 4) 77 | expect(mockDispatch).toHaveBeenNthCalledWith(10, 3) 78 | expect(mockDispatch).toHaveBeenNthCalledWith(11, 4) 79 | expect(mockDispatch).toHaveBeenNthCalledWith(12, 3) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /packages/extension/test/containers/Sidebar.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/dashboard/CommandOptions.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | jest.mock('../../../src/actions', () => ({ 4 | updateSelectedCommand: jest.fn().mockImplementation(() => 1), 5 | startInspecting: jest.fn().mockImplementation(() => 2), 6 | stopInspecting: jest.fn().mockImplementation(() => 3), 7 | setInspectTarget: jest.fn().mockImplementation(() => 4) 8 | })) 9 | 10 | jest.mock('../../../src/common/ipc/ipc_cs', () => {}) 11 | 12 | // eslint-disable-next-line react/display-name 13 | jest.mock('../../../src/components/dashboard/CommandOptions', () => () => ( 14 |

CommandOptions

15 | )) 16 | 17 | afterEach(() => { 18 | jest.clearAllMocks() 19 | }) 20 | 21 | describe('CommandButtons store', () => { 22 | it('operates', () => { 23 | const mockConnect = jest.fn().mockImplementation(() => component => {}) 24 | jest.doMock('react-redux', () => ({ 25 | connect: mockConnect 26 | })) 27 | require('../../../src/containers/dashboard/CommandOptions') 28 | const [mapStateToProps, mapDispatchToProps] = mockConnect.mock.calls[0] 29 | 30 | const state = { 31 | app: { 32 | status: 'a', 33 | config: 'b' 34 | }, 35 | editor: { 36 | editing: { 37 | meta: { 38 | selectedIndex: 0 39 | } 40 | }, 41 | clipboard: 'd', 42 | selectedCmds: 'e' 43 | }, 44 | player: 'f' 45 | } 46 | let result = mapStateToProps(state) 47 | expect(result).toEqual({ 48 | status: 'a', 49 | editor: { 50 | editing: { meta: expect.any(Object) }, 51 | clipboard: 'd', 52 | selectedCmds: 'e' 53 | }, 54 | editing: { meta: { selectedIndex: 0 } }, 55 | clipboard: 'd', 56 | player: 'f', 57 | config: 'b', 58 | selectedCmds: 'e', 59 | selectedCommand: 0 60 | }) 61 | 62 | const mockDispatch = jest.fn().mockImplementation(() => true) 63 | result = mapDispatchToProps(mockDispatch) 64 | result.updateSelectedCommand() 65 | result.startInspecting() 66 | result.stopInspecting() 67 | result.setInspectTarget() 68 | 69 | expect(mockDispatch).toHaveBeenCalledTimes(4) 70 | expect(mockDispatch).toHaveBeenNthCalledWith(1, 1) 71 | expect(mockDispatch).toHaveBeenNthCalledWith(2, 2) 72 | expect(mockDispatch).toHaveBeenNthCalledWith(3, 3) 73 | expect(mockDispatch).toHaveBeenNthCalledWith(4, 4) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /packages/extension/test/containers/dashboard/DashboardBottom.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/dashboard/DashboardEditor.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/dashboard/PlayButton.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/dashboard/PlayMenu.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/dashboard/SuiteEditor.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/dashboard/TestcaseDropdown.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/dashboard/fields.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/containers/dashboard/fields/CommandButtons.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../../src/actions', () => ({ 2 | updateSelectedCommand: jest.fn().mockImplementation(() => 1) 3 | })) 4 | 5 | jest.mock('../../../../src/common/ipc/ipc_cs', () => {}) 6 | 7 | afterEach(() => { 8 | jest.clearAllMocks() 9 | }) 10 | 11 | describe('CommandButtons store', () => { 12 | it('operates', () => { 13 | const mockConnect = jest.fn().mockImplementation(() => component => {}) 14 | jest.doMock('react-redux', () => ({ 15 | connect: mockConnect 16 | })) 17 | require('../../../../src/containers/dashboard/fields/CommandButtons') 18 | const [mapStateToProps, mapDispatchToProps] = mockConnect.mock.calls[0] 19 | 20 | const state = { 21 | editor: { 22 | editing: { 23 | meta: { 24 | selectedIndex: 0 25 | }, 26 | commands: ['a'], 27 | filterCommands: [0] 28 | } 29 | } 30 | } 31 | let result = mapStateToProps(state) 32 | expect(result).toEqual({ 33 | selectedCmd: 'a' 34 | }) 35 | 36 | const mockDispatch = jest.fn().mockImplementation(() => true) 37 | result = mapDispatchToProps(mockDispatch) 38 | result.updateSelectedCommand({}) 39 | 40 | expect(mockDispatch).toHaveBeenCalledTimes(1) 41 | expect(mockDispatch).toHaveBeenCalledWith(1) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /packages/extension/test/containers/dashboard/fields/CommandField.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../../src/actions', () => ({ 2 | updateSelectedCommand: jest.fn().mockImplementation(() => 1), 3 | startInspecting: jest.fn().mockImplementation(() => 2), 4 | stopInspecting: jest.fn().mockImplementation(() => 3), 5 | setInspectTarget: jest.fn().mockImplementation(() => 4) 6 | })) 7 | 8 | jest.mock('../../../../src/common/ipc/ipc_cs', () => {}) 9 | 10 | afterEach(() => { 11 | jest.clearAllMocks() 12 | }) 13 | 14 | describe('CommandField store', () => { 15 | it('operates', () => { 16 | const mockConnect = jest.fn().mockImplementation(() => component => {}) 17 | jest.doMock('react-redux', () => ({ 18 | connect: mockConnect 19 | })) 20 | require('../../../../src/containers/dashboard/fields/CommandField') 21 | const [mapStateToProps, mapDispatchToProps] = mockConnect.mock.calls[0] 22 | 23 | const state = { 24 | editor: { 25 | editing: { 26 | meta: { 27 | selectedIndex: 0 28 | }, 29 | filterCommands: ['a'], 30 | commands: ['b'] 31 | }, 32 | blocks: ['c'], 33 | inspectTarget: 'd' 34 | }, 35 | player: { 36 | status: 'RUNNING' 37 | }, 38 | app: { 39 | status: 'INSPECTOR' 40 | } 41 | } 42 | let result = mapStateToProps(state) 43 | expect(result).toEqual({ 44 | selectedCmd: 'a', 45 | commands: ['b'], 46 | isCmdEditable: false, 47 | editing: { 48 | meta: { 49 | selectedIndex: 0 50 | }, 51 | filterCommands: ['a'], 52 | commands: ['b'] 53 | }, 54 | filterCommands: ['a'], 55 | blocks: ['c'], 56 | selectedIndex: 0, 57 | isInspecting: true, 58 | inspectTarget: 'd' 59 | }) 60 | 61 | const mockDispatch = jest.fn().mockImplementation(() => true) 62 | result = mapDispatchToProps(mockDispatch) 63 | result.updateSelectedCommand({}, {}) 64 | result.startInspecting() 65 | result.stopInspecting() 66 | result.setInspectTarget('t') 67 | 68 | expect(mockDispatch).toHaveBeenCalledTimes(4) 69 | expect(mockDispatch).toHaveBeenNthCalledWith(1, 1) 70 | expect(mockDispatch).toHaveBeenNthCalledWith(2, 2) 71 | expect(mockDispatch).toHaveBeenNthCalledWith(3, 3) 72 | expect(mockDispatch).toHaveBeenNthCalledWith(4, 4) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /packages/extension/test/containers/dashboard/index.test.js: -------------------------------------------------------------------------------- 1 | describe.skip('TODO') 2 | -------------------------------------------------------------------------------- /packages/extension/test/reducers/reducers.dropdowns.test.js: -------------------------------------------------------------------------------- 1 | import reducer, { initialState } from '../../src/reducers/dropdowns' 2 | 3 | import { types } from '../../src/actions/action_types' 4 | 5 | describe('dropdowns reducer', () => { 6 | it('should do nothing if invalid action', () => { 7 | const action = { 8 | type: 'INVALID' 9 | } 10 | const expected = Object.assign({}, initialState) 11 | expect(reducer(initialState, action)).toEqual(expected) 12 | }) 13 | it('should close testcase dropdown', () => { 14 | const action = { 15 | type: types.DROPDOWN_STATE, 16 | dropdown: 'testcase', 17 | state: true 18 | } 19 | const expected = Object.assign({}, initialState, { 20 | testcase: true 21 | }) 22 | expect(reducer(initialState, action)).toEqual(expected) 23 | }) 24 | it('should open testcase dropdown', () => { 25 | const init = Object.assign({}, initialState, { testcase: true }) 26 | const action = { 27 | type: types.DROPDOWN_STATE, 28 | dropdown: 'testcase', 29 | state: false 30 | } 31 | const expected = Object.assign({}, init, { 32 | testcase: false 33 | }) 34 | expect(reducer(init, action)).toEqual(expected) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /packages/extension/test/reducers/reducers.files.test.js: -------------------------------------------------------------------------------- 1 | import reducer, { initialState } from '../../src/reducers/files' 2 | 3 | import { types } from '../../src/actions/action_types' 4 | 5 | describe('files reducer', () => { 6 | it('should do nothing if invalid action', () => { 7 | const action = { 8 | type: 'INVALID' 9 | } 10 | const expected = Object.assign({}, initialState) 11 | expect(reducer(initialState, action)).toEqual(expected) 12 | }) 13 | it('should set error state', () => { 14 | const action = { 15 | type: types.FILE_ERROR 16 | } 17 | const expected = Object.assign({}, initialState, { error: true }) 18 | expect(reducer(initialState, action)).toEqual(expected) 19 | }) 20 | it('should open file modal', () => { 21 | const action = { 22 | type: types.FOLDER_MODAL, 23 | open: true 24 | } 25 | const expected = Object.assign({}, initialState, { 26 | modalVisible: true 27 | }) 28 | expect(reducer(initialState, action)).toEqual(expected) 29 | }) 30 | it('should close file modal', () => { 31 | const init = Object.assign({}, initialState, { modalVisible: true }) 32 | const action = { 33 | type: types.FOLDER_MODAL, 34 | open: false 35 | } 36 | const expected = Object.assign({}, init, { 37 | modalVisible: false 38 | }) 39 | expect(reducer(init, action)).toEqual(expected) 40 | }) 41 | it('should select folder', () => { 42 | const action = { 43 | type: types.SELECT_FOLDER, 44 | folder: '~/', 45 | contents: ['testfile'] 46 | } 47 | const expected = Object.assign({}, initialState, { 48 | folder: '~/', 49 | contents: ['testfile'] 50 | }) 51 | expect(reducer(initialState, action)).toEqual(expected) 52 | }) 53 | it('should get folder contents', () => { 54 | const action = { 55 | type: types.FOLDER_CONTENTS, 56 | folder: '~/', 57 | files: ['testfile'] 58 | } 59 | const expected = Object.assign({}, initialState, { 60 | activeFolder: '~/', 61 | activeContents: ['testfile'] 62 | }) 63 | expect(reducer(initialState, action)).toEqual(expected) 64 | }) 65 | it('should list folders', () => { 66 | const action = { 67 | type: types.SET_FOLDERS, 68 | folderList: ['folder1'] 69 | } 70 | const expected = Object.assign({}, initialState, { 71 | modalVisible: false, 72 | folderList: ['folder1'] 73 | }) 74 | expect(reducer(initialState, action)).toEqual(expected) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /packages/extension/test/reducers/reducers.modals.test.js: -------------------------------------------------------------------------------- 1 | import reducer, { initialState } from '../../src/reducers/modals' 2 | 3 | import { types } from '../../src/actions/action_types' 4 | 5 | describe('modals reducer', () => { 6 | ;[ 7 | 'playLoop', 8 | 'settings', 9 | 'duplicate', 10 | 'shareBlock', 11 | 'rename', 12 | 'projectSetup', 13 | 'browser', 14 | 'multiselect', 15 | 'save' 16 | ].forEach(m => { 17 | it(`should open ${m} modal`, () => { 18 | const action = { 19 | type: types.MODAL_STATE, 20 | modal: m, 21 | state: true 22 | } 23 | const expected = Object.assign({}, initialState, { 24 | [m]: true 25 | }) 26 | expect(reducer(initialState, action)).toEqual(expected) 27 | }) 28 | it(`should close ${m} modal`, () => { 29 | const action = { 30 | type: types.MODAL_STATE, 31 | modal: m, 32 | state: false 33 | } 34 | const expected = Object.assign({}, initialState, { 35 | [m]: false 36 | }) 37 | expect(reducer(initialState, action)).toEqual(expected) 38 | }) 39 | }) 40 | it('should do nothing if invalid action', () => { 41 | const action = { 42 | type: 'INVALID' 43 | } 44 | const expected = Object.assign({}, initialState) 45 | expect(reducer(initialState, action)).toEqual(expected) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /packages/extension/test/redux/index.test.js: -------------------------------------------------------------------------------- 1 | import { Provider, createStore } from '../../src/redux/index' 2 | 3 | describe('redux index', () => { 4 | it('returns the expected', () => { 5 | expect(Provider).not.toBeNull() 6 | expect(createStore).not.toBeNull() 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /packages/extension/test/redux/post_logic_middleware.test.js: -------------------------------------------------------------------------------- 1 | import postLogicMiddleWare from '../../src/redux/post_logic_middleware' 2 | 3 | describe('postLogicMiddleWare', () => { 4 | it('does not call setTimeout if post is null', () => { 5 | jest.useFakeTimers() 6 | const next = jest.fn().mockImplementation(() => 'retVal') 7 | const action = { post: null } 8 | const resp = postLogicMiddleWare()({})(next)(action) 9 | expect(resp).toBe('retVal') 10 | expect(next).toHaveBeenCalledTimes(1) 11 | expect(next).toHaveBeenCalledWith(action) 12 | expect(setTimeout).not.toHaveBeenCalled() 13 | }) 14 | 15 | it('calls setTimeout if post is a function', () => { 16 | jest.useFakeTimers() 17 | const next = jest.fn().mockImplementation(() => 'retVal') 18 | const action = { post: () => {} } 19 | const resp = postLogicMiddleWare()({})(next)(action) 20 | expect(resp).toBe('retVal') 21 | expect(next).toHaveBeenCalledTimes(1) 22 | expect(next).toHaveBeenCalledWith(action) 23 | expect(setTimeout).toHaveBeenCalledTimes(1) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/extension/test/redux/promise_middleware.test.js: -------------------------------------------------------------------------------- 1 | import promiseMiddleWare from '../../src/redux/promise_middleware' 2 | 3 | describe('promiseMiddleWare', () => { 4 | it('calls next with just the action if promise is null', () => { 5 | const next = jest.fn() 6 | const action = { 7 | promise: null, 8 | types: [1, 2, 3], 9 | foo: 'bar' 10 | } 11 | promiseMiddleWare()({})(next)(action) 12 | expect(next).toHaveBeenCalledWith(action) 13 | }) 14 | 15 | it('calls next with multiple args if promise is a fn', () => { 16 | const next = jest.fn() 17 | const then = jest.fn() 18 | const promise = jest.fn().mockImplementation(() => ({ then })) 19 | const action = { 20 | promise, 21 | types: [1, 2, 3], 22 | foo: 'bar' 23 | } 24 | promiseMiddleWare()({})(next)(action) 25 | expect(next).toHaveBeenCalledTimes(1) 26 | expect(next).toHaveBeenCalledWith({ foo: 'bar', type: 1 }) 27 | expect(promise).toHaveBeenCalledTimes(1) 28 | expect(then).toHaveBeenCalledTimes(1) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /packages/extension/test/utils.js: -------------------------------------------------------------------------------- 1 | import configureStore from 'redux-mock-store' 2 | import thunk from 'redux-thunk' 3 | 4 | export const mockStore = configureStore([thunk]) 5 | -------------------------------------------------------------------------------- /packages/extension/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const CopyWebpackPlugin = require('copy-webpack-plugin') 4 | const CleanWebpackPlugin = require('clean-webpack-plugin') 5 | const HardSourcePlugin = require('hard-source-webpack-plugin') 6 | 7 | module.exports = { 8 | mode: process.env.NODE_ENV || 'development', 9 | entry: { 10 | popup: './src/index.js', 11 | content_script: './src/ext/content_script.js', 12 | inject: './src/ext/inject.js', 13 | background: './src/ext/bg.js' 14 | }, 15 | resolve: { 16 | alias: { 17 | handlebars: 'handlebars/dist/handlebars.min.js' 18 | }, 19 | extensions: ['.js', '.jsx'] 20 | }, 21 | output: { 22 | path: path.join(__dirname, 'dist', 'ext'), 23 | filename: '[name].js' 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.(js|jsx)$/, 29 | loader: 'babel-loader', 30 | options: { 31 | rootMode: 'upward' 32 | }, 33 | exclude: /(node_modules|bower_components)/ 34 | }, 35 | { 36 | test: /\.scss$/, 37 | use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'] 38 | }, 39 | { 40 | test: /\.css$/, 41 | use: ['style-loader', 'css-loader'] 42 | }, 43 | { 44 | test: /\.svg$/, 45 | loader: 'svg-react-loader', 46 | exclude: /(node_modules|bower_components)/ 47 | } 48 | ] 49 | }, 50 | plugins: [ 51 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), 52 | new HardSourcePlugin(), 53 | new CleanWebpackPlugin(path.resolve(__dirname, 'dist', 'ext')), 54 | new CopyWebpackPlugin([ 55 | { 56 | from: 'extension/*', 57 | flatten: true, 58 | ignore: ['*.pem'] 59 | } 60 | ]) 61 | ], 62 | devtool: 'inline-source-map' 63 | } 64 | 65 | if (process.env.NODE_ENV === 'production') { 66 | delete module.exports.devtool 67 | module.exports.plugins = (module.exports.plugins || []).concat([ 68 | new webpack.DefinePlugin({ 69 | 'process.env': { 70 | NODE_ENV: '"production"' 71 | } 72 | }), 73 | /* 74 | new webpack.optimize.UglifyJsPlugin({ 75 | compress: { 76 | warnings: false, 77 | screw_ie8: true, 78 | drop_console: true, 79 | drop_debugger: true 80 | } 81 | }), 82 | */ 83 | new webpack.LoaderOptionsPlugin({ 84 | minimize: true 85 | }) 86 | ]) 87 | } 88 | -------------------------------------------------------------------------------- /packages/host/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ], 12 | "env": { 13 | "test": { 14 | "plugins": ["rewire"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/host/.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | target 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /packages/host/README.md: -------------------------------------------------------------------------------- 1 | # ReplayWeb Host 2 | 3 | This repository contains code for the native host for the ReplayWeb Extension. The native host is used by the extension to interact with the host filesystem. Without this, it would not be possible to interact with files other than through the regular file upload dialog, and sending downloads to the downloads directory. 4 | 5 | ## Installation 6 | 7 | Run this bash command to setup the host repository. It installs to `~/.replay-host` 8 | 9 | In a terminal, run one of the following commands based on how you authenticate with github: 10 | 11 | - Personal Access Token: `curl -L https://raw.githubusercontent.com/intuit/replayweb/master/packages/host/src/static/setup.sh | bash` 12 | - SSH Keys: `curl -L https://raw.githubusercontent.com/intuit/replayweb/master/packages/host/src/static/setup-ssh.sh | bash` 13 | 14 | ## Usage details 15 | 16 | See the [docs](src/lib/README.md) for reference on what the methods do. 17 | 18 | ## Developing 19 | 20 | ### Building 21 | 22 | ```sh 23 | npm run build:dev #uses whatever node is in your $PATH 24 | ``` 25 | 26 | ### Installing 27 | 28 | This package is capable of installing itself on Mac: 29 | 30 | ```sh 31 | npm run register 32 | ``` 33 | 34 | This will register it with chrome as a native host, linked back to your build directory, this only needs to be run once. 35 | -------------------------------------------------------------------------------- /packages/host/__mocks__/simple-git/promise.js: -------------------------------------------------------------------------------- 1 | const gitP = jest.genMockFromModule('simple-git/promise') 2 | gitP.constructor = () => ({}) 3 | export default gitP 4 | -------------------------------------------------------------------------------- /packages/host/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@replayweb/host", 3 | "version": "1.0.4", 4 | "description": "Native Host for ReplayWeb extension", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "BABEL_ENV=test jest --coverage", 8 | "lint": "eslint src test", 9 | "docs": "jsdoc2md --files src/lib/*.js > src/lib/README.md", 10 | "build": "NODE_ENV=production webpack", 11 | "build:dev": "NODE_ENV=development webpack", 12 | "register": "./dist/install.sh" 13 | }, 14 | "publishConfig": { 15 | "registry": "https://registry.npmjs.org/", 16 | "access": "public" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/intuit/replayweb" 21 | }, 22 | "keywords": [ 23 | "replay", 24 | "functional", 25 | "selenium", 26 | "testing", 27 | "test" 28 | ], 29 | "author": "Harris Borawski", 30 | "license": "AGPL", 31 | "devDependencies": { 32 | "babel-loader": "^7.1.4", 33 | "babel-plugin-rewire": "^1.1.0", 34 | "babel-preset-env": "^1.7.0", 35 | "chai": "^4.1.2", 36 | "chai-as-promised": "^7.1.1", 37 | "clean-webpack-plugin": "^0.1.19", 38 | "copy-webpack-plugin": "^4.5.2", 39 | "jest": "^23.2.0", 40 | "jest-junit": "^5.1.0", 41 | "jsdoc-to-markdown": "^4.0.1", 42 | "webpack": "^4.14.0", 43 | "webpack-cli": "^3.0.8", 44 | "webpack-permissions-plugin": "^1.0.0" 45 | }, 46 | "dependencies": { 47 | "chrome-native-messaging": "^0.2.0", 48 | "dockerode": "^2.5.5", 49 | "mkdirp": "^0.5.1", 50 | "simple-git": "^1.96.0" 51 | }, 52 | "jest": { 53 | "reporters": [ 54 | "default", 55 | "jest-junit" 56 | ], 57 | "coverageReporters": [ 58 | "json", 59 | "lcov", 60 | "text", 61 | "cobertura" 62 | ] 63 | }, 64 | "jest-junit": { 65 | "output": "./target/surefire-reports/junit.xml" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/host/src/index.js: -------------------------------------------------------------------------------- 1 | import { Input, Transform, Output } from 'chrome-native-messaging' 2 | import { messageHandler } from './lib' 3 | 4 | process.stdin 5 | .pipe(new Input()) 6 | .pipe(new Transform(messageHandler)) 7 | .pipe(new Output()) 8 | .pipe(process.stdout) 9 | -------------------------------------------------------------------------------- /packages/host/src/lib/common.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | // Since it updates itself, but runs from dist, go up one directory 4 | export const LOCATION = path.join(process.cwd(), '..') 5 | -------------------------------------------------------------------------------- /packages/host/src/lib/docker.js: -------------------------------------------------------------------------------- 1 | import Docker from 'dockerode' 2 | 3 | const docker = new Docker({ socketPath: '/var/run/docker.sock' }) 4 | const dockerFolder = 'docker.example.com/dev/test/replayui/team' 5 | 6 | /** 7 | * Function to determine the image name based on looking for it locally built 8 | * @private 9 | * @param {boolean} local - Whether or not to generate the local name 10 | * @returns {string} - The image name 11 | */ 12 | export function imageName(local = false) { 13 | return `${ 14 | local ? 'replay' : dockerFolder 15 | }/crossbrowser-local-orchestrator:latest` 16 | } 17 | 18 | /** 19 | * Pulls the specified image from docker, and resolves when the download 20 | * completes 21 | * @private 22 | * @async 23 | * @param {string} image - The name of the image to pull 24 | * @returns {Promise} - An object with the output from it finishing 25 | */ 26 | export function pullImage(image) { 27 | return new Promise((resolve, reject) => { 28 | docker.pull(image, (err, stream) => { 29 | if (err) { 30 | reject(err) 31 | } 32 | function onFinished(e, output) { 33 | if (e) { 34 | reject(e) 35 | } 36 | resolve(output) 37 | } 38 | docker.modem.followProgress(stream, onFinished, () => {}) 39 | }) 40 | }) 41 | } 42 | 43 | /** 44 | * Finds if any local images in docker are tagged with the image name 45 | * so it can use the local image instead of pulling 46 | * @private 47 | * @async 48 | * @param {string} image - The name of the image to look for 49 | * @returns {Array} - An array of docker images with the image name in the RepoTags 50 | */ 51 | export async function findLocalImages(image) { 52 | const images = await docker.listImages() 53 | return images.filter(i => i.RepoTags && i.RepoTags.includes(image)) 54 | } 55 | /** 56 | * Finds if the orchestrator exists locally, and either starts it or 57 | * pulls the remote version from artifactory and starts it 58 | * @async 59 | * @param {number} port - The port to pass on to the orchestrator to listen on 60 | * @returns {Promise} - Information about the running container 61 | */ 62 | export async function startOrchestrator(port) { 63 | const localImages = await findLocalImages(imageName(true)) 64 | if (localImages.length === 0) { 65 | await pullImage(imageName()) 66 | } 67 | const container = await docker.createContainer({ 68 | Image: imageName(localImages.length > 0), 69 | name: 'local-orchestrator', 70 | Env: [`PORT=${port}`], 71 | ExposedPorts: { 72 | [`${port}/tcp`]: {} 73 | }, 74 | HostConfig: { 75 | AutoRemove: true, 76 | Binds: ['/var/run/docker.sock:/var/run/docker.sock'], 77 | PortBindings: { 78 | [`${port}/tcp`]: [{ HostPort: `${port}` }] 79 | } 80 | } 81 | }) 82 | return container.start() 83 | } 84 | -------------------------------------------------------------------------------- /packages/host/src/lib/git.js: -------------------------------------------------------------------------------- 1 | import gitP from 'simple-git/promise' 2 | import { LOCATION } from './common' 3 | 4 | const git = gitP(LOCATION) 5 | 6 | /** 7 | * Gets the current version of the repo 8 | * @async 9 | * @returns {string} - The version 10 | */ 11 | export async function getCurrentVersion() { 12 | const branches = await git.branch() 13 | return branches.current 14 | } 15 | 16 | /** 17 | * Fetches changes from the remote 18 | * @async 19 | * @returns {string} - stdout from the git command 20 | */ 21 | export async function fetchChanges() { 22 | return git.fetch({ '--all': null }) 23 | } 24 | 25 | /** 26 | * Gets the tags for the repo 27 | * @async 28 | * @returns {Promise} - An array of the available tags 29 | */ 30 | export function fetchTags() { 31 | // for some reason this isnt working as a promise though it does in the REPL 32 | // return git.tags().then(tags => tags.all) 33 | return new Promise((resolve, reject) => { 34 | git.tags({}, (err, result) => { 35 | if (err) { 36 | reject(err) 37 | } else { 38 | resolve(result.all) 39 | } 40 | }) 41 | }) 42 | } 43 | 44 | /** 45 | * Checks out the provided tag 46 | * @async 47 | * @returns {string} - stdout from the git command 48 | */ 49 | export async function checkoutTag(tag) { 50 | return git.checkout(tag) 51 | } 52 | 53 | /** 54 | * Switches to the specified tag if available 55 | * @async 56 | * @returns {string} - The version 57 | * @throws {Error} - Thrown if the specified tag was not found 58 | */ 59 | export async function switchToTag(tag) { 60 | await fetchChanges() 61 | const tags = await fetchTags() 62 | 63 | if (tags.find(e => e === tag)) { 64 | return checkoutTag(tag) 65 | } else { 66 | throw new Error(`Tag not found: ${tag}`) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/host/src/lib/shell.js: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | import { expandHome } from './filesystem' 3 | import { LOCATION } from './common' 4 | /** 5 | * Wraps `exec` in a promise 6 | * @private 7 | * @async 8 | * @param {string} command - The command to exec 9 | * @returns {Promise} - exec wrapped in a promise 10 | */ 11 | export function promiseExec(command) { 12 | return new Promise((resolve, reject) => { 13 | exec(command, (err, stdout, stderr) => { 14 | if (err) { 15 | reject(err) 16 | } else { 17 | resolve(stdout) 18 | } 19 | }) 20 | }) 21 | } 22 | 23 | /** 24 | * Executes the `whoami` bash command in the host environment 25 | * @async 26 | * @returns {Promise} - The username of the host 27 | */ 28 | export function whoami() { 29 | return promiseExec('whoami') 30 | } 31 | 32 | /** 33 | * Uses `which` to see if the provided executable is installed on the host 34 | * @async 35 | * @param {string} executable - The executable to 36 | * @returns {Promise} - The output from `which` 37 | */ 38 | export function checkExecutable(executable) { 39 | return promiseExec(`which "${executable}"`) 40 | } 41 | 42 | /** 43 | * Changes directory to the provided path and builds with npm 44 | * @async 45 | * @param {string} dir - The directory to build in 46 | * @returns {Promise} - The output of the build command 47 | */ 48 | export function buildPackage(dir = LOCATION) { 49 | return promiseExec(`cd ${expandHome(dir)} && npm run build`) 50 | } 51 | -------------------------------------------------------------------------------- /packages/host/src/static/com.intuit.replayweb.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.intuit.replayweb", 3 | "description": "replayweb", 4 | "path": "//dist/nativeHost.sh", 5 | "type": "stdio", 6 | "allowed_origins": ["chrome-extension://kdkncplbcfaiieokihnlffldgaicdkpk/"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/host/src/static/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PATH="$(pwd)/dist" 4 | SRC="$PATH/com.intuit.replayweb.json" 5 | DESTINATION="/Users/$(/usr/bin/whoami)/Library/Application Support/Google/Chrome/NativeMessagingHosts" 6 | /usr/bin/sed -i '' "s~/~$(pwd)~g" $SRC 7 | /bin/cp "$SRC" "$DESTINATION" 8 | -------------------------------------------------------------------------------- /packages/host/src/static/nativeHost.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" 3 | PATH="$ROOT_DIR/node/bin:$PATH" 4 | DIST_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | $(which node) $DIST_DIR/native.js 6 | -------------------------------------------------------------------------------- /packages/host/src/static/nativeHostLocal.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source ~/.bash_profile 3 | DIST_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | $(which node) $DIST_DIR/native.js 5 | -------------------------------------------------------------------------------- /packages/host/src/static/setup-ssh.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | mkdir /tmp/replayweb 5 | git clone git@github.com:intuit/replayweb.git /tmp/replayweb 6 | cp -r /tmp/replayweb/packages/host ~/.replay-host 7 | rm -r /tmp/replayweb 8 | cd ~/.replay-host 9 | curl -o node.tar.gz https://nodejs.org/dist/v8.12.0/node-v8.12.0-darwin-x64.tar.gz 10 | mkdir node 11 | tar xvf node.tar.gz -C node --strip-components 1 12 | rm node.tar.gz 13 | node/bin/npm i 14 | node/bin/npm run build 15 | node/bin/npm run register 16 | -------------------------------------------------------------------------------- /packages/host/src/static/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | mkdir /tmp/replayweb 5 | git clone https://github.com/intuit/replayweb.git /tmp/replayweb 6 | cp -r /tmp/replayweb/packages/host ~/.replay-host 7 | rm -r /tmp/replayweb 8 | cd ~/.replay-host 9 | curl -o node.tar.gz https://nodejs.org/dist/v8.12.0/node-v8.12.0-darwin-x64.tar.gz 10 | mkdir node 11 | tar xvf node.tar.gz -C node --strip-components 1 12 | rm node.tar.gz 13 | node/bin/npm i 14 | node/bin/npm run build 15 | node/bin/npm run register 16 | -------------------------------------------------------------------------------- /packages/host/test/index.test.js: -------------------------------------------------------------------------------- 1 | describe('index', () => { 2 | it('runs', () => { 3 | require('../src/index') 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /packages/host/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const CopyWebpackPlugin = require('copy-webpack-plugin') 3 | const CleanWebpackPlugin = require('clean-webpack-plugin') 4 | const PermissionsPlugin = require('webpack-permissions-plugin') 5 | 6 | module.exports = { 7 | target: 'node', 8 | mode: 'production', 9 | entry: './src/index.js', 10 | resolve: { 11 | extensions: ['.js', '.jsx'] 12 | }, 13 | output: { 14 | path: path.join(__dirname, 'dist'), 15 | filename: 'native.js' 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.(js|jsx)$/, 21 | use: 'babel-loader', 22 | exclude: /(node_modules|bower_components)/ 23 | } 24 | ] 25 | }, 26 | plugins: [ 27 | new CleanWebpackPlugin(path.resolve(__dirname, 'dist')), 28 | new CopyWebpackPlugin([ 29 | { 30 | from: 'src/static/*', 31 | flatten: true 32 | }, 33 | { 34 | from: `src/static/${ 35 | process.env.NODE_ENV === 'production' 36 | ? 'nativeHost.sh' 37 | : 'nativeHostLocal.sh' 38 | }`, 39 | to: 'nativeHost.sh' 40 | } 41 | ]), 42 | new PermissionsPlugin({ 43 | buildFiles: [ 44 | { 45 | path: path.resolve(__dirname, 'dist', 'nativeHost.sh'), 46 | fileMode: '755' 47 | }, 48 | { 49 | path: path.resolve(__dirname, 'dist', 'install.sh'), 50 | fileMode: '755' 51 | } 52 | ] 53 | }) 54 | ], 55 | devtool: 'inline-source-map' 56 | } 57 | 58 | // if (process.env.NODE_ENV === 'production') { 59 | // delete module.exports.devtool 60 | // module.exports.plugins = (module.exports.plugins || []).concat([ 61 | // new webpack.DefinePlugin({ 62 | // 'process.env': { 63 | // NODE_ENV: '"production"' 64 | // } 65 | // }), 66 | // /* 67 | // new webpack.optimize.UglifyJsPlugin({ 68 | // compress: { 69 | // warnings: false, 70 | // screw_ie8: true, 71 | // drop_console: true, 72 | // drop_debugger: true 73 | // } 74 | // }), 75 | // */ 76 | // new webpack.LoaderOptionsPlugin({ 77 | // minimize: true 78 | // }) 79 | // ]) 80 | // } 81 | -------------------------------------------------------------------------------- /packages/testrunner/.gitignore: -------------------------------------------------------------------------------- 1 | log 2 | lib 3 | node_modules 4 | errorShots 5 | target 6 | coverage 7 | .idea/ 8 | replay-testrunner.iml 9 | -------------------------------------------------------------------------------- /packages/testrunner/__mocks__/realPlugin.js: -------------------------------------------------------------------------------- 1 | module.exports = class FakePlugin { 2 | constructor(v) { 3 | this.data = v 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/testrunner/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | collectCoverageFrom: ['src/**/*.js'], 4 | reporters: ['default', 'jest-junit'], 5 | coverageReporters: ['json', 'lcov', 'text', 'cobertura'] 6 | } 7 | -------------------------------------------------------------------------------- /packages/testrunner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@replayweb/testrunner", 3 | "version": "1.0.4", 4 | "description": "Run tests generated via ReplayWeb", 5 | "main": "lib/index.js", 6 | "publishConfig": { 7 | "registry": "https://registry.npmjs.org/", 8 | "access": "public" 9 | }, 10 | "files": [ 11 | "lib" 12 | ], 13 | "scripts": { 14 | "build": "babel --root-mode upward src --out-dir lib", 15 | "functional": "wdio wdio.conf.js", 16 | "lint": "eslint src test/unit", 17 | "docs": "jsdoc2md --files src/*.js > src/README.md", 18 | "prepublishOnly": "yarn build", 19 | "test": "BABEL_ENV=test jest -c jest.config.js", 20 | "test:integration": "wdio", 21 | "serve": "http-server test/utilities/" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/intuit/replayweb" 26 | }, 27 | "keywords": [ 28 | "webdriverio", 29 | "functional", 30 | "selenium", 31 | "replay" 32 | ], 33 | "author": "", 34 | "license": "AGPL", 35 | "dependencies": { 36 | "@ffmpeg-installer/ffmpeg": "^1.0.17", 37 | "@replayweb/utils": "link:../utils", 38 | "@wdio/applitools-service": "^5.12.1", 39 | "deep-equal": "^1.0.1", 40 | "es6-promise": "~4.2.4", 41 | "isomorphic-fetch": "~2.2.1", 42 | "mocha": "~5.2.0", 43 | "tapable": "^1.1.3", 44 | "wdio-video-reporter": "^1.3.4", 45 | "yargs": "~11.0.0" 46 | }, 47 | "devDependencies": { 48 | "@babel/cli": "^7.4.4", 49 | "@babel/core": "^7.4.4", 50 | "@babel/register": "^7.4.4", 51 | "@wdio/cli": "^5.3.0", 52 | "@wdio/interface": "^5.4.6", 53 | "@wdio/jasmine-framework": "^5.4.14", 54 | "@wdio/junit-reporter": "^5.2.3", 55 | "@wdio/local-runner": "^5.3.0", 56 | "@wdio/mocha-framework": "^5.2.8", 57 | "@wdio/selenium-standalone-service": "^5.2.2", 58 | "@wdio/spec-reporter": "^5.2.3", 59 | "@wdio/sync": "^5.8.1", 60 | "chai": "^4.2.0", 61 | "chai-as-promised": "~7.1.1", 62 | "eslint": "^4.19.1", 63 | "eslint-config-standard": "^11.0.0", 64 | "eslint-plugin-chai-friendly": "^0.4.1", 65 | "eslint-plugin-import": "^2.12.0", 66 | "eslint-plugin-mocha": "^5.0.0", 67 | "eslint-plugin-node": "^6.0.1", 68 | "eslint-plugin-promise": "^3.8.0", 69 | "eslint-plugin-standard": "^3.1.0", 70 | "fetch-mock": "^7.2.0", 71 | "http-server": "^0.11.1", 72 | "jsdoc-to-markdown": "~4.0.1", 73 | "webdriverio": "^5.3.0" 74 | }, 75 | "peerDependencies": { 76 | "@wdio/allure-reporter": "^5.7.11", 77 | "@wdio/cli": "^5.0.0", 78 | "@wdio/junit-reporter": "^5.0.0", 79 | "@wdio/local-runner": "^5.0.0", 80 | "@wdio/mocha-framework": "^5.0.0", 81 | "@wdio/selenium-standalone-service": "^5.0.0", 82 | "@wdio/spec-reporter": "^5.0.0", 83 | "chai": "^4.0", 84 | "webdriverio": "^5.0.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/testrunner/replay.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testPath": "./test/integration/json", 3 | "blockPath": "./test/integration/blocks", 4 | "suites": {} 5 | } 6 | -------------------------------------------------------------------------------- /packages/testrunner/test/integration/tests/smokeTest.js: -------------------------------------------------------------------------------- 1 | // This file is necessary because the generated file would try to 2 | // require @replayweb/testrunner, but we need to import the current src 3 | global.expect = require('chai').expect 4 | const replayRunner = require('../../../src') 5 | const baseCommands = require('../json/smokeTest.json') 6 | const { expandBlocks } = require('@replayweb/utils') 7 | 8 | const expandedCommands = expandBlocks(baseCommands.commands, {}) 9 | const commands = { commands: expandedCommands } 10 | describe('smokeTest', function() { 11 | it('smokeTest', () => { 12 | process.env.REPLAY_TEST_NAME = 'smokeTest' 13 | return replayRunner.runCommands(commands) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/testrunner/test/unit/index.removeTemporaryFiles.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | removeTemporaryFiles, 3 | __RewireAPI__ as removeTemporaryFilesRewire 4 | } from '../../src' 5 | 6 | describe('removeTemporaryFiles', () => { 7 | beforeEach(() => { 8 | global.browser = {} 9 | global.process.cwd = () => '/test' 10 | }) 11 | afterEach(() => { 12 | removeTemporaryFilesRewire.__ResetDependency__('fs') 13 | removeTemporaryFilesRewire.__ResetDependency__('process') 14 | }) 15 | it('should remove temporary files if they are not directories', () => { 16 | const unlinked = [] 17 | removeTemporaryFilesRewire.__Rewire__('fs', { 18 | readdirSync: path => ['.replay-tests-123'], 19 | lstatSync: () => ({ isDirectory: () => false }), 20 | unlinkSync: path => unlinked.push(path) 21 | }) 22 | removeTemporaryFiles() 23 | expect(unlinked).toHaveLength(1) 24 | }) 25 | it('should remove temporary files if they are not directories and filter', () => { 26 | const unlinked = [] 27 | removeTemporaryFilesRewire.__Rewire__('fs', { 28 | readdirSync: path => ['.replay-tests-123', '.babelrc'], 29 | lstatSync: () => ({ isDirectory: () => false }), 30 | unlinkSync: path => unlinked.push(path) 31 | }) 32 | removeTemporaryFiles() 33 | expect(unlinked).toHaveLength(1) 34 | }) 35 | it('should remove temporary files if they are are directories', () => { 36 | const unlinked = [] 37 | const rmdirs = [] 38 | removeTemporaryFilesRewire.__Rewire__('fs', { 39 | readdirSync: path => 40 | path === '/test/.replay-tests-123' 41 | ? ['SomeTest.json'] 42 | : ['.replay-tests-123'], 43 | lstatSync: () => ({ isDirectory: () => true }), 44 | unlinkSync: path => unlinked.push(path), 45 | rmdirSync: path => rmdirs.push(path) 46 | }) 47 | removeTemporaryFiles() 48 | expect(unlinked).toHaveLength(1) 49 | expect(rmdirs).toHaveLength(1) 50 | }) 51 | it('should remove temporary files if they are are directories and filter', () => { 52 | const unlinked = [] 53 | const rmdirs = [] 54 | removeTemporaryFilesRewire.__Rewire__('fs', { 55 | readdirSync: path => 56 | path === '/test/.replay-tests-123' 57 | ? ['SomeTest.json'] 58 | : ['.replay-tests-123', 'src'], 59 | lstatSync: () => ({ isDirectory: () => true }), 60 | unlinkSync: path => unlinked.push(path), 61 | rmdirSync: path => rmdirs.push(path) 62 | }) 63 | removeTemporaryFiles() 64 | expect(unlinked).toHaveLength(1) 65 | expect(rmdirs).toHaveLength(1) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /packages/testrunner/test/unit/plugins.test.js: -------------------------------------------------------------------------------- 1 | import { makeHooks, __RewireAPI__ as rewireAPI } from '../../src/index' 2 | 3 | describe('plugins', () => { 4 | afterEach(() => { 5 | rewireAPI.__ResetDependency__('loadPlugin') 6 | }) 7 | it('should make hooks', () => { 8 | const hooks = makeHooks() 9 | const mock = jest.fn() 10 | hooks.beforeCommand.tap('Test', mock) 11 | hooks.beforeCommand.promise({ command: 'test', parameters: {} }, {}) 12 | expect(mock).toHaveBeenCalled() 13 | }) 14 | it('should apply plugins if they load', () => { 15 | const mock = jest.fn() 16 | rewireAPI.__Rewire__('loadPlugin', () => ({ apply: mock })) 17 | makeHooks(['test']) 18 | expect(mock).toHaveBeenCalledTimes(1) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/testrunner/test/unit/utilities.elements.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | waitForElements, 3 | __RewireAPI__ as utilitiesRewire 4 | } from '../../src/utilities' 5 | 6 | describe('waitForElements', () => { 7 | afterEach(() => { 8 | utilitiesRewire.__ResetDependency__('getAndWaitForElement') 9 | }) 10 | it('should get a single element', async () => { 11 | const mock = jest.fn(async el => el) 12 | utilitiesRewire.__Rewire__('getAndWaitForElement', mock) 13 | const els = await waitForElements(['id=target1']) 14 | expect(els).toEqual(['id=target1']) 15 | expect(mock).toBeCalledTimes(1) 16 | }) 17 | it('should get a list of elements', async () => { 18 | const mock = jest.fn(async el => el) 19 | utilitiesRewire.__Rewire__('getAndWaitForElement', mock) 20 | const els = await waitForElements(['id=target1', 'css=someClass']) 21 | expect(els).toEqual(['id=target1', 'css=someClass']) 22 | expect(mock).toBeCalledTimes(2) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/testrunner/test/unit/utilities.getExecElString.test.js: -------------------------------------------------------------------------------- 1 | import { getExecElString } from '../../src/utilities' 2 | 3 | describe('getExecElString', () => { 4 | it('should get exec string for xpath selector', () => { 5 | const selector = '//html/body' 6 | expect(getExecElString(selector)).toBe( 7 | 'document.evaluate(`//html/body`, document.body, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(0)' 8 | ) 9 | }) 10 | it('should get exec string for regular selector', () => { 11 | const selector = '#test' 12 | expect(getExecElString(selector)).toBe('document.querySelector(`#test`)') 13 | }) 14 | it('should get exec string for regular selector and escape \\: sequences', () => { 15 | const selector = '#test\\:2' 16 | expect(getExecElString(selector)).toBe( 17 | 'document.querySelector(`#test\\\\:2`)' 18 | ) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/testrunner/test/unit/utilities.getSelector.test.js: -------------------------------------------------------------------------------- 1 | import { getSelector } from '../../src/utilities' 2 | 3 | describe('getSelector', () => { 4 | it('should throw error for no parameters', () => { 5 | expect(getSelector).toThrow("Cannot read property 'indexOf' of undefined") 6 | }) 7 | it('should throw error for no parameters', () => { 8 | expect(getSelector.bind(null, 'invalidSelector')).toThrow( 9 | 'invalidSelector is not a valid selector' 10 | ) 11 | }) 12 | it('should get selector for implicit xpath', () => { 13 | expect(getSelector('//html/body')).toBe('//html/body') 14 | }) 15 | it('should get selector for explicit xpath', () => { 16 | expect(getSelector('xpath=//html/body')).toBe('//html/body') 17 | }) 18 | it('should get selector for automation-id', () => { 19 | expect(getSelector('automation-id=testId')).toBe( 20 | '[data-automation-id="testId"]' 21 | ) 22 | }) 23 | it('should get selector for automationid', () => { 24 | expect(getSelector('automationid=testId')).toBe('[automationid="testId"]') 25 | }) 26 | it('should get selector for data-auto-sel', () => { 27 | expect(getSelector('data-auto-sel=testId')).toBe('[data-auto-sel="testId"]') 28 | }) 29 | it('should get selector for id', () => { 30 | expect(getSelector('id=testId')).toBe('//*[@id="testId"]') 31 | }) 32 | it('should get selector for id and escape :', () => { 33 | expect(getSelector('id=testId:1')).toBe('//*[@id="testId\\:1"]') 34 | }) 35 | it('should get selector for name', () => { 36 | expect(getSelector('name=testName')).toBe('[name="testName"]') 37 | }) 38 | it('should get selector for identifier', () => { 39 | expect(getSelector('identifier=testId')).toBe('#testId') 40 | }) 41 | it('should get selector for exact link', () => { 42 | expect(getSelector('link=exact:testLink')).toBe('=testLink') 43 | }) 44 | it('should get selector for POS link', () => { 45 | expect(getSelector('link=test@POS=4')).toBe('=test') 46 | }) 47 | it('should get selector for css', () => { 48 | expect(getSelector('css=testClass')).toBe('testClass') 49 | }) 50 | it('should get selector for index', () => { 51 | expect(getSelector('index=1')).toBe('1') 52 | }) 53 | it('should get selector for title', () => { 54 | expect(getSelector('title=testId')).toBe('.//*[contains(@title ,"testId")]') 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /packages/testrunner/test/unit/utilities.loadPlugin.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | tryRequire, 3 | loadPlugin, 4 | __RewireAPI__ as utilitiesRewire 5 | } from '../../src/utilities' 6 | jest.mock('realPlugin') 7 | describe('loadPlugin', () => { 8 | afterEach(() => { 9 | utilitiesRewire.__ResetDependency__('tryRequire') 10 | }) 11 | it('should load a string plugin definition', () => { 12 | const mock = jest.fn() 13 | utilitiesRewire.__Rewire__('tryRequire', mock) 14 | loadPlugin('testplugin') 15 | expect(mock).toHaveBeenCalledWith('testplugin', {}) 16 | }) 17 | it('should load a array plugin definition', () => { 18 | const mock = jest.fn() 19 | utilitiesRewire.__Rewire__('tryRequire', mock) 20 | loadPlugin(['testplugin', { option: 'value' }]) 21 | expect(mock).toHaveBeenCalledWith('testplugin', { option: 'value' }) 22 | }) 23 | it('should throw an error for an invalid plugin definition', () => { 24 | const mock = jest.spyOn(console, 'error') 25 | utilitiesRewire.__Rewire__('tryRequire', mock) 26 | expect(loadPlugin).toThrow('Invalid plugin format: undefined') 27 | expect(mock).toHaveBeenCalled() 28 | }) 29 | }) 30 | describe('tryRequire', () => { 31 | it('should throw an error if it cant find the package', () => { 32 | expect(tryRequire.bind(null, 'fakepackage')).toThrow() 33 | }) 34 | it('should construct a plugin if the package is found', () => { 35 | global.require = () => 36 | class FakePlugin { 37 | constructor(parameter) { 38 | this.data = parameter 39 | } 40 | } 41 | const p = tryRequire('realPlugin', 'parameterValue') 42 | expect(p.data).toBe('parameterValue') 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /packages/testrunner/test/unit/utilities.log.test.js: -------------------------------------------------------------------------------- 1 | import { log } from '../../src/utilities' 2 | 3 | describe('log', () => { 4 | it('should log if verbose', () => { 5 | let logged = false 6 | global.console.log = () => (logged = true) 7 | log('a', true) 8 | expect(logged).toBe(true) 9 | }) 10 | it('should not log if not verbose', () => { 11 | let logged = false 12 | global.console.log = () => (logged = true) 13 | log('a', false) 14 | expect(logged).toBe(false) 15 | }) 16 | it('should not log when not given verbose arg', () => { 17 | let logged = false 18 | global.console.log = () => (logged = true) 19 | log('a') 20 | expect(logged).toBe(false) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /packages/testrunner/test/utilities/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intuit/ReplayWeb/8b4cb972f86bbdbf00417674caff30a279563cac/packages/testrunner/test/utilities/favicon.ico -------------------------------------------------------------------------------- /packages/testrunner/test/utilities/framed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | framed.html 5 | 10 | 11 | 12 | 13 |

Initial Values

14 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /packages/testrunner/test/utilities/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @replayweb/testrunner integration tests 5 | 16 | 17 | 18 | 19 |

Initial Value

20 | 26 |

Initial Value

27 |

Invisible

28 | 29 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /packages/testrunner/wdio.conf.js: -------------------------------------------------------------------------------- 1 | require('@babel/register')({ 2 | rootMode: 'upward' 3 | }) 4 | const { getDefaults } = require('./src') 5 | 6 | exports.config = { 7 | ...getDefaults(), 8 | specs: ['./test/integration/tests/smokeTest.js'] 9 | } 10 | -------------------------------------------------------------------------------- /packages/utils/.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | coverage 4 | .nyc_output 5 | target 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /packages/utils/README.md: -------------------------------------------------------------------------------- 1 | # ReplayWeb Utils 2 | 3 | This module contains common functionality used between ReplayWeb Extension and ReplayWeb Test Runner to avoid code duplication. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | yarn add @replayweb/utils 9 | ``` 10 | 11 | ## Usage details 12 | 13 | ```js 14 | import { doReplace } from '@replayweb/utils' 15 | 16 | doReplace(string) // returns a promise 17 | .then(replaced => console.log(replaced)) // replaced is the string with all replacements done 18 | ``` 19 | 20 | See individual function information in the [docs](src/README.md). 21 | 22 | ## Development 23 | 24 | #### System Requirements 25 | 26 | - [NodeJS](https://nodejs.org/en/) 10+ 27 | 28 | ### QuickStart 29 | 30 | ```sh 31 | git clone https://github.com/intuit/replayweb.git 32 | cd packages/utils 33 | yarn 34 | yarn test # outputs test results and coverage 35 | ``` 36 | 37 | #### Local Integration 38 | 39 | To test changes without publishing a new version, you can use the `link` feature of yarn to use your local build with other locally built applications: 40 | 41 | ```sh 42 | replay-utils $ yarn build 43 | replay-utils $ yarn link 44 | 45 | some-other-repo $ yarn link @replayweb/utils 46 | some-other-repo $ # run your build command, @replayweb/utils is now a symlink to your local build of replay-utils 47 | ``` 48 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@replayweb/utils", 3 | "version": "1.0.4", 4 | "description": "Common utilities for ReplayWeb and the ReplayWeb Test Runner", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib" 8 | ], 9 | "scripts": { 10 | "build": "babel --root-mode upward src --out-dir lib", 11 | "prepublishOnly": "yarn build", 12 | "prepublish": "yarn build", 13 | "docs": "jsdoc2md --files src/*.js > src/README.md", 14 | "test": "jest --coverage", 15 | "lint": "eslint src/*.js test/*.js" 16 | }, 17 | "author": "Harris Borawski", 18 | "license": "AGPL", 19 | "publishConfig": { 20 | "registry": "https://registry.npmjs.org/", 21 | "access": "public" 22 | }, 23 | "dependencies": { 24 | "enumify": "~1.0.4", 25 | "moment": "~2.20.1" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/intuit/replayweb.git" 30 | }, 31 | "devDependencies": { 32 | "@babel/cli": "^7.4.4", 33 | "@babel/core": "^7.4.4", 34 | "chai": "~4.1.2", 35 | "es6-promise": "~4.2.4", 36 | "fetch-mock": "~6.0.0", 37 | "isomorphic-fetch": "~2.2.1", 38 | "jsdoc-to-markdown": "~4.0.1", 39 | "mocha": "~5.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/utils/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const CleanWebpackPlugin = require('clean-webpack-plugin') 4 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 5 | const name = 'replay-utils' 6 | module.exports = { 7 | target: 'node', 8 | mode: 'production', 9 | entry: path.join(__dirname, 'src', 'index.js'), 10 | resolve: { 11 | extensions: ['.js'] 12 | }, 13 | output: { 14 | path: path.join(__dirname, 'lib'), 15 | filename: `${name}.js`, 16 | libraryTarget: 'umd', 17 | library: name 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | use: 'babel-loader', 24 | exclude: /(node_modules|bower_components)/ 25 | } 26 | ] 27 | }, 28 | plugins: [ 29 | new CleanWebpackPlugin(path.resolve(__dirname, 'lib')), 30 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) 31 | ] 32 | } 33 | 34 | if (process.env.NODE_ENV === 'production') { 35 | module.exports.plugins = [...module.exports.plugins, new UglifyJsPlugin()] 36 | } 37 | -------------------------------------------------------------------------------- /setupEnzyme.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme' 2 | import Adapter from 'enzyme-adapter-react-16' 3 | 4 | configure({ adapter: new Adapter() }) 5 | --------------------------------------------------------------------------------