Skip to content

Commit

Permalink
Add e2e tests for form recovery (#3201)
Browse files Browse the repository at this point in the history
Adds tests for #3200 and #2281.
  • Loading branch information
SteffenDE committed Apr 27, 2024
1 parent 62a9f5b commit 66f1cc3
Show file tree
Hide file tree
Showing 5 changed files with 329 additions and 168 deletions.
3 changes: 3 additions & 0 deletions test/e2e/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ defmodule Phoenix.LiveViewTest.E2E.Router do
live "/form", E2E.FormLive
live "/form/dynamic-inputs", E2E.FormDynamicInputsLive
live "/form/feedback", E2E.FormFeedbackLive
live "/form/nested", E2E.NestedFormLive
live "/js", E2E.JsLive
end

Expand All @@ -78,6 +79,8 @@ defmodule Phoenix.LiveViewTest.E2E.Router do
live "/3026", Issue3026Live
live "/3040", Issue3040Live
live "/3117", Issue3117Live
live "/3200/messages", Issue3200.PanelLive, :messages_tab
live "/3200/settings", Issue3200.PanelLive, :settings_tab
end
end

Expand Down
300 changes: 151 additions & 149 deletions test/e2e/tests/forms.spec.js
Original file line number Diff line number Diff line change
@@ -1,188 +1,190 @@
const { test, expect } = require("@playwright/test");
const { syncLV, attributeMutations } = require("../utils");

// see also https://github.com/phoenixframework/phoenix_live_view/issues/1759
// https://github.com/phoenixframework/phoenix_live_view/issues/2993
test.describe("restores disabled and readonly states", () => {
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("#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("#submit").click();
await syncLV(page);
await expect(page.locator("input[name=a]")).toHaveAttribute("readonly");
});
for (let path of ["/form/nested", "/form"]) {
// see also https://github.com/phoenixframework/phoenix_live_view/issues/1759
// https://github.com/phoenixframework/phoenix_live_view/issues/2993
test.describe("restores disabled and readonly states", () => {
test(`${path} - readonly state is restored after submits`, async ({ page }) => {
await page.goto(path);
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("#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("#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, "#submit");
await page.locator("#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(`${path} - button disabled state is restored after submits`, async ({ page }) => {
await page.goto(path);
await syncLV(page);
let changes = attributeMutations(page, "#submit");
await page.locator("#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 },
]));
test(`${path} - non-form button (phx-disable-with) disabled state is restored after click`, async ({ page }) => {
await page.goto(path);
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 },
]));
});
});
});

