Skip to content

Commit

Permalink
Proof of concept: playwright e2e tests (#3001)
Browse files Browse the repository at this point in the history
playwright e2e tests


references #3009
references #2993
references #1759

* build assets in e2e pipeline

We want to run the e2e tests with the latest assets without the need to
commit them to the repository. Otherwise we would not see test
failures from changes to the js code as early as possible.
  • Loading branch information
SteffenDE authored Jan 17, 2024
1 parent 9644dab commit 5f76736
Show file tree
Hide file tree
Showing 18 changed files with 1,204 additions and 17 deletions.
78 changes: 76 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ jobs:

npm_test:
name: npm test

strategy:
matrix:
include:
- elixir: 1.13.2
otp: 24.2

runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -85,8 +92,8 @@ jobs:
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: 1.13.2
otp-version: 24.2
elixir-version: ${{ matrix.elixir }}
otp-version: ${{ matrix.otp }}

- name: Restore deps and _build cache
uses: actions/cache@v2
Expand Down Expand Up @@ -119,3 +126,70 @@ jobs:
cd assets
npm install
npm test
e2e_test:
name: e2e test

strategy:
matrix:
include:
- elixir: 1.16.0
otp: 26.2

runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.40.1-jammy
env:
ImageOS: ubuntu22
HOME: /root
steps:
- name: Checkout
uses: actions/checkout@v2

- name: install unzip
run: apt update && apt -y install unzip

- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: ${{ matrix.elixir }}
otp-version: ${{ matrix.otp }}

- name: Restore deps and _build cache
uses: actions/cache@v2
with:
path: |
deps
_build
key: deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}
restore-keys: |
deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}
- name: Install dependencies
run: mix deps.get --only e2e --only dev

- name: Restore npm cache
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: npm install (assets)
run: cd assets && npm install && cd ..

- name: Build assets
run: mix assets.build

- name: Run e2e tests
run: |
npm install
npm run e2e:test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ phoenix_live_view-*.tar

node_modules

/test/e2e/test-results/
/playwright-report/
4 changes: 3 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ defmodule Phoenix.LiveView.MixProject do
]
end

defp elixirc_paths(:e2e), do: ["lib", "test/support"]
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]

Expand All @@ -49,7 +50,8 @@ defmodule Phoenix.LiveView.MixProject do
{:makeup_eex, ">= 0.1.1", only: :docs},
{:makeup_diff, "~> 0.1", only: :docs},
{:html_entities, ">= 0.0.0", only: :test},
{:phoenix_live_reload, "~> 1.4.1", only: :test}
{:phoenix_live_reload, "~> 1.4.1", only: :test},
{:plug_cowboy, "~> 2.6", only: :e2e}
]
end

Expand Down
5 changes: 5 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
%{
"castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
"esbuild": {:hex, :esbuild, "0.7.1", "fa0947e8c3c3c2f86c9bf7e791a0a385007ccd42b86885e8e893bdb6631f5169", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "66661cdf70b1378ee4dc16573fcee67750b59761b2605a0207c267ab9d19f13c"},
"ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"},
Expand All @@ -22,6 +25,8 @@
"phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
"phoenix_view": {:hex, :phoenix_view, "2.0.1", "a653e3d9d944aace0a064e4a13ad473ffa68f7bc4ca42dbf83cc1d464f1fb295", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "6c358e2cefc5f341c728914b867c556bbfd239fed9e881bac257d70cb2b8a6f6"},
"plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"},
"plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
}
75 changes: 75 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,12 @@
"package.json",
"priv/static/*",
"assets/js/phoenix_live_view/*"
]
],
"devDependencies": {
"@playwright/test": "^1.40.1"
},
"scripts": {
"e2e:server": "MIX_ENV=e2e mix run test/e2e/test_helper.exs",
"e2e:test": "cd test/e2e && npx playwright test"
}
}
31 changes: 31 additions & 0 deletions test/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# End-to-end tests

