boydm / phoenix_integration Goto Github PK
View Code? Open in Web Editor NEWLightweight server side integration test tools for Phoenix
License: Apache License 2.0
Lightweight server side integration test tools for Phoenix
License: Apache License 2.0
Hi!
I find this library very elegant and handy to make integration test readable without compromising simplicity and speed. Congratulations!
Today I came across a not covered scenario that maybe could be worthwhile to add to it. The idea is to allow asserting custom functions that take Plug.Conn
as first parameter.
I describe my scenario: I'm testing that a user signs in. With assert_response
there is no problem to assert that the name of the user is printed on screen, but I would like to really be sure that the user has been added to the session (or refute it, in an invalid attempt). As I'm using Guardian for that, now I have something like the following:
#...
defp assert_user_in_session(conn) do
assert Guardian.Plug.current_resource(conn)
conn
end
defp refute_user_in_session(conn) do
refute Guardian.Plug.current_resource(conn)
conn
end
describe "when credentials are valid" do
test "signs the user in", %{conn: conn} do
get(conn, session_path(conn, :new))
# sign_in is just a helper function that uses follow_form
|> sign_in("[email protected]", "secure_password")
|> assert_user_in_session
|> assert_response(
status: 200,
html: "[email protected]"
)
end
end
#...
Adding the functionality to this library, we could write something like:
get(conn, session_path(conn, :new))
|> sign_in("[email protected]", "secure_password")
|> assert_response(
status: 200,
html: "[email protected]",
# Ideally, accepting several `function` keys
function: fn(conn) -> Guardian.Plug.current_resource(conn) end
)
or
get(conn, session_path(conn, :new))
|> sign_in("[email protected]", "secure_password")
|> assert_response(
status: 200,
html: "[email protected]",
)
|> assert_function(
fn(conn) -> Guardian.Plug.current_resource(conn) end
)
What do you think? Maybe another noun other than function
could be used for the key/function name.
I'm not able to use this library with phoenix 1.3.0-rc.0. I've get the following error when running mix deps.get
:
Failed to use "phoenix" (version 1.3.0-rc.0) because
phoenix_integration (version 0.2.0) requires ~> 1.1 *
phoenix_live_reload (version 1.0.8) requires ~> 1.0 or ~> 1.2-rc
mix.lock specifies 1.3.0-rc.0
* This requirement does not match pre-releases. To match pre-releases include a pre-release in the requirement, such as: "~> 2.0-beta".
** (Mix) Hex dependency resolution failed, relax the version requirements of your dependencies or unlock them (by using mix deps.update or mix deps.unlock). If you are unable to resolve the conflicts you can try overriding with {:dependency, "~> 1.0", override: true}
Hi, thanks for this project, I wanted to use browser-less acceptance framework for a long time and I didn't know it was there all along!
I was trying to get to code from docs but it 404s because there's no v0.5.0 tag on GitHub. It would be great if you could add that in case anyone else runs into this problem in the future.
I've built a tool https://github.com/wojtekmach/shipit that is supposed to automate common steps in publishing a package. If this is something you'd consider using I'd appreciate any feedback!
I'm trying to use it in Phoenix 1.3.0-rc.0. I've added integration_case.ex
to my test/support
folder:
defmodule MyApp.Web.IntegrationCase do
use ExUnit.CaseTemplate
using do
quote do
use MyApp.Web.ConnCase
use PhoenixIntegration
end
end
end
and I wrote simple spec which look like this:
defmodule MyApp.AboutIntegrationTest do
use MyApp.Web.IntegrationCase, async: true
setup do
conn = build_conn()
{:ok, conn: conn}
end
test "Basic page flow", %{conn: conn} do
get( conn, project_path(conn, :index) )
|> follow_link( "MyApp )
|> assert_response( status: 200, path: project_path(conn, :index) )
end
end
When I try to run specs it returns me following error:
1) test Basic page flow (MyApp.AboutIntegrationTest)
test/integrations/first_test.exs:9
** (RuntimeError) no @endpoint set in test case
stacktrace:
(phoenix) lib/phoenix/test/conn_test.ex:214: Phoenix.ConnTest.dispatch/5
(phoenix_integration) lib/phoenix_integration/requests.ex:233: PhoenixIntegration.Requests.follow_link/3
test/integrations/first_test.exs:11: (test)
What I'm doing wrong?
Hi! I have html like this:
<button
class="btn btn-info btn-block"
data-csrf="csrf=="
data-method="post"
data-to="/test?level=test"
role="button">Test</button>
As I understand follow_link
and click_link
maethods works only with <a>
tags. (Floki.find(html, "a")
)
Is there any way to follow the buttons?
I can make PR, if this functionality is missing.
Thanks all!
I really like this library -- it's so much nicer with |>
. I have just got started with phoenix, so excuse me while still learning basics!
So my confusion: in setup, Step 3 you say:
Alternately you could place the call to use PhoenixIntegration in your conn_case.ex file.
Just make sure it is after the definition of @endpoint.
Ignoring the use PhoenixIntegration
point, given that there is an @endpoint definition in conn_case.ex, why is it necessary to do it again under config/test.exs for :phonix_integration (as stated in "Step 1"?)
I tried without that config, and everything seems to work ok...
I have an API endopoint expecting a JSON payload in the body. We are trying to use the follow_path/3 but cannot seem to find a way of adding the request body without treating it as url params.
We are getting the following error if we include the request as params
Postgrex expected a binary, got 2. Please make sure the value you are passing matches the definition in your table or in your query or convert the value accordingly.
We feel if we are able to send the parameters as the request body it will be handled correctly by phoenix.
Despite the failure above, when we test the endpoints manually it works okay
conn
|> follow_path(Routes.api_resource_path(conn, :create, valid_attrs), method: :post)
|> assert_response(json: valid_response())
Since phoenix_html 2.10, link
with :method
option other than get does not generate an inline form anymore, so follow_link
function should be updated to work with it.
This is the commit which introduced the change:
Not sure if I'm doing something wrong or that this is a bug, but I am passing an authenticated conn struct into a test pipeline (conn.assigns.current_user is set). This works fine on the first assertion, but when I try to continue the path to the next page it wipes the current_user...
Here is the test:
test "Basic page flow", %{conn: conn, student: student} do
conn
|> get(Routes.student_path(conn, :index))
|> follow_link(student.first_name)
|> IO.inspect # => at this point conn.assigns.current_user is nil
|> assert_response(status: 200, path: Routes.student_path(conn, :show, student))
end
I have tracked it down to here:
Basically that return does not keep the current_user intact. I am not sure what is happening, but it seems it might have something to do with the macro implementation in ConnTest.
Hope you are able to understand and track it down. If you have pointers for me on how to fix it, I'd be happy to contribute!
Thanks!
Tjaco
Hi!
I'm trying to follow a search form. It is a form with :get
method pointing to index action.
If I do:
conn
|> get(event_path(conn, :index))
|> follow_form(%{search: %{
source_eq: "web"
}}, identifier: event_path(conn, :index))
It gets confused with the create
action:
** (Phoenix.ActionClauseError) could not find a matching Eventbox.Web.EventController.create clause
to process request. This typically happens when there is a
parameter mismatch but may also happen when any of the other
action arguments do not match. The request parameters are:
%{"_utf8" => "✓", "search" => %{"category_id_eq" => nil, "client_type_id_eq" => nil, "lounge_id_eq" => nil, "people_eq" => nil, "source_eq" =>
"web", "start_datetime_gte" => nil, "start_datetime_lte" => nil, "status_eq" => nil, "subcategory_id_eq" => nil}}
While if I specify the method:
conn
|> sign_in_user(user)
|> get(event_path(conn, :index))
|> follow_form(%{search: %{
source_eq: "web"
}}, identifier: event_path(conn, :index), method: :get)
It doesn't find the form:
** (RuntimeError) Failed to find form "/events", :get in the response
Expected to find a form with action="/events"
Thanks!
I have a use case where form has multiple buttons each of them pointing at the same controller action but each with a separate name and value:
<button class="btn btn-sm btn-danger" id="mark-released" name="action[is_released]" type="submit" value="true">Release</button>
<button class="btn btn-sm btn-danger" id="mark-released" name="action[is_deployed]" type="submit" value="true">Deployed</button>
In my tests, with the current implementation of follow_form
I can never get the value of the button to be set as:
follow_form(conn, %{
action: %{is_deployed: "true"},
metadata: %{"#{version0.id}": "true", "#{version1.id}": "true"},
},
We currently use v0.7.0 but if this is of interest I'd be willing to submit a patch to 0.7.0 and higher versions.
From a quick look at it in 0.7.0, it requires adding some lines in request.ex
defp get_form_data(form) do
%{}
|> build_form_by_type(form, "input")
|> build_form_by_type(form, "textarea")
|> build_form_by_type(form, "select")
|> build_form_by_type(form, "button")
end
...
defp get_input_value(input, "button") do
Floki.attribute(input, "value")
end
I can see this would be very different for current HEAD, so I wanted to check before jumping into the code currently in master.
Hi,
First of all, thanks for this awesome library, we use it on a few projects and it helps a lot, also thanks @marick for the amazing work refactoring the way it parses the form tree.
I have all tests green on 0.7.0
and the application works as intended, but when updating to 0.8.2
I started getting the following error:
Error: You are combining list and scalar values.
The value you want to use is a list:
["ci"]
But the name of the tag doesn't end in `[]`:
"ab_test[environments]"
Form action: "/manage/config/games/VGIIQQZCKNSNVRYN/ab_tests"
The form itself has this rendered HTML:
<div>
<input id="ab_test_environments" name="ab_test[environments]" type="hidden" value="" />
<label class="checkbox-inline">
<input data-persisted="false" name="ab_test[environments][]" type="checkbox" value="ci" checked="" />
Ci
</label>
<label class="checkbox-inline">
<input data-persisted="false" name="ab_test[environments][]" type="checkbox" value="staging" checked="" />
Staging
</label>
<label class="checkbox-inline">
<input data-persisted="false" name="ab_test[environments][]" type="checkbox" value="production" />
Production
</label>
</div>
What this code accomplishes is when we have at least one selected checkbox the Phoenix controller receives an array like ["ci", "staging"]
that Ecto does understand, and when there is none selected it receives an empty string that Ecto does transform automatically into an empty array when creating the Changeset.
I've IO.inspect
'ed a bit into the library code and it seams like the multiple tags are not merged, probably because the name differ(with and without []
), and the checkbox tags are simply ignored.
If we change the name of the hidden field to match the other inputs this error is fixed, but the functionality is broken since it will actually always send the empty string as the first element of the array(["", "ci", "staging"]
) and Ecto does not understand that.
I could remove the first element of the array, but that would require also changing a lot of validations around it, and it feels wrong to change so much battle tested production code to fix a test.
Any other ideas on how I could fix this while keeping the same behaviour?
Or could it be possible to backport #42 to 0.7.x in order to have it compatible with phoenix 1.5?
Sorry for ask this here, I'm not sure if I need anything else in my github action workflow, the whole integration tests fails.
I really like phoenix_integration
for integration tests, I really would appreciate If anyone can give me a good approach to run it using github actions.
1) test Try to login with invalid company (Web.CompanyLoginTest)
apps/web/test/web/integration/company_login_test.exs:7
** (RuntimeError) Failed to find link "Mis ofertas", :get in the response
Expected to find an anchor with text containing "Mis ofertas"
code: |> follow_link("Mis ofertas")
stacktrace:
(phoenix_integration) lib/phoenix_integration/requests.ex:773: PhoenixIntegration.Requests.find_html_link/3
(phoenix_integration) lib/phoenix_integration/requests.ex:193: PhoenixIntegration.Requests.click_link/3
(phoenix_integration) lib/phoenix_integration/requests.ex:255: PhoenixIntegration.Requests.follow_link/3
test/web/integration/company_login_test.exs:9: (test)
name: Continuous Integration
on: [push]
# This workflow will build and test a Phoenix application that uses
# PostgreSQL as a database. Will also cache dependencies and build
# artifacts to speed up the build.
jobs:
test:
env:
MIX_ENV: test
TEST_DATABASE_NAME: postgres
TEST_DATABASE_USERNAME: postgres
TEST_DATABASE_PASSWORD: postgres
TEST_DATABASE_HOSTNAME: localhost
TEST_DATABASE_PORT: 5432
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x]
otp-version: [22.2]
elixir-version: [1.9.4]
services:
db:
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
image: postgres:11
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/[email protected]
- name: Recover dependency cache
uses: actions/cache@v1
id: deps_cache
with:
path: deps
key: ${{ runner.OS }}-deps-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.OS }}-deps-${{ env.cache-name }}-
${{ runner.OS }}-deps-
${{ runner.OS }}-
- name: Copy config file
run: cp config/env.local.exs.example config/env.local.exs
- name: Recover build cache
uses: actions/cache@v1
with:
path: _build
key: ${{ runner.OS }}-build-${{ env.cache-name }}
restore-keys: |
${{ runner.OS }}-build-
${{ runner.OS }}-
- uses: actions/[email protected]
with:
otp-version: ${{ matrix.otp-version }}
elixir-version: ${{ matrix.elixir-version }}
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
run: mix local.rebar --force
- run: mix local.hex --force
- run: mix deps.get
- run: mix compile
- run: mix ecto.migrate
- run: npm install --prefix ./apps/web/assets
- run: npm run deploy --prefix ./apps/web/assets
- name: Run tests
run: mix test --trace
https://github.com/philss/floki version 0.24.0 emits deprecations on PhoenixIntegration v0.6
warning: deprecation: parse the HTML with parse_document or parse_fragment before using find/2
lib/floki.ex:170: Floki.find/2
(phoenix_integration) lib/phoenix_integration/requests.ex:675: PhoenixIntegration.Requests.find_html_link/3
(phoenix_integration) lib/phoenix_integration/requests.ex:189: PhoenixIntegration.Requests.click_link/3
(phoenix_integration) lib/phoenix_integration/requests.ex:251: PhoenixIntegration.Requests.follow_link/3
...
Elixir 1.10 added Kernel.is_struct/1
==> phoenix_integration
Compiling 3 files (.ex)
warning: function is_struct/1 is unused
lib/phoenix_integration/requests.ex:1159
== Compilation error in file lib/phoenix_integration/requests.ex ==
** (CompileError) lib/phoenix_integration/requests.ex:1159: imported Kernel.is_struct/1 conflicts with local function
(elixir 1.10.0) src/elixir_locals.erl:94: :elixir_locals."-ensure_no_import_conflict/3-lc$^0/1-0-"/2
(elixir 1.10.0) src/elixir_locals.erl:95: anonymous fn/3 in :elixir_locals.ensure_no_import_conflict/3
(stdlib 3.11.1) erl_eval.erl:680: :erl_eval.do_apply/6
Given the following form and test:
# Form ----
<%= form_for @changeset, @action, fn f -> %>
<%= label f, :email %>
<%= email_input f, :email %>
<%= error_tag f, :email %>
<div>
<%= submit "Save" %>
</div>
<% end %>
# Test ----
test "create user", %{conn: conn} do
get(conn, Routes.user_path(conn, :new))
|> follow_form(%{user: %{email: "[email protected]"}})
end
Will fail with this error:
Error: You tried to set the value of a tag that isn't in the form.
Path tried: [:user, :email]
Is this a typo?: :user
Your value: "[email protected]"
Form action: "/users"
Changing the email_input
in the form to a normal text_input
and running the test again works.
email_input
is just a normal Phoenix tag that sets the type
of the HTML input to "email"
instead of "text"
.
Hi, I have a JSON API implemented with ja_serializer that uses the content-type "application/vnd.api+json" as in the JSON:API spec.
Is there a way to make this work with phoenix_integration
so I can use asserts like assert_response(json: "...")
?
Currently this fails as it expects a content type of application/json
.
Minor bug; I'll try to have a look at it soonish
Consider the following and note that the name
of the second and third line ends in []
.
<input name="procedures[0][name]" type="text" value="">
<input name="procedures[0][species_ids][]" type="checkbox" value="1">
<input name="procedures[0][species_ids][]" type="checkbox" value="2">
That means a value is submitted with something like this:
|> follow_form(%{procedures:
%{0 => %{name: "procedure #1",
species_ids: [1]}
}})
That works fine. However, I would expect the result for fetch_form
to look something like this:
inputs: %{
_csrf_token: "XCgQR0wwBREzPykwC2Y3egtGIhp0HioOegs4xQORPuldT9s6l7O-CvsZ",
procedures: %{
"0": %{name: "",
species_ids: []}, ... # <<<<<<
However, it looks like this:
inputs: %{
_csrf_token: "XCgQR0wwBREzPykwC2Y3egtGIhp0HioOegs4xQORPuldT9s6l7O-CvsZ",
procedures: %{
"0": %{name: ""}, ... # <<<<<<
I am having issues getting the follow_form function to work for my tests. Any help with getting it working would be greatly appreciated. I am getting the following error.
** (RuntimeError) Attempted to set missing input in form
Setting key: name
And value: CSP
Into fields: %{}
stacktrace:
lib/phoenix_integration/requests.ex:630: PhoenixIntegration.Requests.put_if_available!/3
(stdlib) lists.erl:1263: :lists.foldl/3
lib/phoenix_integration/requests.ex:612: anonymous fn/2 in PhoenixIntegration.Requests.merge_grouped_fields/2
(stdlib) lists.erl:1263: :lists.foldl/3
lib/phoenix_integration/requests.ex:290: PhoenixIntegration.Requests.submit_form/3
lib/phoenix_integration/requests.ex:340: PhoenixIntegration.Requests.follow_form/3
test/features/user_edits_a_project_feature_test.exs:14: (test)
This is my test
defmodule KitetimerElixir.EditProjectTest do
use KitetimerElixir.IntegrationCase, async: true
alias KitetimerElixir.User
alias KitetimerElixir.Project
test "edits a project", %{conn: conn} do
Repo.insert!(%Project{id: 1, name: "2RK"}
login(conn)
|> follow_link(project_path(conn, :index))
|> follow_link(project_path(conn, :show, 1))
|> follow_link(project_path(conn, :edit, 1))
|> IO.inspect
|> follow_form(%{ project: %{
name: "CSP"
}})
|> assert_response(
status: 200,
path: project_path(conn, :show, 1),
html: "CSP" )
end
def login(conn) do
Repo.insert!(%User{id: 1, name: "Test User", email: "[email protected]", encrypted_password: Comeonin.Bcrypt.hashpwsalt("password")})
get(conn, session_path(conn, :new))
|> follow_form(%{ session: %{
email: "[email protected]",
password: "password"
}
})
end
end
Here is the result of an IO.inspect just before this I try to run follow_form
%Plug.Conn{adapter: {Plug.Adapters.Test.Conn, :...},
assigns: %{changeset: #Ecto.Changeset<action: nil, changes: %{},
errors: [uid: {"can't be blank", [validation: :required]}],
data: #KitetimerElixir.Project<>, valid?: false>,
layout: {KitetimerElixir.LayoutView, "app.html"},
project: %KitetimerElixir.Project{__meta__: #Ecto.Schema.Metadata<:loaded, "projects">,
id: 1, inserted_at: ~N[2017-02-01 10:52:05.763467], internal: false,
minimum_duration: nil, name: "2RK",
tickets: #Ecto.Association.NotLoaded<association :tickets is not loaded>,
uid: nil, updated_at: ~N[2017-02-01 10:52:05.769452]}},
before_send: [#Function<0.101282891/1 in Plug.CSRFProtection.call/2>,
#Function<4.45310566/1 in Phoenix.Controller.fetch_flash/2>,
#Function<0.61377594/1 in Plug.Session.before_send/2>,
#Function<1.70936004/1 in Plug.Logger.call/2>], body_params: %{},
cookies: %{"_kitetimer_elixir_key" => "SFMyNTY.g3QAAAACbQAAAAtfY3NyZl90b2tlbm0AAAAYbGtrL1FuakJqWlRQemw2ZW15QUR4dz09bQAAAAxjdXJyZW50X3VzZXJhAQ.d1bWLgR79pEuLgoLjFKgUDfsFcKaAnXJ3d6tqQ6fYAw"},
halted: false, host: "www.example.com", method: "GET", owner: #PID<0.450.0>,
params: %{"id" => "1"}, path_info: ["projects", "1", "edit"], path_params: %{},
peer: {{127, 0, 0, 1}, 111317}, port: 80,
private: %{KitetimerElixir.Router => {[], %{}}, :phoenix_action => :edit,
:phoenix_controller => KitetimerElixir.ProjectController,
:phoenix_endpoint => KitetimerElixir.Endpoint, :phoenix_flash => %{},
:phoenix_format => "html",
:phoenix_layout => {KitetimerElixir.LayoutView, :app},
:phoenix_pipelines => [:browser], :phoenix_recycled => false,
:phoenix_route => #Function<63.71185309/1 in KitetimerElixir.Router.match_route/4>,
:phoenix_router => KitetimerElixir.Router, :phoenix_template => "edit.html",
:phoenix_view => KitetimerElixir.ProjectView,
:plug_session => %{"_csrf_token" => "lkk/QnjBjZTPzl6emyADxw==",
"current_user" => 1}, :plug_session_fetch => :done,
:plug_skip_csrf_protection => true}, query_params: %{}, query_string: "",
remote_ip: {127, 0, 0, 1},
req_cookies: %{"_kitetimer_elixir_key" => "SFMyNTY.g3QAAAACbQAAAAtfY3NyZl90b2tlbm0AAAAYbGtrL1FuakJqWlRQemw2ZW15QUR4dz09bQAAAAxjdXJyZW50X3VzZXJhAQ.d1bWLgR79pEuLgoLjFKgUDfsFcKaAnXJ3d6tqQ6fYAw"},
req_headers: [{"cookie",
"_kitetimer_elixir_key=SFMyNTY.g3QAAAACbQAAAAtfY3NyZl90b2tlbm0AAAAYbGtrL1FuakJqWlRQemw2ZW15QUR4dz09bQAAAAxjdXJyZW50X3VzZXJhAQ.d1bWLgR79pEuLgoLjFKgUDfsFcKaAnXJ3d6tqQ6fYAw"},
{"content-type", "multipart/mixed; charset: utf-8"}],
request_path: "/projects/1/edit",
resp_body: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <meta name=\"description\" content=\"\">\n <meta name=\"author\" content=\"\">\n\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/semantic-ui/2.2.6/semantic.min.css\">\n\n <title>Kitetimer</title>\n <link rel=\"stylesheet\" href=\"/css/app.css\">\n</head>\n\n<body>\n<div class=\"ui small menu\">\n<a class=\"header item\" href=\"/\">KiteTimer</a> \n<a class=\"header item\" href=\"/\">Team Dashboard</a> <a class=\"item\" href=\"/users/1/personal_dashboard\">Test User</a>\n <div class=\"right menu\">\n<form action=\"/logout\" class=\"link\" method=\"post\"><input name=\"_method\" type=\"hidden\" value=\"delete\"><input name=\"_csrf_token\" type=\"hidden\" value=\"NjMaGBgjKHsSHD0WLgNMSlo2FCUCAAAAZXq7IMB9xFiFToz/7OUazw==\"><a class=\"item\" data-submit=\"parent\" href=\"#\" rel=\"nofollow\">Logout</a></form> </div>\n</div>\n\n<div class=\"ui fluid container\">\n <div class=\"ui two column stackable grid\">\n <div class=\"three wide column\">\n <div class=\"activating element\">\n <h3 id=\"running_timer\">\n Timer:\n Stopped\n </h3>\n</div>\n\n\n<div class=\"ui fluid vertical menu\">\n <div class=\"item\">\n <div class=\"header\" id=\"username\">\nTest User </div>\n <div class=\"menu\">\n<a class=\"item\" href=\"/users/1/personal_dashboard\">My Dashboard</a> <a class=\"item\" href=\"/users/1\">Test User</a>\n<a class=\"item\" href=\"/users/1/tickets\">Tickets</a> <a class=\"item\" href=\"/users/1/intervals\">Intervals</a>\n<a class=\"item\" href=\"/users/1/favorites\">Favourites</a> </div>\n </div>\n <div class=\"item\">\n <div class=\"menu\">\n </div>\n </div>\n <div class=\"item\">\n <div class=\"header\">Global Nav</div>\n <div class=\"menu\">\n<a class=\"item\" href=\"/users\">Users</a> <a class=\"item\" href=\"/tickets\">Tickets</a>\n<a class=\"item\" href=\"/favorites\">Favourites</a> <a class=\"item\" href=\"/intervals\">Intervals</a>\n<a class=\"item\" href=\"/projects\">Projects</a> <a class=\"item\" href=\"/tickets/new\">Import Tickets</a>\n </div>\n </div>\n</div>\n \n </div>\n <div class=\"thirteen wide column\">\n <p class=\"alert alert-info\" role=\"alert\"></p>\n <p class=\"alert alert-danger\" role=\"alert\"></p>\n\n \n\n<h2>Edit project</h2>\n\n<form accept-charset=\"UTF-8\" action=\"/projects/1\" class=\"ui form\" method=\"post\"><input name=\"_method\" type=\"hidden\" value=\"put\"><input name=\"_csrf_token\" type=\"hidden\" value=\"NjMaGBgjKHsSHD0WLgNMSlo2FCUCAAAAZXq7IMB9xFiFToz/7OUazw==\"><input name=\"_utf8\" type=\"hidden\" value=\"✓\"> \n <div class=\"field\">\n<label for=\"project_uid\">Uid</label> <input id=\"project_uid\" name=\"project[uid]\" type=\"text\">\n </div>\n\n <div class=\"field\">\n<label for=\"project_name\">Name</label> <input id=\"project_name\" name=\"project[name]\" type=\"text\" value=\"2RK\">\n </div>\n\n <div class=\"field\">\n<label for=\"project_internal\">Internal</label> <input name=\"project[internal]\" type=\"hidden\" value=\"false\"><input id=\"project_internal\" name=\"project[internal]\" type=\"checkbox\" value=\"true\">\n </div>\n\n <div class=\"field\">\n<label for=\"project_minimum_duration\">Minimum duration</label> <input id=\"project_minimum_duration\" name=\"project[minimum_duration]\" type=\"number\">\n </div>\n\n<button class=\"ui right floated positive primary button\" type=\"submit\">Submit</button></form>\n\n<a href=\"/projects\">Back</a> </div>\n </div>\n</div>\n<script src=\"/js/app.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/semantic-ui/2.2.6/semantic.min.js\"></script>\n</body>\n</html>\n\n",
resp_cookies: %{},
resp_headers: [{"content-type", "text/html; charset=utf-8"},
{"cache-control", "max-age=0, private, must-revalidate"},
{"x-request-id", "3vfbo56q3nnevc59upg8fbbbk8g8qlgg"},
{"x-frame-options", "SAMEORIGIN"}, {"x-xss-protection", "1; mode=block"},
{"x-content-type-options", "nosniff"}], scheme: :http, script_name: [],
secret_key_base: "OxKW6JNpNggIVPsHXNbwu/PzgwPJ72ZNqBYwyiQP5/Sivgx3bR1UHV39FRCqapQW",
state: :sent, status: 200}
@marick I'm thinking of adding a new validation api to extract all links on a give page into a list, then scan the list to make sure each link goes somewhere valid. Essentially a scan for broken links. I think it would be useful for blog posts and such where some of the links may be hand-built in markdown.
Thoughts?
When compiling the dependencies I got the following error
==> phoenix_integration
Compiling 3 files (.ex)
warning: function Poison.decode!/1 is undefined (module Poison is not available)
Found at 2 locations:
lib/phoenix_integration/assertions.ex:388
lib/phoenix_integration/assertions.ex:411
Later on when doing assertions for json
response I got the error shown below
** (UndefinedFunctionError) function Poison.decode!/1 is undefined (module Poison is not available)
Will this switch to using Jason or can Poison
be added as a dependency of this project. Let me know of your preferred solution so I can help 😄
Added the poison
library to my mix file for use on test environment only and now things are working
Just upgraded to Phoenix 1.5.1. Getting this warning
==> phoenix_integration
Compiling 10 files (.ex)
warning: Using Phoenix.ConnTest is deprecated, instead of:
use Phoenix.ConnTest
do:
import Plug.Conn
import Phoenix.ConnTest
lib/phoenix_integration/requests.ex:2: PhoenixIntegration.Requests (module)
Generated phoenix_integration app
I have checkboxes that I am perhaps creating wrongly - if so, what would be the right way?
<%= for a <- @animals do %>
<div class="field">
<div class="ui checkbox">
<%= checkbox(f, :animal_ids,
name: "#{input_name(:animals, :chosen_animal_ids)}[#{a.id}]") %>
In manual testing, the checkboxes work as expected. In particular, the checkboxes are unchecked when the form first shows.
However, fetch_form
shows both values starting as "true"
:
"chosen_animal_ids" => %{"237" => "true", "238" => "true"},
"transaction_key" => "0f10e52f-abc4-49b5-9e05-14658295742f"
}
(Perhaps this is because the "real" checkbox input comes after the hidden one?)
Further, I can't seem to change the value the form delivers. For example, suppose I do this:
follow_form(has_animal_form,
%{ animals:
%{chosen_animal_ids: %{:"#{bossie.id}" => "true" }}})
What's delivered to the controller is this:
%{
"chosen_animal_ids" => %{"239" => "true", "240" => "true"},
"transaction_key" => "a5743aa5-f1ce-42fe-ad12-1d44e97d86e1"
}
Is there a correct way to do this?
Hello!
I want to test a blog that paginates posts, showing 5 per page. I'd like to get the post list, and check that I can find the post title of the first 10 posts, something like this:
test "Blog post pagination", %{conn: conn} do
get(conn, "/blog")
|> assert_response(status, 200, path: "/blog", html: "Listing recent posts")
|> assert_response(html: "Post title 1")
|> assert_response(html: "Post title 2")
|> assert_response(html: "Post title 3")
|> assert_response(html: "Post title 4")
|> assert_response(html: "Post title 5")
end
Instead of this, it would be great if we could pass a list of strings to :html
, so we could do this:
test "Blog post pagination", %{conn: conn, posts: posts} do
get(conn, "/blog")
|> assert_response(status, 200, path: "/blog", html: "Listing recent posts")
|> assert_response(html: Enum.map(posts, & &1.title))
end
Or this, check for different strings explicitly:
test "Pricing plans", %{conn: conn} do
get(conn, "/pricing")
|> assert_response(status, 200,
path: "/pricing",
html: [
"Pricing",
"Starter plan",
"Pro plan",
"Enterprise plan"
]
)
end
Same for refute_response
.
What do you think?
First of all, I love this! Thanks for making it. It fills a Phoenix need that's been there for awhile.
I'd like to propose perhaps extracting this out to another package that would work with any plug-based application, similar to how capybara in Ruby land works with any Rack-based app.
A prerequisite to this would involve making a phoenix-agnostic alternative to Phoenix.ConnTest, which I've been considering creating. Perhaps this could also eventually replace Phoenix.ConnTest within Phoenix.
Do you have any thoughts on this?
follow_link("Some Text") when the method on the link isn't get crashes with an unhelpful error.
Would be nice if it raised with a nice helpful error suggesting the dev add the method option.
Hello,
I'm testing against some html with a regex using Regex.compile!
and there seems to be a bug where a valid regex will start to throw an error with the following error log:
** (Protocol.UndefinedError) protocol String.Chars not implemented for ~r/Team-oriented ReducedInc\s*blah/ of type Regex (a struct). This protocol is implemented for the following type(s): Phoenix.LiveComponent.CID, Postgrex.Copy, Postgrex.Query, Floki.Selector.AttributeSelector, Floki.Selector, Floki.Selector.Functional, Floki.Selector.Combinator, Floki.Selector.PseudoClass, Decimal, Float, DateTime, Time, List, Version.Requirement, Atom, Integer, Version, Date, BitString, NaiveDateTime, URI
code: |> assert_response(
stacktrace:
(elixir 1.11.3) lib/string/chars.ex:3: String.Chars.impl_for!/1
(elixir 1.11.3) lib/string/chars.ex:22: String.Chars.to_string/1
(phoenix_integration 0.8.2) lib/phoenix_integration/assertions.ex:437: PhoenixIntegration.Assertions.assert_body/3
(elixir 1.11.3) lib/enum.ex:798: Enum."-each/2-lists^foreach/1-0-"/2
(phoenix_integration 0.8.2) lib/phoenix_integration/assertions.ex:81: PhoenixIntegration.Assertions.assert_response/2
When I look at line 437 of assertions.ex I don't understand what's happening, or how it got there, or why it's in the stack trace at all. It should be going into assert_body_html
, and presumably it does when the very similar regex's pass the same test. To that point, a bunch of other versions of the Regex.compile!
will work, in particular: Regex.compile!("#{company_name}\\s*")
will work, but the minute I add anything after the *
, e.g., Regex.compile!("#{company_name}\\s*x")
it breaks.
The code which works is essentially:
conn |> follow_path("/") |> assert_response(html: Regex.compile!("#{company_name}\\s*"))
The code which will result in a stacktrace (instead of a pass/fail) is essentially:
conn |> follow_path("/") |> assert_response(html: Regex.compile!("#{company_name}\\s*x"))
The one difference is the x
after the *
. This is valid regex, of course, and will work in iex.
Thanks :D
Hey, thanks for this! I like having nice fast server side integration tests.
I was trying this out for the first time today, and I ran into an issue where I'm trying to use the follow_form/3
function, but it's not seeing any of my radio buttons as acceptable inputs. The
select and text inputs work just fine - it's just the radios that are weird.
Here's what I have in my test:
|> submit_form(%{
user: %{
num_students: "200",
num_schools: "12",
usage: "moderate",
locale: "urban",
contract_length: "5",
cost_of_hardware: "5000.0",
circuit_of_choice: "1000.0",
installation_method: "aerial"
}
})
and with that I'm getting the following error:
** (RuntimeError) Attempted to set missing input in form
Form action: /defaults
Setting key: usage
And value: moderate
Into fields: %{circuit_of_choice: "1000.0", contract_length: "5", cost_of_hardware: "5000.0", num_schools: "12", num_students: "200", result_type: "Calculate Defaults"}
Here's the HTML that I have for my form:
<form action="/ia/lease_build/defaults">
<span class="uppercase-title">About Your School District:</span>
<br>
<input class="text-input" type="text" name="user[num_students]" placeholder="Number of Students" required="">
<br>
<input class="text-input" type="text" name="user[num_schools]" placeholder="Number of Schools" required="">
<br>
<span class="form-title">Broadband Usage:</span>
<input type="radio" name="user[usage]" value="media_rich" required=""> Media Rich<br>
<input type="radio" name="user[usage]" value="moderate" required=""> Moderate<br>
<input type="radio" name="user[usage]" value="one_to_one" required=""> One to One<br>
<span class="form-title">School Locale:</span>
<input type="radio" name="user[locale]" value="rural" required=""> Rural - Distant/Remote<br>
<input type="radio" name="user[locale]" value="town" required=""> Town<br>
<input type="radio" name="user[locale]" value="urban" required=""> Urban/Suburban<br>
<span class="uppercase-title">ABOUT YOUR DESIRED CIRCUIT:</span>
<br>
<input class="text-input" type="number" name="user[contract_length]" placeholder="Contract Length (years)" required="">
<br>
<input class="text-input" type="text" name="user[cost_of_hardware]" placeholder="Cost of Hardware (per Circuit)" required="">
<br>
<span class="form-title">Circuit of Choice (Mbps):</span>
<select id="circut_of_choice_bandwidth" name="user[circuit_of_choice]" required="required"></select> <br>
<span class="form-title">Installation Method:</span>
<input type="radio" name="user[installation_method]" id="installation_method_underground_bored" value="underground_bored" required=""> Underground Bored<br>
<input type="radio" name="user[installation_method]" id="installation_method_aerial" value="aerial" required=""> Aerial / Existing Conduit<br>
<input type="radio" name="user[installation_method]" id="installation_method_underground_plowed" value="underground_plowed" required=""> Underground Plowed<br>
<br>
<input class="submit-button" name="user[result_type]" type="submit" value="Calculate Defaults">
<input class="clear-form-button" type="reset" value="Clear Form">
</form>
Hopefully this is just something I'm doing weird.
login_conn = get( conn, page_path(conn, :index) ) |> follow_link("Login", %{max_redirects: 10000}) |> follow_form(%{ login: %{ login: "abc", password: "test.password"}}, %{identifier: "#login_new_user", max_redirects: 10000}) |> follow_form(%{ tweet: %{ text: "hello"}}, %{identifier: "#send_tweets", max_redirects: 10000}) assert login_conn.status == 200
It gives me the following error
** (RuntimeError) expected action/2 to return a Plug.Conn, all plugs must receive a connection (conn) and return a connection
@boydm Do you have any idea on how to solve this problem? Thank you very much.
I'm setting a host in the setup
block like this:
setup do
conn = %{build_conn() | host: "foo.bar.dev"}
%{conn: conn}
end
However, this seems to not be respected when I call follow_redirect
. Is this a known issue?
Note: in a previous version of this lib there was a method called preserve_host
, which now seems gone.
I am currently trying to test a form that has a file upload field. Is there a way to test these form using the follow_form
function ?
I tried the following code:
upload = %Plug.Upload{content_type: "text", path: "mix.exs", filename: "mix.exs"}
get(conn, expense_path(conn, :index) )
|> follow_form(%{ expense: %{receipt: upload, description: "Cell"}})
|> assert_response(status: 200, path: expense_path(conn, :index), html: "Cell")
And I got the following message:
1) test Expense page flow (CieExpenses.ExpenseIntegrationTest)
test/integrations/expense_integration_test.exs:4
** (Protocol.UndefinedError) protocol Enumerable not implemented for %Plug.Upload{content_type: "text", filename: "mix.exs", path: "mix.exs"}
stacktrace:
(elixir) lib/enum.ex:1: Enumerable.impl_for!/1
(elixir) lib/enum.ex:116: Enumerable.reduce/3
(elixir) lib/enum.ex:1627: Enum.reduce/3
lib/phoenix_integration/requests.ex:612: anonymous fn/2 in PhoenixIntegration.Requests.merge_grouped_fields/2
(stdlib) lists.erl:1263: :lists.foldl/3
lib/phoenix_integration/requests.ex:612: anonymous fn/2 in PhoenixIntegration.Requests.merge_grouped_fields/2
(stdlib) lists.erl:1263: :lists.foldl/3
lib/phoenix_integration/requests.ex:290: PhoenixIntegration.Requests.submit_form/3
lib/phoenix_integration/requests.ex:340: PhoenixIntegration.Requests.follow_form/3
test/integrations/expense_integration_test.exs:15: (test)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.