added cypress tests and GitHub action config

This commit is contained in:
Tai Nguyen Trong 2023-09-06 14:07:07 +07:00
parent d8703e8140
commit 25e0472fc7
16 changed files with 896 additions and 54 deletions

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

@ -0,0 +1,22 @@
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: .
browser: chrome
build: pnpm build
start: pnpm start
wait-on: 'http://localhost:3000'
env:
SHOPIFY_STOREFRONT_ACCESS_TOKEN: ${{ secrets.SHOPIFY_STOREFRONT_ACCESS_TOKEN }}
SHOPIFY_STORE_DOMAIN: ${{ secrets.SHOPIFY_STORE_DOMAIN }}

View File

@ -4,7 +4,7 @@ import { GridTileImage } from './grid/tile';
export async function Carousel() { export async function Carousel() {
// Collections that start with `hidden-*` are hidden from the search page. // Collections that start with `hidden-*` are hidden from the search page.
const products = await getCollectionProducts({ collection: 'hidden-homepage-carousel' }); const products = await getCollectionProducts({ collection: 'automated-collection' });
if (!products?.length) return null; if (!products?.length) return null;

View File

@ -40,7 +40,7 @@ function ThreeItemGridItem({
export async function ThreeItemGrid() { export async function ThreeItemGrid() {
// Collections that start with `hidden-*` are hidden from the search page. // Collections that start with `hidden-*` are hidden from the search page.
const homepageItems = await getCollectionProducts({ const homepageItems = await getCollectionProducts({
collection: 'hidden-homepage-featured-items' collection: 'hydrogen'
}); });
if (!homepageItems[0] || !homepageItems[1] || !homepageItems[2]) return null; if (!homepageItems[0] || !homepageItems[1] || !homepageItems[2]) return null;
@ -48,7 +48,10 @@ export async function ThreeItemGrid() {
const [firstProduct, secondProduct, thirdProduct] = homepageItems; const [firstProduct, secondProduct, thirdProduct] = homepageItems;
return ( return (
<section className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2"> <section
className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2"
data-test="three-items-grid"
>
<ThreeItemGridItem size="full" item={firstProduct} priority={true} /> <ThreeItemGridItem size="full" item={firstProduct} priority={true} />
<ThreeItemGridItem size="half" item={secondProduct} priority={true} /> <ThreeItemGridItem size="half" item={secondProduct} priority={true} />
<ThreeItemGridItem size="half" item={thirdProduct} /> <ThreeItemGridItem size="half" item={thirdProduct} />

View File

@ -17,9 +17,15 @@ const Label = ({
className={clsx('absolute bottom-0 left-0 flex w-full px-4 pb-4 @container/label', { className={clsx('absolute bottom-0 left-0 flex w-full px-4 pb-4 @container/label', {
'lg:px-20 lg:pb-[35%]': position === 'center' 'lg:px-20 lg:pb-[35%]': position === 'center'
})} })}
data-test="product-label"
> >
<div className="flex items-center rounded-full border bg-white/70 p-1 text-xs font-semibold text-black backdrop-blur-md dark:border-neutral-800 dark:bg-black/70 dark:text-white"> <div className="flex items-center rounded-full border bg-white/70 p-1 text-xs font-semibold text-black backdrop-blur-md dark:border-neutral-800 dark:bg-black/70 dark:text-white">
<h3 className="mr-4 line-clamp-2 flex-grow pl-2 leading-none tracking-tight">{title}</h3> <h3
className="mr-4 line-clamp-2 flex-grow pl-2 leading-none tracking-tight"
data-test="product-name"
>
{title}
</h3>
<Price <Price
className="flex-none rounded-full bg-blue-600 p-2 text-white" className="flex-none rounded-full bg-blue-600 p-2 text-white"
amount={amount} amount={amount}

View File

@ -19,7 +19,11 @@ export default async function Navbar() {
</div> </div>
<div className="flex w-full items-center"> <div className="flex w-full items-center">
<div className="flex w-full md:w-1/3"> <div className="flex w-full md:w-1/3">
<Link href="/" className="mr-2 flex w-full items-center justify-center md:w-auto lg:mr-6"> <Link
href="/"
className="mr-2 flex w-full items-center justify-center md:w-auto lg:mr-6"
data-test="logo"
>
<LogoSquare /> <LogoSquare />
<div className="ml-2 flex-none text-sm font-medium uppercase md:hidden lg:block"> <div className="ml-2 flex-none text-sm font-medium uppercase md:hidden lg:block">
{SITE_NAME} {SITE_NAME}

View File

@ -41,6 +41,7 @@ export default function Search() {
value={searchValue} value={searchValue}
onChange={(e) => setSearchValue(e.target.value)} onChange={(e) => setSearchValue(e.target.value)}
className="w-full rounded-lg border bg-white px-4 py-2 text-sm text-black placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400" className="w-full rounded-lg border bg-white px-4 py-2 text-sm text-black placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400"
data-test="search-input"
/> />
<div className="absolute right-0 top-0 mr-3 flex h-full items-center"> <div className="absolute right-0 top-0 mr-3 flex h-full items-center">
<MagnifyingGlassIcon className="h-4" /> <MagnifyingGlassIcon className="h-4" />

View File

@ -11,7 +11,7 @@ const Price = ({
currencyCode: string; currencyCode: string;
currencyCodeClassName?: string; currencyCodeClassName?: string;
} & React.ComponentProps<'p'>) => ( } & React.ComponentProps<'p'>) => (
<p suppressHydrationWarning={true} className={className}> <p suppressHydrationWarning={true} className={className} data-test="product-price">
{`${new Intl.NumberFormat(undefined, { {`${new Intl.NumberFormat(undefined, {
style: 'currency', style: 'currency',
currency: currencyCode, currency: currencyCode,

10
cypress.config.ts Normal file
View File

@ -0,0 +1,10 @@
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
baseUrl: 'http://localhost:3000'
}
});

21
cypress/e2e/header.cy.ts Normal file
View File

@ -0,0 +1,21 @@
describe('Header', () => {
beforeEach(() => {
cy.visit('/');
});
it('links to the correct pages', () => {
cy.getBySel('logo').click();
cy.location('pathname').should('eq', '/');
});
it.only('the search bar returns the correct search results', () => {
cy.getBySel('search-input').type('liquid{enter}');
cy.getBySel('product-label')
.should('have.length', 1)
.within(() => {
cy.getBySel('product-name').should('contain', 'The Collection Snowboard: Liquid');
cy.getBySel('product-price').should('contain', '₫750VND');
});
});
});

28
cypress/e2e/home.cy.ts Normal file
View File

@ -0,0 +1,28 @@
describe('Home Page', () => {
it('displays all 3 products on the home page', () => {
cy.visit('http://localhost:3000');
cy.getBySel('three-items-grid').within(() => {
cy.getBySel('product-label')
.eq(0)
.within(() => {
cy.getBySel('product-name').should('contain', 'The Collection Snowboard: Liquid');
cy.getBySel('product-price').should('contain', '₫750VND');
});
cy.getBySel('product-label')
.eq(1)
.within(() => {
cy.getBySel('product-name').should('contain', 'The Collection Snowboard: Oxygen');
cy.getBySel('product-price').should('contain', '₫1,025VND');
});
cy.getBySel('product-label')
.eq(2)
.within(() => {
cy.getBySel('product-name').should('contain', 'The Collection Snowboard: Hydrogen');
cy.getBySel('product-price').should('contain', '₫600VND');
});
});
});
});

View File

@ -0,0 +1,9 @@
describe('Shopping Cart', () => {
it('users can add products to the cart', () => {
cy.visit('/');
cy.getBySel('product-label').eq(0).click();
cy.location('pathname').should('include', '/product/');
cy.get('[aria-label="Add item to cart"]').click();
});
});

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,40 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
declare namespace Cypress {
interface Chainable<Subject> {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
getBySel(selector: string, ...args: any[]): Chainable<Subject>;
}
}
Cypress.Commands.add('getBySel', (selector, ...args) => {
return cy.get(`[data-test=${selector}]`, ...args);
});

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

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts 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

@ -13,7 +13,9 @@
"lint-staged": "lint-staged", "lint-staged": "lint-staged",
"prettier": "prettier --write --ignore-unknown .", "prettier": "prettier --write --ignore-unknown .",
"prettier:check": "prettier --check --ignore-unknown .", "prettier:check": "prettier --check --ignore-unknown .",
"test": "pnpm lint && pnpm prettier:check" "test": "pnpm lint && pnpm prettier:check",
"cypress:open": "cypress open",
"cypress:run": "cypress run"
}, },
"git": { "git": {
"pre-commit": "lint-staged" "pre-commit": "lint-staged"
@ -25,7 +27,7 @@
"@headlessui/react": "^1.7.15", "@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"next": "13.4.13-canary.15", "next": "latest",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0" "react-dom": "18.2.0"
}, },
@ -37,6 +39,7 @@
"@types/react-dom": "18.2.7", "@types/react-dom": "18.2.7",
"@vercel/git-hooks": "^1.0.0", "@vercel/git-hooks": "^1.0.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"cypress": "^13.1.0",
"eslint": "^8.45.0", "eslint": "^8.45.0",
"eslint-config-next": "^13.4.12", "eslint-config-next": "^13.4.12",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",

762
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff