Skip to content

Commit

Permalink
add test for disabled and readonly state
Browse files Browse the repository at this point in the history
  • Loading branch information
SteffenDE committed Jan 15, 2024
1 parent 1ff3489 commit 90bb9bd
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 1 deletion.
1 change: 1 addition & 0 deletions test/e2e/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ defmodule Phoenix.LiveViewTest.E2E.Router do
live "/healthy/:category", Phoenix.LiveViewTest.HealthyLive

live "/upload", Phoenix.LiveViewTest.E2E.UploadLive
live "/form", Phoenix.LiveViewTest.E2E.FormLive
end
end
end
Expand Down
65 changes: 65 additions & 0 deletions test/e2e/tests/forms.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const { test, expect } = require("@playwright/test");
const { syncLV, attributeMutations } = require("../utils");

test("readonly state is restored after submits", async ({ page }) => {
await page.goto("/form");
await syncLV(page);
await expect(page.locator("input[name=a]")).toHaveAttribute("readonly");
let changesA = attributeMutations(page, "input[name=a]");
let changesB = attributeMutations(page, "input[name=b]");
// can submit multiple times and readonly input stays readonly
await page.locator("button[type=submit]").click();
await syncLV(page);
// a is readonly and should stay readonly
await expect(await changesA()).toEqual(expect.arrayContaining([
{ attr: "data-phx-readonly", oldValue: null, newValue: "true" },
{ attr: "readonly", oldValue: "", newValue: "" },
{ attr: "data-phx-readonly", oldValue: "true", newValue: null },
{ attr: "readonly", oldValue: "", newValue: "" },
]));
// b is not readonly, but LV will set it to readonly while submitting
await expect(await changesB()).toEqual(expect.arrayContaining([
{ attr: "data-phx-readonly", oldValue: null, newValue: "false" },
{ attr: "readonly", oldValue: null, newValue: "" },
{ attr: "data-phx-readonly", oldValue: "false", newValue: null },
{ attr: "readonly", oldValue: "", newValue: null },
]));
await expect(page.locator("input[name=a]")).toHaveAttribute("readonly");
await page.locator("button[type=submit]").click();
await syncLV(page);
await expect(page.locator("input[name=a]")).toHaveAttribute("readonly");
});

test("button disabled state is restored after submits", async ({ page }) => {
await page.goto("/form");
await syncLV(page);
let changes = attributeMutations(page, "button[type=submit]");
await page.locator("button[type=submit]").click();
await syncLV(page);
// submit button is disabled while submitting, but then restored
await expect(await changes()).toEqual(expect.arrayContaining([
{ attr: "data-phx-disabled", oldValue: null, newValue: "false" },
{ attr: "disabled", oldValue: null, newValue: "" },
{ attr: "class", oldValue: null, newValue: "phx-submit-loading" },
{ attr: "data-phx-disabled", oldValue: "false", newValue: null },
{ attr: "disabled", oldValue: "", newValue: null },
{ attr: "class", oldValue: "phx-submit-loading", newValue: null },
]));
});

test("non-form button (phx-disable-with) disabled state is restored after click", async ({ page }) => {
await page.goto("/form");
await syncLV(page);
let changes = attributeMutations(page, "button[type=button]");
await page.locator("button[type=button]").click();
await syncLV(page);
// submit button is disabled while submitting, but then restored
await expect(await changes()).toEqual(expect.arrayContaining([
{ attr: "data-phx-disabled", oldValue: null, newValue: "false" },
{ attr: "disabled", oldValue: null, newValue: "" },
{ attr: "class", oldValue: null, newValue: "phx-click-loading" },
{ attr: "data-phx-disabled", oldValue: "false", newValue: null },
{ attr: "disabled", oldValue: "", newValue: null },
{ attr: "class", oldValue: "phx-click-loading", newValue: null },
]));
});
50 changes: 49 additions & 1 deletion test/e2e/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const { expect } = require("@playwright/test");
const Crypto = require("crypto");

const randomString = (size = 21) => Crypto.randomBytes(size).toString("base64").slice(0, size);

// a helper function to wait until the LV has no pending events
const syncLV = async (page) => {
Expand All @@ -11,4 +14,49 @@ const syncLV = async (page) => {
return Promise.all(promises);
};

module.exports = { syncLV };
const attributeMutations = (page, selector) => {
// this is a bit of a hack, basically we create a MutationObserver on the page
// that will record any changes to a selector until the promise is awaited
//
// we use a random id to store the resolve function in the window object
const id = randomString(24);
// this promise resolves to the mutation list
const promise = page.locator(selector).evaluate((target, id) => {
return new Promise((resolve) => {
const mutations = [];
let observer;
window[id] = () => {
if (observer) observer.disconnect();
resolve(mutations);
delete window[id];
};
// https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
observer = new MutationObserver((mutationsList, _observer) => {
mutationsList.forEach(mutation => {
if (mutation.type === "attributes") {
mutations.push({
attr: mutation.attributeName,
oldValue: mutation.oldValue,
newValue: mutation.target.getAttribute(mutation.attributeName)
});
}
});
}).observe(target, { attributes: true, attributeOldValue: true });
});
}, id);

return () => {
// we want to stop observing!
page.locator(selector).evaluate((_target, id) => {
if (id in window) {
window[id]();
} else {
return Promise.reject(`window.${id} not found... that should not happen! :(`);
}
}, id);
// return the result of the initial promise
return promise;
};
}

module.exports = { randomString, syncLV, attributeMutations };
33 changes: 33 additions & 0 deletions test/support/e2e/form_live.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule Phoenix.LiveViewTest.E2E.FormLive do
use Phoenix.LiveView

@impl Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, socket}
end

@impl Phoenix.LiveView
def handle_event("validate", _params, socket) do
{:noreply, socket}
end

def handle_event("save", _params, socket) do
{:noreply, socket}
end

def handle_event("button-test", _params, socket) do
{:noreply, socket}
end

@impl Phoenix.LiveView
def render(assigns) do
~H"""
<form id="test-form" phx-submit="save" phx-change="validate">
<input type="text" name="a" readonly value="foo" />
<input type="text" name="b" value="bar" />
<button type="submit" phx-disable-with="Submitting">Submit</button>
<button type="button" phx-click="button-test" phx-disable-with="Loading">Non-form Button</button>
</form>
"""
end
end

0 comments on commit 90bb9bd

Please sign in to comment.