├── .credo.exs ├── .dialyzer_ignore.exs ├── .formatter.exs ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── issue.yaml ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── LICENSE.md ├── NOTICE.md ├── README.md ├── bin └── docker-run ├── config ├── config.exs ├── dev.exs ├── docs.exs ├── prod.exs └── test.exs ├── coveralls.json ├── flake.lock ├── flake.nix ├── guides └── images │ ├── icon.png │ └── logo.png ├── integration_test ├── cases │ ├── browser │ │ ├── all_test.exs │ │ ├── assert_css_test.exs │ │ ├── assert_refute_has_test.exs │ │ ├── assert_text_test.exs │ │ ├── attr_test.exs │ │ ├── button_down_test.exs │ │ ├── button_up_test.exs │ │ ├── clear_test.exs │ │ ├── click_button_test.exs │ │ ├── click_mouse_button_test.exs │ │ ├── click_test.exs │ │ ├── cookies_test.exs │ │ ├── current_path_test.exs │ │ ├── dialog_test.exs │ │ ├── double_click_test.exs │ │ ├── execute_script_test.exs │ │ ├── file_test.exs │ │ ├── fill_in_test.exs │ │ ├── find_test.exs │ │ ├── frames_test.exs │ │ ├── has_css_test.exs │ │ ├── has_text_test.exs │ │ ├── has_value_test.exs │ │ ├── hover_test.exs │ │ ├── invalid_selectors_test.exs │ │ ├── js_errors_test.exs │ │ ├── local_storage_test.exs │ │ ├── move_mouse_by_test.exs │ │ ├── navigation_test.exs │ │ ├── page_source_test.exs │ │ ├── screenshot_test.exs │ │ ├── select_test.exs │ │ ├── send_keys_test.exs │ │ ├── send_keys_to_active_element_test.exs │ │ ├── set_value_test.exs │ │ ├── stale_nodes_test.exs │ │ ├── tap_test.exs │ │ ├── text_test.exs │ │ ├── title_test.exs │ │ ├── touch_down_test.exs │ │ ├── touch_move_test.exs │ │ ├── touch_scroll_test.exs │ │ ├── touch_up_test.exs │ │ ├── visible_test.exs │ │ ├── window_handles_test.exs │ │ ├── window_position_test.exs │ │ └── window_size_test.exs │ ├── browser_test.exs │ ├── element │ │ ├── hover_test.exs │ │ ├── location_test.exs │ │ ├── send_keys_test.exs │ │ ├── size_test.exs │ │ ├── tap_test.exs │ │ ├── touch_down_test.exs │ │ └── touch_scroll_test.exs │ ├── feature │ │ ├── automatic_screenshot_test.exs │ │ ├── import_feature_test.exs │ │ └── use_feature_test.exs │ ├── inspect_test.exs │ ├── query_test.exs │ └── wallaby_test.exs ├── chrome │ ├── all_test.exs │ ├── capabilities_test.exs │ ├── starting_sessions_test.exs │ └── test_helper.exs ├── selenium │ ├── all_test.exs │ ├── selenium_capabilities_test.exs │ └── test_helper.exs ├── support │ ├── fixtures │ │ └── file.txt │ ├── helpers.ex │ ├── pages │ │ ├── click.html │ │ ├── dialogs.html │ │ ├── errors.html │ │ ├── forms.html │ │ ├── frames.html │ │ ├── index.html │ │ ├── index_page.ex │ │ ├── logs.html │ │ ├── mouse_down_and_up.html │ │ ├── move_mouse.html │ │ ├── nesting.html │ │ ├── page_1.ex │ │ ├── page_1.html │ │ ├── page_2.html │ │ ├── page_3.html │ │ ├── select_boxes.html │ │ ├── stale_nodes.html │ │ ├── touch.html │ │ ├── wait.html │ │ └── windows.html │ ├── session_case.ex │ └── test_server.ex └── tests.exs ├── lib ├── event_emitter.ex ├── wallaby.ex └── wallaby │ ├── browser.ex │ ├── chrome.ex │ ├── chrome │ ├── chromedriver.ex │ ├── chromedriver │ │ ├── readiness_checker.ex │ │ └── server.ex │ └── logger.ex │ ├── driver.ex │ ├── driver │ ├── external_command.ex │ ├── log_checker.ex │ ├── log_store.ex │ ├── temporary_path.ex │ └── utils.ex │ ├── dsl.ex │ ├── element.ex │ ├── exceptions.ex │ ├── feature.ex │ ├── helpers │ └── key_codes.ex │ ├── httpclient.ex │ ├── metadata.ex │ ├── partition_supervisor.ex │ ├── query.ex │ ├── query │ ├── error_message.ex │ └── xpath.ex │ ├── selenium.ex │ ├── session.ex │ ├── session_store.ex │ └── webdriver_client.ex ├── mix.exs ├── mix.lock ├── priv └── run_command.sh └── test ├── support ├── application_control.ex ├── chrome │ └── chrome_test_script.ex ├── http_client_case.ex ├── json_wire_protocol_responses.ex ├── settings_test_helpers.ex ├── test_script_utils.ex ├── test_workspace.ex └── utils.ex ├── test_helper.exs └── wallaby ├── browser_test.exs ├── chrome ├── chromedriver │ └── server_test.exs └── logger_test.exs ├── driver ├── temporary_path_test.exs └── utils_test.exs ├── helpers └── key_codes_test.exs ├── http_client_test.exs ├── metadata_test.exs ├── query ├── error_message_test.exs └── xpath_test.exs ├── query_test.exs ├── selenium ├── start_session_config_test.exs └── webdriver_client_test.exs ├── selenium_test.exs └── session_store_test.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any exec using `mix credo -C `. If no exec name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: ["lib/", "src/", "web/", "apps/"], 25 | excluded: [~r"/_build/", ~r"/deps/"] 26 | }, 27 | # 28 | # If you create your own checks, you must specify the source files for 29 | # them here, so they can be loaded by Credo before running the analysis. 30 | # 31 | requires: [], 32 | # 33 | # If you want to enforce a style guide and need a more traditional linting 34 | # experience, you can change `strict` to `true` below: 35 | # 36 | strict: true, 37 | # 38 | # If you want to use uncolored output by default, you can change `color` 39 | # to `false` below: 40 | # 41 | color: true, 42 | # 43 | # You can customize the parameters of any check by adding a second element 44 | # to the tuple. 45 | # 46 | # To disable a check put `false` as second element: 47 | # 48 | # {Credo.Check.Design.DuplicatedCode, false} 49 | # 50 | checks: [ 51 | {Credo.Check.Consistency.ExceptionNames}, 52 | {Credo.Check.Consistency.LineEndings}, 53 | {Credo.Check.Consistency.ParameterPatternMatching}, 54 | {Credo.Check.Consistency.SpaceAroundOperators}, 55 | {Credo.Check.Consistency.SpaceInParentheses}, 56 | {Credo.Check.Consistency.TabsOrSpaces}, 57 | 58 | # For some checks, like AliasUsage, you can only customize the priority 59 | # Priority values are: `low, normal, high, higher` 60 | # 61 | {Credo.Check.Design.AliasUsage, false}, 62 | 63 | # For others you can set parameters 64 | 65 | # If you don't want the `setup` and `test` macro calls in ExUnit tests 66 | # or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just 67 | # set the `excluded_macros` parameter to `[:schema, :setup, :test]`. 68 | # 69 | {Credo.Check.Design.DuplicatedCode, false}, 70 | 71 | # You can also customize the exit_status of each check. 72 | # If you don't want TODO comments to cause `mix credo` to fail, just 73 | # set this value to 0 (zero). 74 | # 75 | {Credo.Check.Design.TagTODO, exit_status: 0, include_doc: false}, 76 | {Credo.Check.Design.TagFIXME}, 77 | {Credo.Check.Readability.FunctionNames}, 78 | {Credo.Check.Readability.LargeNumbers}, 79 | {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 100}, 80 | {Credo.Check.Readability.ModuleAttributeNames}, 81 | {Credo.Check.Readability.ModuleDoc}, 82 | {Credo.Check.Readability.ModuleNames}, 83 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, false}, 84 | {Credo.Check.Readability.ParenthesesInCondition}, 85 | {Credo.Check.Readability.PredicateFunctionNames}, 86 | {Credo.Check.Readability.PreferImplicitTry}, 87 | {Credo.Check.Readability.RedundantBlankLines}, 88 | {Credo.Check.Readability.StringSigils}, 89 | {Credo.Check.Readability.TrailingBlankLine}, 90 | {Credo.Check.Readability.TrailingWhiteSpace}, 91 | {Credo.Check.Readability.VariableNames}, 92 | {Credo.Check.Readability.Semicolons}, 93 | {Credo.Check.Readability.SpaceAfterCommas}, 94 | {Credo.Check.Refactor.DoubleBooleanNegation}, 95 | {Credo.Check.Refactor.CondStatements}, 96 | {Credo.Check.Refactor.CyclomaticComplexity, false}, 97 | {Credo.Check.Refactor.FunctionArity}, 98 | {Credo.Check.Refactor.LongQuoteBlocks}, 99 | {Credo.Check.Refactor.MatchInCondition}, 100 | {Credo.Check.Refactor.NegatedConditionsInUnless}, 101 | {Credo.Check.Refactor.NegatedConditionsWithElse}, 102 | {Credo.Check.Refactor.Nesting}, 103 | {Credo.Check.Refactor.PipeChainStart, false}, 104 | {Credo.Check.Refactor.UnlessWithElse}, 105 | {Credo.Check.Warning.BoolOperationOnSameValues}, 106 | {Credo.Check.Warning.IExPry}, 107 | {Credo.Check.Warning.IoInspect}, 108 | {Credo.Check.Warning.Dbg}, 109 | {Credo.Check.Warning.LazyLogging}, 110 | {Credo.Check.Warning.OperationOnSameValues}, 111 | {Credo.Check.Warning.OperationWithConstantResult}, 112 | {Credo.Check.Warning.UnusedEnumOperation}, 113 | {Credo.Check.Warning.UnusedFileOperation}, 114 | {Credo.Check.Warning.UnusedKeywordOperation}, 115 | {Credo.Check.Warning.UnusedListOperation}, 116 | {Credo.Check.Warning.UnusedPathOperation}, 117 | {Credo.Check.Warning.UnusedRegexOperation}, 118 | {Credo.Check.Warning.UnusedStringOperation}, 119 | {Credo.Check.Warning.UnusedTupleOperation}, 120 | {Credo.Check.Warning.RaiseInsideRescue}, 121 | 122 | # Controversial and experimental checks (opt-in, just remove `, false`) 123 | # 124 | {Credo.Check.Refactor.ABCSize, false}, 125 | {Credo.Check.Refactor.AppendSingleItem, false}, 126 | {Credo.Check.Refactor.VariableRebinding, false}, 127 | {Credo.Check.Warning.MapGetUnsafePass, false}, 128 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 129 | 130 | # Deprecated checks (these will be deleted after a grace period) 131 | # 132 | {Credo.Check.Readability.Specs, false} 133 | # Custom checks can be created using `mix credo.gen.check`. 134 | # 135 | ] 136 | } 137 | ] 138 | } 139 | -------------------------------------------------------------------------------- /.dialyzer_ignore.exs: -------------------------------------------------------------------------------- 1 | # Used for ignoring dialyzer errors 2 | # See: https://github.com/jeremyjh/dialyxir#elixir-term-format 3 | [ 4 | ~r"Function Mix.env/0 does not exist.", 5 | ~r"Function ExUnit.after_suite/1 does not exist." 6 | ] 7 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,integration_test,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [mhanberg] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report an issue 3 | description: 4 | Please describe the the bug you believe to have discovered. 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: > 9 | Thank you for contributing to Elixir! :heart: 10 | 11 | 12 | Please, do not use this form for guidance, questions or support. 13 | Try instead in [Elixir Forum](https://elixirforum.com), 14 | the [IRC Chat](https://web.libera.chat/#elixir), 15 | [Stack Overflow](https://stackoverflow.com/questions/tagged/elixir), 16 | [Slack](https://elixir-slackin.herokuapp.com), 17 | [Discord](https://discord.gg/elixir) or in other online communities. 18 | 19 | - type: textarea 20 | id: elixir-and-otp-version 21 | attributes: 22 | label: Elixir and Erlang/OTP versions 23 | description: Paste the output of `elixir --version` here. 24 | validations: 25 | required: true 26 | 27 | - type: input 28 | id: os 29 | attributes: 30 | label: Operating system 31 | description: The operating system that this issue is happening on. 32 | validations: 33 | required: true 34 | 35 | - type: input 36 | id: browser 37 | attributes: 38 | label: Browser 39 | description: The browser which is demonstrating the error. 40 | validations: 41 | required: true 42 | 43 | - type: input 44 | id: driver 45 | attributes: 46 | label: Driver 47 | description: The webdriver that is demonstrating the error (ChromeDriver, GeckoDriver, Selenium, etc) 48 | validations: 49 | required: true 50 | 51 | - type: checkboxes 52 | id: confirmation 53 | attributes: 54 | label: Correct Configuration 55 | description: This includes `base_url` is set, assets are being compiled before running your tests, Ecto sandbox is configured, server is st to true for Phoenix Endpoints. 56 | options: 57 | - label: I confirm that I have Wallaby configured correctly. 58 | required: true 59 | 60 | - type: textarea 61 | id: current-behavior 62 | attributes: 63 | label: Current behavior 64 | description: > 65 | Include code samples, errors, and stacktraces if appropriate. 66 | 67 | 68 | If reporting a bug, please include the reproducing steps. 69 | validations: 70 | required: true 71 | 72 | - type: textarea 73 | id: expected-behavior 74 | attributes: 75 | label: Expected behavior 76 | description: A short description on how you expect the code to behave. 77 | validations: 78 | required: true 79 | 80 | - type: textarea 81 | id: user-code 82 | attributes: 83 | label: Test Code & HTML 84 | description: Please include your failing test and any relevant HTML. This can be your template and the HTML that is actually rendered in the browser. 85 | validations: 86 | required: true 87 | 88 | - type: input 89 | id: demonstration-project 90 | attributes: 91 | label: Demonstration Project 92 | description: If you are able to provide a minimal reproduction, please include a link to the repository. 93 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | /.tool-versions-e 7 | 8 | /screenshots 9 | /doc 10 | /benchmarks/html 11 | /docs 12 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.18.1 2 | erlang 27.2 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Chris Keathley 4 | Copyright (c) 2022 Mitchell Hanberg 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | LEGAL NOTICE INFORMATION 2 | ------------------------ 3 | 4 | All the files in this distribution are copyright to the terms below. 5 | 6 | == lib/wallaby/partition_supervisor.ex 7 | 8 | Copyright 2012 Plataformatec 9 | Copyright 2021 The Elixir Team 10 | 11 | Licensed under the Apache License, Version 2.0 (the "License"); 12 | you may not use this file except in compliance with the License. 13 | You may obtain a copy of the License at 14 | 15 | https://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, 19 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | See the License for the specific language governing permissions and 21 | limitations under the License. 22 | 23 | == All other files 24 | 25 | The MIT License (MIT) 26 | 27 | Copyright (c) 2016 Chris Keathley 28 | Copyright (c) 2022 Mitchell Hanberg 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining a copy 31 | of this software and associated documentation files (the "Software"), to deal 32 | in the Software without restriction, including without limitation the rights 33 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 34 | copies of the Software, and to permit persons to whom the Software is 35 | furnished to do so, subject to the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be included in all 38 | copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 45 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 46 | SOFTWARE. 47 | -------------------------------------------------------------------------------- /bin/docker-run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function run() { 4 | local elixir="$1" 5 | local erlang="$2" 6 | 7 | cat < all(Query.css("li")) 14 | |> Enum.count() == 3 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /integration_test/cases/browser/assert_css_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.AssertCssTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | test "has_css/2 returns true if the css is on the page", %{session: session} do 5 | page = 6 | session 7 | |> visit("nesting.html") 8 | 9 | assert has_css?(page, ".user") 10 | end 11 | 12 | test "has_css/2 returns false if the css is not on the page", %{session: session} do 13 | page = 14 | session 15 | |> visit("nesting.html") 16 | 17 | refute has_css?(page, ".something_else") 18 | end 19 | 20 | test "has_css/3 returns true if the css is on the page", %{session: session} do 21 | page = 22 | session 23 | |> visit("nesting.html") 24 | 25 | assert has_css?(page, Query.css(".dashboard"), ".users") 26 | end 27 | 28 | test "has_css/3 returns false if the css is not on the page", %{session: session} do 29 | page = 30 | session 31 | |> visit("nesting.html") 32 | 33 | refute has_css?(page, Query.css(".dashboard"), ".something_else") 34 | end 35 | 36 | test "has_no_css/2 returns true if the css is not on the page", %{session: session} do 37 | page = 38 | session 39 | |> visit("nesting.html") 40 | 41 | assert has_no_css?(page, ".something_else") 42 | end 43 | 44 | test "has_no_css/2 returns false if the css is on the page", %{session: session} do 45 | page = 46 | session 47 | |> visit("nesting.html") 48 | 49 | refute has_no_css?(page, ".user") 50 | end 51 | 52 | test "has_no_css/3 returns false if the css is on the page", %{session: session} do 53 | page = 54 | session 55 | |> visit("nesting.html") 56 | 57 | refute has_no_css?(page, Query.css(".dashboard"), ".user") 58 | end 59 | 60 | test "has_no_css/3 returns true if the css is not on the page", %{session: session} do 61 | page = 62 | session 63 | |> visit("nesting.html") 64 | 65 | assert has_no_css?(page, Query.css(".dashboard"), ".something_else") 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /integration_test/cases/browser/assert_refute_has_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.AssertRefuteHasTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | alias Wallaby.ExpectationNotMetError 5 | 6 | @found_query Query.css(".user", count: :any) 7 | @not_found_query Query.css(".something-else") 8 | @wrong_exact_found_query Query.css(".user", count: 5) 9 | @at_query Query.css(".user", at: 3) 10 | @wrong_at_query Query.css(".user", at: 7) 11 | describe "assert_has/2" do 12 | test "passes if the query is present on the page", %{session: session} do 13 | return = 14 | session 15 | |> visit("nesting.html") 16 | |> assert_has(@found_query) 17 | 18 | assert %Wallaby.Session{} = return 19 | end 20 | 21 | test "raises if the query is not found", %{session: session} do 22 | assert_raise ExpectationNotMetError, ~r/Expected.+ 1.*css.*\.something-else.*0/i, fn -> 23 | session 24 | |> visit("nesting.html") 25 | |> assert_has(@not_found_query) 26 | end 27 | end 28 | 29 | test "mentions the count of found vs. expected elements", %{session: session} do 30 | assert_raise ExpectationNotMetError, ~r/Expected.+ 5.*css.*\.user.*6/i, fn -> 31 | session 32 | |> visit("nesting.html") 33 | |> assert_has(@wrong_exact_found_query) 34 | end 35 | end 36 | 37 | test "mentions the count of found vs. expected index", %{session: session} do 38 | expect = 39 | ~r/Expected.*and return element at index 7, but only 6 visible elements were found/i 40 | 41 | assert_raise ExpectationNotMetError, expect, fn -> 42 | session 43 | |> visit("nesting.html") 44 | |> assert_has(@wrong_at_query) 45 | end 46 | end 47 | 48 | test "passes if `at` element exists", %{session: session} do 49 | return = 50 | session 51 | |> visit("nesting.html") 52 | |> assert_has(@at_query) 53 | 54 | assert %Wallaby.Session{} = return 55 | end 56 | end 57 | 58 | describe "refute_has/2" do 59 | test "passes if the query is not found on the page", %{session: session} do 60 | return = 61 | session 62 | |> visit("nesting.html") 63 | |> refute_has(@not_found_query) 64 | 65 | assert %Wallaby.Session{} = return 66 | end 67 | 68 | test "raises if the query is found on the page", %{session: session} do 69 | assert_raise ExpectationNotMetError, ~r/Expected not.+any.*css.*\.user.*6/i, fn -> 70 | session 71 | |> visit("nesting.html") 72 | |> refute_has(@found_query) 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /integration_test/cases/browser/assert_text_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.AssertTextTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | alias Wallaby.ExpectationNotMetError 5 | 6 | test "has_text?/2 waits for presence of text and returns a bool", %{session: session} do 7 | element = 8 | session 9 | |> visit("wait.html") 10 | |> find(Query.css("#container")) 11 | 12 | assert has_text?(element, "main") 13 | refute has_text?(element, "rain") 14 | end 15 | 16 | test "assert_text/2 waits for presence of text and and returns the parent if found", %{ 17 | session: session 18 | } do 19 | element = 20 | session 21 | |> visit("wait.html") 22 | |> find(Query.css("#container")) 23 | 24 | assert element == assert_text(element, "main") 25 | end 26 | 27 | test "assert_text/2 will raise an exception for text not found", %{session: session} do 28 | element = 29 | session 30 | |> visit("wait.html") 31 | |> find(Query.css("#container")) 32 | 33 | assert_raise ExpectationNotMetError, "Text 'rain' was not found.", fn -> 34 | assert_text(element, "rain") 35 | end 36 | end 37 | 38 | test "assert_text/2 works with sessions", %{session: session} do 39 | session 40 | |> Browser.visit("wait.html") 41 | 42 | assert session == Browser.assert_text(session, "main") 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /integration_test/cases/browser/attr_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.AttrTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | test "can get the attributes of a query", %{session: session} do 5 | class = 6 | session 7 | |> visit("/") 8 | |> attr(Query.css("body"), "class") 9 | 10 | assert class =~ "bootstrap" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /integration_test/cases/browser/button_down_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.ButtonDownTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | import Wallaby.Browser 4 | 5 | setup %{session: session} do 6 | {:ok, page: visit(session, "mouse_down_and_up.html")} 7 | end 8 | 9 | describe "button_down/2" do 10 | test "clicks and holds left mouse button at the current cursor position", %{page: page} do 11 | button_down_test(page, :left, "Left") 12 | end 13 | 14 | test "clicks and holds middle mouse button at the current cursor position", %{page: page} do 15 | button_down_test(page, :middle, "Middle") 16 | end 17 | 18 | test "clicks and holds right mouse button at the current cursor position", %{page: page} do 19 | button_down_test(page, :right, "Right") 20 | end 21 | end 22 | 23 | defp button_down_test(page, button, expected_log_prefix) do 24 | refute page 25 | |> visible?(Query.text("#{expected_log_prefix} Down")) 26 | 27 | assert page 28 | |> hover(Query.text("Button 1")) 29 | |> button_down(button) 30 | |> visible?(Query.text("#{expected_log_prefix} Down")) 31 | 32 | refute page 33 | |> visible?(Query.text("#{expected_log_prefix} Up")) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /integration_test/cases/browser/button_up_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.ButtonUpTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | import Wallaby.Browser 4 | 5 | setup %{session: session} do 6 | {:ok, page: visit(session, "mouse_down_and_up.html")} 7 | end 8 | 9 | describe "button_down/2 releases previously held mouse button at the current cursor position" do 10 | test "for left button", %{page: page} do 11 | button_up_test(page, :left, "Left") 12 | end 13 | 14 | test "for middle button", %{page: page} do 15 | button_up_test(page, :middle, "Middle") 16 | end 17 | 18 | test "for right button", %{page: page} do 19 | button_up_test(page, :right, "Right") 20 | end 21 | end 22 | 23 | describe "button_down/2 releases previously held mouse button if cursor is moved from the position where the button was pressed" do 24 | test "for left button", %{page: page} do 25 | move_cursor_then_button_up_test(page, :left, "Left") 26 | end 27 | 28 | test "for middle button", %{page: page} do 29 | move_cursor_then_button_up_test(page, :middle, "Middle") 30 | end 31 | 32 | test "for right button", %{page: page} do 33 | move_cursor_then_button_up_test(page, :right, "Right") 34 | end 35 | end 36 | 37 | defp button_up_test(page, button, expected_log_prefix) do 38 | refute page 39 | |> visible?(Query.text("#{expected_log_prefix} Up")) 40 | 41 | assert page 42 | |> hover(Query.text("Button 1")) 43 | |> button_down(button) 44 | |> button_up(button) 45 | |> visible?(Query.text("#{expected_log_prefix} Up")) 46 | 47 | refute page 48 | |> visible?(Query.text("#{expected_log_prefix} Down")) 49 | end 50 | 51 | defp move_cursor_then_button_up_test(page, button, expected_log_prefix) do 52 | refute page 53 | |> visible?(Query.text("#{expected_log_prefix} Up")) 54 | 55 | assert page 56 | |> hover(Query.text("Button 1")) 57 | |> button_down(button) 58 | |> hover(Query.text("Button 2")) 59 | |> button_up(button) 60 | |> visible?(Query.text("#{expected_log_prefix} Up")) 61 | 62 | refute page 63 | |> visible?(Query.text("#{expected_log_prefix} Down")) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /integration_test/cases/browser/clear_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.ClearTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | test "clearing input", %{session: session} do 5 | element = 6 | session 7 | |> visit("forms.html") 8 | |> find(Query.css("#name_field")) 9 | 10 | Element.fill_in(element, with: "Chris") 11 | assert has_value?(element, "Chris") 12 | 13 | Element.clear(element) 14 | refute has_value?(element, "Chris") 15 | assert has_value?(element, "") 16 | end 17 | 18 | describe "clear/2" do 19 | setup %{session: session} do 20 | page = visit(session, "forms.html") 21 | {:ok, %{page: page}} 22 | end 23 | 24 | test "works with queries", %{page: page} do 25 | assert page 26 | |> fill_in(Query.text_field("name_field"), with: "test") 27 | |> clear(Query.text_field("name_field")) 28 | |> text(Query.text_field("name_field")) == "" 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /integration_test/cases/browser/click_mouse_button_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.ClickMouseButtonTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "click.html") 6 | 7 | {:ok, %{page: page}} 8 | end 9 | 10 | describe "click/2 for clicking at current mouse position" do 11 | test "clicks left button", %{page: page} do 12 | refute page 13 | |> visible?(Query.text("Left")) 14 | 15 | assert page 16 | |> hover(Query.text("Click")) 17 | |> click(:left) 18 | |> visible?(Query.text("Left")) 19 | end 20 | 21 | test "clicks middle button", %{page: page} do 22 | refute page 23 | |> visible?(Query.text("Middle")) 24 | 25 | assert page 26 | |> hover(Query.text("Click")) 27 | |> click(:middle) 28 | |> visible?(Query.text("Middle")) 29 | end 30 | 31 | test "clicks right button", %{page: page} do 32 | refute page 33 | |> visible?(Query.text("Right")) 34 | 35 | assert page 36 | |> hover(Query.text("Click")) 37 | |> click(:right) 38 | |> visible?(Query.text("Right")) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /integration_test/cases/browser/click_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.ClickTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "forms.html") 6 | 7 | {:ok, %{page: page}} 8 | end 9 | 10 | describe "click/2" do 11 | test "accepts queries", %{page: page} do 12 | assert page 13 | |> click(Query.button("Submit button")) 14 | end 15 | 16 | test "can click invisible elements", %{page: page} do 17 | assert page 18 | |> click(Query.button("Invisible Button", visible: false)) 19 | end 20 | 21 | test "can be chained/returns parent", %{page: page} do 22 | page 23 | |> click(Query.css("#option1")) 24 | |> click(Query.css("#option2")) 25 | 26 | assert selected?(page, Query.css("#option2")) 27 | end 28 | end 29 | 30 | describe "click/2 with radio buttons (choose replacement)" do 31 | test "choosing a radio button", %{page: page} do 32 | refute selected?(page, Query.css("#option2")) 33 | 34 | page 35 | |> click(Query.radio_button("option2")) 36 | 37 | assert selected?(page, Query.css("#option2")) 38 | end 39 | 40 | test "choosing a radio button unchecks other buttons in the group", %{page: page} do 41 | page 42 | |> click(Query.radio_button("Option 1")) 43 | |> selected?(Query.css("#option1")) 44 | |> assert 45 | 46 | page 47 | |> click(Query.radio_button("option2")) 48 | 49 | refute selected?(page, Query.css("#option1")) 50 | assert selected?(page, Query.css("#option2")) 51 | end 52 | 53 | test "throw an error if a label exists but does not have a for attribute", %{page: page} do 54 | bad_form = 55 | page 56 | |> find(Query.css(".bad-form")) 57 | 58 | assert_raise Wallaby.QueryError, fn -> 59 | click(bad_form, Query.radio_button("Radio with bad label")) 60 | end 61 | end 62 | 63 | test "throw an error if the query matches multiple labels", %{page: page} do 64 | assert_raise Wallaby.QueryError, ~r/Expected (.*) 1/, fn -> 65 | click(page, Query.radio_button("Duplicate Radiobutton")) 66 | end 67 | end 68 | 69 | test "waits until the radio button appears", %{page: page} do 70 | assert click(page, Query.radio_button("Hidden Radio Button")) 71 | end 72 | 73 | test "escape quotes", %{page: page} do 74 | assert click(page, Query.radio_button("I'm a radio button")) 75 | end 76 | end 77 | 78 | describe "click/2 with checkboxes" do 79 | test "checking a checkbox", %{page: page} do 80 | assert page 81 | |> click(Query.checkbox("Checkbox 1")) 82 | |> click(Query.checkbox("Checkbox 1")) 83 | 84 | refute page 85 | |> find(Query.checkbox("Checkbox 1")) 86 | |> Element.selected?() 87 | end 88 | 89 | test "escapes quotes", %{page: page} do 90 | assert click(page, Query.checkbox("I'm a checkbox")) 91 | end 92 | 93 | test "throw an error if a label exists but does not have a for attribute", %{page: page} do 94 | assert_raise Wallaby.QueryError, fn -> 95 | click(page, Query.checkbox("Checkbox with bad label")) 96 | end 97 | end 98 | 99 | test "waits until the checkbox appears", %{page: page} do 100 | assert click(page, Query.checkbox("Hidden Checkbox")) 101 | end 102 | end 103 | 104 | describe "click/2 with links" do 105 | test "works with queries", %{page: page} do 106 | assert page 107 | |> visit("") 108 | |> click(Query.link("Page 1")) 109 | |> assert_has(Query.css(".blue")) 110 | end 111 | end 112 | 113 | describe "click/2 with buttons" do 114 | test "works with queries", %{page: page} do 115 | assert page 116 | |> click(Query.button("Reset input")) 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /integration_test/cases/browser/cookies_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.CookiesTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | alias Wallaby.CookieError 5 | 6 | describe "cookies/1" do 7 | test "returns all of the cookies in the browser", %{session: session} do 8 | list = 9 | session 10 | |> visit("/") 11 | |> Browser.cookies() 12 | 13 | assert list == [] 14 | end 15 | end 16 | 17 | describe "set_cookie/3" do 18 | test "sets a cookie in the browser", %{session: session} do 19 | cookie = 20 | session 21 | |> visit("/") 22 | |> Browser.set_cookie("api_token", "abc123") 23 | |> visit("/index.html") 24 | |> Browser.cookies() 25 | |> hd() 26 | 27 | assert cookie["name"] == "api_token" 28 | assert cookie["value"] == "abc123" 29 | assert cookie["path"] == "/" 30 | assert cookie["secure"] == false 31 | assert cookie["httpOnly"] == false 32 | end 33 | 34 | test "without visiting a page first throws an error", %{session: session} do 35 | assert_raise CookieError, fn -> 36 | session 37 | |> Browser.set_cookie("other_cookie", "test") 38 | end 39 | end 40 | end 41 | 42 | describe "set_cookie/4" do 43 | test "sets a cookie in the browser", %{session: session} do 44 | expiry = DateTime.utc_now() |> DateTime.to_unix() |> Kernel.+(1000) 45 | 46 | cookie = 47 | session 48 | |> visit("/") 49 | |> Browser.set_cookie("api_token", "abc123", 50 | path: "/index.html", 51 | secure: true, 52 | httpOnly: true, 53 | expiry: expiry 54 | ) 55 | |> visit("/index.html") 56 | |> Browser.cookies() 57 | |> hd() 58 | 59 | assert cookie["name"] == "api_token" 60 | assert cookie["value"] == "abc123" 61 | assert cookie["path"] == "/index.html" 62 | assert cookie["secure"] == true 63 | assert cookie["httpOnly"] == true 64 | assert cookie["expiry"] == expiry 65 | end 66 | 67 | test "without visiting a page first throws an error", %{session: session} do 68 | assert_raise CookieError, fn -> 69 | session 70 | |> Browser.set_cookie("other_cookie", "test", secure: true, httpOnly: true) 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /integration_test/cases/browser/current_path_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.CurrentPathTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | alias Wallaby.Integration.Pages.{IndexPage, Page1} 5 | 6 | test "gets the current_url of the session", %{session: session} do 7 | url = 8 | session 9 | |> IndexPage.visit() 10 | |> IndexPage.click_page_1_link() 11 | |> Page1.ensure_page_loaded() 12 | |> current_url() 13 | 14 | assert url == "http://localhost:#{URI.parse(url).port}/page_1.html" 15 | end 16 | 17 | test "gets the current_path of the session", %{session: session} do 18 | path = 19 | session 20 | |> IndexPage.visit() 21 | |> IndexPage.click_page_1_link() 22 | |> Page1.ensure_page_loaded() 23 | |> current_path() 24 | 25 | assert path == "/page_1.html" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /integration_test/cases/browser/dialog_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.DialogTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "dialogs.html") 6 | {:ok, %{page: page}} 7 | end 8 | 9 | describe "accept_alert/2" do 10 | test "accept window.alert and get message", %{page: page} do 11 | message = 12 | accept_alert(page, fn p -> 13 | click(p, Query.link("Alert")) 14 | end) 15 | 16 | result = 17 | page 18 | |> find(Query.css("#result")) 19 | |> Element.text() 20 | 21 | assert message == "This is an alert!" 22 | assert result == "Alert accepted" 23 | end 24 | end 25 | 26 | describe "accept_confirm/2" do 27 | test "accept window.confirm and get message", %{page: page} do 28 | message = 29 | accept_confirm(page, fn p -> 30 | click(p, Query.link("Confirm")) 31 | end) 32 | 33 | result = 34 | page 35 | |> find(Query.css("#result")) 36 | |> Element.text() 37 | 38 | assert message == "Are you sure?" 39 | assert result == "Confirm returned true" 40 | end 41 | end 42 | 43 | describe "dismiss_confirm/2" do 44 | test "dismiss window.confirm and get message", %{page: page} do 45 | message = 46 | dismiss_confirm(page, fn p -> 47 | click(p, Query.link("Confirm")) 48 | end) 49 | 50 | result = 51 | page 52 | |> find(Query.css("#result")) 53 | |> Element.text() 54 | 55 | assert message == "Are you sure?" 56 | assert result == "Confirm returned false" 57 | end 58 | end 59 | 60 | describe "accept_prompt/2" do 61 | test "accept window.prompt with default value and get message", %{page: page} do 62 | message = 63 | accept_prompt(page, fn p -> 64 | click(p, Query.link("Prompt")) 65 | end) 66 | 67 | result = 68 | page 69 | |> find(Query.css("#result")) 70 | |> Element.text() 71 | 72 | assert message == "What's your name?" 73 | assert result == "Prompt returned default" 74 | end 75 | 76 | test "ensure the input value is a string", %{page: page} do 77 | assert_raise FunctionClauseError, fn -> 78 | accept_prompt(page, [with: nil], fn _ -> :noop end) 79 | end 80 | 81 | assert_raise FunctionClauseError, fn -> 82 | accept_prompt(page, [with: 123], fn _ -> :noop end) 83 | end 84 | 85 | assert_raise FunctionClauseError, fn -> 86 | accept_prompt(page, [with: :foo], fn _ -> :noop end) 87 | end 88 | end 89 | end 90 | 91 | describe "accept_prompt/3" do 92 | test "accept window.prompt with value and get message", %{page: page} do 93 | message = 94 | accept_prompt(page, [with: "Wallaby"], fn p -> 95 | click(p, Query.link("Prompt")) 96 | end) 97 | 98 | result = 99 | page 100 | |> find(Query.css("#result")) 101 | |> Element.text() 102 | 103 | assert message == "What's your name?" 104 | assert result == "Prompt returned Wallaby" 105 | end 106 | end 107 | 108 | describe "dismiss_prompt/2" do 109 | test "dismiss window.prompt and get message", %{page: page} do 110 | message = 111 | dismiss_prompt(page, fn p -> 112 | click(p, Query.link("Prompt")) 113 | end) 114 | 115 | result = 116 | page 117 | |> find(Query.css("#result")) 118 | |> Element.text() 119 | 120 | assert message == "What's your name?" 121 | assert result == "Prompt returned null" 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /integration_test/cases/browser/double_click_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.DoubleClickTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "click.html") 6 | 7 | {:ok, %{page: page}} 8 | end 9 | 10 | describe "double_click/1" do 11 | test "double-clicks left mouse button at the current cursor position", %{page: page} do 12 | refute page 13 | |> visible?(Query.text("Double")) 14 | 15 | assert page 16 | |> hover(Query.text("Click")) 17 | |> double_click() 18 | |> visible?(Query.text("Double")) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /integration_test/cases/browser/execute_script_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.ExecuteScriptTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | @script """ 5 | var element = document.createElement("div") 6 | element.id = "new-element" 7 | var text = document.createTextNode(arguments[0]) 8 | element.appendChild(text) 9 | document.body.appendChild(element) 10 | return arguments[1] 11 | """ 12 | test "executing scripts with arguments and returning", %{session: session} do 13 | assert session 14 | |> visit("page_1.html") 15 | |> execute_script(@script, ["now you see me", "return value"]) 16 | |> find(Query.css("#new-element")) 17 | |> Element.text() == "now you see me" 18 | end 19 | 20 | test "executing scripts with arguments and callback returns session", %{session: session} do 21 | result = 22 | session 23 | |> visit("page_1.html") 24 | |> execute_script(@script, ["now you see me", "return value"], fn value -> 25 | assert value == "return value" 26 | send(self(), {:callback, value}) 27 | end) 28 | 29 | assert result == session 30 | 31 | assert_received {:callback, "return value"} 32 | 33 | assert session 34 | |> find(Query.css("#new-element")) 35 | |> Element.text() == "now you see me" 36 | end 37 | 38 | test "executing asynchronous script and callback returns session", %{session: session} do 39 | result = 40 | session 41 | |> visit("page_1.html") 42 | |> execute_script_async("arguments[arguments.length - 1]('hello')", [], fn value -> 43 | assert value == "hello" 44 | send(self(), {:callback, value}) 45 | end) 46 | 47 | assert result == session 48 | assert_received {:callback, "hello"} 49 | end 50 | 51 | test "executing asynchronous script with arguments and callback returns session", %{ 52 | session: session 53 | } do 54 | result = 55 | session 56 | |> visit("page_1.html") 57 | |> execute_script_async( 58 | "arguments[arguments.length - 1](arguments[0]);", 59 | ["hello"], 60 | fn value -> 61 | assert value == "hello" 62 | send(self(), {:callback, value}) 63 | end 64 | ) 65 | 66 | assert result == session 67 | assert_received {:callback, "hello"} 68 | end 69 | 70 | test "returning element after asynchronous operation with timeout", %{session: session} do 71 | result = 72 | session 73 | |> visit("page_1.html") 74 | |> execute_script_async( 75 | "var callback = arguments[0]; setTimeout(function() { callback(document.getElementById('visible').innerHTML) }, 300);", 76 | [], 77 | fn value -> 78 | assert value == "Visible" 79 | send(self(), {:callback, value}) 80 | end 81 | ) 82 | 83 | assert result == session 84 | assert_received {:callback, "Visible"} 85 | end 86 | 87 | test "returning element after asynchronous operation", %{session: session} do 88 | result = 89 | session 90 | |> visit("page_1.html") 91 | |> execute_script_async( 92 | "var callback = arguments[0]; callback(document.getElementById('visible').innerHTML);", 93 | [], 94 | fn value -> 95 | assert value == "Visible" 96 | send(self(), {:callback, value}) 97 | end 98 | ) 99 | 100 | assert result == session 101 | assert_received {:callback, "Visible"} 102 | end 103 | 104 | test "returning element after asynchronous operation with arguments", %{session: session} do 105 | result = 106 | session 107 | |> visit("page_1.html") 108 | |> execute_script_async( 109 | "arguments[arguments.length - 1](document.getElementById(arguments[0]).innerHTML);", 110 | ["visible"], 111 | fn value -> 112 | assert value == "Visible" 113 | send(self(), {:callback, value}) 114 | end 115 | ) 116 | 117 | assert result == session 118 | assert_received {:callback, "Visible"} 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /integration_test/cases/browser/file_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.FileTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | import Wallaby.Query, only: [css: 1, file_field: 1] 5 | 6 | setup %{session: session} do 7 | page = 8 | session 9 | |> visit("forms.html") 10 | 11 | {:ok, %{page: page}} 12 | end 13 | 14 | describe "attaching a file to a form" do 15 | test "by name", %{page: page} do 16 | page 17 | |> attach_file(file_field("file_input"), path: "integration_test/support/fixtures/file.txt") 18 | 19 | find(page, css("#file_field"), fn element -> 20 | assert Wallaby.Element.value(element) == "C:\\fakepath\\file.txt" 21 | end) 22 | end 23 | 24 | test "by DOM ID", %{page: page} do 25 | page 26 | |> attach_file(file_field("file_field"), path: "integration_test/support/fixtures/file.txt") 27 | 28 | find(page, css("#file_field"), fn element -> 29 | assert Wallaby.Element.value(element) == "C:\\fakepath\\file.txt" 30 | end) 31 | end 32 | 33 | test "by label", %{page: page} do 34 | page 35 | |> attach_file(file_field("File"), path: "integration_test/support/fixtures/file.txt") 36 | 37 | find(page, css("#file_field"), fn element -> 38 | assert Wallaby.Element.value(element) == "C:\\fakepath\\file.txt" 39 | end) 40 | end 41 | end 42 | 43 | test "attaching a non-extant file does nothing", %{page: page} do 44 | page 45 | |> attach_file(file_field("File"), path: "integration_test/support/fixtures/fool.txt") 46 | 47 | find(page, css("#file_field"), fn element -> 48 | assert Wallaby.Element.value(element) == "" 49 | end) 50 | end 51 | 52 | test "checks for labels without for attributes", %{page: page} do 53 | assert_raise Wallaby.QueryError, ~r/label has no 'for'/, fn -> 54 | attach_file(page, file_field("File field with bad label"), 55 | path: "integration_test/support/fixtures/file.txt" 56 | ) 57 | end 58 | end 59 | 60 | test "escapes quotes", %{page: page} do 61 | assert attach_file(page, file_field("I'm a file field"), 62 | path: "integration_test/support/fixtures/file.txt" 63 | ) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /integration_test/cases/browser/fill_in_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.FillInTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = 6 | session 7 | |> visit("forms.html") 8 | 9 | {:ok, %{page: page}} 10 | end 11 | 12 | test "fill_in/2 accepts a query", %{page: page} do 13 | page 14 | |> fill_in(Query.text_field("name"), with: "Chris") 15 | 16 | assert page 17 | |> find(Query.text_field("name")) 18 | |> has_value?("Chris") 19 | end 20 | 21 | test "filling in input by id", %{page: page} do 22 | page 23 | |> fill_in(Query.css("#name_field"), with: "Chris") 24 | 25 | assert find(page, Query.css("#name_field")) |> has_value?("Chris") 26 | end 27 | 28 | test "fill_in accepts numbers", %{page: page} do 29 | page 30 | |> fill_in(Query.text_field("password"), with: 1234) 31 | 32 | assert find(page, Query.css("#password_field")) |> has_value?("1234") 33 | end 34 | 35 | test "filling in multiple inputs", %{page: page} do 36 | page 37 | |> fill_in(Query.text_field("name"), with: "Alex") 38 | |> fill_in(Query.text_field("email"), with: "alex@example.com") 39 | 40 | assert page 41 | |> find(Query.css("#name_field")) 42 | |> has_value?("Alex") 43 | 44 | assert page 45 | |> find(Query.css("#email_field")) 46 | |> has_value?("alex@example.com") 47 | end 48 | 49 | test "fill_in replaces all of the text", %{page: page} do 50 | page 51 | |> fill_in(Query.text_field("name"), with: "Chris") 52 | |> fill_in(Query.text_field("name"), with: "Alex") 53 | 54 | assert find(page, Query.css("#name_field")) |> has_value?("Alex") 55 | end 56 | 57 | test "waits until the input appears", %{page: page} do 58 | fill_in(page, Query.text_field("Hidden Text Field"), with: "Test Label Text") 59 | 60 | assert page 61 | |> find(Query.css("#hidden-text-field-id")) 62 | |> has_value?("Test Label Text") 63 | end 64 | 65 | test "checks for labels without for attributes", %{page: page} do 66 | assert_raise Wallaby.QueryError, ~r/label has no 'for'/, fn -> 67 | fill_in(page, Query.text_field("Input with bad label"), with: "Test") 68 | end 69 | end 70 | 71 | test "checks for mismatched ids on labels", %{page: page} do 72 | assert_raise Wallaby.QueryError, 73 | ~r/but the label's 'for' attribute\sdoesn't match the id/, 74 | fn -> 75 | fill_in(page, Query.text_field("Input with bad id"), with: "Test") 76 | end 77 | end 78 | 79 | test "checks for duplicate ids on labels", %{page: page} do 80 | assert_raise Wallaby.QueryError, 81 | ~r/but the label's 'for' attribute\smatches 3 elements/, 82 | fn -> 83 | fill_in(page, Query.text_field("Input with duplicate id"), with: "Test") 84 | end 85 | end 86 | 87 | test "provides guidance for labels with type mismatch", %{page: page} do 88 | assert_raise Wallaby.QueryError, 89 | ~r/but the label's 'for' attribute\smatches one element/, 90 | fn -> 91 | click(page, Query.radio_button("Name")) 92 | end 93 | end 94 | 95 | test "escapes quotes", %{page: page} do 96 | assert fill_in(page, Query.text_field("I'm a text field"), with: "Stuff") 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /integration_test/cases/browser/find_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.FindTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | import Wallaby.Query, only: [css: 1] 5 | 6 | setup %{session: session} do 7 | page = 8 | session 9 | |> visit("forms.html") 10 | 11 | {:ok, page: page} 12 | end 13 | 14 | describe "find/3" do 15 | setup %{session: session} do 16 | page = 17 | session 18 | |> visit("page_1.html") 19 | 20 | {:ok, page: page} 21 | end 22 | 23 | test "can find an element on a page", %{session: session} do 24 | element = 25 | session 26 | |> find(Query.css(".blue")) 27 | 28 | assert element 29 | end 30 | 31 | test "queries can be scoped by elements", %{session: session} do 32 | users = 33 | session 34 | |> visit("nesting.html") 35 | |> find(css(".dashboard")) 36 | |> find(css(".users")) 37 | |> all(css(".user")) 38 | 39 | assert Enum.count(users) == 3 40 | assert List.first(users) |> Element.text() == "Chris" 41 | end 42 | 43 | test "throws a not found error if the element could not be found", %{page: page} do 44 | assert_raise Wallaby.QueryError, ~r/Expected to find/, fn -> 45 | find(page, Query.css("#not-there")) 46 | end 47 | end 48 | 49 | test "throws a not found error if the xpath could not be found", %{page: page} do 50 | assert_raise Wallaby.QueryError, ~r/Expected (.*) xpath '\/\/test-element'/, fn -> 51 | find(page, Query.xpath("//test-element")) 52 | end 53 | end 54 | 55 | test "ambiguous queries raise an exception", %{page: page} do 56 | assert_raise Wallaby.QueryError, ~r/Expected (.*) 1(.*) but 5/, fn -> 57 | find(page, Query.css(".user")) 58 | end 59 | end 60 | 61 | test "throws errors if element should not be visible", %{page: page} do 62 | assert_raise Wallaby.QueryError, ~r/invisible/, fn -> 63 | find(page, Query.css("#visible", visible: false)) 64 | end 65 | end 66 | 67 | test "find/2 raises an error if the element is not visible", %{session: session} do 68 | session 69 | |> visit("page_1.html") 70 | 71 | assert_raise Wallaby.QueryError, fn -> 72 | find(session, css("#invisible")) 73 | end 74 | 75 | assert find(session, Query.css("#visible", count: :any)) 76 | |> length == 1 77 | end 78 | 79 | test "finds invisible elements", %{page: page} do 80 | assert find(page, Query.css("#invisible", visible: false)) 81 | end 82 | 83 | test "can be scoped with inner text", %{page: page} do 84 | user1 = find(page, Query.css(".user", text: "Chris K.")) 85 | user2 = find(page, Query.css(".user", text: "Grace H.")) 86 | assert user1 != user2 87 | end 88 | 89 | test "can be scoped by inner text when there are multiple elements with text", %{page: page} do 90 | element = find(page, Query.css(".inner-text", text: "Inner Text")) 91 | assert element 92 | end 93 | 94 | test "scoping with text escapes the text", %{page: page} do 95 | assert find(page, Query.css(".plus-one", text: "+ 1")) 96 | end 97 | 98 | test "scopes can be composed together", %{page: page} do 99 | assert find(page, Query.css(".user", text: "Same User", count: 2)) 100 | assert find(page, Query.css(".user", text: "Visible User", visible: true)) 101 | assert find(page, Query.css(".invisible-elements", visible: false, count: 3)) 102 | end 103 | 104 | test "returns the element as the argument to the callback", %{page: page} do 105 | page 106 | |> find(Query.css("h1"), &assert(has_text?(&1, "Page 1"))) 107 | end 108 | 109 | test "returns the parent", %{page: page} do 110 | assert page 111 | |> find(Query.css("h1"), fn _ -> nil end) == page 112 | end 113 | 114 | test "returns all the elements found with the query", %{page: page} do 115 | assert find(page, Query.css(".user", count: 5), fn elements -> 116 | assert Enum.count(elements) == 5 117 | end) 118 | end 119 | end 120 | 121 | test "waits for an element to be visible", %{session: session} do 122 | session 123 | |> visit("wait.html") 124 | 125 | assert find(session, css(".main")) 126 | end 127 | 128 | test "waits for count elements to be visible", %{session: session} do 129 | session 130 | |> visit("wait.html") 131 | 132 | assert find(session, Query.css(".orange", count: 5)) |> length == 5 133 | end 134 | 135 | test "finding one or more elements", %{session: session} do 136 | session 137 | |> visit("page_1.html") 138 | 139 | assert_raise Wallaby.QueryError, fn -> 140 | find(session, css(".not-there")) 141 | end 142 | 143 | assert find(session, Query.css("li", count: :any)) |> length == 4 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /integration_test/cases/browser/frames_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.FramesTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | test "switching between frames", %{session: session} do 5 | session 6 | |> visit("frames.html") 7 | |> assert_has(Query.css("h1", text: "Frames Page")) 8 | |> assert_has(Query.css("h1", text: "Page 1", count: 0)) 9 | |> assert_has(Query.css("h1", text: "Page 2", count: 0)) 10 | |> focus_frame(Query.css("#frame1")) 11 | |> assert_has(Query.css("h1", text: "Frames Page", count: 0)) 12 | |> assert_has(Query.css("h1", text: "Page 1")) 13 | |> assert_has(Query.css("h1", text: "Page 2", count: 0)) 14 | |> focus_parent_frame() 15 | |> focus_frame(Query.css("#frame2")) 16 | |> assert_has(Query.css("h1", text: "Frames Page", count: 0)) 17 | |> assert_has(Query.css("h1", text: "Page 1", count: 0)) 18 | |> assert_has(Query.css("h1", text: "Page 2")) 19 | |> focus_default_frame() 20 | |> assert_has(Query.css("h1", text: "Frames Page")) 21 | |> assert_has(Query.css("h1", text: "Page 1", count: 0)) 22 | |> assert_has(Query.css("h1", text: "Page 2", count: 0)) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /integration_test/cases/browser/has_css_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.HasCssTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "page_1.html") 6 | {:ok, %{page: page}} 7 | end 8 | 9 | describe "has_css/2" do 10 | test "checks if the query has the specified text", %{page: page} do 11 | assert page 12 | |> has_css?(".user") 13 | end 14 | end 15 | 16 | describe "has_no_css/2" do 17 | test "checks that there is no visible matching css", %{page: page} do 18 | assert page 19 | |> has_no_css?("#invisible") 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /integration_test/cases/browser/has_text_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.HasTextTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "page_1.html") 6 | {:ok, %{page: page}} 7 | end 8 | 9 | @h1 Query.css("h1") 10 | 11 | describe "has_text/3" do 12 | test "checks if the query has the specified text", %{page: page} do 13 | assert page 14 | |> has_text?(@h1, "Page 1") 15 | end 16 | end 17 | 18 | describe "has_text/2" do 19 | test "checks if the element has the specified text", %{page: page} do 20 | assert page 21 | |> find(@h1) 22 | |> has_text?("Page 1") 23 | end 24 | 25 | test "matches all text under the element", %{page: page} do 26 | assert page 27 | |> find(Query.css(".lots-of-text")) 28 | |> has_text?("Text 2") 29 | end 30 | 31 | test "works with sessions", %{page: page} do 32 | assert page 33 | |> has_text?("Page 1") 34 | end 35 | 36 | test "retries the query", %{page: page} do 37 | assert page 38 | |> visit("wait.html") 39 | |> has_text?("orange") == true 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /integration_test/cases/browser/has_value_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.HasValueTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "forms.html") 6 | 7 | {:ok, %{page: page}} 8 | end 9 | 10 | @name_field Query.text_field("Name") 11 | 12 | describe "has_value?/3" do 13 | test "checks to see if query has a specific value", %{page: page} do 14 | assert page 15 | |> fill_in(@name_field, with: "Chris") 16 | |> has_value?(@name_field, "Chris") 17 | end 18 | end 19 | 20 | describe "has_value?/2" do 21 | test "checks that the elements value matches the specified value", %{page: page} do 22 | assert page 23 | |> fill_in(@name_field, with: "Chris") 24 | |> find(@name_field) 25 | |> has_value?("Chris") 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /integration_test/cases/browser/hover_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.HoverTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | {:ok, page: visit(session, "move_mouse.html")} 6 | end 7 | 8 | describe "hover/2" do 9 | test "hovers over the specified element", %{page: page} do 10 | refute page 11 | |> visible?(Query.text("B")) 12 | 13 | assert page 14 | |> hover(Query.css(".group")) 15 | |> visible?(Query.text("B")) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /integration_test/cases/browser/invalid_selectors_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.InvalidSelectorsTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | import Wallaby.Query, only: [css: 1] 5 | 6 | describe "with an invalid selector state" do 7 | test "find returns an exception", %{session: session} do 8 | assert_raise Wallaby.QueryError, ~r/The css 'checkbox:foo' is not a valid query/, fn -> 9 | find(session, css("checkbox:foo")) 10 | end 11 | end 12 | 13 | test "assert_has raises an exception", %{session: session} do 14 | assert_raise Wallaby.QueryError, ~r/The css 'checkbox:foo' is not a valid query/, fn -> 15 | assert_has(session, css("checkbox:foo")) 16 | end 17 | end 18 | 19 | test "refute_has raises an exception", %{session: session} do 20 | assert_raise Wallaby.QueryError, ~r/The css 'checkbox:foo' is not a valid query/, fn -> 21 | refute_has(session, css("checkbox:foo")) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /integration_test/cases/browser/js_errors_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.JSErrorsTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | import ExUnit.CaptureIO 5 | import Wallaby.Query, only: [button: 1] 6 | 7 | test "it captures javascript errors", %{session: session} do 8 | assert_raise Wallaby.JSError, fn -> 9 | session 10 | |> visit("/errors.html") 11 | |> click(button("Throw an Error")) 12 | end 13 | end 14 | 15 | test "it captures javascript console logs", %{session: session} do 16 | fun = fn -> 17 | session 18 | |> visit("/logs.html") 19 | end 20 | 21 | assert capture_io(fun) == "Capture console logs\n" 22 | end 23 | 24 | test "it only captures logs once", %{session: session} do 25 | output = """ 26 | Capture console logs 27 | Button clicked 28 | """ 29 | 30 | fun = fn -> 31 | session 32 | |> visit("/logs.html") 33 | |> click(button("Print Log")) 34 | end 35 | 36 | assert capture_io(fun) == output 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /integration_test/cases/browser/local_storage_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.LocalStorageTest do 2 | use ExUnit.Case, async: false 3 | import Wallaby.Integration.SessionCase, only: [start_test_session: 0] 4 | 5 | use Wallaby.DSL 6 | 7 | @get_value_script "return localStorage.getItem('test')" 8 | @set_value_script "localStorage.setItem('test', 'foo')" 9 | 10 | @tag :skip_test_session 11 | test "local storage is not shared between sessions" do 12 | # Checkout all sessions 13 | {:ok, session} = start_test_session() 14 | {:ok, s2} = start_test_session() 15 | {:ok, s3} = start_test_session() 16 | 17 | session 18 | |> visit("index.html") 19 | |> execute_script(@set_value_script) 20 | 21 | session 22 | |> execute_script(@get_value_script, fn value -> send(self(), {:result, value}) end) 23 | 24 | assert_received {:result, "foo"} 25 | 26 | s2 27 | |> visit("index.html") 28 | |> execute_script(@get_value_script, fn value -> send(self(), {:callback, value}) end) 29 | 30 | assert_received {:callback, nil} 31 | 32 | s3 33 | |> visit("index.html") 34 | |> execute_script(@get_value_script, fn value -> send(self(), {:callback2, value}) end) 35 | 36 | assert_received {:callback2, nil} 37 | 38 | Wallaby.end_session(session) 39 | {:ok, new_session} = start_test_session() 40 | 41 | assert session.server == new_session.server 42 | 43 | new_session 44 | |> visit("index.html") 45 | |> execute_script(@get_value_script, fn value -> send(self(), {:callback3, value}) end) 46 | 47 | assert_received {:callback3, nil} 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /integration_test/cases/browser/move_mouse_by_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.MoveMouseByTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | import Wallaby.Browser 4 | 5 | setup %{session: session} do 6 | {:ok, page: visit(session, "move_mouse.html")} 7 | end 8 | 9 | describe "move_mouse_by/3" do 10 | test "moves mouse cursor by the given offset from the current position", %{page: page} do 11 | refute page 12 | |> visible?(Query.text("B")) 13 | 14 | assert page 15 | |> hover(Query.text("A")) 16 | |> move_mouse_by(40, 68) 17 | |> visible?(Query.text("B")) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /integration_test/cases/browser/navigation_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.NavigationTest do 2 | use Wallaby.Integration.SessionCase, async: false 3 | 4 | test "navigating by path only", %{session: session} do 5 | visit(session, "page_1.html") 6 | 7 | element = 8 | session 9 | |> find(Query.css(".blue")) 10 | 11 | assert element 12 | end 13 | 14 | test "visit/2 with an absolute path does not use the base url", %{session: session} do 15 | session 16 | |> visit("/page_1.html") 17 | 18 | assert has_css?(session, "#visible") 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /integration_test/cases/browser/page_source_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.PageSourceTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | test "page_source/1 retrieves the source of the current page", %{session: session} do 5 | source = 6 | session 7 | |> visit("/index.html") 8 | |> page_source 9 | |> clean_up_html 10 | 11 | actual_html = 12 | "integration_test/support/pages/index.html" 13 | |> Path.absname() 14 | |> File.read!() 15 | |> clean_up_html 16 | 17 | # Firefox inserts a so you can't do an exact comparison 18 | assert actual_html =~ source 19 | end 20 | 21 | def clean_up_html(string) do 22 | string 23 | |> String.replace(~r/\s+/, "") 24 | |> String.downcase() 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /integration_test/cases/browser/select_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.SelectTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | import Wallaby.Query, only: [option: 1, select: 1] 5 | 6 | setup %{session: session} do 7 | page = 8 | session 9 | |> visit("select_boxes.html") 10 | 11 | {:ok, page: page} 12 | end 13 | 14 | test "escapes quotes", %{page: page} do 15 | assert click(page, option("I'm an option")) 16 | end 17 | 18 | describe "selected?/2" do 19 | test "returns a boolean if the option is selected", %{page: page} do 20 | select = 21 | page 22 | |> find(select("My Select")) 23 | 24 | assert select 25 | |> selected?(option("Option 2")) == false 26 | 27 | assert select 28 | |> click(option("Option 2")) 29 | |> selected?(option("Option 2")) == true 30 | end 31 | end 32 | 33 | describe "selected?/1" do 34 | test "returns a boolean if the option is selected", %{page: page} do 35 | page 36 | |> find(select("My Select")) 37 | |> find(option("Option 2"), &refute(Element.selected?(&1))) 38 | |> click(option("Option 2")) 39 | |> find(option("Option 2"), &assert(Element.selected?(&1))) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /integration_test/cases/browser/send_keys_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.SendKeysTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "forms.html") 6 | {:ok, %{page: page}} 7 | end 8 | 9 | describe "send_keys/3" do 10 | test "accepts a query", %{page: page} do 11 | page 12 | |> send_keys(Query.text_field("Name"), ["Chris", :tab, "c@keathley.io"]) 13 | 14 | assert page 15 | |> find(Query.text_field("Name")) 16 | |> has_value?("Chris") 17 | 18 | assert page 19 | |> find(Query.text_field("email")) 20 | |> has_value?("c@keathley.io") 21 | end 22 | end 23 | 24 | describe "send_keys/2" do 25 | test "allows text to be sent", %{page: page} do 26 | page 27 | |> find(Query.text_field("email")) 28 | |> send_keys("Example text") 29 | |> has_value?("Example text") 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /integration_test/cases/browser/send_keys_to_active_element_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.SendKeysToActiveElementTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "forms.html") 6 | {:ok, %{page: page}} 7 | end 8 | 9 | describe "send_keys/2" do 10 | test "allows to send text to the active element", %{page: page} do 11 | page 12 | |> click(Query.text_field("Name")) 13 | |> send_keys(["Chris", :tab, "c@keathley.io"]) 14 | 15 | assert page 16 | |> find(Query.text_field("Name")) 17 | |> has_value?("Chris") 18 | 19 | assert page 20 | |> find(Query.text_field("email")) 21 | |> has_value?("c@keathley.io") 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /integration_test/cases/browser/set_value_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.SetValueTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "forms.html") 6 | {:ok, %{page: page}} 7 | end 8 | 9 | describe "set_value/3" do 10 | test "allows text field to be set", %{page: page} do 11 | assert page 12 | |> set_value(Query.text_field("email"), "Example text") 13 | |> find(Query.text_field("email")) 14 | |> has_value?("Example text") 15 | end 16 | 17 | test "allows checkbox to be checked", %{page: page} do 18 | assert page 19 | |> set_value(Query.checkbox("checkbox1"), :selected) 20 | |> find(Query.checkbox("checkbox1")) 21 | |> Element.selected?() 22 | end 23 | 24 | test "allows checkbox to be unchecked", %{page: page} do 25 | refute page 26 | |> set_value(Query.checkbox("checkbox1"), :selected) 27 | |> set_value(Query.checkbox("checkbox1"), :unselected) 28 | |> find(Query.checkbox("checkbox1")) 29 | |> Element.selected?() 30 | end 31 | 32 | test "allows radio buttons to be selected", %{page: page} do 33 | assert page 34 | |> set_value(Query.radio_button("option1"), :selected) 35 | |> find(Query.radio_button("option1")) 36 | |> Element.selected?() 37 | 38 | refute page 39 | |> set_value(Query.radio_button("option1"), :selected) 40 | |> set_value(Query.radio_button("option2"), :selected) 41 | |> find(Query.radio_button("option1")) 42 | |> Element.selected?() 43 | end 44 | 45 | test "allows options to be selected", %{page: page} do 46 | assert page 47 | |> set_value(Query.option("Select Option 1"), :selected) 48 | |> find(Query.option("Select Option 1")) 49 | |> Element.selected?() 50 | 51 | refute page 52 | |> set_value(Query.option("Select Option 1"), :selected) 53 | |> set_value(Query.option("Select Option 2"), :selected) 54 | |> find(Query.option("Select Option 1")) 55 | |> Element.selected?() 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /integration_test/cases/browser/stale_nodes_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.StaleElementsTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | alias Wallaby.StaleReferenceError 5 | 6 | describe "when a DOM element becomes stale" do 7 | test "the query is retried", %{session: session} do 8 | element = 9 | session 10 | |> visit("stale_nodes.html") 11 | |> find(Query.css(".stale-node", text: "Stale", count: 1)) 12 | 13 | assert element 14 | end 15 | 16 | test "when a DOM element disappears", %{session: session} do 17 | element = 18 | session 19 | |> visit("stale_nodes.html") 20 | |> find(Query.css("#removed-node")) 21 | 22 | session 23 | |> assert_has(Query.css("#removed-node", count: 0)) 24 | 25 | assert_raise StaleReferenceError, fn -> 26 | Element.value(element) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /integration_test/cases/browser/tap_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.TapTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "touch.html") 6 | 7 | {:ok, %{page: page}} 8 | end 9 | 10 | describe "tap/2" do 11 | test "taps the given element", %{page: page} do 12 | assert visible?(page, Query.text("Start", count: 0)) 13 | assert visible?(page, Query.text("End", count: 0)) 14 | 15 | tap(page, Query.text("Touch me!")) 16 | 17 | assert visible?(page, Query.text("Start")) 18 | assert visible?(page, Query.text("End")) 19 | assert page |> find(Query.css("#log-count-touches")) |> Element.text() == "0" 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /integration_test/cases/browser/text_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.TextTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | test "can get text of an element", %{session: session} do 5 | text = 6 | session 7 | |> visit("/") 8 | |> find(Query.css("#header")) 9 | |> Element.text() 10 | 11 | assert text == "Test Index" 12 | end 13 | 14 | test "can get text of an element and its descendants", %{session: session} do 15 | text = 16 | session 17 | |> visit("/") 18 | |> find(Query.css("#parent")) 19 | |> Element.text() 20 | 21 | assert text == "The Parent\nThe Child" 22 | end 23 | 24 | test "can get the text of a query", %{session: session} do 25 | text = 26 | session 27 | |> visit("/") 28 | |> text(Query.css("#parent")) 29 | 30 | assert text == "The Parent\nThe Child" 31 | end 32 | 33 | test "can get text of a session", %{session: session} do 34 | text = 35 | session 36 | |> visit("/") 37 | |> text() 38 | 39 | assert text == "Test Index\nPage 1\nPage 2\nPage 3\nThe Parent\nThe Child" 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /integration_test/cases/browser/title_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.TitleTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | test "finding the title", %{session: session} do 5 | text = 6 | session 7 | |> visit("/") 8 | |> page_title 9 | 10 | assert text == "Test Index" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /integration_test/cases/browser/touch_down_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.TouchDownTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "touch.html") 6 | 7 | {:ok, %{page: page}} 8 | end 9 | 10 | describe "touch_down/4" do 11 | test "touches and holds given element on its top-left corner", %{page: page} do 12 | assert visible?(page, Query.text("Start", count: 0)) 13 | 14 | assert page 15 | |> touch_down(Query.text("Touch me!")) 16 | |> visible?(Query.text("Start 0 16")) 17 | 18 | assert page |> find(Query.css("#log-count-touches")) |> Element.text() == "1" 19 | 20 | assert visible?(page, Query.text("End", count: 0)) 21 | end 22 | 23 | test "touches and holds given element on the point moved by given offset from its top-left corner", 24 | %{page: page} do 25 | assert visible?(page, Query.text("Start", count: 0)) 26 | 27 | assert page 28 | |> touch_down(Query.text("Touch me!"), 10, 20) 29 | |> visible?(Query.text("Start 10 36")) 30 | 31 | assert page |> find(Query.css("#log-count-touches")) |> Element.text() == "1" 32 | 33 | assert visible?(page, Query.text("End", count: 0)) 34 | end 35 | end 36 | 37 | describe "touch_down/3" do 38 | test "touches page at the point defined by the given coordinates", %{page: page} do 39 | assert visible?(page, Query.text("Start", count: 0)) 40 | 41 | assert page 42 | |> touch_down(25, 42) 43 | |> visible?(Query.text("Start 25 42")) 44 | 45 | assert page |> find(Query.css("#log-count-touches")) |> Element.text() == "1" 46 | 47 | assert visible?(page, Query.text("End", count: 0)) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /integration_test/cases/browser/touch_move_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.TouchMoveTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "touch.html") 6 | 7 | {:ok, %{page: page}} 8 | end 9 | 10 | describe "touch_move/3" do 11 | test "moves touch pointer to the given point", %{page: page} do 12 | assert visible?(page, Query.text("Start", count: 0)) 13 | assert visible?(page, Query.text("Move", count: 0)) 14 | assert visible?(page, Query.text("End", count: 0)) 15 | 16 | page 17 | |> touch_down(Query.text("Touch me!")) 18 | |> touch_move(200, 250) 19 | 20 | assert visible?(page, Query.text("Start 0 16")) 21 | assert visible?(page, Query.text("Move 200 250")) 22 | assert visible?(page, Query.text("End", count: 0)) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /integration_test/cases/browser/touch_scroll_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.TouchScrollTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | alias Wallaby.Integration.Helpers 4 | 5 | setup %{session: session} do 6 | page = visit(session, "touch.html") 7 | 8 | {:ok, %{page: page}} 9 | end 10 | 11 | describe "touch_scroll/4" do 12 | test "scrolls the page using touch events", %{page: page} do 13 | refute Helpers.displayed_in_viewport?(page, Query.text("Hello there")) 14 | 15 | touch_scroll(page, Query.text("Touch me!"), 1000, 1000) 16 | 17 | assert Helpers.displayed_in_viewport?(page, Query.text("Hello there")) 18 | refute Helpers.displayed_in_viewport?(page, Query.text("Touch me!")) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /integration_test/cases/browser/touch_up_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.TouchUpTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "touch.html") 6 | 7 | {:ok, %{page: page}} 8 | end 9 | 10 | describe "touch_up/1" do 11 | test "stops touching screen over the given element", %{page: page} do 12 | assert visible?(page, Query.text("Start", count: 0)) 13 | 14 | assert page 15 | |> touch_down(Query.text("Touch me!")) 16 | |> visible?(Query.text("Start")) 17 | 18 | assert page |> find(Query.css("#log-count-touches")) |> Element.text() == "1" 19 | 20 | assert visible?(page, Query.text("End", count: 0)) 21 | 22 | assert page 23 | |> touch_up() 24 | |> visible?(Query.text("End")) 25 | 26 | assert page |> find(Query.css("#log-count-touches")) |> Element.text() == "0" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /integration_test/cases/browser/visible_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.VisibleTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | describe "visible?/1" do 5 | setup :visit_page 6 | 7 | test "determines if the element is visible to the user", %{page: page} do 8 | page 9 | |> find(Query.css("#visible")) 10 | |> Element.visible?() 11 | |> assert 12 | 13 | page 14 | |> find(Query.css("#invisible", visible: false)) 15 | |> Element.visible?() 16 | |> refute 17 | end 18 | 19 | test "handles elements that are not on the page", %{page: page} do 20 | element = find(page, Query.css("#off-the-page", visible: false)) 21 | 22 | assert Element.visible?(element) == false 23 | end 24 | end 25 | 26 | describe "visible?/2" do 27 | setup :visit_page 28 | 29 | test "returns a boolean", %{page: page} do 30 | assert page 31 | |> visible?(Query.css("#visible")) == true 32 | 33 | assert page 34 | |> visible?(Query.css("#invisible")) == false 35 | end 36 | end 37 | 38 | def visit_page(%{session: session}) do 39 | page = 40 | session 41 | |> visit("page_1.html") 42 | 43 | {:ok, page: page} 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /integration_test/cases/browser/window_handles_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.WindowHandlesTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | test "switching between tabs and windows", %{session: session} do 5 | session 6 | |> visit("windows.html") 7 | 8 | initial_handle = window_handle(session) 9 | assert [initial_handle] == window_handles(session) 10 | 11 | session 12 | |> click(Query.link("New tab")) 13 | 14 | :timer.sleep(500) 15 | 16 | handles = window_handles(session) 17 | assert length(handles) == 2 18 | 19 | new_tab_handle = Enum.find(handles, fn handle -> handle != initial_handle end) 20 | focus_window(session, new_tab_handle) 21 | 22 | assert new_tab_handle == window_handle(session) 23 | assert_has(session, Query.css("h1", text: "Page 1")) 24 | 25 | session 26 | |> focus_window(initial_handle) 27 | |> click(Query.link("New window")) 28 | 29 | :timer.sleep(500) 30 | 31 | handles2 = window_handles(session) 32 | assert length(handles2) == 3 33 | 34 | new_window_handle = 35 | Enum.find(handles2, fn handle -> handle not in [initial_handle, new_tab_handle] end) 36 | 37 | focus_window(session, new_window_handle) 38 | 39 | assert new_window_handle == window_handle(session) 40 | assert_has(session, Query.css("h1", text: "Page 2")) 41 | end 42 | 43 | test "closing tabs and windows", %{session: session} do 44 | session 45 | |> visit("windows.html") 46 | 47 | initial_handle = window_handle(session) 48 | 49 | session 50 | |> click(Query.link("New tab")) 51 | 52 | :timer.sleep(500) 53 | 54 | new_tab_handle = Enum.find(window_handles(session), fn handle -> handle != initial_handle end) 55 | 56 | session 57 | |> focus_window(new_tab_handle) 58 | |> close_window() 59 | |> focus_window(initial_handle) 60 | 61 | assert [initial_handle] == window_handles(session) 62 | 63 | session 64 | |> click(Query.link("New window")) 65 | 66 | :timer.sleep(500) 67 | 68 | new_window_handle = 69 | Enum.find(window_handles(session), fn handle -> handle != initial_handle end) 70 | 71 | session 72 | |> focus_window(new_window_handle) 73 | |> close_window() 74 | |> focus_window(initial_handle) 75 | 76 | assert [initial_handle] == window_handles(session) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /integration_test/cases/browser/window_position_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.WindowPositionTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | # this test dows not return the right values on mac 5 | # reason is unclear, I think it's a bug in chromedriver on mac 6 | if :os.type() != {:unix, :darwin} do 7 | test "getting the window position", %{session: session} do 8 | window_position = 9 | session 10 | |> visit("/") 11 | |> move_window(100, 200) 12 | |> window_position() 13 | 14 | assert %{"x" => 100, "y" => 200} = window_position 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /integration_test/cases/browser/window_size_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.WindowSizeTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | test "getting the window size", %{session: session} do 5 | window_size = 6 | session 7 | |> visit("/") 8 | |> resize_window(600, 400) 9 | |> window_size 10 | 11 | assert %{"height" => 400, "width" => 600} = window_size 12 | end 13 | 14 | describe "default window size" do 15 | setup do 16 | {:ok, session} = start_test_session(window_size: [width: 600, height: 400]) 17 | 18 | {:ok, %{session: session}} 19 | end 20 | 21 | @tag :skip_test_session 22 | test "sets window size from config option", %{session: session} do 23 | window_size = 24 | session 25 | |> visit("/") 26 | |> window_size 27 | 28 | assert %{"height" => 400, "width" => 600} = window_size 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /integration_test/cases/browser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.BrowserTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | describe "has?/2" do 5 | test "allows css queries", %{session: session} do 6 | session 7 | |> visit("/page_1.html") 8 | |> has?(Query.css(".blue")) 9 | |> assert 10 | end 11 | 12 | test "allows text queries", %{session: session} do 13 | session 14 | |> visit("/page_1.html") 15 | |> has?(Query.text("Page 1")) 16 | |> assert 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /integration_test/cases/element/hover_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Element.HoverTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | {:ok, page: visit(session, "move_mouse.html")} 6 | end 7 | 8 | describe "hover/2" do 9 | test "hovers over the specified element", %{page: page} do 10 | page 11 | |> find(Query.text("B", visible: false), fn el -> 12 | refute Element.visible?(el) 13 | end) 14 | |> find(Query.css(".group"), fn el -> 15 | Element.hover(el) 16 | end) 17 | |> find(Query.text("B"), fn el -> 18 | assert Element.visible?(el) 19 | end) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /integration_test/cases/element/location_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Element.LocationTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "touch.html") 6 | 7 | {:ok, %{page: page}} 8 | end 9 | 10 | describe "location/1" do 11 | test "returns coordinates of the top-left corner of the given element", %{page: page} do 12 | element = find(page, Query.text("Touch me!")) 13 | 14 | assert Element.location(element) == {0, 16} 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /integration_test/cases/element/send_keys_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Element.SendKeysTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | {:ok, page: visit(session, "forms.html")} 6 | end 7 | 8 | @name_field Query.text_field("Name") 9 | @email_field Query.text_field("email_field") 10 | 11 | describe "send_keys/2" do 12 | test "sends keys to the specified element", %{page: page} do 13 | page 14 | |> click(@email_field) 15 | |> find(@name_field, fn element -> 16 | assert element 17 | |> Element.send_keys("Chris") 18 | |> Element.value() == "Chris" 19 | end) 20 | |> find(@email_field, fn email -> 21 | assert email 22 | |> Element.value() == "" 23 | end) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /integration_test/cases/element/size_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Element.SizeTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "touch.html") 6 | 7 | {:ok, %{page: page}} 8 | end 9 | 10 | describe "size/1" do 11 | test "returns size of the given element", %{page: page} do 12 | element = find(page, Query.text("Touch me!")) 13 | 14 | assert Element.size(element) == {200, 100} 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /integration_test/cases/element/tap_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Element.TapTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "touch.html") 6 | 7 | {:ok, %{page: page}} 8 | end 9 | 10 | describe "tap/1" do 11 | test "taps the given element", %{page: page} do 12 | element = find(page, Query.text("Touch me!")) 13 | 14 | assert visible?(page, Query.text("Start", count: 0)) 15 | assert visible?(page, Query.text("End", count: 0)) 16 | 17 | Element.tap(element) 18 | 19 | assert visible?(page, Query.text("Start")) 20 | assert visible?(page, Query.text("End")) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /integration_test/cases/element/touch_down_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Element.TouchDownTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | setup %{session: session} do 5 | page = visit(session, "touch.html") 6 | 7 | {:ok, %{page: page}} 8 | end 9 | 10 | describe "touch_down/3" do 11 | test "touches and holds given element on its top-left corner", %{page: page} do 12 | element = find(page, Query.text("Touch me!")) 13 | 14 | assert visible?(page, Query.text("Start", count: 0)) 15 | 16 | Element.touch_down(element) 17 | 18 | assert visible?(page, Query.text("Start 0 16")) 19 | assert visible?(page, Query.text("End", count: 0)) 20 | assert page |> find(Query.css("#log-count-touches")) |> Element.text() == "1" 21 | end 22 | 23 | test "touches and holds given element on the point moved by given offset from its top-left corner", 24 | %{page: page} do 25 | element = find(page, Query.text("Touch me!")) 26 | 27 | assert visible?(page, Query.text("Start", count: 0)) 28 | 29 | Element.touch_down(element, 10, 20) 30 | 31 | assert visible?(page, Query.text("Start 10 36")) 32 | assert visible?(page, Query.text("End", count: 0)) 33 | assert page |> find(Query.css("#log-count-touches")) |> Element.text() == "1" 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /integration_test/cases/element/touch_scroll_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Element.TouchScrollTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | alias Wallaby.Integration.Helpers 4 | 5 | setup %{session: session} do 6 | page = visit(session, "touch.html") 7 | 8 | {:ok, %{page: page}} 9 | end 10 | 11 | describe "touch_scroll/3" do 12 | test "scrolls the page using touch events", %{page: page} do 13 | refute Helpers.displayed_in_viewport?(page, Query.text("Hello there")) 14 | 15 | element = find(page, Query.text("Touch me!")) 16 | Element.touch_scroll(element, 1000, 1000) 17 | 18 | assert Helpers.displayed_in_viewport?(page, Query.text("Hello there")) 19 | refute Helpers.displayed_in_viewport?(page, Query.text("Touch me!")) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /integration_test/cases/feature/automatic_screenshot_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Feature.AutomaticScreenshotTest do 2 | use ExUnit.Case 3 | 4 | alias ExUnit.CaptureIO 5 | 6 | describe "import Feature" do 7 | test "feature takes a screenshot on failure for each open wallaby session" do 8 | defmodule ImportFeature.FailureWithMultipleSessionsTest do 9 | use ExUnit.Case 10 | import Wallaby.Feature 11 | 12 | setup do 13 | Wallaby.SettingsTestHelpers.ensure_setting_is_reset(:wallaby, :screenshot_on_failure) 14 | Application.put_env(:wallaby, :screenshot_on_failure, true) 15 | 16 | :ok 17 | end 18 | 19 | feature "fails" do 20 | {:ok, _} = Wallaby.start_session() 21 | {:ok, _} = Wallaby.start_session() 22 | 23 | assert false 24 | end 25 | end 26 | 27 | configure_and_reload_on_exit(colors: [enabled: false]) 28 | 29 | output = 30 | CaptureIO.capture_io(fn -> 31 | assert ExUnit.run() == %{failures: 1, skipped: 0, total: 1, excluded: 0} 32 | end) 33 | 34 | assert output =~ "\n1 feature, 1 failure\n" 35 | assert screenshot_taken_count(output) == 2 36 | end 37 | end 38 | 39 | describe "use Feature" do 40 | test "feature takes a screenshot on failure for each open wallaby session" do 41 | defmodule UseFeature.FailureWithMultipleSessionsTest do 42 | use ExUnit.Case 43 | use Wallaby.Feature 44 | 45 | @sessions 2 46 | feature "fails", %{sessions: _sessions} do 47 | Wallaby.SettingsTestHelpers.ensure_setting_is_reset(:wallaby, :screenshot_on_failure) 48 | Application.put_env(:wallaby, :screenshot_on_failure, true) 49 | 50 | assert false 51 | end 52 | end 53 | 54 | configure_and_reload_on_exit(colors: [enabled: false]) 55 | 56 | output = 57 | CaptureIO.capture_io(fn -> 58 | assert ExUnit.run() == %{failures: 1, skipped: 0, total: 1, excluded: 0} 59 | end) 60 | 61 | assert output =~ "\n1 feature, 1 failure\n" 62 | assert screenshot_taken_count(output) == 2 63 | end 64 | end 65 | 66 | defp configure_and_reload_on_exit(opts) do 67 | old_opts = ExUnit.configuration() 68 | ExUnit.configure(opts) 69 | 70 | on_exit(fn -> ExUnit.configure(old_opts) end) 71 | end 72 | 73 | defp screenshot_taken_count(output) do 74 | ~r{- file:///} 75 | |> Regex.scan(output) 76 | |> length() 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /integration_test/cases/feature/import_feature_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.ImportFeatureTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | import Wallaby.Feature 4 | 5 | feature "works", %{session: session} do 6 | session 7 | |> visit("/page_1.html") 8 | |> find(Query.css("body > h1"), fn el -> 9 | assert Element.text(el) == "Page 1" 10 | end) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /integration_test/cases/feature/use_feature_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Browser.UseFeatureTest do 2 | use ExUnit.Case, async: true 3 | use Wallaby.Feature 4 | 5 | @sessions 2 6 | feature "multi session", %{sessions: [session_1, session_2]} do 7 | session_1 8 | |> visit("/page_1.html") 9 | |> find(Query.css("body > h1"), fn el -> 10 | assert Element.text(el) == "Page 1" 11 | end) 12 | 13 | session_2 14 | |> visit("/page_2.html") 15 | |> find(Query.css("body > h1"), fn el -> 16 | assert Element.text(el) == "Page 2" 17 | end) 18 | end 19 | 20 | feature "single session", %{session: only_session} do 21 | only_session 22 | |> visit("/page_1.html") 23 | |> find(Query.css("body > h1"), fn el -> 24 | assert Element.text(el) == "Page 1" 25 | end) 26 | end 27 | 28 | @expected_capabilities Map.put( 29 | Wallaby.driver().default_capabilities(), 30 | :test, 31 | "I'm a capability" 32 | ) 33 | @sessions [[capabilities: @expected_capabilities]] 34 | feature "reads capabilities from session attribute", %{session: %{capabilities: capabilities}} do 35 | assert capabilities.test == @expected_capabilities.test 36 | end 37 | 38 | test "does not set up a session for non-feature tests", context do 39 | refute is_map_key(context, :session) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /integration_test/cases/inspect_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.InspectTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | import ExUnit.CaptureIO 4 | 5 | describe "inspect/2" do 6 | test "prints the outerHTML of the element", %{session: session} do 7 | expected = 8 | """ 9 | #{IO.ANSI.cyan()}outerHTML: 10 | 11 | #{IO.ANSI.reset()}#{IO.ANSI.yellow()} 12 |

