Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions e2e-backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ services:
# never run — fine here, since the checkout golden path doesn't depend on
# any job output (see note above), and the tests never assert on images.
SOLID_QUEUE_IN_PUMA: "false"
# Spree throttles sign-ups (3/min) and sign-ins (5/min) per client IP.
# Every test signs a fresh customer up from the same IP, so the
# defaults lock the suite out after its first few tests.
SPREE_RATE_LIMIT_REGISTER: "1000"
SPREE_RATE_LIMIT_LOGIN: "1000"
SPREE_RATE_LIMIT_REFRESH: "1000"
ports:
- "4000:3000"
healthcheck:
Expand Down
107 changes: 107 additions & 0 deletions e2e/account.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { expect, test } from "@playwright/test";
import { signUp, waitForHydration } from "./support";

test.describe("customer account", () => {
test.beforeEach(async ({ page }) => {
await signUp(page, { firstName: "Grace", lastName: "Hopper" });
});

test("the account navigation reaches every section", async ({ page }) => {
const nav = page.getByRole("complementary").getByRole("navigation");
const sections = [
{ link: "Orders", heading: "Order History", empty: "No orders yet" },
{
link: "Payment Methods",
heading: "Payment Methods",
empty: "No payment methods saved",
},
{ link: "Gift Cards", heading: "Gift Cards", empty: "No gift cards" },
{ link: "Addresses", heading: "Addresses", empty: "No saved addresses" },
];

for (const section of sections) {
const link = nav.getByRole("link", { name: section.link });
await waitForHydration(link);
await link.click();
await expect(
page.getByRole("heading", { level: 1, name: section.heading }),
).toBeVisible({ timeout: 30_000 });
await expect(
page.getByRole("heading", { name: section.empty }),
).toBeVisible();
}
});

test("a customer adds and deletes an address", async ({ page }) => {
await page
.getByRole("complementary")
.getByRole("link", { name: "Addresses" })
.click();
const addFirst = page.getByRole("button", {
name: "Add Your First Address",
});
await waitForHydration(addFirst);
await addFirst.click();

const dialog = page.getByRole("dialog", { name: "Add New Address" });
await dialog
.getByRole("combobox", { name: "Country" })
.selectOption({ label: "United States" });
await dialog
.getByRole("textbox", { name: "Address", exact: true })
.fill("42 Analytical Engine Way");
await dialog.getByRole("textbox", { name: "City" }).fill("New York");
await dialog
.getByRole("textbox", { name: "ZIP / Postal code" })
.fill("10001");
// With the United States selected the state becomes a list, enabled once
// the country's states have loaded.
await dialog
.getByRole("combobox", { name: /state/i })
.selectOption({ label: "New York" });
await dialog.getByRole("button", { name: "Save Address" }).click();

await expect(dialog).toBeHidden({ timeout: 30_000 });
await expect(page.getByText("42 Analytical Engine Way")).toBeVisible();

// The list refreshes once more after the save, and that closes a confirm
// dialog opened just before it.
const confirmDelete = page
.getByRole("alertdialog")
.getByRole("button", { name: "Delete" });
await expect(async () => {
if (!(await confirmDelete.isVisible())) {
await page
.getByRole("button", { name: "Delete" })
.first()
.click({ timeout: 3_000 });
}
await confirmDelete.click({ timeout: 3_000 });
}).toPass({ timeout: 30_000 });
await expect(
page.getByRole("heading", { name: "No saved addresses" }),
).toBeVisible({ timeout: 30_000 });
});

test("a customer updates their name", async ({ page }) => {
await page
.getByRole("complementary")
.getByRole("link", { name: "Profile" })
.click();
const firstName = page.getByRole("textbox", { name: "First Name" });
await expect(firstName).toHaveValue("Grace", { timeout: 30_000 });
await waitForHydration(firstName);

await firstName.fill("Amazing Grace");
await page.getByRole("button", { name: "Save Changes" }).click();
await expect(page.getByText("Profile updated successfully!")).toBeVisible({
timeout: 30_000,
});

await page.reload();
await expect(page.getByRole("textbox", { name: "First Name" })).toHaveValue(
"Amazing Grace",
{ timeout: 30_000 },
);
});
});
81 changes: 81 additions & 0 deletions e2e/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { expect, test } from "@playwright/test";
import {
BASE_PATH,
formWithButton,
signIn,
signUp,
waitForHydration,
} from "./support";

