added cypress tests and GitHub action config

This commit is contained in:
AB 2023-02-13 22:33:53 +05:00
parent d1d9e8c434
commit 2faae84491
27 changed files with 3680 additions and 20 deletions

23
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,23 @@
name: E2E on Chrome
on: [push]
jobs:
install:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Cypress run
uses: cypress-io/github-action@v3
with:
project: ./site
browser: chrome
build: yarn build
start: yarn start
wait-on: 'http://localhost:3000'
env:
COMMERCE_PROVIDER: ${{ secrets.COMMERCE_PROVIDER }}
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN: ${{ secrets.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN }}
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN: ${{ secrets.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN }}

13
cypress.config.js Normal file
View File

@ -0,0 +1,13 @@
//import axios from 'axios'
const axios = require('axios')
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/tests/**/*.spec.{js,jsx,ts,tsx}',
viewportHeight: 1000,
viewportWidth: 1280,
},
})

View File

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -0,0 +1,3 @@
Cypress.Commands.add('getBySel', (selector, ...args) => {
return cy.get(`[data-test=${selector}]`, ...args)
})

20
cypress/support/e2e.js Normal file
View File

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -0,0 +1,66 @@
describe('User Sign-up and Login', function () {
beforeEach(function () {
cy.intercept('POST', '/api/2022-07/graphql.json').as('signup')
})
it('should allow a visitor to sign-up,login and logout', function () {
const userInfo = {
firstName: 'Ashar',
lastName: 'Ali',
email: 'Ashar' + Math.random() + '@gmail.com',
password: 's3cret',
}
// Sign-up User
cy.visit('/')
cy.getBySel('avatarButton').should('be.visible').click()
cy.getBySel('loginModal').should('be.visible').and('contain', 'Sign Up')
cy.getBySel('signup').click()
cy.getBySel('signupModal').should('be.visible').and('contain', 'Log In')
cy.getBySel('signupModal').within(($form) => {
cy.getBySel('signup-first-name')
.click()
.type(userInfo.firstName)
.should('have.value', userInfo.firstName)
cy.getBySel('signup-last-name')
.click()
.type(userInfo.lastName)
.should('have.value', userInfo.lastName)
cy.getBySel('signup-email')
.click()
.type(userInfo.email)
.should('have.value', userInfo.email)
cy.getBySel('signup-password')
.type(userInfo.password)
.should('have.value', userInfo.password)
cy.getBySel('signup-submit').click()
cy.wait('@signup')
})
cy.getBySel('signupModal').should('not.exist')
// Login User
cy.getBySel('avatarButton').click()
cy.getBySel('signupModal').should('be.visible').and('contain', 'Log In')
cy.getBySel('LogIn').click()
cy.getBySel('loginModal')
.should('be.visible')
.and('contain', 'Sign Up')
.within(($form) => {
cy.getBySel('signin-email')
.click()
.type(userInfo.email)
.should('have.value', userInfo.email)
cy.getBySel('signin-password')
.type(userInfo.password)
.should('have.value', userInfo.password)
cy.getBySel('signin-submit').click()
cy.wait('@signup')
})
// logout user
cy.getBySel('avatarButton').click()
cy.getBySel('logoutButton').should('be.visible').click()
cy.getBySel('logoutButton').should('not.exist')
})
})

View File

@ -0,0 +1,18 @@
describe('Header', () => {
beforeEach(() => {
cy.visit('/')
})
it.only('the search bar returns the correct search results', () => {
cy.getBySel('search-input').eq(0).type('navy{enter}')
cy.get('.animated.fadeIn').contains('Showing 2 results for "navy"')
// search by Price: Low to high
cy.visit('/search?q=navy&sort=price-asc')
cy.getBySel('productPrice').eq(0).contains('2,500.00')
// search by Price: High to low
cy.visit('/search?q=navy&sort=price-desc')
cy.getBySel('productPrice').eq(0).contains('7,000.00')
})
})

View File

@ -0,0 +1,30 @@
describe('Shopping Cart', () => {
beforeEach(function () {
cy.intercept('GET', '/_next/data/development/en-US/product/*').as('product')
})
it('users can add and remove products to the cart', () => {
cy.visit('/')
cy.getBySel('product-tag').eq(0).click()
cy.getBySel('addToCart').should('be.visible').click()
cy.getBySel('cartItems').should('be.visible').and('contain', '1')
cy.getBySel('closeSidebar').should('be.visible').click()
//Add another product from related products
cy.getBySel('relatedProducts').eq(1).click()
cy.wait(2000)
cy.getBySel('product-tag').within(() => {
cy.getBySel('product-name').should('be.visible')
cy.getBySel('product-price').should('be.visible')
})
cy.getBySel('nextProductImage').should('be.visible').click()
cy.getBySel('previousProductImage').should('be.visible').click()
cy.getBySel('addToCart').should('be.visible').click()
cy.getBySel('cartItems').should('be.visible').and('contain', '2')
//View cart and remove item
cy.getBySel('goToCart').click()
cy.get('[data-test="removeItem"]:nth-child(1) button').first().click()
cy.getBySel('cartItems').should('be.visible').and('contain', '1')
})
})