Test Index

13 | 18 | 19 |
20 | The Parent 21 |
22 | The Child 23 |
24 |
25 | #{IO.ANSI.reset()} 26 | """ 27 | |> String.replace(~r/\s/, "") 28 | 29 | actual = 30 | capture_io(fn -> 31 | session 32 | |> visit("/index.html") 33 | |> find(Query.css("body")) 34 | |> IO.inspect() 35 | end) 36 | |> String.replace(~r/\s/, "") 37 | 38 | IO.puts(actual) 39 | 40 | assert actual =~ expected 41 | end 42 | 43 | test "doesn't fail when request to fetch outerHTML fails", %{session: session} do 44 | actual = 45 | capture_io(fn -> 46 | element = 47 | session 48 | |> visit("/index.html") 49 | |> find(Query.css("body")) 50 | 51 | Wallaby.end_session(session) 52 | 53 | element 54 | |> IO.inspect() 55 | end) 56 | |> String.replace(~r/\s/, "") 57 | 58 | refute actual =~ "outerHTML:" 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /integration_test/cases/wallaby_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.WallabyTest do 2 | use Wallaby.Integration.SessionCase, async: true 3 | 4 | describe "end_session/2" do 5 | test "calling end_session on an active session", %{session: session} do 6 | assert :ok = Wallaby.end_session(session) 7 | end 8 | 9 | test "calling end_session on an already closed session", %{session: session} do 10 | Wallaby.end_session(session) 11 | 12 | assert :ok = Wallaby.end_session(session) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /integration_test/chrome/all_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../tests.exs", __DIR__) 2 | 3 | # Additional test cases supported by chromedriver 4 | Code.require_file("../cases/browser/file_test.exs", __DIR__) 5 | Code.require_file("../cases/browser/js_errors_test.exs", __DIR__) 6 | Code.require_file("../cases/browser/click_mouse_button_test.exs", __DIR__) 7 | Code.require_file("../cases/browser/double_click_test.exs", __DIR__) 8 | Code.require_file("../cases/browser/button_down_test.exs", __DIR__) 9 | Code.require_file("../cases/browser/button_up_test.exs", __DIR__) 10 | Code.require_file("../cases/browser/frames_test.exs", __DIR__) 11 | Code.require_file("../cases/browser/hover_test.exs", __DIR__) 12 | Code.require_file("../cases/element/hover_test.exs", __DIR__) 13 | Code.require_file("../cases/browser/move_mouse_by_test.exs", __DIR__) 14 | Code.require_file("../cases/browser/send_keys_to_active_element_test.exs", __DIR__) 15 | Code.require_file("../cases/browser/window_handles_test.exs", __DIR__) 16 | Code.require_file("../cases/browser/window_position_test.exs", __DIR__) 17 | Code.require_file("../cases/browser/touch_down_test.exs", __DIR__) 18 | Code.require_file("../cases/element/touch_down_test.exs", __DIR__) 19 | Code.require_file("../cases/browser/touch_up_test.exs", __DIR__) 20 | Code.require_file("../cases/browser/tap_test.exs", __DIR__) 21 | Code.require_file("../cases/element/tap_test.exs", __DIR__) 22 | Code.require_file("../cases/browser/touch_scroll_test.exs", __DIR__) 23 | Code.require_file("../cases/element/touch_scroll_test.exs", __DIR__) 24 | Code.require_file("../cases/browser/touch_move_test.exs", __DIR__) 25 | Code.require_file("../cases/element/size_test.exs", __DIR__) 26 | Code.require_file("../cases/element/location_test.exs", __DIR__) 27 | Code.require_file("../chrome/capabilities_test.exs", __DIR__) 28 | Code.require_file("../cases/feature/use_feature_test.exs", __DIR__) 29 | Code.require_file("../cases/feature/import_feature_test.exs", __DIR__) 30 | Code.require_file("../cases/feature/automatic_screenshot_test.exs", __DIR__) 31 | -------------------------------------------------------------------------------- /integration_test/chrome/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.configure(exclude: [pending: true]) 2 | ExUnit.start() 3 | 4 | # Load support files 5 | Code.require_file("../support/test_server.ex", __DIR__) 6 | Code.require_file("../support/pages/index_page.ex", __DIR__) 7 | Code.require_file("../support/pages/page_1.ex", __DIR__) 8 | Code.require_file("../support/session_case.ex", __DIR__) 9 | Code.require_file("../support/helpers.ex", __DIR__) 10 | 11 | {:ok, server} = Wallaby.Integration.TestServer.start() 12 | Application.put_env(:wallaby, :base_url, server.base_url) 13 | -------------------------------------------------------------------------------- /integration_test/selenium/all_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("../tests.exs", __DIR__) 2 | 3 | # Additional test cases supported by selenium 4 | Code.require_file("../cases/browser/double_click_test.exs", __DIR__) 5 | Code.require_file("../cases/browser/frames_test.exs", __DIR__) 6 | Code.require_file("../cases/browser/hover_test.exs", __DIR__) 7 | Code.require_file("../cases/element/hover_test.exs", __DIR__) 8 | Code.require_file("../cases/browser/move_mouse_by_test.exs", __DIR__) 9 | Code.require_file("../cases/browser/window_handles_test.exs", __DIR__) 10 | Code.require_file("../cases/browser/window_position_test.exs", __DIR__) 11 | Code.require_file("../cases/element/size_test.exs", __DIR__) 12 | Code.require_file("../cases/element/location_test.exs", __DIR__) 13 | Code.require_file("../cases/feature/use_feature_test.exs", __DIR__) 14 | Code.require_file("../cases/feature/import_feature_test.exs", __DIR__) 15 | Code.require_file("../cases/feature/automatic_screenshot_test.exs", __DIR__) 16 | -------------------------------------------------------------------------------- /integration_test/selenium/selenium_capabilities_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.SeleniumCapabilitiesTest do 2 | use ExUnit.Case, async: false 3 | use Wallaby.DSL 4 | 5 | import Wallaby.SettingsTestHelpers 6 | 7 | alias Wallaby.Integration.SessionCase 8 | alias Wallaby.WebdriverClient 9 | 10 | setup do 11 | ensure_setting_is_reset(:wallaby, :selenium) 12 | end 13 | 14 | describe "capabilities" do 15 | test "reads default capabilities" do 16 | expected_capabilities = %{ 17 | browserName: "firefox", 18 | "moz:firefoxOptions": %{ 19 | args: ["-headless"], 20 | prefs: %{ 21 | "general.useragent.override" => 22 | "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36" 23 | } 24 | } 25 | } 26 | 27 | create_session_fn = fn url, capabilities -> 28 | assert capabilities == expected_capabilities 29 | 30 | WebdriverClient.create_session(url, capabilities) 31 | end 32 | 33 | {:ok, session} = SessionCase.start_test_session(create_session_fn: create_session_fn) 34 | 35 | session 36 | |> visit("page_1.html") 37 | |> assert_has(Query.text("Page 1")) 38 | 39 | assert :ok = Wallaby.end_session(session) 40 | end 41 | 42 | test "reads capabilities from application config" do 43 | expected_capabilities = %{ 44 | browserName: "firefox", 45 | "moz:firefoxOptions": %{ 46 | args: ["-headless"] 47 | } 48 | } 49 | 50 | Application.put_env(:wallaby, :selenium, capabilities: expected_capabilities) 51 | 52 | create_session_fn = fn url, capabilities -> 53 | assert capabilities == expected_capabilities 54 | 55 | WebdriverClient.create_session(url, capabilities) 56 | end 57 | 58 | {:ok, session} = SessionCase.start_test_session(create_session_fn: create_session_fn) 59 | 60 | session 61 | |> visit("page_1.html") 62 | |> assert_has(Query.text("Page 1")) 63 | 64 | assert :ok = Wallaby.end_session(session) 65 | end 66 | 67 | test "adds the beam metadata when it is present" do 68 | user_agent = "Mozilla/5.0" 69 | 70 | defined_capabilities = %{ 71 | browserName: "firefox", 72 | "moz:firefoxOptions": %{ 73 | args: ["-headless"], 74 | prefs: %{ 75 | "general.useragent.override" => user_agent 76 | } 77 | } 78 | } 79 | 80 | metadata = 81 | if Version.compare(System.version(), "1.16.0") in [:eq, :gt] do 82 | "g2gCdwJ2MXQAAAABbQAAAARzb21lbQAAAAhtZXRhZGF0YQ==" 83 | else 84 | "g2gCZAACdjF0AAAAAW0AAAAEc29tZW0AAAAIbWV0YWRhdGE=" 85 | end 86 | 87 | expected_capabilities = %{ 88 | browserName: "firefox", 89 | "moz:firefoxOptions": %{ 90 | args: ["-headless"], 91 | prefs: %{ 92 | "general.useragent.override" => "#{user_agent}/BeamMetadata (#{metadata})" 93 | } 94 | } 95 | } 96 | 97 | Application.put_env(:wallaby, :selenium, capabilities: defined_capabilities) 98 | 99 | create_session_fn = fn url, capabilities -> 100 | assert capabilities == expected_capabilities 101 | 102 | WebdriverClient.create_session(url, capabilities) 103 | end 104 | 105 | {:ok, session} = 106 | SessionCase.start_test_session( 107 | create_session_fn: create_session_fn, 108 | metadata: %{"some" => "metadata"} 109 | ) 110 | 111 | session 112 | |> visit("page_1.html") 113 | |> assert_has(Query.text("Page 1")) 114 | 115 | assert :ok = Wallaby.end_session(session) 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /integration_test/selenium/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.configure(max_cases: 2, exclude: [pending: true]) 2 | 3 | # Load support files 4 | Code.require_file("../support/test_server.ex", __DIR__) 5 | Code.require_file("../support/pages/index_page.ex", __DIR__) 6 | Code.require_file("../support/pages/page_1.ex", __DIR__) 7 | Code.require_file("../support/session_case.ex", __DIR__) 8 | Code.require_file("../support/helpers.ex", __DIR__) 9 | 10 | {:ok, server} = Wallaby.Integration.TestServer.start() 11 | Application.put_env(:wallaby, :base_url, server.base_url) 12 | 13 | ExUnit.start() 14 | -------------------------------------------------------------------------------- /integration_test/support/fixtures/file.txt: -------------------------------------------------------------------------------- 1 | @stevegraham was here. Wallaby FTW. 2 | -------------------------------------------------------------------------------- /integration_test/support/helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Helpers do 2 | @moduledoc false 3 | 4 | def displayed_in_viewport?(session, %Wallaby.Query{} = query), 5 | do: displayed_in_viewport?(session, Wallaby.Browser.find(session, query)) 6 | 7 | # source: https://github.com/webdriverio/webdriverio/blob/9b83046725ea9ba68f7d2e5a4207b50a798f944f/packages/webdriverio/src/scripts/isDisplayedInViewport.js 8 | def displayed_in_viewport?(session, %Wallaby.Element{} = element) do 9 | {:ok, result} = 10 | element.driver.execute_script( 11 | session, 12 | """ 13 | let elem = arguments[0] 14 | const dde = document.documentElement 15 | let isWithinViewport = true 16 | 17 | while (elem.parentNode && elem.parentNode.getBoundingClientRect) { 18 | const elemDimension = elem.getBoundingClientRect() 19 | const elemComputedStyle = window.getComputedStyle(elem) 20 | const viewportDimension = { 21 | width: dde.clientWidth, 22 | height: dde.clientHeight 23 | } 24 | 25 | isWithinViewport = isWithinViewport && 26 | (elemComputedStyle.display !== 'none' && 27 | elemComputedStyle.visibility === 'visible' && 28 | parseFloat(elemComputedStyle.opacity, 10) > 0 && 29 | elemDimension.bottom > 0 && 30 | elemDimension.right > 0 && 31 | elemDimension.top < viewportDimension.height && 32 | elemDimension.left < viewportDimension.width) 33 | 34 | elem = elem.parentNode 35 | } 36 | 37 | return isWithinViewport 38 | """, 39 | [%{"element-6066-11e4-a52e-4f735466cecf" => element.id, "ELEMENT" => element.id}] 40 | ) 41 | 42 | result 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /integration_test/support/pages/click.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