test.describe("sign up and sign in", () => {
test("a new customer signs up, signs out and signs back in", async ({
page,
}) => {
const { email, password } = await signUp(page, {
firstName: "Ada",
lastName: "Lovelace",
});
const sidebar = page.getByRole("complementary");
await expect(sidebar.getByText("Ada Lovelace")).toBeVisible();
await expect(sidebar.getByText(email)).toBeVisible();

const signOut = page.getByRole("button", { name: "Sign Out" });
await waitForHydration(signOut);
await signOut.click();
await expect(page.getByRole("button", { name: "Sign In" })).toBeVisible({
timeout: 30_000,
});

await page.goto(`${BASE_PATH}/account`);
await signIn(page, email, password);
await expect(
page.getByRole("heading", { name: "Account Overview" }),
).toBeVisible({ timeout: 30_000 });
});

test("a wrong password is refused", async ({ page }) => {
const { email } = await signUp(page);
await page.context().clearCookies();

await page.goto(`${BASE_PATH}/account`);
await signIn(page, email, "not-the-password");
await expect(page.getByText("Invalid email or password")).toBeVisible({
timeout: 30_000,
});
});

test("an account page sends a guest to sign in and back", async ({
page,
}) => {
const { email, password } = await signUp(page);
await page.context().clearCookies();

await page.goto(`${BASE_PATH}/account/orders`);
await expect(page).toHaveURL(/\/account\?redirect=/);
await signIn(page, email, password);
await expect(
page.getByRole("heading", { level: 1, name: "Order History" }),
).toBeVisible({ timeout: 30_000 });
});

test("sign up refuses passwords that do not match", async ({ page }) => {
await page.goto(`${BASE_PATH}/account/register`);
await waitForHydration(formWithButton(page, "Create Account"));
await page.getByRole("textbox", { name: "First name" }).fill("E2E");
await page.getByRole("textbox", { name: "Last name" }).fill("Customer");
await page
.getByRole("textbox", { name: "Email" })
.fill(`e2e-mismatch-${Date.now()}@example.com`);
await page
.getByRole("textbox", { name: "Password", exact: true })
.fill("spree123!");
await page
.getByRole("textbox", { name: "Confirm Password" })
.fill("different123!");
await page.getByRole("checkbox", { name: /i agree/i }).check();
await page.getByRole("button", { name: "Create Account" }).click();

await expect(page.getByText("Passwords do not match")).toBeVisible();
await expect(page).toHaveURL(/\/account\/register$/);
});
});
61 changes: 61 additions & 0 deletions e2e/cart.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { expect, test } from "@playwright/test";
import {
addToCart,
BASE_PATH,
openFirstProduct,
waitForHydration,
} from "./support";

