You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

store.spec.ts 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import { test, expect } from "@playwright/test";
  2. import AxeBuilder from "@axe-core/playwright";
  3. const product = "/product/the-original-salted-egg-cornflakes-clan";
  4. test("bag quantities, subtotal, persistence and removal", async ({ page }) => {
  5. await page.goto("/");
  6. await page
  7. .getByRole("button", {
  8. name: "Quick add The Original Salted Egg Cornflakes 180g",
  9. exact: true,
  10. })
  11. .click();
  12. let dialog = page.getByRole("dialog");
  13. await expect(dialog).toBeVisible();
  14. await expect(dialog).toContainText("RM 17.90");
  15. await dialog
  16. .getByRole("button", {
  17. name: "Increase The Original Salted Egg Cornflakes",
  18. })
  19. .click();
  20. await expect(dialog).toContainText("RM 35.80");
  21. await page.keyboard.press("Escape");
  22. await page.reload();
  23. await page.getByRole("button", { name: "Open shopping bag (2)" }).click();
  24. await expect(dialog).toContainText("RM 35.80");
  25. await dialog.getByRole("link", { name: "View your bag" }).click();
  26. await expect(page).toHaveURL("/cart");
  27. await page
  28. .getByRole("button", {
  29. name: "Remove The Original Salted Egg Cornflakes",
  30. exact: true,
  31. })
  32. .click();
  33. await expect(
  34. page.getByRole("heading", { name: "Your cart is empty" }),
  35. ).toBeVisible();
  36. });
  37. test("wishlist persists and can be removed", async ({ page }) => {
  38. await page.goto("/");
  39. await page
  40. .getByRole("button", {
  41. name: "Save The Original Salted Egg Cornflakes to wishlist",
  42. exact: true,
  43. })
  44. .click();
  45. await page.goto("/wishlist");
  46. await expect(page.locator(".product-card")).toHaveCount(1);
  47. await page
  48. .getByRole("button", {
  49. name: "Remove The Original Salted Egg Cornflakes from wishlist",
  50. exact: true,
  51. })
  52. .click();
  53. await expect(
  54. page.getByRole("heading", { name: "A little room for favourites" }),
  55. ).toBeVisible();
  56. });
  57. test("filters, sorting, load more and no results", async ({ page }) => {
  58. await page.goto("/shop");
  59. await expect(page.locator(".product-card")).toHaveCount(12);
  60. await page.getByRole("button", { name: "LOAD MORE" }).click();
  61. await expect(page.locator(".product-card")).toHaveCount(13);
  62. await page.getByRole("button", { name: "Tempeh chips", exact: true }).click();
  63. await expect(page.locator(".product-card")).toHaveCount(2);
  64. await page.getByRole("button", { name: "All snacks", exact: true }).click();
  65. await page.getByLabel("Sort by").selectOption("low");
  66. await expect(page.locator(".product-card").first()).toContainText("RM 3.50");
  67. await page
  68. .getByRole("searchbox", { name: "Search products" })
  69. .fill("nothingmatches");
  70. await expect(
  71. page.getByRole("heading", { name: "No snacks found" }),
  72. ).toBeVisible();
  73. await page.getByRole("button", { name: "Clear filters" }).click();
  74. await expect(page.locator(".result-count")).toContainText("13 snacks");
  75. });
  76. test("search returns products and journal articles and restores focus", async ({
  77. page,
  78. }) => {
  79. await page.goto("/");
  80. const trigger = page.getByRole("button", { name: "Search", exact: true });
  81. await trigger.click();
  82. const search = page.getByRole("searchbox", {
  83. name: "Search snacks, collections and stories",
  84. });
  85. await search.fill("mushroom");
  86. await expect(page.locator(".search-results>a")).toHaveCount(3);
  87. await search.fill("Mauritius");
  88. await expect(
  89. page.getByRole("link", { name: "Journal: Mauritius, We’ve Arrived" }),
  90. ).toBeVisible();
  91. await page.keyboard.press("Escape");
  92. await expect(trigger).toBeFocused();
  93. await trigger.click();
  94. await page.getByRole("searchbox").fill("tempeh");
  95. await page.getByRole("button", { name: "View all search results" }).click();
  96. await expect(page).toHaveURL("/shop?q=tempeh");
  97. await expect(page.locator(".product-card")).toHaveCount(2);
  98. });
  99. test("gallery, quantity, alternate packs and checkout preview", async ({
  100. page,
  101. }) => {
  102. await page.goto(product);
  103. await page
  104. .getByRole("button", { name: "View product image 2", exact: true })
  105. .click();
  106. await expect(page.locator(".gallery-main img")).toHaveAttribute(
  107. "src",
  108. "/images/snack-23-1.webp",
  109. );
  110. await page.getByRole("button", { name: "Zoom product image" }).click();
  111. await expect(page.getByRole("dialog")).toBeVisible();
  112. await page.keyboard.press("Escape");
  113. await page.getByRole("button", { name: "Increase Quantity" }).click();
  114. await page.getByRole("button", { name: "BUY NOW" }).click();
  115. await expect(page).toHaveURL("/checkout");
  116. await expect(page.locator(".checkout-layout")).toContainText("RM 35.80");
  117. await expect(page.getByRole("radio", { name: /Credit \/ debit card/ })).toBeChecked();
  118. await expect(page.locator(".checkout-order-summary .checkout-subtotal")).toContainText("RM 40.80");
  119. });
  120. test("mobile navigation and filter panel", async ({ page }) => {
  121. await page.setViewportSize({ width: 390, height: 844 });
  122. await page.goto("/");
  123. await page.getByRole("button", { name: "Open menu" }).click();
  124. await page
  125. .getByRole("dialog")
  126. .getByRole("link", { name: "Shop", exact: true })
  127. .click();
  128. await expect(page).toHaveURL("/shop");
  129. await expect(page.getByRole("dialog")).toHaveCount(0);
  130. await page.getByRole("button", { name: "Filters", exact: true }).click();
  131. await page.getByLabel("Pack size").selectOption("Fun pack");
  132. await expect(page.locator(".product-card")).toHaveCount(2);
  133. });
  134. test("contact validation and honest email preparation state", async ({
  135. page,
  136. }) => {
  137. await page.goto("/contact");
  138. await page.getByRole("button", { name: "SEND MESSAGE" }).click();
  139. await expect(page.locator("input[name=name]")).toBeFocused();
  140. await page.getByLabel("Your name").fill("Test Customer");
  141. await page
  142. .getByLabel("Email address", { exact: true })
  143. .first()
  144. .fill("customer@example.com");
  145. await page
  146. .getByLabel("Your message")
  147. .fill("I would like to ask about product ingredients.");
  148. await page.getByRole("button", { name: "SEND MESSAGE" }).click();
  149. await expect(page.locator(".form-status")).toContainText(
  150. "Send it there to reach ADUCKTIVE",
  151. );
  152. });
  153. test("newsletter validates email and does not claim a subscription", async ({
  154. page,
  155. }) => {
  156. await page.goto("/");
  157. await page.locator("#newsletter-email").fill("snacker@example.com");
  158. await page.getByRole("button", { name: "Subscribe to newsletter" }).click();
  159. await expect(page.locator(".newsletter [role=status]")).toContainText(
  160. "Send it to ask our team to subscribe you",
  161. );
  162. });
  163. test("journal filtering and article navigation", async ({ page }) => {
  164. await page.goto("/journal");
  165. await page
  166. .getByRole("searchbox", { name: "Search journal" })
  167. .fill("Mauritius");
  168. await expect(page.locator(".featured-article")).toContainText("Mauritius");
  169. await page.locator(".featured-article").click();
  170. await expect(page.getByRole("heading", { level: 1 })).toHaveText(
  171. "Mauritius, We’ve Arrived",
  172. );
  173. await expect(page.locator(".article-body")).toContainText("official debut");
  174. });
  175. test("major routes render without browser errors and respect every target width", async ({
  176. page,
  177. }) => {
  178. const errors: string[] = [];
  179. page.on("pageerror", (e) => errors.push(e.message));
  180. for (const width of [320, 390, 768, 1024, 1200, 1440]) {
  181. await page.setViewportSize({ width, height: 900 });
  182. for (const route of [
  183. "/",
  184. "/shop",
  185. "/collections",
  186. product,
  187. "/about",
  188. "/contact",
  189. "/journal",
  190. "/journal/90",
  191. "/cart",
  192. "/checkout",
  193. "/account",
  194. "/help/faq",
  195. "/help/shipping",
  196. "/help/returns",
  197. "/help/privacy",
  198. "/help/terms",
  199. ]) {
  200. await page.goto(route);
  201. await expect(
  202. page
  203. .getByRole("heading", { level: 1 })
  204. .or(page.getByRole("heading", { name: "Your cart is empty" }))
  205. .first(),
  206. ).toBeVisible();
  207. const overflow = await page.evaluate(
  208. () => document.documentElement.scrollWidth > innerWidth + 1,
  209. );
  210. expect(overflow, `${route} at ${width}px`).toBe(false);
  211. }
  212. }
  213. expect(errors).toEqual([]);
  214. });
  215. test("core pages pass automated WCAG checks", async ({ page }) => {
  216. await page.emulateMedia({ reducedMotion: "reduce" });
  217. for (const route of ["/", "/shop", product, "/contact", "/cart"]) {
  218. await page.goto(route);
  219. const result = await new AxeBuilder({ page })
  220. .withTags(["wcag2a", "wcag2aa", "wcag21aa"])
  221. .analyze();
  222. expect(
  223. result.violations.map((v) => ({
  224. id: v.id,
  225. nodes: v.nodes.map((n) => n.target),
  226. })),
  227. route,
  228. ).toEqual([]);
  229. }
  230. });
  231. test("scroll reveals appear and respect reduced-motion preferences", async ({
  232. page,
  233. }) => {
  234. await page.emulateMedia({ reducedMotion: "no-preference" });
  235. await page.goto("/");
  236. const story = page.locator(".story-copy");
  237. await expect(story).toHaveCSS("opacity", "0");
  238. await story.scrollIntoViewIfNeeded();
  239. await expect(story).toHaveCSS("opacity", "1");
  240. await page.emulateMedia({ reducedMotion: "reduce" });
  241. await page.reload();
  242. await expect(story).toHaveCSS("opacity", "1");
  243. await expect(page.locator(".pack-original")).toHaveCSS(
  244. "animation-name",
  245. "none",
  246. );
  247. });
  248. test("shop dropdown supports keyboard dismissal, outside click and category navigation", async ({ page }) => {
  249. await page.goto("/");
  250. const trigger = page.getByRole("button", { name: "Shop", exact: true });
  251. await trigger.focus();
  252. await page.keyboard.press("Enter");
  253. await expect(trigger).toHaveAttribute("aria-expanded", "true");
  254. await page.keyboard.press("Tab");
  255. await expect(page.locator("#desktop-shop-links a").first()).toBeFocused();
  256. await page.keyboard.press("Escape");
  257. await expect(trigger).toBeFocused();
  258. await expect(trigger).toHaveAttribute("aria-expanded", "false");
  259. await trigger.click();
  260. await page.locator("h1").click();
  261. await expect(trigger).toHaveAttribute("aria-expanded", "false");
  262. await trigger.click();
  263. await page.locator("#desktop-shop-links").getByRole("link", { name: "Tempeh chips" }).click();
  264. await expect(page).toHaveURL("/shop?category=Tempeh%20chips");
  265. await expect(page.locator(".product-card")).toHaveCount(2);
  266. await expect(trigger).toHaveAttribute("aria-expanded", "false");
  267. });
  268. test("mobile shop categories expand and close the drawer on selection", async ({ page }) => {
  269. await page.setViewportSize({ width: 390, height: 844 });
  270. await page.goto("/");
  271. await page.getByRole("button", { name: "Open menu" }).click();
  272. await page.getByRole("button", { name: "Shop categories" }).click();
  273. await page.locator("#mobile-shop-links").getByRole("link", { name: "Cornflakes", exact: true }).click();
  274. await expect(page).toHaveURL("/shop?category=Cornflakes");
  275. await expect(page.getByRole("dialog")).toHaveCount(0);
  276. await expect(page.getByLabel("Main navigation")).toBeHidden();
  277. });
  278. test("checkout preview supports delivery, payment, review and completion", async ({ page }) => {
  279. await page.goto(product);
  280. await page.getByRole("button", { name: "BUY NOW" }).click();
  281. await page.getByRole("button", { name: "REVIEW ORDER", exact: true }).click();
  282. await page.getByRole("button", { name: "SAVE ADDRESS" }).click();
  283. await expect(page.getByLabel("Full name", { exact: true })).toBeFocused();
  284. await page.getByLabel("Full name", { exact: true }).fill("Test Snacker");
  285. await page.getByLabel("Phone number", { exact: true }).fill("0123456789");
  286. await page.getByLabel("Street address", { exact: true }).fill("12 Test Street");
  287. await page.getByLabel("City", { exact: true }).fill("Subang Jaya");
  288. await page.getByLabel("Postcode", { exact: true }).fill("47500");
  289. await page.getByLabel("State / region", { exact: true }).fill("Selangor");
  290. await page.getByRole("button", { name: "SAVE ADDRESS" }).click();
  291. await expect(page.locator(".address-card")).toContainText("Test Snacker");
  292. await page.locator(".address-card button").click();
  293. await page.getByLabel("Full name", { exact: true }).fill("Unsaved edit");
  294. await page.keyboard.press("Escape");
  295. await expect(page.locator(".address-card")).toContainText("Test Snacker");
  296. await page.getByLabel("Voucher code").fill("SNACK10");
  297. await page.getByRole("button", { name: "Check code", exact: true }).click();
  298. await expect(page.locator(".checkout-option-note[role=status]")).toContainText("No discount has been applied");
  299. await page.getByRole("button", { name: "Increase The Original Salted Egg Cornflakes" }).click();
  300. await expect(page.locator(".checkout-order-summary .checkout-subtotal")).toContainText("RM 40.80");
  301. await page.getByRole("radio", { name: /Express delivery/ }).check();
  302. await expect(page.locator(".checkout-order-summary .checkout-subtotal")).toContainText("RM 45.80");
  303. await page.getByRole("radio", { name: /Online banking/ }).check();
  304. await page.getByRole("radio", { name: /E-wallet/ }).check();
  305. await page.getByRole("button", { name: "REVIEW ORDER", exact: true }).click();
  306. await expect(page.getByRole("dialog")).toContainText("Express delivery");
  307. await expect(page.getByRole("dialog")).toContainText("E-wallet");
  308. await expect(page.getByRole("dialog")).toContainText("RM 45.80");
  309. await expect(page.locator('.checkout-page a[href^="https:"]')).toHaveCount(0);
  310. await page.getByRole("button", { name: "COMPLETE CHECKOUT PREVIEW" }).click();
  311. await expect(page.getByRole("dialog")).toContainText("No order was placed, no payment was taken");
  312. await expect(page).toHaveURL("/checkout");
  313. await page.keyboard.press("Escape");
  314. await page.reload();
  315. await expect(page.locator(".address-card")).toContainText("Add a delivery address");
  316. await expect(page.locator(".checkout-card-stack")).not.toContainText("SNACK10");
  317. await page.getByRole("button", { name: "Remove The Original Salted Egg Cornflakes", exact: true }).click();
  318. await expect(page.getByRole("heading", { name: "Your cart is empty" })).toBeVisible();
  319. await expect(page.locator(".checkout-paybar")).toHaveCount(0);
  320. });
  321. test("local account forms validate and do not simulate authentication", async ({ page }) => {
  322. await page.goto("/account");
  323. await page.getByRole("link", { name: "CREATE AN ACCOUNT", exact: true }).click();
  324. await expect(page).toHaveURL("/register");
  325. await page.getByLabel("Full name", { exact: true }).fill("Test Snacker");
  326. await page.getByLabel("Email address", { exact: true }).fill("snacker@example.com");
  327. await page.getByLabel("Password", { exact: true }).fill("sample-password");
  328. await page.getByRole("button", { name: "Show password", exact: true }).click();
  329. await expect(page.locator("#club-password")).toHaveAttribute("type", "text");
  330. await page.getByLabel("Confirm password", { exact: true }).fill("different-password");
  331. await page.getByRole("button", { name: "CREATE AN ACCOUNT", exact: true }).click();
  332. await expect(page.locator("#club-confirm")).toBeFocused();
  333. expect(await page.locator("#club-confirm").evaluate((input: HTMLInputElement) => input.validity.customError)).toBe(true);
  334. await page.getByLabel("Confirm password", { exact: true }).fill("sample-password");
  335. await page.getByRole("button", { name: "CREATE AN ACCOUNT", exact: true }).click();
  336. await expect(page.locator(".club-form-status")).toContainText("no account has been created");
  337. await expect(page.locator("#club-password")).toHaveValue("");
  338. await page.getByRole("link", { name: "Log in", exact: true }).click();
  339. await expect(page).toHaveURL("/login");
  340. await page.getByLabel("Email address", { exact: true }).fill("snacker@example.com");
  341. await page.getByLabel("Password", { exact: true }).fill("sample-password");
  342. await page.getByRole("button", { name: "LOG IN", exact: true }).click();
  343. await expect(page.locator(".club-form-status")).toContainText("you haven’t been signed in");
  344. await expect(page.locator("#club-password")).toHaveValue("");
  345. await page.getByRole("link", { name: "Forgot your password?" }).click();
  346. await expect(page).toHaveURL("/forgot-password");
  347. await page.getByLabel("Email address", { exact: true }).fill("snacker@example.com");
  348. await page.getByRole("button", { name: "REQUEST PASSWORD RESET" }).click();
  349. await expect(page.locator(".club-form-status")).toContainText("no reset email has been sent");
  350. expect(await page.evaluate(() => JSON.stringify({...localStorage, ...sessionStorage}))).not.toContain("sample-password");
  351. });