8 | 9 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /integration_test/support/pages/dialogs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dialog Test 5 | 6 | 7 |

Dialog Test

8 | Alert 9 | Confirm 10 | Prompt 11 |

12 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /integration_test/support/pages/errors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Wallaby - JS Errors test page 6 | 7 | 8 | 11 | 12 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /integration_test/support/pages/frames.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Frames 5 | 6 | 7 | 8 |

Frames Page

9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /integration_test/support/pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test Index 5 | 6 | 7 |

Test Index

8 | 13 | 14 |
15 | The Parent 16 |
17 | The Child 18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /integration_test/support/pages/index_page.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Pages.IndexPage do 2 | use Wallaby.DSL 3 | 4 | def visit(session) do 5 | session 6 | |> visit("") 7 | end 8 | 9 | def click_page_1_link(session) do 10 | session 11 | |> click(Query.link("Page 1")) 12 | end 13 | 14 | def ensure_page_loaded(session) do 15 | session 16 | |> Browser.find(Query.css(".index-page")) 17 | 18 | session 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /integration_test/support/pages/logs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Wallaby - JS log test page 6 | 7 | 8 | 11 | 12 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /integration_test/support/pages/mouse_down_and_up.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 | 10 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /integration_test/support/pages/move_mouse.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test Index 6 | 7 | 8 | 9 | 30 |
31 | A 32 |
33 |
34 |
B
35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /integration_test/support/pages/nesting.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Nesting html 5 | 6 | 7 |
8 |
    9 |
  • 10 | Chris 11 |
  • 12 |
  • 13 | Alex 14 |
  • 15 |
  • 16 | Tommy 17 |
  • 18 |
