├── .config └── dotnet-tools.json ├── .devcontainer ├── Dockerfile ├── devcontainer.json └── settings.vscode.json ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── build.yml ├── .gitignore ├── .paket └── Paket.Restore.targets ├── .travis.yml ├── .vscode ├── extensions.json └── settings.json ├── Build.fsx ├── CHANGELOG.md ├── Directory.Build.props ├── LICENSE.md ├── NOTES_TO_SELF.md ├── README.md ├── RELEASE_NOTES.md ├── Rakefile.rb ├── appveyor.yml ├── build.cmd ├── build.sh ├── canopy.sln ├── docs ├── Api_Reference │ ├── canopy.integration │ │ ├── canopy-csharp-integration.html │ │ ├── canopy-csharp-loadtest-job.html │ │ ├── canopy-csharp-loadtest-runner.html │ │ ├── canopy-csharp-loadtest-task.html │ │ ├── canopy-integration-loadtest-job.html │ │ ├── canopy-integration-loadtest-task.html │ │ ├── canopy-integration-loadtest.html │ │ ├── canopy.integration.html │ │ ├── global-jsonvalidator.html │ │ ├── jsonvalidator-difference.html │ │ ├── jsonvalidator-meta.html │ │ └── jsonvalidator-type.html │ └── canopy │ │ ├── canopy-classic.html │ │ ├── canopy-configuration.html │ │ ├── canopy-csharp-canopy.html │ │ ├── canopy-finders-byjquery.html │ │ ├── canopy-finders.html │ │ ├── canopy-history.html │ │ ├── canopy-jarowinkler-result.html │ │ ├── canopy-jarowinkler.html │ │ ├── canopy-parallell-functions-navigate.html │ │ ├── canopy-parallell-functions.html │ │ ├── canopy-parallell-instanced-instance.html │ │ ├── canopy-parallell-instanced.html │ │ ├── canopy-reporters-consolereporter.html │ │ ├── canopy-reporters-junitreporter.html │ │ ├── canopy-reporters-livehtmlreporter.html │ │ ├── canopy-reporters-teamcityreporter.html │ │ ├── canopy-reporters.html │ │ ├── canopy-runner-classic.html │ │ ├── canopy-screen-screenboundary.html │ │ ├── canopy-screen.html │ │ ├── canopy-types-browsers.html │ │ ├── canopy-types-browserstartmode.html │ │ ├── canopy-types-canopycheckfailedexception.html │ │ ├── canopy-types-canopycontainsfailedexception.html │ │ ├── canopy-types-canopycountexception.html │ │ ├── canopy-types-canopydeselectionfailedexception.html │ │ ├── canopy-types-canopydisabledfailedexception.html │ │ ├── canopy-types-canopydisplayedfailedexception.html │ │ ├── canopy-types-canopyelementnotfoundexception.html │ │ ├── canopy-types-canopyenabledfailedexception.html │ │ ├── canopy-types-canopyequalityfailedexception.html │ │ ├── canopy-types-canopyexception.html │ │ ├── canopy-types-canopymorethanoneelementfoundexception.html │ │ ├── canopy-types-canopynobrowserexception.html │ │ ├── canopy-types-canopynotcontainsfailedexception.html │ │ ├── canopy-types-canopynotdisplayedfailedexception.html │ │ ├── canopy-types-canopynotequalsfailedexception.html │ │ ├── canopy-types-canopynotstringorelementexception.html │ │ ├── canopy-types-canopyonexception.html │ │ ├── canopy-types-canopyoptionnotfoundexception.html │ │ ├── canopy-types-canopyreadexception.html │ │ ├── canopy-types-canopyreadonlyexception.html │ │ ├── canopy-types-canopyselectionfailedexeception.html │ │ ├── canopy-types-canopyskiptestexception.html │ │ ├── canopy-types-canopyuncheckfailedexception.html │ │ ├── canopy-types-canopyvalueinlistexception.html │ │ ├── canopy-types-canopyvaluenotinlistexception.html │ │ ├── canopy-types-canopywaitforexception.html │ │ ├── canopy-types-direction.html │ │ ├── canopy-types-ireporter.html │ │ ├── canopy-types-result.html │ │ ├── canopy-types-suite.html │ │ ├── canopy-types-test.html │ │ ├── canopy-types.html │ │ ├── canopy-wait.html │ │ ├── canopy.html │ │ ├── global-screensizes.html │ │ └── global-useragents.html ├── Docs │ ├── actions.html │ ├── assertions.html │ ├── configuration.html │ ├── reporting.html │ └── testing.html ├── content │ ├── cleanups.js │ ├── hotload.js │ ├── style.css │ ├── submenu.js │ ├── themes.js │ ├── tips.js │ ├── toggle-bootstrap-dark.min.css │ ├── toggle-bootstrap.min.css │ └── upgrade1to2.fsx.broken ├── files │ ├── canopy_orig.jpg │ ├── canopy_small.jpg │ ├── img │ │ ├── console.png │ │ ├── installCanopy.png │ │ ├── installChromeDriver.png │ │ ├── livehtmlreport.png │ │ ├── logo.jpg │ │ ├── logo.jpg.bak │ │ ├── newProject.png │ │ └── run.png │ ├── placeholder.html │ ├── placeholder.md │ └── testpages │ │ ├── alert.html │ │ ├── autocomplete.html │ │ ├── count.html │ │ ├── ctrlClick.html │ │ ├── displayed.html │ │ ├── doubleClick.html │ │ ├── elementWithin.html │ │ ├── home.html │ │ ├── iframe1.html │ │ ├── iframe2.html │ │ ├── index.html │ │ ├── noClickTilVisible.html │ │ ├── notDisplayed.html │ │ ├── parent.html │ │ ├── readonly.html │ │ ├── ryansError.html │ │ ├── sandbox.html │ │ ├── selectOptions.html │ │ └── waitFor.html ├── index.html ├── reporttemplate.html ├── reporttemplatep.html └── testpages │ ├── alert.html │ ├── autocomplete.html │ ├── count.html │ ├── ctrlClick.html │ ├── displayed.html │ ├── doubleClick.html │ ├── elementWithin.html │ ├── home.html │ ├── iframe1.html │ ├── iframe2.html │ ├── index.html │ ├── noClickTilVisible.html │ ├── notDisplayed.html │ ├── parent.html │ ├── readonly.html │ ├── ryansError.html │ ├── sandbox.html │ ├── selectOptions.html │ └── waitFor.html ├── docsSrc ├── Docs │ ├── actions.fsx │ ├── assertions.fsx │ ├── configuration.fsx │ ├── reporting.fsx │ └── testing.fsx ├── content │ ├── cleanups.js │ ├── hotload.js │ ├── style.css │ ├── submenu.js │ ├── themes.js │ ├── tips.js │ ├── toggle-bootstrap-dark.min.css │ ├── toggle-bootstrap.min.css │ └── upgrade1to2.fsx.broken ├── files │ ├── canopy_orig.jpg │ ├── canopy_small.jpg │ ├── img │ │ ├── console.png │ │ ├── installCanopy.png │ │ ├── installChromeDriver.png │ │ ├── livehtmlreport.png │ │ ├── logo.jpg │ │ ├── logo.jpg.bak │ │ ├── newProject.png │ │ └── run.png │ ├── placeholder.md │ ├── reporttemplate.html │ ├── reporttemplatep.html │ └── testpages │ │ ├── alert.html │ │ ├── autocomplete.html │ │ ├── count.html │ │ ├── ctrlClick.html │ │ ├── displayed.html │ │ ├── doubleClick.html │ │ ├── elementWithin.html │ │ ├── home.html │ │ ├── iframe1.html │ │ ├── iframe2.html │ │ ├── index.html │ │ ├── noClickTilVisible.html │ │ ├── notDisplayed.html │ │ ├── parent.html │ │ ├── readonly.html │ │ ├── ryansError.html │ │ ├── sandbox.html │ │ ├── selectOptions.html │ │ └── waitFor.html ├── index.fsx └── index.md.bak ├── docsTool ├── CLI.fs ├── Prelude.fs ├── Program.fs ├── README.md ├── docsTool.fsproj ├── paket.references └── templates │ ├── helpers.fs │ ├── master.fs │ ├── modules.fs │ ├── namespaces.fs │ ├── nav.fs │ ├── partMembers.fs │ ├── partNested.fs │ └── types.fs ├── icon.jpg ├── paket.dependencies ├── paket.lock ├── src ├── Directory.Build.props ├── canopy.integration │ ├── AssemblyInfo.fs │ ├── canopy.integration.XML │ ├── canopy.integration.fsproj │ ├── chsharpLoadTest.fs │ ├── csharp.fs │ ├── jsonValidator.fs │ ├── loadTest.fs │ └── paket.references └── canopy │ ├── AssemblyInfo.fs │ ├── canopy.fs │ ├── canopy.fsproj │ ├── canopy.paket.template │ ├── canopy.parallell.functions.fs │ ├── canopy.parallell.instanced.fs │ ├── companion │ ├── companion.fsx │ ├── crx │ │ ├── background.js │ │ ├── companion.js │ │ ├── fable-core │ │ │ └── umd │ │ │ │ ├── Array.js │ │ │ │ ├── Assert.js │ │ │ │ ├── Async.js │ │ │ │ ├── AsyncBuilder.js │ │ │ │ ├── BigInt.js │ │ │ │ ├── BigInt │ │ │ │ ├── BigNat.js │ │ │ │ └── FFT.js │ │ │ │ ├── BitConverter.js │ │ │ │ ├── Choice.js │ │ │ │ ├── Date.js │ │ │ │ ├── Event.js │ │ │ │ ├── GenericComparer.js │ │ │ │ ├── Lazy.js │ │ │ │ ├── List.js │ │ │ │ ├── ListClass.js │ │ │ │ ├── Long.js │ │ │ │ ├── MailboxProcessor.js │ │ │ │ ├── Map.js │ │ │ │ ├── Observable.js │ │ │ │ ├── Reflection.js │ │ │ │ ├── RegExp.js │ │ │ │ ├── Seq.js │ │ │ │ ├── Serialize.js │ │ │ │ ├── Set.js │ │ │ │ ├── String.js │ │ │ │ ├── Symbol.js │ │ │ │ ├── TimeSpan.js │ │ │ │ ├── Timer.js │ │ │ │ └── Util.js │ │ ├── icon.png │ │ ├── jquery.js │ │ ├── manifest.json │ │ └── styles.css │ ├── fableconfig.json │ ├── icon128.png │ ├── screenshot.png │ └── webpack.config.js │ ├── configuration.fs │ ├── csharp.fs │ ├── finders.fs │ ├── history.fs │ ├── icon.jpg │ ├── jarowinkler.fs │ ├── paket.references │ ├── reporters.fs │ ├── runner.fs │ ├── screen.fs │ ├── screenSizes.fs │ ├── types.fs │ ├── userAgents.fs │ └── wait.fs └── tests ├── Directory.Build.props ├── basictests ├── AssemblyInfo.fs ├── Program.fs ├── app.config ├── basictests.fsproj ├── file1.fs ├── file2.fs ├── jsonValidatorTests.fs ├── loadTestTests.fs └── paket.references ├── csharptests ├── App.config ├── Program.cs ├── csharptests.csproj ├── jsonValidatorTests.cs └── paket.references └── paralleltests ├── App.config ├── AssemblyInfo.fs ├── Program.fs ├── functionsTests.fs ├── instancedTests.fs ├── paket.references ├── paralleltests.fsproj └── prunner.fs /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "sourcelink": { 6 | "version": "3.1.1", 7 | "commands": [ 8 | "sourcelink" 9 | ] 10 | }, 11 | "dotnet-reportgenerator-globaltool": { 12 | "version": "4.2.15", 13 | "commands": [ 14 | "reportgenerator" 15 | ] 16 | }, 17 | "fake-cli": { 18 | "version": "5.21.0", 19 | "commands": [ 20 | "fake" 21 | ] 22 | }, 23 | "paket": { 24 | "version": "7.1.5", 25 | "commands": [ 26 | "paket" 27 | ] 28 | }, 29 | "fcswatch-cli": { 30 | "version": "0.7.14", 31 | "commands": [ 32 | "fcswatch" 33 | ] 34 | }, 35 | "fantomas-tool": { 36 | "version": "3.2.0", 37 | "commands": [ 38 | "fantomas" 39 | ] 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fsharp:10.2.3-netcore 2 | 3 | # Copy endpoint specific user settings into container to specify 4 | # .NET Core should be used as the runtime. 5 | COPY settings.vscode.json /root/.vscode-remote/data/Machine/settings.json 6 | 7 | # Install git, process tools 8 | RUN apt-get update && apt-get -y install git procps 9 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MiniScaffold", 3 | "dockerFile": "Dockerfile", 4 | "appPort": [8080], 5 | "extensions": [ 6 | "ionide.ionide-fsharp", 7 | "ms-vscode.csharp", 8 | "editorconfig.editorconfig", 9 | "ionide.ionide-paket", 10 | "ionide.ionide-fake" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.devcontainer/settings.vscode.json: -------------------------------------------------------------------------------- 1 | { 2 | "FSharp.fsacRuntime":"netcore" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: 2 | http://EditorConfig.org 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Default settings: 8 | # A newline ending every file 9 | # Use 4 spaces as indentation 10 | [*] 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{fs,fsi,fsx,config}] 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | 19 | [paket.*] 20 | trim_trailing_whitespace = true 21 | indent_size = 2 22 | 23 | [*.paket.references] 24 | trim_trailing_whitespace = true 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp text=auto eol=lf 6 | *.vb diff=csharp text=auto eol=lf 7 | *.fs diff=csharp text=auto eol=lf 8 | *.fsi diff=csharp text=auto eol=lf 9 | *.fsx diff=csharp text=auto eol=lf 10 | *.sln text eol=crlf merge=union 11 | *.csproj merge=union 12 | *.vbproj merge=union 13 | *.fsproj merge=union 14 | *.dbproj merge=union 15 | *.sh text eol=lf 16 | 17 | # Standard to msysgit 18 | *.doc diff=astextplain 19 | *.DOC diff=astextplain 20 | *.docx diff=astextplain 21 | *.DOCX diff=astextplain 22 | *.dot diff=astextplain 23 | *.DOT diff=astextplain 24 | *.pdf diff=astextplain 25 | *.PDF diff=astextplain 26 | *.rtf diff=astextplain 27 | *.RTF diff=astextplain 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please insert a description of your problem or question. 4 | 5 | ## Error messages, screenshots 6 | 7 | Please add any error logs or screenshots if available. 8 | 9 | ## Failing test, failing GitHub repo, or reproduction steps 10 | 11 | Please add either a failing test, a GitHub repo of the problem or detailed reproduction steps. 12 | 13 | ## Expected Behavior 14 | 15 | Please define what you would expect the behavior to be like. 16 | 17 | ## Known workarounds 18 | 19 | Please provide a description of any known workarounds. 20 | 21 | ## Other information 22 | 23 | * Operating System: 24 | - [ ] windows [insert version here] 25 | - [ ] macOs [insert version] 26 | - [ ] linux [insert flavor/version here] 27 | * Platform 28 | - [ ] dotnet core 29 | - [ ] dotnet full 30 | - [ ] mono 31 | * Branch or release version: 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed Changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 4 | 5 | ## Types of changes 6 | 7 | What types of changes does your code introduce to canopy? 8 | _Put an `x` in the boxes that apply_ 9 | 10 | - [ ] Bugfix (non-breaking change which fixes an issue) 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | 14 | 15 | ## Checklist 16 | 17 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 18 | 19 | - [ ] Build and tests pass locally 20 | - [ ] I have added tests that prove my fix is effective or that my feature works (if appropriate) 21 | - [ ] I have added necessary documentation (if appropriate) 22 | 23 | ## Further comments 24 | 25 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 26 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build master 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, windows-latest, macOS-latest] 11 | dotnet: [3.1.100] 12 | runs-on: ${{ matrix.os }} 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: ${{ matrix.dotnet }} 20 | - name: Build 21 | if: runner.os != 'Windows' 22 | run: | 23 | chmod +x ./build.sh 24 | ./build.sh 25 | env: 26 | # Work around https://github.com/actions/setup-dotnet/issues/29 27 | DOTNET_ROOT: ${{ runner.tool_cache }}/dncs/${{ matrix.dotnet }}/x64 28 | CI: true 29 | - name: Build 30 | if: runner.os == 'Windows' 31 | run: ./build.cmd 32 | env: 33 | # Work around https://github.com/actions/setup-dotnet/issues/29 34 | DOTNET_ROOT: ${{ runner.tool_cache }}/dncs/${{ matrix.dotnet }}/x64 35 | CI: true 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | sudo: required 3 | dist: xenial 4 | 5 | dotnet: 3.0.100 6 | mono: 7 | - latest # => "stable release" 8 | - alpha 9 | - beta 10 | - weekly # => "latest commits" 11 | os: 12 | - linux 13 | 14 | script: 15 | - ./build.sh 16 | 17 | matrix: 18 | fast_finish: true 19 | allow_failures: 20 | - mono: alpha 21 | - mono: beta 22 | - mono: weekly 23 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ionide.ionide-paket", 4 | "ionide.ionide-fsharp", 5 | "ionide.ionide-fake", 6 | "ms-vscode.csharp", 7 | "editorConfig.editorConfig" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "tab.unfocusedActiveBorder": "#fff0" 4 | } 5 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [2.1.5] - 2020-03-07 10 | Fix test pages 11 | 12 | ## [2.1.4] - 2020-03-07 13 | Add back nuget icon 14 | 15 | ## [2.1.3] - 2020-03-07 16 | New project structure release! 17 | 18 | ## [2.1.2] - 2020-02-20 19 | New project structure release! 20 | 21 | ### Changed 22 | - Changed project scaffolding 23 | - Fixed docs 24 | 25 | [2.1.2]: https://github.com/lefthandedgoat/canopy/releases/tag/v2.1.2 -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | f#, fsharp 5 | https://github.com/lefthandedgoat/canopy 6 | https://github.com/lefthandedgoat/canopy/blob/master/LICENSE.md 7 | https://raw.githubusercontent.com/lefthandedgoat/canopy/master/docs/files/canopy_small.jpg 8 | false 9 | git 10 | Chris Holt, Amir Rajan, Jeremy Bellows 11 | https://github.com/lefthandedgoat/canopy 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2011 Chris Holt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /NOTES_TO_SELF.md: -------------------------------------------------------------------------------- 1 | build.cmd Release 2.1.3 2 | dist folder has nuget package to manually update 3 | on new pc grab the githubt environment token and put it in envioronment variable 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | canopy 2 | ====== 3 | 4 | Documentation available at http://lefthandedgoat.github.io/canopy/. 5 | 6 | Getting started kit available at https://github.com/lefthandedgoat/canopyStarterKit/. 7 | 8 | Real world examples available at https://github.com/lefthandedgoat/turtletestAutomation 9 | 10 | Some C# examples available at https://github.com/lefthandedgoat/canopy/blob/master/tests/csharptests/Program.cs 11 | -------------------------------------------------------------------------------- /Rakefile.rb: -------------------------------------------------------------------------------- 1 | 2 | module OS 3 | def OS.windows? 4 | (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil 5 | end 6 | 7 | def OS.mac? 8 | (/darwin/ =~ RUBY_PLATFORM) != nil 9 | end 10 | 11 | def OS.unix? 12 | !OS.windows? 13 | end 14 | 15 | def OS.linux? 16 | OS.unix? and not OS.mac? 17 | end 18 | end 19 | 20 | 21 | 22 | task :default => [:build, :ui] 23 | 24 | task :build do 25 | if OS.windows? then 26 | sh 'C:\Windows\Microsoft.NET\Framework\v4.0.30319\msbuild.exe canopy.sln' 27 | #sh 'C:\Windows\Microsoft.NET\Framework\v4.0.30319\msbuild.exe canopy.sln /p:VisualStudioVersion=12.0' 28 | end 29 | 30 | if OS.mac? then 31 | sh 'xbuild canopy.sln' 32 | end 33 | end 34 | 35 | task :ui do 36 | if OS.windows? then 37 | sh 'tests\basictests\bin\debug\basictests.exe' 38 | end 39 | 40 | if OS.mac? then 41 | sh "mono --debug tests/basictests/bin/debug/basictests.exe" 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf input 3 | build_script: 4 | - cmd: build.cmd 5 | test: off 6 | version: 0.0.1.{build} 7 | image: Visual Studio 2019 8 | install: 9 | - cmd: choco install dotnetcore-sdk -y 10 | artifacts: 11 | - path: bin 12 | name: bin 13 | - path: dist 14 | name: dist 15 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | echo Restoring dotnet tools... 2 | dotnet tool restore 3 | 4 | dotnet fake build -t %* 5 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | echo "Restoring dotnet tools..." 7 | dotnet tool restore 8 | 9 | PAKET_SKIP_RESTORE_TARGETS=true FAKE_DETAILED_ERRORS=true dotnet fake build -t "$@" 10 | -------------------------------------------------------------------------------- /docs/content/cleanups.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | // Makes code snippets responsive 3 | $("table").addClass("table-responsive"); 4 | }) 5 | 6 | -------------------------------------------------------------------------------- /docs/content/hotload.js: -------------------------------------------------------------------------------- 1 | var refreshSocket = new WebSocket('ws://' + window.location.host) 2 | .onmessage = () => { 3 | location.reload(); 4 | } 5 | -------------------------------------------------------------------------------- /docs/content/submenu.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | // ------------------------------------------------------- // 3 | // Multi Level dropdowns 4 | // ------------------------------------------------------ // 5 | $("ul.dropdown-menu [data-toggle='dropdown']").on("click", function(event) { 6 | event.preventDefault(); 7 | event.stopPropagation(); 8 | 9 | $(this).siblings().toggleClass("show"); 10 | 11 | 12 | if (!$(this).next().hasClass('show')) { 13 | $(this).parents('.dropdown-menu').first().find('.show').removeClass("show"); 14 | } 15 | $(this).parents('li.nav-item.dropdown.show').on('hidden.bs.dropdown', function(e) { 16 | $('.dropdown-submenu .show').removeClass("show"); 17 | }); 18 | 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /docs/content/themes.js: -------------------------------------------------------------------------------- 1 | 2 | var themes = { 3 | "light" : { 4 | "button-text" : "Swap to Dark", 5 | "button-classes" : "btn btn-dark border-light", 6 | "next-theme" : "dark", 7 | "body-class" : "bootstrap" 8 | }, 9 | "dark" : { 10 | "button-text" : "Swap to Light", 11 | "button-classes" : "btn btn-light", 12 | "next-theme" : "light", 13 | "body-class" : "bootstrap-dark" 14 | } 15 | }; 16 | 17 | var themeStorageKey = 'theme'; 18 | 19 | function swapThemeInDom(theme) { 20 | var newTheme = themes[theme]; 21 | var bootstrapCSS = document.getElementsByTagName('body')[0]; 22 | bootstrapCSS.setAttribute('class', newTheme['body-class']) 23 | } 24 | 25 | function persistNewTheme(theme) { 26 | window.localStorage.setItem(themeStorageKey, theme); 27 | } 28 | 29 | function setToggleButton(theme) { 30 | var newTheme = themes[theme]; 31 | var themeToggleButton = document.getElementById('theme-toggle'); 32 | themeToggleButton.textContent = newTheme['button-text']; 33 | themeToggleButton.className = newTheme['button-classes']; 34 | themeToggleButton.onclick = function() { 35 | setTheme(newTheme['next-theme']); 36 | } 37 | } 38 | 39 | function setTheme(theme) { 40 | try { 41 | swapThemeInDom(theme); 42 | } 43 | catch(e){ 44 | } 45 | try { 46 | persistNewTheme(theme); 47 | } 48 | catch(e) { 49 | } 50 | try { 51 | setToggleButton(theme); 52 | } 53 | catch (e) { 54 | } 55 | } 56 | 57 | function getThemeFromStorage() { 58 | return window.localStorage.getItem(themeStorageKey); 59 | } 60 | 61 | function getThemeFromScheme() { 62 | try { 63 | if (window.matchMedia("(prefers-color-scheme: dark)").matches){ 64 | return 'dark'; 65 | } 66 | else { 67 | return 'light'; 68 | } 69 | } 70 | catch(e) { 71 | return null; 72 | } 73 | } 74 | 75 | function loadTheme() { 76 | var theme = getThemeFromStorage() || getThemeFromScheme() || 'light'; 77 | setTheme(theme); 78 | } 79 | 80 | document.addEventListener('readystatechange', (event) => { 81 | loadTheme() 82 | }); 83 | -------------------------------------------------------------------------------- /docs/content/tips.js: -------------------------------------------------------------------------------- 1 | var currentTip = null; 2 | var currentTipElement = null; 3 | 4 | function hideTip(evt, name, unique) { 5 | var el = document.getElementById(name); 6 | el.style.display = "none"; 7 | currentTip = null; 8 | } 9 | 10 | function findPos(obj) { 11 | // no idea why, but it behaves differently in webbrowser component 12 | if (window.location.search == "?inapp") 13 | return [obj.offsetLeft + 10, obj.offsetTop + 30]; 14 | 15 | var curleft = 0; 16 | var curtop = obj.offsetHeight; 17 | while (obj) { 18 | curleft += obj.offsetLeft; 19 | curtop += obj.offsetTop; 20 | obj = obj.offsetParent; 21 | }; 22 | return [curleft, curtop]; 23 | } 24 | 25 | function hideUsingEsc(e) { 26 | if (!e) { e = event; } 27 | hideTip(e, currentTipElement, currentTip); 28 | } 29 | 30 | function showTip(evt, name, unique, owner) { 31 | document.onkeydown = hideUsingEsc; 32 | if (currentTip == unique) return; 33 | currentTip = unique; 34 | currentTipElement = name; 35 | 36 | var pos = findPos(owner ? owner : (evt.srcElement ? evt.srcElement : evt.target)); 37 | var posx = pos[0]; 38 | var posy = pos[1]; 39 | 40 | var el = document.getElementById(name); 41 | var parent = (document.documentElement == null) ? document.body : document.documentElement; 42 | el.style.position = "absolute"; 43 | el.style.left = posx + "px"; 44 | el.style.top = posy + "px"; 45 | el.style.display = "block"; 46 | } 47 | -------------------------------------------------------------------------------- /docs/content/upgrade1to2.fsx.broken: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | // This block of code is omitted in the generated HTML documentation. Use 3 | // it to define helpers that you do not want to show in the documentation. 4 | #I "../../src/canopy/bin/Release/netstandard2.0" 5 | 6 | #if !FAKE 7 | #r "netstandard" 8 | #endif 9 | 10 | #r "canopy.dll" 11 | open canopy.classic 12 | 13 | (** 14 | Upgrade 1.x to 2.x 15 | ======================== 16 | *) 17 | 18 | (** 19 | Breaking changes 20 | ----- 21 | * Removed coverage report support, made move to 2.0 more difficult and I don't think they were used 22 | * Removed phantomjs support, the project is no longer updated because of chrome headless 23 | * Removed writeToSelectWithOptionValue, old backwards compat flag 24 | * Removed optimizeByDisablingCoverageReport, old optimization around coverage reports 25 | * Changed default driver path from c:\ to executing directory 26 | *) 27 | 28 | (** 29 | Namespace changes 30 | ----- 31 | ``` 32 | canopy -> canopy.classic 33 | canopy.runner -> canopy.runner.classic //no longer auto opened 34 | 35 | canopy.configuration //no longer auto opened 36 | canopy.reporters 37 | canopy.screen 38 | canopy.types //no longer auto opened 39 | canopy.wait //no longer auto opened 40 | canopy.finders 41 | canopy.history 42 | canopy.userAgents 43 | 44 | canopy.parallell.functions //new 45 | canopy.parallell.instanced //new 46 | ``` 47 | *) 48 | 49 | (** 50 | Signature changes 51 | ----- 52 | * The signature for custom finders now requires an instance of IWebDriver (the browser) also 53 | * From ```string -> (By -> ReadOnlyCollection) -> IWebElement list``` 54 | * To ```string -> (By -> ReadOnlyCollection) -> IWebDriver -> IWebElement list``` 55 | *) 56 | 57 | (** 58 | Screen management changes 59 | ----- 60 | * Due to the upgrade to support .net Core canopy can no longer determine your resolution 61 | * You will need to manually set the resolutions so things like `pin` work correctly 62 | 63 | ``` 64 | canopy.screen.screenWidth <- 3840 //default 1920 65 | canopy.screen.screenHeight <- 2160 //default 1080 66 | canopy.screen.monitorCount <- 2 //default 1 67 | ``` 68 | *) 69 | 70 | (** 71 | New features 72 | ----- 73 | * .net Standard 2.0 support 74 | * Added parallel support in two styles in two new namespaces 75 | *) 76 | 77 | (** 78 | Parallel testing 79 | ----- 80 | * Added parallel support in two styles in two new namespaces 81 | ``` 82 | canopy.parallell.functions 83 | canopy.parallell.instanced 84 | ``` 85 | * See examples of function based tests: https://github.com/lefthandedgoat/canopy/blob/master/tests/paralleltests/functionsTests.fs 86 | * See examples of instanced based tests: https://github.com/lefthandedgoat/canopy/blob/master/tests/paralleltests/instancedTests.fs 87 | 88 | * `canopy.runner.classic` does NOT support parallel tests. 89 | * You should choose a unit test library that you like and supports this and write canopy tests in it 90 | * In the future I may release a parallel friendly test runner for canopy that includes some features from the classic runner 91 | * Parallel test running is HARD. Your database is a global mutable variable and your tests may interfere with each other 92 | * You will also need to understand how your test runner runs tests, as it may not run them in order 93 | *) 94 | 95 | (** 96 | Upgrading from .net 4.5.2 97 | ----- 98 | * In an real world code base I simply updated the project from 4.5.2 to 4.6.1, installed the .net core SDK, and it worked 99 | * If you have problems please let me know in the github issues and I will see what I can do to repro and help 100 | *) 101 | () 102 | -------------------------------------------------------------------------------- /docs/files/canopy_orig.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docs/files/canopy_orig.jpg -------------------------------------------------------------------------------- /docs/files/canopy_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docs/files/canopy_small.jpg -------------------------------------------------------------------------------- /docs/files/img/console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docs/files/img/console.png -------------------------------------------------------------------------------- /docs/files/img/installCanopy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docs/files/img/installCanopy.png -------------------------------------------------------------------------------- /docs/files/img/installChromeDriver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docs/files/img/installChromeDriver.png -------------------------------------------------------------------------------- /docs/files/img/livehtmlreport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docs/files/img/livehtmlreport.png -------------------------------------------------------------------------------- /docs/files/img/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docs/files/img/logo.jpg -------------------------------------------------------------------------------- /docs/files/img/logo.jpg.bak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docs/files/img/logo.jpg.bak -------------------------------------------------------------------------------- /docs/files/img/newProject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docs/files/img/newProject.png -------------------------------------------------------------------------------- /docs/files/img/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docs/files/img/run.png -------------------------------------------------------------------------------- /docs/files/placeholder.md: -------------------------------------------------------------------------------- 1 | place images or other files here 2 | -------------------------------------------------------------------------------- /docs/files/testpages/alert.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | autocomplete 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/files/testpages/autocomplete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | autocomplete 4 | 5 | 6 | 7 |
8 | 9 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/files/testpages/count.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/files/testpages/ctrlClick.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ctrl click 4 | 5 | 6 | 7 | 8 | 14 | 15 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/files/testpages/displayed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/files/testpages/doubleClick.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | double click 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/files/testpages/elementWithin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/files/testpages/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 |

Home

8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/files/testpages/iframe1.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/files/testpages/iframe2.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/files/testpages/noClickTilVisible.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | floak testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/files/testpages/notDisplayed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/files/testpages/parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/files/testpages/readonly.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | autocomplete 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/files/testpages/ryansError.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 15 | 16 | 17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /docs/files/testpages/sandbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | sandbox 4 | 5 | 6 | 7 |
8 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/files/testpages/selectOptions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 |
11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/files/testpages/waitFor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/testpages/alert.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | autocomplete 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/testpages/autocomplete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | autocomplete 4 | 5 | 6 | 7 |
8 | 9 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/testpages/count.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/testpages/ctrlClick.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ctrl click 4 | 5 | 6 | 7 | 8 | 14 | 15 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/testpages/displayed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/testpages/doubleClick.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | double click 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/testpages/elementWithin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/testpages/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 |

Home

8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/testpages/iframe1.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/testpages/iframe2.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/testpages/noClickTilVisible.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | floak testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/testpages/notDisplayed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/testpages/parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/testpages/readonly.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | autocomplete 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/testpages/ryansError.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 15 | 16 | 17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /docs/testpages/sandbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | sandbox 4 | 5 | 6 | 7 |
8 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/testpages/selectOptions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 |
11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/testpages/waitFor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docsSrc/Docs/assertions.fsx: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | // This block of code is omitted in the generated HTML documentation. Use 3 | // it to define helpers that you do not want to show in the documentation. 4 | #I "../../src/canopy/bin/Debug/netstandard2.0" 5 | #I "../../packages/Selenium.WebDriver/lib/netstandard2.0" 6 | #r "canopy.dll" 7 | #r "WebDriver.dll" 8 | #if !FAKE 9 | #r "netstandard" 10 | #endif 11 | 12 | open canopy.classic 13 | open canopy.runner 14 | open System 15 | 16 | 17 | (** 18 | Assertions 19 | ======================== 20 | *) 21 | 22 | (** 23 | == (equals) 24 | ----------- 25 | Assert that the element on the left is equal to the value on the right. 26 | *) 27 | "#firstName" == "Alex" 28 | 29 | (** 30 | != (does not equal) 31 | ------------------- 32 | Assert that the element on the left is not equal to the value on the right. 33 | *) 34 | "#firstName" != "Tom" 35 | 36 | (** 37 | === (aliased as is) 38 | ----------- 39 | Assert that the value on the left is equal to the value on right. 40 | * Note: does not use retry-ability. Equivalent to Assert.Equals. 41 | *) 42 | "Not a selector" === "Not a selector" 43 | 44 | (** 45 | *= (one of many equals) 46 | ----------------------- 47 | Assert that at least one element in a list equals a value. 48 | *) 49 | ".todoItem" *= "Buy milk" 50 | 51 | (** 52 | *!= (none equals) 53 | ----------------- 54 | Assert that none of the items in a list equals a value. 55 | *) 56 | ".todoItem" *!= "Sell everything" 57 | 58 | (** 59 | contains 60 | -------- 61 | Assert that one string contains another. 62 | *) 63 | contains "Log" (read "#logout") 64 | 65 | (** 66 | containsInsensitive 67 | -------- 68 | Assert that one string contains (case insensitive) another. 69 | *) 70 | containsInsensitive "Log" (read "#logout") 71 | 72 | (** 73 | notContains 74 | -------- 75 | Assert that one string does not contain another. 76 | *) 77 | notContains "Hello Bob!" (read "#name") 78 | 79 | (** 80 | count 81 | ----- 82 | Assert there are `X` items of given css selector. 83 | *) 84 | count ".todoItem" 5 85 | 86 | (** 87 | =~ (regex match) 88 | ---------------- 89 | Assert that an element `regex` matches a value. 90 | *) 91 | "#lastName" << "Gray" 92 | "#lastName" =~ "Gr[ae]y" 93 | "#lastName" << "Grey" 94 | "#lastName" =~ "Gr[ae]y" 95 | 96 | (** 97 | !=~ (regex match) 98 | ---------------- 99 | Assert that an element does not `regex` match a value. 100 | *) 101 | "#lastName" << "Gr0y" 102 | "#lastName" !=~ "Gr[ae]y" 103 | "#lastName" << "Gr1y" 104 | "#lastName" !=~ "Gr[ae]y" 105 | 106 | (** 107 | *~ (one of many regex match) 108 | ---------------------------- 109 | Assert that one of many element `regex` matches a value. 110 | *) 111 | "#colors li" *~ "gr[ea]y" 112 | 113 | (** 114 | on 115 | -- 116 | Assert that the browser is currently on a url. Falls back to using `String.Contains` after page timeout. 117 | *) 118 | url "https://duckduckgo.com/?q=canopy+f%23" 119 | on "https://duckduckgo.com/?q" 120 | 121 | (** 122 | onn 123 | -- 124 | Same as `on` but does not fall back to using `String.Contains`. 125 | *) 126 | url "https://duckduckgo.com/about" 127 | onn "https://duckduckgo.com/about" 128 | 129 | (** 130 | selected 131 | -------- 132 | Assert that a radio or checkbox is selected. 133 | *) 134 | selected "#yes" 135 | 136 | (** 137 | deselected 138 | ---------- 139 | Assert that a radio or checkbox is not selected. 140 | *) 141 | deselected "#yes" 142 | 143 | (** 144 | displayed 145 | --------- 146 | Assert that an element is displayed on the screen. (Note: Will not walk up the dom. If a parent container is hidden this may give the wrong results, try adding :visible to selector) 147 | *) 148 | displayed "#modal" 149 | 150 | (** 151 | notDisplayed 152 | ------------ 153 | Assert that an element is not displayed on the screen. (Note: Will not walk up the dom. If a parent container is hidden this may give the wrong results, try adding :visible to selector) 154 | *) 155 | notDisplayed "#modal" 156 | 157 | (** 158 | enabled 159 | --------- 160 | Assert that an element is enabled. 161 | *) 162 | enabled "#button" 163 | 164 | (** 165 | disabled 166 | --------- 167 | Assert that an element is not enabled. 168 | *) 169 | disabled "#button" 170 | 171 | (** 172 | fadedIn 173 | --------- 174 | Returns true/false if element has finished fading in and is shown. 175 | *) 176 | let isShown = (fadedIn "#message")() 177 | waitFor <| fadedIn "#message" 178 | -------------------------------------------------------------------------------- /docsSrc/Docs/reporting.fsx: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | // This block of code is omitted in the generated HTML documentation. Use 3 | // it to define helpers that you do not want to show in the documentation. 4 | #I "../../src/canopy/bin/Debug/netstandard2.0" 5 | #I "../../packages/Selenium.WebDriver/lib/netstandard2.0" 6 | #r "canopy.dll" 7 | #r "WebDriver.dll" 8 | #if !FAKE 9 | #r "netstandard" 10 | #endif 11 | 12 | open canopy 13 | open canopy.configuration 14 | open canopy.types 15 | open System 16 | 17 | (** 18 | Reporting 19 | ========= 20 | 21 | Console Reporter 22 | ---------------- 23 | The default reporter. Prints results to console. 24 | Console Reporter 25 | 26 | Live HTML Reporter 27 | ------------------ 28 | Prints results to an html page. Support images. Screenshots on error. 29 | *) 30 | open configuration 31 | open reporters 32 | reporter <- new reporters.LiveHtmlReporter(BrowserStartMode.Chrome, "driver path") :> IReporter 33 | 34 | (** 35 | Live HTML Reporter 36 | 37 | TeamCity Reporter 38 | ----------------- 39 | Prints special outputs that are compatible with team city. 40 | *) 41 | 42 | open configuration 43 | open reporters 44 | reporter <- new TeamCityReporter() :> IReporter 45 | 46 | //screenshot is TODO 47 | 48 | (** 49 | JUnit Reporter 50 | ----------------- 51 | Produces test results in basic JUnit format. Compatible with CircleCI. 52 | *) 53 | 54 | open configuration 55 | open reporters 56 | reporter <- new JUnitReporter("./TestResults.xml") :> IReporter 57 | -------------------------------------------------------------------------------- /docsSrc/content/cleanups.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | // Makes code snippets responsive 3 | $("table").addClass("table-responsive"); 4 | }) 5 | 6 | -------------------------------------------------------------------------------- /docsSrc/content/hotload.js: -------------------------------------------------------------------------------- 1 | var refreshSocket = new WebSocket('ws://' + window.location.host) 2 | .onmessage = () => { 3 | location.reload(); 4 | } 5 | -------------------------------------------------------------------------------- /docsSrc/content/submenu.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | // ------------------------------------------------------- // 3 | // Multi Level dropdowns 4 | // ------------------------------------------------------ // 5 | $("ul.dropdown-menu [data-toggle='dropdown']").on("click", function(event) { 6 | event.preventDefault(); 7 | event.stopPropagation(); 8 | 9 | $(this).siblings().toggleClass("show"); 10 | 11 | 12 | if (!$(this).next().hasClass('show')) { 13 | $(this).parents('.dropdown-menu').first().find('.show').removeClass("show"); 14 | } 15 | $(this).parents('li.nav-item.dropdown.show').on('hidden.bs.dropdown', function(e) { 16 | $('.dropdown-submenu .show').removeClass("show"); 17 | }); 18 | 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /docsSrc/content/themes.js: -------------------------------------------------------------------------------- 1 | 2 | var themes = { 3 | "light" : { 4 | "button-text" : "Swap to Dark", 5 | "button-classes" : "btn btn-dark border-light", 6 | "next-theme" : "dark", 7 | "body-class" : "bootstrap" 8 | }, 9 | "dark" : { 10 | "button-text" : "Swap to Light", 11 | "button-classes" : "btn btn-light", 12 | "next-theme" : "light", 13 | "body-class" : "bootstrap-dark" 14 | } 15 | }; 16 | 17 | var themeStorageKey = 'theme'; 18 | 19 | function swapThemeInDom(theme) { 20 | var newTheme = themes[theme]; 21 | var bootstrapCSS = document.getElementsByTagName('body')[0]; 22 | bootstrapCSS.setAttribute('class', newTheme['body-class']) 23 | } 24 | 25 | function persistNewTheme(theme) { 26 | window.localStorage.setItem(themeStorageKey, theme); 27 | } 28 | 29 | function setToggleButton(theme) { 30 | var newTheme = themes[theme]; 31 | var themeToggleButton = document.getElementById('theme-toggle'); 32 | themeToggleButton.textContent = newTheme['button-text']; 33 | themeToggleButton.className = newTheme['button-classes']; 34 | themeToggleButton.onclick = function() { 35 | setTheme(newTheme['next-theme']); 36 | } 37 | } 38 | 39 | function setTheme(theme) { 40 | try { 41 | swapThemeInDom(theme); 42 | } 43 | catch(e){ 44 | } 45 | try { 46 | persistNewTheme(theme); 47 | } 48 | catch(e) { 49 | } 50 | try { 51 | setToggleButton(theme); 52 | } 53 | catch (e) { 54 | } 55 | } 56 | 57 | function getThemeFromStorage() { 58 | return window.localStorage.getItem(themeStorageKey); 59 | } 60 | 61 | function getThemeFromScheme() { 62 | try { 63 | if (window.matchMedia("(prefers-color-scheme: dark)").matches){ 64 | return 'dark'; 65 | } 66 | else { 67 | return 'light'; 68 | } 69 | } 70 | catch(e) { 71 | return null; 72 | } 73 | } 74 | 75 | function loadTheme() { 76 | var theme = getThemeFromStorage() || getThemeFromScheme() || 'light'; 77 | setTheme(theme); 78 | } 79 | 80 | document.addEventListener('readystatechange', (event) => { 81 | loadTheme() 82 | }); 83 | -------------------------------------------------------------------------------- /docsSrc/content/tips.js: -------------------------------------------------------------------------------- 1 | var currentTip = null; 2 | var currentTipElement = null; 3 | 4 | function hideTip(evt, name, unique) { 5 | var el = document.getElementById(name); 6 | el.style.display = "none"; 7 | currentTip = null; 8 | } 9 | 10 | function findPos(obj) { 11 | // no idea why, but it behaves differently in webbrowser component 12 | if (window.location.search == "?inapp") 13 | return [obj.offsetLeft + 10, obj.offsetTop + 30]; 14 | 15 | var curleft = 0; 16 | var curtop = obj.offsetHeight; 17 | while (obj) { 18 | curleft += obj.offsetLeft; 19 | curtop += obj.offsetTop; 20 | obj = obj.offsetParent; 21 | }; 22 | return [curleft, curtop]; 23 | } 24 | 25 | function hideUsingEsc(e) { 26 | if (!e) { e = event; } 27 | hideTip(e, currentTipElement, currentTip); 28 | } 29 | 30 | function showTip(evt, name, unique, owner) { 31 | document.onkeydown = hideUsingEsc; 32 | if (currentTip == unique) return; 33 | currentTip = unique; 34 | currentTipElement = name; 35 | 36 | var pos = findPos(owner ? owner : (evt.srcElement ? evt.srcElement : evt.target)); 37 | var posx = pos[0]; 38 | var posy = pos[1]; 39 | 40 | var el = document.getElementById(name); 41 | var parent = (document.documentElement == null) ? document.body : document.documentElement; 42 | el.style.position = "absolute"; 43 | el.style.left = posx + "px"; 44 | el.style.top = posy + "px"; 45 | el.style.display = "block"; 46 | } 47 | -------------------------------------------------------------------------------- /docsSrc/content/upgrade1to2.fsx.broken: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | // This block of code is omitted in the generated HTML documentation. Use 3 | // it to define helpers that you do not want to show in the documentation. 4 | #I "../../src/canopy/bin/Release/netstandard2.0" 5 | 6 | #if !FAKE 7 | #r "netstandard" 8 | #endif 9 | 10 | #r "canopy.dll" 11 | open canopy.classic 12 | 13 | (** 14 | Upgrade 1.x to 2.x 15 | ======================== 16 | *) 17 | 18 | (** 19 | Breaking changes 20 | ----- 21 | * Removed coverage report support, made move to 2.0 more difficult and I don't think they were used 22 | * Removed phantomjs support, the project is no longer updated because of chrome headless 23 | * Removed writeToSelectWithOptionValue, old backwards compat flag 24 | * Removed optimizeByDisablingCoverageReport, old optimization around coverage reports 25 | * Changed default driver path from c:\ to executing directory 26 | *) 27 | 28 | (** 29 | Namespace changes 30 | ----- 31 | ``` 32 | canopy -> canopy.classic 33 | canopy.runner -> canopy.runner.classic //no longer auto opened 34 | 35 | canopy.configuration //no longer auto opened 36 | canopy.reporters 37 | canopy.screen 38 | canopy.types //no longer auto opened 39 | canopy.wait //no longer auto opened 40 | canopy.finders 41 | canopy.history 42 | canopy.userAgents 43 | 44 | canopy.parallell.functions //new 45 | canopy.parallell.instanced //new 46 | ``` 47 | *) 48 | 49 | (** 50 | Signature changes 51 | ----- 52 | * The signature for custom finders now requires an instance of IWebDriver (the browser) also 53 | * From ```string -> (By -> ReadOnlyCollection) -> IWebElement list``` 54 | * To ```string -> (By -> ReadOnlyCollection) -> IWebDriver -> IWebElement list``` 55 | *) 56 | 57 | (** 58 | Screen management changes 59 | ----- 60 | * Due to the upgrade to support .net Core canopy can no longer determine your resolution 61 | * You will need to manually set the resolutions so things like `pin` work correctly 62 | 63 | ``` 64 | canopy.screen.screenWidth <- 3840 //default 1920 65 | canopy.screen.screenHeight <- 2160 //default 1080 66 | canopy.screen.monitorCount <- 2 //default 1 67 | ``` 68 | *) 69 | 70 | (** 71 | New features 72 | ----- 73 | * .net Standard 2.0 support 74 | * Added parallel support in two styles in two new namespaces 75 | *) 76 | 77 | (** 78 | Parallel testing 79 | ----- 80 | * Added parallel support in two styles in two new namespaces 81 | ``` 82 | canopy.parallell.functions 83 | canopy.parallell.instanced 84 | ``` 85 | * See examples of function based tests: https://github.com/lefthandedgoat/canopy/blob/master/tests/paralleltests/functionsTests.fs 86 | * See examples of instanced based tests: https://github.com/lefthandedgoat/canopy/blob/master/tests/paralleltests/instancedTests.fs 87 | 88 | * `canopy.runner.classic` does NOT support parallel tests. 89 | * You should choose a unit test library that you like and supports this and write canopy tests in it 90 | * In the future I may release a parallel friendly test runner for canopy that includes some features from the classic runner 91 | * Parallel test running is HARD. Your database is a global mutable variable and your tests may interfere with each other 92 | * You will also need to understand how your test runner runs tests, as it may not run them in order 93 | *) 94 | 95 | (** 96 | Upgrading from .net 4.5.2 97 | ----- 98 | * In an real world code base I simply updated the project from 4.5.2 to 4.6.1, installed the .net core SDK, and it worked 99 | * If you have problems please let me know in the github issues and I will see what I can do to repro and help 100 | *) 101 | () 102 | -------------------------------------------------------------------------------- /docsSrc/files/canopy_orig.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docsSrc/files/canopy_orig.jpg -------------------------------------------------------------------------------- /docsSrc/files/canopy_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docsSrc/files/canopy_small.jpg -------------------------------------------------------------------------------- /docsSrc/files/img/console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docsSrc/files/img/console.png -------------------------------------------------------------------------------- /docsSrc/files/img/installCanopy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docsSrc/files/img/installCanopy.png -------------------------------------------------------------------------------- /docsSrc/files/img/installChromeDriver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docsSrc/files/img/installChromeDriver.png -------------------------------------------------------------------------------- /docsSrc/files/img/livehtmlreport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docsSrc/files/img/livehtmlreport.png -------------------------------------------------------------------------------- /docsSrc/files/img/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docsSrc/files/img/logo.jpg -------------------------------------------------------------------------------- /docsSrc/files/img/logo.jpg.bak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docsSrc/files/img/logo.jpg.bak -------------------------------------------------------------------------------- /docsSrc/files/img/newProject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docsSrc/files/img/newProject.png -------------------------------------------------------------------------------- /docsSrc/files/img/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/docsSrc/files/img/run.png -------------------------------------------------------------------------------- /docsSrc/files/placeholder.md: -------------------------------------------------------------------------------- 1 | place images or other files here 2 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/alert.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | autocomplete 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/autocomplete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | autocomplete 4 | 5 | 6 | 7 |
8 | 9 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/count.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 |
    9 | 10 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/ctrlClick.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ctrl click 4 | 5 | 6 | 7 | 8 | 14 | 15 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/displayed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
      12 |
    13 | 14 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/doubleClick.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | double click 4 | 5 | 6 | 7 | 8 |
    9 |
    10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/elementWithin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 |

    Home

    8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/iframe1.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/iframe2.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/noClickTilVisible.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | floak testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/notDisplayed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
      12 |
    13 | 14 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 |
      9 |
    • item 1
    • 10 |
    • item 2
    • 11 |
    12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/readonly.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | autocomplete 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/ryansError.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 15 | 16 | 17 | 18 |
    19 | 20 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/sandbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | sandbox 4 | 5 | 6 | 7 |
    8 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/selectOptions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 |
    11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docsSrc/files/testpages/waitFor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy testing page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
      12 |
    13 | 14 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docsSrc/index.fsx: -------------------------------------------------------------------------------- 1 | (*** hide ***) 2 | // This block of code is omitted in the generated HTML documentation. Use 3 | // it to define helpers that you do not want to show in the documentation. 4 | #I "../src/canopy/bin/Debug/netstandard2.0" 5 | #I "../packages/Selenium.WebDriver/lib/netstandard2.0" 6 | #r "canopy.dll" 7 | #r "WebDriver.dll" 8 | #if !FAKE 9 | #r "netstandard" 10 | #r "System.Runtime.Extensions" 11 | #endif 12 | 13 | 14 | (** 15 | canopy - f#rictionless web testing 16 | =================== 17 |
    18 |
    19 |
    20 |
    21 | 22 |
    23 |
    24 |
    25 |
    26 |
    27 |

    28 | The canopy library can be installed from NuGet: 29 |

    30 |

    31 |

    PM> Install-Package canopy
    32 |

    33 |
    34 |
    35 |
    36 |
    37 | 38 |
    39 |
    40 | canopy is a web testing framework with one goal in mind, make UI testing simple: 41 | 42 | * Solid stabilization layer built on top of Selenium. Death to "brittle, quirky, UI tests". 43 | 44 | * Quick to learn. Even if you've never done UI Automation, and don't know F#. 45 | 46 | * Clean, concise API. 47 | 48 | * .net Standard 2.0. 49 | 50 | * MIT License. 51 |
    52 |
    53 | 54 | Getting Started 55 | --------------- 56 | ####1\. Create a new F# console application (4.6.1+ or .net core) 57 |
    58 |
    59 | F# New Project 60 |
    61 |
    62 | 63 | ####2\. Install canopy via Nuget 64 |
    65 |
    66 | Install canopy 67 |
    68 |
    69 | 70 | ####3\. Install chromedriver via Nuget 71 |
    72 |
    73 | Install chromedriver 74 |
    75 |
    76 | 77 | ####4\. Paste the following code into `Program.fs` 78 | 79 | *) 80 | //these are similar to C# using statements 81 | open canopy.runner.classic 82 | open canopy.configuration 83 | open canopy.classic 84 | 85 | canopy.configuration.chromeDir <- System.AppContext.BaseDirectory 86 | 87 | //start an instance of chrome 88 | start chrome 89 | 90 | //this is how you define a test 91 | "taking canopy for a spin" &&& fun _ -> 92 | //this is an F# function body, it's whitespace enforced 93 | 94 | //go to url 95 | url "http://lefthandedgoat.github.io/canopy/testpages/" 96 | 97 | //assert that the element with an id of 'welcome' has 98 | //the text 'Welcome' 99 | "#welcome" == "Welcome" 100 | 101 | //assert that the element with an id of 'firstName' has the value 'John' 102 | "#firstName" == "John" 103 | 104 | //change the value of element with 105 | //an id of 'firstName' to 'Something Else' 106 | "#firstName" << "Something Else" 107 | 108 | //verify another element's value, click a button, 109 | //verify the element is updated 110 | "#button_clicked" == "button not clicked" 111 | click "#button" 112 | "#button_clicked" == "button clicked" 113 | 114 | //run all tests 115 | run() 116 | 117 | printfn "press [enter] to exit" 118 | System.Console.ReadLine() |> ignore 119 | 120 | quit() 121 | (** 122 |
    123 |
    124 | ####5\. Run 125 | Run 126 |
    127 |
    128 | 129 |
    130 |
    131 | ####6\. Explore the rest of canopy's API 132 | 133 | * [Actions](/canopy/Docs/actions.html): documentation of everything you can do on a page 134 | * [Assertions](/canopy/Docs/assertions.html): all the ways you can verify what's on the page is correct 135 | * [Configuration](/canopy/Docs/configuration.html): configure and fine tune canopy 136 | * [Testing](/canopy/Docs/content.html): different ways to orchestrate tests and troubleshoot issues with a page 137 | * [Reporting](/canopy/Docs/reporting.html): different ways to output the results of your test suite 138 |
    139 |
    140 | 141 | ####7\. Watch some intro videos 142 |
    143 |
    144 | 4 minute canopy starter kit 145 | 146 |
    147 |
    148 | 149 |
    150 |
    151 | 5 minutes with Amir Rajan 152 | 158 |
    159 |
    160 | 161 |
    162 |
    163 | 30 minutes with Chris Holt at fsharpConf 164 | 170 |
    171 |
    172 | *) 173 | -------------------------------------------------------------------------------- /docsSrc/index.md.bak: -------------------------------------------------------------------------------- 1 | # canopy 2 | 3 | --- 4 | 5 | ## What is canopy? 6 | 7 | canopy is a library that does this specific thing. 8 | 9 | ## Why use canopy? 10 | 11 | I created it because I had to solve an issue with this other thing. 12 | 13 | --- 14 | 15 |
    16 |
    17 |
    18 |
    19 |
    Tutorials
    20 |

    Takes you by the hand through a series of steps to create your first thing.

    21 |
    22 | 25 |
    26 |
    27 |
    28 |
    29 |
    30 |
    How-To Guides
    31 |

    Guides you through the steps involved in addressing key problems and use-cases.

    32 |
    33 | 36 |
    37 |
    38 |
    39 |
    40 |
    41 |
    Explanations
    42 |

    Discusses key topics and concepts at a fairly high level and provide useful background information and explanation..

    43 |
    44 | 47 |
    48 |
    49 |
    50 |
    51 |
    52 |
    Api Reference
    53 |

    Contain technical reference for APIs.

    54 |
    55 | 58 |
    59 |
    60 |
    61 | -------------------------------------------------------------------------------- /docsTool/CLI.fs: -------------------------------------------------------------------------------- 1 | namespace DocsTool 2 | 3 | module CLIArgs = 4 | open Argu 5 | open Fake.IO.Globbing.Operators 6 | 7 | type WatchArgs = 8 | | ProjectGlob of string 9 | | DocsSourceDirectory of string 10 | | GitHubRepoUrl of string 11 | | ProjectName of string 12 | | ReleaseVersion of string 13 | with 14 | interface IArgParserTemplate with 15 | member this.Usage = 16 | match this with 17 | | ProjectGlob _ -> "The glob for the dlls to generate API documentation." 18 | | DocsSourceDirectory _ -> "The docs source directory." 19 | | GitHubRepoUrl _ -> "The GitHub repository url." 20 | | ProjectName _ -> "The project name." 21 | | ReleaseVersion _ -> "The project's Release Version name." 22 | 23 | type BuildArgs = 24 | | SiteBaseUrl of string 25 | | ProjectGlob of string 26 | | DocsOutputDirectory of string 27 | | DocsSourceDirectory of string 28 | | GitHubRepoUrl of string 29 | | ProjectName of string 30 | | ReleaseVersion of string 31 | with 32 | interface IArgParserTemplate with 33 | member this.Usage = 34 | match this with 35 | | SiteBaseUrl _ -> "The public site's base url." 36 | | ProjectGlob _ -> "The glob for the dlls to generate API documentation" 37 | | DocsOutputDirectory _ -> "The docs output directory." 38 | | DocsSourceDirectory _ -> "The docs source directory." 39 | | GitHubRepoUrl _ -> "The GitHub repository url." 40 | | ProjectName _ -> "The project name." 41 | | ReleaseVersion _ -> "The project's Release Version name." 42 | 43 | type CLIArguments = 44 | | [] Watch of ParseResults 45 | | [] Build of ParseResults 46 | with 47 | interface IArgParserTemplate with 48 | member this.Usage = 49 | match this with 50 | | Watch _ -> "Builds the docs, serves the content, and watches for changes to the content." 51 | | Build _ -> "Builds the docs" 52 | -------------------------------------------------------------------------------- /docsTool/Prelude.fs: -------------------------------------------------------------------------------- 1 | namespace DocsTool 2 | 3 | module Uri = 4 | open System 5 | let simpleCombine (slug : string) (baseUri : Uri) = 6 | sprintf "%s/%s" (baseUri.AbsoluteUri.TrimEnd('/')) (slug.TrimStart('/')) 7 | 8 | let create (url : string) = 9 | match Uri.TryCreate(url, UriKind.Absolute) with 10 | | (true, v) -> v 11 | | _ -> failwithf "Bad url %s" url 12 | -------------------------------------------------------------------------------- /docsTool/README.md: -------------------------------------------------------------------------------- 1 | # Docs Tool 2 | 3 | ## Example 4 | [MiniScaffold docs example](https://www.jimmybyrd.me/miniscaffold-docs-test/) 5 | 6 | ## Docs High Level Design 7 | 8 | This template is based heavily on [What nobody tells you about documentation](https://www.divio.com/blog/documentation/). In `docsSrc` folder you'll see a similar structure to what is described below: 9 | 10 | - **Tutorials** 11 | - is learning-oriented 12 | - allows the newcomer to get started 13 | - is a lesson 14 | - Analogy: teaching a small child how to cook 15 | - **How-To Guides** 16 | - is goal-oriented 17 | - shows how to solve a specific problem 18 | - is a series of steps 19 | - Analogy: a recipe in a cookery book 20 | - **Explanation** 21 | - is understanding-oriented 22 | - explains 23 | - provides background and context 24 | - Analogy: an article on culinary social history 25 | - **Reference** 26 | - is information-oriented 27 | - describes the machinery 28 | - is accurate and complete 29 | - Analogy: a reference encyclopedia article 30 | 31 | 32 | The folders in `docsSrc` are: 33 | 34 | - `content` - custom css, javascript, and similar go here. 35 | - `Explanations` - A content section as defined above. 36 | - `files` - extra files like screenshots, images, videos. 37 | - `How_Tos` - A content section as defined above. 38 | - `Tutorials` - A content section as defined above. 39 | - `index.md` - The entry page to your documentation 40 | 41 | The navbar is generated by the folders in docsSrc, excluding `content` and `files` folders. Looking at the [example](https://www.jimmybyrd.me/miniscaffold-docs-test/) we can the navbar containing: 42 | 43 | - `Api References` 44 | - `Explanations` 45 | - `How Tos` 46 | - `Tutorials` 47 | 48 | The odd one not generated from the convention of your folders in docsSrc is Api References. This is generated by the [XML Doc Comments](https://docs.microsoft.com/en-us/dotnet/csharp/codedoc) in your libraries under the `src` folder. 49 | 50 | 51 | ## Running docs tool 52 | 53 | ``` 54 | USAGE: docsTool [--help] [ []] 55 | 56 | SUBCOMMANDS: 57 | 58 | watch Builds the docs, serves the content, and watches for changes to the content. 59 | build Builds the docs 60 | 61 | Use 'docsTool --help' for additional information. 62 | ``` 63 | 64 | ### build 65 | 66 | Builds the docs 67 | 68 | ``` 69 | USAGE: docsTool build [--help] [--sitebaseurl ] [--projectglob ] [--docsoutputdirectory ] [--docssourcedirectory ] [--githubrepourl ] [--projectname ] 70 | [--releaseversion ] 71 | 72 | OPTIONS: 73 | 74 | --sitebaseurl 75 | The public site's base url. 76 | --projectglob 77 | The glob for the dlls to generate API documentation 78 | --docsoutputdirectory 79 | The docs output directory. 80 | --docssourcedirectory 81 | The docs source directory. 82 | --githubrepourl 83 | The GitHub repository url. 84 | --projectname 85 | The project name. 86 | --releaseversion 87 | The project's Release Version name. 88 | --help display this list of options. 89 | 90 | ``` 91 | 92 | 93 | ### watch 94 | 95 | Builds the docs, serves the content, and watches for changes to the content. 96 | 97 | ``` 98 | 99 | USAGE: docsTool watch [--help] [--projectglob ] [--docsoutputdirectory ] [--docssourcedirectory ] [--githubrepourl ] [--projectname ] [--releaseversion ] 100 | 101 | OPTIONS: 102 | 103 | --projectglob 104 | The glob for the dlls to generate API documentation. 105 | --docsoutputdirectory 106 | The docs output directory. 107 | --docssourcedirectory 108 | The docs source directory. 109 | --githubrepourl 110 | The GitHub repository url. 111 | --projectname 112 | The project name. 113 | --releaseversion 114 | The project's Release Version name. 115 | --help display this list of options. 116 | ``` 117 | 118 | -------------------------------------------------------------------------------- /docsTool/docsTool.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docsTool/paket.references: -------------------------------------------------------------------------------- 1 | group Docs 2 | Argu 3 | FSharp.Core 4 | Fake.IO.FileSystem 5 | Fake.DotNet.Cli 6 | FSharp.Literate 7 | Fable.React 8 | Dotnet.ProjInfo.Workspace.FCS 9 | -------------------------------------------------------------------------------- /docsTool/templates/helpers.fs: -------------------------------------------------------------------------------- 1 | module Helpers 2 | open System 3 | open Fable.React 4 | open Fable.React.Props 5 | open FSharp.MetadataFormat 6 | 7 | 8 | let createAnchorIcon name = 9 | let normalized = name 10 | let href = sprintf "#%s" normalized 11 | a [Href href; Id normalized] [ 12 | str "#" 13 | ] 14 | 15 | let createAnchor fullName name = 16 | let fullNameNormalize = fullName 17 | a [ 18 | Name fullNameNormalize 19 | Href (sprintf "#%s" fullNameNormalize) 20 | Class "anchor" 21 | ] [ 22 | str name 23 | ] 24 | 25 | let renderNamespace (ns: Namespace) = [ 26 | h3 [] [ str "Namespace" ] 27 | str ns.Name 28 | ] 29 | 30 | let inline isObsolete< ^t when ^t : (member IsObsolete: bool)> t = 31 | (^t : (member IsObsolete: bool) (t)) 32 | 33 | let inline obsoleteMessage< ^t when ^t : (member ObsoleteMessage: string)> t = 34 | (^t : (member ObsoleteMessage:string) (t)) 35 | 36 | let inline renderObsoleteMessage item = 37 | if isObsolete item 38 | then 39 | let text = match obsoleteMessage item with | "" | null -> "This member is obsolete" | s -> s 40 | [ 41 | div [Class "alert alert-warning"] [ 42 | strong [] [ str "OBSOLETE: "] 43 | str text 44 | ] 45 | ] 46 | else 47 | [] 48 | -------------------------------------------------------------------------------- /docsTool/templates/namespaces.fs: -------------------------------------------------------------------------------- 1 | module Namespaces 2 | 3 | open System 4 | open Fable.React 5 | open Fable.React.Props 6 | open FSharp.MetadataFormat 7 | 8 | 9 | type ByCategory = { 10 | Name : string 11 | Index : string 12 | Types : Type array 13 | Modules : Module array 14 | } 15 | 16 | let generateNamespaceDocs (asm : AssemblyGroup) (props) = 17 | let parts = 18 | asm.Namespaces 19 | |> Seq.mapi(fun nsi ns -> 20 | let allByCategories = 21 | ns.Types 22 | |> Seq.map(fun t -> t.Category) 23 | |> Seq.append (ns.Modules |> Seq.map(fun m -> m.Category)) 24 | |> Seq.distinct 25 | |> Seq.sortBy(fun s -> 26 | if String.IsNullOrEmpty(s) then "ZZZ" 27 | else s) 28 | |> Seq.mapi(fun ci c -> 29 | { 30 | Name = if String.IsNullOrEmpty(c) then "Other namespace members" else c 31 | Index = sprintf "%d_%d" nsi ci 32 | Types = ns.Types |> Seq.filter(fun t -> t.Category = c) |> Seq.toArray 33 | Modules = ns.Modules |> Seq.filter(fun m -> m.Category = c) |> Seq.toArray 34 | }) 35 | |> Seq.filter(fun c -> c.Types.Length + c.Modules.Length > 0) 36 | |> Seq.toArray 37 | [ 38 | yield h2 [] [ 39 | Helpers.createAnchor ns.Name ns.Name 40 | ] 41 | if allByCategories.Length > 1 then 42 | yield ul [] [ 43 | for c in allByCategories do 44 | yield 45 | li [] [ 46 | a [Href (sprintf "#section%s" c.Index)] [ 47 | str c.Name 48 | ] 49 | ] 50 | ] 51 | 52 | 53 | for c in allByCategories do 54 | if allByCategories.Length > 1 then 55 | yield h3 [] [ 56 | a [Class "anchor"; Name (sprintf "section%s" c.Index); Href (sprintf "#section%s" c.Index)] [ 57 | str c.Name 58 | ] 59 | ] 60 | yield! PartNested.partNested c.Types c.Modules 61 | ] 62 | ) 63 | |> Seq.collect id 64 | div [ Class "container-fluid py-3" ] [ 65 | div [ Class "row" ] [ 66 | div [ Class "col-12" ] [ 67 | yield h1 [] [ 68 | Helpers.createAnchor asm.Name asm.Name 69 | ] 70 | yield! parts 71 | ] 72 | ] 73 | ] 74 | -------------------------------------------------------------------------------- /docsTool/templates/partMembers.fs: -------------------------------------------------------------------------------- 1 | module PartMembers 2 | 3 | open System 4 | open Fable.React 5 | open Fable.React.Props 6 | open FSharp.MetadataFormat 7 | open System.Collections.Generic 8 | open Helpers 9 | 10 | type ModuleByCategory = { 11 | Index : int 12 | GroupKey : string 13 | Members : list 14 | Name : string 15 | } 16 | 17 | 18 | let signature (m : Member) = seq { 19 | if m.Details.Signature |> String.IsNullOrEmpty |> not then 20 | yield 21 | code [ Class "function-or-value"] [ 22 | str m.Details.Signature 23 | ] 24 | } 25 | 26 | let repoSourceLink (m: Member) = seq { 27 | if m.Details.FormatSourceLocation |> String.IsNullOrEmpty |> not then 28 | yield a [ 29 | Href m.Details.FormatSourceLocation 30 | Class "float-right" 31 | HTMLAttr.Custom("aria-label", "View source on GitHub") 32 | ] [ 33 | yield i [ 34 | Class "fab fa-github text-dark" 35 | ] [] 36 | ] 37 | } 38 | 39 | let replaceh2withh5 (content : string) = 40 | content.Replace("

    ", "

    ") 41 | 42 | 43 | let normalize (content : string) = 44 | content 45 | |> replaceh2withh5 46 | 47 | 48 | 49 | let commentBlock (c: Comment) = 50 | let (|EmptyDefaultBlock|NonEmptyDefaultBlock|Section|) (KeyValue(section, content)) = 51 | match section, content with 52 | | "", c when String.IsNullOrEmpty c -> EmptyDefaultBlock 53 | | "", c -> NonEmptyDefaultBlock c 54 | | section, content -> Section (section, content) 55 | 56 | let renderSection (s : KeyValuePair): Fable.React.ReactElement list = 57 | match s with 58 | | EmptyDefaultBlock -> [] 59 | | NonEmptyDefaultBlock content -> [ div [ Class "comment-block" ] [ RawText (normalize content) ] ] 60 | | Section(name, content) -> [ h5 [] [ str name ] // h2 is obnoxiously large for this context, go with the smaller h5 61 | RawText (normalize content) ] 62 | c.Sections 63 | |> List.collect renderSection 64 | 65 | let compiledName (m: Member) = seq { 66 | if m.Details.FormatCompiledName |> String.IsNullOrEmpty |> not then 67 | yield p [] [ 68 | strong [] [ str "CompiledName:" ] 69 | code [] [ str m.Details.FormatCompiledName ] 70 | ] 71 | } 72 | 73 | let partMembers (header : string) (tableHeader : string) (members : #seq) = [ 74 | if members |> Seq.length > 0 then 75 | yield h3 [] [ 76 | str header 77 | ] 78 | 79 | yield table [ 80 | Class "table" 81 | ] [ 82 | thead [] [ 83 | 84 | tr [] [ 85 | th [Class "fit"] [ 86 | 87 | ] 88 | th [] [ 89 | str tableHeader 90 | ] 91 | 92 | th [] [ 93 | str "Signature" 94 | ] 95 | 96 | th [] [ 97 | str "Description" 98 | ] 99 | ] 100 | ] 101 | tbody [] [ 102 | for it in members do 103 | let id = Guid.NewGuid().ToString() 104 | yield tr [] [ 105 | td [] [ 106 | Helpers.createAnchorIcon (it.Details.FormatUsage(40)) 107 | ] 108 | td [ 109 | Class "member-name" 110 | ] [ 111 | code [ 112 | Class "function-or-value" 113 | HTMLAttr.Custom("data-guid", id) 114 | ] [ 115 | str (it.Details.FormatUsage(40)) 116 | ] 117 | ] 118 | td [ 119 | Class "member-name" 120 | ] [ 121 | yield! signature it 122 | ] 123 | 124 | td [ 125 | Class "xmldoc" 126 | ] [ 127 | yield! renderObsoleteMessage it 128 | yield! repoSourceLink it 129 | yield! commentBlock it.Comment 130 | yield! compiledName it 131 | ] 132 | ] 133 | ] 134 | ] 135 | ] 136 | -------------------------------------------------------------------------------- /docsTool/templates/partNested.fs: -------------------------------------------------------------------------------- 1 | module PartNested 2 | 3 | open System 4 | open Fable.React 5 | open Fable.React.Props 6 | open FSharp.MetadataFormat 7 | open Helpers 8 | 9 | let partNested (types : Type array) (modules : Module array) = 10 | [ 11 | if types.Length > 0 then 12 | yield table [ Class "table" ] [ 13 | thead [] [ 14 | tr [] [ 15 | th [Class "fit"] [ 16 | 17 | ] 18 | th [] [ 19 | str "Type" 20 | ] 21 | th [] [ 22 | str "Description" 23 | ] 24 | ] 25 | ] 26 | tbody [] [ 27 | for t in types do 28 | yield tr [] [ 29 | td [] [ 30 | Helpers.createAnchorIcon t.Name 31 | ] 32 | td [Class "type-name"] [ 33 | a [Href (sprintf "%s.html" t.UrlName)] [ 34 | str t.Name 35 | ] 36 | ] 37 | td [Class "xmldoc"] [ 38 | yield! renderObsoleteMessage t 39 | yield RawText t.Comment.Blurb 40 | ] 41 | ] 42 | ] 43 | ] 44 | if modules.Length > 0 then 45 | yield table [ Class "table" ] [ 46 | thead [] [ 47 | tr [] [ 48 | th [Class "fit"] [ 49 | 50 | ] 51 | th [] [ 52 | str "Module" 53 | ] 54 | th [] [ 55 | str "Description" 56 | ] 57 | ] 58 | ] 59 | tbody [] [ 60 | for t in modules do 61 | yield tr [] [ 62 | td [] [ 63 | Helpers.createAnchorIcon t.Name 64 | ] 65 | td [Class "Modules-name"] [ 66 | a [Href (sprintf "%s.html" t.UrlName)] [ 67 | str t.Name 68 | ] 69 | ] 70 | td [Class "xmldoc"] [ 71 | yield! renderObsoleteMessage t 72 | yield RawText t.Comment.Blurb 73 | ] 74 | ] 75 | ] 76 | ] 77 | ] 78 | -------------------------------------------------------------------------------- /docsTool/templates/types.fs: -------------------------------------------------------------------------------- 1 | module Types 2 | 3 | open System 4 | open Fable.React 5 | open Fable.React.Props 6 | open FSharp.MetadataFormat 7 | open PartMembers 8 | open Helpers 9 | 10 | let generateTypeDocs (model : TypeInfo) (props) = 11 | let members = model.Type.AllMembers 12 | let comment = model.Type.Comment 13 | let ``type`` = model.Type 14 | let byCategory = 15 | members 16 | |> List.groupBy (fun m -> m.Category) 17 | |> List.sortBy (fun (k,v) -> if String.IsNullOrEmpty(k) then "ZZZ" else k ) 18 | |> List.mapi (fun i (k,v) -> { 19 | Index = i 20 | GroupKey = k 21 | Members = v |> List.sortBy (fun m -> if m.Kind = MemberKind.StaticParameter then "" else m.Name) 22 | Name = if String.IsNullOrEmpty(k) then "Other type members" else k 23 | }) 24 | [ 25 | yield h1 [] [ 26 | str model.Type.Name 27 | ] 28 | 29 | yield p [] [ 30 | yield! renderObsoleteMessage model.Type 31 | yield! renderNamespace model.Namespace 32 | if model.HasParentModule then 33 | yield br [] 34 | yield span [] [ 35 | str "Parent Module: " 36 | 37 | a [ 38 | Href (sprintf "%s.html" model.ParentModule.Value.UrlName) 39 | ] [ 40 | str model.ParentModule.Value.Name 41 | ] 42 | ] 43 | 44 | 45 | if ``type``.Attributes |> Seq.isEmpty |> not then 46 | yield br [] 47 | yield span [] [ 48 | yield str "Attributes: " 49 | 50 | yield br [] 51 | 52 | for attr in ``type``.Attributes do 53 | yield str (attr.Format()) 54 | yield br [] 55 | ] 56 | ] 57 | 58 | yield div [ 59 | Class "xmldoc" 60 | ] [ 61 | for sec in comment.Sections do 62 | if byCategory |> Seq.exists (fun m -> m.GroupKey = sec.Key) |> not then 63 | if sec.Key <> "" then 64 | yield h2 [] [ 65 | str sec.Key 66 | ] 67 | yield RawText sec.Value 68 | ] 69 | 70 | if byCategory |> Seq.length > 1 then 71 | yield h2 [] [ 72 | str "Table of contents" 73 | ] 74 | 75 | yield ul [] [ 76 | for g in byCategory do 77 | yield li [] [ 78 | a [ 79 | Href (sprintf "#section%d" g.Index) 80 | ] [ 81 | str g.Name 82 | ] 83 | ] 84 | ] 85 | 86 | for g in byCategory do 87 | if byCategory |> Seq.length > 1 then 88 | yield h2 [] [ 89 | str g.Name 90 | 91 | a [ 92 | Name (sprintf "section%d" g.Index) 93 | ] [ 94 | str " " 95 | ] 96 | ] 97 | 98 | match comment.Sections |> Seq.tryFind (fun kvp -> kvp.Key = g.GroupKey) with 99 | | Some info -> 100 | yield div [ 101 | Class "xmldoc" 102 | ] [ 103 | str info.Value 104 | ] 105 | | None -> yield nothing 106 | 107 | yield! partMembers "Union Cases" "Union Case" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.UnionCase)) 108 | yield! partMembers "Record Fields" "Record Field" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.RecordField)) 109 | yield! partMembers "Static parameters" "Static parameters" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.StaticParameter)) 110 | yield! partMembers "Contructors" "Constructor" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.Constructor)) 111 | yield! partMembers "Instance members" "Instance member" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.InstanceMember)) 112 | yield! partMembers "Static members" "Static member" (g.Members |> Seq.filter(fun m -> m.Kind = MemberKind.StaticMember)) 113 | ] 114 | -------------------------------------------------------------------------------- /icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/icon.jpg -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | version 6.2.1 2 | source https://www.nuget.org/api/v2/ 3 | source https://api.nuget.org/v3/index.json 4 | 5 | framework: net6.0 6 | 7 | nuget NuGet.CommandLine 8 | nuget FSharp.Core 9 | nuget Selenium.WebDriver 10 | nuget FAKE prerelease 11 | nuget FSharp.Formatting 12 | nuget FSharp.Data 13 | nuget Selenium.WebDriver.ChromeDriver 14 | nuget Selenium.WebDriver.GeckoDriver 15 | nuget System.Drawing.Common >= 4.6.0 16 | nuget System.Text.Encoding.CodePages = 5.0.0 17 | nuget System.Runtime.CompilerServices.Unsafe = 6.0.0 18 | 19 | // [ FAKE GROUP ] 20 | group Build 21 | source https://www.nuget.org/api/v2 22 | source https://api.nuget.org/v3/index.json 23 | framework: net6.0 24 | storage: none 25 | nuget Expecto 26 | nuget YoloDev.Expecto.TestSdk 27 | nuget Microsoft.NET.Test.Sdk 28 | nuget Fake.IO.FileSystem 29 | nuget Fake.Core.Target 30 | nuget Fake.Core.ReleaseNotes 31 | nuget FAKE.Core.Environment 32 | nuget Fake.DotNet.Cli 33 | nuget FAKE.Core.Process 34 | nuget Fake.DotNet.AssemblyInfoFile 35 | nuget Fake.Tools.Git 36 | nuget Fake.DotNet.Paket 37 | nuget Fake.Api.GitHub 38 | nuget Fake.BuildServer.AppVeyor 39 | nuget Fake.BuildServer.Travis 40 | nuget Argu 41 | 42 | group Docs 43 | storage: none 44 | source https://www.nuget.org/api/v2 45 | source https://api.nuget.org/v3/index.json 46 | nuget Argu 47 | nuget FSharp.Core 48 | nuget Fake.IO.FileSystem 49 | nuget FAKE.Core.Environment 50 | nuget Fake.DotNet.Cli 51 | nuget FSharp.Literate 52 | nuget Fable.React 53 | nuget Dotnet.ProjInfo.Workspace.FCS -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | false 6 | 7 | true 8 | true 9 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/canopy.integration/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "canopy.integration" 17 | let [] AssemblyProduct = "canopy" 18 | let [] AssemblyVersion = "2.1.3" 19 | let [] AssemblyMetadata_ReleaseDate = "2020-03-07T00:00:00.0000000-06:00" 20 | let [] AssemblyFileVersion = "2.1.3" 21 | let [] AssemblyInformationalVersion = "2.1.3" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "d4bd2b919c8ab9142ed6bca402415c92d76c5eab" 24 | -------------------------------------------------------------------------------- /src/canopy.integration/canopy.integration.XML: -------------------------------------------------------------------------------- 1 | 2 | 3 | canopy.integration 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/canopy.integration/canopy.integration.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 6 | 7 | NU1701 8 | 9 | 10 | .\canopy.integration.XML 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/canopy.integration/chsharpLoadTest.fs: -------------------------------------------------------------------------------- 1 | namespace canopy.csharp.loadTest 2 | 3 | type task(description:string, action:System.Action, frequency:int) = 4 | member this.Description = description 5 | member this.Action = action 6 | member this.Frequency = frequency 7 | 8 | type job(warmup:bool, baseline:bool, acceptableRatioPercent:int, minutes:int, load: int, tasks:ResizeArray) = 9 | member this.Warmup = warmup 10 | member this.Baseline = baseline 11 | member this.AcceptableRatioPercent = acceptableRatioPercent 12 | member this.Minutes = minutes 13 | member this.Load = load 14 | member this.Tasks = tasks 15 | 16 | open canopy.integration.loadTest 17 | 18 | type runner () = 19 | static member run (job:job) = 20 | let newJob = 21 | { 22 | Warmup = job.Warmup 23 | Baseline = job.Baseline 24 | AcceptableRatioPercent = job.AcceptableRatioPercent 25 | Minutes = job.Minutes 26 | Load = job.Load 27 | Tasks = job.Tasks 28 | |> Seq.map(fun task -> { Description = task.Description; Frequency = task.Frequency; Action = fun () -> task.Action.Invoke(); }) 29 | |> List.ofSeq 30 | } 31 | 32 | runLoadTest newJob 33 | -------------------------------------------------------------------------------- /src/canopy.integration/csharp.fs: -------------------------------------------------------------------------------- 1 | namespace canopy.csharp 2 | 3 | type integration () = 4 | 5 | static member diffJson example actual = 6 | let diff = jsonValidator.diff example actual 7 | let diffString = diff |> List.map (fun d -> match d with | jsonValidator.Missing s -> sprintf "Missing %s" s | jsonValidator.Extra s -> sprintf "Extra %s" s) 8 | ResizeArray(diffString) 9 | 10 | static member validateJson example actual = jsonValidator.validate example actual -------------------------------------------------------------------------------- /src/canopy.integration/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | FSharp.Data -------------------------------------------------------------------------------- /src/canopy/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "canopy" 17 | let [] AssemblyProduct = "canopy" 18 | let [] AssemblyVersion = "2.1.3" 19 | let [] AssemblyMetadata_ReleaseDate = "2020-03-07T00:00:00.0000000-06:00" 20 | let [] AssemblyFileVersion = "2.1.3" 21 | let [] AssemblyInformationalVersion = "2.1.3" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "d4bd2b919c8ab9142ed6bca402415c92d76c5eab" 24 | -------------------------------------------------------------------------------- /src/canopy/canopy.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/canopy/canopy.paket.template: -------------------------------------------------------------------------------- 1 | type file 2 | id 3 | canopy 4 | version 5 | 2.1.1 6 | authors 7 | Chris Holt 8 | iconUrl 9 | http://lefthandedgoat.github.io/canopy/canopy_orig.jpg 10 | projectUrl 11 | http://lefthandedgoat.github.io/canopy/ 12 | licenseUrl 13 | https://github.com/lefthandedgoat/canopy/blob/master/LICENSE.txt 14 | tags 15 | f# fsharp canopy selenium ui automation tests 16 | description 17 | A simple framework in F# on top of selenium for writing UI automation and tests. 18 | requireLicenseAcceptance 19 | false 20 | files 21 | bin\Debug\netstandard2.0 ==> lib\netstandard2.0 22 | 23 | releaseNotes 24 | 2.1.1 - October 23 2019 25 | Added option to select WebDriver port and allow insecure SSL cert 26 | Thanks @OmanF (https://github.com/lefthandedgoat/canopy/pull/482) 27 | 28 | dependencies 29 | framework: netstandard2.0 30 | FSharp.Core >= 4.3.4 31 | Selenium.WebDriver >= 3.14.0 32 | System.Drawing.Common >= 4.5.0 -------------------------------------------------------------------------------- /src/canopy/companion/crx/background.js: -------------------------------------------------------------------------------- 1 | chrome.browserAction.onClicked.addListener(function(tab) { 2 | chrome.tabs.insertCSS(tab.id, { file: "styles.css" }); 3 | chrome.tabs.executeScript(tab.id, { file: "bundle.js" }); 4 | }); -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/Array.js: -------------------------------------------------------------------------------- 1 | export function addRangeInPlace(range, xs) { 2 | var iter = range[Symbol.iterator](); 3 | var cur = iter.next(); 4 | while (!cur.done) { 5 | xs.push(cur.value); 6 | cur = iter.next(); 7 | } 8 | } 9 | export function copyTo(source, sourceIndex, target, targetIndex, count) { 10 | while (count--) 11 | target[targetIndex++] = source[sourceIndex++]; 12 | } 13 | export function partition(f, xs) { 14 | var ys = [], zs = [], j = 0, k = 0; 15 | for (var i = 0; i < xs.length; i++) 16 | if (f(xs[i])) 17 | ys[j++] = xs[i]; 18 | else 19 | zs[k++] = xs[i]; 20 | return [ys, zs]; 21 | } 22 | export function permute(f, xs) { 23 | var ys = xs.map(function () { return null; }); 24 | var checkFlags = new Array(xs.length); 25 | for (var i = 0; i < xs.length; i++) { 26 | var j = f(i); 27 | if (j < 0 || j >= xs.length) 28 | throw new Error("Not a valid permutation"); 29 | ys[j] = xs[i]; 30 | checkFlags[j] = 1; 31 | } 32 | for (var i = 0; i < xs.length; i++) 33 | if (checkFlags[i] != 1) 34 | throw new Error("Not a valid permutation"); 35 | return ys; 36 | } 37 | export function removeInPlace(item, xs) { 38 | var i = xs.indexOf(item); 39 | if (i > -1) { 40 | xs.splice(i, 1); 41 | return true; 42 | } 43 | return false; 44 | } 45 | export function setSlice(target, lower, upper, source) { 46 | var length = (upper || target.length - 1) - lower; 47 | if (ArrayBuffer.isView(target) && source.length <= length) 48 | target.set(source, lower); 49 | else 50 | for (var i = lower | 0, j = 0; j <= length; i++, j++) 51 | target[i] = source[j]; 52 | } 53 | export function sortInPlaceBy(f, xs, dir) { 54 | if (dir === void 0) { dir = 1; } 55 | return xs.sort(function (x, y) { 56 | x = f(x); 57 | y = f(y); 58 | return (x < y ? -1 : x == y ? 0 : 1) * dir; 59 | }); 60 | } 61 | export function unzip(xs) { 62 | var bs = new Array(xs.length), cs = new Array(xs.length); 63 | for (var i = 0; i < xs.length; i++) { 64 | bs[i] = xs[i][0]; 65 | cs[i] = xs[i][1]; 66 | } 67 | return [bs, cs]; 68 | } 69 | export function unzip3(xs) { 70 | var bs = new Array(xs.length), cs = new Array(xs.length), ds = new Array(xs.length); 71 | for (var i = 0; i < xs.length; i++) { 72 | bs[i] = xs[i][0]; 73 | cs[i] = xs[i][1]; 74 | ds[i] = xs[i][2]; 75 | } 76 | return [bs, cs, ds]; 77 | } 78 | export function getSubArray(xs, startIndex, count) { 79 | return xs.slice(startIndex, startIndex + count); 80 | } 81 | export function fill(target, targetIndex, count, value) { 82 | target.fill(value, targetIndex, targetIndex + count); 83 | } 84 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/Assert.js: -------------------------------------------------------------------------------- 1 | var __extends = (this && this.__extends) || function (d, b) { 2 | for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; 3 | function __() { this.constructor = d; } 4 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 5 | }; 6 | import { equals } from "./Util"; 7 | var AssertionError = (function (_super) { 8 | __extends(AssertionError, _super); 9 | function AssertionError(msg, actual, expected) { 10 | var _this = _super.call(this, msg) || this; 11 | _this.actual = actual; 12 | _this.expected = expected; 13 | return _this; 14 | } 15 | return AssertionError; 16 | }(Error)); 17 | export { AssertionError }; 18 | export function equal(actual, expected, msg) { 19 | if (!equals(actual, expected)) { 20 | throw new AssertionError(msg || "Expected: " + expected + " - Actual: " + actual, actual, expected); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/Async.js: -------------------------------------------------------------------------------- 1 | import { Trampoline } from "./AsyncBuilder"; 2 | import { protectedCont } from "./AsyncBuilder"; 3 | import { protectedBind } from "./AsyncBuilder"; 4 | import { protectedReturn } from "./AsyncBuilder"; 5 | import { choice1Of2 } from "./Choice"; 6 | import { choice2Of2 } from "./Choice"; 7 | import { map } from "./Seq"; 8 | var Async = (function () { 9 | function Async() { 10 | } 11 | return Async; 12 | }()); 13 | export default Async; 14 | function emptyContinuation(x) { 15 | } 16 | export function awaitPromise(p) { 17 | return fromContinuations(function (conts) { 18 | return p.then(conts[0]).catch(function (err) { 19 | return (err == "cancelled" ? conts[2] : conts[1])(err); 20 | }); 21 | }); 22 | } 23 | export function cancellationToken() { 24 | return protectedCont(function (ctx) { return ctx.onSuccess(ctx.cancelToken); }); 25 | } 26 | export var defaultCancellationToken = { isCancelled: false }; 27 | export function catchAsync(work) { 28 | return protectedCont(function (ctx) { 29 | work({ 30 | onSuccess: function (x) { return ctx.onSuccess(choice1Of2(x)); }, 31 | onError: function (ex) { return ctx.onSuccess(choice2Of2(ex)); }, 32 | onCancel: ctx.onCancel, 33 | cancelToken: ctx.cancelToken, 34 | trampoline: ctx.trampoline 35 | }); 36 | }); 37 | } 38 | export function fromContinuations(f) { 39 | return protectedCont(function (ctx) { return f([ctx.onSuccess, ctx.onError, ctx.onCancel]); }); 40 | } 41 | export function ignore(computation) { 42 | return protectedBind(computation, function (x) { return protectedReturn(void 0); }); 43 | } 44 | export function parallel(computations) { 45 | return awaitPromise(Promise.all(map(function (w) { return startAsPromise(w); }, computations))); 46 | } 47 | export function sleep(millisecondsDueTime) { 48 | return protectedCont(function (ctx) { 49 | setTimeout(function () { return ctx.cancelToken.isCancelled ? ctx.onCancel("cancelled") : ctx.onSuccess(void 0); }, millisecondsDueTime); 50 | }); 51 | } 52 | export function start(computation, cancellationToken) { 53 | return startWithContinuations(computation, cancellationToken); 54 | } 55 | export function startImmediate(computation, cancellationToken) { 56 | return start(computation, cancellationToken); 57 | } 58 | export function startWithContinuations(computation, continuation, exceptionContinuation, cancellationContinuation, cancelToken) { 59 | if (typeof continuation !== "function") { 60 | cancelToken = continuation; 61 | continuation = null; 62 | } 63 | var trampoline = new Trampoline(); 64 | computation({ 65 | onSuccess: continuation ? continuation : emptyContinuation, 66 | onError: exceptionContinuation ? exceptionContinuation : emptyContinuation, 67 | onCancel: cancellationContinuation ? cancellationContinuation : emptyContinuation, 68 | cancelToken: cancelToken ? cancelToken : defaultCancellationToken, 69 | trampoline: trampoline 70 | }); 71 | } 72 | export function startAsPromise(computation, cancellationToken) { 73 | return new Promise(function (resolve, reject) { 74 | return startWithContinuations(computation, resolve, reject, reject, cancellationToken ? cancellationToken : defaultCancellationToken); 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/AsyncBuilder.js: -------------------------------------------------------------------------------- 1 | var Trampoline = (function () { 2 | function Trampoline() { 3 | this.callCount = 0; 4 | } 5 | Object.defineProperty(Trampoline, "maxTrampolineCallCount", { 6 | get: function () { 7 | return 2000; 8 | }, 9 | enumerable: true, 10 | configurable: true 11 | }); 12 | Trampoline.prototype.incrementAndCheck = function () { 13 | return this.callCount++ > Trampoline.maxTrampolineCallCount; 14 | }; 15 | Trampoline.prototype.hijack = function (f) { 16 | this.callCount = 0; 17 | setTimeout(f, 0); 18 | }; 19 | return Trampoline; 20 | }()); 21 | export { Trampoline }; 22 | export function protectedCont(f) { 23 | return function (ctx) { 24 | if (ctx.cancelToken.isCancelled) 25 | ctx.onCancel("cancelled"); 26 | else if (ctx.trampoline.incrementAndCheck()) 27 | ctx.trampoline.hijack(function () { 28 | try { 29 | f(ctx); 30 | } 31 | catch (err) { 32 | ctx.onError(err); 33 | } 34 | }); 35 | else 36 | try { 37 | f(ctx); 38 | } 39 | catch (err) { 40 | ctx.onError(err); 41 | } 42 | }; 43 | } 44 | export function protectedBind(computation, binder) { 45 | return protectedCont(function (ctx) { 46 | computation({ 47 | onSuccess: function (x) { return binder(x)(ctx); }, 48 | onError: ctx.onError, 49 | onCancel: ctx.onCancel, 50 | cancelToken: ctx.cancelToken, 51 | trampoline: ctx.trampoline 52 | }); 53 | }); 54 | } 55 | export function protectedReturn(value) { 56 | return protectedCont(function (ctx) { return ctx.onSuccess(value); }); 57 | } 58 | var AsyncBuilder = (function () { 59 | function AsyncBuilder() { 60 | } 61 | AsyncBuilder.prototype.Bind = function (computation, binder) { 62 | return protectedBind(computation, binder); 63 | }; 64 | AsyncBuilder.prototype.Combine = function (computation1, computation2) { 65 | return this.Bind(computation1, function () { return computation2; }); 66 | }; 67 | AsyncBuilder.prototype.Delay = function (generator) { 68 | return protectedCont(function (ctx) { return generator()(ctx); }); 69 | }; 70 | AsyncBuilder.prototype.For = function (sequence, body) { 71 | var iter = sequence[Symbol.iterator](); 72 | var cur = iter.next(); 73 | return this.While(function () { return !cur.done; }, this.Delay(function () { 74 | var res = body(cur.value); 75 | cur = iter.next(); 76 | return res; 77 | })); 78 | }; 79 | AsyncBuilder.prototype.Return = function (value) { 80 | return protectedReturn(value); 81 | }; 82 | AsyncBuilder.prototype.ReturnFrom = function (computation) { 83 | return computation; 84 | }; 85 | AsyncBuilder.prototype.TryFinally = function (computation, compensation) { 86 | return protectedCont(function (ctx) { 87 | computation({ 88 | onSuccess: function (x) { 89 | compensation(); 90 | ctx.onSuccess(x); 91 | }, 92 | onError: function (x) { 93 | compensation(); 94 | ctx.onError(x); 95 | }, 96 | onCancel: function (x) { 97 | compensation(); 98 | ctx.onCancel(x); 99 | }, 100 | cancelToken: ctx.cancelToken, 101 | trampoline: ctx.trampoline 102 | }); 103 | }); 104 | }; 105 | AsyncBuilder.prototype.TryWith = function (computation, catchHandler) { 106 | return protectedCont(function (ctx) { 107 | computation({ 108 | onSuccess: ctx.onSuccess, 109 | onCancel: ctx.onCancel, 110 | cancelToken: ctx.cancelToken, 111 | trampoline: ctx.trampoline, 112 | onError: function (ex) { 113 | try { 114 | catchHandler(ex)(ctx); 115 | } 116 | catch (ex2) { 117 | ctx.onError(ex2); 118 | } 119 | } 120 | }); 121 | }); 122 | }; 123 | AsyncBuilder.prototype.Using = function (resource, binder) { 124 | return this.TryFinally(binder(resource), function () { return resource.Dispose(); }); 125 | }; 126 | AsyncBuilder.prototype.While = function (guard, computation) { 127 | var _this = this; 128 | if (guard()) 129 | return this.Bind(computation, function () { return _this.While(guard, computation); }); 130 | else 131 | return this.Return(void 0); 132 | }; 133 | AsyncBuilder.prototype.Zero = function () { 134 | return protectedCont(function (ctx) { return ctx.onSuccess(void 0); }); 135 | }; 136 | return AsyncBuilder; 137 | }()); 138 | export { AsyncBuilder }; 139 | export var singleton = new AsyncBuilder(); 140 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/BitConverter.js: -------------------------------------------------------------------------------- 1 | import * as Long from "./Long"; 2 | var littleEndian = true; 3 | export function isLittleEndian() { 4 | return littleEndian; 5 | } 6 | export function getBytesBoolean(value) { 7 | var bytes = new Uint8Array(1); 8 | new DataView(bytes.buffer).setUint8(0, value ? 1 : 0); 9 | return bytes; 10 | } 11 | export function getBytesChar(value) { 12 | var bytes = new Uint8Array(2); 13 | new DataView(bytes.buffer).setUint16(0, value.charCodeAt(0), littleEndian); 14 | return bytes; 15 | } 16 | export function getBytesInt16(value) { 17 | var bytes = new Uint8Array(2); 18 | new DataView(bytes.buffer).setInt16(0, value, littleEndian); 19 | return bytes; 20 | } 21 | export function getBytesInt32(value) { 22 | var bytes = new Uint8Array(4); 23 | new DataView(bytes.buffer).setInt32(0, value, littleEndian); 24 | return bytes; 25 | } 26 | export function getBytesInt64(value) { 27 | var bytes = new Uint8Array(8); 28 | new DataView(bytes.buffer).setInt32(littleEndian ? 0 : 4, value.getLowBits(), littleEndian); 29 | new DataView(bytes.buffer).setInt32(littleEndian ? 4 : 0, value.getHighBits(), littleEndian); 30 | return bytes; 31 | } 32 | export function getBytesUInt16(value) { 33 | var bytes = new Uint8Array(2); 34 | new DataView(bytes.buffer).setUint16(0, value, littleEndian); 35 | return bytes; 36 | } 37 | export function getBytesUInt32(value) { 38 | var bytes = new Uint8Array(4); 39 | new DataView(bytes.buffer).setUint32(0, value, littleEndian); 40 | return bytes; 41 | } 42 | export function getBytesUInt64(value) { 43 | var bytes = new Uint8Array(8); 44 | new DataView(bytes.buffer).setUint32(littleEndian ? 0 : 4, value.getLowBitsUnsigned(), littleEndian); 45 | new DataView(bytes.buffer).setUint32(littleEndian ? 4 : 0, value.getHighBitsUnsigned(), littleEndian); 46 | return bytes; 47 | } 48 | export function getBytesSingle(value) { 49 | var bytes = new Uint8Array(4); 50 | new DataView(bytes.buffer).setFloat32(0, value, littleEndian); 51 | return bytes; 52 | } 53 | export function getBytesDouble(value) { 54 | var bytes = new Uint8Array(8); 55 | new DataView(bytes.buffer).setFloat64(0, value, littleEndian); 56 | return bytes; 57 | } 58 | export function int64BitsToDouble(value) { 59 | var buffer = new ArrayBuffer(8); 60 | new DataView(buffer).setInt32(littleEndian ? 0 : 4, value.getLowBits(), littleEndian); 61 | new DataView(buffer).setInt32(littleEndian ? 4 : 0, value.getHighBits(), littleEndian); 62 | return new DataView(buffer).getFloat64(0, littleEndian); 63 | } 64 | export function doubleToInt64Bits(value) { 65 | var buffer = new ArrayBuffer(8); 66 | new DataView(buffer).setFloat64(0, value, littleEndian); 67 | var lowBits = new DataView(buffer).getInt32(littleEndian ? 0 : 4, littleEndian); 68 | var highBits = new DataView(buffer).getInt32(littleEndian ? 4 : 0, littleEndian); 69 | return Long.fromBits(lowBits, highBits, false); 70 | } 71 | export function toBoolean(bytes, offset) { 72 | return new DataView(bytes.buffer).getUint8(offset) === 1 ? true : false; 73 | } 74 | export function toChar(bytes, offset) { 75 | var code = new DataView(bytes.buffer).getUint16(offset, littleEndian); 76 | return String.fromCharCode(code); 77 | } 78 | export function toInt16(bytes, offset) { 79 | return new DataView(bytes.buffer).getInt16(offset, littleEndian); 80 | } 81 | export function toInt32(bytes, offset) { 82 | return new DataView(bytes.buffer).getInt32(offset, littleEndian); 83 | } 84 | export function toInt64(bytes, offset) { 85 | var lowBits = new DataView(bytes.buffer).getInt32(offset + (littleEndian ? 0 : 4), littleEndian); 86 | var highBits = new DataView(bytes.buffer).getInt32(offset + (littleEndian ? 4 : 0), littleEndian); 87 | return Long.fromBits(lowBits, highBits, false); 88 | } 89 | export function toUInt16(bytes, offset) { 90 | return new DataView(bytes.buffer).getUint16(offset, littleEndian); 91 | } 92 | export function toUInt32(bytes, offset) { 93 | return new DataView(bytes.buffer).getUint32(offset, littleEndian); 94 | } 95 | export function toUInt64(bytes, offset) { 96 | var lowBits = new DataView(bytes.buffer).getUint32(offset + (littleEndian ? 0 : 4), littleEndian); 97 | var highBits = new DataView(bytes.buffer).getUint32(offset + (littleEndian ? 4 : 0), littleEndian); 98 | return Long.fromBits(lowBits, highBits, true); 99 | } 100 | export function toSingle(bytes, offset) { 101 | return new DataView(bytes.buffer).getFloat32(offset, littleEndian); 102 | } 103 | export function toDouble(bytes, offset) { 104 | return new DataView(bytes.buffer).getFloat64(offset, littleEndian); 105 | } 106 | export function toString(bytes, offset, count) { 107 | var ar = bytes; 108 | if (typeof offset !== "undefined" && typeof count !== "undefined") 109 | ar = bytes.subarray(offset, offset + count); 110 | else if (typeof offset !== "undefined") 111 | ar = bytes.subarray(offset); 112 | return Array.from(ar).map(function (b) { return ("0" + b.toString(16)).slice(-2); }).join("-"); 113 | } 114 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/Choice.js: -------------------------------------------------------------------------------- 1 | import FSymbol from "./Symbol"; 2 | import { equalsUnions } from "./Util"; 3 | import { compareUnions } from "./Util"; 4 | export function choice1Of2(v) { 5 | return new Choice("Choice1Of2", [v]); 6 | } 7 | export function choice2Of2(v) { 8 | return new Choice("Choice2Of2", [v]); 9 | } 10 | var Choice = (function () { 11 | function Choice(t, d) { 12 | this.Case = t; 13 | this.Fields = d; 14 | } 15 | Object.defineProperty(Choice.prototype, "valueIfChoice1", { 16 | get: function () { 17 | return this.Case === "Choice1Of2" ? this.Fields[0] : null; 18 | }, 19 | enumerable: true, 20 | configurable: true 21 | }); 22 | Object.defineProperty(Choice.prototype, "valueIfChoice2", { 23 | get: function () { 24 | return this.Case === "Choice2Of2" ? this.Fields[0] : null; 25 | }, 26 | enumerable: true, 27 | configurable: true 28 | }); 29 | Choice.prototype.Equals = function (other) { 30 | return equalsUnions(this, other); 31 | }; 32 | Choice.prototype.CompareTo = function (other) { 33 | return compareUnions(this, other); 34 | }; 35 | Choice.prototype[FSymbol.reflection] = function () { 36 | return { 37 | type: "Microsoft.FSharp.Core.FSharpChoice", 38 | interfaces: ["FSharpUnion", "System.IEquatable", "System.IComparable"] 39 | }; 40 | }; 41 | return Choice; 42 | }()); 43 | export default Choice; 44 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/GenericComparer.js: -------------------------------------------------------------------------------- 1 | import { compare } from "./Util"; 2 | import FSymbol from "./Symbol"; 3 | var GenericComparer = (function () { 4 | function GenericComparer(f) { 5 | this.Compare = f || compare; 6 | } 7 | GenericComparer.prototype[FSymbol.reflection] = function () { 8 | return { interfaces: ["System.IComparer"] }; 9 | }; 10 | return GenericComparer; 11 | }()); 12 | export default GenericComparer; 13 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/Lazy.js: -------------------------------------------------------------------------------- 1 | export function createFromValue(v) { 2 | return new Lazy(function () { return v; }); 3 | } 4 | var Lazy = (function () { 5 | function Lazy(factory) { 6 | this.factory = factory; 7 | this.isValueCreated = false; 8 | } 9 | Object.defineProperty(Lazy.prototype, "value", { 10 | get: function () { 11 | if (!this.isValueCreated) { 12 | this.createdValue = this.factory(); 13 | this.isValueCreated = true; 14 | } 15 | return this.createdValue; 16 | }, 17 | enumerable: true, 18 | configurable: true 19 | }); 20 | return Lazy; 21 | }()); 22 | export default Lazy; 23 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/List.js: -------------------------------------------------------------------------------- 1 | import List from "./ListClass"; 2 | import { map as seqMap } from "./Seq"; 3 | import { fold as seqFold } from "./Seq"; 4 | import { foldBack as seqFoldBack } from "./Seq"; 5 | import { toList as seqToList } from "./Seq"; 6 | import { groupBy as mapGroupBy } from "./Map"; 7 | export default List; 8 | export { ofArray } from "./ListClass"; 9 | export function append(xs, ys) { 10 | return seqFold(function (acc, x) { return new List(x, acc); }, ys, reverse(xs)); 11 | } 12 | export function choose(f, xs) { 13 | var r = seqFold(function (acc, x) { 14 | var y = f(x); 15 | return y != null ? new List(y, acc) : acc; 16 | }, new List(), xs); 17 | return reverse(r); 18 | } 19 | export function collect(f, xs) { 20 | return seqFold(function (acc, x) { return append(acc, f(x)); }, new List(), xs); 21 | } 22 | export function concat(xs) { 23 | return collect(function (x) { return x; }, xs); 24 | } 25 | export function filter(f, xs) { 26 | return reverse(seqFold(function (acc, x) { return f(x) ? new List(x, acc) : acc; }, new List(), xs)); 27 | } 28 | export function where(f, xs) { 29 | return filter(f, xs); 30 | } 31 | export function initialize(n, f) { 32 | if (n < 0) { 33 | throw new Error("List length must be non-negative"); 34 | } 35 | var xs = new List(); 36 | for (var i = 1; i <= n; i++) { 37 | xs = new List(f(n - i), xs); 38 | } 39 | return xs; 40 | } 41 | export function map(f, xs) { 42 | return reverse(seqFold(function (acc, x) { return new List(f(x), acc); }, new List(), xs)); 43 | } 44 | export function mapIndexed(f, xs) { 45 | return reverse(seqFold(function (acc, x, i) { return new List(f(i, x), acc); }, new List(), xs)); 46 | } 47 | export function partition(f, xs) { 48 | return seqFold(function (acc, x) { 49 | var lacc = acc[0], racc = acc[1]; 50 | return f(x) ? [new List(x, lacc), racc] : [lacc, new List(x, racc)]; 51 | }, [new List(), new List()], reverse(xs)); 52 | } 53 | export function replicate(n, x) { 54 | return initialize(n, function () { return x; }); 55 | } 56 | export function reverse(xs) { 57 | return seqFold(function (acc, x) { return new List(x, acc); }, new List(), xs); 58 | } 59 | export function singleton(x) { 60 | return new List(x, new List()); 61 | } 62 | export function slice(lower, upper, xs) { 63 | var noLower = (lower == null); 64 | var noUpper = (upper == null); 65 | return reverse(seqFold(function (acc, x, i) { return (noLower || lower <= i) && (noUpper || i <= upper) ? new List(x, acc) : acc; }, new List(), xs)); 66 | } 67 | export function unzip(xs) { 68 | return seqFoldBack(function (xy, acc) { 69 | return [new List(xy[0], acc[0]), new List(xy[1], acc[1])]; 70 | }, xs, [new List(), new List()]); 71 | } 72 | export function unzip3(xs) { 73 | return seqFoldBack(function (xyz, acc) { 74 | return [new List(xyz[0], acc[0]), new List(xyz[1], acc[1]), new List(xyz[2], acc[2])]; 75 | }, xs, [new List(), new List(), new List()]); 76 | } 77 | export function groupBy(f, xs) { 78 | return seqToList(seqMap(function (k) { return [k[0], seqToList(k[1])]; }, mapGroupBy(f, xs))); 79 | } 80 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/ListClass.js: -------------------------------------------------------------------------------- 1 | import FSymbol from "./Symbol"; 2 | import { toString } from "./Util"; 3 | import { equals } from "./Util"; 4 | import { compare } from "./Util"; 5 | export function ofArray(args, base) { 6 | var acc = base || new List(); 7 | for (var i = args.length - 1; i >= 0; i--) { 8 | acc = new List(args[i], acc); 9 | } 10 | return acc; 11 | } 12 | var List = (function () { 13 | function List(head, tail) { 14 | this.head = head; 15 | this.tail = tail; 16 | } 17 | List.prototype.ToString = function () { 18 | return "[" + Array.from(this).map(toString).join("; ") + "]"; 19 | }; 20 | List.prototype.Equals = function (x) { 21 | if (this === x) { 22 | return true; 23 | } 24 | else { 25 | var iter1 = this[Symbol.iterator](), iter2 = x[Symbol.iterator](); 26 | for (;;) { 27 | var cur1 = iter1.next(), cur2 = iter2.next(); 28 | if (cur1.done) 29 | return cur2.done ? true : false; 30 | else if (cur2.done) 31 | return false; 32 | else if (!equals(cur1.value, cur2.value)) 33 | return false; 34 | } 35 | } 36 | }; 37 | List.prototype.CompareTo = function (x) { 38 | if (this === x) { 39 | return 0; 40 | } 41 | else { 42 | var acc = 0; 43 | var iter1 = this[Symbol.iterator](), iter2 = x[Symbol.iterator](); 44 | for (;;) { 45 | var cur1 = iter1.next(), cur2 = iter2.next(); 46 | if (cur1.done) 47 | return cur2.done ? acc : -1; 48 | else if (cur2.done) 49 | return 1; 50 | else { 51 | acc = compare(cur1.value, cur2.value); 52 | if (acc != 0) 53 | return acc; 54 | } 55 | } 56 | } 57 | }; 58 | Object.defineProperty(List.prototype, "length", { 59 | get: function () { 60 | var cur = this, acc = 0; 61 | while (cur.tail != null) { 62 | cur = cur.tail; 63 | acc++; 64 | } 65 | return acc; 66 | }, 67 | enumerable: true, 68 | configurable: true 69 | }); 70 | List.prototype[Symbol.iterator] = function () { 71 | var cur = this; 72 | return { 73 | next: function () { 74 | var tmp = cur; 75 | cur = cur.tail; 76 | return { done: tmp.tail == null, value: tmp.head }; 77 | } 78 | }; 79 | }; 80 | List.prototype[FSymbol.reflection] = function () { 81 | return { 82 | type: "Microsoft.FSharp.Collections.FSharpList", 83 | interfaces: ["System.IEquatable", "System.IComparable"] 84 | }; 85 | }; 86 | return List; 87 | }()); 88 | export default List; 89 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/MailboxProcessor.js: -------------------------------------------------------------------------------- 1 | import { defaultCancellationToken } from "./Async"; 2 | import { fromContinuations } from "./Async"; 3 | import { startImmediate } from "./Async"; 4 | var QueueCell = (function () { 5 | function QueueCell(message) { 6 | this.value = message; 7 | } 8 | return QueueCell; 9 | }()); 10 | var MailboxQueue = (function () { 11 | function MailboxQueue() { 12 | } 13 | MailboxQueue.prototype.add = function (message) { 14 | var itCell = new QueueCell(message); 15 | if (this.firstAndLast) { 16 | this.firstAndLast[1].next = itCell; 17 | this.firstAndLast = [this.firstAndLast[0], itCell]; 18 | } 19 | else 20 | this.firstAndLast = [itCell, itCell]; 21 | }; 22 | MailboxQueue.prototype.tryGet = function () { 23 | if (this.firstAndLast) { 24 | var value = this.firstAndLast[0].value; 25 | if (this.firstAndLast[0].next) 26 | this.firstAndLast = [this.firstAndLast[0].next, this.firstAndLast[1]]; 27 | else 28 | delete this.firstAndLast; 29 | return value; 30 | } 31 | return void 0; 32 | }; 33 | return MailboxQueue; 34 | }()); 35 | var MailboxProcessor = (function () { 36 | function MailboxProcessor(body, cancellationToken) { 37 | this.body = body; 38 | this.cancellationToken = cancellationToken || defaultCancellationToken; 39 | this.messages = new MailboxQueue(); 40 | } 41 | MailboxProcessor.prototype.__processEvents = function () { 42 | if (this.continuation) { 43 | var value = this.messages.tryGet(); 44 | if (value) { 45 | var cont = this.continuation; 46 | delete this.continuation; 47 | cont(value); 48 | } 49 | } 50 | }; 51 | MailboxProcessor.prototype.start = function () { 52 | startImmediate(this.body(this), this.cancellationToken); 53 | }; 54 | MailboxProcessor.prototype.receive = function () { 55 | var _this = this; 56 | return fromContinuations(function (conts) { 57 | if (_this.continuation) 58 | throw new Error("Receive can only be called once!"); 59 | _this.continuation = conts[0]; 60 | _this.__processEvents(); 61 | }); 62 | }; 63 | MailboxProcessor.prototype.post = function (message) { 64 | this.messages.add(message); 65 | this.__processEvents(); 66 | }; 67 | MailboxProcessor.prototype.postAndAsyncReply = function (buildMessage) { 68 | var result; 69 | var continuation; 70 | function checkCompletion() { 71 | if (result && continuation) 72 | continuation(result); 73 | } 74 | var reply = { 75 | reply: function (res) { 76 | result = res; 77 | checkCompletion(); 78 | } 79 | }; 80 | this.messages.add(buildMessage(reply)); 81 | this.__processEvents(); 82 | return fromContinuations(function (conts) { 83 | continuation = conts[0]; 84 | checkCompletion(); 85 | }); 86 | }; 87 | return MailboxProcessor; 88 | }()); 89 | export default MailboxProcessor; 90 | export function start(body, cancellationToken) { 91 | var mbox = new MailboxProcessor(body, cancellationToken); 92 | mbox.start(); 93 | return mbox; 94 | } 95 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/Observable.js: -------------------------------------------------------------------------------- 1 | import { createDisposable } from "./Util"; 2 | import FSymbol from "./Symbol"; 3 | var Observer = (function () { 4 | function Observer(onNext, onError, onCompleted) { 5 | this.OnNext = onNext; 6 | this.OnError = onError || (function (e) { }); 7 | this.OnCompleted = onCompleted || function () { }; 8 | } 9 | Observer.prototype[FSymbol.reflection] = function () { 10 | return { interfaces: ["System.IObserver"] }; 11 | }; 12 | return Observer; 13 | }()); 14 | export { Observer }; 15 | var Observable = (function () { 16 | function Observable(subscribe) { 17 | this.Subscribe = subscribe; 18 | } 19 | Observable.prototype[FSymbol.reflection] = function () { 20 | return { interfaces: ["System.IObservable"] }; 21 | }; 22 | return Observable; 23 | }()); 24 | export function protect(f, succeed, fail) { 25 | try { 26 | return succeed(f()); 27 | } 28 | catch (e) { 29 | fail(e); 30 | } 31 | } 32 | export function add(callback, source) { 33 | source.Subscribe(new Observer(callback)); 34 | } 35 | export function choose(chooser, source) { 36 | return new Observable(function (observer) { 37 | return source.Subscribe(new Observer(function (t) { 38 | return protect(function () { return chooser(t); }, function (u) { if (u != null) 39 | observer.OnNext(u); }, observer.OnError); 40 | }, observer.OnError, observer.OnCompleted)); 41 | }); 42 | } 43 | export function filter(predicate, source) { 44 | return choose(function (x) { return predicate(x) ? x : null; }, source); 45 | } 46 | export function map(mapping, source) { 47 | return new Observable(function (observer) { 48 | return source.Subscribe(new Observer(function (t) { 49 | protect(function () { return mapping(t); }, observer.OnNext, observer.OnError); 50 | }, observer.OnError, observer.OnCompleted)); 51 | }); 52 | } 53 | export function merge(source1, source2) { 54 | return new Observable(function (observer) { 55 | var stopped = false, completed1 = false, completed2 = false; 56 | var h1 = source1.Subscribe(new Observer(function (v) { if (!stopped) 57 | observer.OnNext(v); }, function (e) { 58 | if (!stopped) { 59 | stopped = true; 60 | observer.OnError(e); 61 | } 62 | }, function () { 63 | if (!stopped) { 64 | completed1 = true; 65 | if (completed2) { 66 | stopped = true; 67 | observer.OnCompleted(); 68 | } 69 | } 70 | })); 71 | var h2 = source2.Subscribe(new Observer(function (v) { if (!stopped) { 72 | observer.OnNext(v); 73 | } }, function (e) { 74 | if (!stopped) { 75 | stopped = true; 76 | observer.OnError(e); 77 | } 78 | }, function () { 79 | if (!stopped) { 80 | completed2 = true; 81 | if (completed1) { 82 | stopped = true; 83 | observer.OnCompleted(); 84 | } 85 | } 86 | })); 87 | return createDisposable(function () { 88 | h1.Dispose(); 89 | h2.Dispose(); 90 | }); 91 | }); 92 | } 93 | export function pairwise(source) { 94 | return new Observable(function (observer) { 95 | var last = null; 96 | return source.Subscribe(new Observer(function (next) { 97 | if (last != null) 98 | observer.OnNext([last, next]); 99 | last = next; 100 | }, observer.OnError, observer.OnCompleted)); 101 | }); 102 | } 103 | export function partition(predicate, source) { 104 | return [filter(predicate, source), filter(function (x) { return !predicate(x); }, source)]; 105 | } 106 | export function scan(collector, state, source) { 107 | return new Observable(function (observer) { 108 | return source.Subscribe(new Observer(function (t) { 109 | protect(function () { return collector(state, t); }, function (u) { state = u; observer.OnNext(u); }, observer.OnError); 110 | }, observer.OnError, observer.OnCompleted)); 111 | }); 112 | } 113 | export function split(splitter, source) { 114 | return [choose(function (v) { return splitter(v).valueIfChoice1; }, source), choose(function (v) { return splitter(v).valueIfChoice2; }, source)]; 115 | } 116 | export function subscribe(callback, source) { 117 | return source.Subscribe(new Observer(callback)); 118 | } 119 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/Reflection.js: -------------------------------------------------------------------------------- 1 | import { NonDeclaredType } from "./Util"; 2 | import List from "./List"; 3 | import FSymbol from "./Symbol"; 4 | export function resolveGeneric(idx, enclosing) { 5 | try { 6 | var t = enclosing.head; 7 | if (t.generics == null) { 8 | return resolveGeneric(idx, enclosing.tail); 9 | } 10 | else { 11 | var name_1 = typeof idx === "string" 12 | ? idx : Object.getOwnPropertyNames(t.generics)[idx]; 13 | var resolved = t.generics[name_1]; 14 | if (resolved == null) { 15 | return resolveGeneric(idx, enclosing.tail); 16 | } 17 | else if (resolved instanceof NonDeclaredType && resolved.kind === "GenericParam") { 18 | return resolveGeneric(resolved.definition, enclosing.tail); 19 | } 20 | else { 21 | return new List(resolved, enclosing); 22 | } 23 | } 24 | } 25 | catch (err) { 26 | throw new Error("Cannot resolve generic argument " + idx + ": " + err); 27 | } 28 | } 29 | export function getType(obj) { 30 | var t = typeof obj; 31 | switch (t) { 32 | case "boolean": 33 | case "number": 34 | case "string": 35 | case "function": 36 | return t; 37 | default: 38 | return Object.getPrototypeOf(obj).constructor; 39 | } 40 | } 41 | export function getTypeFullName(typ, option) { 42 | function trim(fullName, option) { 43 | if (typeof fullName !== "string") { 44 | return "unknown"; 45 | } 46 | if (option === "name") { 47 | var i = fullName.lastIndexOf('.'); 48 | return fullName.substr(i + 1); 49 | } 50 | if (option === "namespace") { 51 | var i = fullName.lastIndexOf('.'); 52 | return i > -1 ? fullName.substr(0, i) : ""; 53 | } 54 | return fullName; 55 | } 56 | if (typeof typ === "string") { 57 | return typ; 58 | } 59 | else if (typ instanceof NonDeclaredType) { 60 | switch (typ.kind) { 61 | case "Unit": 62 | return "unit"; 63 | case "Option": 64 | return getTypeFullName(typ.generics, option) + " option"; 65 | case "Array": 66 | return getTypeFullName(typ.generics, option) + "[]"; 67 | case "Tuple": 68 | return typ.generics.map(function (x) { return getTypeFullName(x, option); }).join(" * "); 69 | case "GenericParam": 70 | case "Interface": 71 | return typ.definition; 72 | case "Any": 73 | default: 74 | return "unknown"; 75 | } 76 | } 77 | else { 78 | var proto = typ.prototype; 79 | return trim(typeof proto[FSymbol.reflection] === "function" 80 | ? proto[FSymbol.reflection]().type : null, option); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/RegExp.js: -------------------------------------------------------------------------------- 1 | export function create(pattern, options) { 2 | var flags = "g"; 3 | flags += options & 1 ? "i" : ""; 4 | flags += options & 2 ? "m" : ""; 5 | return new RegExp(pattern, flags); 6 | } 7 | export function escape(str) { 8 | return str.replace(/[\-\[\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 9 | } 10 | export function unescape(str) { 11 | return str.replace(/\\([\-\[\/\{\}\(\)\*\+\?\.\\\^\$\|])/g, "$1"); 12 | } 13 | export function isMatch(str, pattern, options) { 14 | if (options === void 0) { options = 0; } 15 | var reg = str instanceof RegExp 16 | ? (reg = str, str = pattern, reg.lastIndex = options, reg) 17 | : reg = create(pattern, options); 18 | return reg.test(str); 19 | } 20 | export function match(str, pattern, options) { 21 | if (options === void 0) { options = 0; } 22 | var reg = str instanceof RegExp 23 | ? (reg = str, str = pattern, reg.lastIndex = options, reg) 24 | : reg = create(pattern, options); 25 | return reg.exec(str); 26 | } 27 | export function matches(str, pattern, options) { 28 | if (options === void 0) { options = 0; } 29 | var reg = str instanceof RegExp 30 | ? (reg = str, str = pattern, reg.lastIndex = options, reg) 31 | : reg = create(pattern, options); 32 | if (!reg.global) 33 | throw new Error("Non-global RegExp"); 34 | var m; 35 | var matches = []; 36 | while ((m = reg.exec(str)) !== null) 37 | matches.push(m); 38 | return matches; 39 | } 40 | export function options(reg) { 41 | var options = 256; 42 | options |= reg.ignoreCase ? 1 : 0; 43 | options |= reg.multiline ? 2 : 0; 44 | return options; 45 | } 46 | export function replace(reg, input, replacement, limit, offset) { 47 | if (offset === void 0) { offset = 0; } 48 | function replacer() { 49 | var res = arguments[0]; 50 | if (limit !== 0) { 51 | limit--; 52 | var match_1 = []; 53 | var len = arguments.length; 54 | for (var i = 0; i < len - 2; i++) 55 | match_1.push(arguments[i]); 56 | match_1.index = arguments[len - 2]; 57 | match_1.input = arguments[len - 1]; 58 | res = replacement(match_1); 59 | } 60 | return res; 61 | } 62 | if (typeof reg == "string") { 63 | var tmp = reg; 64 | reg = create(input, limit); 65 | input = tmp; 66 | limit = undefined; 67 | } 68 | if (typeof replacement == "function") { 69 | limit = limit == null ? -1 : limit; 70 | return input.substring(0, offset) + input.substring(offset).replace(reg, replacer); 71 | } 72 | else { 73 | if (limit != null) { 74 | var m = void 0; 75 | var sub1 = input.substring(offset); 76 | var _matches = matches(reg, sub1); 77 | var sub2 = matches.length > limit ? (m = _matches[limit - 1], sub1.substring(0, m.index + m[0].length)) : sub1; 78 | return input.substring(0, offset) + sub2.replace(reg, replacement) + input.substring(offset + sub2.length); 79 | } 80 | else { 81 | return input.replace(reg, replacement); 82 | } 83 | } 84 | } 85 | export function split(reg, input, limit, offset) { 86 | if (offset === void 0) { offset = 0; } 87 | if (typeof reg == "string") { 88 | var tmp = reg; 89 | reg = create(input, limit); 90 | input = tmp; 91 | limit = undefined; 92 | } 93 | input = input.substring(offset); 94 | return input.split(reg, limit); 95 | } 96 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/Symbol.js: -------------------------------------------------------------------------------- 1 | var fableGlobal = function () { 2 | var globalObj = typeof window !== "undefined" ? window 3 | : (typeof global !== "undefined" ? global 4 | : (typeof self !== "undefined" ? self : null)); 5 | if (typeof globalObj.__FABLE_CORE__ === "undefined") { 6 | globalObj.__FABLE_CORE__ = { 7 | types: new Map(), 8 | symbols: { 9 | reflection: Symbol("reflection"), 10 | } 11 | }; 12 | } 13 | return globalObj.__FABLE_CORE__; 14 | }(); 15 | export function setType(fullName, cons) { 16 | fableGlobal.types.set(fullName, cons); 17 | } 18 | export function getType(fullName) { 19 | return fableGlobal.types.get(fullName); 20 | } 21 | export default (fableGlobal.symbols); 22 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/TimeSpan.js: -------------------------------------------------------------------------------- 1 | import { compare as utilCompare } from "./Util"; 2 | import * as Long from "./Long"; 3 | export function create(d, h, m, s, ms) { 4 | if (d === void 0) { d = 0; } 5 | if (h === void 0) { h = 0; } 6 | if (m === void 0) { m = 0; } 7 | if (s === void 0) { s = 0; } 8 | if (ms === void 0) { ms = 0; } 9 | switch (arguments.length) { 10 | case 1: 11 | return fromTicks(arguments[0]); 12 | case 3: 13 | d = 0, h = arguments[0], m = arguments[1], s = arguments[2], ms = 0; 14 | break; 15 | default: 16 | d = arguments[0], h = arguments[1], m = arguments[2], s = arguments[3], ms = arguments[4] || 0; 17 | break; 18 | } 19 | return d * 86400000 + h * 3600000 + m * 60000 + s * 1000 + ms; 20 | } 21 | export function fromTicks(ticks) { 22 | return ticks.div(10000).toNumber(); 23 | } 24 | export function fromDays(d) { 25 | return create(d, 0, 0, 0); 26 | } 27 | export function fromHours(h) { 28 | return create(h, 0, 0); 29 | } 30 | export function fromMinutes(m) { 31 | return create(0, m, 0); 32 | } 33 | export function fromSeconds(s) { 34 | return create(0, 0, s); 35 | } 36 | export function days(ts) { 37 | return Math.floor(ts / 86400000); 38 | } 39 | export function hours(ts) { 40 | return Math.floor(ts % 86400000 / 3600000); 41 | } 42 | export function minutes(ts) { 43 | return Math.floor(ts % 3600000 / 60000); 44 | } 45 | export function seconds(ts) { 46 | return Math.floor(ts % 60000 / 1000); 47 | } 48 | export function milliseconds(ts) { 49 | return Math.floor(ts % 1000); 50 | } 51 | export function ticks(ts) { 52 | return Long.fromNumber(ts).mul(10000); 53 | } 54 | export function totalDays(ts) { 55 | return ts / 86400000; 56 | } 57 | export function totalHours(ts) { 58 | return ts / 3600000; 59 | } 60 | export function totalMinutes(ts) { 61 | return ts / 60000; 62 | } 63 | export function totalSeconds(ts) { 64 | return ts / 1000; 65 | } 66 | export function negate(ts) { 67 | return ts * -1; 68 | } 69 | export function add(ts1, ts2) { 70 | return ts1 + ts2; 71 | } 72 | export function subtract(ts1, ts2) { 73 | return ts1 - ts2; 74 | } 75 | export function compare(x, y) { 76 | return utilCompare(x, y); 77 | } 78 | export function compareTo(x, y) { 79 | return utilCompare(x, y); 80 | } 81 | export function duration(x) { 82 | return Math.abs(x); 83 | } 84 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/fable-core/umd/Timer.js: -------------------------------------------------------------------------------- 1 | import Event from "./Event"; 2 | import FSymbol from "./Symbol"; 3 | var Timer = (function () { 4 | function Timer(interval) { 5 | this.Interval = interval > 0 ? interval : 100; 6 | this.AutoReset = true; 7 | this._elapsed = new Event(); 8 | } 9 | Object.defineProperty(Timer.prototype, "Elapsed", { 10 | get: function () { 11 | return this._elapsed; 12 | }, 13 | enumerable: true, 14 | configurable: true 15 | }); 16 | Object.defineProperty(Timer.prototype, "Enabled", { 17 | get: function () { 18 | return this._enabled; 19 | }, 20 | set: function (x) { 21 | var _this = this; 22 | if (!this._isDisposed && this._enabled != x) { 23 | if (this._enabled = x) { 24 | if (this.AutoReset) { 25 | this._intervalId = setInterval(function () { 26 | if (!_this.AutoReset) 27 | _this.Enabled = false; 28 | _this._elapsed.Trigger(new Date()); 29 | }, this.Interval); 30 | } 31 | else { 32 | this._timeoutId = setTimeout(function () { 33 | _this.Enabled = false; 34 | _this._timeoutId = 0; 35 | if (_this.AutoReset) 36 | _this.Enabled = true; 37 | _this._elapsed.Trigger(new Date()); 38 | }, this.Interval); 39 | } 40 | } 41 | else { 42 | if (this._timeoutId) { 43 | clearTimeout(this._timeoutId); 44 | this._timeoutId = 0; 45 | } 46 | if (this._intervalId) { 47 | clearInterval(this._intervalId); 48 | this._intervalId = 0; 49 | } 50 | } 51 | } 52 | }, 53 | enumerable: true, 54 | configurable: true 55 | }); 56 | Timer.prototype.Dispose = function () { 57 | this.Enabled = false; 58 | this._isDisposed = true; 59 | }; 60 | Timer.prototype.Close = function () { 61 | this.Dispose(); 62 | }; 63 | Timer.prototype.Start = function () { 64 | this.Enabled = true; 65 | }; 66 | Timer.prototype.Stop = function () { 67 | this.Enabled = false; 68 | }; 69 | Timer.prototype[FSymbol.reflection] = function () { 70 | return { 71 | type: "System.Timers.Timer", 72 | interfaces: ["System.IDisposable"] 73 | }; 74 | }; 75 | return Timer; 76 | }()); 77 | export default Timer; 78 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/src/canopy/companion/crx/icon.png -------------------------------------------------------------------------------- /src/canopy/companion/crx/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "name": "canopy companion css selector tool", 5 | "description": "This extensions helps you create quality css, jquery, and xpath selectors", 6 | "version": "0.2", 7 | 8 | "browser_action": { 9 | "default_icon": "icon.png" 10 | }, 11 | "permissions": [ 12 | "tabs", "http://*/*", "https://*/*" 13 | ], 14 | "background": { 15 | "scripts": ["background.js"], 16 | "persistent": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/canopy/companion/crx/styles.css: -------------------------------------------------------------------------------- 1 | 2 | /* starting point comes from selector gadget found at https://github.com/cantino/selectorgadget */ 3 | 4 | div#canopy_companion { 5 | azimuth: center !important; 6 | background-attachment: scroll !important; 7 | background-image: none !important; 8 | background-position: 0% 0% !important; 9 | background-repeat: repeat !important; 10 | border-collapse: separate !important; 11 | border-spacing: 0 !important; 12 | caption-side: top !important; 13 | clear: none !important; 14 | clip: auto !important; 15 | color: black !important; 16 | content: normal !important; 17 | counter-increment: none !important; 18 | counter-reset: none !important; 19 | cursor: auto !important; 20 | direction: ltr !important; 21 | elevation: level !important; 22 | empty-cells: show !important; 23 | font-family: sans-serif !important; 24 | font-variant: normal !important; 25 | font-weight: normal !important; 26 | height: auto !important; 27 | left: auto !important; 28 | letter-spacing: normal !important; 29 | line-height: normal !important; 30 | list-style-image: none !important; 31 | list-style-position: outside !important; 32 | list-style-type: disc !important; 33 | max-height: none !important; 34 | max-width: none !important; 35 | min-height: 0 !important; 36 | min-width: 0 !important; 37 | orphans: 2 !important; 38 | outline-color: invert !important; 39 | outline-style: none !important; 40 | outline-width: medium !important; 41 | overflow: visible !important; 42 | page-break-after: auto !important; 43 | page-break-before: auto !important; 44 | page-break-inside: auto !important; 45 | table-layout: auto !important; 46 | text-align: left !important; 47 | text-decoration: none !important; 48 | text-indent: 0 !important; 49 | text-transform: none !important; 50 | top: auto !important; 51 | unicode-bidi: normal !important; 52 | vertical-align: baseline !important; 53 | visibility: visible !important; 54 | white-space: normal !important; 55 | widows: 2 !important; 56 | width: auto !important; 57 | word-spacing: normal !important; 58 | position: fixed !important; 59 | z-index: 9999999 !important; 60 | display: block !important; 61 | right: 5px !important; 62 | /*margin: 5px !important;*/ 63 | padding: 5px !important; 64 | font-size: 14px !important; 65 | float: none !important; 66 | border: 1px solid black !important; 67 | font-style: none !important; 68 | background: white !important; 69 | text-align: left !important; } 70 | 71 | div#canopy_companion { 72 | bottom: 5px !important; 73 | } 74 | 75 | .canopy_companion_border { 76 | position: absolute !important; 77 | z-index: 999999 !important; 78 | background: white !important; 79 | background-color: orange !important; 80 | margin: 0px !important; 81 | padding: 0px !important; 82 | display: block !important; 83 | float: none !important; 84 | border: 0 !important; 85 | font-style: none !important; 86 | outline: 0 !important; 87 | vertical-align: baseline !important; 88 | text-align: left !important; 89 | } 90 | 91 | .canopy_companion_border_green { 92 | background-color: #0F0 !important; 93 | } 94 | 95 | #canopy_companion th { 96 | text-align: left; 97 | } 98 | #canopy_companion .row_selector { 99 | width: 59%; 100 | } 101 | 102 | #canopy_companion .row_count { 103 | width: 10%; 104 | } 105 | 106 | #canopy_companion .row_type { 107 | width: 15%; 108 | } 109 | 110 | #canopy_companion .row_copy { 111 | width: 8%; 112 | } 113 | 114 | #canopy_companion .row_apply { 115 | width: 8%; 116 | } 117 | 118 | #canopy_companion table { 119 | color: #333; 120 | font-family: Helvetica, Arial, sans-serif; 121 | width: 640px; 122 | border-collapse: 123 | collapse; border-spacing: 0; 124 | } 125 | 126 | #canopy_companion td, th { 127 | font-family: Verdana, Geneva, sans-serif; 128 | font-size: 10pt; 129 | border: 1px solid transparent; 130 | height: 30px; 131 | transition: all 0.3s; 132 | } 133 | 134 | #canopy_companion th { 135 | background: #DFDFDF; 136 | font-weight: bold; 137 | } 138 | 139 | #canopy_companion td { 140 | color: #828282; 141 | background: #FAFAFA; 142 | text-align: left; 143 | } 144 | 145 | #canopy_companion .right { 146 | text-align: right; 147 | } 148 | 149 | #canopy_companion tr:nth-child(even) td { background: #F1F1F1; } 150 | 151 | #canopy_companion tr:nth-child(odd) td { background: #FEFEFE; } -------------------------------------------------------------------------------- /src/canopy/companion/fableconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "module": "commonjs", 3 | "sourceMaps": false, 4 | "projFile": "./companion.fsx", 5 | "outDir": "crx", 6 | "scripts": { 7 | "prebuild": "npm install" 8 | } 9 | } -------------------------------------------------------------------------------- /src/canopy/companion/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/src/canopy/companion/icon128.png -------------------------------------------------------------------------------- /src/canopy/companion/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/src/canopy/companion/screenshot.png -------------------------------------------------------------------------------- /src/canopy/companion/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var webpack = require("webpack"); 3 | 4 | var cfg = { 5 | devtool: "source-map", 6 | entry: ['crx\\companion.js'], 7 | output: { 8 | path: path.resolve("crx"), 9 | filename: "bundle.js" 10 | }, 11 | module: { 12 | preLoaders: [ 13 | { test: /\.js$/, exclude: /node_modules/, loader: "source-map-loader" } 14 | ], 15 | loaders: [ 16 | { test: /\.ts$/, exclude: /node_modules/, loader: 'ts-loader' }, 17 | { 18 | test: /\.jsx?$/, 19 | exclude: /node_modules/, 20 | loader: 'babel-loader', 21 | query: { cacheDirectory: true, presets: ['es2015'] }, 22 | plugins: ['transform-runtime'] 23 | }, 24 | { test: /\.styl$/, exclude: /node_modules/, loader: 'style-loader!css-loader!stylus-loader' }, 25 | { test: /\.css$/, loader: 'style-loader!css-loader' }, 26 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file" }, 27 | { test: /\.(woff|woff2)$/, loader:"url?prefix=font/&limit=5000" }, 28 | { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/octet-stream" }, 29 | { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=image/svg+xml" }, 30 | { test: require.resolve("jquery"), loader: "imports?jQuery=jquery" }, 31 | { test: /\.png$/, exclude: /node_modules/, loader: "url?limit=10000&mimetype=image/png" } 32 | ] 33 | }, 34 | plugins: [ 35 | new webpack.ProvidePlugin({ "$": "jquery", "jQuery": "jquery" }) 36 | ], 37 | resolve: { 38 | extensions: ['', '.ts', '.js', '.jsx'], 39 | root: [ 40 | path.resolve('.'), 41 | path.resolve('./crx') 42 | ] 43 | } 44 | }; 45 | 46 | module.exports = cfg; 47 | -------------------------------------------------------------------------------- /src/canopy/configuration.fs: -------------------------------------------------------------------------------- 1 | module canopy.configuration 2 | 3 | open canopy.reporters 4 | open canopy.types 5 | open System 6 | 7 | let executingDir = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) 8 | 9 | //location of drivers depending on OS 10 | let folderByOSType = 11 | match System.Environment.OSVersion.Platform with 12 | | PlatformID.MacOSX 13 | | PlatformID.Unix -> executingDir 14 | | _ -> executingDir 15 | 16 | let folderByOSTypeChromium = 17 | match System.Environment.OSVersion.Platform with 18 | | PlatformID.MacOSX 19 | | PlatformID.Unix -> @"/usr/lib/chromium-browser" 20 | | _ -> executingDir 21 | 22 | let firefoxByOSType = 23 | match System.Environment.OSVersion.Platform with 24 | | PlatformID.MacOSX 25 | | PlatformID.Unix -> 26 | if System.IO.File.Exists(@"/Applications/Firefox.app/Contents/MacOS/firefox-bin") 27 | then @"/Applications/Firefox.app/Contents/MacOS/firefox-bin" //osx 28 | else @"/usr/lib/firefox/firefox" //linux 29 | | _ -> 30 | if System.IO.File.Exists(@"C:\Program Files\Mozilla Firefox\firefox.exe") 31 | then @"C:\Program Files\Mozilla Firefox\firefox.exe" // 64-bit version 32 | else @"C:\Program Files (x86)\Mozilla Firefox\firefox.exe" 33 | 34 | //runner related 35 | (* documented/configuration *) 36 | let failFast = ref false 37 | (* documented/configuration *) 38 | let mutable failScreenshotPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\canopy\" 39 | (* documented/configuration *) 40 | let mutable failScreenshotFileName = fun (test : canopy.types.Test) (suite: canopy.types.suite) -> DateTime.Now.ToString("MMM-d_HH-mm-ss-fff") 41 | 42 | (* documented/configuration *) 43 | let mutable chromeDir = folderByOSType 44 | (* documented/configuration *) 45 | let mutable chromiumDir = folderByOSTypeChromium 46 | (* documented/configuration *) 47 | let mutable ieDir = folderByOSType 48 | (* documented/configuration *) 49 | let mutable safariDir = folderByOSType 50 | (* documented/configuration *) 51 | let mutable edgeDir = @"C:\Program Files (x86)\Microsoft Web Driver\" 52 | (* documented/configuration *) 53 | let mutable hideCommandPromptWindow = false 54 | (* documented/configuration *) 55 | let mutable firefoxDriverDir = folderByOSType 56 | (* documented/configuration *) 57 | let mutable firefoxDir = firefoxByOSType 58 | 59 | (* documented/configuration *) 60 | let mutable elementTimeout = 10.0 61 | (* documented/configuration *) 62 | let mutable compareTimeout = 10.0 63 | (* documented/configuration *) 64 | let mutable pageTimeout = 10.0 65 | (* documented/configuration *) 66 | let mutable wipSleep = 1.0 67 | (* documented/configuration *) 68 | let mutable failIfAnyWipTests = false 69 | (* documented/configuration *) 70 | let mutable runFailedContextsFirst = false 71 | (* documented/configuration *) 72 | let mutable reporter : IReporter = new ConsoleReporter() :> IReporter 73 | (* documented/configuration *) 74 | let mutable disableSuggestOtherSelectors = false 75 | (* documented/configuration *) 76 | let mutable autoPinBrowserRightOnLaunch = true 77 | (* documented/configuration *) 78 | let mutable throwIfMoreThanOneElement = false 79 | (* documented/configuration *) 80 | let mutable configuredFinders = canopy.finders.defaultFinders 81 | (* documented/configuration *) 82 | let mutable optimizeBySkippingIFrameCheck = false 83 | (* documented/configuration *) 84 | let mutable optimizeByDisablingClearBeforeWrite = false 85 | (* documented/configuration *) 86 | let mutable showInfoDiv = true 87 | (* documented/configuration *) 88 | let mutable failureScreenshotsEnabled = true 89 | (* documented/configuration *) 90 | let mutable skipAllTestsOnFailure = false 91 | (* documented/configuration *) 92 | let mutable skipRemainingTestsInContextOnFailure = false 93 | (* documented/configuration *) 94 | let mutable failureMessagesThatShoulBeTreatedAsSkip : string list = [] 95 | (* documented/configuration *) 96 | let mutable driverHostName = "127.0.0.1" 97 | (* documented/configuration *) 98 | let mutable webdriverPort: int option = None 99 | (* documented/configuration *) 100 | let mutable acceptInsecureSslCerts = true 101 | 102 | //do not touch 103 | let mutable wipTest = false 104 | -------------------------------------------------------------------------------- /src/canopy/finders.fs: -------------------------------------------------------------------------------- 1 | module canopy.finders 2 | 3 | open OpenQA.Selenium 4 | open System.Collections.ObjectModel 5 | open System.Collections.Generic 6 | 7 | //have to use the ReadonlyCollection because thats what selenium uses and it wont cast to seq or something, and type inference isnt playing nice 8 | //basically a hack because I dont know a better way 9 | let findByCss (cssSelector : string) (f : (By -> ReadOnlyCollection)) (_ : IWebDriver) = 10 | try 11 | f(By.CssSelector(cssSelector)) |> List.ofSeq 12 | with | ex -> [] 13 | 14 | let findByXpath xpath f (_ : IWebDriver) = 15 | try 16 | f(By.XPath(xpath)) |> List.ofSeq 17 | with | ex -> [] 18 | 19 | let findByLabel locator f (_ : IWebDriver) = 20 | let isInputField (element : IWebElement) = 21 | element.TagName = "input" && element.GetAttribute("type") <> "hidden" 22 | 23 | let isField (element : IWebElement) = 24 | element.TagName = "select" || element.TagName = "textarea" || isInputField element 25 | 26 | let firstFollowingField (label : IWebElement) = 27 | let followingElements = label.FindElements(By.XPath("./following-sibling::*[1]")) |> Seq.toList 28 | match followingElements with 29 | | head :: tail when isField head-> [head] 30 | | _ -> [] 31 | try 32 | let labels = f(By.XPath(sprintf """.//label[text() = "%s"]""" locator)) 33 | if (Seq.isEmpty labels) then 34 | [] 35 | else 36 | let (label : IWebElement) = (labels |> List.ofSeq).Head 37 | match label.GetAttribute("for") with 38 | | null -> firstFollowingField (labels |> List.ofSeq).Head 39 | | id -> f(By.Id(id)) |> List.ofSeq 40 | with | _ -> [] 41 | 42 | let findByText text f (_ : IWebDriver) = 43 | try 44 | f(By.XPath(sprintf """.//*[text() = "%s"]""" text)) |> List.ofSeq 45 | with | _ -> [] 46 | 47 | let findByNormalizeSpaceText text f (_ : IWebDriver) = 48 | try 49 | f(By.XPath(sprintf """.//*[normalize-space(text()) = "%s"]""" text)) |> List.ofSeq 50 | with | _ -> [] 51 | 52 | let findByValue value f (browser : IWebDriver) = 53 | try 54 | findByCss (sprintf """*[value="%s"]""" value) f browser |> List.ofSeq 55 | with | _ -> [] 56 | 57 | //Inspired by https://github.com/RaYell/selenium-webdriver-extensions 58 | let private loadJQuery (browser : IWebDriver) = 59 | let jsBrowser = browser :?> IJavaScriptExecutor 60 | let jqueryExistsScript = """return (typeof window.jQuery) === 'function';""" 61 | let exists = jsBrowser.ExecuteScript(jqueryExistsScript) :?> bool 62 | if not exists then 63 | let load = """ 64 | var jq = document.createElement('script'); 65 | jq.src = '//code.jquery.com/jquery-2.2.1.min.js'; 66 | document.getElementsByTagName('head')[0].appendChild(jq); 67 | """ 68 | jsBrowser.ExecuteScript(load) |> ignore 69 | canopy.wait.wait 2.0 (fun _ -> jsBrowser.ExecuteScript(jqueryExistsScript) :?> bool) 70 | 71 | type ByJQuery (selector, browser) = 72 | inherit OpenQA.Selenium.By() 73 | 74 | do 75 | let findElements (context : ISearchContext) = 76 | loadJQuery browser 77 | if context :? IWebDriver 78 | then 79 | let script = sprintf """return jQuery("%s").get();""" selector 80 | ((browser : IWebDriver) :?> IJavaScriptExecutor).ExecuteScript(script) :?> ReadOnlyCollection 81 | else 82 | let (script: string) = sprintf """return jQuery("%s", arguments[0]).get();""" selector 83 | let wrapper = context :?> OpenQA.Selenium.IWrapsDriver 84 | (wrapper.WrappedDriver :?> IJavaScriptExecutor).ExecuteScript(script, wrapper) :?> ReadOnlyCollection 85 | 86 | base.FindElementsMethod <- fun context -> findElements context 87 | 88 | base.FindElementMethod <- fun context -> findElements context |> Seq.head 89 | 90 | let findByJQuery jquerySelector f (browser: IWebDriver) = 91 | try 92 | f(ByJQuery(jquerySelector, browser) :> By) |> List.ofSeq 93 | with | _ -> [] 94 | 95 | //you can use this as an example to how to extend canopy by creating your own set of finders, tweaking the current collection, or adding/removing 96 | let mutable defaultFinders = 97 | (fun cssSelector f browser -> 98 | seq { 99 | yield findByCss cssSelector f browser 100 | yield findByValue cssSelector f browser 101 | yield findByXpath cssSelector f browser 102 | yield findByLabel cssSelector f browser 103 | yield findByText cssSelector f browser 104 | yield findByJQuery cssSelector f browser 105 | yield findByNormalizeSpaceText cssSelector f browser 106 | } 107 | ) 108 | 109 | let addedHints = Dictionary() 110 | let hints = new Dictionary (By -> ReadOnlyCollection) -> IWebDriver -> IWebElement list)>>() 111 | -------------------------------------------------------------------------------- /src/canopy/history.fs: -------------------------------------------------------------------------------- 1 | module canopy.history 2 | 3 | open System.IO 4 | open System 5 | 6 | let p = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\canopy\" 7 | let path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\canopy\failedContexts.txt" 8 | 9 | let save (results : string list) = 10 | if canopy.configuration.runFailedContextsFirst = true then 11 | if Directory.Exists(p) = false then Directory.CreateDirectory(p) |> ignore 12 | use sw = new StreamWriter(path) 13 | sw.Write (String.concat "|" results) 14 | 15 | let get _ = 16 | if File.Exists(path) = false then 17 | [] 18 | else 19 | use sr = new StreamReader(path) 20 | let line = sr.ReadToEnd() 21 | line.Split('|') |> List.ofArray -------------------------------------------------------------------------------- /src/canopy/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefthandedgoat/canopy/cb659d3367e9d165049db1a185f2b572cc5324fc/src/canopy/icon.jpg -------------------------------------------------------------------------------- /src/canopy/jarowinkler.fs: -------------------------------------------------------------------------------- 1 | /// Functions to compute the edit distance between two strings 2 | /// Taken from the Visual F# compiler 3 | module canopy.jaroWinkler 4 | 5 | /// Given an offset and a radius from that offset, 6 | /// does mChar exist in that part of str? 7 | let inline existsInWin (mChar: char) (str: string) offset rad = 8 | let startAt = max 0 (offset - rad) 9 | let endAt = min (offset + rad) (String.length str - 1) 10 | if endAt - startAt < 0 then false 11 | else 12 | let rec exists index = 13 | if str.[index] = mChar then true 14 | elif index = endAt then false 15 | else exists (index + 1) 16 | exists startAt 17 | 18 | /// The jaro distance between s1 and s2 19 | let jaro s1 s2 = 20 | // The radius is half of the lesser 21 | // of the two string lengths rounded up. 22 | let matchRadius = 23 | let minLen = 24 | min (String.length s1) (String.length s2) in 25 | minLen / 2 + minLen % 2 26 | 27 | // An inner function which recursively finds the number 28 | // of matched characters within the radius. 29 | let commonChars (chars1: string) (chars2: string) = 30 | let rec inner i result = 31 | match i with 32 | | -1 -> result 33 | | _ -> if existsInWin chars1.[i] chars2 i matchRadius 34 | then inner (i - 1) (chars1.[i] :: result) 35 | else inner (i - 1) result 36 | inner (chars1.Length - 1) [] 37 | 38 | // The sets of common characters and their lengths as floats 39 | let c1 = commonChars s1 s2 40 | let c2 = commonChars s2 s1 41 | let c1length = float (List.length c1) 42 | let c2length = float (List.length c2) 43 | 44 | // The number of transpositions within 45 | // the sets of common characters. 46 | let transpositions = 47 | let rec inner cl1 cl2 result = 48 | match cl1, cl2 with 49 | | [], _ | _, [] -> result 50 | | c1h :: c1t, c2h :: c2t -> 51 | if c1h <> c2h 52 | then inner c1t c2t (result + 1.0) 53 | else inner c1t c2t result 54 | let mismatches = inner c1 c2 0.0 55 | // If one common string is longer than the other 56 | // each additional char counts as half a transposition 57 | (mismatches + abs (c1length - c2length)) / 2.0 58 | 59 | let s1length = float (String.length s1) 60 | let s2length = float (String.length s2) 61 | let tLength = max c1length c2length 62 | 63 | // The jaro distance as given by 64 | // 1/3 ( m2/|s1| + m1/|s2| + (mc-t)/mc ) 65 | let result = (c1length / s1length + 66 | c2length / s2length + 67 | (tLength - transpositions) / tLength) 68 | / 3.0 69 | 70 | // This is for cases where |s1|, |s2| or m are zero 71 | if System.Double.IsNaN result then 0.0 else result 72 | 73 | type result = { selector : string; similarity : float } 74 | 75 | /// Calculates the Jaro-Winkler edit distance between two strings. 76 | /// The edit distance is a metric that allows to measure the amount of similarity between two strings. 77 | let JaroWinklerDistance s1 s2 = 78 | let jaroScore = jaro s1 s2 79 | // Accumulate the number of matching initial characters 80 | let maxLength = (min s1.Length s2.Length) - 1 81 | let rec calcL i acc = 82 | if i > maxLength || s1.[i] <> s2.[i] then acc 83 | else calcL (i + 1) (acc + 1.0) 84 | let l = min (calcL 0 0.0) 4.0 85 | // Calculate the JW distance 86 | let p = 0.1 87 | jaroScore + (l * p * (1.0 - jaroScore)) 88 | 89 | // [snippet:Remove first ocurrence from list] 90 | let rec remove char lst = 91 | match lst with 92 | | h::t when h = char -> t 93 | | h::t -> h::remove char t 94 | | _ -> [] 95 | 96 | let editdistance s1 s2 = { selector = s2; similarity = JaroWinklerDistance s1 s2 } -------------------------------------------------------------------------------- /src/canopy/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | Selenium.WebDriver 3 | System.Drawing.Common 4 | System.Text.Encoding.CodePages -------------------------------------------------------------------------------- /src/canopy/screen.fs: -------------------------------------------------------------------------------- 1 | module canopy.screen 2 | 3 | open System 4 | 5 | let mutable screenWidth = 1920 6 | let mutable screenHeight = 1080 7 | let mutable monitorCount = 1 8 | 9 | type ScreenBoundary = 10 | { 11 | width : int 12 | height: int 13 | x: int 14 | y: int 15 | size: Drawing.Size 16 | } 17 | 18 | let getPrimaryScreenResolution () = screenWidth, screenHeight 19 | 20 | let getPrimaryScreenBounds () = 21 | { 22 | width = screenWidth 23 | height = screenHeight 24 | x = 0 25 | y = 0 26 | size = Drawing.Size(screenWidth, screenHeight) 27 | } 28 | -------------------------------------------------------------------------------- /src/canopy/screenSizes.fs: -------------------------------------------------------------------------------- 1 | module screenSizes 2 | 3 | let iPhone4 = 320, 480 4 | let iPhone5 = 320, 568 5 | let iPad = 768, 1024 6 | let Nexus4 = 384, 598 7 | let Nexus7 = 603, 966 8 | -------------------------------------------------------------------------------- /src/canopy/types.fs: -------------------------------------------------------------------------------- 1 | module canopy.types 2 | 3 | open System 4 | open OpenQA.Selenium 5 | open Microsoft.FSharp.Reflection 6 | 7 | type CanopyException(message) = inherit Exception(message) 8 | type CanopyReadOnlyException(message) = inherit CanopyException(message) 9 | type CanopyOptionNotFoundException(message) = inherit CanopyException(message) 10 | type CanopySelectionFailedExeception(message) = inherit CanopyException(message) 11 | type CanopyDeselectionFailedException(message) = inherit CanopyException(message) 12 | type CanopyWaitForException(message) = inherit CanopyException(message) 13 | type CanopyElementNotFoundException(message) = inherit CanopyException(message) 14 | type CanopyMoreThanOneElementFoundException(message) = inherit CanopyException(message) 15 | type CanopyEqualityFailedException(message) = inherit CanopyException(message) 16 | type CanopyNotEqualsFailedException(message) = inherit CanopyException(message) 17 | type CanopyValueNotInListException(message) = inherit CanopyException(message) 18 | type CanopyValueInListException(message) = inherit CanopyException(message) 19 | type CanopyContainsFailedException(message) = inherit CanopyException(message) 20 | type CanopyNotContainsFailedException(message) = inherit CanopyException(message) 21 | type CanopyCountException(message) = inherit CanopyException(message) 22 | type CanopyDisplayedFailedException(message) = inherit CanopyException(message) 23 | type CanopyNotDisplayedFailedException(message) = inherit CanopyException(message) 24 | type CanopyEnabledFailedException(message) = inherit CanopyException(message) 25 | type CanopyDisabledFailedException(message) = inherit CanopyException(message) 26 | type CanopyNotStringOrElementException(message) = inherit CanopyException(message) 27 | type CanopyOnException(message) = inherit CanopyException(message) 28 | type CanopyCheckFailedException(message) = inherit CanopyException(message) 29 | type CanopyUncheckFailedException(message) = inherit CanopyException(message) 30 | type CanopyReadException(message) = inherit CanopyException(message) 31 | type CanopySkipTestException() = inherit CanopyException(String.Empty) 32 | type CanopyNoBrowserException(message) = inherit CanopyException(message) 33 | 34 | //directions 35 | type direction = 36 | | Left 37 | | Right 38 | | FullScreen 39 | 40 | //browser 41 | type BrowserStartMode = 42 | | Firefox 43 | | FirefoxWithPath of string 44 | | FirefoxWithUserAgent of string 45 | | FirefoxWithPathAndTimeSpan of string * TimeSpan 46 | | FirefoxWithProfileAndTimeSpan of Firefox.FirefoxProfile * TimeSpan 47 | | FirefoxWithOptions of Firefox.FirefoxOptions 48 | | FirefoxHeadless 49 | | IE 50 | | IEWithOptions of IE.InternetExplorerOptions 51 | | IEWithOptionsAndTimeSpan of IE.InternetExplorerOptions * TimeSpan 52 | | EdgeBETA 53 | | Chrome 54 | | ChromeWithOptions of Chrome.ChromeOptions 55 | | ChromeWithOptionsAndTimeSpan of Chrome.ChromeOptions * TimeSpan 56 | | ChromeWithUserAgent of string 57 | | ChromeHeadless 58 | | Chromium 59 | | ChromiumWithOptions of Chrome.ChromeOptions 60 | | Safari 61 | | Remote of string * ICapabilities 62 | 63 | let toString (x:'a) = 64 | match FSharpValue.GetUnionFields(x, typeof<'a>) with 65 | | case, _ -> case.Name 66 | 67 | type Test (description: string, func : (unit -> unit), number : int) = 68 | member x.Description = description 69 | member x.Func = func 70 | member x.Number = number 71 | member x.Id = if description = null then (String.Format("Test #{0}", number)) else description 72 | 73 | type suite () = class 74 | member val Context : string = null with get, set 75 | member val TotalTestsCount : int = 0 with get, set 76 | member val Once = fun () -> () with get, set 77 | member val Before = fun () -> () with get, set 78 | member val After = fun () -> () with get, set 79 | member val Lastly = fun () -> () with get, set 80 | member val OnPass = fun () -> () with get, set 81 | member val OnFail = fun () -> () with get, set 82 | member val Tests : Test list = [] with get, set 83 | member val Wips : Test list = [] with get, set 84 | member val Manys : Test list = [] with get, set 85 | member val Always : Test list = [] with get, set 86 | member val IsParallel = false with get, set 87 | member this.Clone() = this.MemberwiseClone() :?> suite 88 | end 89 | 90 | type Result = 91 | | Pass 92 | | Fail of Exception 93 | | Skip 94 | | Todo 95 | | FailFast 96 | | Failed 97 | 98 | type IReporter = 99 | abstract member testStart : string -> unit 100 | abstract member pass : string -> unit 101 | abstract member fail : Exception -> string -> byte [] -> string -> unit 102 | abstract member todo : string -> unit 103 | abstract member skip : string -> unit 104 | abstract member testEnd : string -> unit 105 | abstract member describe : string -> unit 106 | abstract member contextStart : string -> unit 107 | abstract member contextEnd : string -> unit 108 | abstract member summary : int -> int -> int -> int -> int -> unit 109 | abstract member write : string -> unit 110 | abstract member suggestSelectors : string -> string list -> unit 111 | abstract member quit : unit -> unit 112 | abstract member suiteBegin : unit -> unit 113 | abstract member suiteEnd : unit -> unit 114 | abstract member setEnvironment : string -> unit 115 | 116 | type Browsers = 117 | | BrowserStartModes of BrowserStartMode list 118 | | WebDrivers of IWebDriver list 119 | -------------------------------------------------------------------------------- /src/canopy/userAgents.fs: -------------------------------------------------------------------------------- 1 | module userAgents 2 | 3 | let iPhone = "Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25" 4 | let iPad = "Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25" 5 | let GalaxyNexus = "Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19" 6 | -------------------------------------------------------------------------------- /src/canopy/wait.fs: -------------------------------------------------------------------------------- 1 | module canopy.wait 2 | 3 | open OpenQA.Selenium 4 | 5 | let mutable waitSleep = 0.5 6 | 7 | let waitResults timeout (f : unit -> 'a) = 8 | let sw = System.Diagnostics.Stopwatch.StartNew() 9 | let mutable finalResult : 'a = Unchecked.defaultof<'a> 10 | let mutable keepGoing = true 11 | while keepGoing do 12 | try 13 | if sw.Elapsed.TotalSeconds >= timeout then raise <| WebDriverTimeoutException("Timed out!") 14 | 15 | let result = f() 16 | match box result with 17 | | :? bool as b -> 18 | if b then 19 | keepGoing <- false 20 | finalResult <- result 21 | else System.Threading.Thread.Sleep(int (waitSleep * 1000.0)) 22 | | _ as o -> 23 | if o <> null then 24 | keepGoing <- false 25 | finalResult <- result 26 | else System.Threading.Thread.Sleep(int (waitSleep * 1000.0)) 27 | with 28 | | :? WebDriverTimeoutException as ex -> reraise() 29 | | :? canopy.types.CanopyException as ce -> raise(ce) 30 | | _ -> System.Threading.Thread.Sleep(int (waitSleep * 1000.0)) 31 | 32 | finalResult 33 | 34 | let wait timeout (f : unit -> bool) = waitResults timeout f |> ignore 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | true 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/basictests/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "basictests" 17 | let [] AssemblyProduct = "canopy" 18 | let [] AssemblyVersion = "2.1.3" 19 | let [] AssemblyMetadata_ReleaseDate = "2020-03-07T00:00:00.0000000-06:00" 20 | let [] AssemblyFileVersion = "2.1.3" 21 | let [] AssemblyInformationalVersion = "2.1.3" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "d4bd2b919c8ab9142ed6bca402415c92d76c5eab" 24 | -------------------------------------------------------------------------------- /tests/basictests/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/basictests/basictests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/basictests/file1.fs: -------------------------------------------------------------------------------- 1 | module file1 2 | 3 | open canopy.classic 4 | open canopy.runner.classic 5 | 6 | let testpage = "http://lefthandedgoat.github.io/canopy/testpages/" 7 | 8 | let all() = 9 | context "file1" 10 | 11 | "from file 1 2" &&& fun _ -> 12 | url testpage 13 | "#welcome" == "Welcome" 14 | 15 | "from file 1 2" &&& fun _ -> 16 | url testpage 17 | "#welcome" == "Welcome" 18 | -------------------------------------------------------------------------------- /tests/basictests/file2.fs: -------------------------------------------------------------------------------- 1 | module file2 2 | 3 | open canopy.classic 4 | open canopy.runner.classic 5 | 6 | let testpage = "http://lefthandedgoat.github.io/canopy/testpages/" 7 | 8 | let all() = 9 | context "file2" 10 | 11 | "from file 2 1" &&&&& fun _ -> 12 | url testpage 13 | "#welcome" == "Welcome" 14 | 15 | "from file 2 2" &&&&& fun _ -> 16 | url testpage 17 | "#welcome" == "Welcome" 18 | -------------------------------------------------------------------------------- /tests/basictests/jsonValidatorTests.fs: -------------------------------------------------------------------------------- 1 | module jsonValidatorTests 2 | 3 | open canopy.classic 4 | open canopy.runner.classic 5 | open jsonValidator 6 | 7 | let ( == ) left right = if left <> right then failwith (sprintf "expected %A%sgot %A" right System.Environment.NewLine left) 8 | 9 | let person1 = """{ "first":"jane", "middle":"something", "last":"doe" } """ 10 | let person2 = """{ "first":"jane", "last":"doe" } """ 11 | let person3 = """{ "first":"jane", "middle":"something", "last":"doe", "phone":"800-555-5555" } """ 12 | 13 | let location1 = """{ "lat":4.0212, "long":12.102012, "people":[ 1, 2, 3 ] } """ 14 | let location2 = """{ "lat":4.0212, "long":12.102012, "people":[ ] } """ 15 | 16 | let location3 = """{ "lat":4.0212, "long":12.102012, "people":[ { "first":"jane", "middle":"something", "last":"doe" } ] } """ 17 | let location4 = """{ "lat":4.0212, "long":12.102012, "people":[ ] } """ 18 | let location5 = """{ "lat":4.0212, "long":12.102012, "people":[ { "first":"jane", "last":"doe" } ] } """ 19 | let location6 = """{ "lat":4.0212, "long":12.102012, "people":[ { "first":"jane", "middle":"something", "last":"doe", "phone":"800-555-5555" } ] } """ 20 | 21 | let location7 = """{ "lat":4.0212, "long":12.102012, "workers":[ { "first":"jane", "last":"doe" } ] } """ 22 | let location8 = """{ "lat":4.0212, "long":12.102012, "people":[ { "first":"jane", "middle":"something", "last":"doe", "phone":"800-555-5555" } ] } """ 23 | 24 | let class1 = """{ "name":"bio 101", "building":"science", "location": { "lat":4.0212, "long":12.102012, "people": [ { "first":"jane", "middle":"something", "last":"doe" } ] } }""" 25 | let class2 = """{ "name":"chem 101", "building":"science", "location": { "lat":4.0212, "lng":12.102012, "people": [ { "first":"jane", "last":"doe" } ] } }""" 26 | let class3 = """{ "name":"chem 101", "building":"science", "location": null }""" 27 | 28 | let withArray = """{ "name":"bio 101", "people": [ { "first":"jane" } ] }""" 29 | let nullArray = """{ "name":"chem 101", "people": null }""" 30 | 31 | let withProperty = """{ "name":"bio 101", "age": 1 }""" 32 | let nullProperty = """{ "name":"chem 101", "age": null }""" 33 | 34 | let all () = 35 | context "json validator tests" 36 | 37 | "two identical people have no differences" &&& fun _ -> 38 | diff person1 person1 == [] 39 | 40 | "missing property is identified" &&& fun _ -> 41 | diff person1 person2 == [ Missing "{root}.middle" ] 42 | 43 | "extra property is identified" &&& fun _ -> 44 | diff person1 person3 == [ Extra "{root}.phone" ] 45 | 46 | "empty array is acceptable array of ints" &&& fun _ -> 47 | diff location1 location2 == [ ] 48 | 49 | "empty array is acceptable array of records" &&& fun _ -> 50 | diff location3 location4 == [ ] 51 | 52 | "missing fields on records in arrays recognized correctly" &&& fun _ -> 53 | diff location3 location5 == [ Missing "{root}.[people].{}.middle" ] 54 | 55 | "extra fields on records in arrays recognized correctly" &&& fun _ -> 56 | diff location3 location6 == [ Extra "{root}.[people].{}.phone" ] 57 | 58 | "renamed field with extra property shows" &&& fun _ -> 59 | diff location7 location8 == 60 | [ 61 | Missing "{root}.[workers]" 62 | 63 | Extra "{root}.[people]" 64 | Extra "{root}.[people].{}" 65 | Extra "{root}.[people].{}.first" 66 | Extra "{root}.[people].{}.last" 67 | Extra "{root}.[people].{}.middle" 68 | Extra "{root}.[people].{}.phone" 69 | ] 70 | 71 | "nested objects with arrays reocgnized correctly" &&& fun _ -> 72 | diff class1 class2 == 73 | [ 74 | Missing "{root}.{location}.[people].{}.middle" 75 | Missing "{root}.{location}.long" 76 | 77 | Extra "{root}.{location}.lng" 78 | ] 79 | 80 | "null object property is ok" &&& fun _ -> 81 | diff class1 class3 == [ ] 82 | 83 | "null array is acceptable" &&& fun _ -> 84 | diff withArray nullArray == [ ] 85 | 86 | "null property is acceptable" &&& fun _ -> 87 | diff withProperty nullProperty == [ ] -------------------------------------------------------------------------------- /tests/basictests/loadTestTests.fs: -------------------------------------------------------------------------------- 1 | module loadTestTests 2 | 3 | open canopy.runner.classic 4 | open canopy.integration.loadTest 5 | open System.Net 6 | 7 | let all () = 8 | context "tests for the load tester in integration" 9 | 10 | "can create a basic job" &&! fun _ -> 11 | let job = 12 | { 13 | Warmup = true 14 | Baseline = true 15 | AcceptableRatioPercent = 200 16 | Minutes = 1 17 | Load = 1 18 | Tasks = 19 | [ 20 | { 21 | Description = "task1" 22 | Action = (fun _ -> 23 | use client = new WebClient() 24 | client.DownloadString("http://www.turtletest.com/chris") |> ignore 25 | printfn "task1") 26 | Frequency = 6 27 | } 28 | { 29 | Description = "task2" 30 | Action = (fun _ -> 31 | use client = new WebClient() 32 | client.DownloadString("http://www.turtletest.com/chris") |> ignore 33 | printfn "task2") 34 | Frequency = 6 35 | } 36 | ] 37 | } 38 | 39 | runLoadTest job 40 | 41 | "timelines look reasonable" &&! fun _ -> 42 | let job = 43 | { 44 | Warmup = true 45 | Baseline = true 46 | AcceptableRatioPercent = 200 47 | Minutes = 1 48 | Load = 1 49 | Tasks = 50 | [ 51 | { 52 | Description = "print hello world" 53 | Action = fun _ -> printfn "hello world" 54 | Frequency = 4 55 | } 56 | ] 57 | } 58 | 59 | createTimeline job 60 | |> List.iter (fun (timing, task) -> printfn "%A %A" timing task.Description) 61 | -------------------------------------------------------------------------------- /tests/basictests/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | Selenium.WebDriver 3 | Selenium.WebDriver.ChromeDriver 4 | Selenium.WebDriver.GeckoDriver -------------------------------------------------------------------------------- /tests/csharptests/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/csharptests/csharptests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/csharptests/paket.references: -------------------------------------------------------------------------------- 1 | Selenium.WebDriver 2 | Selenium.WebDriver.ChromeDriver 3 | Selenium.WebDriver.GeckoDriver -------------------------------------------------------------------------------- /tests/paralleltests/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/paralleltests/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "paralleltests" 17 | let [] AssemblyProduct = "canopy" 18 | let [] AssemblyVersion = "2.1.3" 19 | let [] AssemblyMetadata_ReleaseDate = "2020-03-07T00:00:00.0000000-06:00" 20 | let [] AssemblyFileVersion = "2.1.3" 21 | let [] AssemblyInformationalVersion = "2.1.3" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "d4bd2b919c8ab9142ed6bca402415c92d76c5eab" 24 | -------------------------------------------------------------------------------- /tests/paralleltests/Program.fs: -------------------------------------------------------------------------------- 1 | open prunner 2 | 3 | functionsTests.add() 4 | instancedTests.add() 5 | 6 | prunner.run 3 |> ignore 7 | 8 | System.Console.ReadKey() |> ignore -------------------------------------------------------------------------------- /tests/paralleltests/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | Selenium.WebDriver 3 | Selenium.WebDriver.ChromeDriver 4 | Selenium.WebDriver.GeckoDriver -------------------------------------------------------------------------------- /tests/paralleltests/paralleltests.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | --------------------------------------------------------------------------------