test.describe("cart page", () => {
test("an empty cart points back to the catalog", async ({ page }) => {
await page.goto(`${BASE_PATH}/cart`);
await expect(
page.getByRole("heading", { name: "Your cart is empty" }),
).toBeVisible({ timeout: 30_000 });

const continueShopping = page.getByRole("link", {
name: "Continue Shopping",
});
await waitForHydration(continueShopping);
await continueShopping.click();
await expect(page).toHaveURL(/\/products$/);
});

test("a customer changes the quantity and removes the item", async ({
page,
}) => {
const name = await openFirstProduct(page, `${BASE_PATH}/products`);
await addToCart(page, name);

await page.goto(`${BASE_PATH}/cart`);
await expect(
page.getByRole("heading", { name: "Shopping Cart" }),
).toBeVisible({ timeout: 30_000 });
const main = page.getByRole("main");
await expect(main.getByRole("heading", { name })).toBeVisible();

const subtotal = main
.getByRole("term")
.filter({ hasText: /^Subtotal$/ })
.locator("xpath=following-sibling::*[1]");
const initialSubtotal = (await subtotal.textContent())?.trim() ?? "";

const increase = main.getByRole("button", { name: "Increase quantity" });
await waitForHydration(increase);
await increase.click();
await expect(main.getByRole("textbox", { name: "Quantity" })).toHaveValue(
"2",
{ timeout: 30_000 },
);
await expect(subtotal).not.toHaveText(initialSubtotal, { timeout: 30_000 });

await expect(
main.getByRole("link", { name: "Proceed to Checkout" }),
).toBeVisible();

await main.getByRole("button", { name: `Remove ${name}` }).click();
await expect(
page.getByRole("heading", { name: "Your cart is empty" }),
).toBeVisible({ timeout: 30_000 });
});
});
95 changes: 95 additions & 0 deletions e2e/catalog.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { expect, type Page, test } from "@playwright/test";
import {
BASE_PATH,
openCategoryFromMenu,
openFirstCategory,
openFirstProduct,
waitForHydration,
} from "./support";

function productNames(page: Page) {
return page
.getByRole("main")
.getByRole("heading", { level: 3 })
.allTextContents();
}

test.describe("home page", () => {
test("shows featured products that open their product page", async ({
page,
}) => {
await page.goto(BASE_PATH);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
await expect(
page.getByRole("heading", { name: "Featured Products" }),
).toBeVisible();

await openFirstProduct(page, BASE_PATH);
});

test("the header menu opens a category", async ({ page }) => {
await openCategoryFromMenu(page);
});
});

test.describe("category page", () => {
test("lists the category's products with a count and breadcrumb", async ({
page,
}) => {
const name = await openFirstCategory(page);
const breadcrumb = page.getByRole("navigation", { name: "Breadcrumb" });
await expect(breadcrumb.getByRole("link", { name: "Home" })).toBeVisible();
await expect(breadcrumb.getByText(name)).toBeVisible();
// The count briefly renders twice while the page streams in.
await expect(
page.getByText(/\d+ products?/).filter({ visible: true }),
).toBeVisible();
expect((await productNames(page)).length).toBeGreaterThan(0);
});

test("a subcategory narrows the listing", async ({ page }) => {
const parent = await openFirstCategory(page);
const subcategory = page
.getByRole("main")
.getByRole("link")
.filter({ hasNot: page.getByRole("heading") })
.and(page.locator('a[href*="/c/"]'))
.first();
await waitForHydration(subcategory);
const name = (await subcategory.textContent())?.trim() ?? "";
await subcategory.click();

await expect(page.getByRole("heading", { level: 1, name })).toBeVisible({
timeout: 30_000,
});
await expect(
page
.getByRole("navigation", { name: "Breadcrumb" })
.getByRole("link", { name: parent }),
).toBeVisible();
});

test("sorts products by name", async ({ page }) => {
await openFirstCategory(page);
const sort = page.getByRole("button", { name: "Sort" });
await waitForHydration(sort);
await sort.click();
await page.getByRole("menuitemradio", { name: "Name (A-Z)" }).click();

await expect(page).toHaveURL(/sort=/, { timeout: 30_000 });
await expect
.poll(async () => {
const names = await productNames(page);
const sorted = [...names].sort((a, b) =>
a.localeCompare(b, "en", { sensitivity: "base" }),
);
return names.length > 1 && names.join("|") === sorted.join("|");
})
.toBe(true);
});

test("a product card opens its product page", async ({ page }) => {
await openFirstCategory(page);
await openFirstProduct(page, page.url());
});
});
Loading
Loading