test.describe("form recovery", () => {
test("form state is recovered when socket reconnects", async ({ page }) => {
let webSocketEvents = [];
page.on("websocket", ws => {
ws.on("framesent", event => webSocketEvents.push({ type: "sent", payload: event.payload }));
ws.on("framereceived", event => webSocketEvents.push({ type: "received", payload: event.payload }));
ws.on("close", () => webSocketEvents.push({ type: "close" }));
});
test.describe(`${path} - form recovery`, () => {
test("form state is recovered when socket reconnects", async ({ page }) => {
let webSocketEvents = [];
page.on("websocket", ws => {
ws.on("framesent", event => webSocketEvents.push({ type: "sent", payload: event.payload }));
ws.on("framereceived", event => webSocketEvents.push({ type: "received", payload: event.payload }));
ws.on("close", () => webSocketEvents.push({ type: "close" }));
});

await page.goto("/form");
await syncLV(page);
await page.goto(path);
await syncLV(page);

await page.locator("input[name=b]").fill("test");
await syncLV(page);
await page.locator("input[name=b]").fill("test");
await syncLV(page);

await page.evaluate(() => new Promise((resolve) => window.liveSocket.disconnect(resolve)));
await expect(page.locator(".phx-loading")).toHaveCount(1);
await page.evaluate(() => new Promise((resolve) => window.liveSocket.disconnect(resolve)));
await expect(page.locator(".phx-loading")).toHaveCount(1);

await expect(webSocketEvents).toEqual(expect.arrayContaining([
{ type: "sent", payload: expect.stringContaining("phx_join") },
{ type: "received", payload: expect.stringContaining("phx_reply") },
{ type: "close" },
]))
await expect(webSocketEvents).toEqual(expect.arrayContaining([
{ type: "sent", payload: expect.stringContaining("phx_join") },
{ type: "received", payload: expect.stringContaining("phx_reply") },
{ type: "close" },
]))

webSocketEvents = [];
webSocketEvents = [];

await page.evaluate(() => window.liveSocket.connect());
await syncLV(page);
await expect(page.locator(".phx-loading")).toHaveCount(0);
await page.evaluate(() => window.liveSocket.connect());
await syncLV(page);
await expect(page.locator(".phx-loading")).toHaveCount(0);

await expect(page.locator("input[name=b]")).toHaveValue("test");
await expect(page.locator("input[name=b]")).toHaveValue("test");

await expect(webSocketEvents).toEqual(expect.arrayContaining([
{ type: "sent", payload: expect.stringContaining("phx_join") },
{ type: "received", payload: expect.stringContaining("phx_reply") },
{ type: "sent", payload: expect.stringMatching(/event.*a=foo&b=test/) },
]))
});
await expect(webSocketEvents).toEqual(expect.arrayContaining([
{ type: "sent", payload: expect.stringContaining("phx_join") },
{ type: "received", payload: expect.stringContaining("phx_reply") },
{ type: "sent", payload: expect.stringMatching(/event.*a=foo&b=test/) },
]))
});

test("does not recover when form is missing id", async ({ page }) => {
await page.goto("/form?no-id");
await syncLV(page);
test("does not recover when form is missing id", async ({ page }) => {
await page.goto(`${path}?no-id`);
await syncLV(page);

await page.locator("input[name=b]").fill("test");
await syncLV(page);
await page.locator("input[name=b]").fill("test");
await syncLV(page);

await page.evaluate(() => new Promise((resolve) => window.liveSocket.disconnect(resolve)));
await expect(page.locator(".phx-loading")).toHaveCount(1);
await page.evaluate(() => new Promise((resolve) => window.liveSocket.disconnect(resolve)));
await expect(page.locator(".phx-loading")).toHaveCount(1);

await page.evaluate(() => window.liveSocket.connect());
await syncLV(page);
await expect(page.locator(".phx-loading")).toHaveCount(0);
await page.evaluate(() => window.liveSocket.connect());
await syncLV(page);
await expect(page.locator(".phx-loading")).toHaveCount(0);

await expect(page.locator("input[name=b]")).toHaveValue("bar");
});
await expect(page.locator("input[name=b]")).toHaveValue("bar");
});

test("does not recover when form is missing phx-change", async ({ page }) => {
await page.goto("/form?no-change-event");
await syncLV(page);
test("does not recover when form is missing phx-change", async ({ page }) => {
await page.goto(`${path}?no-change-event`);
await syncLV(page);

await page.locator("input[name=b]").fill("test");
await syncLV(page);
await page.locator("input[name=b]").fill("test");
await syncLV(page);

await page.evaluate(() => new Promise((resolve) => window.liveSocket.disconnect(resolve)));
await expect(page.locator(".phx-loading")).toHaveCount(1);
await page.evaluate(() => new Promise((resolve) => window.liveSocket.disconnect(resolve)));
await expect(page.locator(".phx-loading")).toHaveCount(1);

await page.evaluate(() => window.liveSocket.connect());
await syncLV(page);
await expect(page.locator(".phx-loading")).toHaveCount(0);
await page.evaluate(() => window.liveSocket.connect());
await syncLV(page);
await expect(page.locator(".phx-loading")).toHaveCount(0);

await expect(page.locator("input[name=b]")).toHaveValue("bar");
});
await expect(page.locator("input[name=b]")).toHaveValue("bar");
});

test("phx-auto-recover", async ({ page }) => {
await page.goto("/form?phx-auto-recover=custom-recovery");
await syncLV(page);
test("phx-auto-recover", async ({ page }) => {
await page.goto(`${path}?phx-auto-recover=custom-recovery`);
await syncLV(page);

await page.locator("input[name=b]").fill("test");
await syncLV(page);
await page.locator("input[name=b]").fill("test");
await syncLV(page);

await page.evaluate(() => new Promise((resolve) => window.liveSocket.disconnect(resolve)));
await expect(page.locator(".phx-loading")).toHaveCount(1);
await page.evaluate(() => new Promise((resolve) => window.liveSocket.disconnect(resolve)));
await expect(page.locator(".phx-loading")).toHaveCount(1);

let webSocketEvents = [];
page.on("websocket", ws => {
ws.on("framesent", event => webSocketEvents.push({ type: "sent", payload: event.payload }));
ws.on("framereceived", event => webSocketEvents.push({ type: "received", payload: event.payload }));
ws.on("close", () => webSocketEvents.push({ type: "close" }));
});
let webSocketEvents = [];
page.on("websocket", ws => {
ws.on("framesent", event => webSocketEvents.push({ type: "sent", payload: event.payload }));
ws.on("framereceived", event => webSocketEvents.push({ type: "received", payload: event.payload }));
ws.on("close", () => webSocketEvents.push({ type: "close" }));
});

await page.evaluate(() => window.liveSocket.connect());
await syncLV(page);
await expect(page.locator(".phx-loading")).toHaveCount(0);
await page.evaluate(() => window.liveSocket.connect());
await syncLV(page);
await expect(page.locator(".phx-loading")).toHaveCount(0);

await expect(page.locator("input[name=b]")).toHaveValue("custom value from server");
await expect(page.locator("input[name=b]")).toHaveValue("custom value from server");

await expect(webSocketEvents).toEqual(expect.arrayContaining([
{ type: "sent", payload: expect.stringContaining("phx_join") },
{ type: "received", payload: expect.stringContaining("phx_reply") },
{ type: "sent", payload: expect.stringMatching(/event.*a=foo&b=test/) },
]))
});
})
await expect(webSocketEvents).toEqual(expect.arrayContaining([
{ type: "sent", payload: expect.stringContaining("phx_join") },
{ type: "received", payload: expect.stringContaining("phx_reply") },
{ type: "sent", payload: expect.stringMatching(/event.*a=foo&b=test/) },
]))
});
})

