-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Playwright driver (no deps) #145
Conversation
4be4ce7
to
01d51d4
Compare
8d6a378
to
bcb492f
Compare
bcb492f
to
fbfd376
Compare
In my eyes this is the most promising approach of the three so far (#136 , #74 #145). The amount of code required to interface with playwright via a @germsvel If you have time I'd appreciate hearing your thoughts. |
fbfd376
to
ee8bd29
Compare
9d399d5
to
6946294
Compare
842463a
to
30ad889
Compare
conn | ||
|> visit("/live/index") | ||
|> fill_in("Email", with: "some@example.com") | ||
|> within("#email-form", fn session -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Workaround for #142
conn | ||
|> visit("/live/index") | ||
|> fill_in("Email", with: "someone@example.com") | ||
|> assert_has(input(label: "Email", value: "someone@example.com")) | ||
|> within("#email-form", fn session -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Workaround for #142
conn | ||
|> visit("/live/index") | ||
|> fill_in("Email", with: "someone@example.com") | ||
|> within("#email-form", fn session -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Workaround for #142
conn | ||
|> visit("/live/index") | ||
|> fill_in("Email", with: "frodo@example.com") | ||
|> within("#email-form", fn session -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Workaround for #142
.github/workflows/ci.yml
Outdated
- name: Run tests | ||
run: mix test | ||
run: "mix test || if [[ $? = 2 ]]; then mix test --failed; else false; fi" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Browser (network) based tests tend to be flaky.
Options:
- Increase timeouts -> test suite takes longer to run (every
refute
has to wait for time out) - Keep timeout small, re run failed tests
I opted for 2.
84e83f0
to
a2eb819
Compare
conn | ||
|> visit("/live/index") | ||
|> click_button("Change page title") | ||
|> assert_has("title", text: "Title changed!") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using render_page_title
fails when running via Playwright.
Since the live title patch is not visible immediately in the browser.
Rather, use assert_has
, which uses a retry mechanism.
@@ -1,2 +1,2 @@ | |||
erlang 27.0 | |||
elixir 1.17.2-otp-27 | |||
elixir main |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need the 1.18 parameterized tests!
@@ -230,6 +230,10 @@ defmodule PhoenixTest do | |||
end | |||
end | |||
|
|||
def visit(driver, path) when is_struct(driver) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like the easiest, extensible way of adding custom drivers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, seems sensible to me. 👍
@@ -299,7 +304,7 @@ defmodule PhoenixTest.LiveTest do | |||
|> within("#email-form", fn session -> | |||
fill_in(session, "Email", with: "someone@example.com") | |||
end) | |||
|> assert_has(input(label: "Email", value: "someone@example.com")) | |||
|> assert_has("#form-data", text: "email: someone@example.com") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was a bad test:
The input
is not controlled (value
not set by server).
When a user inputs a value in the browser, the HTML value
attribute is not changed.
Fix: Assert on the server-rendered #form-data
instead.
a2eb819
to
213e241
Compare
75c51ba
to
400397c
Compare
8c9cbe7
to
c0d57bf
Compare
c0d57bf
to
f16ab9d
Compare
Progress update: I've slowly been converting Wallaby tests in a production project to PhoenixTest + Playwright.
I'm debating how to continue this work. @ Everyone: If you're interested in using this, let me know. |
@ftes love the progress! I'm sorry I haven't had the time to review the PR. Because of the size of it, I need a good few hours to review it properly and give helpful feedback. Something I've been thinking about: what if we make the protocol layer open for external libraries? We would have to change some of the protocol so it can be fully implemented by external libraries, but I think that would allow us to build other adapters at will. So, for example, you could add this adapter as an external library (e.g. What do you think? |
I think that's a great idea - as long as it doesn't make PhoenixTest too complicated. We could start by listing necessary changes (new issue?):
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ftes I haven't finished reviewing the full PR (I haven't looked at the test changes), but hopefully this is a good start (regardless of whether this comes directly into PhoenixTest or we make it a separate library).
Seeing how advanced this is, I'm open to including it directly into PhoenixTest (if you want to do that). I have an alternative solution in progress using Chrome Driver Protocol directly, but in that solution, we still have to provide the path to Chromium. So, if we're going to provide the path to a binary/cli thing, then maybe it makes more sense to just go full on with playwright.
If you prefer to keep it separate (which I also totally understand. Might be easier to move faster), I'd be happy for us to work more towards making the Driver protocol open for that.
One thing that I'm not super clear on yet is -- how close to feature parity is this? Do you consider it ready to merge ASAP, and we're good to go? Or are there things you'd like to finish before this could become part of PhoenixTest?
Any and all thoughts welcome!
otp_app: :phoenix_test, | ||
playwright: [ | ||
browser: [browser: :chromium, headless: System.get_env("PLAYWRIGHT_HEADLESS", "t") in ~w(t true)], | ||
cli: "priv/static/assets/node_modules/playwright/cli.js", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What version of playwright are we using/supporting? I imagine we're testing with a specific version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I honestly have no idea how stable the playwright protocol is.
Since its considered internal, there probably are no strong guarantees, so its difficult to say.
ecto_repos: [PhoenixTest.Repo], | ||
otp_app: :phoenix_test, | ||
playwright: [ | ||
browser: [browser: :chromium, headless: System.get_env("PLAYWRIGHT_HEADLESS", "t") in ~w(t true)], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 nice to have this out of the box.
@@ -230,6 +230,10 @@ defmodule PhoenixTest do | |||
end | |||
end | |||
|
|||
def visit(driver, path) when is_struct(driver) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, seems sensible to me. 👍
setup_all context do | ||
trace = Application.fetch_env!(:phoenix_test, :playwright)[:trace] | ||
|
||
case context do | ||
%{playwright: true} -> | ||
[browser_id: Case.Playwright.launch_browser(@playwright_opts), trace: trace] | ||
|
||
%{playwright: opts} when is_list(opts) -> | ||
opts = Keyword.merge(@playwright_opts, opts) | ||
[browser_id: Case.Playwright.launch_browser(opts), trace: trace] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is in a setup_all
. Does this mean that we'll launch Playwright browser once so long as any test uses playwright?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exactly. The browser is only launched once.
And then one session within that browser per test (think new user profile + window).
And Connection.ensure_started
provides lazy launching of the playwright
node.js server, so it is only started if actually required by a test.
|
||
defmodule Playwright do | ||
@moduledoc false | ||
import PhoenixTest.Playwright.Connection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't have to change, but might be easier to follow the code below if we alias Connection
instead of importing it here.
@@ -0,0 +1,125 @@ | |||
defmodule PhoenixTest.Playwright.Frame do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice module! 👍
@moduledoc """ | ||
Parse playwright messages. | ||
A single `Port` message can contain multiple Playwright messages and/or a fraction of a message. | ||
Such a message fraction is stored in `bufffer` and continued in the next `Port` message. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tiny typo. Thank for the comment! Helpful to know why we need this.
Such a message fraction is stored in `bufffer` and continued in the next `Port` message. | |
Such a message fraction is stored in `buffer` and continued in the next `Port` message. |
end | ||
|
||
cmd = "run-driver" | ||
port = Port.open({:spawn, "#{cli} #{cmd}"}, [:binary]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I asked this before in the PR, but worth asking here again: have you run into any zombie OS processes?
I know the Port docs mention a fix around it. I've used it in the past, and it worked quite well. Not sure if we need it here, but just pointing it out in case it's helpful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't seen any yet.
In general my experience with playwright has been far better than with Wallaby.
The playwright server seems to take better care of terminating the browser if a test is cancelled mid-run.
I'll keep my eyes open.
{k, v} when is_map(v) -> {String.to_atom(k), atom_keys(v)} | ||
{k, list} when is_list(list) -> {String.to_atom(k), Enum.map(list, fn v -> atom_keys(v) end)} | ||
{k, v} -> {String.to_atom(k), v} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any concerns here with using String.to_atom/1
? It's typically not a good idea with data we don't control, right? Are we at risk of hitting the max number of atoms at some point? And could we use to_exiting_atom/1
instead? Or do we do some other type of mapping?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question.
Since we're in a context of tests, I wouldn't worry too much.
We're not talking about a long-running system.
I think the keys we're converting to atoms are all playwright defined, i.e. there's a limited well-defined amount.
But since I don't know all playwright message types I can't say for sure whether or not e.g. some "user-defined" HTML node attribute might not pop up as a nested message key somewhere.
But even then, we're talking about tests and "user provided" would (mostly) mean "provided by the test set up".
I wouldn't want to use String.to_existing_atom
because I'm worried that users might get ArgumentErrors
if they trigger playwright messages with new keys (e.g. because they're using unwrap
in ways that triggers new message types).
The safe way would be to keep everything as strings, but that would just make the code so much more verbose.
def unquote(:and)(left, :none), do: left | ||
def unquote(:and)(left, right), do: concat(left, "internal:and=#{Jason.encode!(right)}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a different function name we could use here so it doesn't conflict with and
, and so we don't have to unquote
here? Or do you think it's worth it so we can have the 1-1 mapping with Playwright? (I think that's why we're doing it, right?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I tried to keep the Selector
, Frame
modules as minimal playwright wrappers which ideally reflect playwright concepts 1:1.
We could always go with and_
or something in that direction.
Good question! I've been using a copy of this for some weeks in a client project now. What I've gotten rid of there is the calls to So the driver is definitely usable. I'm undecided:
Taking everything into consideration, I think it might be best to keep it separate. Excited for the vanilla CDP driver! It will be great to compare and see the different tradeoffs. |
I've published the current version: To anyone interested: |
Closing this for now. |
Add
Playwright
test driver.It communicates with the playwright
node.js
server via aPort
.No additional dependencies.
Details
1.18
to use parameterized ExUnit tests (run live/static tests also with playwright)Port
)PhoenixTest.Case
to allow easily switching drivers via@tag :playwright
ecto
+postgres
to test sandboxing of concurrent browser testsOut of scope (just document how to)