This directory contains end-to-end tests that use the [Playwright](https://playwright.dev/)
test framework.
These tests use all three web engines (Chromium, Firefox, Webkit) and test the interaction
with an actual LiveView server.

## Running the tests

To run the tests, ensure that the npm dependencies are installed by running `npm install` in
the root of the repository. Then, run `npm run e2e:test` to run the tests.

This will execute the `npx playwright test` command in the `test/e2e` directory. Playwright
will start a LiveView server using the `MIX_ENV=e2e mix run test/e2e/test_helper.exs` command.

Playwright supports an [interactive UI mode](https://playwright.dev/docs/test-ui-mode) that
can be used to debug the tests. To run the tests in this mode, run `npm run e2e:test -- --ui`.

Tests can also be run in headed mode by passing the `--headed` flag. This is especially useful
in combination with running only specific tests, for example:

```bash
npm run e2e:test -- tests/streams.spec.js:9 --project chromium --headed
```

To step through a single test, pass `--debug`, which will automatically run the test in headed
mode:

```bash
npm run e2e:test -- tests/streams.spec.js:9 --project chromium --debug
```
40 changes: 40 additions & 0 deletions test/e2e/playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// playwright.config.js
// @ts-check
const { devices } = require("@playwright/test");

/** @type {import("@playwright/test").PlaywrightTestConfig} */
const config = {
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? [["github"], ["html"]] : "list",
use: {
trace: "retain-on-failure",
screenshot: "only-on-failure",
baseURL: "http://localhost:4000/",
ignoreHTTPSErrors: true,
},
webServer: {
command: "npm run e2e:server",
url: "http://127.0.0.1:4000/health",
reuseExistingServer: !process.env.CI,
stdout: "pipe",
stderr: "pipe",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
}
],
outputDir: "test-results"
};

module.exports = config;
95 changes: 95 additions & 0 deletions test/e2e/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
Application.put_env(:phoenix_live_view, Phoenix.LiveViewTest.E2E.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4000],
# TODO: switch to bandit when Phoenix 1.7 is used
# adapter: Bandit.PhoenixAdapter,
server: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64),
render_errors: [
# TODO: uncomment when LV Phoenix 1.7 is used
# formats: [
# html: Phoenix.LiveViewTest.E2E.ErrorHTML,
# ],
view: Phoenix.LiveViewTest.E2E.ErrorHTML,
layout: false
],
pubsub_server: Phoenix.LiveViewTest.E2E.PubSub,
debug_errors: false
)

defmodule Phoenix.LiveViewTest.E2E.ErrorHTML do
def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end

defmodule Phoenix.LiveViewTest.E2E.Layout do
use Phoenix.Component

def render("live.html", assigns) do
~H"""
<script src="/assets/phoenix/phoenix.min.js"></script>
<script src="/assets/phoenix_live_view/phoenix_live_view.js"></script>
<script>
let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
liveSocket.connect()
</script>
<style>
* { font-size: 1.1em; }
</style>
<%= @inner_content %>
"""
end
end

defmodule Phoenix.LiveViewTest.E2E.Router do
use Phoenix.Router
import Phoenix.LiveView.Router

pipeline :browser do
plug(:accepts, ["html"])
end

live_session :default, layout: {Phoenix.LiveViewTest.E2E.Layout, :live} do
scope "/" do
pipe_through(:browser)

live "/stream", Phoenix.LiveViewTest.StreamLive
live "/stream/reset", Phoenix.LiveViewTest.StreamResetLive
live "/stream/reset-lc", Phoenix.LiveViewTest.StreamResetLCLive
live "/healthy/:category", Phoenix.LiveViewTest.HealthyLive

live "/upload", Phoenix.LiveViewTest.E2E.UploadLive
live "/form", Phoenix.LiveViewTest.E2E.FormLive
end
end
end

defmodule Phoenix.LiveViewTest.E2E.Endpoint do
use Phoenix.Endpoint, otp_app: :phoenix_live_view

socket("/live", Phoenix.LiveView.Socket)

plug Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix"
plug Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view"
plug Plug.Static, from: System.tmp_dir!(), at: "/tmp"

plug :health_check

plug Phoenix.LiveViewTest.E2E.Router

defp health_check(%{request_path: "/health"} = conn, _opts) do
conn |> Plug.Conn.send_resp(200, "OK") |> Plug.Conn.halt()
end

defp health_check(conn, _opts), do: conn
end

{:ok, _} =
Supervisor.start_link(
[
Phoenix.LiveViewTest.E2E.Endpoint,
{Phoenix.PubSub, name: Phoenix.LiveViewTest.E2E.PubSub}
],
strategy: :one_for_one
)

Process.sleep(:infinity)
Loading

0 comments on commit 5f76736

Please sign in to comment.