Skip to content

Commit fd3a4b3

Browse files
committed
Merge branch 'develop' into v5/template-retail-react-app
2 parents 6169792 + ca0bc0b commit fd3a4b3

File tree

111 files changed

+3515
-767
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+3515
-767
lines changed

.github/actions/datadog/action.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ runs:
99
steps:
1010
- name: Send metrics to Datadog
1111
run : |
12+
# For the datadog cli, it must be installed via python
13+
# to install python packages on CI environment, we must activate the virtual env
14+
# or otherwise it throws error: externally-managed-environment
15+
python3 -m venv venv
16+
source venv/bin/activate
17+
pip install datadog
18+
1219
# Add a dogrc so we can submit metrics to datadog
1320
printf "[Connection]\napikey = ${{inputs.datadog_api_key}}\nappkey =\n" > ~/.dogrc
1421

.github/actions/setup_ubuntu/action.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,6 @@ runs:
1010
- name: Install Dependencies
1111
working-directory: ${{ inputs.cwd }}
1212
run: |-
13-
# Install system dependencies
14-
sudo apt-get update -yq
15-
sudo apt-get install python2 python3-pip time -yq
16-
sudo pip install -U pip setuptools
17-
sudo pip install awscli==1.18.85 datadog==0.40.1
18-
1913
# Install node dependencies
2014
node ./scripts/gtime.js monorepo_install npm ci
2115