19 |
    20 |
  • 1
  • 21 |
  • 2
  • 22 |
  • 3
  • 23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /integration_test/support/pages/page_1.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.Pages.Page1 do 2 | use Wallaby.DSL 3 | 4 | def visit(session) do 5 | session 6 | |> visit("page_1.html") 7 | end 8 | 9 | def ensure_page_loaded(session) do 10 | session 11 | |> Browser.find(Query.css(".page-1-page")) 12 | 13 | session 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /integration_test/support/pages/page_1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page 1 5 | 6 | 7 |

Page 1

8 |
9 |

Test Div

10 | 11 | Back to Index 12 | 13 |
14 | Chris K. 15 |
16 |
17 | Grace H. 18 |
19 |
20 | Same User 21 |
22 |
23 | Same User 24 |
25 |
26 | Visible User 27 |
28 | 29 | 30 | 31 | 32 | 33 |
    34 |
  • A
  • 35 |
  • B
  • 36 |
  • C
  • 37 |
  • D
  • 38 |
39 |

Visible

40 | 41 |

Off the page

42 |
43 | 44 |
45 |

46 | Inner Text 47 |

48 |

49 | More inner text 50 |

51 |
52 | 53 |
54 | Text 55 | Text 2 56 |
57 | 58 | Single quotes aren't hard 59 | 60 |