test("can submit form with button that has phx-click", async ({ page }) => {
await page.goto("/form?phx-auto-recover=custom-recovery");
await syncLV(page);
test(`${path} - can submit form with button that has phx-click`, async ({ page }) => {
await page.goto(`${path}?phx-auto-recover=custom-recovery`);
await syncLV(page);

await expect(page.getByText("Form was submitted!")).not.toBeVisible();
await expect(page.getByText("Form was submitted!")).not.toBeVisible();

await page.getByRole("button", { name: "Submit with JS" }).click();
await syncLV(page);
await page.getByRole("button", { name: "Submit with JS" }).click();
await syncLV(page);

await expect(page.getByText("Form was submitted!")).toBeVisible();
});
await expect(page.getByText("Form was submitted!")).toBeVisible();
});
}

test("can dynamically add/remove inputs (ecto sort_param/drop_param)", async ({ page }) => {
await page.goto("/form/dynamic-inputs");
Expand Down
27 changes: 27 additions & 0 deletions test/e2e/tests/issues/3200.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { test, expect } = require("@playwright/test");
const { syncLV } = require("../../utils");

// https://github.com/phoenixframework/phoenix_live_view/issues/3200
test("phx-target='selector' is used correctly for form recovery", async ({ page }) => {
const errors = [];
page.on("pageerror", (err) => errors.push(err));

await page.goto("/issues/3200/settings");
await syncLV(page);

await page.getByRole("button", { name: "Messages" }).click();
await syncLV(page);
await expect(page).toHaveURL("/issues/3200/messages");

await page.locator("#new_message_input").fill("Hello");
await syncLV(page);

await page.evaluate(() => new Promise((resolve) => window.liveSocket.disconnect(resolve)));
await expect(page.locator(".phx-loading")).toHaveCount(1);

await page.evaluate(() => window.liveSocket.connect());
await syncLV(page);

await expect(page.locator("#new_message_input")).toHaveValue("Hello");
await expect(errors).toEqual([]);
});
Loading

0 comments on commit 66f1cc3

Please sign in to comment.