Skip to content

Commit

Permalink
🧪 [Frontend] 284 adding cypress e2e tests (open-telemetry#298)
Browse files Browse the repository at this point in the history
  • Loading branch information
xoscar authored Aug 16, 2022
1 parent 2bc4c1e commit 1de1a7d
Show file tree
Hide file tree
Showing 27 changed files with 3,080 additions and 1,702 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ coverage
out/
build
src/frontend/protos
next-env.d.ts
next-env.d.ts
src/frontend/cypress/videos
src/frontend/cypress/screenshots
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,5 @@ significant modifications will be credited to OpenTelemetry Authors.
([#273](https://github.com/open-telemetry/opentelemetry-demo/pull/273))
* Reimplemented Frontend app using [Next.js](https://nextjs.org/) Browser client
([#236](https://github.com/open-telemetry/opentelemetry-demo/pull/236))
* Added Frontend [Cypress](https://www.cypress.io/) E2E tests
([#298](https://github.com/open-telemetry/opentelemetry-demo/pull/298))
44 changes: 44 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,50 @@ services:
- shippingservice
logging: *logging

# Frontend Tests
frontendTests:
image: cypress/included:10.3.1-typescript
depends_on:
- frontend
profiles:
- tests
environment:
- CYPRESS_baseUrl=http://${FRONTEND_ADDR}
- NODE_ENV=production
working_dir: /cypress
volumes:
- ./src/frontend:/cypress

# Integration Tests
integrationTests:
image: ${IMAGE_NAME}:${IMAGE_VERSION}-integrationTests
container_name: integrationTests
profiles:
- tests
build:
context: ./
dockerfile: ./test/Dockerfile
environment:
- AD_SERVICE_ADDR
- CART_SERVICE_ADDR
- CHECKOUT_SERVICE_ADDR
- CURRENCY_SERVICE_ADDR
- EMAIL_SERVICE_ADDR
- PAYMENT_SERVICE_ADDR
- PRODUCT_CATALOG_SERVICE_ADDR
- RECOMMENDATION_SERVICE_ADDR
- SHIPPING_SERVICE_ADDR
depends_on:
- adservice
- cartservice
- checkoutservice
- currencyservice
- emailservice
- paymentservice
- productcatalogservice
- recommendationservice
- shippingservice

# PaymentService
paymentservice:
image: ${IMAGE_NAME}:${IMAGE_VERSION}-paymentservice
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/components/Ad/Ad.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CypressFields } from '../../utils/Cypress';
import { useAd } from '../../providers/Ad.provider';
import * as S from './Ad.styled';

Expand All @@ -7,7 +8,7 @@ const Ad = () => {
} = useAd();

return (
<S.Ad>
<S.Ad data-cy={CypressFields.Ad}>
<S.Link href={redirectUrl}>
<p>{text}</p>
</S.Link>
Expand Down
13 changes: 6 additions & 7 deletions src/frontend/components/CartDropdown/CartDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Link from 'next/link';
import { useEffect, useRef } from 'react';
import { CypressFields } from '../../utils/Cypress';
import { IProductCartItem } from '../../types/Cart';
import ProductPrice from '../ProductPrice';
import * as S from './CartDropdown.styled';
Expand All @@ -18,7 +19,7 @@ const CartDropdown = ({ productList, isOpen, onClose }: IProps) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
onClose();
}
}
};
// Bind the event listener
document.addEventListener('mousedown', handleClickOutside);

Expand All @@ -29,19 +30,17 @@ const CartDropdown = ({ productList, isOpen, onClose }: IProps) => {
}, [ref]);

return isOpen ? (
<S.CartDropdown ref={ref}>
<S.CartDropdown ref={ref} data-cy={CypressFields.CartDropdown}>
<div>
<S.Header>
<S.Title>Shopping Cart</S.Title>
<span onClick={onClose}>Close</span>
</S.Header>
<S.ItemList>
{!productList.length && (
<S.EmptyCart>Your shopping cart is empty</S.EmptyCart>
)}
{!productList.length && <S.EmptyCart>Your shopping cart is empty</S.EmptyCart>}
{productList.map(
({ quantity, product: { name, picture, id, priceUsd = { nanos: 0, currencyCode: 'USD', units: 0 } } }) => (
<S.Item key={id}>
<S.Item key={id} data-cy={CypressFields.CartDropdownItem}>
<S.ItemImage src={picture} alt={name} />
<S.ItemDetails>
<S.ItemName>{name}</S.ItemName>
Expand All @@ -54,7 +53,7 @@ const CartDropdown = ({ productList, isOpen, onClose }: IProps) => {
</S.ItemList>
</div>
<Link href="/cart">
<S.CartButton>Go to Shipping Cart</S.CartButton>
<S.CartButton data-cy={CypressFields.CartGoToShopping}>Go to Shipping Cart</S.CartButton>
</Link>
</S.CartDropdown>
) : null;
Expand Down
5 changes: 3 additions & 2 deletions src/frontend/components/CartIcon/CartIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { CypressFields } from '../../utils/Cypress';
import { useCart } from '../../providers/Cart.provider';
import CartDropdown from '../CartDropdown';
import * as S from './CartIcon.styled';
Expand All @@ -11,9 +12,9 @@ const CartIcon = () => {

return (
<>
<S.CartIcon onClick={() => setIsOpen(true)}>
<S.CartIcon data-cy={CypressFields.CartIcon} onClick={() => setIsOpen(true)}>
<S.Icon src="/icons/Hipster_CartIcon.svg" alt="Cart icon" title="Cart" />
{!!items.length && <S.ItemsCount>{items.length}</S.ItemsCount>}
{!!items.length && <S.ItemsCount data-cy={CypressFields.CartItemCount}>{items.length}</S.ItemsCount>}
</S.CartIcon>
<CartDropdown productList={items} isOpen={isOpen} onClose={() => setIsOpen(false)} />
</>
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/components/CheckoutForm/CheckoutForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Link from 'next/link';
import { useCallback, useState } from 'react';
import { CypressFields } from '../../utils/Cypress';
import Input from '../Input';
import * as S from './CheckoutForm.styled';

Expand Down Expand Up @@ -190,7 +191,7 @@ const CheckoutForm = ({ onSubmit }: IProps) => {
<Link href="/">
<S.CartButton $type="secondary">Continue Shopping</S.CartButton>
</Link>
<S.CartButton type="submit">Place Order</S.CartButton>
<S.CartButton data-cy={CypressFields.CheckoutPlaceOrder} type="submit">Place Order</S.CartButton>
</S.SubmitContainer>
</S.CheckoutForm>
);
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/components/CheckoutItem/CheckoutItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Image from 'next/image';
import { useState } from 'react';
import { CypressFields } from '../../utils/Cypress';
import { Address } from '../../protos/demo';
import { IProductCheckoutItem } from '../../types/Cart';
import ProductPrice from '../ProductPrice';
Expand All @@ -23,7 +24,7 @@ const CheckoutItem = ({
const [isCollapsed, setIsCollapsed] = useState(false);

return (
<S.CheckoutItem>
<S.CheckoutItem data-cy={CypressFields.CheckoutItem}>
<S.ItemDetails>
<S.ItemImage src={picture} alt={name} />
<S.Details>
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/components/CurrencySwitcher/CurrencySwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMemo } from 'react';
import getSymbolFromCurrency from 'currency-symbol-map';
import { useCurrency } from '../../providers/Currency.provider';
import * as S from './CurrencySwitcher.styled';
import { CypressFields } from '../../utils/Cypress';

const CurrencySwitcher = () => {
const { currencyCodeList, setSelectedCurrency, selectedCurrency } = useCurrency();
Expand All @@ -16,6 +17,7 @@ const CurrencySwitcher = () => {
name="currency_code"
onChange={event => setSelectedCurrency(event.target.value)}
value={selectedCurrency}
data-cy={CypressFields.CurrencySwitcher}
>
{currencyCodeList.map(currencyCode => (
<option key={currencyCode} value={currencyCode}>
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import * as S from './Footer.styled';
import SessionGateway from '../../gateways/Session.gateway';
import { CypressFields } from '../../utils/Cypress';

const currentYear = new Date().getFullYear();

Expand All @@ -18,7 +19,7 @@ const Footer = () => {
<div>
<p>This website is hosted for demo purpose only. It is not an actual shop.</p>
<p>
<span>session-id: {sessionId}</span>
<span data-cy={CypressFields.SessionId}>session-id: {sessionId}</span>
</p>
</div>
<p>
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/components/ProductCard/ProductCard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CypressFields } from '../../utils/Cypress';
import { Product } from '../../protos/demo';
import ProductPrice from '../ProductPrice';
import * as S from './ProductCard.styled';
Expand All @@ -20,7 +21,7 @@ const ProductCard = ({
}: IProps) => {
return (
<S.Link href={`/product/${id}`}>
<S.ProductCard>
<S.ProductCard data-cy={CypressFields.ProductCard}>
<S.Image $src={picture} />
<div>
<S.ProductName>{name}</S.ProductName>
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/components/ProductList/ProductList.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CypressFields } from '../../utils/Cypress';
import { Product } from '../../protos/demo';
import ProductCard from '../ProductCard';
import * as S from './ProductList.styled';
Expand All @@ -8,7 +9,7 @@ interface IProps {

const ProductList = ({ productList }: IProps) => {
return (
<S.ProductList>
<S.ProductList data-cy={CypressFields.ProductList}>
{productList.map(product => (
<ProductCard key={product.id} product={product} />
))}
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/components/ProductPrice/ProductPrice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import getSymbolFromCurrency from 'currency-symbol-map';
import { Money } from '../../protos/demo';
import { useCurrency } from '../../providers/Currency.provider';
import { CypressFields } from '../../utils/Cypress';

interface IProps {
price: Money;
Expand All @@ -27,7 +28,7 @@ const ProductPrice = ({ price }: IProps) => {
}, [selectedCurrency, price]);

return (
<span>
<span data-cy={CypressFields.ProductPrice}>
{currencySymbol} {units}.{nanos.toString().slice(0, 2)}
</span>
);
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/components/Recommendations/Recommendations.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CypressFields } from '../../utils/Cypress';
import { useAd } from '../../providers/Ad.provider';
import ProductCard from '../ProductCard';
import * as S from './Recommendations.styled';
Expand All @@ -6,7 +7,7 @@ const Recommendations = () => {
const { recommendedProductList } = useAd();

return (
<S.Recommendations>
<S.Recommendations data-cy={CypressFields.RecommendationList}>
<S.TitleContainer>
<S.Title>You May Also Like</S.Title>
</S.TitleContainer>
Expand Down
26 changes: 26 additions & 0 deletions src/frontend/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { defineConfig } from 'cypress';
import dotEnv from 'dotenv';
import dotenvExpand from 'dotenv-expand';
import { resolve } from 'path';

const myEnv = dotEnv.config({
path: resolve(__dirname, '../../.env'),
});
dotenvExpand.expand(myEnv);

const { FRONTEND_ADDR = '', NODE_ENV, FRONTEND_PORT = '8080' } = process.env;

const baseUrl = NODE_ENV === 'production' ? FRONTEND_ADDR : `http://localhost:${FRONTEND_PORT}`;

export default defineConfig({
env: {
baseUrl,
},
e2e: {
baseUrl,
setupNodeEvents(on, config) {
// implement node event listeners here
},
supportFile: false,
},
});
34 changes: 34 additions & 0 deletions src/frontend/cypress/e2e/Checkout.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { CypressFields, getElementByField } from '../../utils/Cypress';

describe('Checkout Flow', () => {
beforeEach(() => {
cy.visit('/');
});

it('should create an order with two items', () => {
getElementByField(CypressFields.ProductCard).first().click();
getElementByField(CypressFields.ProductAddToCart).click();

getElementByField(CypressFields.CartItemCount).should('contain', '1');

cy.visit('/');

getElementByField(CypressFields.ProductCard).last().click();
getElementByField(CypressFields.ProductAddToCart).click();

getElementByField(CypressFields.CartItemCount).should('contain', '2');

getElementByField(CypressFields.CartIcon).click();
getElementByField(CypressFields.CartGoToShopping).click();

cy.location('href').should('match', /\/cart$/);

getElementByField(CypressFields.CheckoutPlaceOrder).click();

cy.wait(5000);
cy.location('href').should('match', /\/checkout/);
getElementByField(CypressFields.CheckoutItem).should('have.length', 2);
});
});

export {};
25 changes: 25 additions & 0 deletions src/frontend/cypress/e2e/Home.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import getSymbolFromCurrency from 'currency-symbol-map';
import SessionGateway from '../../gateways/Session.gateway';
import { CypressFields, getElementByField } from '../../utils/Cypress';

describe('Home Page', () => {
beforeEach(() => {
cy.visit('/');
});

it('should validate the home page', () => {
getElementByField(CypressFields.HomePage).should('exist');
getElementByField(CypressFields.ProductCard, getElementByField(CypressFields.ProductList)).should('have.length', 9);

getElementByField(CypressFields.SessionId).should('contain', SessionGateway.getSession().userId);
});

it('should change currency', () => {
getElementByField(CypressFields.CurrencySwitcher).select('EUR');
getElementByField(CypressFields.ProductCard, getElementByField(CypressFields.ProductList)).should('have.length', 9);

getElementByField(CypressFields.CurrencySwitcher).should('have.value', 'EUR');

getElementByField(CypressFields.ProductCard).should('contain', getSymbolFromCurrency('EUR'));
});
});
35 changes: 35 additions & 0 deletions src/frontend/cypress/e2e/ProductDetail.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { CypressFields, getElementByField } from '../../utils/Cypress';

describe('Product Detail Page', () => {
beforeEach(() => {
cy.visit('/');
});

it('should validate the product detail page', () => {
getElementByField(CypressFields.ProductCard).first().click();

getElementByField(CypressFields.ProductDetail).should('exist');
getElementByField(CypressFields.ProductPicture).should('exist');
getElementByField(CypressFields.ProductName).should('exist');
getElementByField(CypressFields.ProductDescription).should('exist');
getElementByField(CypressFields.ProductAddToCart).should('exist');

getElementByField(CypressFields.ProductCard, getElementByField(CypressFields.RecommendationList)).should(
'have.length',
4
);
getElementByField(CypressFields.Ad).should('exist');
});

it('should add item to cart', () => {
getElementByField(CypressFields.ProductCard).first().click();
getElementByField(CypressFields.ProductAddToCart).click();

getElementByField(CypressFields.CartItemCount).should('contain', '1');
getElementByField(CypressFields.CartIcon).click();

getElementByField(CypressFields.CartDropdownItem).should('have.length', 1);
});
});

export {};
Loading

0 comments on commit 1de1a7d

Please sign in to comment.