├── .DS_Store
├── .backtracejsrc
├── .dockerignore
├── .env
├── .github
├── CODEOWNERS
└── workflows
│ ├── github-pages.yml
│ └── sample-app-web.yml
├── .gitignore
├── .sauce
└── config.yml
├── .storybook
├── main.js
└── preview.js
├── Dockerfile
├── Jenkinsfile
├── LICENSE
├── README.md
├── babel.config.js
├── orchestrate.sh
├── package-lock.json
├── package.json
├── public
├── 404.html
├── CNAME
├── SL-symbol-color.png
├── favicon.ico
├── frames
│ ├── frame-content.html
│ ├── frame.html
│ ├── global.css
│ ├── header.html
│ ├── iframe-content.html
│ ├── iframe.html
│ ├── index.html
│ ├── main.html
│ └── sidebar.html
├── icon-192x192.png
├── icon-256x256.png
├── icon-384x384.png
├── icon-512x512.png
├── index.html
├── manifest.json
├── manifest.webmanifest
├── robots.txt
└── v1
│ ├── cart.html
│ ├── checkout-complete.html
│ ├── checkout-step-one.html
│ ├── checkout-step-two.html
│ ├── css
│ └── sample-app-web.css
│ ├── img
│ ├── HeaderBar_Bot.png
│ ├── Login_Bot_graphic.png
│ ├── SwagBot_Footer_graphic.png
│ ├── SwagLabs_logo.png
│ ├── bike-light-1200x1500.jpg
│ ├── bolt-shirt-1200x1500.jpg
│ ├── cart.png
│ ├── facebook.png
│ ├── linkedIn.png
│ ├── peek.png
│ ├── pony-express.png
│ ├── red-onesie-1200x1500.jpg
│ ├── red-tatt-1200x1500.jpg
│ ├── sauce-backpack-1200x1500.jpg
│ ├── sauce-pullover-1200x1500.jpg
│ ├── sl-404.jpg
│ └── twitter.png
│ ├── index.html
│ ├── inventory-item.html
│ ├── inventory.html
│ └── main.js
├── scripts
└── postbuild.sh
├── src
├── .DS_Store
├── assets
│ ├── img
│ │ ├── arrow.png
│ │ ├── back-arrow.png
│ │ ├── bike-light-1200x1500.jpg
│ │ ├── bolt-shirt-1200x1500.jpg
│ │ ├── cart.png
│ │ ├── checkmark.png
│ │ ├── close.png
│ │ ├── facebook.png
│ │ ├── filter.png
│ │ ├── linkedIn.png
│ │ ├── menu.png
│ │ ├── pony-express.png
│ │ ├── red-onesie-1200x1500.jpg
│ │ ├── red-tatt-1200x1500.jpg
│ │ ├── sauce-backpack-1200x1500.jpg
│ │ ├── sauce-pullover-1200x1500.jpg
│ │ ├── sl-404.jpg
│ │ └── twitter.png
│ └── svg
│ │ ├── arrow3x.svg
│ │ ├── cart3x.svg
│ │ ├── close@3x.svg
│ │ ├── filter3x.svg
│ │ ├── logo3x.svg
│ │ └── menu3x.svg
├── components
│ ├── BrokenComponent.jsx
│ ├── Button.css
│ ├── Button.jsx
│ ├── CartButton.css
│ ├── CartButton.jsx
│ ├── CartItem.css
│ ├── CartItem.jsx
│ ├── DrawerMenu.css
│ ├── DrawerMenu.jsx
│ ├── ErrorMessage.css
│ ├── ErrorMessage.jsx
│ ├── Footer.css
│ ├── Footer.jsx
│ ├── HeaderContainer.css
│ ├── HeaderContainer.jsx
│ ├── InputError.css
│ ├── InputError.jsx
│ ├── InventoryListItem.css
│ ├── InventoryListItem.jsx
│ ├── PrivateRoute.jsx
│ ├── Select.css
│ ├── Select.jsx
│ ├── SubmitButton.css
│ ├── SubmitButton.jsx
│ └── __tests__
│ │ ├── BrokenComponent.tests.js
│ │ ├── Button.tests.js
│ │ ├── CartButton.tests.js
│ │ ├── CartItem.tests.js
│ │ ├── DrawerMenu.tests.js
│ │ ├── ErrorMessage.tests.js
│ │ ├── Footer.tests.js
│ │ ├── HeaderContainer.tests.js
│ │ ├── InputError.tests.js
│ │ ├── InventoryListItem.tests.js
│ │ ├── Select.tests.js
│ │ ├── SubmitButton.tests.js
│ │ └── __snapshots__
│ │ ├── Button.tests.js.snap
│ │ ├── CartButton.tests.js.snap
│ │ ├── CartItem.tests.js.snap
│ │ ├── DrawerMenu.tests.js.snap
│ │ ├── ErrorMessage.tests.js.snap
│ │ ├── Footer.tests.js.snap
│ │ ├── HeaderContainer.tests.js.snap
│ │ ├── InputError.tests.js.snap
│ │ ├── InventoryListItem.tests.js.snap
│ │ ├── Select.tests.js.snap
│ │ └── SubmitButton.tests.js.snap
├── img
│ └── .DS_Store
├── index.css
├── index.jsx
├── pages
│ ├── Cart.css
│ ├── Cart.jsx
│ ├── CheckOutStepOne.css
│ ├── CheckOutStepOne.jsx
│ ├── CheckOutStepTwo.css
│ ├── CheckOutStepTwo.jsx
│ ├── Finish.css
│ ├── Finish.jsx
│ ├── Inventory.css
│ ├── Inventory.jsx
│ ├── InventoryItem.css
│ ├── InventoryItem.jsx
│ ├── Login.css
│ ├── Login.jsx
│ └── __tests__
│ │ ├── Cart.tests.js
│ │ ├── CheckOutStepOne.tests.js
│ │ ├── CheckOutStepTwo.tests.js
│ │ ├── Finish.tests.js
│ │ ├── Inventory.tests.js
│ │ ├── InventoryItem.tests.js
│ │ ├── Login.tests.js
│ │ └── __snapshots__
│ │ ├── Cart.tests.js.snap
│ │ ├── CheckOutStepOne.tests.js.snap
│ │ ├── CheckOutStepTwo.tests.js.snap
│ │ ├── Finish.tests.js.snap
│ │ ├── Inventory.tests.js.snap
│ │ ├── InventoryItem.tests.js.snap
│ │ └── Login.tests.js.snap
├── service-worker.js
├── serviceWorkerRegistration.js
├── setupTests.js
├── storybook
│ └── stories
│ │ ├── Button.stories.js
│ │ ├── CartItem.stories.js
│ │ ├── ErrorMessage.stories.js
│ │ ├── HeaderContainer.stories.js
│ │ ├── InputError.stories.js
│ │ ├── InventoryListItem.stories.js
│ │ ├── Select.stories.js
│ │ ├── SubmitButton.stories.js
│ │ └── assets
│ │ ├── code-brackets.svg
│ │ ├── colors.svg
│ │ ├── comments.svg
│ │ ├── direction.svg
│ │ ├── flow.svg
│ │ ├── plugin.svg
│ │ ├── repo.svg
│ │ └── stackalt.svg
└── utils
│ ├── Constants.js
│ ├── Credentials.js
│ ├── InventoryData.js
│ ├── InventoryDataLong.js
│ ├── Sorting.js
│ ├── __mocks__
│ └── fileMock.js
│ ├── __tests__
│ ├── Credentials.tests.js
│ ├── Sorting.tests.js
│ ├── __snapshots__
│ │ └── Sorting.tests.js.snap
│ └── shopping-cart.tests.js
│ └── shopping-cart.js
└── test
├── e2e
├── configs
│ ├── e2eConstants.js
│ ├── wdio.local.chrome.conf.js
│ ├── wdio.saucelabs-orchestrate.conf.js
│ ├── wdio.saucelabs.conf.js
│ └── wdio.shared.conf.js
├── helpers
│ └── index.js
├── page-objects
│ ├── AppHeaderPage.js
│ ├── BasePage.js
│ ├── CartSummaryPage.js
│ ├── CheckoutCompletePage.js
│ ├── CheckoutPersonalInfoPage.js
│ ├── CheckoutSummaryPage.js
│ ├── LoginPage.js
│ ├── MenuPage.js
│ ├── SwagDetailsPage.js
│ └── SwagOverviewPage.js
└── specs
│ ├── cart.summary.spec.js
│ ├── checkout.complete.spec.js
│ ├── checkout.personal.info.spec.js
│ ├── checkout.summary.spec.js
│ ├── login.spec.js
│ ├── menu.spec.js
│ ├── swag.item.details.spec.js
│ └── swag.items.list.spec.js
└── visual
└── storybook
├── ci.config.js
├── desktop.config.js
└── mobile.config.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/.DS_Store
--------------------------------------------------------------------------------
/.backtracejsrc:
--------------------------------------------------------------------------------
1 | {
2 | "path": "./build/static/js",
3 | "run": {
4 | "upload": true,
5 | "process": true
6 | },
7 | "upload": {
8 | "url": "https://submit.backtrace.io/UNIVERSE/TOKEN/sourcemap",
9 | "include-sources": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | artifacts
2 | sc-orchestrate.log
3 | servier-orchestrate.log
4 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
2 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @wswebcreation @diemol
2 |
--------------------------------------------------------------------------------
/.github/workflows/github-pages.yml:
--------------------------------------------------------------------------------
1 | name: github pages release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | # paths:
8 | # - '.storybook'
9 | # - 'public'
10 | # - 'src'
11 |
12 | jobs:
13 | deploy:
14 | runs-on: ubuntu-latest
15 | defaults:
16 | run:
17 | working-directory: ./
18 | steps:
19 | - uses: actions/checkout@v2
20 |
21 | - name: Build
22 | uses: actions/setup-node@v2
23 | with:
24 | node-version: '14.x'
25 |
26 | - name: Build site
27 | env:
28 | CI: false
29 | run: |
30 | ls
31 | npm ci
32 | npm run build
33 |
34 | - name: Build storybook
35 | env:
36 | CI: false
37 | run: |
38 | ls
39 | npm run build.storybook
40 |
41 | - name: Deploy
42 | uses: peaceiris/actions-gh-pages@v3
43 | with:
44 | github_token: ${{ secrets.GITHUB_TOKEN }}
45 | publish_dir: ./build
46 | cname: www.saucedemo.com
47 |
--------------------------------------------------------------------------------
/.github/workflows/sample-app-web.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Swag Labs Sample App Workflow
5 |
6 | on:
7 | push:
8 | branches:
9 | - main
10 | pull_request:
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | defaults:
16 | run:
17 | working-directory: ./
18 | env:
19 | BUILD_PREFIX: true
20 | IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
21 | SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
22 | SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
23 | SCREENER_API_KEY: ${{ secrets.SCREENER_API_KEY }}
24 |
25 | strategy:
26 | matrix:
27 | node-version: [14.x]
28 |
29 | steps:
30 | - name: Checkout Repository
31 | uses: actions/checkout@v2
32 |
33 | - name: Setup Node.js ${{ matrix.node-version }}
34 | uses: actions/setup-node@v1
35 | with:
36 | node-version: ${{ matrix.node-version }}
37 |
38 | # Site Testing steps
39 | - name: Install dependencies
40 | run: npm ci
41 |
42 | - name: Build
43 | run: CI=false npm run build
44 |
45 | - name: Run Unit Tests and generate coverage report
46 | run: npm run test.coverage
47 |
48 | - name: Upload coverage to Codecov
49 | uses: codecov/codecov-action@v1
50 | with:
51 | token: ${{ secrets.CODECOV_TOKEN }}
52 |
53 | # Only run the last 2 steps when we are not on the main branch
54 | - name: Run Storybook tests
55 | if: ${{ !env.IS_MAIN }}
56 | run: npm run test.storybook.ci
57 |
58 | - name: Build and E2E test the site
59 | if: ${{ !env.IS_MAIN }}
60 | run: npm run start & npx wait-on --timeout 60000 http://localhost:3000 && npm run test.e2e.sauce.us
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | **/.DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 | *.log
21 | .project
22 | bin
23 | dist
24 | screenshots
25 | .idea
26 | .coverage
27 | storybook-static
28 | .tmp
29 | .eslintcache
30 |
--------------------------------------------------------------------------------
/.sauce/config.yml:
--------------------------------------------------------------------------------
1 | apiVersion: v1alpha
2 | kind: imagerunner
3 | sauce:
4 | region: us-west-1
5 | concurrency: 2
6 | suites:
7 | - name: Start Web App
8 | workload: webdriver
9 | image: mikedonovan1987/sample-app-web-orchestrate:0.0.4
10 | imagePullAuth:
11 | user: $DOCKER_USERNAME
12 | token: $DOCKER_PASSWORD
13 | entrypoint: "./orchestrate.sh"
14 |
15 | - name: Demo App Tests
16 | workload: webdriver
17 | image: mikedonovan1987/sample-app-web-orchestrate:0.0.4
18 | imagePullAuth:
19 | user: $DOCKER_USERNAME
20 | token: $DOCKER_PASSWORD
21 | entrypoint: "npm run test.e2e.sauce.us-orchestrate"
22 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "stories": [
3 | "../src/storybook/**/*.stories.mdx",
4 | "../src/storybook/**/*.stories.@(js|jsx|ts|tsx)"
5 | ],
6 | "addons": [
7 | "@storybook/addon-links",
8 | "@storybook/addon-essentials",
9 | "@storybook/preset-create-react-app"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import '../src/index.css';
2 |
3 | export const parameters = {
4 | actions: { argTypesRegex: "^on[A-Z].*" },
5 | }
6 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14
2 |
3 | # install sauce connect
4 | RUN curl -LO https://saucelabs.com/downloads/sc-4.8.2-linux.tar.gz
5 | RUN tar xvf ./sc-4.8.2-linux.tar.gz
6 | ENV PATH="$HOME/sc-4.8.2-linux/bin:$PATH"
7 |
8 | # web app
9 | WORKDIR /sample-app-web
10 | COPY . .
11 | RUN npm ci
12 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | pipeline {
2 | agent any
3 |
4 | tools {nodejs "12.6"}
5 |
6 | stages {
7 |
8 | stage('Install dependencies') {
9 | steps {
10 | sh "npm install"
11 | }
12 | }
13 |
14 | stage('Build application') {
15 | steps {
16 | sh "npm start & npx wait-on --timeout 60000 http://localhost:3000 &"
17 | }
18 | }
19 |
20 | stage('Run Functional Tests') {
21 | steps {
22 | sh "npm run test.e2e.sauce.eu ${env.CLI_ARGS}"
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Sauce Labs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', {
4 | targets: {
5 | node: 12
6 | }
7 | }]
8 | ],
9 | plugins: ['@babel/plugin-proposal-private-methods']
10 | };
11 |
--------------------------------------------------------------------------------
/orchestrate.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | sc -u $SAUCE_USERNAME -k $SAUCE_ACCESS_KEY --region us-west --tunnel-name mdonovan2010_tunnel_name &> sc-orchestrate.log &
4 |
5 | npm run start
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Single Page Apps for GitHub Pages
6 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/CNAME:
--------------------------------------------------------------------------------
1 | www.saucedemo.com
2 |
--------------------------------------------------------------------------------
/public/SL-symbol-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/SL-symbol-color.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/favicon.ico
--------------------------------------------------------------------------------
/public/frames/frame-content.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/public/frames/frame.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Merchant Detail Page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Your browser does not support frames.
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/public/frames/global.css:
--------------------------------------------------------------------------------
1 | iframe, frame {
2 | border: none;
3 | }
4 |
5 | body, html {
6 | height: 100%;
7 | margin: 0;
8 | padding: 0;
9 | }
10 |
--------------------------------------------------------------------------------
/public/frames/header.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
51 |
52 |
53 |
54 |
57 |
58 |
64 |
65 |
66 |
67 |
68 |
69 | Welcome to My Website
70 | Explore our services and learn more about us!
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/public/frames/iframe-content.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
28 |
29 |
30 |
31 |
32 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/public/frames/iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Merchant Detail Page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/public/frames/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Visual test page
7 |
8 |
9 |
10 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/public/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/icon-192x192.png
--------------------------------------------------------------------------------
/public/icon-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/icon-256x256.png
--------------------------------------------------------------------------------
/public/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/icon-384x384.png
--------------------------------------------------------------------------------
/public/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/icon-512x512.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
24 |
25 |
26 |
48 |
49 |
50 |
51 | Swag Labs
52 |
53 |
54 | You need to enable JavaScript to run this app.
55 |
56 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "theme_color": "#eefcf6",
3 | "background_color": "#132322",
4 | "display": "browser",
5 | "scope": "/",
6 | "start_url": "/.",
7 | "name": "Swag Labs",
8 | "short_name": "Swag Labs",
9 | "icons": [
10 | {
11 | "src": "/icon-192x192.png",
12 | "sizes": "192x192",
13 | "type": "image/png"
14 | },
15 | {
16 | "src": "/icon-256x256.png",
17 | "sizes": "256x256",
18 | "type": "image/png"
19 | },
20 | {
21 | "src": "/icon-384x384.png",
22 | "sizes": "384x384",
23 | "type": "image/png"
24 | },
25 | {
26 | "src": "/icon-512x512.png",
27 | "sizes": "512x512",
28 | "type": "image/png"
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/public/manifest.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "theme_color": "#eefcf6",
3 | "background_color": "#132322",
4 | "display": "browser",
5 | "scope": "/",
6 | "start_url": "/.",
7 | "name": "Swag Labs",
8 | "short_name": "Swag Labs",
9 | "icons": [
10 | {
11 | "src": "/icon-192x192.png",
12 | "sizes": "192x192",
13 | "type": "image/png"
14 | },
15 | {
16 | "src": "/icon-256x256.png",
17 | "sizes": "256x256",
18 | "type": "image/png"
19 | },
20 | {
21 | "src": "/icon-384x384.png",
22 | "sizes": "384x384",
23 | "type": "image/png"
24 | },
25 | {
26 | "src": "/icon-512x512.png",
27 | "sizes": "512x512",
28 | "type": "image/png"
29 | }
30 | ]
31 | }
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/v1/cart.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Swag Labs
7 |
8 |
9 |
10 |
11 |
25 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/public/v1/checkout-complete.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Swag Labs
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
20 |
21 |
22 |
23 |
Your order has been dispatched, and will arrive just as fast as the pony can get
24 | there!
25 |
26 |
27 |
28 |
29 |
30 |
31 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/v1/checkout-step-one.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Swag Labs
7 |
8 |
9 |
10 |
11 |
25 |
26 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/public/v1/checkout-step-two.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Swag Labs
7 |
8 |
9 |
10 |
11 |
26 |
27 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/v1/img/HeaderBar_Bot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/HeaderBar_Bot.png
--------------------------------------------------------------------------------
/public/v1/img/Login_Bot_graphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/Login_Bot_graphic.png
--------------------------------------------------------------------------------
/public/v1/img/SwagBot_Footer_graphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/SwagBot_Footer_graphic.png
--------------------------------------------------------------------------------
/public/v1/img/SwagLabs_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/SwagLabs_logo.png
--------------------------------------------------------------------------------
/public/v1/img/bike-light-1200x1500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/bike-light-1200x1500.jpg
--------------------------------------------------------------------------------
/public/v1/img/bolt-shirt-1200x1500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/bolt-shirt-1200x1500.jpg
--------------------------------------------------------------------------------
/public/v1/img/cart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/cart.png
--------------------------------------------------------------------------------
/public/v1/img/facebook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/facebook.png
--------------------------------------------------------------------------------
/public/v1/img/linkedIn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/linkedIn.png
--------------------------------------------------------------------------------
/public/v1/img/peek.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/peek.png
--------------------------------------------------------------------------------
/public/v1/img/pony-express.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/pony-express.png
--------------------------------------------------------------------------------
/public/v1/img/red-onesie-1200x1500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/red-onesie-1200x1500.jpg
--------------------------------------------------------------------------------
/public/v1/img/red-tatt-1200x1500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/red-tatt-1200x1500.jpg
--------------------------------------------------------------------------------
/public/v1/img/sauce-backpack-1200x1500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/sauce-backpack-1200x1500.jpg
--------------------------------------------------------------------------------
/public/v1/img/sauce-pullover-1200x1500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/sauce-pullover-1200x1500.jpg
--------------------------------------------------------------------------------
/public/v1/img/sl-404.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/sl-404.jpg
--------------------------------------------------------------------------------
/public/v1/img/twitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/public/v1/img/twitter.png
--------------------------------------------------------------------------------
/public/v1/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Swag Labs
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
Accepted usernames are:
27 | standard_user
28 | locked_out_user
29 | problem_user
30 | performance_glitch_user
31 |
32 |
33 |
34 |
Password for all users:
35 | secret_sauce
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/public/v1/inventory-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Swag Labs
7 |
8 |
9 |
10 |
11 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/public/v1/inventory.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Swag Labs
7 |
8 |
9 |
10 |
11 |
24 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/scripts/postbuild.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # copy the content of the storybook build folder to the main build folder
4 | cp -a ../storybook-static/. ../build/storybook/
5 |
--------------------------------------------------------------------------------
/src/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/.DS_Store
--------------------------------------------------------------------------------
/src/assets/img/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/arrow.png
--------------------------------------------------------------------------------
/src/assets/img/back-arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/back-arrow.png
--------------------------------------------------------------------------------
/src/assets/img/bike-light-1200x1500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/bike-light-1200x1500.jpg
--------------------------------------------------------------------------------
/src/assets/img/bolt-shirt-1200x1500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/bolt-shirt-1200x1500.jpg
--------------------------------------------------------------------------------
/src/assets/img/cart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/cart.png
--------------------------------------------------------------------------------
/src/assets/img/checkmark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/checkmark.png
--------------------------------------------------------------------------------
/src/assets/img/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/close.png
--------------------------------------------------------------------------------
/src/assets/img/facebook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/facebook.png
--------------------------------------------------------------------------------
/src/assets/img/filter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/filter.png
--------------------------------------------------------------------------------
/src/assets/img/linkedIn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/linkedIn.png
--------------------------------------------------------------------------------
/src/assets/img/menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/menu.png
--------------------------------------------------------------------------------
/src/assets/img/pony-express.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/pony-express.png
--------------------------------------------------------------------------------
/src/assets/img/red-onesie-1200x1500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/red-onesie-1200x1500.jpg
--------------------------------------------------------------------------------
/src/assets/img/red-tatt-1200x1500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/red-tatt-1200x1500.jpg
--------------------------------------------------------------------------------
/src/assets/img/sauce-backpack-1200x1500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/sauce-backpack-1200x1500.jpg
--------------------------------------------------------------------------------
/src/assets/img/sauce-pullover-1200x1500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/sauce-pullover-1200x1500.jpg
--------------------------------------------------------------------------------
/src/assets/img/sl-404.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/sl-404.jpg
--------------------------------------------------------------------------------
/src/assets/img/twitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/assets/img/twitter.png
--------------------------------------------------------------------------------
/src/assets/svg/arrow3x.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/assets/svg/cart3x.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/assets/svg/close@3x.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/assets/svg/filter3x.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/assets/svg/menu3x.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/components/BrokenComponent.jsx:
--------------------------------------------------------------------------------
1 | const BrokenComponent = () => {
2 | throw new Error("This component failed to render!");
3 | };
4 |
5 | export default BrokenComponent;
6 |
--------------------------------------------------------------------------------
/src/components/Button.css:
--------------------------------------------------------------------------------
1 | .btn {
2 | border: none;
3 | box-sizing: border-box;
4 | cursor: pointer;
5 | outline: none;
6 | padding: 6px 20px;
7 | font-size: 16px;
8 | line-height: 20px;
9 | font-weight: 500;
10 | font-family: "DM Sans", sans-serif;
11 | display: inline-block;
12 | text-decoration: none;
13 | border-radius: 4px;
14 | }
15 |
16 | .btn_primary {
17 | border: 1px solid #132322;
18 | color: #132322;
19 | background-color: #fff;
20 | }
21 |
22 | .btn_secondary {
23 | border: 1px solid #e2231a;
24 | background-color: #fff;
25 | color: #e2231a;
26 | }
27 |
28 | .btn_action {
29 | background-color: #3ddc91;
30 | border-radius: 4px;
31 | color: #132322;
32 | }
33 |
34 | .btn_secondary.back {
35 | position: relative;
36 | color: #132322;
37 | border: 1px solid #132322;
38 | }
39 |
40 | .btn_secondary .back-image {
41 | position: absolute;
42 | left: 0;
43 | top: 10px;
44 | height: 11px;
45 | width: 11px;
46 | }
47 |
48 | .btn_small {
49 | width: 160px;
50 | }
51 |
52 | .btn_medium {
53 | width: 220px;
54 | }
55 |
56 | .btn_large {
57 | width: 100%;
58 | }
59 |
60 | .btn_inventory_misaligned {
61 | position: absolute;
62 | right: -20px;
63 | }
64 |
65 | .btn_visual_failure {
66 | position: absolute;
67 | right: 0;
68 | top: 0;
69 | }
70 |
71 | @media only screen and (max-width: 900px) {
72 | .btn_small,
73 | .btn_medium,
74 | .btn_large {
75 | width: 100%;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/components/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import "./Button.css";
4 | import backPng from "../assets/img/back-arrow.png";
5 |
6 | export const BUTTON_TYPES = {
7 | ACTION: "action",
8 | BACK: "secondary back",
9 | PRIMARY: "primary",
10 | SECONDARY: "secondary",
11 | };
12 | export const BUTTON_SIZES = {
13 | SMALL: "small",
14 | MEDIUM: "medium",
15 | LARGE: "large",
16 | };
17 | const Button = ({
18 | customClass,
19 | label,
20 | onClick,
21 | size,
22 | testId,
23 | type,
24 | ...props
25 | }) => {
26 | const buttonTypeClass = ` btn_${type}`;
27 | const extraClass = customClass ? ` ${customClass}` : "";
28 | const buttonSize = ` btn_${size}`;
29 | /* istanbul ignore next */
30 | const BackImage = () => (
31 |
32 | );
33 |
34 | return (
35 |
47 | {type === BUTTON_TYPES.BACK && }
48 | {label}
49 |
50 | );
51 | };
52 |
53 | Button.propTypes = {
54 | /**
55 | * A custom class
56 | */
57 | customClass: PropTypes.string,
58 | /**
59 | * The label
60 | */
61 | label: PropTypes.string.isRequired,
62 | /**
63 | * The on click handler
64 | */
65 | onClick: PropTypes.func.isRequired,
66 | /**
67 | * Size of the button
68 | */
69 | size: PropTypes.oneOf(Object.values(BUTTON_SIZES)),
70 | /**
71 | * The test id
72 | */
73 | testId: PropTypes.string,
74 | /**
75 | * What type of field is it
76 | */
77 | type: PropTypes.oneOf(Object.values(BUTTON_TYPES)),
78 | };
79 |
80 | Button.defaultProps = {
81 | customClass: undefined,
82 | size: BUTTON_SIZES.LARGE,
83 | testId: undefined,
84 | type: BUTTON_TYPES.PRIMARY,
85 | };
86 |
87 | export default Button;
88 |
--------------------------------------------------------------------------------
/src/components/CartButton.css:
--------------------------------------------------------------------------------
1 | .shopping_cart_link {
2 | background: url("../assets/img/cart.png") no-repeat center center;
3 | background: url("../assets/svg/cart3x.svg") no-repeat center center,
4 | linear-gradient(transparent, transparent);
5 | height: 40px;
6 | width: 40px;
7 | display: block;
8 | position: relative;
9 | }
10 |
11 | .shopping_cart_badge {
12 | background-color: #e2231a;
13 | border-radius: 10px;
14 | box-sizing: border-box;
15 | display: flex;
16 | color: #ffffff;
17 | font-weight: 400;
18 | top: 0;
19 | right: 0;
20 | height: 20px;
21 | width: 20px;
22 | vertical-align: middle;
23 | justify-content: center;
24 | align-items: center;
25 | position: absolute;
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/CartButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { withRouter } from "react-router-dom";
3 | import PropTypes from "prop-types";
4 | import { ShoppingCart } from "../utils/shopping-cart";
5 | import { ROUTES } from "../utils/Constants";
6 | import "./CartButton.css";
7 |
8 | const CartButton = (props) => {
9 | const { history } = props;
10 | let cartBadge = "";
11 | const [cartContents, setCartContents] = useState(
12 | ShoppingCart.getCartContents()
13 | );
14 | // Strangely enough this is being called, but not covered in the report
15 | /* istanbul ignore next */
16 | const cartListener = {
17 | forceUpdate: () => setCartContents(ShoppingCart.getCartContents()),
18 | };
19 |
20 | useEffect(() => {
21 | ShoppingCart.registerCartListener(cartListener);
22 | }, []);
23 |
24 | if (cartContents.length > 0) {
25 | cartBadge = (
26 |
27 | {cartContents.length}
28 |
29 | );
30 | }
31 |
32 | return (
33 | history.push(ROUTES.CART)}
36 | data-test="shopping-cart-link"
37 | >
38 | {cartBadge}
39 |
40 | );
41 | };
42 |
43 | CartButton.propTypes = {
44 | /**
45 | * The history
46 | */
47 | history: PropTypes.shape({
48 | push: PropTypes.func.isRequired,
49 | }).isRequired,
50 | };
51 |
52 | export default withRouter(CartButton);
53 |
--------------------------------------------------------------------------------
/src/components/CartItem.css:
--------------------------------------------------------------------------------
1 | .cart_item {
2 | border: 1px solid #ededed;
3 | border-radius: 8px;
4 | background: #fff;
5 | display: flex;
6 | padding: 20px;
7 | margin-bottom: 20px;
8 | }
9 |
10 | .cart_quantity {
11 | border: 1px solid #ededed;
12 | box-sizing: border-box;
13 | text-align: center;
14 | font-size: 14px;
15 | line-height: 20px;
16 | font-weight: 400;
17 | padding: 6px 20px;
18 | height: 36px;
19 | width: 44px;
20 | }
21 |
22 | .cart_item_label > a {
23 | color: #4a4a4a;
24 | text-decoration: none;
25 | }
26 |
27 | .cart_item_label {
28 | padding: 0 0 0 10px;
29 | display: inline-block;
30 | vertical-align: top;
31 | position: relative;
32 | width: 100%;
33 | }
34 |
35 | .inventory_item_desc {
36 | width: 75%;
37 | }
38 |
39 | .item_pricebar {
40 | margin-top: 20px;
41 | display: flex;
42 | justify-content: space-between;
43 | align-items: flex-end;
44 | }
45 |
46 | @media only screen and (max-width: 900px) {
47 | .item_pricebar .btn {
48 | width: 160px;
49 | }
50 | }
51 | @media only screen and (max-width: 640px) {
52 | /* .cart_item {
53 | padding: 15px 0;
54 | } */
55 |
56 | .cart_quantity {
57 | padding: 5px 10px;
58 | width: 40px;
59 | }
60 |
61 | .inventory_item_desc {
62 | width: 100%;
63 | }
64 |
65 | .cart_item .inventory_item_price {
66 | margin: 0 0 20px 0;
67 | padding: 20px 10px 0 0;
68 | }
69 |
70 | .cart_item .item_pricebar {
71 | align-items: flex-start;
72 | flex-direction: column;
73 | }
74 |
75 | .item_pricebar .btn {
76 | width: 100%;
77 | }
78 |
79 | .cart_item_label {
80 | width: calc(100% - 50px);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/DrawerMenu.css:
--------------------------------------------------------------------------------
1 | @media only screen and (max-width: 480px) {
2 | .bm-menu-wrap {
3 | width: 100% !important;
4 | }
5 | }
6 | .menu-item {
7 | border-bottom: 1px solid #ededed;
8 | color: #18583a;
9 | display: inline-block;
10 | font-family: "DM Mono", "sans-serif";
11 | font-size: 16px;
12 | margin-bottom: 7px;
13 | text-decoration: none;
14 | cursor: pointer;
15 | padding: 10px 0;
16 | }
17 |
18 | .menu-item:hover {
19 | color: #3ddc91;
20 | }
21 |
22 | /* ===============Burger button styling==================== */
23 | /* Position and sizing of burger button */
24 | .bm-burger-button {
25 | position: absolute;
26 | width: 20px;
27 | height: 20px;
28 | left: 20px;
29 | top: 20px;
30 | }
31 |
32 | /* Color/shape of burger icon bars */
33 | .bm-burger-bars {
34 | background: #777;
35 | }
36 |
37 | /* Position and sizing of clickable cross button */
38 | .bm-cross-button {
39 | height: 20px !important;
40 | width: 20px !important;
41 | right: 16px !important;
42 | top: 16px !important;
43 | }
44 |
45 | /* Color/shape of close button cross */
46 | .bm-cross {
47 | background: #ffffff;
48 | }
49 |
50 | /* General sidebar styles */
51 | .bm-menu {
52 | background: #fff;
53 | padding: 2.5em 1.5em 0;
54 | font-size: 16px;
55 | box-shadow: none;
56 | }
57 |
58 | /* Morph shape necessary with bubble or elastic */
59 | .bm-morph-shape {
60 | fill: #373a47;
61 | }
62 |
63 | /* Individual item */
64 | .bm-item {
65 | }
66 |
67 | .visual_failure {
68 | transform: rotate(3deg);
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/ErrorMessage.css:
--------------------------------------------------------------------------------
1 | .error-message-container {
2 | background-color: #ffffff;
3 | height: 45px;
4 | margin-bottom: 5px;
5 | margin-top: -10px;
6 | position: relative;
7 | flex: 1;
8 | display: flex;
9 | align-items: center;
10 | padding-left: 10px;
11 | padding-right: 10px;
12 | justify-content: center;
13 | }
14 |
15 | .error-message-container.error {
16 | background-color: #e2231a;
17 | }
18 |
19 | .error-message-container h3 {
20 | color: #ffffff;
21 | font-size: 14px;
22 | text-align: center;
23 | font-family: Roboto, Arial, Helvetica, sans-serif;
24 | padding-left: 10px;
25 | padding-right: 10px;
26 | }
27 |
28 | .error-message-container .error-button {
29 | border: 0px;
30 | background-color: transparent;
31 | color: #ffffff;
32 | cursor: pointer;
33 | position: absolute;
34 | right: 5px;
35 | top: 5px;
36 | }
37 |
38 | .error-message-container .error-button:focus {
39 | outline: none;
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/ErrorMessage.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4 | import { faTimes } from "@fortawesome/free-solid-svg-icons";
5 | import "./ErrorMessage.css";
6 |
7 | const ErrorMessage = ({ isError, errorMessage, onClick, ...props }) => {
8 | return (
9 |
13 | {isError && (
14 | // This component is not structured how it should,
15 | // But this is done to keep backwards compatibility
16 |
17 |
22 |
23 |
24 | {errorMessage}
25 |
26 | )}
27 |
28 | );
29 | };
30 |
31 | ErrorMessage.propTypes = {
32 | /**
33 | * If this is an isError field yes or no
34 | */
35 | isError: PropTypes.bool.isRequired,
36 | /**
37 | * The value of the input
38 | */
39 | errorMessage: PropTypes.string.isRequired,
40 | /**
41 | * The on change handler
42 | */
43 | onClick: PropTypes.func.isRequired,
44 | };
45 |
46 | export default ErrorMessage;
47 |
--------------------------------------------------------------------------------
/src/components/Footer.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | background-color: #132322;
3 | min-height: 140px;
4 | margin-top: auto;
5 | position: relative;
6 | /*display: inline-table;*/
7 | }
8 | /* .footer_container {
9 | max-width: 1440px;
10 | margin: 0 auto;
11 | position: relative;
12 | height: auto;
13 | width: 100%;
14 |
15 | } */
16 | .footer_copy,
17 | .social {
18 | position: absolute;
19 | }
20 | .footer_copy {
21 | left: 50px;
22 | bottom: 40px;
23 | color: #fff;
24 | }
25 | .social {
26 | list-style-type: none;
27 | left: 10px;
28 | top: 20px;
29 | }
30 | .social li {
31 | float: left;
32 | margin-right: 20px;
33 | display: inline-block;
34 | width: 30px;
35 | height: 30px;
36 | }
37 | .social_twitter {
38 | background: url("../assets/img/twitter.png") no-repeat;
39 | }
40 | .social_facebook {
41 | background: url("../assets/img/facebook.png") no-repeat;
42 | }
43 | .social_linkedin {
44 | background: url("../assets/img/linkedIn.png") no-repeat;
45 | }
46 |
47 | .social a {
48 | width: 30px;
49 | height: 30px;
50 | display: block;
51 | font-size: 0;
52 | }
53 |
54 | @media only screen and (max-width: 960px) {
55 | .footer {
56 | height: auto;
57 | text-align: center;
58 | padding: 0 10px;
59 | display: inline-table;
60 | }
61 | .social,
62 | .footer_copy {
63 | position: relative;
64 | }
65 | .social {
66 | width: 100%;
67 | padding: 0;
68 | left: 0;
69 | }
70 | .social li {
71 | float: none;
72 | }
73 |
74 | .footer_copy {
75 | width: 100%;
76 | left: 0;
77 | bottom: 0;
78 | margin: 40px 0;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./Footer.css";
3 |
4 | const SwagLabsFooter = () => {
5 | return (
6 |
47 | );
48 | };
49 |
50 | export default SwagLabsFooter;
51 |
--------------------------------------------------------------------------------
/src/components/HeaderContainer.css:
--------------------------------------------------------------------------------
1 | .header_container {
2 | border-bottom: 1px solid #ededed;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
7 | .primary_header {
8 | border-bottom: 1px solid #ededed;
9 | height: 56px;
10 | }
11 |
12 | .primary_header .header_label {
13 | background: #fff;
14 | padding: 4px 0;
15 | text-align: center;
16 | font-size: 24px;
17 | position: relative;
18 | }
19 |
20 | .primary_header .app_logo {
21 | font-family: DM Mono, "sans-serif";
22 | font-size: 24px;
23 | line-height: 48px;
24 | text-align: center;
25 | margin: 0 50px;
26 | }
27 |
28 | .primary_header .shopping_cart_container {
29 | height: 40px;
30 | width: 40px;
31 | position: absolute;
32 | top: 10px;
33 | right: 20px;
34 | }
35 | .primary_header .shopping_cart_container.visual_failure {
36 | height: 40px;
37 | width: 40px;
38 | position: absolute;
39 | top: 40px;
40 | right: 205px;
41 | }
42 |
43 | .header_secondary_container {
44 | background: #fff;
45 | width: 100%;
46 | height: 56px;
47 | display: flex;
48 | flex-direction: row;
49 | align-items: center;
50 | padding: 0 20px;
51 | box-sizing: border-box;
52 | }
53 |
54 | .header_secondary_container .title {
55 | color: #132322;
56 | font-family: "DM Sans", sans-serif;
57 | font-size: 18px;
58 | font-weight: 500;
59 | line-height: 48px;
60 | }
61 |
62 | /* .primary_header, .header_secondary_container{
63 | max-width: 1440px;
64 | margin: 0 auto;
65 | position: relative;
66 | width: 100%;
67 | } */
68 |
69 | .right_component {
70 | margin-left: auto;
71 | }
72 |
73 | @media only screen and (max-width: 640px) {
74 | .header_secondary_container {
75 | height: 60px;
76 | padding: 0 15px;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/InputError.css:
--------------------------------------------------------------------------------
1 | .form_group {
2 | margin-bottom: 15px;
3 | position: relative;
4 | }
5 | .input_error {
6 | font-family: DM Sans, Arial, Helvetica, sans-serif;
7 | font-size: 14px;
8 | width: 100%;
9 | border: 0px;
10 | border-bottom: 1px solid #ededed;
11 | outline: none;
12 | padding: 10px 0;
13 | }
14 |
15 | .input_error.error {
16 | border-bottom-color: #e2231a;
17 | }
18 |
19 | .error_icon {
20 | color: #e2231a;
21 | font-size: 18px;
22 | position: absolute;
23 | right: 0;
24 | top: 12px;
25 | }
26 |
27 | /* Chrome/Opera/Safari */
28 | .form_input::-webkit-input-placeholder,
29 | /* Firefox 19+ */
30 | .form_input::-moz-placeholder,
31 | /* IE 10+ */
32 | .form_input:-ms-input-placeholder,
33 | .form_input::placeholder {
34 | color: #6d7584;
35 | }
36 |
37 | /* Chrome/Opera/Safari */
38 | .form_input.error::-webkit-input-placeholder,
39 | /* Firefox 19+ */
40 | .form_input.error::-moz-placeholder,
41 | /* IE 10+ */
42 | .form_input.error:-ms-input-placeholder,
43 | .form_input.error::placeholder {
44 | color: #e2231a !important;
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/InputError.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4 | import { faTimesCircle } from "@fortawesome/free-solid-svg-icons";
5 | import "./InputError.css";
6 |
7 | export const INPUT_TYPES = {
8 | TEXT: "text",
9 | PASSWORD: "password",
10 | };
11 | const InputError = ({
12 | isError,
13 | onChange,
14 | placeholder,
15 | testId,
16 | type,
17 | value,
18 | ...props
19 | }) => {
20 | return (
21 |
22 |
39 | {isError && (
40 |
41 | )}
42 |
43 | );
44 | };
45 |
46 | InputError.propTypes = {
47 | /**
48 | * If this is an isError field yes or no
49 | */
50 | isError: PropTypes.bool.isRequired,
51 | /**
52 | * The on change handler
53 | */
54 | onChange: PropTypes.func.isRequired,
55 | /**
56 | * The placeholder of the input
57 | */
58 | placeholder: PropTypes.string,
59 | /**
60 | * The test id
61 | */
62 | testId: PropTypes.string,
63 | /**
64 | * What type of field is it
65 | */
66 | type: PropTypes.oneOf(["text", "password"]),
67 | /**
68 | * The value of the input
69 | */
70 | value: PropTypes.string,
71 | };
72 |
73 | InputError.defaultProps = {
74 | placeholder: "",
75 | testId: undefined,
76 | type: INPUT_TYPES.TEXT,
77 | value: "",
78 | };
79 |
80 | export default InputError;
81 |
--------------------------------------------------------------------------------
/src/components/InventoryListItem.css:
--------------------------------------------------------------------------------
1 | .inventory_item {
2 | border: 1px solid #ededed;
3 | background: #fff;
4 | position: relative;
5 | margin-bottom: 12px;
6 | display: flex;
7 | box-sizing: border-box;
8 | border-radius: 8px;
9 | }
10 |
11 | .inventory_item_img {
12 | flex: 1;
13 | overflow: hidden;
14 | }
15 | .inventory_item_img a {
16 | display: block;
17 | height: 100%;
18 | width: 100%;
19 | }
20 | .inventory_item_img img {
21 | height: 100%;
22 | border-bottom-left-radius: 4px;
23 | border-top-left-radius: 4px;
24 | }
25 |
26 | .inventory_item_description {
27 | flex: 2;
28 | flex-direction: column;
29 | padding: 20px 34px 20px 20px;
30 | display: flex;
31 | justify-content: space-between;
32 | }
33 |
34 | .inventory_item_label > a {
35 | color: #4a4a4a;
36 | text-decoration: none;
37 | }
38 | .inventory_item_name {
39 | font-family: "DM Mono", sans-serif;
40 | font-size: 20px;
41 | font-weight: 500;
42 | color: #18583a;
43 | }
44 | .align_right {
45 | text-align: right;
46 | }
47 | .inventory_item_desc {
48 | font-family: "DM Sans", sans-serif;
49 | font-size: 14px;
50 | line-height: 20px;
51 | margin-top: 10px;
52 | color: #132322;
53 | }
54 | .inventory_item .inventory_item_desc {
55 | width: inherit;
56 | }
57 | .pricebar {
58 | display: flex;
59 | justify-content: space-between;
60 | align-items: flex-end;
61 | }
62 |
63 | .inventory_item_price {
64 | border-top: 1px solid #ededed;
65 | color: #132322;
66 | font-family: "DM Mono", sans-serif;
67 | font-size: 20px;
68 | font-weight: 500;
69 | line-height: 36px;
70 | display: inline-block;
71 | padding-top: 5px;
72 | }
73 |
74 | @media only screen and (min-width: 1060px) {
75 | .inventory_item {
76 | height: 240px;
77 | width: 505px;
78 | }
79 | }
80 |
81 | @media only screen and (max-width: 1060px) {
82 | .inventory_item {
83 | height: 240px;
84 | width: 100%;
85 | }
86 | .pricebar .btn {
87 | width: 160px;
88 | }
89 | }
90 |
91 | @media only screen and (max-width: 480px) {
92 | .inventory_item {
93 | height: inherit;
94 | flex-direction: column;
95 | border: none;
96 | margin-bottom: 40px;
97 | }
98 |
99 | .inventory_item_img {
100 | box-sizing: border-box;
101 | padding: 0 10px;
102 | }
103 | .inventory_item_img img {
104 | border-radius: 0;
105 | width: 100%;
106 | }
107 |
108 | .inventory_item_description {
109 | padding-bottom: 0;
110 | }
111 |
112 | .pricebar {
113 | align-items: flex-start;
114 | border-bottom: 1px solid #ededed;
115 | flex-direction: column;
116 | padding-bottom: 40px;
117 | }
118 |
119 | .inventory_item_price {
120 | margin: 20px 0;
121 | padding: 20px 10px 0 0;
122 | }
123 |
124 | .pricebar .btn {
125 | width: 100%;
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/components/PrivateRoute.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Redirect, Route } from "react-router-dom";
3 | import PropTypes from "prop-types";
4 | import { isLoggedIn } from "../utils/Credentials";
5 | import { ROUTES } from "../utils/Constants";
6 |
7 | /**
8 | * @TODO: This can't be tested yet because enzyme currently doesn't support ReactJS17,
9 | * see https://github.com/enzymejs/enzyme/issues/2429.
10 | * This means we can't fully mount the component and test all rendered components
11 | * and functions
12 | */
13 | /* istanbul ignore next */
14 | const PrivateRoute = ({ component: Component, ...rest }) => {
15 | return (
16 |
19 | isLoggedIn() ? (
20 |
21 | ) : (
22 |
25 | )
26 | }
27 | />
28 | );
29 | };
30 | /* istanbul ignore next */
31 | PrivateRoute.propTypes = {
32 | /**
33 | * A react component
34 | */
35 | component: PropTypes.element,
36 | };
37 | /* istanbul ignore next */
38 | PrivateRoute.defaultProps = {
39 | customClass: undefined,
40 | secondaryHeaderBot: undefined,
41 | secondaryLeftComponent: undefined,
42 | secondaryRightComponent: undefined,
43 | secondaryTitle: undefined,
44 | };
45 |
46 | export default PrivateRoute;
47 |
--------------------------------------------------------------------------------
/src/components/Select.css:
--------------------------------------------------------------------------------
1 | .select_container {
2 | border: 1px solid #ededed;
3 | border-radius: 4px;
4 | box-sizing: border-box;
5 | background-color: #ffffff;
6 | overflow: hidden;
7 | padding: 0 0 0 40px;
8 | cursor: pointer;
9 | height: 32px;
10 | width: 32px;
11 | display: block;
12 | position: relative;
13 | }
14 |
15 | .active_option {
16 | display: none;
17 | }
18 |
19 | .select_container:before {
20 | content: "";
21 | background: url("../assets/img/filter.png") no-repeat center center;
22 | background: url("../assets/svg/filter3x.svg") no-repeat center center,
23 | linear-gradient(transparent, transparent);
24 | height: 32px;
25 | width: 32px;
26 | position: absolute;
27 | left: 0;
28 | top: 0;
29 | }
30 |
31 | .product_sort_container {
32 | position: absolute;
33 | top: 0;
34 | left: 0;
35 | right: 0;
36 | bottom: 0;
37 | width: 100%;
38 | /* This needs to be like this because otherwise Safari can't click on an element that is not visible */
39 | opacity: 0.001;
40 | }
41 |
42 | @media only screen and (min-width: 900px) {
43 | .select_container {
44 | padding: 0 32px;
45 | width: 225px;
46 | line-height: 32px;
47 | }
48 |
49 | .select_container:after {
50 | content: "";
51 | background: url("../assets/img/arrow.png") no-repeat center center;
52 | background: url("../assets/svg/arrow3x.svg") no-repeat center center,
53 | linear-gradient(transparent, transparent);
54 | height: 32px;
55 | width: 32px;
56 | position: absolute;
57 | right: 0;
58 | top: 0;
59 | }
60 |
61 | .active_option {
62 | display: block;
63 | font-size: 14px;
64 | text-align: center;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/Select.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import "./Select.css";
4 |
5 | const Select = ({ activeOption, onChange, options, testId }) => {
6 | return (
7 |
8 |
9 | {
10 | options[options.findIndex((option) => option.key === activeOption)]
11 | .value
12 | }
13 |
14 |
24 | {options.map(({ key, value }) => (
25 |
26 | {value}
27 |
28 | ))}
29 |
30 |
31 | );
32 | };
33 | Select.propTypes = {
34 | /**
35 | * The active option key
36 | */
37 | activeOption: PropTypes.string.isRequired,
38 | /**
39 | * The on change handler
40 | */
41 | onChange: PropTypes.func.isRequired,
42 | /**
43 | * The options
44 | */
45 | options: PropTypes.arrayOf(
46 | PropTypes.shape({
47 | key: PropTypes.string.isRequired,
48 | value: PropTypes.string.isRequired,
49 | })
50 | ).isRequired,
51 | /**
52 | * The test id
53 | */
54 | testId: PropTypes.string,
55 | };
56 | Select.defaultProps = {
57 | testId: undefined,
58 | };
59 |
60 | export default Select;
61 |
--------------------------------------------------------------------------------
/src/components/SubmitButton.css:
--------------------------------------------------------------------------------
1 | .submit-button {
2 | background-color: #3ddc91;
3 | border: 4px solid #3ddc91;
4 | border-radius: 4px;
5 | color: #132322;
6 | cursor: pointer;
7 | display: inline-block;
8 | outline: none;
9 | font-size: 16px;
10 | line-height: 20px;
11 | /* letter-spacing: 2px; */
12 | font-family: DM Sans, Arial, Helvetica, sans-serif;
13 | line-height: initial;
14 | margin-bottom: 15px;
15 | padding: 10px 30px;
16 | text-decoration: none;
17 | width: 100%;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/SubmitButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import "./SubmitButton.css";
4 |
5 | const SubmitButton = ({ customClass, testId, value, ...props }) => {
6 | const extraClass = customClass ? ` ${customClass}` : "";
7 | return (
8 |
21 | );
22 | };
23 |
24 | SubmitButton.propTypes = {
25 | /**
26 | * A custom class
27 | */
28 | customClass: PropTypes.string,
29 | /**
30 | * The test id
31 | */
32 | testId: PropTypes.string,
33 | /**
34 | * The value of the input
35 | */
36 | value: PropTypes.string.isRequired,
37 | };
38 |
39 | SubmitButton.defaultProps = {
40 | customClass: undefined,
41 | testId: undefined,
42 | };
43 |
44 | export default SubmitButton;
45 |
--------------------------------------------------------------------------------
/src/components/__tests__/BrokenComponent.tests.js:
--------------------------------------------------------------------------------
1 | import { shallow } from "enzyme";
2 | import BrokenComponent from "../BrokenComponent";
3 |
4 | describe("BrokenComponent", () => {
5 | it("should throw an error on render", () => {
6 | expect(() => shallow( )).toThrowError(
7 | "This component failed to render!"
8 | );
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/src/components/__tests__/Button.tests.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import Button, { BUTTON_SIZES, BUTTON_TYPES } from "../Button";
4 |
5 | let props;
6 |
7 | describe("Button", () => {
8 | beforeEach(() => {
9 | props = {
10 | label: "Default button",
11 | onClick: () => {},
12 | };
13 | });
14 |
15 | it("should render correctly with the required options", () => {
16 | const component = shallow( );
17 |
18 | expect(component).toMatchSnapshot();
19 | });
20 |
21 | it("should render correctly with a custom class", () => {
22 | const component = shallow( );
23 |
24 | expect(component).toMatchSnapshot();
25 | });
26 |
27 | it("should render a Action button", () => {
28 | const component = shallow( );
29 |
30 | expect(component).toMatchSnapshot();
31 | });
32 |
33 | it("should render a Back button", () => {
34 | const component = shallow( );
35 |
36 | expect(component).toMatchSnapshot();
37 | });
38 |
39 | it("should render a Primary button", () => {
40 | const component = shallow(
41 |
42 | );
43 |
44 | expect(component).toMatchSnapshot();
45 | });
46 |
47 | it("should render a Secondary button", () => {
48 | const component = shallow(
49 |
50 | );
51 |
52 | expect(component).toMatchSnapshot();
53 | });
54 |
55 | it("should render a Small button", () => {
56 | const component = shallow( );
57 |
58 | expect(component).toMatchSnapshot();
59 | });
60 |
61 | it("should render a Medium button", () => {
62 | const component = shallow( );
63 |
64 | expect(component).toMatchSnapshot();
65 | });
66 |
67 | it("should render a Large button", () => {
68 | const component = shallow( );
69 |
70 | expect(component).toMatchSnapshot();
71 | });
72 |
73 | it("should render a button with a testId", () => {
74 | const component = shallow( );
75 |
76 | expect(component).toMatchSnapshot();
77 | });
78 |
79 | it("should render a button with custom properties", () => {
80 | const component = shallow( );
81 |
82 | expect(component).toMatchSnapshot();
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/src/components/__tests__/CartButton.tests.js:
--------------------------------------------------------------------------------
1 | import React, { useState as useStateMock } from "react";
2 | import { shallow } from "enzyme";
3 | import CartButton from "../CartButton";
4 | import { ShoppingCart } from "../../utils/shopping-cart";
5 |
6 | jest.mock("react", () => ({
7 | ...jest.requireActual("react"),
8 | useState: jest.fn(),
9 | useEffect: (f) => f(),
10 | }));
11 | jest.mock("../../utils/shopping-cart");
12 |
13 | let props;
14 |
15 | describe("CartButton", () => {
16 | const setState = jest.fn();
17 |
18 | beforeEach(() => {
19 | props = {
20 | history: {
21 | push: jest.fn(),
22 | },
23 | };
24 | useStateMock.mockImplementation((init) => [init, setState]);
25 | ShoppingCart.getCartContents = jest.fn().mockReturnValue([]);
26 | });
27 |
28 | afterEach(() => {
29 | jest.clearAllMocks();
30 | });
31 |
32 | it("should render with default props", () => {
33 | const component = shallow( );
34 |
35 | expect(component).toMatchSnapshot();
36 | });
37 |
38 | it("should render the badge if products have been added into the cart", () => {
39 | const cartContents = [1, 2, 3];
40 | ShoppingCart.registerCartListener = jest.fn().mockReturnValue((f) => f());
41 | ShoppingCart.getCartContents = jest.fn().mockReturnValue(cartContents);
42 | const component = shallow( );
43 | expect(ShoppingCart.getCartContents).toHaveBeenCalledTimes(1);
44 | expect(ShoppingCart.registerCartListener).toHaveBeenCalledTimes(1);
45 | expect(useStateMock).toHaveBeenCalledWith(cartContents);
46 | expect(component).toMatchSnapshot();
47 | });
48 |
49 | it("should be able to go to the cart", () => {
50 | const component = shallow( );
51 | const cartButton = component.find(".shopping_cart_link").at(0);
52 | cartButton.simulate("click", {
53 | preventDefault() {},
54 | });
55 |
56 | expect(props.history.push).toBeCalledWith("/cart.html");
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/src/components/__tests__/CartItem.tests.js:
--------------------------------------------------------------------------------
1 | import React, { useState as useStateMock } from "react";
2 | import { shallow } from "enzyme";
3 | import CartItem from "../CartItem";
4 | import { ShoppingCart } from "../../utils/shopping-cart";
5 | import * as Credentials from "../../utils/Credentials";
6 |
7 | jest.mock("react", () => ({
8 | ...jest.requireActual("react"),
9 | useState: jest.fn(),
10 | }));
11 | jest.mock("../../utils/shopping-cart");
12 |
13 | let props;
14 |
15 | describe("InventoryListItem", () => {
16 | const setState = jest.fn();
17 |
18 | beforeEach(() => {
19 | props = {
20 | history: {
21 | push: jest.fn(),
22 | },
23 | item: {
24 | desc: "Swag Item description",
25 | id: 1,
26 | name: "Swag Item name",
27 | price: 9.99,
28 | },
29 | showButton: true,
30 | };
31 | useStateMock.mockImplementation((init) => [init, setState]);
32 | });
33 |
34 | afterEach(() => {
35 | jest.clearAllMocks();
36 | });
37 |
38 | it("should render with no item", () => {
39 | delete props.item;
40 | useStateMock.mockImplementationOnce((init) => [(init = false), setState]);
41 | const component = shallow( );
42 |
43 | expect(component).toMatchSnapshot();
44 | });
45 |
46 | it("should render with default props", () => {
47 | const component = shallow( );
48 |
49 | expect(component).toMatchSnapshot();
50 | });
51 |
52 | it("should be able to open the details page when the swag image is clicked", () => {
53 | const component = shallow( );
54 |
55 | const link = component.find(`#item_${props.item.id}_title_link`).at(0);
56 | link.simulate("click", {
57 | preventDefault() {},
58 | });
59 | expect(props.history.push).toBeCalledWith(
60 | `/inventory-item.html?id=${props.item.id}`
61 | );
62 | });
63 |
64 | it("should remove the item from the cart when the remove button is clicked", () => {
65 | const component = shallow( );
66 |
67 | const removeButton = component.find("Button").at(0);
68 | removeButton.simulate("click", {
69 | preventDefault() {},
70 | });
71 | expect(ShoppingCart.removeItem).toBeCalledTimes(1);
72 | expect(ShoppingCart.removeItem).toBeCalledWith(1);
73 | });
74 |
75 | it("should be able to set the link and id for a problem user", () => {
76 | const isProblemUserSpy = jest.spyOn(Credentials, "isProblemUser");
77 | isProblemUserSpy.mockReturnValue(true);
78 | const component = shallow( );
79 | const link = component.find(`#item_${props.item.id}_title_link`).at(0);
80 | link.simulate("click", {
81 | preventDefault() {},
82 | });
83 |
84 | expect(props.history.push).toBeCalledWith(
85 | `/inventory-item.html?id=${props.item.id + 1}`
86 | );
87 |
88 | isProblemUserSpy.mockClear();
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/src/components/__tests__/DrawerMenu.tests.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import DrawerMenu from "../DrawerMenu";
4 | import * as Credentials from "../../utils/Credentials";
5 | import { ShoppingCart } from "../../utils/shopping-cart";
6 |
7 | jest.mock("../../utils/shopping-cart");
8 |
9 | let props;
10 |
11 | describe("DrawerMenu", () => {
12 | beforeEach(() => {
13 | props = {
14 | history: {
15 | push: jest.fn(),
16 | },
17 | };
18 | });
19 |
20 | it("should render correctly", () => {
21 | const component = shallow( );
22 |
23 | expect(component).toMatchSnapshot();
24 | });
25 |
26 | it("should render correctly for a visual user", () => {
27 | const isVisualUserSpy = jest.spyOn(Credentials, "isVisualUser");
28 | isVisualUserSpy.mockReturnValue(true);
29 | const component = shallow( );
30 |
31 | expect(component).toMatchSnapshot();
32 | });
33 |
34 | it("should render return an incorrect about link for a problem user", () => {
35 | const isProblemUserSpy = jest.spyOn(Credentials, "isProblemUser");
36 | isProblemUserSpy.mockReturnValue(true);
37 | const component = shallow( );
38 | const aboutLink = component.find("a").at(1);
39 | aboutLink.simulate("click", {
40 | preventDefault() {},
41 | });
42 |
43 | expect(isProblemUserSpy).toHaveBeenCalledTimes(1);
44 | expect(aboutLink.props().href).toEqual("https://saucelabs.com/error/404");
45 |
46 | isProblemUserSpy.mockClear();
47 | });
48 |
49 | it("should be able to redirect to the inventory page when clicking on the all items link", () => {
50 | const component = shallow( );
51 | const allItemsLink = component.find("a").at(0);
52 | allItemsLink.simulate("click", {
53 | preventDefault() {},
54 | });
55 |
56 | expect(props.history.push).toHaveBeenCalledWith("/inventory.html");
57 | });
58 |
59 | it("should be able to redirect to the login page when clicking on the logout link", () => {
60 | const component = shallow( );
61 | const logoutLink = component.find("a").at(2);
62 | const removeCredentialsSpy = jest.spyOn(Credentials, "removeCredentials");
63 | removeCredentialsSpy.mockReturnValue(true);
64 | logoutLink.simulate("click", {
65 | preventDefault() {},
66 | });
67 |
68 | expect(props.history.push).toHaveBeenCalledWith("/");
69 | expect(removeCredentialsSpy).toHaveBeenCalledTimes(1);
70 | });
71 |
72 | it("should be able to reset the storage when Reset App State is being called", () => {
73 | const component = shallow( );
74 | const resetAppStateLink = component.find("a").at(3);
75 | ShoppingCart.resetCart = jest.fn();
76 | resetAppStateLink.simulate("click", {
77 | preventDefault() {},
78 | });
79 |
80 | expect(ShoppingCart.resetCart).toHaveBeenCalledTimes(1);
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/src/components/__tests__/ErrorMessage.tests.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import ErrorMessage from "../ErrorMessage";
4 |
5 | describe("ErrorMessage", () => {
6 | it("should render no error message when there is not error", () => {
7 | const props = {
8 | isError: false,
9 | errorMessage: "Error Message",
10 | onClick: () => {},
11 | };
12 | const component = shallow( );
13 |
14 | expect(component).toMatchSnapshot();
15 | });
16 |
17 | it("should render an error message when there is an error", () => {
18 | const props = {
19 | isError: true,
20 | errorMessage: "Error Message",
21 | onClick: () => {},
22 | };
23 | const component = shallow( );
24 |
25 | expect(component).toMatchSnapshot();
26 | });
27 |
28 | it("should be able to render custom props on the container level", () => {
29 | const props = {
30 | isError: false,
31 | errorMessage: "",
32 | onClick: () => {},
33 | foo: "bar",
34 | bar: "foo-bar",
35 | };
36 | const component = shallow( );
37 |
38 | expect(component).toMatchSnapshot();
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/src/components/__tests__/Footer.tests.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import Footer from "../Footer";
4 |
5 | describe("Footer", () => {
6 | beforeEach(() => {
7 | jest.spyOn(Date.prototype, "getFullYear").mockReturnValue(1234);
8 | });
9 |
10 | it("should render correctly", () => {
11 | const component = shallow();
12 |
13 | expect(component).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/__tests__/HeaderContainer.tests.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import HeaderContainer from "../HeaderContainer";
4 | import * as Credentials from "../../utils/Credentials";
5 |
6 | describe("HeaderContainer", () => {
7 | it("should render without any props", () => {
8 | const component = shallow( );
9 |
10 | expect(component).toMatchSnapshot();
11 | });
12 |
13 | it("should render for a visual user", () => {
14 | const isVisualUserSpy = jest.spyOn(Credentials, "isVisualUser");
15 | isVisualUserSpy.mockReturnValue(true);
16 | const component = shallow( );
17 |
18 | expect(component).toMatchSnapshot();
19 | isVisualUserSpy.mockClear();
20 | });
21 |
22 | it("should render with all props", () => {
23 | const component = shallow(
24 | }
28 | secondaryRightComponent={ }
29 | secondaryTitle="sample title"
30 | />
31 | );
32 |
33 | expect(component).toMatchSnapshot();
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/components/__tests__/InputError.tests.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import InputError, { INPUT_TYPES } from "../InputError";
4 |
5 | let props;
6 |
7 | describe("InputError", () => {
8 | beforeEach(() => {
9 | props = {
10 | isError: false,
11 | onChange: jest.fn(),
12 | };
13 | });
14 |
15 | it("should render correctly with the required options", () => {
16 | const component = shallow( );
17 |
18 | expect(component).toMatchSnapshot();
19 | });
20 |
21 | it("should render secure input", () => {
22 | const component = shallow(
23 |
24 | );
25 |
26 | expect(component).toMatchSnapshot();
27 | });
28 |
29 | it("should render an error input", () => {
30 | const component = shallow( );
31 |
32 | expect(component).toMatchSnapshot();
33 | });
34 |
35 | it("should render an input with a testId", () => {
36 | const component = shallow( );
37 |
38 | expect(component).toMatchSnapshot();
39 | });
40 |
41 | it("should render an input with custom props", () => {
42 | const component = shallow(
43 |
44 | );
45 |
46 | expect(component).toMatchSnapshot();
47 | });
48 |
49 | it("should render an input with full props", () => {
50 | const component = shallow(
51 |
59 | );
60 |
61 | expect(component).toMatchSnapshot();
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/src/components/__tests__/InventoryListItem.tests.js:
--------------------------------------------------------------------------------
1 | import React, { useState as useStateMock } from "react";
2 | import { shallow } from "enzyme";
3 | import InventoryListItem from "../InventoryListItem";
4 | import * as Credentials from "../../utils/Credentials";
5 |
6 | jest.mock("react", () => ({
7 | ...jest.requireActual("react"),
8 | useState: jest.fn(),
9 | }));
10 |
11 | let props;
12 |
13 | describe("InventoryListItem", () => {
14 | const setState = jest.fn();
15 |
16 | beforeEach(() => {
17 | props = {
18 | desc: "Swag Item description",
19 | history: {
20 | push: jest.fn(),
21 | },
22 | id: 1,
23 | image_url: "image.png",
24 | name: "Swag Item name",
25 | price: 9.99,
26 | isTextAlignRight: false,
27 | missAlignButton: false,
28 | };
29 | useStateMock.mockImplementation((init) => [init, setState]);
30 | });
31 |
32 | afterEach(() => {
33 | jest.clearAllMocks();
34 | });
35 |
36 | it("should render with default props", () => {
37 | const component = shallow(
38 |
39 | );
40 |
41 | expect(component).toMatchSnapshot();
42 | });
43 |
44 | it("should render with text aligned right", () => {
45 | const component = shallow(
46 |
47 | );
48 |
49 | expect(component).toMatchSnapshot();
50 | });
51 |
52 | it("should be able to open the details page when the swag image is clicked", () => {
53 | const component = shallow(
54 |
55 | );
56 |
57 | const imageLink = component.find(`#item_${props.id}_img_link`).at(0);
58 | imageLink.simulate("click", {
59 | preventDefault() {},
60 | });
61 | expect(props.history.push).toBeCalledWith(
62 | `/inventory-item.html?id=${props.id}`
63 | );
64 | });
65 |
66 | it("should be able to open the details page when the swag item title is clicked", () => {
67 | const component = shallow(
68 |
69 | );
70 |
71 | const imageLink = component.find(`#item_${props.id}_title_link`).at(0);
72 | imageLink.simulate("click", {
73 | preventDefault() {},
74 | });
75 | expect(props.history.push).toBeCalledWith(
76 | `/inventory-item.html?id=${props.id}`
77 | );
78 | });
79 |
80 | it("should be able to open the details page for a problem user when the swag item title is clicked", () => {
81 | const isProblemUserSpy = jest.spyOn(Credentials, "isProblemUser");
82 | isProblemUserSpy.mockReturnValue(true);
83 | const component = shallow(
84 |
85 | );
86 | const imageLink = component.find(`#item_${props.id}_title_link`).at(0);
87 | imageLink.simulate("click", {
88 | preventDefault() {},
89 | });
90 |
91 | expect(props.history.push).toBeCalledWith(
92 | `/inventory-item.html?id=${props.id + 1}`
93 | );
94 |
95 | isProblemUserSpy.mockClear();
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/src/components/__tests__/Select.tests.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import Select from "../Select";
4 |
5 | let props;
6 |
7 | describe("Select", () => {
8 | beforeEach(() => {
9 | props = {
10 | activeOption: "az",
11 | options: [
12 | { key: "az", value: "Name (A to Z)" },
13 | { key: "za", value: "Name (Z to A)" },
14 | { key: "lohi", value: "Price (low to high)" },
15 | { key: "hilo", value: "Price (high to low)" },
16 | ],
17 | onChange: jest.fn(),
18 | };
19 | });
20 |
21 | it("should render with default props", () => {
22 | const component = shallow( );
23 |
24 | expect(component).toMatchSnapshot();
25 | });
26 |
27 | it("should render with a testID", () => {
28 | const component = shallow( );
29 |
30 | expect(component).toMatchSnapshot();
31 | });
32 |
33 | it("should be able to trigger the onChange", () => {
34 | const component = shallow( );
35 | component
36 | .find("select")
37 | .at(0)
38 | .simulate("change", {
39 | target: { value: "za", name: "Name (Z to A)" },
40 | });
41 |
42 | expect(props.onChange).toHaveBeenCalledTimes(1);
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/src/components/__tests__/SubmitButton.tests.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import SubmitButton from "../SubmitButton";
4 |
5 | describe("SubmitButton", () => {
6 | it("should render with default props", () => {
7 | const props = {
8 | value: "Submit",
9 | };
10 | const component = shallow( );
11 |
12 | expect(component).toMatchSnapshot();
13 | });
14 |
15 | it("should render with a testId", () => {
16 | const props = {
17 | testId: "test-id",
18 | value: "Submit",
19 | };
20 | const component = shallow( );
21 |
22 | expect(component).toMatchSnapshot();
23 | });
24 |
25 | it("should render with a testId and a name prop", () => {
26 | const props = {
27 | testId: "test-id",
28 | value: "Submit",
29 | name: "name-id",
30 | };
31 | const component = shallow( );
32 |
33 | expect(component).toMatchSnapshot();
34 | });
35 |
36 | it("should render with custom props", () => {
37 | const props = {
38 | value: "Submit",
39 | foo: "bar",
40 | bar: "foo",
41 | };
42 | const component = shallow( );
43 |
44 | expect(component).toMatchSnapshot();
45 | });
46 |
47 | it("should render with a custom class", () => {
48 | const props = {
49 | customClass: "custom_class",
50 | value: "Submit",
51 | };
52 | const component = shallow( );
53 |
54 | expect(component).toMatchSnapshot();
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/src/components/__tests__/__snapshots__/Button.tests.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Button should render a Action button 1`] = `
4 |
8 | Default button
9 |
10 | `;
11 |
12 | exports[`Button should render a Back button 1`] = `
13 |
17 |
18 | Default button
19 |
20 | `;
21 |
22 | exports[`Button should render a Large button 1`] = `
23 |
27 | Default button
28 |
29 | `;
30 |
31 | exports[`Button should render a Medium button 1`] = `
32 |
36 | Default button
37 |
38 | `;
39 |
40 | exports[`Button should render a Primary button 1`] = `
41 |
45 | Default button
46 |
47 | `;
48 |
49 | exports[`Button should render a Secondary button 1`] = `
50 |
54 | Default button
55 |
56 | `;
57 |
58 | exports[`Button should render a Small button 1`] = `
59 |
63 | Default button
64 |
65 | `;
66 |
67 | exports[`Button should render a button with a testId 1`] = `
68 |
75 | Default button
76 |
77 | `;
78 |
79 | exports[`Button should render a button with custom properties 1`] = `
80 |
86 | Default button
87 |
88 | `;
89 |
90 | exports[`Button should render correctly with a custom class 1`] = `
91 |
95 | Default button
96 |
97 | `;
98 |
99 | exports[`Button should render correctly with the required options 1`] = `
100 |
104 | Default button
105 |
106 | `;
107 |
--------------------------------------------------------------------------------
/src/components/__tests__/__snapshots__/CartButton.tests.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`CartButton should render the badge if products have been added into the cart 1`] = `
4 |
9 |
13 | 3
14 |
15 |
16 | `;
17 |
18 | exports[`CartButton should render with default props 1`] = `
19 |
24 | `;
25 |
--------------------------------------------------------------------------------
/src/components/__tests__/__snapshots__/CartItem.tests.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`InventoryListItem should render with default props 1`] = `
4 |
8 |
12 | 1
13 |
14 |
56 |
57 | `;
58 |
59 | exports[`InventoryListItem should render with no item 1`] = `
60 |
63 | `;
64 |
--------------------------------------------------------------------------------
/src/components/__tests__/__snapshots__/ErrorMessage.tests.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`ErrorMessage should be able to render custom props on the container level 1`] = `
4 |
9 | `;
10 |
11 | exports[`ErrorMessage should render an error message when there is an error 1`] = `
12 |
15 |
18 |
23 |
63 |
64 | Error Message
65 |
66 |
67 | `;
68 |
69 | exports[`ErrorMessage should render no error message when there is not error 1`] = `
70 |
73 | `;
74 |
--------------------------------------------------------------------------------
/src/components/__tests__/__snapshots__/Footer.tests.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Footer should render correctly 1`] = `
4 |
57 | `;
58 |
--------------------------------------------------------------------------------
/src/components/__tests__/__snapshots__/Select.tests.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Select should render with a testID 1`] = `
4 |
7 |
11 | Name (A to Z)
12 |
13 |
19 |
23 | Name (A to Z)
24 |
25 |
29 | Name (Z to A)
30 |
31 |
35 | Price (low to high)
36 |
37 |
41 | Price (high to low)
42 |
43 |
44 |
45 | `;
46 |
47 | exports[`Select should render with default props 1`] = `
48 |
51 |
55 | Name (A to Z)
56 |
57 |
62 |
66 | Name (A to Z)
67 |
68 |
72 | Name (Z to A)
73 |
74 |
78 | Price (low to high)
79 |
80 |
84 | Price (high to low)
85 |
86 |
87 |
88 | `;
89 |
--------------------------------------------------------------------------------
/src/components/__tests__/__snapshots__/SubmitButton.tests.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`SubmitButton should render with a custom class 1`] = `
4 |
9 | `;
10 |
11 | exports[`SubmitButton should render with a testId 1`] = `
12 |
20 | `;
21 |
22 | exports[`SubmitButton should render with a testId and a name prop 1`] = `
23 |
31 | `;
32 |
33 | exports[`SubmitButton should render with custom props 1`] = `
34 |
41 | `;
42 |
43 | exports[`SubmitButton should render with default props 1`] = `
44 |
49 | `;
50 |
--------------------------------------------------------------------------------
/src/img/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/saucelabs/sample-app-web/9192e428a8b6e913219c617e10b0b7eb27e24a62/src/img/.DS_Store
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body,
2 | html {
3 | font-family: DM Sans, Arial, Helvetica, sans-serif;
4 | font-size: 14px;
5 | line-height: 20px;
6 | /* letter-spacing: 1px; */
7 | background-color: #fff;
8 | color: #132322;
9 | height: 100vh;
10 | margin: 0;
11 | -webkit-font-smoothing: antialiased;
12 | -moz-osx-font-smoothing: antialiased;
13 | }
14 |
15 | :focus {
16 | outline: none;
17 | }
18 |
19 | input {
20 | -webkit-appearance: none;
21 | -moz-appearance: none;
22 | appearance: none;
23 | border-radius: 0;
24 | color: #484c55;
25 | }
26 |
27 | .page_wrapper {
28 | display: flex;
29 | height: 100vh;
30 | flex-direction: column;
31 | }
32 |
33 | /*Sub header */
34 | .subheader {
35 | position: relative; /* Need this here so the position: absolute on subelements anchors here rather than to the browser viewport */
36 | background: #474c55;
37 | height: 70px;
38 | color: #fff;
39 | font-size: 28px;
40 | padding-left: 15px;
41 | line-height: 66px;
42 | }
43 |
44 | .subheader_label {
45 | position: absolute;
46 | left: 110px;
47 | top: 25px;
48 | font-size: 32px;
49 | font-weight: 100;
50 | color: #fff;
51 | }
52 |
53 | h4 {
54 | font-size: 16px;
55 | line-height: 20px;
56 | /* letter-spacing: 2px; */
57 | }
58 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | // Needed to add the below due to issues in IE11, see this thread
2 | // https://github.com/facebook/create-react-app/issues/9906#issuecomment-720905753
3 | /** @jsxRuntime classic */
4 | import "react-app-polyfill/ie11";
5 | import "react-app-polyfill/stable";
6 |
7 | import { BacktraceClient, ErrorBoundary } from "@backtrace-labs/react";
8 | import React from "react";
9 | import ReactDOM from "react-dom";
10 | import { Route, BrowserRouter as Router } from "react-router-dom";
11 | import PrivateRoute from "./components/PrivateRoute";
12 | import "./index.css";
13 | import Cart from "./pages/Cart";
14 | import CheckOutStepOne from "./pages/CheckOutStepOne";
15 | import CheckOutStepTwo from "./pages/CheckOutStepTwo";
16 | import Finish from "./pages/Finish";
17 | import Inventory from "./pages/Inventory";
18 | import InventoryItem from "./pages/InventoryItem";
19 | import Login from "./pages/Login";
20 | import * as serviceWorkerRegistration from "./serviceWorkerRegistration";
21 | import { ROUTES } from "./utils/Constants";
22 | import { currentUser } from "./utils/Credentials";
23 | import { ShoppingCart } from "./utils/shopping-cart";
24 | import { InventoryData } from "./utils/InventoryData.js";
25 | import { InventoryDataLong } from "./utils/InventoryDataLong.js";
26 |
27 | BacktraceClient.initialize({
28 | name: "Swag Store",
29 | version: "3.0.0",
30 | url: "https://submit.backtrace.io/UNIVERSE/TOKEN/json",
31 | userAttributes: () => ({
32 | user: currentUser(),
33 | shoppingCart: ShoppingCart.getCartContents(),
34 | }),
35 | });
36 |
37 | const routing = (
38 |
39 |
40 |
41 | } />
42 | } />
43 |
44 |
45 |
49 |
53 |
54 |
55 |
56 | );
57 |
58 | ReactDOM.render(routing, document.getElementById("root"));
59 |
60 | // If you want your app to work offline and load faster, you can change
61 | // unregister() to register() below. Note this comes with some pitfalls.
62 | // Learn more about service workers: https://cra.link/PWA
63 | serviceWorkerRegistration.register();
64 |
--------------------------------------------------------------------------------
/src/pages/Cart.css:
--------------------------------------------------------------------------------
1 | .cart_contents_container {
2 | background-color: #fff;
3 | max-width: 1280px;
4 | margin: 0 auto;
5 | padding: 0 15px 30px;
6 | }
7 |
8 | .cart_quantity_label {
9 | display: inline-block;
10 | font-size: 16px;
11 | font-weight: 500;
12 | line-height: 24px;
13 | font-family: "DM Mono", sans-serif;
14 | padding: 20px 0;
15 | background-color: #fff;
16 | color: #484c55;
17 | width: 80px;
18 | }
19 |
20 | .cart_desc_label {
21 | font-size: 16px;
22 | font-weight: 500;
23 | color: #484c55;
24 | display: inline-block;
25 | }
26 |
27 | .cart_contents_container .btn_secondary.back {
28 | color: #132322;
29 | border: 1px solid #132322;
30 | }
31 |
32 | .cart_contents_container .btn_secondary.back .back-image {
33 | left: 12px;
34 | }
35 |
36 | .cart_footer {
37 | padding: 40px 0;
38 | margin-bottom: 30px;
39 | display: flex;
40 | justify-content: space-between;
41 | }
42 |
43 | @media only screen and (max-width: 900px) {
44 | .cart_quantity_label {
45 | width: 50px;
46 | }
47 | .cart_footer {
48 | display: block;
49 | justify-content: inherit;
50 | padding: 20px 0;
51 | }
52 |
53 | .cart_footer .btn {
54 | margin-bottom: 15px;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/pages/Cart.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { withRouter } from "react-router-dom";
3 | import { ROUTES } from "../utils/Constants";
4 | import { ShoppingCart } from "../utils/shopping-cart";
5 | import { InventoryData } from "../utils/InventoryData";
6 | import CartItem from "../components/CartItem";
7 | import SwagLabsFooter from "../components/Footer";
8 | import HeaderContainer from "../components/HeaderContainer";
9 | import Button, { BUTTON_SIZES, BUTTON_TYPES } from "../components/Button";
10 | import "./Cart.css";
11 | import { isVisualUser } from "../utils/Credentials";
12 |
13 | const Cart = ({ history }) => {
14 | const contents = ShoppingCart.getCartContents();
15 | const buttonClass = `checkout_button ${
16 | isVisualUser() ? "btn_visual_failure" : ""
17 | }`;
18 |
19 | return (
20 |
21 |
22 |
23 |
28 |
29 |
30 |
34 | QTY
35 |
36 |
37 | Description
38 |
39 | {contents.map((item, i) => (
40 |
41 | ))}
42 |
43 |
44 | {
47 | evt.preventDefault();
48 | history.push(ROUTES.INVENTORY);
49 | }}
50 | size={BUTTON_SIZES.MEDIUM}
51 | testId="continue-shopping"
52 | type={BUTTON_TYPES.BACK}
53 | />
54 | {
60 | evt.preventDefault();
61 | history.push(ROUTES.CHECKOUT_STEP_ONE);
62 | }}
63 | size={BUTTON_SIZES.MEDIUM}
64 | testId="checkout"
65 | type={BUTTON_TYPES.ACTION}
66 | />
67 |
68 |
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default withRouter(Cart);
77 |
--------------------------------------------------------------------------------
/src/pages/CheckOutStepOne.css:
--------------------------------------------------------------------------------
1 | .checkout_info_container {
2 | max-width: 1280px;
3 | margin: 0 auto;
4 | padding-top: 30px;
5 | }
6 |
7 | .checkout_info_wrapper {
8 | padding: 0 15px;
9 | }
10 |
11 | .checkout_info {
12 | max-width: 380px;
13 | margin: 0 auto;
14 | padding-top: 30px;
15 | }
16 |
17 | .checkout_buttons {
18 | margin-top: 5px;
19 | border-top: 1px solid #ededed;
20 | display: flex;
21 | justify-content: space-between;
22 | padding: 40px 0;
23 | margin-bottom: 30px;
24 | }
25 |
26 | .checkout_buttons .btn_primary {
27 | border: none;
28 | }
29 |
30 | .checkout_buttons .btn_secondary.back {
31 | color: #132322;
32 | border: 1px solid #132322;
33 | }
34 |
35 | .checkout_buttons .btn_secondary.back .back-image {
36 | left: 12px;
37 | }
38 |
39 | .checkout_buttons .btn {
40 | width: 220px;
41 | }
42 |
43 | .checkout_buttons .btn {
44 | margin-bottom: 0;
45 | }
46 |
47 | @media only screen and (min-width: 900px) {
48 | .checkout_info {
49 | border: 1px solid #ededed;
50 | border-radius: 8px;
51 | padding: 40px 40px 0 40px;
52 | margin: 70px auto 100px auto;
53 | }
54 | }
55 |
56 | @media only screen and (max-width: 640px) {
57 | .checkout_buttons {
58 | display: block;
59 | justify-content: inherit;
60 | }
61 |
62 | .checkout_buttons .btn {
63 | margin-bottom: 15px;
64 | width: 100%;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/pages/CheckOutStepTwo.css:
--------------------------------------------------------------------------------
1 | .checkout_summary_container {
2 | max-width: 1280px;
3 | margin: 0 auto;
4 | padding: 0 15px 30px;
5 | }
6 |
7 | .summary_info_label {
8 | padding: 20px 0 0;
9 | }
10 |
11 | .summary_value_label {
12 | font-weight: 800;
13 | padding: 5px 0 20px 0;
14 | }
15 |
16 | .summary_subtotal_label {
17 | padding: 20px 0 10px;
18 | }
19 |
20 | .summary_tax_label {
21 | padding: 0;
22 | }
23 |
24 | .summary_total_label {
25 | padding: 10px 0;
26 | font-weight: 800;
27 | margin: 0 0 8px 0;
28 | }
29 |
30 | .summary_info_label {
31 | font-family: "DM Mono", sans-serif;
32 | font-weight: 500;
33 | font-size: 18px;
34 | line-height: 24px;
35 | }
36 | .summary_value_label {
37 | font-family: "DM Sans", sans-serif;
38 | font-weight: 400;
39 | font-size: 14px;
40 | line-height: 20px;
41 | }
42 |
43 | .summary_info_label,
44 | .summary_value_label,
45 | .summary_subtotal_label,
46 | .summary_tax_label,
47 | .summary_total_label {
48 | color: #132322;
49 | }
50 |
51 | .cart_footer .btn_secondary.back {
52 | color: #132322;
53 | border: 1px solid #132322;
54 | }
55 |
56 | .cart_footer .btn_secondary.back .back-image {
57 | left: 12px;
58 | }
59 |
60 | @media only screen and (min-width: 900px) {
61 | .summary_info_label {
62 | padding: 20px 0 0;
63 | }
64 |
65 | .summary_value_label {
66 | padding: 5px 0 0 0;
67 | }
68 |
69 | .summary_subtotal_label {
70 | padding: 0;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/pages/Finish.css:
--------------------------------------------------------------------------------
1 | .checkout_complete_container {
2 | max-width: 1280px;
3 | margin: 0 auto;
4 | padding: 30px 15px 50px;
5 | text-align: center;
6 | display: flex;
7 | flex-direction: column;
8 | align-items: center;
9 | }
10 |
11 | .pony_express {
12 | width: 100%;
13 | height: 100%;
14 | max-width: 72px;
15 | max-height: 72px;
16 | margin: 30px auto;
17 | }
18 |
19 | .complete-header {
20 | font-family: "DM Mono", sans-serif;
21 | font-size: 24px;
22 | line-height: 32px;
23 | font-weight: 500;
24 | }
25 |
26 | .complete-text {
27 | font-family: "DM Sans", sans-serif;
28 | font-size: 14px;
29 | line-height: 20px;
30 | font-weight: 400;
31 | margin-bottom: 70px;
32 | }
33 | .complete-header,
34 | .complete-text {
35 | max-width: 450px;
36 | }
37 |
38 | .checkout_complete_container .btn {
39 | border: none;
40 | background-color: #3ddc91;
41 | border-radius: 4px;
42 | color: #132322;
43 | }
44 |
45 | @media only screen and (min-width: 900px) {
46 | .checkout_complete_container {
47 | padding-bottom: 100px;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/pages/Finish.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { withRouter } from "react-router-dom";
3 | import Checkmark from "../assets/img/checkmark.png";
4 | import SwagLabsFooter from "../components/Footer";
5 | import HeaderContainer from "../components/HeaderContainer";
6 | import PropTypes from "prop-types";
7 | import Button, { BUTTON_SIZES } from "../components/Button";
8 | import { ROUTES } from "../utils/Constants";
9 | import "./Finish.css";
10 |
11 | const Finish = ({ history }) => {
12 | return (
13 |
14 |
15 |
16 |
21 |
27 |
28 | Thank you for your order!
29 |
30 |
31 | Your order has been dispatched, and will arrive just as fast as the
32 | pony can get there!
33 |
34 |
history.push(ROUTES.INVENTORY)}
37 | size={BUTTON_SIZES.SMALL}
38 | testId="back-to-products"
39 | />
40 |
41 |
42 |
43 |
44 | );
45 | };
46 | Finish.propTypes = {
47 | /**
48 | * The history
49 | */
50 | history: PropTypes.shape({
51 | push: PropTypes.func.isRequired,
52 | }).isRequired,
53 | };
54 |
55 | export default withRouter(Finish);
56 |
--------------------------------------------------------------------------------
/src/pages/Inventory.css:
--------------------------------------------------------------------------------
1 | .product_label {
2 | position: absolute;
3 | left: 110px;
4 | top: 25px;
5 | font-size: 32px;
6 | font-weight: 100;
7 | color: #fff;
8 | }
9 |
10 | .inventory_container {
11 | background-color: #fff;
12 | overflow: hidden;
13 | padding-top: 40px;
14 | margin: 0 auto 30px auto;
15 | }
16 |
17 | .inventory_list {
18 | max-width: 1060px;
19 | margin: 0 auto;
20 | display: flex;
21 | justify-content: space-between;
22 | flex-flow: row wrap;
23 | padding: 0 15px 30px;
24 | }
25 |
--------------------------------------------------------------------------------
/src/pages/Login.css:
--------------------------------------------------------------------------------
1 | .login_container {
2 | background-color: #eefcf6;
3 | min-height: 100vh;
4 | }
5 | .login_logo {
6 | font-family: DM Mono, "sans-serif";
7 | font-size: 24px;
8 | line-height: 48px;
9 | text-align: center;
10 | padding: 30px 0;
11 | }
12 | .login_wrapper {
13 | margin: 0px auto;
14 | }
15 | .login_wrapper-inner {
16 | overflow: hidden;
17 | }
18 | .login_wrapper-inner,
19 | .login_credentials_wrap-inner {
20 | width: 70%;
21 | max-width: 780px;
22 | padding: 15px 30px 30px;
23 | margin: 0 auto;
24 | border: 1px solid #ededed;
25 | display: grid;
26 | grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
27 | }
28 | .login_wrapper-inner {
29 | background-color: #fff;
30 | border-top-right-radius: 8px;
31 | border-top-left-radius: 8px;
32 | }
33 | .login_credentials_wrap {
34 | color: #ffffff;
35 | font-family: DM Mono, "sans-serif";
36 | }
37 | .form_column {
38 | float: none;
39 | width: 100%;
40 | }
41 |
42 | .login_credentials {
43 | clear: both;
44 | line-height: 24px;
45 | }
46 |
47 | .login_credentials_wrap-inner {
48 | background-color: #132322;
49 | overflow: hidden;
50 | border-bottom-right-radius: 8px;
51 | border-bottom-left-radius: 8px;
52 | }
53 | @media only screen and (min-width: 900px) {
54 | .form_column {
55 | max-width: 292px;
56 | padding-top: 30px;
57 | margin: 0 auto;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/pages/__tests__/Cart.tests.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import Cart from "../Cart";
4 | import { ShoppingCart } from "../../utils/shopping-cart";
5 | import * as Credentials from "../../utils/Credentials";
6 |
7 | jest.mock("../../utils/shopping-cart");
8 |
9 | let props;
10 |
11 | describe("Cart", () => {
12 | beforeEach(() => {
13 | props = {
14 | history: { push: jest.fn() },
15 | };
16 | ShoppingCart.getCartContents = jest.fn().mockReturnValue([]);
17 | });
18 |
19 | afterEach(() => {
20 | jest.clearAllMocks();
21 | });
22 |
23 | it("should render correctly without any items", () => {
24 | const wrapper = shallow( );
25 | expect(wrapper).toMatchSnapshot();
26 | });
27 |
28 | it("should render correctly for a visual user", () => {
29 | const isVisualUserSpy = jest.spyOn(Credentials, "isVisualUser");
30 | isVisualUserSpy.mockReturnValue(true);
31 | const wrapper = shallow( );
32 |
33 | expect(wrapper).toMatchSnapshot();
34 | isVisualUserSpy.mockClear();
35 | });
36 |
37 | it("should render correctly items", () => {
38 | const cartContents = [1, 2, 3];
39 | ShoppingCart.getCartContents = jest.fn().mockReturnValue(cartContents);
40 | const wrapper = shallow( );
41 |
42 | expect(wrapper).toMatchSnapshot();
43 | });
44 |
45 | it("should redirect when trying to continue shopping", () => {
46 | const wrapper = shallow( );
47 | const ContinueShopping = wrapper.find("Button").at(0);
48 | ContinueShopping.simulate("click", {
49 | preventDefault() {},
50 | });
51 |
52 | expect(props.history.push).toBeCalledWith("/inventory.html");
53 | });
54 |
55 | it("should redirect when trying to checkout", () => {
56 | const wrapper = shallow( );
57 | const Checkout = wrapper.find("Button").at(1);
58 | Checkout.simulate("click", {
59 | preventDefault() {},
60 | });
61 |
62 | expect(props.history.push).toBeCalledWith("/checkout-step-one.html");
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/src/pages/__tests__/Finish.tests.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import Finish from "../Finish";
4 |
5 | jest.mock("../../utils/shopping-cart");
6 |
7 | let props;
8 |
9 | describe("CheckOutStepTwo", () => {
10 | beforeEach(() => {
11 | props = {
12 | history: { push: jest.fn() },
13 | };
14 | });
15 |
16 | afterEach(() => {
17 | jest.clearAllMocks();
18 | });
19 |
20 | it("should render correctly with default props", () => {
21 | const wrapper = shallow( );
22 | expect(wrapper).toMatchSnapshot();
23 | });
24 |
25 | it("should redirect when clicking on back home", () => {
26 | const wrapper = shallow( );
27 | const backHome = wrapper.find("Button").at(0);
28 | backHome.simulate("click", {
29 | preventDefault() {},
30 | });
31 |
32 | expect(props.history.push).toBeCalledWith("/inventory.html");
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/src/pages/__tests__/Inventory.tests.js:
--------------------------------------------------------------------------------
1 | import React, { useState as useStateMock } from "react";
2 | import { shallow } from "enzyme";
3 | import Inventory from "../Inventory";
4 | import * as Credentials from "../../utils/Credentials";
5 | import {InventoryData} from "../../utils/InventoryData";
6 | import {InventoryDataLong} from "../../utils/InventoryDataLong";
7 |
8 | jest.mock("react", () => ({
9 | ...jest.requireActual("react"),
10 | useState: jest.fn(),
11 | }));
12 |
13 | describe("Inventory", () => {
14 | const setState = jest.fn();
15 |
16 | beforeEach(() => {
17 | useStateMock.mockImplementation((init) => [init, setState]);
18 | });
19 |
20 | afterEach(() => {
21 | jest.clearAllMocks();
22 | });
23 |
24 | it("should render correctly", () => {
25 | const wrapper = shallow( );
26 | expect(wrapper).toMatchSnapshot();
27 | });
28 |
29 | it("should render correctly long", () => {
30 | const wrapper = shallow( );
31 | expect(wrapper.find('[data-test="inventory-list"]').length).toEqual(1);
32 | });
33 |
34 | it("should render correctly for a problem user", () => {
35 | const isProblemUserSpy = jest.spyOn(Credentials, "isProblemUser");
36 | isProblemUserSpy.mockReturnValue(true);
37 | const wrapper = shallow( );
38 |
39 | expect(wrapper).toMatchSnapshot();
40 | isProblemUserSpy.mockClear();
41 | });
42 |
43 | it("should render correctly for a visual user", () => {
44 | const isVisualUserSpy = jest.spyOn(Credentials, "isVisualUser");
45 | isVisualUserSpy.mockReturnValue(true);
46 |
47 | const randomSpy = jest.spyOn(Math, "random");
48 | randomSpy.mockReturnValue(0.5);
49 |
50 | const wrapper = shallow( );
51 |
52 | expect(wrapper).toMatchSnapshot();
53 | isVisualUserSpy.mockClear();
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/src/pages/__tests__/__snapshots__/CheckOutStepOne.tests.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`CheckOutStepOne should render correctly 1`] = `
4 |
90 | `;
91 |
--------------------------------------------------------------------------------
/src/pages/__tests__/__snapshots__/Finish.tests.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`CheckOutStepTwo should render correctly with default props 1`] = `
4 |
8 |
11 |
14 |
19 |
25 |
29 | Thank you for your order!
30 |
31 |
35 | Your order has been dispatched, and will arrive just as fast as the pony can get there!
36 |
37 |
44 |
45 |
46 |
47 |
48 | `;
49 |
--------------------------------------------------------------------------------
/src/pages/__tests__/__snapshots__/Login.tests.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Login should render correctly 1`] = `
4 |
7 |
10 | Swag Labs
11 |
12 |
16 |
65 |
69 |
72 |
77 |
78 | Accepted usernames are:
79 |
80 | standard_user
81 |
82 | locked_out_user
83 |
84 | problem_user
85 |
86 | performance_glitch_user
87 |
88 | error_user
89 |
90 | visual_user
91 |
92 |
93 |
97 |
98 | Password for all users:
99 |
100 | secret_sauce
101 |
102 |
103 |
104 |
105 |
106 | `;
107 |
--------------------------------------------------------------------------------
/src/service-worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 |
3 | // This service worker can be customized!
4 | // See https://developers.google.com/web/tools/workbox/modules
5 | // for the list of available Workbox modules, or add any other
6 | // code you'd like.
7 | // You can also remove this file if you'd prefer not to use a
8 | // service worker, and the Workbox build step will be skipped.
9 |
10 | import { clientsClaim } from "workbox-core";
11 | import { ExpirationPlugin } from "workbox-expiration";
12 | import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching";
13 | import { registerRoute } from "workbox-routing";
14 | import { StaleWhileRevalidate } from "workbox-strategies";
15 |
16 | clientsClaim();
17 |
18 | // Precache all of the assets generated by your build process.
19 | // Their URLs are injected into the manifest variable below.
20 | // This variable must be present somewhere in your service worker file,
21 | // even if you decide not to use precaching. See https://cra.link/PWA
22 | precacheAndRoute(self.__WB_MANIFEST);
23 |
24 | // Set up App Shell-style routing, so that all navigation requests
25 | // are fulfilled with your index.html shell. Learn more at
26 | // https://developers.google.com/web/fundamentals/architecture/app-shell
27 | const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$");
28 | registerRoute(
29 | // Return false to exempt requests from being fulfilled by index.html.
30 | ({ request, url }) => {
31 | // If this isn't a navigation, skip.
32 | if (request.mode !== "navigate") {
33 | return false;
34 | } // If this is a URL that starts with /_, skip.
35 |
36 | if (url.pathname.startsWith("/_")) {
37 | return false;
38 | } // If this looks like a URL for a resource, because it contains // a file extension, skip.
39 |
40 | if (url.pathname.match(fileExtensionRegexp)) {
41 | return false;
42 | } // Return true to signal that we want to use the handler.
43 |
44 | return true;
45 | },
46 | createHandlerBoundToURL(process.env.PUBLIC_URL + "/index.html")
47 | );
48 |
49 | // An example runtime caching route for requests that aren't handled by the
50 | // precache, in this case same-origin .png requests like those from in public/
51 | registerRoute(
52 | // Add in any other file extensions or routing criteria as needed.
53 | ({ url }) =>
54 | url.origin === self.location.origin && url.pathname.endsWith(".png"), // Customize this strategy as needed, e.g., by changing to CacheFirst.
55 | new StaleWhileRevalidate({
56 | cacheName: "images",
57 | plugins: [
58 | // Ensure that once this runtime cache reaches a maximum size the
59 | // least-recently used images are removed.
60 | new ExpirationPlugin({ maxEntries: 50 }),
61 | ],
62 | })
63 | );
64 |
65 | // This allows the web app to trigger skipWaiting via
66 | // registration.waiting.postMessage({type: 'SKIP_WAITING'})
67 | self.addEventListener("message", (event) => {
68 | if (event.data && event.data.type === "SKIP_WAITING") {
69 | self.skipWaiting();
70 | }
71 | });
72 |
73 | // Any other custom service worker logic can go here.
74 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import "@testing-library/jest-dom";
6 | import { configure } from "enzyme";
7 | import Adapter from "enzyme-adapter-react-16";
8 |
9 | configure({ adapter: new Adapter() });
10 |
--------------------------------------------------------------------------------
/src/storybook/stories/Button.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import Button, { BUTTON_SIZES, BUTTON_TYPES } from "../../components/Button";
4 |
5 | export default {
6 | title: "SwagLabs/Button",
7 | component: Button,
8 | };
9 |
10 | const Template = (args) => {
11 | const [value, setValue] = useState(0);
12 |
13 | return (
14 | <>
15 | setValue(value + 1)} />
16 |
17 | You clicked {value} times.
18 |
19 | >
20 | );
21 | };
22 |
23 | export const Primary = Template.bind({});
24 | Primary.args = {
25 | label: "Primary",
26 | size: BUTTON_SIZES.MEDIUM,
27 | type: BUTTON_TYPES.PRIMARY,
28 | };
29 |
30 | export const Secondary = Template.bind({});
31 | Secondary.args = {
32 | label: "Secondary",
33 | size: BUTTON_SIZES.MEDIUM,
34 | type: BUTTON_TYPES.SECONDARY,
35 | };
36 |
37 | export const Action = Template.bind({});
38 | Action.args = {
39 | label: "Some Action",
40 | size: BUTTON_SIZES.MEDIUM,
41 | type: BUTTON_TYPES.ACTION,
42 | };
43 |
44 | export const Back = Template.bind({});
45 | Back.args = {
46 | label: "Go back",
47 | size: BUTTON_SIZES.MEDIUM,
48 | type: BUTTON_TYPES.BACK,
49 | };
50 |
51 | export const Small = Template.bind({});
52 | Small.args = {
53 | label: "Small",
54 | size: BUTTON_SIZES.SMALL,
55 | };
56 |
57 | export const Medium = Template.bind({});
58 | Medium.args = {
59 | label: "Medium button",
60 | size: BUTTON_SIZES.MEDIUM,
61 | };
62 |
63 | export const Large = Template.bind({});
64 | Large.args = {
65 | label: "Large button",
66 | size: BUTTON_SIZES.LARGE,
67 | };
68 |
--------------------------------------------------------------------------------
/src/storybook/stories/CartItem.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import StoryRouter from "storybook-react-router";
3 | import CartItem from "../../components/CartItem";
4 |
5 | export default {
6 | title: "SwagLabs/Swag Cart Item",
7 | component: CartItem,
8 | decorators: [StoryRouter()],
9 | parameters: {
10 | layout: "centered",
11 | },
12 | };
13 |
14 | const Template = (args) => (
15 | <>
16 |
17 | Resize the preview to see the mobile view.
18 | >
19 | );
20 |
21 | export const SwagCartItem = Template.bind({});
22 | SwagCartItem.args = {
23 | history: {
24 | push: () => {},
25 | },
26 | item: {
27 | id: 1,
28 | name: "Sauce Labs Bolt T-Shirt",
29 | desc: `Get your testing superhero on with the Sauce Labs bolt T-shirt. From American Apparel, 100% ringspun combed cotton, heather gray with red bolt.`,
30 | price: 15.99,
31 | },
32 | showButton: true,
33 | };
34 |
35 | export const SwagCartItemNoButton = Template.bind({});
36 | SwagCartItemNoButton.args = {
37 | history: {
38 | push: () => {},
39 | },
40 | item: {
41 | id: 1,
42 | name: "Sauce Labs Bolt T-Shirt",
43 | desc: `Get your testing superhero on with the Sauce Labs bolt T-shirt. From American Apparel, 100% ringspun combed cotton, heather gray with red bolt.`,
44 | price: 15.99,
45 | },
46 | showButton: false,
47 | };
48 |
--------------------------------------------------------------------------------
/src/storybook/stories/ErrorMessage.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import ErrorMessage from "../../components/ErrorMessage";
3 |
4 | export default {
5 | title: "SwagLabs/Form/ErrorMessage",
6 | component: ErrorMessage,
7 | };
8 |
9 | const Template = (args) => {
10 | const [value, setValue] = useState(0);
11 | return (
12 | <>
13 | setValue(value + 1)} />
14 |
15 | You clicked {value} times.
16 |
17 | >
18 | );
19 | };
20 |
21 | export const Error = Template.bind({});
22 | Error.args = {
23 | isError: true,
24 | errorMessage: "This is an error message",
25 | onClick: () => console.log("clicked"),
26 | };
27 |
--------------------------------------------------------------------------------
/src/storybook/stories/HeaderContainer.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import HeaderContainer from "../../components/HeaderContainer";
3 | import Button, { BUTTON_TYPES } from "../../components/Button";
4 | import StoryRouter from "storybook-react-router";
5 |
6 | export default {
7 | title: "SwagLabs/Headers",
8 | component: HeaderContainer,
9 | decorators: [StoryRouter()],
10 | parameters: {
11 | layout: "fullscreen",
12 | },
13 | };
14 |
15 | const Template = (args) => (
16 | <>
17 |
18 |
19 | Resize the preview to see the mobile view. The Sauce Bot will disappear in
20 | mobile view.
21 |
22 | >
23 | );
24 |
25 | export const HeaderDefault = Template.bind({});
26 | HeaderDefault.args = {};
27 |
28 | export const HeaderLeftComponent = Template.bind({});
29 | HeaderLeftComponent.args = {
30 | secondaryLeftComponent: (
31 | {}}
34 | type={BUTTON_TYPES.BACK}
35 | />
36 | ),
37 | };
38 |
39 | export const HeaderTitle = Template.bind({});
40 | HeaderTitle.args = {
41 | secondaryTitle: "Title only",
42 | };
43 |
44 | export const HeaderTitleBotRight = Template.bind({});
45 | HeaderTitleBotRight.args = {
46 | secondaryHeaderBot: true,
47 | secondaryRightComponent: (
48 | {}}
51 | type={BUTTON_TYPES.ACTION}
52 | />
53 | ),
54 | secondaryTitle: "Title and Bot",
55 | };
56 |
--------------------------------------------------------------------------------
/src/storybook/stories/InputError.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import InputError, { INPUT_TYPES } from "../../components/InputError";
3 |
4 | export default {
5 | title: "SwagLabs/Form/Input",
6 | component: InputError,
7 | argTypes: {
8 | // controlled value prop
9 | value: {
10 | control: {
11 | disable: true,
12 | },
13 | },
14 | },
15 | };
16 |
17 | const Template = (args) => {
18 | const [value, setValue] = useState(args.value ?? "");
19 |
20 | return (
21 | <>
22 | setValue(evt.target.value)}
24 | value={value}
25 | {...args}
26 | />
27 |
28 | Your typed "{value}" .
29 |
30 | >
31 | );
32 | };
33 |
34 | export const Input = Template.bind({});
35 | Input.args = {
36 | isError: false,
37 | placeholder: "Placeholder",
38 | };
39 |
40 | export const ErrorInput = Template.bind({});
41 | ErrorInput.args = {
42 | isError: true,
43 | placeholder: "Placeholder",
44 | value: "This is the value",
45 | };
46 |
47 | export const Secure = Template.bind({});
48 | Secure.args = {
49 | isError: false,
50 | type: INPUT_TYPES.PASSWORD,
51 | placeholder: "Type you password here",
52 | value: "SecurePassword123!",
53 | };
54 |
--------------------------------------------------------------------------------
/src/storybook/stories/InventoryListItem.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import StoryRouter from "storybook-react-router";
3 | import InventoryListItem from "../../components/InventoryListItem";
4 |
5 | export default {
6 | title: "SwagLabs/Swag Overview Item",
7 | component: InventoryListItem,
8 | decorators: [StoryRouter()],
9 | };
10 |
11 | const Template = (args) => (
12 | <>
13 |
14 | Resize the preview to see the mobile view.
15 | >
16 | );
17 |
18 | export const SwagOverviewItem = Template.bind({});
19 | SwagOverviewItem.args = {
20 | history: {
21 | push: () => {},
22 | },
23 | id: 1,
24 | name: "Sauce Labs Bolt T-Shirt",
25 | desc: `Get your testing superhero on with the Sauce Labs bolt T-shirt. From American Apparel, 100% ringspun combed cotton, heather gray with red bolt.`,
26 | price: 15.99,
27 | image_url: "bolt-shirt-1200x1500.jpg",
28 | };
29 |
--------------------------------------------------------------------------------
/src/storybook/stories/Select.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Select from "../../components/Select";
3 |
4 | export default {
5 | title: "SwagLabs/Form/Select",
6 | component: Select,
7 | parameters: {
8 | // layout: "centered",
9 | backgrounds: {
10 | default: "grey",
11 | values: [
12 | {
13 | name: "grey",
14 | value: "#474c55",
15 | },
16 | ],
17 | },
18 | },
19 | };
20 |
21 | const Template = () => {
22 | const [activeOption, setActiveOption] = useState("az");
23 | const options = [
24 | { key: "az", value: "Name (A to Z)" },
25 | { key: "za", value: "Name (Z to A)" },
26 | { key: "lohi", value: "Price (low to high)" },
27 | { key: "hilo", value: "Price (high to low)" },
28 | ];
29 | const sortByOption = (event) => {
30 | setActiveOption(event.target.value);
31 | };
32 |
33 | return (
34 | <>
35 |
41 |
42 | You selected{" "}
43 |
44 | {
45 | options[options.findIndex((option) => option.key === activeOption)]
46 | .value
47 | }
48 |
49 | .
50 |
51 | >
52 | );
53 | };
54 |
55 | export const DefaultSelect = Template.bind({});
56 |
--------------------------------------------------------------------------------
/src/storybook/stories/SubmitButton.stories.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import SubmitButton from "../../components/SubmitButton";
3 |
4 | export default {
5 | title: "SwagLabs/Form/Submit Button",
6 | component: SubmitButton,
7 | };
8 |
9 | const Template = (args) => {
10 | const [value, setValue] = useState(0);
11 | return (
12 | <>
13 | setValue(value + 1)} />
14 |
15 | You clicked {value} times.
16 |
17 | >
18 | );
19 | };
20 |
21 | export const Submit = Template.bind({});
22 | Submit.args = {
23 | value: "Submit button",
24 | };
25 |
--------------------------------------------------------------------------------
/src/storybook/stories/assets/code-brackets.svg:
--------------------------------------------------------------------------------
1 | illustration/code-brackets
--------------------------------------------------------------------------------
/src/storybook/stories/assets/comments.svg:
--------------------------------------------------------------------------------
1 | illustration/comments
--------------------------------------------------------------------------------
/src/storybook/stories/assets/direction.svg:
--------------------------------------------------------------------------------
1 | illustration/direction
--------------------------------------------------------------------------------
/src/storybook/stories/assets/flow.svg:
--------------------------------------------------------------------------------
1 | illustration/flow
--------------------------------------------------------------------------------
/src/storybook/stories/assets/plugin.svg:
--------------------------------------------------------------------------------
1 | illustration/plugin
--------------------------------------------------------------------------------
/src/storybook/stories/assets/repo.svg:
--------------------------------------------------------------------------------
1 | illustration/repo
--------------------------------------------------------------------------------
/src/storybook/stories/assets/stackalt.svg:
--------------------------------------------------------------------------------
1 | illustration/stackalt
--------------------------------------------------------------------------------
/src/utils/Constants.js:
--------------------------------------------------------------------------------
1 | export const FONT_FAMILY = "Roboto, Arial, Helvetica, sans-serif";
2 | export const VALID_USERNAMES = [
3 | "standard_user",
4 | "locked_out_user",
5 | "problem_user",
6 | "performance_glitch_user",
7 | "error_user",
8 | "visual_user",
9 | ];
10 | export const VALID_PASSWORD = "secret_sauce";
11 | export const ROUTES = {
12 | LOGIN: "/",
13 | INVENTORY: "/inventory.html",
14 | INVENTORY_LONG: "/inventory-long.html",
15 | INVENTORY_LIST: "/inventory-item.html",
16 | CART: "/cart.html",
17 | CHECKOUT_STEP_ONE: "/checkout-step-one.html",
18 | CHECKOUT_STEP_TWO: "/checkout-step-two.html",
19 | CHECKOUT_COMPLETE: "/checkout-complete.html",
20 | };
21 | export const CART_CONTENTS = "cart-contents";
22 | export const SESSION_USERNAME = "session-username";
23 |
--------------------------------------------------------------------------------
/src/utils/Credentials.js:
--------------------------------------------------------------------------------
1 | import Cookies from "js-cookie";
2 | import { SESSION_USERNAME, VALID_PASSWORD, VALID_USERNAMES } from "./Constants";
3 |
4 | /**
5 | * Verify the credentials
6 | *
7 | * @param {string} username
8 | * @param {string} password
9 | *
10 | * @return {boolean}
11 | */
12 | export function verifyCredentials(username, password) {
13 | if (password !== VALID_PASSWORD) {
14 | return false;
15 | }
16 |
17 | return VALID_USERNAMES.includes(username);
18 | }
19 |
20 | /**
21 | * Store the data in our cookies
22 | *
23 | * @param {string} username
24 | *
25 | * @param {string} password
26 | */
27 | export function setCredentials(username, password) {
28 | let date = new Date();
29 | date.setTime(date.getTime() + 10 * 60 * 1000);
30 |
31 | Cookies.set(SESSION_USERNAME, username, { expires: date });
32 | }
33 |
34 | /**
35 | * Remove the credentials
36 | */
37 | export function removeCredentials() {
38 | Cookies.remove(SESSION_USERNAME);
39 | }
40 |
41 | /**
42 | * Return current logged username
43 | *
44 | * @return {string | undefined}
45 | */
46 | export function currentUser() {
47 | return Cookies.get(SESSION_USERNAME);
48 | }
49 |
50 | /**
51 | * Check if this is a problem user
52 | *
53 | * @return {boolean}
54 | */
55 | export function isProblemUser() {
56 | return Cookies.get(SESSION_USERNAME) === "problem_user";
57 | }
58 |
59 | /**
60 | * Check if this is a performance user
61 | *
62 | * @return {boolean}
63 | */
64 | export function isPerformanceGlitchUser() {
65 | return Cookies.get(SESSION_USERNAME) === "performance_glitch_user";
66 | }
67 |
68 | /**
69 | * Check if this a logged out user
70 | *
71 | * @return {boolean}
72 | */
73 | export function isLockedOutUser() {
74 | return Cookies.get(SESSION_USERNAME) === "locked_out_user";
75 | }
76 |
77 | /**
78 | * Check if this is an error user
79 | *
80 | * @return {boolean}
81 | */
82 | export function isErrorUser() {
83 | return Cookies.get(SESSION_USERNAME) === "error_user";
84 | }
85 |
86 | /**
87 | * Check if the user is logged in with a valid username
88 | *
89 | * @return {boolean}
90 | */
91 | export function isLoggedIn() {
92 | const sessionUsername = Cookies.get(SESSION_USERNAME);
93 | const isValidUsername = VALID_USERNAMES.includes(sessionUsername);
94 |
95 | return isValidUsername && sessionUsername !== "locked_out_user";
96 | }
97 |
98 | /**
99 | * Check if this is a visual user
100 | *
101 | * @return {boolean}
102 | */
103 | export function isVisualUser() {
104 | return Cookies.get(SESSION_USERNAME) === "visual_user";
105 | }
106 |
--------------------------------------------------------------------------------
/src/utils/InventoryData.js:
--------------------------------------------------------------------------------
1 | export const InventoryData = [
2 | {
3 | id: 0,
4 | name: "Sauce Labs Bike Light",
5 | desc: `A red light isn't the desired state in testing but it sure helps when riding your bike at night. Water-resistant with 3 lighting modes, 1 AAA battery included.`,
6 | price: 9.99,
7 | image_url: "bike-light-1200x1500.jpg",
8 | },
9 | {
10 | id: 1,
11 | name: "Sauce Labs Bolt T-Shirt",
12 | desc: `Get your testing superhero on with the Sauce Labs bolt T-shirt. From American Apparel, 100% ringspun combed cotton, heather gray with red bolt.`,
13 | price: 15.99,
14 | image_url: "bolt-shirt-1200x1500.jpg",
15 | },
16 | {
17 | id: 2,
18 | name: "Sauce Labs Onesie",
19 | desc: `Rib snap infant onesie for the junior automation engineer in development. Reinforced 3-snap bottom closure, two-needle hemmed sleeved and bottom won't unravel.`,
20 | price: 7.99,
21 | image_url: "red-onesie-1200x1500.jpg",
22 | },
23 | {
24 | id: 3,
25 | name: "Test.allTheThings() T-Shirt (Red)",
26 | desc: `This classic Sauce Labs t-shirt is perfect to wear when cozying up to your keyboard to automate a few tests. Super-soft and comfy ringspun combed cotton.`,
27 | price: 15.99,
28 | image_url: "red-tatt-1200x1500.jpg",
29 | },
30 | {
31 | id: 4,
32 | name: "Sauce Labs Backpack",
33 | desc: `carry.allTheThings() with the sleek, streamlined Sly Pack that melds uncompromising style with unequaled laptop and tablet protection.`,
34 | price: 29.99,
35 | image_url: "sauce-backpack-1200x1500.jpg",
36 | },
37 | {
38 | id: 5,
39 | name: "Sauce Labs Fleece Jacket",
40 | desc: `It's not every day that you come across a midweight quarter-zip fleece jacket capable of handling everything from a relaxing day outdoors to a busy day at the office.`,
41 | price: 49.99,
42 | image_url: "sauce-pullover-1200x1500.jpg",
43 | },
44 | ];
45 |
--------------------------------------------------------------------------------
/src/utils/Sorting.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Sort array of objects asc
3 | *
4 | * @param {Array} data
5 | * @param {string} property
6 | * @returns {Array}
7 | */
8 | export function sortAsc(data, property) {
9 | return [...data].sort((a, b) => a[property].localeCompare(b[property]));
10 | }
11 |
12 | /**
13 | * Sort array of objects desc
14 | *
15 | * @param {Array} data
16 | * @param {string} property
17 | * @returns {Array}
18 | */
19 | export function sortDesc(data, property) {
20 | return [...data].sort((a, b) => b[property].localeCompare(a[property]));
21 | }
22 |
23 | /**
24 | * Sort array of objects asc
25 | *
26 | * @param {Array} data
27 | * @param {string} property
28 | * @returns {Array}
29 | */
30 | export function sortLoHi(data, property) {
31 | return [...data].sort((a, b) => Number(a[property]) - Number(b[property]));
32 | }
33 |
34 | /**
35 | * Sort array of objects asc
36 | *
37 | * @param {Array} data
38 | * @param {string} property
39 | * @returns {Array}
40 | */
41 | export function sortHiLo(data, property) {
42 | return [...data].sort((a, b) => Number(b[property]) - Number(a[property]));
43 | }
44 |
--------------------------------------------------------------------------------
/src/utils/__mocks__/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = "test-file-stub";
2 |
--------------------------------------------------------------------------------
/src/utils/__tests__/Sorting.tests.js:
--------------------------------------------------------------------------------
1 | import { sortAsc, sortDesc, sortHiLo, sortLoHi } from "../Sorting";
2 |
3 | const data = [
4 | { id: 0, name: "Bar" },
5 | { id: 2.1, name: "Ar" },
6 | { id: 99.9, name: "Rab" },
7 | ];
8 |
9 | it("should be able to sort an array with objects asc based on a property", () => {
10 | expect(sortAsc(data, "name")).toMatchSnapshot();
11 | });
12 |
13 | it("should be able to sort an array with objects desc based on a property", () => {
14 | expect(sortDesc(data, "name")).toMatchSnapshot();
15 | });
16 |
17 | it("should be able to sort an array with objects lo to high based on a property", () => {
18 | expect(sortLoHi(data, "id")).toMatchSnapshot();
19 | });
20 |
21 | it("should be able to sort an array with objects hi to low based on a property", () => {
22 | expect(sortHiLo(data, "id")).toMatchSnapshot();
23 | });
24 |
--------------------------------------------------------------------------------
/src/utils/__tests__/__snapshots__/Sorting.tests.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should be able to sort an array with objects asc based on a property 1`] = `
4 | Array [
5 | Object {
6 | "id": 2.1,
7 | "name": "Ar",
8 | },
9 | Object {
10 | "id": 0,
11 | "name": "Bar",
12 | },
13 | Object {
14 | "id": 99.9,
15 | "name": "Rab",
16 | },
17 | ]
18 | `;
19 |
20 | exports[`should be able to sort an array with objects desc based on a property 1`] = `
21 | Array [
22 | Object {
23 | "id": 99.9,
24 | "name": "Rab",
25 | },
26 | Object {
27 | "id": 0,
28 | "name": "Bar",
29 | },
30 | Object {
31 | "id": 2.1,
32 | "name": "Ar",
33 | },
34 | ]
35 | `;
36 |
37 | exports[`should be able to sort an array with objects hi to low based on a property 1`] = `
38 | Array [
39 | Object {
40 | "id": 99.9,
41 | "name": "Rab",
42 | },
43 | Object {
44 | "id": 2.1,
45 | "name": "Ar",
46 | },
47 | Object {
48 | "id": 0,
49 | "name": "Bar",
50 | },
51 | ]
52 | `;
53 |
54 | exports[`should be able to sort an array with objects lo to high based on a property 1`] = `
55 | Array [
56 | Object {
57 | "id": 0,
58 | "name": "Bar",
59 | },
60 | Object {
61 | "id": 2.1,
62 | "name": "Ar",
63 | },
64 | Object {
65 | "id": 99.9,
66 | "name": "Rab",
67 | },
68 | ]
69 | `;
70 |
--------------------------------------------------------------------------------
/src/utils/__tests__/shopping-cart.tests.js:
--------------------------------------------------------------------------------
1 | import { ShoppingCart } from "../../utils/shopping-cart";
2 |
3 | describe("shopping-cart", () => {
4 | beforeEach(() => {
5 | // Clear the testing storage
6 | window.localStorage.removeItem("cart-contents");
7 |
8 | // Rest the listeners
9 | ShoppingCart.LISTENERS = [];
10 | });
11 |
12 | it("should be able to add an item to the cart", () => {
13 | window.localStorage.setItem("cart-contents", JSON.stringify([1, 2]));
14 |
15 | expect(window.localStorage.getItem("cart-contents")).toEqual(
16 | JSON.stringify([1, 2])
17 | );
18 |
19 | ShoppingCart.addItem(3);
20 |
21 | expect(ShoppingCart.getCartContents()).toEqual([1, 2, 3]);
22 | });
23 |
24 | it("should be able to remove an item from the cart", () => {
25 | window.localStorage.setItem("cart-contents", JSON.stringify([1, 2, 3]));
26 |
27 | expect(window.localStorage.getItem("cart-contents")).toEqual(
28 | JSON.stringify([1, 2, 3])
29 | );
30 |
31 | ShoppingCart.removeItem(2);
32 |
33 | expect(ShoppingCart.getCartContents()).toEqual([1, 3]);
34 | });
35 |
36 | it("should get an empty array when there are no items in the cart", () => {
37 | expect(ShoppingCart.getCartContents()).toEqual([]);
38 | });
39 |
40 | it("should get the cart content if there are items", () => {
41 | const items = [1, 2, 3];
42 | window.localStorage.setItem("cart-contents", JSON.stringify(items));
43 |
44 | expect(ShoppingCart.getCartContents()).toEqual(items);
45 | });
46 |
47 | it("should be able to set the content in the cart", () => {
48 | const items = [1, 2, 3];
49 |
50 | expect(window.localStorage.getItem("cart-contents")).toEqual(null);
51 |
52 | ShoppingCart.setCartContents(items);
53 |
54 | expect(window.localStorage.getItem("cart-contents")).toEqual(
55 | JSON.stringify(items)
56 | );
57 | });
58 |
59 | it("should be able to reset the cart", () => {
60 | const items = JSON.stringify([1, 2, 3]);
61 | window.localStorage.setItem("cart-contents", items);
62 |
63 | ShoppingCart.resetCart();
64 |
65 | expect(window.localStorage.getItem("cart-contents")).toEqual(null);
66 | });
67 |
68 | it("should be able to register cart listeners", () => {
69 | expect(ShoppingCart.LISTENERS).toEqual([]);
70 |
71 | ShoppingCart.registerCartListener({ foo: true });
72 |
73 | expect(ShoppingCart.LISTENERS).toEqual([{ foo: true }]);
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/src/utils/shopping-cart.js:
--------------------------------------------------------------------------------
1 | export class ShoppingCart {
2 | static addItem(itemId) {
3 | // pull out our current cart contents
4 | const curContents = ShoppingCart.getCartContents();
5 |
6 | /* istanbul ignore else */
7 | if (curContents.indexOf(itemId) < 0) {
8 | // Item's not yet present - add it now
9 | curContents.push(itemId);
10 |
11 | // We modified our cart, so store it now
12 | ShoppingCart.setCartContents(curContents);
13 | }
14 | }
15 |
16 | static removeItem(itemId) {
17 | // pull out our current cart contents
18 | const curContents = ShoppingCart.getCartContents();
19 | const itemIndex = curContents.indexOf(itemId);
20 |
21 | /* istanbul ignore else */
22 | if (itemIndex >= 0) {
23 | // Remove this item from the array
24 | curContents.splice(itemIndex, 1);
25 |
26 | // We modified our cart, so store it now
27 | ShoppingCart.setCartContents(curContents);
28 | }
29 | }
30 |
31 | static isItemInCart(itemId) {
32 | // pull out our current cart contents
33 | const curContents = ShoppingCart.getCartContents();
34 |
35 | // If the item is in the array, return true
36 | return curContents.indexOf(itemId) >= 0;
37 | }
38 |
39 | static getCartContents() {
40 | // pull out our current cart contents
41 | let curContents = window.localStorage.getItem("cart-contents");
42 |
43 | // Make an empty list if this is the first item
44 | if (curContents == null) {
45 | curContents = [];
46 | } else {
47 | // We have an existing cart, so deserialize it now since localStorage stores in JSON strings
48 | curContents = JSON.parse(curContents);
49 | }
50 |
51 | return curContents;
52 | }
53 |
54 | static setCartContents(newContents) {
55 | window.localStorage.setItem("cart-contents", JSON.stringify(newContents));
56 |
57 | // Notify our listeners
58 | /* istanbul ignore next */
59 | ShoppingCart.LISTENERS.forEach((curListener) => {
60 | curListener.forceUpdate();
61 | });
62 | }
63 |
64 | static resetCart() {
65 | window.localStorage.removeItem("cart-contents");
66 |
67 | // Notify our listeners
68 | /* istanbul ignore next */
69 | ShoppingCart.LISTENERS.forEach((curListener) => {
70 | curListener.forceUpdate();
71 | });
72 | }
73 |
74 | static registerCartListener(handler) {
75 | ShoppingCart.LISTENERS.push(handler);
76 | }
77 | }
78 |
79 | ShoppingCart.LISTENERS = [];
80 |
--------------------------------------------------------------------------------
/test/e2e/configs/e2eConstants.js:
--------------------------------------------------------------------------------
1 | export const DEFAULT_TIMEOUT = 15000;
2 | export const PAGES = {
3 | CART: '/cart.html',
4 | CHECKOUT_COMPLETE: '/checkout-complete.html',
5 | CHECKOUT_PERSONAL_INFO: '/checkout-step-one.html',
6 | CHECKOUT_SUMMARY: '/checkout-step-two.html',
7 | LOGIN: '',
8 | SWAG_DETAILS: '/inventory-item.html',
9 | SWAG_ITEMS: '/inventory.html',
10 | };
11 | export const PRODUCTS = {
12 | BIKE_LIGHT: 0,
13 | BOLT_SHIRT: 1,
14 | ONE_SIE: 2,
15 | TATT_SHIRT: 3,
16 | BACKPACK: 4,
17 | FLEECE_JACKET: 5,
18 | };
19 | export const LOGIN_USERS = {
20 | LOCKED: {
21 | username: 'locked_out_user',
22 | password: 'secret_sauce',
23 | },
24 | NO_MATCH: {
25 | username: 'd',
26 | password: 'd',
27 | },
28 | NO_USER_DETAILS: {
29 | username: '',
30 | password: '',
31 | },
32 | NO_PASSWORD: {
33 | username: 'standard_user',
34 | password: '',
35 | },
36 | PERFORMANCE: {
37 | username: 'performance_glitch_user',
38 | password: 'secret_sauce',
39 | },
40 | STANDARD: {
41 | username: 'standard_user',
42 | password: 'secret_sauce',
43 | },
44 | };
45 | export const PERSONAL_INFO = {
46 | STANDARD: {
47 | firstName: 'Sauce',
48 | lastName: 'Bot',
49 | zip: '94105',
50 | },
51 | NO_FIRSTNAME: {
52 | firstName: '',
53 | lastName: 'Bot',
54 | zip: '94105',
55 | },
56 | NO_LAST_NAME: {
57 | firstName: 'Sauce',
58 | lastName: '',
59 | zip: '94105',
60 | },
61 | NO_POSTAL_CODE: {
62 | firstName: 'Sauce',
63 | lastName: 'Bot',
64 | zip: '',
65 | },
66 | };
67 |
--------------------------------------------------------------------------------
/test/e2e/configs/wdio.local.chrome.conf.js:
--------------------------------------------------------------------------------
1 | const { config } = require('./wdio.shared.conf');
2 |
3 | // ============
4 | // Capabilities
5 | // ============
6 | config.capabilities = [
7 | // Chrome example
8 | {
9 | browserName: 'chrome',
10 | 'goog:chromeOptions': {
11 | args: [
12 | '--no-sandbox',
13 | '--disable-infobars',
14 | '--headless',
15 | ],
16 | },
17 |
18 | },
19 | ];
20 |
21 | // ========
22 | // Services
23 | // ========
24 | config.services = ['chromedriver'];
25 |
26 | exports.config = config;
27 |
--------------------------------------------------------------------------------
/test/e2e/configs/wdio.saucelabs-orchestrate.conf.js:
--------------------------------------------------------------------------------
1 | const BUILD_PREFIX = process.env.BUILD_PREFIX ? `GitHub Actions-` : '';
2 | const {config} = require('./wdio.shared.conf');
3 | const defaultBrowserSauceOptions = {
4 | build: `${BUILD_PREFIX}Sauce Demo App build-${new Date().getTime()}`,
5 | screenResolution: '1600x1200',
6 | };
7 |
8 | // =========================
9 | // Sauce RDC specific config
10 | // =========================
11 | config.user = process.env.SAUCE_USERNAME;
12 | config.key = process.env.SAUCE_ACCESS_KEY;
13 | // If you run your tests on Sauce Labs you can specify the region you want to run your tests
14 | // in via the `region` property. Available short handles for regions are `us` (default) and `eu`.
15 | // These regions are used for the Sauce Labs VM cloud and the Sauce Labs Real Device Cloud.
16 | // If you don't provide the region, it defaults to `us`.
17 | config.region = process.env.REGION || 'us';
18 |
19 | // ============
20 | // Capabilities
21 | // ============
22 | config.capabilities = [
23 | /**
24 | * Desktop browsers
25 | */
26 | {
27 | browserName: 'chrome',
28 | platformName: 'Windows 10',
29 | browserVersion: 'latest',
30 | 'sauce:options': {
31 | ...defaultBrowserSauceOptions,
32 | tunnelIdentifier: "mdonovan2010_tunnel_name"
33 | },
34 | },
35 | ];
36 |
37 | exports.config = config;
38 |
--------------------------------------------------------------------------------
/test/e2e/configs/wdio.shared.conf.js:
--------------------------------------------------------------------------------
1 | exports.config = {
2 | // ====================
3 | // Runner Configuration
4 | // ====================
5 | runner: 'local',
6 | // ==================
7 | // Specify Test Files
8 | // ==================
9 | specs: [
10 | './test/e2e/specs/**/*.js'
11 | ],
12 | // ============
13 | // Capabilities
14 | // ============
15 | maxInstances: 20,
16 | // capabilities can be found in the `wdio.local.chrome.conf.js` or `wdio.sauce.conf.js`
17 | // ===================
18 | // Test Configurations
19 | // ===================
20 | logLevel: 'silent',
21 | bail: 0,
22 | baseUrl: 'http://localhost:3000',
23 | waitforTimeout: 10000,
24 | connectionRetryTimeout: 90000,
25 | connectionRetryCount: 3,
26 | framework: 'jasmine',
27 | reporters: ['spec'],
28 | jasmineOpts: {
29 | defaultTimeoutInterval: 60000,
30 | helpers: [require.resolve('@babel/register')],
31 | },
32 | services: [],
33 | };
34 |
--------------------------------------------------------------------------------
/test/e2e/helpers/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Set the test context
3 | *
4 | * @param {object} data
5 | * @param {object} data.user
6 | * @param {string} data.user.username
7 | * @param {string} data.user.password
8 | * @param {string} data.path
9 | * @param {array} data.products
10 | */
11 | export function setTestContext(data = {}) {
12 | const isSauce = browser.config.hostname && browser.config.hostname.includes('saucelabs');
13 | const {path, products = [], user} = data;
14 | const {username} = user;
15 | const userCookies = `document.cookie="session-username=${username}";`;
16 | // We initially used `sessionStorage` in the browsers but that one didn't work properly and got lost after a new
17 | // `browser.url('{some-url}')`. `localStorage` is more stable
18 | const productStorage = products.length > 0 ? `localStorage.setItem("cart-contents", "[${products.toString()}]");` : '';
19 |
20 | if(isSauce) {
21 | // Log extra context in Sauce
22 | browser.execute('sauce:context=#### Start `setTestContext` ####');
23 | }
24 |
25 | // Go to the domain
26 | browser.url('');
27 | // Clear the cookies and storage
28 | browser.deleteAllCookies();
29 | browser.execute('localStorage.clear();');
30 | // Set the new cookies and storage
31 | browser.execute(`${userCookies} ${productStorage}`);
32 | browser.pause(1000);
33 | // Now got to the page
34 | browser.url(path);
35 |
36 | if(isSauce) {
37 | // Log extra context in Sauce
38 | browser.execute('sauce:context=#### End `setTestContext` ####');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/test/e2e/page-objects/AppHeaderPage.js:
--------------------------------------------------------------------------------
1 | class AppHeaderPage {
2 | // Make it private so people can't mess with it
3 | // Source: https://github.com/tc39/proposal-class-fields#private-fields
4 | get #cart() {
5 | return $('.shopping_cart_link');
6 | }
7 |
8 | get #productFilter() {
9 | return $('.product_sort_container');
10 | }
11 |
12 | /**
13 | * Get the cart amount
14 | *
15 | * @return {string}
16 | */
17 | getCartAmount() {
18 | browser.pause(500)
19 |
20 | return this.#cart.getText();
21 | }
22 |
23 | /**
24 | * Open the cart
25 | */
26 | openCart() {
27 | this.#cart.click();
28 | }
29 |
30 | /**
31 | * Select the order based on visible text
32 | *
33 | * @param {string} text
34 | */
35 | selectProductOrder(text){
36 | this.#productFilter.selectByVisibleText(text)
37 | }
38 | }
39 |
40 | export default new AppHeaderPage();
41 |
--------------------------------------------------------------------------------
/test/e2e/page-objects/BasePage.js:
--------------------------------------------------------------------------------
1 | import {DEFAULT_TIMEOUT} from '../configs/e2eConstants';
2 |
3 | export default class BasePage {
4 | constructor(selector) {
5 | this.selector = selector;
6 | }
7 |
8 | /**
9 | * Wait for the element to be displayed
10 | *
11 | * @return {boolean}
12 | */
13 | waitForIsShown(isShown = true) {
14 | try{
15 | return $(this.selector).waitForDisplayed({
16 | timeout: DEFAULT_TIMEOUT,
17 | reverse: !isShown
18 | });
19 | } catch (e) {
20 | return !isShown;
21 | }
22 | }
23 |
24 | /**
25 | * Give back if the element is displayed
26 | *
27 | * @return {boolean}
28 | */
29 | isDisplayed() {
30 | return $(this.selector).isDisplayed();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/test/e2e/page-objects/CartSummaryPage.js:
--------------------------------------------------------------------------------
1 | import BasePage from './BasePage';
2 |
3 | const SCREEN_SELECTOR = '#cart_contents_container';
4 |
5 | class CartSummaryPage extends BasePage {
6 | constructor() {
7 | super(SCREEN_SELECTOR);
8 | }
9 |
10 | // Make it private so people can't mess with it
11 | // Source: https://github.com/tc39/proposal-class-fields#private-fields
12 | get #screen() {
13 | return $(SCREEN_SELECTOR);
14 | }
15 |
16 | get #checkoutButton() {
17 | return $('.checkout_button');
18 | }
19 |
20 | get #continueShoppingButton() {
21 | return $('.btn_secondary');
22 | }
23 |
24 | get #items() {
25 | return $$('.cart_item');
26 | }
27 |
28 | /**
29 | * Get the amount of swag items in the cart
30 | */
31 | getSwagAmount() {
32 | return this.#items.length;
33 | }
34 |
35 | /**
36 | * Get a cart Item based on a search string or a number of the visible items
37 | *
38 | * @param {number|string} needle
39 | *
40 | * @return the selected cart swag
41 | */
42 | swag(needle) {
43 | if (typeof needle === 'string') {
44 | return this.#items.find(cartItem => cartItem.getText().includes(needle));
45 | }
46 |
47 | return this.#items[needle];
48 | }
49 |
50 | /**
51 | * Get the text of the cart swag text
52 | *
53 | * @param {number|string} needle
54 | *
55 | * @return {string}
56 | */
57 | getSwagText(needle) {
58 | return this.swag(needle).getText();
59 | }
60 |
61 | /**
62 | * Remove an swag from the cart
63 | *
64 | * @param {number|string} needle
65 | */
66 | removeSwag(needle) {
67 | this.swag(needle).$('.btn_secondary.cart_button').click();
68 | }
69 |
70 | /**
71 | * Continue shopping
72 | */
73 | continueShopping() {
74 | this.#continueShoppingButton.click();
75 | }
76 |
77 | /**
78 | * Go to the checkout process
79 | */
80 | goToCheckout() {
81 | this.#checkoutButton.click();
82 | }
83 | }
84 |
85 | export default new CartSummaryPage();
86 |
--------------------------------------------------------------------------------
/test/e2e/page-objects/CheckoutCompletePage.js:
--------------------------------------------------------------------------------
1 | import BasePage from './BasePage';
2 |
3 | const SCREEN_SELECTOR = '#checkout_complete_container';
4 |
5 | class CheckoutCompletePage extends BasePage {
6 | constructor() {
7 | super(SCREEN_SELECTOR);
8 | }
9 |
10 | // Make it private so people can't mess with it
11 | // Source: https://github.com/tc39/proposal-class-fields#private-fields
12 | get #screen() {
13 | return $(SCREEN_SELECTOR);
14 | }
15 | }
16 |
17 | export default new CheckoutCompletePage();
18 |
--------------------------------------------------------------------------------
/test/e2e/page-objects/CheckoutPersonalInfoPage.js:
--------------------------------------------------------------------------------
1 | import BasePage from './BasePage';
2 | import {DEFAULT_TIMEOUT} from '../configs/e2eConstants';
3 |
4 | const SCREEN_SELECTOR = '#checkout_info_container';
5 |
6 | class CheckoutPersonalInfoPage extends BasePage {
7 | constructor() {
8 | super(SCREEN_SELECTOR);
9 | }
10 |
11 | // Make it private so people can't mess with it
12 | // Source: https://github.com/tc39/proposal-class-fields#private-fields
13 | get #screen() {
14 | return $(SCREEN_SELECTOR);
15 | }
16 |
17 | get #cancelButton() {
18 | return $('.cart_cancel_link');
19 | }
20 |
21 | get #continueCheckoutButton() {
22 | return $('.cart_button');
23 | }
24 |
25 | get #firstName() {
26 | return $('[data-test="firstName"]');
27 | }
28 |
29 | get #lastName() {
30 | return $('[data-test="lastName"]');
31 | }
32 |
33 | get #postalCode() {
34 | return $('[data-test="postalCode"]');
35 | }
36 |
37 | get #errorMessage() {
38 | return $('[data-test="error"]');
39 | }
40 |
41 | /**
42 | * Submit personal info
43 | *
44 | * @param {object} personalInfo
45 | * @param {string} personalInfo.firstName
46 | * @param {string} personalInfo.lastName
47 | * @param {string} personalInfo.zip
48 | */
49 | submitPersonalInfo(personalInfo) {
50 | const {firstName, lastName, zip} = personalInfo;
51 |
52 | this.waitForIsShown();
53 | this.#firstName.addValue(firstName);
54 | this.#lastName.addValue(lastName);
55 | this.#postalCode.addValue(zip);
56 | this.#continueCheckoutButton.click();
57 | }
58 |
59 | /**
60 | * Get the text or the error message container
61 | *
62 | * @return {string}
63 | */
64 | getErrorMessage() {
65 | this.#errorMessage.waitForDisplayed({timeout: DEFAULT_TIMEOUT});
66 |
67 | return this.#errorMessage.getText();
68 | }
69 |
70 | /**
71 | * Cancel checkout
72 | */
73 | cancelCheckout() {
74 | this.#cancelButton.click();
75 | }
76 | }
77 |
78 | export default new CheckoutPersonalInfoPage();
79 |
--------------------------------------------------------------------------------
/test/e2e/page-objects/CheckoutSummaryPage.js:
--------------------------------------------------------------------------------
1 | import BasePage from './BasePage';
2 |
3 | const SCREEN_SELECTOR = '#checkout_summary_container';
4 |
5 | class CheckoutSummaryPage extends BasePage {
6 | constructor() {
7 | super(SCREEN_SELECTOR);
8 | }
9 |
10 | // Make it private so people can't mess with it
11 | // Source: https://github.com/tc39/proposal-class-fields#private-fields
12 | get #screen() {
13 | return $(SCREEN_SELECTOR);
14 | }
15 |
16 | title(needle) {
17 | return this.swag(needle).$('.inventory_item_name');
18 | }
19 |
20 | description(needle) {
21 | return this.swag(needle).$('.inventory_item_desc');
22 | }
23 |
24 | price(needle) {
25 | return this.swag(needle).$('.inventory_item_price');
26 | }
27 |
28 | get #cancelButton() {
29 | return $('.cart_cancel_link');
30 | }
31 |
32 | get #finishButton() {
33 | return $('.cart_button');
34 | }
35 |
36 | get #items() {
37 | return $$('.cart_item');
38 | }
39 |
40 | /**
41 | * Get the amount of swag items listed on the page
42 | * @returns {number}
43 | */
44 | getSwagAmount() {
45 | return this.#items.length;
46 | }
47 |
48 | /**
49 | * Get a cart Item based on a search string or a number of the visible items
50 | *
51 | * @param {number|string} needle
52 | *
53 | * @return the selected cart swag
54 | */
55 | swag(needle) {
56 | if (typeof needle === 'string') {
57 | return this.#items.find(cartItem => cartItem.getText().includes(needle));
58 | }
59 |
60 | return this.#items[needle];
61 | }
62 |
63 | /**
64 | * Get the text of the cart
65 | *
66 | * @param {number|string} needle
67 | *
68 | * @return {string}
69 | */
70 | getSwagText(needle) {
71 | return `${this.title(needle).getText()} ${this.description(needle).getText()} ${this.price(needle).getText()}`;
72 | }
73 |
74 | /**
75 | * Cancel checkout
76 | */
77 | cancelCheckout() {
78 | this.#cancelButton.click();
79 | }
80 |
81 | /**
82 | * Finish checkout
83 | */
84 | finishCheckout() {
85 | this.#finishButton.click();
86 | }
87 | }
88 |
89 | export default new CheckoutSummaryPage();
90 |
--------------------------------------------------------------------------------
/test/e2e/page-objects/LoginPage.js:
--------------------------------------------------------------------------------
1 | import BasePage from './BasePage';
2 | import {DEFAULT_TIMEOUT} from '../configs/e2eConstants';
3 |
4 | const SCREEN_SELECTOR = '#login_button_container';
5 |
6 | class LoginPage extends BasePage {
7 | constructor() {
8 | super(SCREEN_SELECTOR);
9 | }
10 |
11 | // Make it private so people can't mess with it
12 | // Source: https://github.com/tc39/proposal-class-fields#private-fields
13 | get #screen() {
14 | return $(SCREEN_SELECTOR);
15 | }
16 |
17 | get #username() {
18 | return $('#user-name');
19 | }
20 |
21 | get #password() {
22 | return $('#password');
23 | }
24 |
25 | get #loginButton() {
26 | return $('.btn_action');
27 | }
28 |
29 | get #errorMessage() {
30 | return $('[data-test="error"]');
31 | }
32 |
33 | /**
34 | * Sign in
35 | *
36 | * @param {object} userDetails
37 | * @param {string} userDetails.username
38 | * @param {string} userDetails.password
39 | */
40 | signIn(userDetails) {
41 | const {password, username} = userDetails;
42 |
43 | this.waitForIsShown();
44 | this.#username.setValue(username);
45 | this.#password.setValue(password);
46 | if (browser.isAndroid) {
47 | return browser.execute('document.querySelector(\'.btn_action\').click()');
48 | }
49 |
50 | this.#loginButton.click();
51 | }
52 |
53 | /**
54 | * Get the text or the error message container
55 | *
56 | * @return {string}
57 | */
58 | getErrorMessage() {
59 | this.#errorMessage.waitForDisplayed({timeout: DEFAULT_TIMEOUT});
60 |
61 | return this.#errorMessage.getText();
62 | }
63 |
64 | /**
65 | * Check if the error message is displayed
66 | *
67 | * @return {boolean}
68 | */
69 | isErrorMessageDisplayed() {
70 | return this.#errorMessage.isDisplayed();
71 | }
72 | }
73 |
74 | export default new LoginPage();
75 |
--------------------------------------------------------------------------------
/test/e2e/page-objects/MenuPage.js:
--------------------------------------------------------------------------------
1 | class MenuPage {
2 | // Make it private so people can't mess with it
3 | // Source: https://github.com/tc39/proposal-class-fields#private-fields
4 | get #menu() {
5 | return $('.bm-burger-button');
6 | }
7 |
8 | get #inventoryListButton() {
9 | return $('#inventory_sidebar_link');
10 | }
11 |
12 | get #aboutButton() {
13 | return $('#about_sidebar_link');
14 | }
15 |
16 | get #logoutButton() {
17 | return $('#logout_sidebar_link');
18 | }
19 |
20 | get #resetButton() {
21 | return $('#reset_sidebar_link');
22 | }
23 |
24 | /**
25 | * Open the menu
26 | */
27 | open() {
28 | if (browser.isIOS) {
29 | browser.execute('document.querySelector(\'.bm-burger-button button\').click()');
30 | } else {
31 | this.#menu.click();
32 | }
33 |
34 | browser.pause(500);
35 | }
36 |
37 | /**
38 | * Open the inventory list page
39 | */
40 | openInventoryList() {
41 | this.#inventoryListButton.click();
42 | }
43 |
44 | /**
45 | * Open the about page
46 | */
47 | openAboutPage() {
48 | this.#aboutButton.click();
49 | }
50 |
51 | /**
52 | * Logout
53 | */
54 | logout() {
55 | this.#logoutButton.click();
56 | }
57 |
58 | /**
59 | * Reset the app state
60 | */
61 | restAppState() {
62 | this.#resetButton.click();
63 | }
64 | }
65 |
66 | export default new MenuPage();
67 |
--------------------------------------------------------------------------------
/test/e2e/page-objects/SwagDetailsPage.js:
--------------------------------------------------------------------------------
1 | import BasePage from './BasePage';
2 |
3 | const SCREEN_SELECTOR = '.inventory_details';
4 |
5 | class SwagDetailsPage extends BasePage {
6 | constructor() {
7 | super(SCREEN_SELECTOR);
8 | }
9 |
10 | // Make it private so people can't mess with it
11 | // Source: https://github.com/tc39/proposal-class-fields#private-fields
12 | get #screen() {
13 | return $(SCREEN_SELECTOR);
14 | }
15 |
16 | get #title() {
17 | return $('.inventory_details_name');
18 | }
19 |
20 | get #description() {
21 | return $('.inventory_details_desc');
22 | }
23 |
24 | get #price() {
25 | return $('.inventory_details_price');
26 | }
27 |
28 | get #addButton() {
29 | return $('.btn_primary.btn_inventory');
30 | }
31 |
32 | get #removeButton() {
33 | return $('.btn_secondary.btn_inventory');
34 | }
35 |
36 | get #goBackButton() {
37 | return $('.inventory_details_back_button');
38 | }
39 |
40 | /**
41 | * Get the text of the swag swag
42 | *
43 | * @return {string}
44 | */
45 | getText() {
46 | return `${this.#title.getText()} ${this.#description.getText()} ${this.#price.getText()}`;
47 | }
48 |
49 | /**
50 | * Add a swag items to the cart
51 | */
52 | addToCart() {
53 | this.#addButton.click();
54 | }
55 |
56 | /**
57 | * Remove a swag items from the cart
58 | */
59 | removeFromCart() {
60 | this.#removeButton.click();
61 | }
62 |
63 | /**
64 | * Go back to the inventory list
65 | */
66 | goBack() {
67 | this.#goBackButton.click();
68 | }
69 | }
70 |
71 | export default new SwagDetailsPage();
72 |
--------------------------------------------------------------------------------
/test/e2e/page-objects/SwagOverviewPage.js:
--------------------------------------------------------------------------------
1 | import BasePage from './BasePage';
2 |
3 | const SCREEN_SELECTOR = '.inventory_list';
4 |
5 | class SwagOverviewPage extends BasePage {
6 | constructor() {
7 | super(SCREEN_SELECTOR);
8 | }
9 |
10 | // Make it private so people can't mess with it
11 | // Source: https://github.com/tc39/proposal-class-fields#private-fields
12 | get #screen() {
13 | return $(SCREEN_SELECTOR);
14 | }
15 |
16 | get #swagItems() {
17 | return $$('.inventory_item');
18 | }
19 |
20 | /**
21 | * Get the amount of swag items listed on the page
22 | * @returns {number}
23 | */
24 | getAmount() {
25 | return this.#swagItems.length;
26 | }
27 |
28 | /**
29 | * Get a swag Item based on a search string or a number of the visible items
30 | *
31 | * @param {number|string} needle
32 | *
33 | * @return {Element[]} the selected swag
34 | */
35 | swag(needle) {
36 | if (typeof needle === 'string') {
37 | return this.#swagItems.find(swagItem => swagItem.getText().includes(needle));
38 | }
39 |
40 | return this.#swagItems[needle];
41 | }
42 |
43 | /**
44 | * Get the text of the swag swag text
45 | *
46 | * @param {number|string} needle
47 | *
48 | * @return {string}
49 | */
50 | getSwagText(needle) {
51 | return this.swag(needle).getText();
52 | }
53 |
54 | /**
55 | * Add a swag items to the cart
56 | *
57 | * @param {number|string} needle
58 | */
59 | addSwagToCart(needle) {
60 | this.swag(needle).$('.btn_primary.btn_inventory').click();
61 | }
62 |
63 | /**
64 | * Remove swag items from the cart
65 | *
66 | * @param {number|string} needle
67 | */
68 | removeSwagFromCart(needle) {
69 | this.swag(needle).$('.btn_secondary.btn_inventory').click();
70 | }
71 |
72 | /**
73 | * Open the details of a swag swag
74 | *
75 | * @param {number|string} needle
76 | */
77 | openSwagDetails(needle) {
78 | this.swag(needle).$('.inventory_item_name').click();
79 | }
80 | }
81 |
82 | export default new SwagOverviewPage();
83 |
--------------------------------------------------------------------------------
/test/e2e/specs/cart.summary.spec.js:
--------------------------------------------------------------------------------
1 | import AppHeaderPage from '../page-objects/AppHeaderPage';
2 | import SwagOverviewPage from '../page-objects/SwagOverviewPage';
3 | import CartSummaryPage from '../page-objects/CartSummaryPage';
4 | import CheckoutPersonalInfoPage from '../page-objects/CheckoutPersonalInfoPage';
5 | import {setTestContext} from '../helpers';
6 | import {LOGIN_USERS, PAGES, PRODUCTS} from "../configs/e2eConstants";
7 |
8 | describe('Cart Summary page', () => {
9 | it('should validate that we can continue shopping', () => {
10 | setTestContext({
11 | user: LOGIN_USERS.STANDARD,
12 | path: PAGES.CART,
13 | });
14 |
15 | expect(CartSummaryPage.waitForIsShown()).toEqual(
16 | true,
17 | 'Cart summary screen is still not visible'
18 | );
19 |
20 | // Actual test starts here
21 | CartSummaryPage.continueShopping();
22 |
23 | expect(SwagOverviewPage.waitForIsShown()).toEqual(
24 | true,
25 | 'Inventory screen is still not visible'
26 | );
27 | });
28 |
29 | it('should validate that we can go from the cart to the checkout page', () => {
30 | setTestContext({
31 | user: LOGIN_USERS.STANDARD,
32 | path: PAGES.CART,
33 | });
34 |
35 | expect(CartSummaryPage.waitForIsShown()).toEqual(
36 | true,
37 | 'Cart summary screen is still not visible'
38 | );
39 |
40 | // Actual test starts here
41 | CartSummaryPage.goToCheckout();
42 |
43 | expect(CheckoutPersonalInfoPage.waitForIsShown()).toEqual(
44 | true,
45 | 'Inventory screen is still not visible'
46 | );
47 | });
48 |
49 | it('should validate that a product can be removed from the cart', () => {
50 | setTestContext({
51 | user: LOGIN_USERS.STANDARD,
52 | path: PAGES.CART,
53 | products: [PRODUCTS.BACKPACK],
54 | });
55 |
56 | expect(CartSummaryPage.waitForIsShown()).toEqual(
57 | true,
58 | 'Cart summary screen is still not visible'
59 | );
60 |
61 | // Actual test starts here
62 | expect(AppHeaderPage.getCartAmount()).toEqual(
63 | '1',
64 | 'The amount of cart items is not equal to 1',
65 | );
66 | expect(CartSummaryPage.getSwagAmount()).toEqual(
67 | 1,
68 | 'The amount of items in the cart overview is not equal to 1',
69 | );
70 |
71 | CartSummaryPage.removeSwag(0);
72 |
73 | expect(AppHeaderPage.getCartAmount()).toEqual(
74 | '',
75 | 'The amount of cart items is not equal to nothing',
76 | );
77 |
78 | expect(CartSummaryPage.getSwagAmount()).toEqual(
79 | 0,
80 | 'The amount of items in the cart overview is not equal to 1',
81 | );
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/test/e2e/specs/checkout.complete.spec.js:
--------------------------------------------------------------------------------
1 | import CheckoutCompletePage from '../page-objects/CheckoutCompletePage';
2 | import {setTestContext} from '../helpers';
3 | import {LOGIN_USERS, PAGES} from "../configs/e2eConstants";
4 |
5 | describe('Checkout - Complete', () => {
6 | it('should be able to test loading of login page', () => {
7 | setTestContext({
8 | user: LOGIN_USERS.STANDARD,
9 | path: PAGES.CHECKOUT_COMPLETE,
10 | });
11 |
12 | expect(CheckoutCompletePage.waitForIsShown()).toEqual(
13 | true,
14 | 'Checkout complete page was not shown',
15 | );
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/test/e2e/specs/checkout.personal.info.spec.js:
--------------------------------------------------------------------------------
1 | import CartSummaryPage from '../page-objects/CartSummaryPage';
2 | import CheckoutPersonalInfoPage from '../page-objects/CheckoutPersonalInfoPage';
3 | import CheckoutSummaryPage from '../page-objects/CheckoutSummaryPage';
4 | import {LOGIN_USERS, PAGES, PERSONAL_INFO} from "../configs/e2eConstants";
5 | import {setTestContext} from '../helpers';
6 |
7 | describe('Checkout - Personal info', () => {
8 | beforeEach(() => {
9 | setTestContext({
10 | user: LOGIN_USERS.STANDARD,
11 | path: PAGES.CHECKOUT_PERSONAL_INFO,
12 | });
13 | CheckoutPersonalInfoPage.waitForIsShown();
14 | });
15 |
16 | it('should validate we get an error if we don not provide all personal information', () => {
17 | // It doesn't matter which error we check here, all error states should have been tested in a UT
18 | // Reason for selecting this one is that it triggers multiple fields and thus triggers the state
19 | CheckoutPersonalInfoPage.submitPersonalInfo(PERSONAL_INFO.NO_POSTAL_CODE);
20 |
21 | expect(CheckoutPersonalInfoPage.waitForIsShown()).toEqual(
22 | true,
23 | 'Error message is shown, this is not correct',
24 | );
25 |
26 | expect(CheckoutPersonalInfoPage.getErrorMessage()).toEqual(
27 | 'Error: Postal Code is required',
28 | 'Error message is shown, but not with the correct message',
29 | );
30 | });
31 |
32 | it('should validate that we can cancel the first checkout', () => {
33 | expect(CartSummaryPage.isDisplayed()).toEqual(
34 | false,
35 | 'Cart screen is already visible'
36 | );
37 |
38 | CheckoutPersonalInfoPage.cancelCheckout();
39 |
40 | expect(CartSummaryPage.waitForIsShown()).toEqual(
41 | true,
42 | 'Cart content screen is still not visible'
43 | );
44 | });
45 |
46 | it('should be able to continue the checkout', () => {
47 | CheckoutPersonalInfoPage.submitPersonalInfo(PERSONAL_INFO.STANDARD);
48 |
49 | expect(CheckoutSummaryPage.waitForIsShown()).toEqual(
50 | true,
51 | 'Checkout page two is still not visible'
52 | );
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/test/e2e/specs/checkout.summary.spec.js:
--------------------------------------------------------------------------------
1 | import SwagOverviewPage from '../page-objects/SwagOverviewPage';
2 | import CheckoutCompletePage from '../page-objects/CheckoutCompletePage';
3 | import CheckoutSummaryPage from '../page-objects/CheckoutSummaryPage';
4 | import {setTestContext} from '../helpers';
5 | import {LOGIN_USERS, PAGES, PRODUCTS} from "../configs/e2eConstants";
6 |
7 | describe('Checkout - Summary', () => {
8 | beforeEach(() => {
9 | setTestContext({
10 | user: LOGIN_USERS.STANDARD,
11 | path: PAGES.CHECKOUT_SUMMARY,
12 | products: [PRODUCTS.BACKPACK],
13 | });
14 | CheckoutSummaryPage.waitForIsShown();
15 | });
16 |
17 | it('should validate that we can continue shopping', () => {
18 | CheckoutSummaryPage.finishCheckout();
19 |
20 | expect(CheckoutCompletePage.waitForIsShown()).toEqual(
21 | true,
22 | 'The checkout complete page is still not shown',
23 | );
24 | });
25 |
26 | it('should validate that we can cancel checkout and go to the inventory page', () => {
27 | CheckoutSummaryPage.cancelCheckout();
28 |
29 | expect(SwagOverviewPage.waitForIsShown()).toEqual(
30 | true,
31 | 'Inventory screen is still not visible'
32 | );
33 | });
34 |
35 | it('should validate that we have 1 product in our checkout overview', () => {
36 | expect(CheckoutSummaryPage.getSwagAmount()).toEqual(
37 | 1,
38 | 'Not the correct items are shown in the checkout overview page'
39 | );
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/test/e2e/specs/login.spec.js:
--------------------------------------------------------------------------------
1 | import { LOGIN_USERS } from '../configs/e2eConstants';
2 | import LoginPage from '../page-objects/LoginPage';
3 | import SwagOverviewPage from '../page-objects/SwagOverviewPage';
4 |
5 | describe('LoginPage', () => {
6 | beforeEach(() => {
7 | // Load url to be able to clear cookies and storage
8 | browser.url('');
9 | // Clear the cookies and storage
10 | browser.deleteAllCookies();
11 | browser.execute('localStorage.clear();');
12 |
13 | // Load again in a fresh state
14 | browser.url('');
15 | LoginPage.waitForIsShown();
16 | });
17 |
18 | it('should be able to test loading of login page', () => {
19 | expect(LoginPage.waitForIsShown()).toEqual(
20 | true,
21 | 'LoginPage page was not shown',
22 | );
23 | });
24 |
25 | it('should be able to login with a standard user', () => {
26 | LoginPage.signIn(LOGIN_USERS.STANDARD);
27 |
28 | // Wait for the inventory screen and check it
29 | expect(SwagOverviewPage.waitForIsShown()).toEqual(
30 | true,
31 | 'Inventory List screen was not shown',
32 | );
33 | });
34 |
35 | it('should not be able to login with a locked user', () => {
36 | // It doesn't matter which error we check, all errors should be checked in a UT
37 | // With this UT we just check that A failure is triggered
38 | LoginPage.signIn(LOGIN_USERS.LOCKED);
39 |
40 | expect(LoginPage.isErrorMessageDisplayed()).toEqual(true, 'Error message is shown');
41 | expect(LoginPage.getErrorMessage()).toContain(
42 | 'Epic sadface: Sorry, this user has been locked out.',
43 | 'The error message is not as expected',
44 | );
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/test/e2e/specs/menu.spec.js:
--------------------------------------------------------------------------------
1 | import AppHeaderPage from '../page-objects/AppHeaderPage';
2 | import SwagOverviewPage from '../page-objects/SwagOverviewPage';
3 | import LoginPage from "../page-objects/LoginPage";
4 | import CartSummaryPage from "../page-objects/CartSummaryPage";
5 | import MenuPage from "../page-objects/MenuPage";
6 | import {setTestContext} from '../helpers';
7 | import {LOGIN_USERS, PAGES, PRODUCTS} from "../configs/e2eConstants";
8 |
9 | describe('Menu', () => {
10 | beforeEach(() => {
11 | setTestContext({
12 | user: LOGIN_USERS.STANDARD,
13 | path: PAGES.CART,
14 | products: [PRODUCTS.BACKPACK],
15 | });
16 | CartSummaryPage.waitForIsShown();
17 | MenuPage.open();
18 | });
19 |
20 | it('should be able to the swag items overview page', () => {
21 | MenuPage.openInventoryList();
22 |
23 | expect(SwagOverviewPage.waitForIsShown()).toEqual(
24 | true,
25 | 'Swag Items overview page was not shown',
26 | );
27 | });
28 |
29 | // Don't execute this test on the EU DC, the saucelabs.com url is not working there making this test fail
30 | if(!process.env.REGION) {
31 | it('should be able to open the about page', () => {
32 | MenuPage.openAboutPage();
33 |
34 | expect(CartSummaryPage.waitForIsShown(false)).toEqual(
35 | true,
36 | 'Swag Cart should not be shown anymore',
37 | );
38 | });
39 | }
40 |
41 | it('should be able to log out', () => {
42 | MenuPage.logout();
43 |
44 | expect(LoginPage.waitForIsShown()).toEqual(
45 | true,
46 | 'Login is not shown',
47 | );
48 | });
49 |
50 | it('should be able to clear the cart', () => {
51 | expect(AppHeaderPage.getCartAmount()).toEqual(
52 | '1',
53 | 'The amount of cart items is not equal to nothing',
54 | );
55 |
56 | MenuPage.restAppState();
57 |
58 | expect(AppHeaderPage.getCartAmount()).toEqual(
59 | '',
60 | 'The amount of cart items is not equal to nothing',
61 | );
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/test/e2e/specs/swag.item.details.spec.js:
--------------------------------------------------------------------------------
1 | import AppHeaderPage from '../page-objects/AppHeaderPage';
2 | import SwagDetailsPage from '../page-objects/SwagDetailsPage';
3 | import {setTestContext} from '../helpers';
4 | import {LOGIN_USERS, PAGES, PRODUCTS} from "../configs/e2eConstants";
5 |
6 | describe('Swag Item Details', () => {
7 | it('should validate that we can go back from the details to the inventory page', () => {
8 | // Need to start with the inventory url here to get the correct routing
9 | setTestContext({
10 | user: LOGIN_USERS.STANDARD,
11 | path: PAGES.SWAG_ITEMS,
12 | });
13 | browser.url(`${PAGES.SWAG_DETAILS}?id=${PRODUCTS.BACKPACK}`);
14 |
15 | // Actual test starts here
16 | SwagDetailsPage.waitForIsShown()
17 | SwagDetailsPage.goBack();
18 |
19 | expect(SwagDetailsPage.waitForIsShown(false)).toEqual(
20 | true,
21 | 'Inventory screen is still not visible'
22 | );
23 | });
24 |
25 | it('should validate that a product can be added to a cart', () => {
26 | // Need to start with the inventory url here to get the correct routing
27 | setTestContext({
28 | user: LOGIN_USERS.STANDARD,
29 | path: PAGES.SWAG_ITEMS,
30 | });
31 | browser.url(`${PAGES.SWAG_DETAILS}?id=${PRODUCTS.BACKPACK}`);
32 | SwagDetailsPage.waitForIsShown();
33 |
34 | // Actual test starts here
35 | expect(AppHeaderPage.getCartAmount()).toEqual(
36 | '',
37 | 'The amount of cart items is not equal to nothing',
38 | );
39 |
40 | SwagDetailsPage.addToCart();
41 |
42 | expect(AppHeaderPage.getCartAmount()).toEqual(
43 | '1',
44 | 'The amount of cart items is not equal to 1',
45 | );
46 | });
47 |
48 | it('should validate that a product can be removed from the cart', () => {
49 | // Need to start with the inventory url here to get the correct routing
50 | setTestContext({
51 | user: LOGIN_USERS.STANDARD,
52 | path: PAGES.SWAG_ITEMS,
53 | products: [PRODUCTS.BACKPACK],
54 | });
55 | browser.url(`${PAGES.SWAG_DETAILS}?id=${PRODUCTS.BACKPACK}`);
56 | SwagDetailsPage.waitForIsShown();
57 |
58 | // Actual test starts here
59 | expect(AppHeaderPage.getCartAmount()).toEqual(
60 | '1',
61 | 'The amount of cart items is not equal to 1',
62 | );
63 |
64 | SwagDetailsPage.removeFromCart();
65 |
66 | expect(AppHeaderPage.getCartAmount()).toEqual(
67 | '',
68 | 'The amount of cart items is not equal to nothing',
69 | );
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/test/visual/storybook/ci.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | projectRepo: 'saucelabs/sample-app-web',
3 | baseBranch: 'master',
4 | storybookConfigDir: '.storybook',
5 | apiKey: process.env.SCREENER_API_KEY,
6 | resolutions: [
7 | // These are all the breakpoints in the code
8 | '1200x900',
9 | '980x735',
10 | '900x680',
11 | '640x480',
12 | '480x640',
13 | {
14 | deviceName: 'iPhone X'
15 | },
16 | {
17 | deviceName: 'Galaxy S8'
18 | },
19 | ],
20 | failureExitCode: 0,
21 | };
22 |
--------------------------------------------------------------------------------
/test/visual/storybook/desktop.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | projectRepo: 'saucelabs/sample-app-web',
3 | baseBranch: 'master',
4 | storybookConfigDir: '.storybook',
5 | apiKey: process.env.SCREENER_API_KEY,
6 | resolutions: [
7 | // These are all the breakpoints in the code
8 | '1200x900',
9 | '980x735',
10 | '900x680',
11 | '640x480',
12 | '480x640',
13 | ],
14 | sauce: {
15 | username: process.env.SAUCE_USERNAME,
16 | accessKey: process.env.SAUCE_ACCESS_KEY,
17 | maxConcurrent: 30,
18 | },
19 | browsers: [
20 | {
21 | browserName: 'chrome',
22 | version: '89.0',
23 | },
24 | {
25 | browserName: 'firefox',
26 | version: '86.0',
27 | },
28 | // IE doesn't seem to work with Storybook 6+
29 | // {
30 | // browserName: 'internet explorer',
31 | // version: '11.285'
32 | // },
33 | {
34 | browserName: 'microsoftedge',
35 | version: '88.0',
36 | },
37 | {
38 | browserName: 'safari',
39 | version: '13.1',
40 | },
41 | ]
42 | };
43 |
--------------------------------------------------------------------------------
/test/visual/storybook/mobile.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | projectRepo: 'saucelabs/sample-app-web',
3 | baseBranch: 'master',
4 | storybookConfigDir: '.storybook',
5 | apiKey: process.env.SCREENER_API_KEY,
6 | resolutions: [
7 | // Galaxy S8
8 | '360x740',
9 | // iPhone X
10 | '375x812',
11 | ],
12 | sauce: {
13 | username: process.env.SAUCE_USERNAME,
14 | accessKey: process.env.SAUCE_ACCESS_KEY,
15 | maxConcurrent: 30,
16 | },
17 | browsers: [
18 | {
19 | browserName: 'chrome',
20 | version: '89.0',
21 | },
22 | {
23 | browserName: 'safari',
24 | version: '13.1',
25 | },
26 | ]
27 | };
28 |
--------------------------------------------------------------------------------