2210
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,11 +7,14 @@
"dev": "turbo run dev",
"start": "turbo run start",
"types": "turbo run types",
"prettier-fix": "prettier --write ."
"prettier-fix": "prettier --write .",
"cypress:open": "cypress open",
"cypress:run": "cypress run"
},
"devDependencies": {
"axios": "^0.26.1",
"cypress": "^12.5.1",
"husky": "^8.0.1",
"prettier": "^2.7.1",
"turbo": "^1.4.6"
},
"husky": {

View File

@ -46,7 +46,7 @@ export const handler: MutationHook<SignupHook> = {
})
throwUserErrors(customerCreate?.customerUserErrors)
await handleAutomaticLogin(fetch, { email, password })
//await handleAutomaticLogin(fetch, { email, password })
return null
},

View File

@ -61,6 +61,7 @@ const LoginView: React.FC = () => {
<form
onSubmit={handleLogin}
className="w-80 flex flex-col justify-between p-3"
data-test="loginModal"
>
<div className="flex justify-center pb-12 ">
<Logo width="64px" height="64px" />
@ -77,12 +78,23 @@ const LoginView: React.FC = () => {
</a>
</div>
)}
<Input type="email" placeholder="Email" onChange={setEmail} />
<Input type="password" placeholder="Password" onChange={setPassword} />
<Input
type="email"
placeholder="Email"
data-test="signin-email"
onChange={setEmail}
/>
<Input
type="password"
placeholder="Password"
data-test="signin-password"
onChange={setPassword}
/>
<Button
variant="slim"
type="submit"
data-test="signin-submit"
loading={loading}
disabled={disabled}
>
@ -93,6 +105,7 @@ const LoginView: React.FC = () => {
{` `}
<a
className="text-accent-9 font-bold hover:underline cursor-pointer"
data-test="signup"
onClick={() => setModalView('SIGNUP_VIEW')}
>
Sign Up

View File

@ -70,6 +70,7 @@ const SignUpView: FC<Props> = () => {
<form
onSubmit={handleSignup}
className="w-80 flex flex-col justify-between p-3"
data-test="signupModal"
>
<div className="flex justify-center pb-12 ">
<Logo width="64px" height="64px" />
@ -83,10 +84,28 @@ const SignUpView: FC<Props> = () => {
}}
></div>
)}
<Input placeholder="First Name" onChange={setFirstName} />
<Input placeholder="Last Name" onChange={setLastName} />
<Input type="email" placeholder="Email" onChange={setEmail} />
<Input type="password" placeholder="Password" onChange={setPassword} />
<Input
data-test="signup-first-name"
placeholder="First Name"
onChange={setFirstName}
/>
<Input
data-test="signup-last-name"
placeholder="Last Name"
onChange={setLastName}
/>
<Input
type="email"
placeholder="Email"
data-test="signup-email"
onChange={setEmail}
/>
<Input
type="password"
placeholder="Password"
data-test="signup-password"
onChange={setPassword}
/>
<span className="text-accent-8">
<span className="inline-block align-middle ">
<Info width="15" height="15" />
@ -100,6 +119,7 @@ const SignUpView: FC<Props> = () => {
<Button
variant="slim"
type="submit"
data-test="signup-submit"
loading={loading}
disabled={disabled}
>
@ -112,6 +132,7 @@ const SignUpView: FC<Props> = () => {
{` `}
<a
className="text-accent-9 font-bold hover:underline cursor-pointer"
data-test="LogIn"
onClick={() => setModalView('LOGIN_VIEW')}
>
Log In

View File

@ -73,7 +73,7 @@ const CartSidebarView: FC = () => {
) : (
<>
<div className="px-4 sm:px-6 flex-1">
<Link href="/cart">
<Link href="/cart" data-test="goToCart">
<Text variant="sectionHeading" onClick={handleClose}>
My Cart
</Text>

View File

@ -43,6 +43,7 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
placeholder="Search for products..."
defaultValue={router.query.q}
onKeyUp={handleKeyUp}
data-test="search-input"
/>
<div className={s.iconContainer}>
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">

View File

@ -23,6 +23,7 @@ const SidebarLayout: FC<ComponentProps> = ({
onClick={handleClose}
aria-label="Close"
className="hover:text-accent-5 transition ease-in-out duration-150 flex items-center focus:outline-none mr-6"
data-test="closeSidebar"
>
<Cross className="h-6 w-6 hover:text-accent-3" />
<span className="ml-2 text-accent-7 text-sm ">Close</span>

View File

@ -70,6 +70,7 @@ export default function CustomerMenuContent() {
<DropdownMenuItem>
<a
className={cn(s.link, 'border-t border-accent-2 mt-4')}
data-test="logoutButton"
onClick={() => logout()}
>
Logout

View File

@ -38,6 +38,7 @@ const UserNav: React.FC<{
<li className={s.item}>
<Button
className={s.item}
data-test="cartItems"
variant="naked"
onClick={() => {
setSidebarView('CART_VIEW')
@ -62,12 +63,13 @@ const UserNav: React.FC<{
</li>
)}
{process.env.COMMERCE_CUSTOMERAUTH_ENABLED && (
<li className={s.item}>
<li className={s.item} data-test="avatarButton">
<Dropdown>
<DropdownTrigger>
<button
aria-label="Menu"
className={s.avatarButton}
data-set="avatarButton"
onClick={() => (isCustomerLoggedIn ? null : openModal())}
>
<Avatar />

View File

@ -46,7 +46,7 @@ const ProductCard: FC<Props> = ({
{variant === 'slim' && (
<>
<div className={s.header}>
<span>{product.name}</span>
<span>{product.name} </span>
</div>
{product?.images && (
<Image
@ -72,10 +72,10 @@ const ProductCard: FC<Props> = ({
)}
{!noNameTag && (
<div className={s.header}>
<h3 className={s.name}>
<h3 className={s.name} data-test="productName">
<span>{product.name}</span>
</h3>
<div className={s.price}>
<div className={s.price} data-test="productPrice">
{`${price} ${product.price?.currencyCode}`}
</div>
</div>

View File

@ -73,6 +73,7 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
aria-label="Add to Cart"
type="button"
className={s.button}
data-test="addToCart"
onClick={addToCart}
loading={loading}
disabled={variant?.availableForSale === false}

View File

@ -14,6 +14,7 @@ const ProductSliderControl: FC<ProductSliderControl> = ({ onPrev, onNext }) => (
className={cn(s.leftControl)}
onClick={onPrev}
aria-label="Previous Product Image"
data-test="previousProductImage"
>
<ArrowLeft />
</button>
@ -21,6 +22,7 @@ const ProductSliderControl: FC<ProductSliderControl> = ({ onPrev, onNext }) => (
className={cn(s.rightControl)}
onClick={onNext}
aria-label="Next Product Image"
data-test="nextProductImage"
>
<ArrowRight />
</button>

View File

@ -1,4 +1,5 @@
import cn from 'clsx'
import { inherits } from 'util'
import s from './ProductTag.module.css'
interface ProductTagProps {
@ -15,7 +16,7 @@ const ProductTag: React.FC<ProductTagProps> = ({
fontSize = 32,
}) => {
return (
<div className={cn(s.root, className)}>
<div className={cn(s.root, className)} data-test="product-tag">
<h3 className={s.name}>
<span
className={cn({ [s.fontsizing]: fontSize < 32 })}
@ -23,11 +24,14 @@ const ProductTag: React.FC<ProductTagProps> = ({
fontSize: `${fontSize}px`,
lineHeight: `${fontSize}px`,
}}
data-test="product-name"
>
{name}
</span>
</h3>
<div className={s.price}>{price}</div>
<div className={s.price} data-test="product-price">
{price}
</div>
</div>
)
}

View File

@ -69,7 +69,11 @@ const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
<Text variant="sectionHeading">Related Products</Text>
<div className={s.relatedProductsGrid}>
{relatedProducts.map((p) => (
<div key={p.path} className="bg-accent-0 border border-accent-2">
<div
key={p.path}
className="bg-accent-0 border border-accent-2"
data-test="relatedProducts"
>
<ProductCard
noNameTag
product={p}

View File

@ -320,7 +320,10 @@ export default function Search({ categories, brands }: SearchPropsType) {
</div>
)}
{data ? (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div
className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"
data-test="productsCard"
>
{data.products.map((product: Product) => (
<ProductCard
variant="simple"

View File

@ -91,6 +91,7 @@ export default function Cart() {
key={item.id}
item={item}
currencyCode={data?.currency.code!}
data-test="removeItem"
/>
))}
</ul>

View File

@ -23,8 +23,8 @@
"@components/*": ["components/*"],
"@commerce": ["../packages/commerce/src"],
"@commerce/*": ["../packages/commerce/src/*"],
"@framework": ["../packages/local/src"],
"@framework/*": ["../packages/local/src/*"]
"@framework": ["../packages/shopify/src"],
"@framework/*": ["../packages/shopify/src/*"]
}
},
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],

1215
yarn.lock Normal file

File diff suppressed because it is too large Load Diff