+ 1

61 | 62 |
63 | Visible 64 | Visible 65 | 66 |
67 | 68 | Span text 69 | 70 | A data attribute 71 | 72 | 73 | -------------------------------------------------------------------------------- /integration_test/support/pages/page_2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page 2 5 | 6 | 7 |

Page 2

8 |
9 | 10 | 11 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /integration_test/support/pages/page_3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page 3 5 | 6 | 7 |

Page 3

8 | Thanks! 9 | 10 | 11 | -------------------------------------------------------------------------------- /integration_test/support/pages/select_boxes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Select Boxes 5 | 6 | 7 |

Select Boxes

8 |
9 | 10 | 15 | 16 | 17 | 22 | 23 | 24 | 27 | 28 | 29 | 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /integration_test/support/pages/stale_nodes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Wallaby Test Page - Stale Nodes 6 | 7 | 8 |
9 | Stale 10 |
11 |
12 | Stale 13 |
14 | 15 | 16 | 22 | 23 | -------------------------------------------------------------------------------- /integration_test/support/pages/touch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 35 | 36 |

Touch events

37 | 38 |

39 |

40 |

41 |

42 |

0

43 |

Hello there!

44 | 45 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /integration_test/support/pages/wait.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Wait 5 | 6 | 7 |
8 |
9 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /integration_test/support/pages/windows.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Windows 5 | 6 | 7 | 8 | New tab or window link 9 | New 10 | window link 11 | 12 | 13 | -------------------------------------------------------------------------------- /integration_test/support/session_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.SessionCase do 2 | @moduledoc false 3 | use ExUnit.CaseTemplate 4 | 5 | using do 6 | quote do 7 | use Wallaby.DSL 8 | import Wallaby.Integration.SessionCase 9 | end 10 | end 11 | 12 | setup :inject_test_session 13 | 14 | @doc """ 15 | Starts a test session with the default opts for the given driver 16 | """ 17 | def start_test_session(opts \\ []) do 18 | retry(2, fn -> Wallaby.start_session(opts) end) 19 | end 20 | 21 | @doc """ 22 | Injects a test session into the test context 23 | """ 24 | def inject_test_session(%{skip_test_session: true}), do: :ok 25 | 26 | def inject_test_session(_context) do 27 | {:ok, session} = start_test_session() 28 | 29 | {:ok, %{session: session}} 30 | end 31 | 32 | defp retry(0, f), do: f.() 33 | 34 | defp retry(times, f) do 35 | case f.() do 36 | {:ok, session} -> 37 | {:ok, session} 38 | 39 | _ -> 40 | Process.sleep(250) 41 | retry(times - 1, f) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /integration_test/support/test_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Integration.TestServer do 2 | @moduledoc false 3 | 4 | @config [ 5 | port: 0, 6 | server_root: String.to_charlist(Path.absname("./", __DIR__)), 7 | document_root: String.to_charlist(Path.absname("./pages", __DIR__)), 8 | server_name: ~c"wallaby_test", 9 | directory_index: [~c"index.html"] 10 | ] 11 | 12 | defstruct [:base_url, :pid] 13 | 14 | def start do 15 | :inets.start() 16 | 17 | case :inets.start(:httpd, @config) do 18 | {:ok, pid} -> 19 | port = :httpd.info(pid)[:port] 20 | {:ok, %__MODULE__{base_url: "http://localhost:#{port}/", pid: pid}} 21 | 22 | error -> 23 | error 24 | end 25 | end 26 | 27 | def port(%__MODULE__{pid: pid}) do 28 | :httpd.info(pid)[:port] 29 | end 30 | 31 | def stop(%__MODULE__{pid: pid}) do 32 | :ok = :inets.stop(:httpd, pid) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /integration_test/tests.exs: -------------------------------------------------------------------------------- 1 | # Common tests supported by all drivers 2 | Code.require_file("cases/browser_test.exs", __DIR__) 3 | Code.require_file("cases/browser/all_test.exs", __DIR__) 4 | Code.require_file("cases/browser/assert_css_test.exs", __DIR__) 5 | Code.require_file("cases/browser/assert_refute_has_test.exs", __DIR__) 6 | Code.require_file("cases/browser/assert_text_test.exs", __DIR__) 7 | Code.require_file("cases/browser/attr_test.exs", __DIR__) 8 | Code.require_file("cases/browser/clear_test.exs", __DIR__) 9 | Code.require_file("cases/browser/click_button_test.exs", __DIR__) 10 | Code.require_file("cases/browser/click_test.exs", __DIR__) 11 | Code.require_file("cases/browser/cookies_test.exs", __DIR__) 12 | Code.require_file("cases/browser/current_path_test.exs", __DIR__) 13 | Code.require_file("cases/browser/dialog_test.exs", __DIR__) 14 | Code.require_file("cases/browser/execute_script_test.exs", __DIR__) 15 | Code.require_file("cases/browser/fill_in_test.exs", __DIR__) 16 | Code.require_file("cases/browser/find_test.exs", __DIR__) 17 | Code.require_file("cases/browser/has_css_test.exs", __DIR__) 18 | Code.require_file("cases/browser/has_text_test.exs", __DIR__) 19 | Code.require_file("cases/browser/has_value_test.exs", __DIR__) 20 | Code.require_file("cases/browser/invalid_selectors_test.exs", __DIR__) 21 | Code.require_file("cases/browser/local_storage_test.exs", __DIR__) 22 | Code.require_file("cases/browser/navigation_test.exs", __DIR__) 23 | Code.require_file("cases/browser/page_source_test.exs", __DIR__) 24 | Code.require_file("cases/browser/screenshot_test.exs", __DIR__) 25 | Code.require_file("cases/browser/select_test.exs", __DIR__) 26 | Code.require_file("cases/browser/set_value_test.exs", __DIR__) 27 | Code.require_file("cases/browser/send_keys_test.exs", __DIR__) 28 | Code.require_file("cases/browser/stale_nodes_test.exs", __DIR__) 29 | Code.require_file("cases/browser/text_test.exs", __DIR__) 30 | Code.require_file("cases/browser/title_test.exs", __DIR__) 31 | Code.require_file("cases/browser/visible_test.exs", __DIR__) 32 | Code.require_file("cases/browser/window_size_test.exs", __DIR__) 33 | Code.require_file("cases/element/send_keys_test.exs", __DIR__) 34 | Code.require_file("cases/query_test.exs", __DIR__) 35 | Code.require_file("cases/wallaby_test.exs", __DIR__) 36 | -------------------------------------------------------------------------------- /lib/event_emitter.ex: -------------------------------------------------------------------------------- 1 | defmodule EventEmitter do 2 | @moduledoc false 3 | # This module offers telemetry style event emission for testing purposes. 4 | 5 | # If you'd like to emit a message to the event stream, you can call `emit/1` from your 6 | # implementation code. This is macro, and will not result in any AST injection when not being 7 | # compiled in the test env. 8 | 9 | # ```elixir 10 | # defmodule ImplMod do 11 | # use EventEmitter, :emitter 12 | 13 | # def implementation do 14 | # # some logic 15 | 16 | # emit %{ 17 | # name: :implementation, 18 | # module: __MODULE__, 19 | # metadata: %{unique_identifier: some_variable} 20 | # } 21 | # end 22 | # end 23 | # ``` 24 | 25 | # If you'd like to await on a message emitted by implementation code, you can call `await/3` from 26 | # your test code after registering a handler for your test process 27 | 28 | # ```elixir 29 | # defmodule TestMod do 30 | # use EventEmitter, :receiver 31 | 32 | # test "some test" do 33 | # EventEmitter.add_handler(self()) 34 | 35 | # # some tricky asynchronous code 36 | 37 | # await :implementation, __MODULE__, %{unique_identifier: some_variable} 38 | # end 39 | # end 40 | # ``` 41 | 42 | # You can use EventEmitter by starting it in your test helper. 43 | 44 | # ```elixir 45 | # # test_helper.exs 46 | 47 | # EventEmitter.start_link([]) 48 | 49 | # ExUnit.start() 50 | # ``` 51 | 52 | use GenServer 53 | 54 | @type event :: %{ 55 | optional(:metadata) => map(), 56 | required(:name) => String.t() 57 | } 58 | 59 | def emitter do 60 | quote do 61 | import EventEmitter, only: [emit: 1] 62 | end 63 | end 64 | 65 | def receiver do 66 | quote do 67 | import EventEmitter, only: [await: 3] 68 | end 69 | end 70 | 71 | defmacro __using__(which) do 72 | apply(__MODULE__, which, []) 73 | end 74 | 75 | defmacro emit(event) do 76 | if Mix.env() == :test do 77 | quote do 78 | EventEmitter.emit_event(unquote(event)) 79 | end 80 | else 81 | nil 82 | end 83 | end 84 | 85 | def await(name, metadata, module) do 86 | e = {:event, %{metadata: metadata, name: name, module: module}} 87 | 88 | receive do 89 | ^e -> :ok 90 | end 91 | end 92 | 93 | def start_link(args) do 94 | GenServer.start_link(__MODULE__, args, name: __MODULE__) 95 | end 96 | 97 | def add_handler(pid), do: GenServer.call(__MODULE__, {:add_handler, pid}) 98 | def emit_event(event), do: GenServer.cast(__MODULE__, {:event, event}) 99 | 100 | @impl GenServer 101 | def init(_) do 102 | {:ok, %{handlers: []}} 103 | end 104 | 105 | @impl GenServer 106 | def handle_call({:add_handler, pid}, _, state) do 107 | {:reply, pid, %{state | handlers: [pid | state.handlers]}} 108 | end 109 | 110 | @impl GenServer 111 | def handle_cast({:event, event}, %{handlers: handlers} = state) do 112 | for h <- handlers do 113 | send(h, {:event, event}) 114 | end 115 | 116 | {:noreply, state} 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/wallaby.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby do 2 | @moduledoc """ 3 | A concurrent feature testing library. 4 | 5 | ## Configuration 6 | 7 | Wallaby supports the following options: 8 | 9 | * `:otp_app` - The name of your OTP application. This is used to check out your Ecto repos into the SQL Sandbox. 10 | * `:screenshot_dir` - The directory to store screenshots. 11 | * `:screenshot_on_failure` - if Wallaby should take screenshots on test failures (defaults to `false`). 12 | * `:max_wait_time` - The amount of time that Wallaby should wait to find an element on the page. (defaults to `3_000`) 13 | * `:js_errors` - if Wallaby should re-throw JavaScript errors in elixir (defaults to true). 14 | * `:js_logger` - IO device where JavaScript console logs are written to. Defaults to :stdio. This option can also be set to a file or any other io device. You can disable JavaScript console logging by setting this to `nil`. 15 | """ 16 | 17 | @drivers %{ 18 | "chrome" => Wallaby.Chrome, 19 | "selenium" => Wallaby.Selenium 20 | } 21 | 22 | use Application 23 | 24 | alias Wallaby.Session 25 | alias Wallaby.SessionStore 26 | 27 | @doc false 28 | def start(_type, _args) do 29 | import Supervisor.Spec, warn: false 30 | 31 | case driver().validate() do 32 | :ok -> :ok 33 | {:error, exception} -> raise exception 34 | end 35 | 36 | children = [ 37 | {driver(), [name: Wallaby.Driver.Supervisor]}, 38 | :hackney_pool.child_spec(:wallaby_pool, 39 | timeout: 15_000, 40 | max_connections: System.schedulers_online() 41 | ), 42 | {Wallaby.SessionStore, [name: Wallaby.SessionStore]} 43 | ] 44 | 45 | opts = [strategy: :one_for_one, name: Wallaby.Supervisor] 46 | Supervisor.start_link(children, opts) 47 | end 48 | 49 | @type reason :: any 50 | @type start_session_opts :: {atom, any} 51 | 52 | @doc """ 53 | Starts a browser session. 54 | 55 | ## Multiple sessions 56 | 57 | Each session runs in its own browser so that each test runs in isolation. 58 | Because of this isolation multiple sessions can be created for a test: 59 | 60 | ``` 61 | @message_field Query.text_field("Share Message") 62 | @share_button Query.button("Share") 63 | @message_list Query.css(".messages") 64 | 65 | test "That multiple sessions work" do 66 | {:ok, user1} = Wallaby.start_session 67 | user1 68 | |> visit("/page.html") 69 | |> fill_in(@message_field, with: "Hello there!") 70 | |> click(@share_button) 71 | 72 | {:ok, user2} = Wallaby.start_session 73 | user2 74 | |> visit("/page.html") 75 | |> fill_in(@message_field, with: "Hello yourself") 76 | |> click(@share_button) 77 | 78 | assert user1 |> find(@message_list) |> List.last |> text == "Hello yourself" 79 | assert user2 |> find(@message_list) |> List.first |> text == "Hello there" 80 | end 81 | ``` 82 | """ 83 | @spec start_session([start_session_opts]) :: {:ok, Session.t()} | {:error, reason} 84 | def start_session(opts \\ []) do 85 | with {:ok, session} <- driver().start_session(opts), 86 | :ok <- SessionStore.monitor(session), 87 | do: {:ok, session} 88 | end 89 | 90 | @doc """ 91 | Ends a browser session. 92 | """ 93 | @spec end_session(Session.t()) :: :ok | {:error, reason} 94 | def end_session(%Session{driver: driver} = session) do 95 | with :ok <- SessionStore.demonitor(session) do 96 | driver.end_session(session) 97 | end 98 | end 99 | 100 | @doc false 101 | def screenshot_on_failure? do 102 | Application.get_env(:wallaby, :screenshot_on_failure) 103 | end 104 | 105 | @doc false 106 | def js_errors? do 107 | Application.get_env(:wallaby, :js_errors, true) 108 | end 109 | 110 | @doc false 111 | def js_logger do 112 | Application.get_env(:wallaby, :js_logger, :stdio) 113 | end 114 | 115 | def driver do 116 | Map.get( 117 | @drivers, 118 | System.get_env("WALLABY_DRIVER"), 119 | Application.get_env(:wallaby, :driver, Wallaby.Chrome) 120 | ) 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/wallaby/chrome/chromedriver.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Chrome.Chromedriver do 2 | @moduledoc false 3 | 4 | alias Wallaby.Chrome 5 | alias Wallaby.Chrome.Chromedriver.Server 6 | 7 | def child_spec(_arg) do 8 | {:ok, chromedriver_path} = Chrome.find_chromedriver_executable() 9 | Server.child_spec([chromedriver_path, []]) 10 | end 11 | 12 | @spec wait_until_ready(timeout()) :: :ok | {:error, :timeout} 13 | def wait_until_ready(timeout) do 14 | process_name = {:via, PartitionSupervisor, {Wallaby.Chromedrivers, self()}} 15 | Server.wait_until_ready(process_name, timeout) 16 | end 17 | 18 | @spec base_url :: String.t() 19 | def base_url do 20 | process_name = {:via, PartitionSupervisor, {Wallaby.Chromedrivers, self()}} 21 | Server.get_base_url(process_name) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/wallaby/chrome/chromedriver/readiness_checker.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Chrome.Chromedriver.ReadinessChecker do 2 | @moduledoc false 3 | 4 | alias WebDriverClient.Config 5 | alias WebDriverClient.ServerStatus 6 | 7 | @type url :: String.t() 8 | 9 | @spec wait_until_ready(url, non_neg_integer()) :: :ok 10 | def wait_until_ready(base_url, delay \\ 200) 11 | when is_binary(base_url) and is_integer(delay) and delay >= 0 do 12 | if ready?(base_url) do 13 | :ok 14 | else 15 | Process.sleep(delay) 16 | wait_until_ready(base_url, delay) 17 | end 18 | end 19 | 20 | @spec ready?(url) :: boolean 21 | defp ready?(base_url) do 22 | base_url 23 | |> build_config() 24 | |> WebDriverClient.fetch_server_status() 25 | |> case do 26 | {:ok, %ServerStatus{ready?: true}} -> true 27 | _ -> false 28 | end 29 | end 30 | 31 | # @spec build_config(url) :: Config.t() 32 | def build_config(base_url) do 33 | # Chromedriver responds to the status endpoint check in w3c 34 | # protocol. 35 | Config.build(base_url, 36 | protocol: :w3c, 37 | http_client_options: hackney_options() 38 | ) 39 | end 40 | 41 | @default_httpoison_options [hackney: [pool: :wallaby_pool]] 42 | 43 | # The :hackney_options key in the environment is misnamed. These 44 | # are actually the options as they're passed to HTTPoison. 45 | @spec hackney_options() :: list 46 | defp hackney_options do 47 | :wallaby 48 | |> Application.get_env(:hackney_options, @default_httpoison_options) 49 | |> Keyword.get(:hackney, []) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/wallaby/chrome/logger.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Chrome.Logger do 2 | @moduledoc false 3 | @log_regex ~r/^(?\S+) (?\d+):(?\d+) (?.*)$/s 4 | @string_regex ~r/^"(?.+)"$/ 5 | 6 | def parse_log(%{"level" => "SEVERE", "source" => "javascript", "message" => msg}) do 7 | if Wallaby.js_errors?() do 8 | raise Wallaby.JSError, msg 9 | end 10 | end 11 | 12 | def parse_log(%{"level" => "INFO", "source" => "console-api", "message" => msg}) do 13 | if Wallaby.js_logger() do 14 | case Regex.named_captures(@log_regex, msg) do 15 | %{"message" => message} -> print_message(message) 16 | end 17 | end 18 | end 19 | 20 | def parse_log(_), do: nil 21 | 22 | defp print_message(message) do 23 | message = 24 | case Regex.named_captures(@string_regex, message) do 25 | %{"string" => string} -> format_string(string) 26 | nil -> message 27 | end 28 | 29 | IO.puts(Wallaby.js_logger(), message) 30 | end 31 | 32 | defp format_string(message) do 33 | unescaped = String.replace(message, ~r/\\(.)/, "\\1") 34 | 35 | case Jason.decode(unescaped) do 36 | {:ok, data} -> "\n#{Jason.encode!(data, pretty: true)}" 37 | {:error, _} -> unescaped 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/wallaby/driver/external_command.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Driver.ExternalCommand do 2 | @moduledoc false 3 | 4 | @type t :: %__MODULE__{ 5 | executable: String.t(), 6 | args: [String.t()] 7 | } 8 | 9 | defstruct [:executable, args: []] 10 | end 11 | -------------------------------------------------------------------------------- /lib/wallaby/driver/log_checker.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Driver.LogChecker do 2 | @moduledoc false 3 | alias Wallaby.Driver.LogStore 4 | 5 | def check_logs!(%{driver: driver} = session, fun) do 6 | return_value = fun.() 7 | 8 | {:ok, logs} = driver.log(session) 9 | 10 | session.session_url 11 | |> LogStore.append_logs(logs) 12 | |> Enum.each(&driver.parse_log/1) 13 | 14 | return_value 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/wallaby/driver/log_store.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Driver.LogStore do 2 | @moduledoc false 3 | 4 | use Agent 5 | 6 | def start_link(opts) do 7 | opts = Keyword.put_new(opts, :name, __MODULE__) 8 | 9 | Agent.start_link(fn -> Map.new() end, opts) 10 | end 11 | 12 | @doc """ 13 | Appends logs to a session 14 | """ 15 | def append_logs(session, logs, log_store \\ __MODULE__) 16 | 17 | def append_logs(session, logs, log_store) when is_binary(session) and not is_list(logs) do 18 | append_logs(session, List.wrap(logs), log_store) 19 | end 20 | 21 | def append_logs(session, logs, log_store) when is_binary(session) do 22 | Agent.get_and_update(log_store, fn map -> 23 | Map.get_and_update(map, session, fn 24 | nil -> unique_logs([], logs) 25 | old_logs -> unique_logs(old_logs, logs) 26 | end) 27 | end) 28 | end 29 | 30 | def get_logs(session, log_store \\ __MODULE__) when is_binary(session) do 31 | Agent.get(log_store, fn map -> Map.get(map, session) end) 32 | end 33 | 34 | defp unique_logs(old_logs, new_logs) do 35 | union = new_logs -- old_logs 36 | {union, old_logs ++ union} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/wallaby/driver/temporary_path.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Driver.TemporaryPath do 2 | @moduledoc false 3 | 4 | @spec generate(String.t()) :: String.t() 5 | def generate(base_path \\ System.tmp_dir!()) do 6 | dirname = 7 | 0x100000000 8 | |> :rand.uniform() 9 | |> Integer.to_string(36) 10 | |> String.downcase() 11 | 12 | Path.join(base_path, dirname) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/wallaby/driver/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Driver.Utils do 2 | @moduledoc false 3 | 4 | @type port_number :: 0..65_535 5 | 6 | @spec find_available_port() :: port_number 7 | def find_available_port do 8 | {:ok, listen} = :gen_tcp.listen(0, []) 9 | {:ok, port} = :inet.port(listen) 10 | :gen_tcp.close(listen) 11 | port 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/wallaby/dsl.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.DSL do 2 | @moduledoc """ 3 | Sets up the Wallaby DSL in a module. 4 | 5 | All functions in `Wallaby.Browser` are now accessible without a module name 6 | and `Wallaby.Browser`, `Wallaby.Element` and `Wallaby.Query` are all aliased. 7 | 8 | ## Example 9 | 10 | ```elixir 11 | defmodule MyPage do 12 | use Wallaby.DSL 13 | 14 | @name_field Query.text_field("Name") 15 | @email_field Query.text_field("email") 16 | @save_button Query.button("Save") 17 | 18 | def register(session) do 19 | session 20 | |> visit("/registration.html") 21 | |> fill_in(@name_field, with: "Chris") 22 | |> fill_in(@email_field, with: "c@keathly.io") 23 | |> click(@save_button) 24 | end 25 | end 26 | ``` 27 | """ 28 | 29 | defmacro __using__([]) do 30 | quote do 31 | alias Wallaby.Browser 32 | alias Wallaby.Element 33 | alias Wallaby.Query 34 | 35 | # Kernel.tap/2 was introduced in 1.12 and conflicts with Browser.tap/2 36 | import Kernel, except: [tap: 2] 37 | import Wallaby.Browser 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/wallaby/exceptions.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.QueryError do 2 | defexception [:message] 3 | 4 | def exception(error) do 5 | %__MODULE__{message: error} 6 | end 7 | end 8 | 9 | defmodule Wallaby.ExpectationNotMetError do 10 | defexception [:message] 11 | end 12 | 13 | defmodule Wallaby.BadMetadataError do 14 | defexception [:message] 15 | end 16 | 17 | defmodule Wallaby.NoBaseUrlError do 18 | defexception [:message] 19 | 20 | def exception(relative_path) do 21 | msg = """ 22 | You called visit with #{relative_path}, but did not set a base_url. 23 | Set this in config/test.exs or in test/test_helper.exs: 24 | 25 | Application.put_env(:wallaby, :base_url, "http://localhost:4001") 26 | 27 | If using Phoenix, you can use the url from your endpoint: 28 | 29 | Application.put_env(:wallaby, :base_url, YourApplication.Endpoint.url) 30 | """ 31 | 32 | %__MODULE__{message: msg} 33 | end 34 | end 35 | 36 | defmodule Wallaby.JSError do 37 | defexception [:message] 38 | 39 | def exception(js_error) do 40 | msg = """ 41 | There was an uncaught JavaScript error: 42 | 43 | #{js_error} 44 | """ 45 | 46 | %__MODULE__{message: msg} 47 | end 48 | end 49 | 50 | defmodule Wallaby.StaleReferenceError do 51 | defexception [:message] 52 | 53 | def exception(_) do 54 | msg = """ 55 | The element you are trying to reference is stale or no longer attached to the 56 | DOM. The most likely reason is that it has been removed with JavaScript. 57 | 58 | You can typically solve this problem by using `find` to block until the DOM is in a 59 | stable state. 60 | """ 61 | 62 | %__MODULE__{message: msg} 63 | end 64 | end 65 | 66 | defmodule Wallaby.CookieError do 67 | defexception [:message] 68 | 69 | def exception(_) do 70 | msg = """ 71 | The cookie you are trying to set has no domain. 72 | 73 | You're most likely seeing this error because you're trying to set a cookie before 74 | you have visited a page. You can fix this issue by calling `visit/1` 75 | before you call `set_cookie/3` or `set_cookie/4`. 76 | """ 77 | 78 | %__MODULE__{message: msg} 79 | end 80 | end 81 | 82 | defmodule Wallaby.DependencyError do 83 | defexception [:message] 84 | 85 | @type t :: %__MODULE__{ 86 | message: String.t() 87 | } 88 | 89 | def exception(msg) do 90 | %__MODULE__{message: msg} 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/wallaby/helpers/key_codes.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Helpers.KeyCodes do 2 | @moduledoc """ 3 | Shortcuts for various keys. 4 | 5 | - :null 6 | - :cancel 7 | - :help 8 | - :backspace 9 | - :tab 10 | - :clear 11 | - :return 12 | - :enter 13 | - :shift 14 | - :control 15 | - :alt 16 | - :pause 17 | - :escape 18 | - :space 19 | - :pageup 20 | - :pagedown 21 | - :end 22 | - :home 23 | - :left_arrow 24 | - :up_arrow 25 | - :right_arrow 26 | - :down_arrow 27 | - :insert 28 | - :delete 29 | - :semicolon 30 | - :equals 31 | - :num0 32 | - :num1 33 | - :num2 34 | - :num3 35 | - :num4 36 | - :num5 37 | - :num6 38 | - :num7 39 | - :num8 40 | - :num9 41 | - :multiply 42 | - :add 43 | - :seperator 44 | - :subtract 45 | - :decimal 46 | - :divide 47 | - :command 48 | """ 49 | 50 | # Encode a list of key codes to a usable JSON representation. 51 | @spec json(list(atom)) :: String.t() 52 | def json(keys) when is_list(keys) do 53 | unicode = 54 | keys 55 | |> Enum.reduce([], fn x, acc -> acc ++ split_strings(x) end) 56 | |> Enum.map_join(",", &"\"#{code(&1)}\"") 57 | 58 | "{\"value\": [#{unicode}]}" 59 | end 60 | 61 | # Ensures a list of keys are in binary form to check for local files. 62 | @spec chars(list() | binary()) :: [binary()] 63 | def chars(keys) do 64 | keys 65 | |> List.wrap() 66 | |> Enum.map(fn 67 | a when is_atom(a) -> code(a) 68 | s -> s 69 | end) 70 | end 71 | 72 | defp split_strings(x) when is_binary(x), do: String.graphemes(x) 73 | defp split_strings(x), do: [x] 74 | 75 | defp code(:null), do: "\\uE000" 76 | defp code(:cancel), do: "\\uE001" 77 | defp code(:help), do: "\\uE002" 78 | defp code(:backspace), do: "\\uE003" 79 | defp code(:tab), do: "\\uE004" 80 | defp code(:clear), do: "\\uE005" 81 | defp code(:return), do: "\\uE006" 82 | defp code(:enter), do: "\\uE007" 83 | defp code(:shift), do: "\\uE008" 84 | defp code(:control), do: "\\uE009" 85 | defp code(:alt), do: "\\uE00A" 86 | defp code(:pause), do: "\\uE00B" 87 | defp code(:escape), do: "\\uE00C" 88 | 89 | defp code(:space), do: "\\uE00D" 90 | defp code(:pageup), do: "\\uE00E" 91 | defp code(:pagedown), do: "\\uE00F" 92 | defp code(:end), do: "\\uE010" 93 | defp code(:home), do: "\\uE011" 94 | defp code(:left_arrow), do: "\\uE012" 95 | defp code(:up_arrow), do: "\\uE013" 96 | defp code(:right_arrow), do: "\\uE014" 97 | defp code(:down_arrow), do: "\\uE015" 98 | defp code(:insert), do: "\\uE016" 99 | defp code(:delete), do: "\\uE017" 100 | defp code(:semicolon), do: "\\uE018" 101 | defp code(:equals), do: "\\uE019" 102 | 103 | defp code(:num0), do: "\\uE01A" 104 | defp code(:num1), do: "\\uE01B" 105 | defp code(:num2), do: "\\uE01C" 106 | defp code(:num3), do: "\\uE01D" 107 | defp code(:num4), do: "\\uE01E" 108 | defp code(:num5), do: "\\uE01F" 109 | defp code(:num6), do: "\\uE020" 110 | defp code(:num7), do: "\\uE021" 111 | defp code(:num8), do: "\\uE022" 112 | defp code(:num9), do: "\\uE023" 113 | 114 | defp code(:multiply), do: "\\uE024" 115 | defp code(:add), do: "\\uE025" 116 | defp code(:seperator), do: "\\uE026" 117 | defp code(:subtract), do: "\\uE027" 118 | defp code(:decimal), do: "\\uE028" 119 | defp code(:divide), do: "\\uE029" 120 | 121 | defp code(:command), do: "\\uE03D" 122 | 123 | defp code(char), do: char 124 | end 125 | -------------------------------------------------------------------------------- /lib/wallaby/httpclient.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.HTTPClient do 2 | @moduledoc false 3 | 4 | alias Wallaby.Query 5 | 6 | @type method :: :post | :get | :delete 7 | @type url :: String.t() 8 | @type params :: map | String.t() 9 | @type request_opts :: {:encode_json, boolean} 10 | @type response :: map 11 | @type web_driver_error_reason :: :stale_reference | :invalid_selector | :unexpected_alert 12 | 13 | @status_obscured 13 14 | # The maximum time we'll sleep is for 50ms 15 | @max_jitter 50 16 | 17 | @doc """ 18 | Sends a request to the webdriver API and parses the 19 | response. 20 | """ 21 | @spec request(method, url, params, [request_opts]) :: 22 | {:ok, response} 23 | | {:error, web_driver_error_reason | Jason.DecodeError.t() | String.t()} 24 | | no_return 25 | 26 | def request(method, url, params \\ %{}, opts \\ []) 27 | 28 | def request(method, url, params, _opts) when map_size(params) == 0 do 29 | make_request(method, url, "") 30 | end 31 | 32 | def request(method, url, params, [{:encode_json, false} | _]) do 33 | make_request(method, url, params) 34 | end 35 | 36 | def request(method, url, params, _opts) do 37 | make_request(method, url, Jason.encode!(params)) 38 | end 39 | 40 | defp make_request(method, url, body), do: make_request(method, url, body, 0, []) 41 | 42 | @spec make_request(method, url, String.t() | map, non_neg_integer(), [String.t()]) :: 43 | {:ok, response} 44 | | {:error, web_driver_error_reason | Jason.DecodeError.t() | String.t()} 45 | | no_return 46 | defp make_request(_, _, _, 5, retry_reasons) do 47 | ["Wallaby had an internal issue with HTTPoison:" | retry_reasons] 48 | |> Enum.uniq() 49 | |> Enum.join("\n") 50 | |> raise 51 | end 52 | 53 | defp make_request(method, url, body, retry_count, retry_reasons) do 54 | method 55 | |> HTTPoison.request(url, body, headers(), request_opts()) 56 | |> handle_response() 57 | |> case do 58 | {:error, :httpoison, error} -> 59 | :timer.sleep(jitter()) 60 | make_request(method, url, body, retry_count + 1, [inspect(error) | retry_reasons]) 61 | 62 | result -> 63 | result 64 | end 65 | end 66 | 67 | @spec handle_response({:ok, HTTPoison.Response.t()} | {:error, HTTPoison.Error.t()}) :: 68 | {:ok, response} 69 | | {:error, web_driver_error_reason | Jason.DecodeError.t() | String.t()} 70 | | {:error, :httpoison, HTTPoison.Error.t()} 71 | | no_return 72 | defp handle_response(resp) do 73 | case resp do 74 | {:error, %HTTPoison.Error{} = error} -> 75 | {:error, :httpoison, error} 76 | 77 | {:ok, %HTTPoison.Response{status_code: 204}} -> 78 | {:ok, %{"value" => nil}} 79 | 80 | {:ok, %HTTPoison.Response{body: body}} -> 81 | with {:ok, decoded} <- Jason.decode(body), 82 | {:ok, response} <- check_status(decoded) do 83 | check_for_response_errors(response) 84 | end 85 | end 86 | end 87 | 88 | @spec check_status(response) :: {:ok, response} | {:error, String.t()} 89 | defp check_status(response) do 90 | case Map.get(response, "status") do 91 | @status_obscured -> 92 | message = get_in(response, ["value", "message"]) 93 | 94 | {:error, message} 95 | 96 | _ -> 97 | {:ok, response} 98 | end 99 | end 100 | 101 | @spec check_for_response_errors(response) :: 102 | {:ok, response} 103 | | {:error, web_driver_error_reason} 104 | | no_return 105 | defp check_for_response_errors(response) do 106 | response = coerce_json_message(response) 107 | 108 | case Map.get(response, "value") do 109 | %{"class" => "org.openqa.selenium.StaleElementReferenceException"} -> 110 | {:error, :stale_reference} 111 | 112 | %{"message" => "Stale element reference" <> _} -> 113 | {:error, :stale_reference} 114 | 115 | %{"message" => "stale element reference" <> _} -> 116 | {:error, :stale_reference} 117 | 118 | %{ 119 | "message" => 120 | "An element command failed because the referenced element is no longer available" <> _ 121 | } -> 122 | {:error, :stale_reference} 123 | 124 | %{"message" => %{"value" => "An invalid or illegal selector was specified"}} -> 125 | {:error, :invalid_selector} 126 | 127 | %{"message" => "invalid selector" <> _} -> 128 | {:error, :invalid_selector} 129 | 130 | %{"class" => "org.openqa.selenium.InvalidSelectorException"} -> 131 | {:error, :invalid_selector} 132 | 133 | %{"class" => "org.openqa.selenium.InvalidElementStateException"} -> 134 | {:error, :invalid_selector} 135 | 136 | %{"message" => "unexpected alert" <> _} -> 137 | {:error, :unexpected_alert} 138 | 139 | %{"error" => _, "message" => message} -> 140 | raise message 141 | 142 | _ -> 143 | {:ok, response} 144 | end 145 | end 146 | 147 | defp request_opts do 148 | Application.get_env(:wallaby, :hackney_options, hackney: [pool: :wallaby_pool]) 149 | end 150 | 151 | defp headers do 152 | [{"Accept", "application/json"}, {"Content-Type", "application/json;charset=UTF-8"}] 153 | end 154 | 155 | @spec to_params(Query.compiled()) :: map 156 | def to_params({:xpath, xpath}) do 157 | %{using: "xpath", value: xpath} 158 | end 159 | 160 | def to_params({:css, css}) do 161 | %{using: "css selector", value: css} 162 | end 163 | 164 | defp jitter, do: :rand.uniform(@max_jitter) 165 | 166 | defp coerce_json_message(%{"value" => %{"message" => message} = value} = response) do 167 | value = 168 | with %{"payload" => payload, "type" => type} <- 169 | Regex.named_captures(~r/(?.*): (?{.*})\n.*/, message), 170 | {:ok, message} <- Jason.decode(payload) do 171 | %{ 172 | "message" => message, 173 | "type" => type 174 | } 175 | else 176 | _ -> 177 | value 178 | end 179 | 180 | put_in(response["value"], value) 181 | end 182 | 183 | defp coerce_json_message(response) do 184 | response 185 | end 186 | end 187 | -------------------------------------------------------------------------------- /lib/wallaby/metadata.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Metadata do 2 | @moduledoc false 3 | 4 | # Metadata is used to encode information about the browser and test. This 5 | # information is then stored in a User Agent string. The information from the 6 | # test can then be extracted in the application. 7 | 8 | @prefix "BeamMetadata" 9 | @regex ~r{#{@prefix} \((.*?)\)} 10 | 11 | def append(user_agent, nil), do: user_agent 12 | 13 | def append(user_agent, metadata) when is_map(metadata) or is_list(metadata) do 14 | append(user_agent, format(metadata)) 15 | end 16 | 17 | def append(user_agent, metadata) when is_binary(metadata) do 18 | "#{user_agent}/#{metadata}" 19 | end 20 | 21 | @doc """ 22 | Formats a string to a valid UserAgent string. 23 | """ 24 | def format(metadata) do 25 | encoded = 26 | {:v1, metadata} 27 | |> :erlang.term_to_binary() 28 | |> Base.url_encode64() 29 | 30 | "#{@prefix} (#{encoded})" 31 | end 32 | 33 | def extract(str) do 34 | ua = 35 | str 36 | |> String.split("/") 37 | |> List.last() 38 | 39 | case Regex.run(@regex, ua) do 40 | [_, metadata] -> parse(metadata) 41 | _ -> %{} 42 | end 43 | end 44 | 45 | def parse(encoded_metadata) do 46 | encoded_metadata 47 | |> Base.url_decode64!() 48 | |> :erlang.binary_to_term() 49 | |> case do 50 | {:v1, metadata} -> metadata 51 | _ -> raise Wallaby.BadMetadataError, message: "#{encoded_metadata} is not valid" 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/wallaby/query/xpath.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Query.XPath do 2 | @moduledoc false 3 | # credo:disable-for-this-file Credo.Check.Readability.MaxLineLength 4 | 5 | @type query :: String.t() 6 | @type xpath :: String.t() 7 | @type name :: query 8 | @type id :: query 9 | @type label :: query 10 | 11 | @doc """ 12 | XPath for links 13 | 14 | This xpath is gracious ripped from capybara via 15 | https://github.com/jnicklas/xpath/blob/master/lib/xpath/html.rb 16 | """ 17 | def link(lnk) do 18 | ~s{.//a[./@href][(((./@id = "#{lnk}" or contains(normalize-space(string(.)), "#{lnk}")) or contains(./@title, "#{lnk}")) or .//img[contains(./@alt, "#{lnk}")])]} 19 | end 20 | 21 | @doc """ 22 | Match any clickable buttons 23 | """ 24 | def button(query) do 25 | types = "./@type = 'submit' or ./@type = 'reset' or ./@type = 'button' or ./@type = 'image'" 26 | 27 | locator = 28 | ~s{(((./@id = "#{query}" or ./@name = "#{query}" or ./@value = "#{query}" or ./@alt = "#{query}" or ./@title = "#{query}" or contains(normalize-space(string(.)), "#{query}"))))} 29 | 30 | ~s{.//input[#{types}][#{locator}] | .//button[(not(./@type) or #{types})][#{locator}]} 31 | end 32 | 33 | @doc """ 34 | Match any radio buttons 35 | """ 36 | def radio_button(query) do 37 | ~s{.//input[./@type = 'radio'][(((./@id = "#{query}" or ./@name = "#{query}") or ./@placeholder = "#{query}") or ./@id = //label[contains(normalize-space(string(.)), "#{query}")]/@for)] | .//label[contains(normalize-space(string(.)), "#{query}")]//.//input[./@type = "radio"]} 38 | end 39 | 40 | @doc """ 41 | Match any `input` or `textarea` that can be filled with text. 42 | Excludes any inputs with types of `submit`, `image`, `radio`, `checkbox`, 43 | `hidden`, or `file`. 44 | """ 45 | def fillable_field(query) when is_binary(query) do 46 | ~s{.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = "#{query}" or ./@name = "#{query}") or ./@placeholder = "#{query}") or ./@id = //label[contains(normalize-space(string(.)), "#{query}")]/@for)] | .//label[contains(normalize-space(string(.)), "#{query}")]//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')]} 47 | end 48 | 49 | @doc """ 50 | Match any checkboxes 51 | """ 52 | def checkbox(query) do 53 | ~s{.//input[./@type = 'checkbox'][(((./@id = "#{query}" or ./@name = "#{query}") or ./@placeholder = "#{query}") or ./@id = //label[contains(normalize-space(string(.)), "#{query}")]/@for)] | .//label[contains(normalize-space(string(.)), "#{query}")]//.//input[./@type = "checkbox"]} 54 | end 55 | 56 | @doc """ 57 | Match any `select` by name, id, or label. 58 | """ 59 | def select(query) do 60 | ~s{.//select[(((./@id = "#{query}" or ./@name = "#{query}")) or ./@name = //label[contains(normalize-space(string(.)), "#{query}")]/@for or ./@id = //label[contains(normalize-space(string(.)), "#{query}")]/@for)] | .//label[contains(normalize-space(string(.)), "#{query}")]//.//select} 61 | end 62 | 63 | @doc """ 64 | Match any `option` by visible text 65 | """ 66 | def option(query) do 67 | ~s{.//option[normalize-space(text())="#{query}"]} 68 | end 69 | 70 | @doc """ 71 | Matches any file field by name, id, or label 72 | """ 73 | def file_field(query) do 74 | ~s{.//input[./@type = 'file'][(((./@id = "#{query}" or ./@name = "#{query}")) or ./@id = //label[contains(normalize-space(string(.)), "#{query}")]/@for)] | .//label[contains(normalize-space(string(.)), "#{query}")]//.//input[./@type = 'file']} 75 | end 76 | 77 | @doc """ 78 | Matches any element by its inner text. 79 | """ 80 | def text(selector) do 81 | ~s{.//*[contains(normalize-space(text()), "#{selector}")]} 82 | end 83 | 84 | @doc """ 85 | Matches any element by its attribute name and value pair. 86 | """ 87 | def attribute(name, value) do 88 | ~s{.//*[./@#{name} = "#{value}"]} 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/wallaby/session.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Session do 2 | @moduledoc """ 3 | Struct containing details about the webdriver session. 4 | """ 5 | 6 | @type t :: %__MODULE__{ 7 | id: String.t(), 8 | session_url: String.t(), 9 | url: String.t(), 10 | server: pid | :none, 11 | screenshots: list, 12 | driver: module, 13 | capabilities: map() 14 | } 15 | 16 | defstruct [:id, :url, :session_url, :driver, :capabilities, server: :none, screenshots: []] 17 | end 18 | -------------------------------------------------------------------------------- /lib/wallaby/session_store.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.SessionStore do 2 | @moduledoc false 3 | use GenServer 4 | use EventEmitter, :emitter 5 | 6 | alias Wallaby.WebdriverClient 7 | 8 | def start_link(opts \\ []) do 9 | {opts, args} = Keyword.split(opts, [:name]) 10 | 11 | GenServer.start_link(__MODULE__, args, opts) 12 | end 13 | 14 | def monitor(store \\ __MODULE__, session) do 15 | GenServer.call(store, {:monitor, session}, 10_000) 16 | end 17 | 18 | def demonitor(store \\ __MODULE__, session) do 19 | GenServer.call(store, {:demonitor, session}) 20 | end 21 | 22 | def list_sessions_for(opts \\ []) do 23 | name = Keyword.get(opts, :name, :session_store) 24 | owner_pid = Keyword.get(opts, :owner_pid, self()) 25 | 26 | :ets.select(name, [{{{:_, :_, :"$1"}, :"$2"}, [{:==, :"$1", owner_pid}], [:"$2"]}]) 27 | end 28 | 29 | def init(args) do 30 | name = Keyword.get(args, :ets_name, :session_store) 31 | 32 | opts = 33 | if(name == :session_store, do: [:named_table], else: []) ++ 34 | [:set, :public, read_concurrency: true] 35 | 36 | Process.flag(:trap_exit, true) 37 | tid = :ets.new(name, opts) 38 | 39 | Application.ensure_all_started(:ex_unit) 40 | 41 | ExUnit.after_suite(fn _ -> 42 | try do 43 | :ets.tab2list(tid) 44 | |> Enum.each(&delete_sessions/1) 45 | rescue 46 | _ -> nil 47 | end 48 | end) 49 | 50 | {:ok, %{ets_table: tid}} 51 | end 52 | 53 | def handle_call({:monitor, session}, {pid, _ref}, state) do 54 | ref = Process.monitor(pid) 55 | 56 | :ets.insert(state.ets_table, {{ref, session.id, pid}, session}) 57 | 58 | emit(%{module: __MODULE__, name: :monitor, metadata: %{monitored_session: session}}) 59 | 60 | {:reply, :ok, state} 61 | end 62 | 63 | def handle_call({:demonitor, session}, _from, state) do 64 | result = 65 | :ets.select(state.ets_table, [ 66 | {{{:"$1", :"$2", :"$3"}, :_}, [{:==, :"$2", session.id}], [{{:"$1", :"$3"}}]} 67 | ]) 68 | 69 | case result do 70 | [{ref, pid}] -> 71 | true = Process.demonitor(ref) 72 | :ets.delete(state.ets_table, {ref, session.id, pid}) 73 | 74 | [] -> 75 | :ok 76 | end 77 | 78 | {:reply, :ok, state} 79 | end 80 | 81 | def handle_info({:DOWN, ref, :process, pid, _reason}, state) do 82 | [session] = 83 | :ets.select(state.ets_table, [ 84 | {{{:"$1", :_, :_}, :"$4"}, [{:==, :"$1", ref}], [:"$4"]} 85 | ]) 86 | 87 | WebdriverClient.delete_session(session) 88 | 89 | :ets.delete(state.ets_table, {ref, session.id, pid}) 90 | 91 | emit(%{module: __MODULE__, name: :DOWN, metadata: %{monitored_session: session}}) 92 | 93 | {:noreply, state} 94 | end 95 | 96 | defp delete_sessions({_, session}) do 97 | WebdriverClient.delete_session(session) 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/elixir-wallaby/wallaby" 5 | @version "0.30.10" 6 | @drivers ~w(selenium chrome) 7 | @selected_driver System.get_env("WALLABY_DRIVER") 8 | @maintainers ["Mitchell Hanberg"] 9 | 10 | def project do 11 | [ 12 | app: :wallaby, 13 | version: @version, 14 | elixir: "~> 1.12", 15 | elixirc_paths: elixirc_paths(Mix.env()), 16 | build_embedded: Mix.env() == :prod, 17 | start_permanent: Mix.env() == :prod, 18 | package: package(), 19 | description: "Concurrent feature tests for elixir", 20 | deps: deps(), 21 | docs: docs(), 22 | 23 | # Custom testing 24 | aliases: ["test.all": ["test", "test.drivers"], "test.drivers": &test_drivers/1], 25 | preferred_cli_env: [ 26 | "test.all": :test, 27 | "test.drivers": :test 28 | ], 29 | test_paths: test_paths(@selected_driver), 30 | dialyzer: dialyzer() 31 | ] 32 | end 33 | 34 | def application do 35 | [extra_applications: [:logger], mod: {Wallaby, []}] 36 | end 37 | 38 | defp elixirc_paths(:test), do: ["lib", "test/support"] 39 | # need the testserver in dev for benchmarks to run 40 | defp elixirc_paths(:dev), do: ["lib", "integration_test/support/test_server.ex"] 41 | defp elixirc_paths(_), do: ["lib"] 42 | 43 | defp deps do 44 | [ 45 | {:jason, "~> 1.1"}, 46 | {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0"}, 47 | {:web_driver_client, "~> 0.2.0"}, 48 | {:dialyxir, "~> 1.0", only: :dev, runtime: false}, 49 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, 50 | {:bypass, "~> 1.0.0", only: :test}, 51 | {:ex_doc, "~> 0.28", only: :dev}, 52 | {:ecto_sql, ">= 3.0.0", optional: true}, 53 | {:phoenix_ecto, ">= 3.0.0", optional: true} 54 | ] 55 | end 56 | 57 | defp package do 58 | [ 59 | files: ["lib", "mix.exs", "README.md", "LICENSE.md", "priv"], 60 | maintainers: @maintainers, 61 | licenses: ["MIT"], 62 | links: %{ 63 | "Github" => @source_url, 64 | "Sponsor" => "https://github.com/sponsors/mhanberg" 65 | } 66 | ] 67 | end 68 | 69 | defp docs do 70 | [ 71 | extras: ["README.md": [title: "Introduction"]], 72 | source_ref: "v#{@version}", 73 | source_url: @source_url, 74 | main: "readme", 75 | logo: "guides/images/icon.png" 76 | ] 77 | end 78 | 79 | defp dialyzer do 80 | [ 81 | plt_add_apps: [:inets, :phoenix_ecto, :ecto_sql], 82 | ignore_warnings: ".dialyzer_ignore.exs", 83 | list_unused_filters: true 84 | ] 85 | end 86 | 87 | defp test_paths(driver) when driver in @drivers, do: ["integration_test/#{driver}"] 88 | defp test_paths(_), do: ["test"] 89 | 90 | defp test_drivers(args) do 91 | for driver <- @drivers, do: run_integration_test(driver, args) 92 | end 93 | 94 | defp run_integration_test(driver, args) do 95 | args = if IO.ANSI.enabled?(), do: ["--color" | args], else: ["--no-color" | args] 96 | 97 | IO.puts("==> Running tests for WALLABY_DRIVER=#{driver} mix test") 98 | 99 | {_, res} = 100 | System.cmd("mix", ["test" | args], 101 | into: IO.binstream(:stdio, :line), 102 | env: [{"WALLABY_DRIVER", driver}] 103 | ) 104 | 105 | if res > 0 do 106 | System.at_exit(fn _ -> exit({:shutdown, 1}) end) 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /priv/run_command.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | script_status="running" 4 | 5 | # Start the program in the background 6 | exec "$@" & 7 | pid1=$! 8 | 9 | echo "PID: $pid1" 10 | 11 | shutdown(){ 12 | local pid1=$1 13 | local pid2=$1 14 | 15 | if [ $script_status = "running" ]; then 16 | script_status="shutting down" 17 | wait $pid1 18 | ret=$? 19 | kill -KILL $pid2 20 | exit $ret 21 | fi 22 | } 23 | 24 | # Silence warnings from here on 25 | exec >/dev/null 2>&1 26 | 27 | # Read from stdin in the background and 28 | # kill running program when stdin closes 29 | exec 0<&0 "$( 30 | while read -r; do :; done 31 | kill -KILL $pid1 32 | )" & 33 | pid2=$! 34 | 35 | # Clean up 36 | trap 'shutdown $pid1 $pid2' INT HUP TERM 37 | shutdown $pid1 $pid2 38 | -------------------------------------------------------------------------------- /test/support/application_control.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.TestSupport.ApplicationControl do 2 | @moduledoc """ 3 | Test helpers for starting/stopping wallaby during test setup 4 | """ 5 | 6 | import ExUnit.Assertions, only: [flunk: 1] 7 | import ExUnit.Callbacks, only: [on_exit: 1] 8 | 9 | @doc """ 10 | Stops the wallaby application 11 | """ 12 | def stop_wallaby(_) do 13 | Application.stop(:wallaby) 14 | end 15 | 16 | @doc """ 17 | Restarts wallaby after the current test process exits. 18 | 19 | This ensures wallaby is restarted in a fresh state after 20 | a test that modifies wallaby's startup config. 21 | """ 22 | def restart_wallaby_on_exit!(_) do 23 | on_exit(fn -> 24 | # Stops wallaby if it's been started so it can be 25 | # restarted successfully 26 | Application.stop(:wallaby) 27 | 28 | case Application.start(:wallaby) do 29 | :ok -> 30 | :ok 31 | 32 | result -> 33 | flunk("failed to restart wallaby: #{inspect(result)}") 34 | end 35 | end) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/support/chrome/chrome_test_script.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.TestSupport.Chrome.ChromeTestScript do 2 | @moduledoc """ 3 | Generates scripts that allow testing wallaby's interaction with the 4 | chromedriver executable 5 | """ 6 | 7 | @type test_script_opt :: {:startup_delay, non_neg_integer} 8 | @type path :: String.t() 9 | 10 | @doc """ 11 | Builds a wrapper script around the given chrome executable 12 | that logs script invocations and allows for controlling startup delay. 13 | """ 14 | @spec build_chrome_wrapper_script(String.t(), [test_script_opt]) :: String.t() 15 | def build_chrome_wrapper_script(chrome_path, opts \\ []) when is_list(opts) do 16 | startup_delay = Keyword.get(opts, :startup_delay, 0) 17 | 18 | """ 19 | #!/bin/sh 20 | 21 | echo "#{chrome_path} $@" >> "$0-output" 22 | 23 | if [ "$1" != "--version" ]; then 24 | sleep #{startup_delay / 1000} 25 | fi 26 | 27 | exec #{chrome_path} $@ 28 | """ 29 | end 30 | 31 | @type chrome_version_mock_script_opt :: {:version, String.t()} 32 | 33 | @doc """ 34 | Builds a test script used to test version constraints 35 | """ 36 | @spec build_chrome_version_mock_script([chrome_version_mock_script_opt]) :: String.t() 37 | def build_chrome_version_mock_script(opts) do 38 | version = Keyword.get(opts, :version, "79.0.3945.36") 39 | 40 | """ 41 | #!/bin/sh 42 | 43 | if [ "$1" = "--version" ]; then 44 | echo "Google Chrome #{version}" 45 | fi 46 | """ 47 | end 48 | 49 | @doc """ 50 | Builds a wrapper script around the given chromedriver executable 51 | that logs script invocations and allows for controlling startup delay. 52 | """ 53 | @spec build_chromedriver_wrapper_script(String.t(), [test_script_opt]) :: String.t() 54 | def build_chromedriver_wrapper_script(chromedriver_path, opts \\ []) when is_list(opts) do 55 | startup_delay = Keyword.get(opts, :startup_delay, 0) 56 | 57 | """ 58 | #!/bin/sh 59 | 60 | echo "#{chromedriver_path} $@" >> "$0-output" 61 | 62 | if [ "$1" != "--version" ]; then 63 | sleep #{startup_delay / 1000} 64 | fi 65 | 66 | exec #{chromedriver_path} $@ 67 | """ 68 | end 69 | 70 | @type chromedriver_version_mock_script_opt :: {:version, String.t()} 71 | 72 | @doc """ 73 | Builds a test script used to test version constraints 74 | """ 75 | @spec build_chromedriver_version_mock_script([chromedriver_version_mock_script_opt]) :: 76 | String.t() 77 | def build_chromedriver_version_mock_script(opts) do 78 | version = Keyword.get(opts, :version, "79.0.3945.36") 79 | 80 | """ 81 | #!/bin/sh 82 | 83 | if [ "$1" = "--version" ]; then 84 | echo "ChromeDriver #{version}" 85 | fi 86 | """ 87 | end 88 | 89 | @doc """ 90 | Returns a list of command-line invocations for a given script instance 91 | """ 92 | @spec get_invocations(path) :: [String.t()] 93 | def get_invocations(script_path) when is_binary(script_path) do 94 | script_path 95 | |> output_path() 96 | |> Path.expand() 97 | |> File.read() 98 | |> case do 99 | {:ok, contents} -> 100 | String.split(contents, "\n", trim: true) 101 | 102 | {:error, :enoent} -> 103 | [] 104 | end 105 | end 106 | 107 | @spec output_path(path) :: path 108 | defp output_path(script_path) do 109 | script_path <> "-output" 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /test/support/http_client_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.HttpClientCase do 2 | use ExUnit.CaseTemplate 3 | 4 | import Plug.Conn 5 | 6 | using do 7 | quote do 8 | import Plug.Conn 9 | import Wallaby.HttpClientCase 10 | 11 | setup [:start_bypass] 12 | end 13 | end 14 | 15 | @doc """ 16 | Starts a bypass session and inserts it into the test session 17 | """ 18 | def start_bypass(_) do 19 | {:ok, bypass: Bypass.open()} 20 | end 21 | 22 | @doc """ 23 | Builds a url from the current bypass session 24 | """ 25 | def bypass_url(bypass), do: "http://localhost:#{bypass.port}" 26 | 27 | def bypass_url(bypass, path) do 28 | "#{bypass_url(bypass)}#{path}" 29 | end 30 | 31 | @doc """ 32 | Parses the body of an incoming http request 33 | """ 34 | def parse_body(conn) do 35 | opts = Plug.Parsers.init(parsers: [:urlencoded, :json], json_decoder: Jason) 36 | Plug.Parsers.call(conn, opts) 37 | end 38 | 39 | @doc """ 40 | Sends a response with the json content type 41 | """ 42 | @spec send_json_resp(Conn.t(), Conn.status(), term) :: Conn.t() 43 | def send_json_resp(conn, status_code, body) when is_binary(body) do 44 | conn 45 | |> put_resp_content_type("application/json") 46 | |> send_resp(status_code, body) 47 | end 48 | 49 | def send_json_resp(conn, status_code, body) do 50 | send_json_resp(conn, status_code, Jason.encode!(body)) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/support/json_wire_protocol_responses.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.TestSupport.JSONWireProtocolResponses do 2 | @moduledoc """ 3 | Server response generator for the JSONWireProtocol. 4 | """ 5 | 6 | def start_session_response(opts \\ []) do 7 | session_id = Keyword.get(opts, :session_id, "sample_session") 8 | 9 | %{"sessionId" => session_id, "value" => %{}, "status" => 0} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/support/settings_test_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.SettingsTestHelpers do 2 | @moduledoc """ 3 | Test helpers for working with app environments 4 | """ 5 | 6 | import ExUnit.Callbacks, only: [on_exit: 1] 7 | 8 | @doc """ 9 | Ensures a setting is reset for the app's environment after the test is run. 10 | """ 11 | def ensure_setting_is_reset(app, setting) do 12 | orig_result = Application.fetch_env(app, setting) 13 | 14 | on_exit(fn -> reset_env(orig_result, app, setting) end) 15 | end 16 | 17 | defp reset_env({:ok, orig}, app, setting) do 18 | Application.put_env(app, setting, orig) 19 | end 20 | 21 | defp reset_env(:error, app, setting), do: Application.delete_env(app, setting) 22 | end 23 | -------------------------------------------------------------------------------- /test/support/test_script_utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.TestSupport.TestScriptUtils do 2 | @moduledoc """ 3 | Helper functions for working with webdriver test scripts 4 | """ 5 | 6 | import ExUnit.Assertions 7 | 8 | @doc """ 9 | Pops `switch` from `switches` so each switch value can be checked. 10 | 11 | Raises an `AssertionError` if `switch` key does not exist, or `fun` does not return true. 12 | """ 13 | @spec assert_switch(keyword, atom, (term -> boolean)) :: keyword | no_return 14 | def assert_switch(switches, switch, fun \\ fn _ -> true end) 15 | when is_list(switches) and is_atom(switch) and is_function(fun, 1) do 16 | case Keyword.pop_first(switches, switch) do 17 | {nil, remaining_switches} -> 18 | flunk(""" 19 | Switch #{inspect(switch)} not found 20 | 21 | Switches: #{inspect(remaining_switches, pretty: true)} 22 | """) 23 | 24 | {value, remaining_switches} -> 25 | assert fun.(value) 26 | 27 | remaining_switches 28 | end 29 | end 30 | 31 | @doc """ 32 | Asserts all switches have been analyzed and removed from `switches` 33 | """ 34 | @spec assert_no_remaining_switches(keyword) :: :ok | no_return 35 | def assert_no_remaining_switches(switches) when is_list(switches) do 36 | assert switches == [], 37 | """ 38 | Expected all switches to have already been checked, but got: 39 | 40 | #{inspect(switches, pretty: true)} 41 | """ 42 | end 43 | 44 | @doc """ 45 | Writes `script_contents` into a test script in 46 | `base_directory` and makes it executable. 47 | """ 48 | @spec write_test_script!(String.t(), String.t()) :: String.t() | no_return 49 | def write_test_script!(script_contents, base_directory) do 50 | script_name = "test_script-#{random_string()}" 51 | script_path = Path.join([base_directory, script_name]) 52 | 53 | expanded_script_path = Path.expand(script_path) 54 | 55 | File.write!(expanded_script_path, script_contents) 56 | File.chmod!(expanded_script_path, 0o755) 57 | 58 | script_path 59 | end 60 | 61 | defp random_string do 62 | 0x100000000 63 | |> :rand.uniform() 64 | |> Integer.to_string(36) 65 | |> String.downcase() 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/support/test_workspace.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.TestSupport.TestWorkspace do 2 | @moduledoc """ 3 | Test helpers that create temporary directory that exists 4 | for the lifetime of the test. 5 | """ 6 | 7 | import ExUnit.Callbacks, only: [on_exit: 1] 8 | 9 | @doc """ 10 | Create a directory that will be removed after the test exits. 11 | 12 | See `generate_temporary_path/1` 13 | """ 14 | @spec mkdir!(String.t()) :: String.t() | no_return 15 | def mkdir!(path \\ default_tmp_path()) do 16 | path = generate_temporary_path(path) 17 | 18 | :ok = path |> Path.expand() |> File.mkdir_p!() 19 | 20 | path 21 | end 22 | 23 | @doc """ 24 | Generates a temporary path (without creating the directory) 25 | that will be cleaned up after the test exits. 26 | 27 | ## Placeholders 28 | 29 | In order to not have filenames overlap, the following placeholders are supported 30 | * `%{random_string}` - This placeholder is replaced with a random string 31 | """ 32 | def generate_temporary_path(path \\ default_tmp_path()) do 33 | path = replace_placeholders(path) 34 | 35 | on_exit(fn -> 36 | path 37 | |> Path.expand() 38 | |> File.rm_rf!() 39 | end) 40 | 41 | path 42 | end 43 | 44 | defp default_tmp_path do 45 | Path.join([ 46 | System.tmp_dir!(), 47 | Application.get_env(:wallaby, :tmp_dir_prefix, ""), 48 | "test-workspace-%{random_string}" 49 | ]) 50 | end 51 | 52 | defp replace_placeholders(path) do 53 | String.replace(path, "%{random_string}", random_string()) 54 | end 55 | 56 | defp random_string do 57 | 0x100000000 58 | |> :rand.uniform() 59 | |> Integer.to_string(36) 60 | |> String.downcase() 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/support/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.TestSupport.Utils do 2 | @moduledoc """ 3 | This module contains generic testing helpers. 4 | """ 5 | 6 | @doc """ 7 | Repeatedly execute a closure, with a timeout. Useful for assertions that are relying on asynchronous operations. 8 | """ 9 | def attempt_with_timeout(doer, timeout \\ 100), 10 | do: attempt_with_timeout(doer, now_in_milliseconds(), timeout) 11 | 12 | defp attempt_with_timeout(doer, start, timeout) do 13 | doer.() 14 | rescue 15 | e -> 16 | passed_timeout? = now_in_milliseconds() - start >= timeout 17 | 18 | if passed_timeout? do 19 | reraise e, __STACKTRACE__ 20 | else 21 | attempt_with_timeout(doer, start, timeout) 22 | end 23 | end 24 | 25 | defp now_in_milliseconds(), do: DateTime.utc_now() |> DateTime.to_unix(:millisecond) 26 | end 27 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.configure(exclude: [pending: true]) 2 | EventEmitter.start_link([]) 3 | 4 | ExUnit.start() 5 | -------------------------------------------------------------------------------- /test/wallaby/browser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.BrowserTest do 2 | use ExUnit.Case 3 | 4 | import Wallaby.SettingsTestHelpers 5 | 6 | alias Wallaby.Browser 7 | alias Wallaby.NoBaseUrlError 8 | alias Wallaby.Session 9 | 10 | defmodule TestDriver do 11 | use Agent 12 | 13 | def start_link(_opts) do 14 | Agent.start_link(fn -> [] end, name: __MODULE__) 15 | end 16 | 17 | def visit(%Session{} = session, path) do 18 | Agent.update(__MODULE__, fn visits -> 19 | [path | visits] 20 | end) 21 | 22 | session 23 | end 24 | 25 | def assert_visited(url) do 26 | unless Enum.member?(visits(), url) do 27 | raise ExUnit.AssertionError, 28 | """ 29 | #{inspect(url)} was not visited. 30 | 31 | Visited urls: 32 | #{visits() |> Enum.map_join("\n", fn v -> " #{inspect(v)}" end)} 33 | """ 34 | end 35 | end 36 | 37 | defp visits do 38 | Agent.get(__MODULE__, fn visits -> visits end) 39 | end 40 | end 41 | 42 | describe "visit/2" do 43 | setup do 44 | ensure_setting_is_reset(:wallaby, :base_url) 45 | start_supervised!(TestDriver) 46 | :ok 47 | end 48 | 49 | test "relative path without leading slash, base url with trailing slash" do 50 | Application.put_env(:wallaby, :base_url, "http://example.com/") 51 | 52 | session = session_for_driver(TestDriver) 53 | Browser.visit(session, "form.html") 54 | 55 | TestDriver.assert_visited("http://example.com/form.html") 56 | end 57 | 58 | test "relative path with leading slash, base url no trailing slash" do 59 | Application.put_env(:wallaby, :base_url, "http://example.com") 60 | 61 | session = session_for_driver(TestDriver) 62 | Browser.visit(session, "/form.html") 63 | 64 | TestDriver.assert_visited("http://example.com/form.html") 65 | end 66 | 67 | test "relative path without leading slash, base url no trailing slash" do 68 | Application.put_env(:wallaby, :base_url, "http://example.com") 69 | 70 | session = session_for_driver(TestDriver) 71 | Browser.visit(session, "form.html") 72 | 73 | TestDriver.assert_visited("http://example.com/form.html") 74 | end 75 | 76 | test "relative path with leading slash, base url trailing slash" do 77 | Application.put_env(:wallaby, :base_url, "http://example.com/") 78 | 79 | session = session_for_driver(TestDriver) 80 | Browser.visit(session, "/form.html") 81 | 82 | TestDriver.assert_visited("http://example.com/form.html") 83 | end 84 | 85 | test "relative path with leading slash, base url ending in /api" do 86 | Application.put_env(:wallaby, :base_url, "https://example.com:9090/api/") 87 | 88 | session = session_for_driver(TestDriver) 89 | Browser.visit(session, "/form.html?something=2") 90 | 91 | TestDriver.assert_visited("https://example.com:9090/api/form.html?something=2") 92 | end 93 | 94 | test "relative url when the base_url isn't configured" do 95 | Application.delete_env(:wallaby, :base_url) 96 | 97 | assert_raise NoBaseUrlError, fn -> 98 | session = session_for_driver(TestDriver) 99 | Browser.visit(session, "/form.html") 100 | end 101 | end 102 | 103 | test "absolute url when the base_url isn't configured" do 104 | Application.delete_env(:wallaby, :base_url) 105 | uri = "https://example.com:9090/api/form.html" 106 | 107 | session = session_for_driver(TestDriver) 108 | Browser.visit(session, uri) 109 | 110 | TestDriver.assert_visited(uri) 111 | end 112 | 113 | test "absolute url when the base_url is configured" do 114 | Application.put_env(:wallaby, :base_url, "http://example.org:5555/test") 115 | uri = "https://example.com:9090/api/form.html" 116 | 117 | session = session_for_driver(TestDriver) 118 | Browser.visit(session, uri) 119 | 120 | TestDriver.assert_visited(uri) 121 | end 122 | end 123 | 124 | describe "retry/2" do 125 | test "returns a valid result" do 126 | assert Browser.retry(fn -> {:ok, []} end) == {:ok, []} 127 | end 128 | 129 | test "it retries if the dom element is stale" do 130 | {:ok, agent} = Agent.start_link(fn -> {:error, :stale_reference} end) 131 | 132 | run_query = fn -> 133 | Agent.get_and_update(agent, fn initial -> 134 | {initial, {:ok, []}} 135 | end) 136 | end 137 | 138 | assert Browser.retry(run_query) 139 | end 140 | 141 | test "it retries until time runs out" do 142 | assert Browser.retry(fn -> {:error, :some_error} end) == {:error, :some_error} 143 | end 144 | end 145 | 146 | defp session_for_driver(driver) do 147 | %Session{driver: driver} 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /test/wallaby/chrome/logger_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Chrome.LoggerTest do 2 | use ExUnit.Case, async: false 3 | 4 | alias Wallaby.Chrome.Logger 5 | import ExUnit.CaptureIO 6 | alias Wallaby.SettingsTestHelpers 7 | 8 | describe "parse_log/1" do 9 | test "removes line numbers from the end of INFO logs" do 10 | fun = fn -> 11 | build_log(~s(http://localhost:52615/logs.html 13:14 "test")) 12 | |> Logger.parse_log() 13 | end 14 | 15 | assert capture_io(fun) == "test\n" 16 | end 17 | 18 | test "prints non-string data types" do 19 | fun = fn -> 20 | build_log(~s(http://localhost:52615/logs.html 13:14 1)) 21 | |> Logger.parse_log() 22 | end 23 | 24 | assert capture_io(fun) == "1\n" 25 | 26 | fun = fn -> 27 | build_log(~s[http://localhost:52615/logs.html 13:14 Array(2)]) 28 | |> Logger.parse_log() 29 | end 30 | 31 | assert capture_io(fun) == "Array(2)\n" 32 | end 33 | 34 | test "prints messages with embedded newlines" do 35 | fun = fn -> 36 | build_log(~s[http://localhost:52615/logs.html 13:14 "test\nlog"]) 37 | |> Logger.parse_log() 38 | end 39 | 40 | assert capture_io(fun) == "\"test\nlog\"\n" 41 | end 42 | 43 | test "pretty prints json" do 44 | message = 45 | ~s(http://localhost:54579/logs.html 13:14 "{\"href\":\"http://localhost:54579/logs.html\",\"ancestorOrigins\":{}}") 46 | 47 | fun = fn -> 48 | message 49 | |> build_log 50 | |> Logger.parse_log() 51 | end 52 | 53 | assert capture_io(fun) == 54 | "\n{\n \"ancestorOrigins\": {},\n \"href\": \"http://localhost:54579/logs.html\"\n}\n" 55 | end 56 | 57 | test "can be disabled" do 58 | SettingsTestHelpers.ensure_setting_is_reset(:wallaby, :js_logger) 59 | Application.put_env(:wallaby, :js_logger, nil) 60 | 61 | fun = fn -> 62 | "test log" 63 | |> build_log() 64 | |> Logger.parse_log() 65 | end 66 | 67 | assert capture_io(fun) == "" 68 | end 69 | end 70 | 71 | def build_log(msg) do 72 | %{"level" => "INFO", "source" => "console-api", "message" => msg} 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/wallaby/driver/temporary_path_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Driver.TemporaryPathTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Wallaby.Driver.TemporaryPath 5 | 6 | describe "generate/1" do 7 | test "generates a temporary path in System.tmp_dir! by default" do 8 | assert TemporaryPath.generate() =~ ~r(^#{System.tmp_dir()}) 9 | end 10 | 11 | test "generates a temporary path in the given base dir" do 12 | base_dir = "/srv/wallaby" 13 | 14 | assert TemporaryPath.generate(base_dir) =~ ~r(^#{base_dir}) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/wallaby/driver/utils_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Driver.UtilsTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Wallaby.Driver.Utils 5 | 6 | describe "find_available_port/0" do 7 | test "returns an unused port" do 8 | port = Utils.find_available_port() 9 | 10 | assert port >= 0 11 | assert port <= 65535 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/wallaby/helpers/key_codes_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Helpers.KeyCodesTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Wallaby.Helpers.KeyCodes 5 | 6 | test "encoding unicode values as JSON" do 7 | assert json([:enter]) == "{\"value\": [\"\\uE007\"]}" 8 | assert json([:shift, :enter]) == "{\"value\": [\"\\uE008\",\"\\uE007\"]}" 9 | end 10 | 11 | test "encoding values with strings as JSON" do 12 | assert json(["te", :enter]) == "{\"value\": [\"t\",\"e\",\"\\uE007\"]}" 13 | end 14 | 15 | test "ensuring chars returns list of strings" do 16 | assert chars([:enter]) == ["\\uE007"] 17 | assert chars(:enter) == ["\\uE007"] 18 | assert chars([:shift, :enter]) == ["\\uE008", "\\uE007"] 19 | assert chars(["John", :tab, "Smith"]) == ["John", "\\uE004", "Smith"] 20 | end 21 | 22 | test "ensuring chars returns file paths as file paths" do 23 | assert chars("/some/path/to/foo.txt") == ["/some/path/to/foo.txt"] 24 | files = ["/path/to/foo.txt", "/path/to/bar.txt"] 25 | assert chars(files) == files 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/wallaby/http_client_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.HTTPClientTest do 2 | use Wallaby.HttpClientCase, async: true 3 | 4 | alias Wallaby.HTTPClient, as: Client 5 | 6 | describe "request/4" do 7 | test "sends the request with the correct params and headers", %{bypass: bypass} do 8 | Bypass.expect(bypass, fn conn -> 9 | conn = parse_body(conn) 10 | assert conn.method == "POST" 11 | assert conn.request_path == "/my_url" 12 | assert conn.body_params == %{"hello" => "world"} 13 | assert get_req_header(conn, "accept") == ["application/json"] 14 | assert get_req_header(conn, "content-type") == ["application/json;charset=UTF-8"] 15 | 16 | send_json_resp(conn, 200, %{ 17 | "sessionId" => "abc123", 18 | "status" => 0, 19 | "value" => nil 20 | }) 21 | end) 22 | 23 | assert {:ok, _} = Client.request(:post, bypass_url(bypass, "/my_url"), %{hello: "world"}) 24 | end 25 | 26 | test "with a 200 status response", %{bypass: bypass} do 27 | Bypass.expect(bypass, fn conn -> 28 | send_json_resp(conn, 200, %{ 29 | "sessionId" => "abc123", 30 | "status" => 0, 31 | "value" => nil 32 | }) 33 | end) 34 | 35 | {:ok, response} = Client.request(:post, bypass_url(bypass, "/my_url")) 36 | 37 | assert response == %{ 38 | "sessionId" => "abc123", 39 | "status" => 0, 40 | "value" => nil 41 | } 42 | end 43 | 44 | test "with a 500 response and StaleElementReferenceException", %{bypass: bypass} do 45 | Bypass.expect(bypass, fn conn -> 46 | send_json_resp(conn, 500, %{ 47 | "sessionId" => "abc123", 48 | "status" => 10, 49 | "value" => %{ 50 | "class" => "org.openqa.selenium.StaleElementReferenceException" 51 | } 52 | }) 53 | end) 54 | 55 | assert {:error, :stale_reference} = Client.request(:post, bypass_url(bypass, "/my_url")) 56 | end 57 | 58 | test "with an obscure status code", %{bypass: bypass} do 59 | expected_message = "message from an obscure error" 60 | 61 | Bypass.expect(bypass, fn conn -> 62 | send_json_resp(conn, 200, %{ 63 | "sessionId" => "abc123", 64 | "status" => 13, 65 | "value" => %{ 66 | "message" => "#{expected_message}" 67 | } 68 | }) 69 | end) 70 | 71 | assert {:error, ^expected_message} = Client.request(:post, bypass_url(bypass, "/my_url")) 72 | end 73 | 74 | test "includes the original HTTPoison error when there is one", %{bypass: bypass} do 75 | expected_message = 76 | if Version.compare(System.version(), "1.16.0") in [:eq, :gt] do 77 | "Wallaby had an internal issue with HTTPoison:\n%HTTPoison.Error{reason: :econnrefused, id: nil}" 78 | else 79 | "Wallaby had an internal issue with HTTPoison:\n%HTTPoison.Error{id: nil, reason: :econnrefused}" 80 | end 81 | 82 | Bypass.down(bypass) 83 | 84 | assert_raise RuntimeError, expected_message, fn -> 85 | Client.request(:post, bypass_url(bypass, "/my_url")) 86 | end 87 | end 88 | 89 | test "raises a runtime error when the request returns a generic error", %{bypass: bypass} do 90 | expected_message = "The session could not be created" 91 | 92 | Bypass.expect(bypass, fn conn -> 93 | send_json_resp(conn, 200, %{ 94 | "sessionId" => "abc123", 95 | "value" => %{ 96 | "error" => "An error", 97 | "message" => "#{expected_message}" 98 | } 99 | }) 100 | end) 101 | 102 | assert_raise RuntimeError, expected_message, fn -> 103 | Client.request(:post, bypass_url(bypass, "/my_url")) 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /test/wallaby/metadata_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.MetadataTest do 2 | end 3 | -------------------------------------------------------------------------------- /test/wallaby/query/xpath_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.XPathTest do 2 | use ExUnit.Case 3 | end 4 | -------------------------------------------------------------------------------- /test/wallaby/query_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.QueryTest do 2 | use ExUnit.Case, async: true 3 | doctest Wallaby.Query 4 | 5 | alias Wallaby.Query 6 | 7 | describe "default count" do 8 | test "the count defaults to 1 if no count is specified" do 9 | query = Query.css(nil) 10 | assert Query.count(query) == 1 11 | 12 | query = Query.css(nil, count: 1) 13 | assert Query.count(query) == 1 14 | 15 | query = Query.css(nil, count: 3) 16 | assert Query.count(query) == 3 17 | end 18 | 19 | test "the count is nil if a minimum or maximum is set" do 20 | query = Query.css(nil, minimum: 1) 21 | assert Query.count(query) == nil 22 | assert query.conditions[:minimum] == 1 23 | 24 | query = Query.css(nil, maximum: 1) 25 | assert Query.count(query) == nil 26 | assert query.conditions[:maximum] == 1 27 | end 28 | end 29 | 30 | describe "matches_count?/1" do 31 | test "the results must match exactly if the count key is specified" do 32 | query = %Query{conditions: [count: 0]} 33 | assert Query.matches_count?(query, 0) 34 | 35 | query = %Query{conditions: [count: 1]} 36 | assert Query.matches_count?(query, 1) 37 | 38 | query = %Query{conditions: [count: 1]} 39 | refute Query.matches_count?(query, 0) 40 | 41 | query = %Query{conditions: [count: 1]} 42 | refute Query.matches_count?(query, 2) 43 | end 44 | 45 | test "the count key overrides other matching strategies" do 46 | query = %Query{conditions: [count: 1, minimum: 2], result: [%{}]} 47 | assert Query.matches_count?(query, 1) 48 | 49 | query = %Query{conditions: [count: 1, minimum: 4, maximum: 2], result: [%{}]} 50 | assert Query.matches_count?(query, 1) 51 | end 52 | 53 | test "the count must be above the minimum" do 54 | query = %Query{conditions: [minimum: 1], result: [%{}, %{}]} 55 | assert Query.matches_count?(query, 2) 56 | 57 | query = %Query{conditions: [minimum: 2], result: [%{}]} 58 | refute Query.matches_count?(query, 1) 59 | end 60 | 61 | test "the count must be below the maximum" do 62 | query = %Query{conditions: [maximum: 3], result: [%{}, %{}]} 63 | assert Query.matches_count?(query, 2) 64 | 65 | query = %Query{conditions: [maximum: 1], result: [%{}, %{}]} 66 | refute Query.matches_count?(query, 2) 67 | end 68 | 69 | test "the count must match minimum and maximum filters" do 70 | query = %Query{ 71 | conditions: [minimum: 1, maximum: 3], 72 | result: [%{}, %{}] 73 | } 74 | 75 | assert Query.matches_count?(query, 2) 76 | 77 | query = %Query{conditions: [minimum: 1, maximum: 1], result: [%{}]} 78 | assert Query.matches_count?(query, 1) 79 | 80 | query = %Query{conditions: [minimum: 1, maximum: 1], result: [%{}, %{}]} 81 | refute Query.matches_count?(query, 2) 82 | end 83 | 84 | test "the result is greater than zero if count is any" do 85 | query = %Query{conditions: [count: :any], result: [%{}]} 86 | assert Query.matches_count?(query, 1) 87 | 88 | query = %Query{conditions: [count: :any], result: []} 89 | refute Query.matches_count?(query, 0) 90 | end 91 | end 92 | 93 | describe "validate/1" do 94 | test "when minimum is less than the maximum" do 95 | query = Query.css("#test", minimum: 5, maximum: 3) 96 | assert Query.validate(query) == {:error, :min_max} 97 | end 98 | end 99 | 100 | describe "visible/2" do 101 | test "marks query as visible when true is passed" do 102 | query = 103 | Query.css("#test", visible: false) 104 | |> Query.visible(true) 105 | 106 | assert Query.visible?(query) 107 | end 108 | 109 | test "marks query as hidden when false is passed" do 110 | query = 111 | Query.css("#test", visible: true) 112 | |> Query.visible(false) 113 | 114 | refute Query.visible?(query) 115 | end 116 | end 117 | 118 | describe "selected/2" do 119 | test "marks query as selected when true is passed" do 120 | query = 121 | Query.css("#test", selected: false) 122 | |> Query.selected(true) 123 | 124 | assert Query.selected?(query) 125 | end 126 | 127 | test "marks query as unselected when false is passed" do 128 | query = 129 | Query.css("#test", selected: true) 130 | |> Query.selected(false) 131 | 132 | refute Query.selected?(query) 133 | end 134 | end 135 | 136 | describe "text/2 when a query is passed" do 137 | test "sets the text option of the query" do 138 | query = 139 | Query.css("#test") 140 | |> Query.text("Submit") 141 | 142 | assert Query.inner_text(query) == "Submit" 143 | end 144 | end 145 | 146 | describe "text/2 when a selector is passed" do 147 | test "creates a text query" do 148 | query = Query.text("Submit") 149 | 150 | assert query.method == :text 151 | end 152 | 153 | test "accepts options" do 154 | query = Query.text("Submit", count: 1) 155 | 156 | assert query.method == :text 157 | assert Query.count(query) == 1 158 | end 159 | end 160 | 161 | describe "count/2" do 162 | test "sets the count in a query" do 163 | query = 164 | Query.css(".test") 165 | |> Query.count(9) 166 | 167 | assert Query.count(query) == 9 168 | end 169 | end 170 | 171 | describe "at/2" do 172 | test "sets at option in a query and count defaults to nil" do 173 | query = 174 | Query.css(".test") 175 | |> Query.at(3) 176 | 177 | assert Query.at_number(query) == 3 178 | assert Query.count(query) == nil 179 | end 180 | 181 | test "maintains count if it was specified" do 182 | query = 183 | Query.css(".test", count: 1) 184 | |> Query.at(0) 185 | 186 | assert Query.at_number(query) == 0 187 | assert Query.count(query) == 1 188 | end 189 | end 190 | 191 | describe "at option" do 192 | test "sets at option in a query and count defaults to nil" do 193 | query = Query.css(".test", at: 3) 194 | 195 | assert Query.at_number(query) == 3 196 | assert Query.count(query) == nil 197 | end 198 | 199 | test "maintains count if it was specified" do 200 | query = Query.css(".test", count: 1, at: 0) 201 | 202 | assert Query.at_number(query) == 0 203 | assert Query.count(query) == 1 204 | end 205 | end 206 | end 207 | -------------------------------------------------------------------------------- /test/wallaby/selenium/start_session_config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.Selenium.StartSessionConfigTest do 2 | use Wallaby.HttpClientCase, async: false 3 | 4 | # These tests modify the application environment so need 5 | # to be run with async: false 6 | 7 | alias Wallaby.Selenium 8 | alias Wallaby.Session 9 | alias Wallaby.SettingsTestHelpers 10 | alias Wallaby.TestSupport.JSONWireProtocolResponses 11 | 12 | setup do 13 | SettingsTestHelpers.ensure_setting_is_reset(:wallaby, :selenium) 14 | end 15 | 16 | test "starting a session uses default capabilities when none is set", %{bypass: bypass} do 17 | remote_url = bypass_url(bypass, "/") 18 | Application.delete_env(:wallaby, :selenium) 19 | 20 | Bypass.expect(bypass, "POST", "/session", fn conn -> 21 | conn = parse_body(conn) 22 | 23 | assert conn.body_params == %{ 24 | "desiredCapabilities" => %{ 25 | "browserName" => "firefox", 26 | "moz:firefoxOptions" => %{ 27 | "args" => ["-headless"], 28 | "prefs" => %{ 29 | "general.useragent.override" => 30 | "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36" 31 | } 32 | } 33 | } 34 | } 35 | 36 | response = JSONWireProtocolResponses.start_session_response() 37 | send_json_resp(conn, 200, response) 38 | end) 39 | 40 | assert {:ok, %Session{}} = Selenium.start_session(remote_url: remote_url) 41 | end 42 | 43 | test "starting a session reads capabilities from app env when set", %{bypass: bypass} do 44 | remote_url = bypass_url(bypass, "/") 45 | 46 | configured_capabilities = %{ 47 | browserName: "firefox", 48 | "moz:firefoxOptions": %{ 49 | args: ["-headless"] 50 | } 51 | } 52 | 53 | Application.put_env(:wallaby, :selenium, capabilities: configured_capabilities) 54 | 55 | Bypass.expect(bypass, "POST", "/session", fn conn -> 56 | conn = parse_body(conn) 57 | 58 | assert conn.body_params == %{ 59 | "desiredCapabilities" => %{ 60 | "browserName" => "firefox", 61 | "moz:firefoxOptions" => %{"args" => ["-headless"]} 62 | } 63 | } 64 | 65 | response = JSONWireProtocolResponses.start_session_response() 66 | send_json_resp(conn, 200, response) 67 | end) 68 | 69 | assert {:ok, %Session{}} = Selenium.start_session(remote_url: remote_url) 70 | end 71 | 72 | test "starting a session reads capabilities from opts and app env when set", %{bypass: bypass} do 73 | remote_url = bypass_url(bypass, "/") 74 | 75 | Application.put_env(:wallaby, :selenium, capabilities: %{}) 76 | 77 | capabilities_opt = %{ 78 | browserName: "firefox", 79 | "moz:firefoxOptions": %{ 80 | args: ["-headless"] 81 | } 82 | } 83 | 84 | Bypass.expect(bypass, "POST", "/session", fn conn -> 85 | conn = parse_body(conn) 86 | 87 | assert conn.body_params == %{ 88 | "desiredCapabilities" => %{ 89 | "browserName" => "firefox", 90 | "moz:firefoxOptions" => %{"args" => ["-headless"]} 91 | } 92 | } 93 | 94 | response = JSONWireProtocolResponses.start_session_response() 95 | send_json_resp(conn, 200, response) 96 | end) 97 | 98 | assert {:ok, %Session{}} = 99 | Selenium.start_session(remote_url: remote_url, capabilities: capabilities_opt) 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /test/wallaby/selenium_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.SeleniumTest do 2 | use Wallaby.HttpClientCase, async: true 3 | 4 | alias Wallaby.Selenium 5 | alias Wallaby.Session 6 | alias Wallaby.TestSupport.JSONWireProtocolResponses 7 | 8 | describe "start_session/1" do 9 | test "starts a selenium session with remote_url", %{bypass: bypass} do 10 | remote_url = bypass_url(bypass, "/") 11 | session_id = "abc123" 12 | 13 | Bypass.expect(bypass, "POST", "/session", fn conn -> 14 | response = JSONWireProtocolResponses.start_session_response(session_id: session_id) 15 | send_json_resp(conn, 200, response) 16 | end) 17 | 18 | assert {:ok, session} = Selenium.start_session(remote_url: remote_url) 19 | 20 | assert session == %Wallaby.Session{ 21 | session_url: remote_url |> URI.merge("session/#{session_id}") |> to_string(), 22 | url: remote_url |> URI.merge("session/#{session_id}") |> to_string(), 23 | id: session_id, 24 | server: :none, 25 | capabilities: Wallaby.Selenium.default_capabilities(), 26 | driver: Wallaby.Selenium 27 | } 28 | end 29 | 30 | test "raises a RuntimeError on unknown domain" do 31 | remote_url = "http://does.not.exist-asdf/" 32 | 33 | assert_raise RuntimeError, ~r/:nxdomain/, fn -> 34 | Selenium.start_session(remote_url: remote_url) 35 | end 36 | end 37 | 38 | test "raises a RuntimeError when unable to connect", %{bypass: bypass} do 39 | remote_url = bypass_url(bypass, "/") 40 | 41 | Bypass.down(bypass) 42 | 43 | assert_raise RuntimeError, ~r/:econnrefused/, fn -> 44 | Selenium.start_session(remote_url: remote_url) 45 | end 46 | end 47 | end 48 | 49 | describe "end_session/1" do 50 | test "returns :ok on success", %{bypass: bypass} do 51 | %Session{id: session_id} = 52 | session = 53 | bypass 54 | |> bypass_url("/") 55 | |> build_session() 56 | 57 | Bypass.expect_once(bypass, "DELETE", "/session/#{session_id}", fn conn -> 58 | response = %{"sessionId" => session_id, "value" => nil, "status" => 0} 59 | send_json_resp(conn, 200, response) 60 | end) 61 | 62 | assert :ok = Selenium.end_session(session) 63 | end 64 | 65 | test "returns :ok when unable to connect", %{bypass: bypass} do 66 | session = 67 | bypass 68 | |> bypass_url("/") 69 | |> build_session() 70 | 71 | Bypass.down(bypass) 72 | 73 | assert :ok = Selenium.end_session(session) 74 | end 75 | end 76 | 77 | defp build_session(remote_url) do 78 | session_id = random_string(24) 79 | session_url = remote_url |> URI.merge("session/#{session_id}") |> to_string() 80 | 81 | %Wallaby.Session{ 82 | session_url: session_url, 83 | url: session_url, 84 | id: session_id, 85 | driver: Wallaby.Selenium 86 | } 87 | end 88 | 89 | defp random_string(length) do 90 | :crypto.strong_rand_bytes(length) |> Base.url_encode64() |> binary_part(0, length) 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/wallaby/session_store_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Wallaby.SessionStoreTest do 2 | @moduledoc false 3 | use ExUnit.Case 4 | alias Wallaby.SessionStore 5 | alias Wallaby.Session 6 | 7 | use EventEmitter, :receiver 8 | 9 | setup do 10 | session_store = start_supervised!({SessionStore, [ets_name: :test_table]}) 11 | 12 | [session_store: session_store, table: :sys.get_state(session_store).ets_table] 13 | end 14 | 15 | describe "monitor/1" do 16 | test "adds session to the store", %{session_store: session_store, table: table} do 17 | assert [] == SessionStore.list_sessions_for(name: table) 18 | session = %Session{id: "foo"} 19 | :ok = SessionStore.monitor(session_store, session) 20 | 21 | assert [_] = SessionStore.list_sessions_for(name: table) 22 | end 23 | 24 | test "adds multiple sessions to store", %{session_store: session_store, table: table} do 25 | assert [] == SessionStore.list_sessions_for(name: table) 26 | sessions = [%Session{id: "foo"}, %Session{id: "bar"}] 27 | 28 | for session <- sessions do 29 | :ok = SessionStore.monitor(session_store, session) 30 | end 31 | 32 | store = SessionStore.list_sessions_for(name: table) 33 | 34 | for session <- sessions do 35 | assert Enum.member?(store, session) 36 | end 37 | end 38 | end 39 | 40 | describe "demonitor/1" do 41 | test "removes session from list of active sessions", %{ 42 | session_store: session_store, 43 | table: table 44 | } do 45 | session = %Session{id: "foo"} 46 | :ok = SessionStore.monitor(session_store, session) 47 | :ok = SessionStore.demonitor(session_store, session) 48 | 49 | assert [] == SessionStore.list_sessions_for(name: table) 50 | end 51 | 52 | test "removes a single session from the store", %{ 53 | session_store: session_store, 54 | table: table 55 | } do 56 | assert [] == SessionStore.list_sessions_for(name: table) 57 | first = %Session{id: "foo"} 58 | second = %Session{id: "bar"} 59 | third = %Session{id: "baz"} 60 | sessions = [first, second, third] 61 | 62 | for session <- sessions do 63 | :ok = SessionStore.monitor(session_store, session) 64 | end 65 | 66 | :ok = SessionStore.demonitor(session_store, second) 67 | 68 | store = SessionStore.list_sessions_for(name: table) 69 | 70 | assert Enum.member?(store, first) 71 | refute Enum.member?(store, second) 72 | assert Enum.member?(store, third) 73 | end 74 | end 75 | 76 | test "removes sessions when the monitored process dies", %{ 77 | session_store: session_store, 78 | table: table 79 | } do 80 | EventEmitter.add_handler(self()) 81 | 82 | # spawn some processes that monitor some sessions 83 | pids = 84 | for i <- 1..5 do 85 | spawn(fn -> 86 | for j <- 1..i do 87 | session = %Session{id: "session#{i}#{j}"} 88 | :ok = SessionStore.monitor(session_store, session) 89 | end 90 | 91 | receive do 92 | :done -> :ok 93 | end 94 | end) 95 | end 96 | 97 | # wait for each process to successfully monitor the sessions 98 | for i <- 1..5, 99 | j <- 1..i, 100 | do: 101 | await( 102 | :monitor, 103 | %{monitored_session: %Session{id: "session#{i}#{j}"}}, 104 | Wallaby.SessionStore 105 | ) 106 | 107 | # assert the correct number of sessions are being monitored 108 | sessions = 109 | for(pid <- pids, into: [], do: SessionStore.list_sessions_for(name: table, owner_pid: pid)) 110 | |> List.flatten() 111 | 112 | assert 15 == Enum.count(sessions) 113 | 114 | # end each process, causing them to send the DOWN message to store 115 | for pid <- pids, do: send(pid, :done) 116 | 117 | # wait for each DOWN message to be processed 118 | for i <- 1..5, 119 | j <- 1..i, 120 | do: 121 | await( 122 | :DOWN, 123 | %{monitored_session: %Session{id: "session#{i}#{j}"}}, 124 | Wallaby.SessionStore 125 | ) 126 | 127 | # assert there are no longer any sessions being monitored 128 | assert for( 129 | pid <- pids, 130 | into: [], 131 | do: SessionStore.list_sessions_for(name: table, owner_pid: pid) 132 | ) 133 | |> List.flatten() 134 | |> Enum.empty?() 135 | end 136 | end 137 | --------------------------------------------------------------------------------