e2e/scripts/pageHelpers.js

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
const { expect } = require("@playwright/test");
2+
const config = require("../config");
3+
const { getCreditCardExpiry } = require("../scripts/utils.js")
4+
5+
/**
6+
* Navigates to the `Cotton Turtleneck Sweater` PDP (Product Detail Page) on mobile
7+
* with the black variant selected
8+
*
9+
* @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright
10+
*/
11+
export const navigateToPDPMobile = async ({page}) => {
12+
// Home page
13+
await page.goto(config.RETAIL_APP_HOME);
14+
15+
await page.getByLabel("Menu", { exact: true }).click();
16+
17+
// SSR nav loads top level categories as direct links so we wait till all sub-categories load in the accordion
18+
const categoryAccordion = page.locator(
19+
"#category-nav .chakra-accordion__button svg+:text('Womens')"
20+
);
21+
await categoryAccordion.waitFor();
22+
23+
await page.getByRole("button", { name: "Womens" }).click();
24+
25+
const clothingNav = page.getByRole("button", { name: "Clothing" });
26+
27+
await clothingNav.waitFor();
28+
29+
await clothingNav.click();
30+
31+
const topsLink = page.getByLabel('Womens').getByRole("link", { name: "Tops" });
32+
await topsLink.click();
33+
// Wait for the nav menu to close first
34+
await topsLink.waitFor({state: 'hidden'})
35+
36+
await expect(page.getByRole("heading", { name: "Tops" })).toBeVisible();
37+
38+
// PLP
39+
const productTile = page.getByRole("link", {
40+
name: /Cotton Turtleneck Sweater/i,
41+
});
42+
await productTile.scrollIntoViewIfNeeded()
43+
// selecting swatch
44+
const productTileImg = productTile.locator("img");
45+
await productTileImg.waitFor({state: 'visible'})
46+
const initialSrc = await productTileImg.getAttribute("src");
47+
await expect(productTile.getByText(/From \$39\.99/i)).toBeVisible();
48+
49+
await productTile.getByLabel(/Black/, { exact: true }).click();
50+
// Make sure the image src has changed
51+
await expect(async () => {
52+
const newSrc = await productTileImg.getAttribute("src")
53+
expect(newSrc).not.toBe(initialSrc)
54+
}).toPass()
55+
await expect(productTile.getByText(/From \$39\.99/i)).toBeVisible();
56+
await productTile.click();
57+
}
58+
59+
/**
60+
* Navigates to the `Cotton Turtleneck Sweater` PDP (Product Detail Page) on Desktop
61+
* with the black variant selected.
62+
*
63+
* @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright
64+
*/
65+
export const navigateToPDPDesktop = async ({page}) => {
66+
await page.goto(config.RETAIL_APP_HOME);
67+
68+
await page.getByRole("link", { name: "Womens" }).hover();
69+
const topsNav = await page.getByRole("link", { name: "Tops", exact: true });
70+
await expect(topsNav).toBeVisible();
71+
72+
await topsNav.click();
73+
74+
// PLP
75+
const productTile = page.getByRole("link", {
76+
name: /Cotton Turtleneck Sweater/i,
77+
});
78+
// selecting swatch
79+
const productTileImg = productTile.locator("img");
80+
await productTileImg.waitFor({state: 'visible'})
81+
const initialSrc = await productTileImg.getAttribute("src");
82+
await expect(productTile.getByText(/From \$39\.99/i)).toBeVisible();
83+
84+
await productTile.getByLabel(/Black/, { exact: true }).hover();
85+
// Make sure the image src has changed
86+
await expect(async () => {
87+
const newSrc = await productTileImg.getAttribute("src")
88+
expect(newSrc).not.toBe(initialSrc)
89+
}).toPass()
90+
await expect(productTile.getByText(/From \$39\.99/i)).toBeVisible();
91+
await productTile.click();
92+
}
93+
94+
/**
95+
* Adds the `Cotton Turtleneck Sweater` product to the cart with the variant:
96+
* Color: Black
97+
* Size: L
98+
*
99+
* @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright
100+
* @param {Boolean} options.isMobile - Flag to indicate if device type is mobile or not, defaulted to false
101+
*/
102+
export const addProductToCart = async ({page, isMobile = false}) => {
103+
// Navigate to Cotton Turtleneck Sweater with Black color variant selected
104+
if(isMobile) {
105+
await navigateToPDPMobile({page})
106+
} else {
107+
await navigateToPDPDesktop({page})
108+
}
109+
110+
// PDP
111+
await expect(
112+
page.getByRole("heading", { name: /Cotton Turtleneck Sweater/i })
113+
).toBeVisible();
114+
await page.getByRole("radio", { name: "L", exact: true }).click();
115+
116+
await page.locator("button[data-testid='quantity-increment']").click();
117+
118+
// Selected Size and Color texts are broken into multiple elements on the page.
119+
// So we need to look at the page URL to verify selected variants
120+
const updatedPageURL = await page.url();
121+
const params = updatedPageURL.split("?")[1];
122+
expect(params).toMatch(/size=9LG/i);
123+
expect(params).toMatch(/color=JJ169XX/i);
124+
await page.getByRole("button", { name: /Add to Cart/i }).click();
125+
126+
const addedToCartModal = page.getByText(/2 items added to cart/i);
127+
128+
await addedToCartModal.waitFor();
129+
130+
await page.getByLabel("Close").click();
131+
}
132+
133+
/**
134+
* Registers a shopper with provided user credentials
135+
*
136+
* @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright
137+
* @param {Object} options.userCredentials - Object containing user credentials with the following properties:
138+
* - firstName
139+
* - lastName
140+
* - email
141+
* - password
142+
* @param {Boolean} options.isMobile - flag to indicate if device type is mobile or not, defaulted to false
143+
*/
144+
export const registerShopper = async ({page, userCredentials, isMobile = false}) => {
145+
// Create Account and Sign In
146+
await page.goto(config.RETAIL_APP_HOME + "/registration");
147+
148+
await page.waitForLoadState();
149+
150+
const registrationFormHeading = page.getByText(/Let's get started!/i);
151+
await registrationFormHeading.waitFor();
152+
153+
await page
154+
.locator("input#firstName")
155+
.fill(userCredentials.firstName);
156+
await page
157+
.locator("input#lastName")
158+
.fill(userCredentials.lastName);
159+
await page.locator("input#email").fill(userCredentials.email);
160+
await page
161+
.locator("input#password")
162+
.fill(userCredentials.password);
163+
164+
await page.getByRole("button", { name: /Create Account/i }).click();
165+
166+
await page.waitForLoadState();
167+
168+
await expect(
169+
page.getByRole("heading", { name: /Account Details/i })
170+
).toBeVisible();
171+
172+
if(!isMobile) {
173+
await expect(
174+
page.getByRole("heading", { name: /My Account/i })
175+
).toBeVisible();
176+
}
177+
178+
await expect(page.getByText(/Email/i)).toBeVisible();
179+
await expect(page.getByText(userCredentials.email)).toBeVisible();
180+
}
181+
182+
/**
183+
* Validates that the `Cotton Turtleneck Sweater` product appears in the Order History page
184+
*
185+
* @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright
186+
*/
187+
export const validateOrderHistory = async ({page}) => {
188+
await page.goto(config.RETAIL_APP_HOME + "/account/orders");
189+
await expect(
190+
page.getByRole("heading", { name: /Order History/i })
191+
).toBeVisible();
192+
193+
await page.getByRole('link', { name: 'View details' }).click();
194+
195+
await expect(
196+
page.getByRole("heading", { name: /Order Details/i })
197+
).toBeVisible();
198+
await expect(
199+
page.getByRole("heading", { name: /Cotton Turtleneck Sweater/i })
200+
).toBeVisible();
201+
await expect(page.getByText(/Color: Black/i)).toBeVisible();
202+
await expect(page.getByText(/Size: L/i)).toBeVisible();
203+
}
204+
205+
/**
206+
* Validates that the `Cotton Turtleneck Sweater` product appears in the Wishlist page
207+
*
208+
* @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright
209+
*/
210+
export const validateWishlist = async ({page}) => {
211+
await page.goto(config.RETAIL_APP_HOME + "/account/wishlist");
212+
213+
await expect(
214+
page.getByRole("heading", { name: /Wishlist/i })
215+
).toBeVisible();
216+
217+
await expect(
218+
page.getByRole("heading", { name: /Cotton Turtleneck Sweater/i })
219+
).toBeVisible();
220+
await expect(page.getByText(/Color: Black/i)).toBeVisible()
221+
await expect(page.getByText(/Size: L/i)).toBeVisible()
222+
}
223+
224+
/**
225+
* Attempts to log in a shopper with provided user credentials.
226+
*
227+
* @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright
228+
* @param {Object} options.userCredentials - Object containing user credentials with the following properties:
229+
* - firstName
230+
* - lastName
231+
* - email
232+
* - password
233+
*
234+
* @return {Boolean} - denotes whether or not login was successful
235+
*/
236+
export const loginShopper = async ({page, userCredentials}) => {
237+
try {
238+
await page.goto(config.RETAIL_APP_HOME + "/login");
239+
await page.locator("input#email").fill(userCredentials.email);
240+
await page
241+
.locator("input#password")
242+
.fill(userCredentials.password);
243+
await page.getByRole("button", { name: /Sign In/i }).click();
244+
245+
await page.waitForLoadState();
246+
247+
// redirected to Account Details page after logging in
248+
await expect(
249+
page.getByRole("heading", { name: /Account Details/i })
250+
).toBeVisible({ timeout: 2000 });
251+
return true;
252+
} catch {
253+
return false;
254+
}
255+
}
256+
257+
/**
258+
* Search for products by query string that takes you to the PLP
259+
*
260+
* @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright
261+
* @param {String} options.query - Product name other product related descriptors to search for
262+
* @param {Object} options.isMobile - Flag to indicate if device type is mobile or not, defaulted to false
263+
*/
264+
export const searchProduct = async ({page, query, isMobile = false}) => {
265+
await page.goto(config.RETAIL_APP_HOME);
266+
267+
// For accessibility reasons, we have two search bars
268+
// one for desktop and one for mobile depending on your device type
269+
const searchInputs = page.locator('input[aria-label="Search for products..."]');
270+
271+
let searchInput = isMobile ? searchInputs.nth(1) : searchInputs.nth(0);
272+
await searchInput.fill(query);
273+
await searchInput.press('Enter');
274+
275+
await page.waitForLoadState();
276+
}
277+
278+
/**
279+
* Checkout products that are in the cart
280+
*
281+
* @param {Object} options.page - Object that represents a tab/window in the browser provided by playwright
282+
* @param {Object} options.userCredentials - Object containing user credentials with the following properties:
283+
* - firstName
284+
* - lastName
285+
* - email
286+
* - password
287+
*/
288+
export const checkoutProduct = async ({ page, userCredentials }) => {
289+
await page.getByRole("link", { name: "Proceed to Checkout" }).click();
290+
291+
await expect(
292+
page.getByRole("heading", { name: /Contact Info/i })
293+
).toBeVisible();
294+
295+
await page.locator("input#email").fill("test@gmail.com");
296+
297+
await page.getByRole("button", { name: /Checkout as guest/i }).click();
298+
299+
// Confirm the email input toggles to show edit button on clicking "Checkout as guest"
300+
const step0Card = page.locator("div[data-testid='sf-toggle-card-step-0']");
301+
302+
await expect(step0Card.getByRole("button", { name: /Edit/i })).toBeVisible();
303+
304+
await expect(
305+
page.getByRole("heading", { name: /Shipping Address/i })
306+
).toBeVisible();
307+
308+
await page.locator("input#firstName").fill(userCredentials.firstName);
309+
await page.locator("input#lastName").fill(userCredentials.lastName);
310+
await page.locator("input#phone").fill(userCredentials.phone);
311+
await page
312+
.locator("input#address1")
313+
.fill(userCredentials.address.street);
314+
await page.locator("input#city").fill(userCredentials.address.city);
315+
await page
316+
.locator("select#stateCode")
317+
.selectOption(userCredentials.address.state);
318+
await page
319+
.locator("input#postalCode")
320+
.fill(userCredentials.address.zipcode);
321+
322+
await page
323+
.getByRole("button", { name: /Continue to Shipping Method/i })
324+
.click();
325+
326+
// Confirm the shipping details form toggles to show edit button on clicking "Checkout as guest"
327+
const step1Card = page.locator("div[data-testid='sf-toggle-card-step-1']");
328+
329+
await expect(step1Card.getByRole("button", { name: /Edit/i })).toBeVisible();
330+
331+
await expect(
332+
page.getByRole("heading", { name: /Shipping & Gift Options/i })
333+
).toBeVisible();
334+
335+
try {
336+
// sometimes the shipping & gifts section gets skipped
337+
// so there is no 'Continue to payment' button available
338+
const continueToPayment = page.getByRole("button", {
339+
name: /Continue to Payment/i
340+
});
341+
await expect(continueToPayment).toBeVisible({ timeout: 2000 });
342+
await continueToPayment.click();
343+
} catch {
344+
345+
}
346+
347+
await expect(page.getByRole("heading", { name: /Payment/i })).toBeVisible();
348+
const creditCardExpiry = getCreditCardExpiry();
349+
350+
await page.locator("input#number").fill("4111111111111111");
351+
await page.locator("input#holder").fill("John Doe");
352+
await page.locator("input#expiry").fill(creditCardExpiry);
353+
await page.locator("input#securityCode").fill("213");
354+
355+
await page.getByRole("button", { name: /Review Order/i }).click();
356+
357+
page
358+
.getByRole("button", { name: /Place Order/i })
359+
.first()
360+
.click();
361+
362+
// order confirmation
363+
const orderConfirmationHeading = page.getByRole("heading", {
364+
name: /Thank you for your order!/i,
365+
});
366+
await orderConfirmationHeading.waitFor();
367+
}

0 commit comments

Comments
 (0)