├── .babelrc ├── .editorconfig ├── .env.example ├── .eslintrc.js ├── .github └── workflows │ ├── merge-master.yml │ └── pull-request.yml ├── .gitignore ├── .husky ├── .gitignore ├── pre-commit └── pre-push ├── .prettierrc ├── README.md ├── assets └── README.md ├── components ├── Cart.unit.spec.js ├── Cart.vue ├── CartItem.unit.spec.js ├── CartItem.vue ├── ProductCard.unit.spec.js ├── ProductCard.vue ├── README.md ├── Search.unit.spec.js ├── Search.vue └── __snapshots__ │ └── ProductCard.unit.spec.js.snap ├── cypress.json ├── cypress ├── e2e │ └── Store.spec.js ├── support │ ├── commands.js │ ├── index.d.ts │ └── index.js └── tsconfig.json ├── jest.config.js ├── layouts ├── default.integration.spec.js ├── default.unit.spec.js └── default.vue ├── managers ├── CartManager.js └── CartManager.unit.spec.js ├── middleware └── README.md ├── miragejs ├── .gitignore ├── .prettierrc.json ├── README.md ├── factories │ ├── index.js │ ├── message.js │ ├── product.js │ ├── user.js │ └── utils.js ├── models │ └── index.js ├── routes │ └── index.js ├── seeds │ └── index.js └── server.js ├── modulo2.code-workspace ├── nuxt.config.js ├── package.json ├── pages ├── ProductList.integration.spec.js └── index.vue ├── plugins ├── cart.js └── miragejs.js ├── static ├── README.md ├── favicon.ico └── icon.png ├── store └── README.md ├── tailwind.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | "vue", 6 | [ 7 | "@babel/preset-env", 8 | { 9 | "targets": { 10 | "node": "current" 11 | } 12 | } 13 | ] 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | API_URL=http://localhost:5000 -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | parser: 'vue-eslint-parser', 8 | parserOptions: { 9 | ecmaVersion: 'latest', 10 | requireConfigFile: false, 11 | sourceType: 'module', 12 | }, 13 | extends: [ 14 | '@nuxtjs', 15 | 'prettier', 16 | 'plugin:prettier/recommended', 17 | 'plugin:nuxt/recommended', 18 | 'plugin:cypress/recommended', 19 | ], 20 | plugins: ['prettier', 'cypress'], 21 | rules: { 22 | 'vue/multi-word-component-names': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.github/workflows/merge-master.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest # 11 | 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Setup Node 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: 16.3.x 25 | 26 | - uses: actions/cache@v2 27 | id: yarn-cache 28 | with: 29 | path: '**/node_modules' 30 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 31 | restore-keys: | 32 | ${{ runner.os }}-yarn- 33 | 34 | - name: Install dependencies 35 | if: steps.yarn-cache.outputs.cache-hit != 'true' 36 | run: yarn --frozen-lockfile 37 | 38 | - name: Run Jest tests 39 | run: yarn test 40 | 41 | - name: Run Linter 42 | run: yarn lint 43 | 44 | - name: Build app 45 | run: yarn generate 46 | 47 | - name: Deploy to netlify 48 | uses: netlify/actions/cli@master 49 | env: 50 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 51 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 52 | with: 53 | args: deploy --dir=dist --prod 54 | secrets: '["NETLIFY_AUTH_TOKEN", "NETLIFY_SITE_ID"]' 55 | 56 | - name: Notifica no Telegram - sucesso 57 | uses: appleboy/telegram-action@master 58 | if: ${{ success() }} 59 | with: 60 | to: ${{ secrets.TELEGRAM_TO }} 61 | token: ${{ secrets.TELEGRAM_TOKEN }} 62 | message: | 63 | Após ${{ github.event_name }} o deploy foi feito na Netlify. 🚀 64 | 65 | - name: Notifica no Telegram - falha 66 | uses: appleboy/telegram-action@master 67 | if: ${{ failure() }} 68 | with: 69 | to: ${{ secrets.TELEGRAM_TO }} 70 | token: ${{ secrets.TELEGRAM_TOKEN }} 71 | message: | 72 | Após ${{ github.event_name }} o deploy na Netlify falhou. 🚨 73 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Setup Node 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 12.18.x 23 | 24 | - uses: actions/cache@v2 25 | id: yarn-cache 26 | with: 27 | path: '**/node_modules' 28 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 29 | restore-keys: | 30 | ${{ runner.os }}-yarn- 31 | 32 | - name: Install dependencies 33 | if: steps.yarn-cache.outputs.cache-hit != 'true' 34 | run: yarn --frozen-lockfile 35 | 36 | - name: Run Linter 37 | run: yarn lint 38 | 39 | - name: Comment with Test Coverage 40 | uses: dkershner6/jest-coverage-commenter-action@v1 41 | with: 42 | github_token: '${{ secrets.GITHUB_TOKEN }}' 43 | test_command: 'yarn test:coverage' 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | /logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # macOS 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | 5 | ./node_modules/.bin/pretty-quick 6 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn test:coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # watch-store 2 | 3 | ## Build Setup 4 | 5 | ```bash 6 | # install dependencies 7 | $ yarn install 8 | 9 | # serve with hot reload at localhost:3000 10 | $ yarn dev 11 | 12 | # build for production and launch server 13 | $ yarn build 14 | $ yarn start 15 | 16 | # generate static project 17 | $ yarn generate 18 | ``` 19 | 20 | For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org). 21 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # ASSETS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked). 8 | -------------------------------------------------------------------------------- /components/Cart.unit.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { mount } from '@vue/test-utils'; 3 | import Cart from '@/components/Cart'; 4 | import CartItem from '@/components/CartItem'; 5 | import { makeServer } from '@/miragejs/server'; 6 | import { CartManager } from '@/managers/CartManager'; 7 | 8 | describe('Cart', () => { 9 | let server; 10 | 11 | beforeEach(() => { 12 | server = makeServer({ environment: 'test' }); 13 | }); 14 | 15 | afterEach(() => { 16 | server.shutdown(); 17 | }); 18 | 19 | const mountCart = () => { 20 | const products = server.createList('product', 2); 21 | 22 | const cartManager = new CartManager(); 23 | 24 | const wrapper = mount(Cart, { 25 | propsData: { 26 | products, 27 | }, 28 | mocks: { 29 | $cart: cartManager, 30 | }, 31 | }); 32 | 33 | return { wrapper, products, cartManager }; 34 | }; 35 | 36 | it('should mount the component', () => { 37 | const { wrapper } = mountCart(); 38 | 39 | expect(wrapper.vm).toBeDefined(); 40 | }); 41 | 42 | it('should not display empty cart button when there are no products', () => { 43 | const { cartManager } = mountCart(); 44 | 45 | const wrapper = mount(Cart, { 46 | mocks: { 47 | $cart: cartManager, 48 | }, 49 | }); 50 | 51 | expect(wrapper.find('[data-testid="clear-cart-button"]').exists()).toBe( 52 | false 53 | ); 54 | }); 55 | 56 | it('should emit close event when button gets clicked', async () => { 57 | const { wrapper } = mountCart(); 58 | const button = wrapper.find('[data-testid="close-button"]'); 59 | 60 | await button.trigger('click'); 61 | 62 | expect(wrapper.emitted().close).toBeTruthy(); 63 | expect(wrapper.emitted().close).toHaveLength(1); 64 | }); 65 | 66 | it('should hide the cart when no prop isOpen is passed', () => { 67 | const { wrapper } = mountCart(); 68 | 69 | expect(wrapper.classes()).toContain('hidden'); 70 | }); 71 | 72 | it('should display the cart when prop isOpen is passed', async () => { 73 | const { wrapper } = mountCart(); 74 | 75 | await wrapper.setProps({ 76 | isOpen: true, 77 | }); 78 | 79 | expect(wrapper.classes()).not.toContain('hidden'); 80 | }); 81 | 82 | it('should display "Cart is empty" when there are no products', async () => { 83 | const { wrapper } = mountCart(); 84 | 85 | wrapper.setProps({ 86 | products: [], 87 | }); 88 | 89 | await Vue.nextTick(); 90 | 91 | expect(wrapper.text()).toContain('Cart is empty'); 92 | }); 93 | 94 | it('should display 2 instances of CartItem when 2 products are provided', () => { 95 | const { wrapper } = mountCart(); 96 | 97 | expect(wrapper.findAllComponents(CartItem)).toHaveLength(2); 98 | expect(wrapper.text()).not.toContain('Cart is empty'); 99 | }); 100 | 101 | it('should display a button to clear cart', () => { 102 | const { wrapper } = mountCart(); 103 | const button = wrapper.find('[data-testid="clear-cart-button"]'); 104 | 105 | expect(button.exists()).toBe(true); 106 | }); 107 | 108 | it('should call cart manager clearProducts() when button gets clicked', async () => { 109 | const { wrapper, cartManager } = mountCart(); 110 | const spy = jest.spyOn(cartManager, 'clearProducts'); 111 | await wrapper.find('[data-testid="clear-cart-button"]').trigger('click'); 112 | 113 | expect(spy).toHaveBeenCalledTimes(1); 114 | }); 115 | 116 | it('should display an input type e-mail when there are items in the cart', () => { 117 | const { wrapper } = mountCart(); 118 | const input = wrapper.find('input[type="email"]'); 119 | 120 | expect(input.exists()).toBe(true); 121 | }); 122 | 123 | it('should hide the input type e-mail when there are NO items in the cart', async () => { 124 | const { wrapper } = mountCart(); 125 | 126 | wrapper.setProps({ 127 | products: [], 128 | }); 129 | 130 | await Vue.nextTick(); 131 | 132 | const input = wrapper.find('input[type="email"]'); 133 | 134 | expect(input.exists()).toBe(false); 135 | }); 136 | 137 | it('should emit checkout event and send email when checkout button is clicked', async () => { 138 | const { wrapper } = mountCart(); 139 | const form = wrapper.find('[data-testid="checkout-form"]'); 140 | const input = wrapper.find('input[type="email"]'); 141 | const email = 'vedovelli@gmail.com'; 142 | 143 | input.setValue(email); 144 | 145 | await form.trigger('submit'); 146 | 147 | expect(wrapper.emitted().checkout).toBeTruthy(); 148 | expect(wrapper.emitted().checkout).toHaveLength(1); 149 | expect(wrapper.emitted().checkout[0][0]).toEqual({ 150 | email, 151 | }); 152 | }); 153 | 154 | it('should NOT emit checkout event when input email is empty', async () => { 155 | const { wrapper } = mountCart(); 156 | const button = wrapper.find('[data-testid="checkout-button"]'); 157 | 158 | await button.trigger('click'); 159 | 160 | expect(wrapper.emitted().checkout).toBeFalsy(); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /components/Cart.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 120 | -------------------------------------------------------------------------------- /components/CartItem.unit.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import CartItem from '@/components/CartItem'; 3 | import { makeServer } from '@/miragejs/server'; 4 | import { CartManager } from '@/managers/CartManager'; 5 | 6 | describe('CartItem', () => { 7 | let server; 8 | 9 | beforeEach(() => { 10 | server = makeServer({ environment: 'test' }); 11 | }); 12 | 13 | afterEach(() => { 14 | server.shutdown(); 15 | }); 16 | 17 | const mountCartItem = () => { 18 | const cartManager = new CartManager(); 19 | 20 | const product = server.create('product', { 21 | title: 'Lindo relogio', 22 | price: '22.33', 23 | }); 24 | 25 | const wrapper = mount(CartItem, { 26 | propsData: { 27 | product, 28 | }, 29 | mocks: { 30 | $cart: cartManager, 31 | }, 32 | }); 33 | 34 | return { wrapper, product, cartManager }; 35 | }; 36 | 37 | it('should mount the component', () => { 38 | const { wrapper } = mountCartItem(); 39 | expect(wrapper.vm).toBeDefined(); 40 | }); 41 | 42 | it('should display product info', () => { 43 | const { 44 | wrapper, 45 | product: { title, price }, 46 | } = mountCartItem(); 47 | const content = wrapper.text(); 48 | 49 | expect(content).toContain(title); 50 | expect(content).toContain(price); 51 | }); 52 | 53 | it('should display quantity 1 when product is first displayed', () => { 54 | const { wrapper } = mountCartItem(); 55 | const quantity = wrapper.find('[data-testid="quantity"]'); 56 | 57 | expect(quantity.text()).toContain('1'); 58 | }); 59 | 60 | it('should increase quantity when + button gets clicked', async () => { 61 | const { wrapper } = mountCartItem(); 62 | const quantity = wrapper.find('[data-testid="quantity"]'); 63 | const button = wrapper.find('[data-testid="+"]'); 64 | 65 | await button.trigger('click'); 66 | expect(quantity.text()).toContain('2'); 67 | await button.trigger('click'); 68 | expect(quantity.text()).toContain('3'); 69 | await button.trigger('click'); 70 | expect(quantity.text()).toContain('4'); 71 | }); 72 | 73 | it('should decrease quantity when - button gets clicked', async () => { 74 | const { wrapper } = mountCartItem(); 75 | const quantity = wrapper.find('[data-testid="quantity"]'); 76 | const button = wrapper.find('[data-testid="-"]'); 77 | 78 | await button.trigger('click'); 79 | expect(quantity.text()).toContain('0'); 80 | }); 81 | 82 | it('should not go below zero when button - is repeatedly clicked', async () => { 83 | const { wrapper } = mountCartItem(); 84 | const quantity = wrapper.find('[data-testid="quantity"]'); 85 | const button = wrapper.find('[data-testid="-"]'); 86 | 87 | await button.trigger('click'); 88 | await button.trigger('click'); 89 | 90 | expect(quantity.text()).toContain('0'); 91 | }); 92 | 93 | it('should display a button to remove item from cart', () => { 94 | const { wrapper } = mountCartItem(); 95 | const button = wrapper.find('[data-testid="remove-button"]'); 96 | 97 | expect(button.exists()).toBe(true); 98 | }); 99 | 100 | it('should call cart manager removeProduct() when button gets clicked', async () => { 101 | const { wrapper, cartManager, product } = mountCartItem(); 102 | const spy = jest.spyOn(cartManager, 'removeProduct'); 103 | await wrapper.find('[data-testid="remove-button"]').trigger('click'); 104 | 105 | expect(spy).toHaveBeenCalledTimes(1); 106 | expect(spy).toHaveBeenCalledWith(product.id); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /components/CartItem.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 87 | -------------------------------------------------------------------------------- /components/ProductCard.unit.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import ProductCard from '@/components/ProductCard'; 3 | import { makeServer } from '@/miragejs/server'; 4 | import { CartManager } from '@/managers/CartManager'; 5 | 6 | describe('ProductCard - unit', () => { 7 | let server; 8 | 9 | beforeEach(() => { 10 | server = makeServer({ environment: 'test' }); 11 | }); 12 | 13 | afterEach(() => { 14 | server.shutdown(); 15 | }); 16 | 17 | const mountProductCard = () => { 18 | const product = server.create('product', { 19 | title: 'Relógio bonito', 20 | price: '23.00', 21 | image: 22 | 'https://images.unsplash.com/photo-1532667449560-72a95c8d381b?ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80', 23 | }); 24 | 25 | const cartManager = new CartManager(); 26 | 27 | const wrapper = mount(ProductCard, { 28 | propsData: { 29 | product, 30 | }, 31 | mocks: { 32 | $cart: cartManager, 33 | }, 34 | }); 35 | 36 | return { 37 | wrapper, 38 | product, 39 | cartManager, 40 | }; 41 | }; 42 | 43 | it('should match snapshot', () => { 44 | const { wrapper } = mountProductCard(); 45 | 46 | expect(wrapper.element).toMatchSnapshot(); 47 | }); 48 | 49 | it('should mount the component', () => { 50 | const { wrapper } = mountProductCard(); 51 | 52 | expect(wrapper.vm).toBeDefined(); 53 | expect(wrapper.text()).toContain('Relógio bonito'); 54 | expect(wrapper.text()).toContain('$23.00'); 55 | }); 56 | 57 | it('should add item to cartState on button click', async () => { 58 | const { wrapper, cartManager, product } = mountProductCard(); 59 | const spy1 = jest.spyOn(cartManager, 'open'); 60 | const spy2 = jest.spyOn(cartManager, 'addProduct'); 61 | 62 | await wrapper.find('button').trigger('click'); 63 | 64 | expect(spy1).toHaveBeenCalledTimes(1); 65 | expect(spy2).toHaveBeenCalledTimes(1); 66 | expect(spy2).toHaveBeenCalledWith(product); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /components/ProductCard.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 51 | -------------------------------------------------------------------------------- /components/README.md: -------------------------------------------------------------------------------- 1 | # COMPONENTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | The components directory contains your Vue.js Components. 6 | 7 | _Nuxt.js doesn't supercharge these components._ 8 | -------------------------------------------------------------------------------- /components/Search.unit.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import Search from '@/components/Search'; 3 | 4 | describe('Search - unit', () => { 5 | it('should mount the component', () => { 6 | const wrapper = mount(Search); 7 | expect(wrapper.vm).toBeDefined(); 8 | }); 9 | 10 | it('should emit search event when form is submitted', async () => { 11 | const wrapper = mount(Search); 12 | const term = 'termo para busca'; 13 | 14 | await wrapper.find('input[type="search"]').setValue(term); 15 | await wrapper.find('form').trigger('submit'); 16 | 17 | expect(wrapper.emitted().doSearch).toBeTruthy(); 18 | expect(wrapper.emitted().doSearch.length).toBe(1); 19 | expect(wrapper.emitted().doSearch[0]).toEqual([{ term }]); 20 | }); 21 | 22 | it('should emit search event when search input is cleared', async () => { 23 | const wrapper = mount(Search); 24 | const term = 'termo para busca'; 25 | const input = wrapper.find('input[type="search"]'); 26 | 27 | await input.setValue(term); 28 | await input.setValue(''); 29 | 30 | expect(wrapper.emitted().doSearch).toBeTruthy(); 31 | expect(wrapper.emitted().doSearch.length).toBe(1); 32 | expect(wrapper.emitted().doSearch[0]).toEqual([{ term: '' }]); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /components/Search.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 49 | -------------------------------------------------------------------------------- /components/__snapshots__/ProductCard.unit.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ProductCard - unit should match snapshot 1`] = ` 4 |
7 |
11 | 29 |
30 | 31 |
34 |

37 | Relógio bonito 38 |

39 | 40 | 43 | $23.00 44 | 45 |
46 |
47 | `; 48 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "integrationFolder": "./cypress/e2e", 3 | "baseUrl": "http://localhost:3000" 4 | } 5 | -------------------------------------------------------------------------------- /cypress/e2e/Store.spec.js: -------------------------------------------------------------------------------- 1 | import { makeServer } from '../../miragejs/server'; 2 | 3 | context('Store', () => { 4 | let server; 5 | const g = cy.get; 6 | const gid = cy.getByTestId; 7 | 8 | beforeEach(() => { 9 | server = makeServer({ environment: 'test' }); 10 | }); 11 | 12 | afterEach(() => { 13 | server.shutdown(); 14 | }); 15 | 16 | it('should display the store', () => { 17 | cy.visit('/'); 18 | 19 | g('body').contains('Brand'); 20 | g('body').contains('Wrist Watch'); 21 | }); 22 | 23 | context('Store > Shopping Cart', () => { 24 | const quantity = 10; 25 | 26 | beforeEach(() => { 27 | server.createList('product', quantity); 28 | cy.visit('/'); 29 | }); 30 | 31 | it('should not display shopping cart when page first loads', () => { 32 | gid('shopping-cart').should('have.class', 'hidden'); 33 | }); 34 | 35 | it('should toggle shopping cart visibility when button is clicked', () => { 36 | gid('toggle-button').as('toggleButton'); 37 | g('@toggleButton').click(); 38 | gid('shopping-cart').should('not.have.class', 'hidden'); 39 | g('@toggleButton').click({ force: true }); 40 | gid('shopping-cart').should('have.class', 'hidden'); 41 | }); 42 | 43 | it('should not display "Clear cart" button when cart is empty', () => { 44 | gid('toggle-button').as('toggleButton'); 45 | g('@toggleButton').click(); 46 | gid('clear-cart-button').should('not.be.visible'); 47 | }); 48 | 49 | it('should display "Cart is empty" message when there are no products', () => { 50 | gid('toggle-button').as('toggleButton'); 51 | g('@toggleButton').click(); 52 | gid('shopping-cart').contains('Cart is empty'); 53 | }); 54 | 55 | it('should open shopping cart when a product is added', () => { 56 | gid('product-card').first().find('button').click(); 57 | gid('shopping-cart').should('not.have.class', 'hidden'); 58 | }); 59 | 60 | it('should add first product to the cart', () => { 61 | gid('product-card').first().find('button').click(); 62 | gid('cart-item').should('have.length', 1); 63 | }); 64 | 65 | it('should add 3 products to the cart', () => { 66 | cy.addToCart({ indexes: [1, 3, 5] }); 67 | 68 | gid('cart-item').should('have.length', 3); 69 | }); 70 | 71 | it('should add 1 product to the cart', () => { 72 | cy.addToCart({ index: 6 }); 73 | 74 | gid('cart-item').should('have.length', 1); 75 | }); 76 | 77 | it('should add all products to the cart', () => { 78 | cy.addToCart({ indexes: 'all' }); 79 | 80 | gid('cart-item').should('have.length', quantity); 81 | }); 82 | 83 | it('should display quantity 1 when product is added to cart', () => { 84 | cy.addToCart({ index: 1 }); 85 | gid('quantity').contains(1); 86 | }); 87 | 88 | it('should increase quantity when button + gets clicked', () => { 89 | cy.addToCart({ index: 1 }); 90 | gid('+').click(); 91 | gid('quantity').contains(2); 92 | gid('+').click(); 93 | gid('quantity').contains(3); 94 | }); 95 | 96 | it('should decrease quantity when button - gets clicked', () => { 97 | cy.addToCart({ index: 1 }); 98 | gid('+').click(); 99 | gid('+').click(); 100 | gid('quantity').contains(3); 101 | gid('-').click(); 102 | gid('quantity').contains(2); 103 | gid('-').click(); 104 | gid('quantity').contains(1); 105 | }); 106 | 107 | it('should not decrease below zero when button - gets clicked', () => { 108 | cy.addToCart({ index: 1 }); 109 | gid('-').click(); 110 | gid('-').click(); 111 | gid('quantity').contains(0); 112 | }); 113 | 114 | it('should remove a product from cart', () => { 115 | cy.addToCart({ index: 2 }); 116 | 117 | gid('cart-item').as('cartItems'); 118 | g('@cartItems').should('have.length', 1); 119 | g('@cartItems').first().find('[data-testid="remove-button"]').click(); 120 | g('@cartItems').should('have.length', 0); 121 | }); 122 | 123 | it('should clear cart when "Clear cart" button is clicked', () => { 124 | cy.addToCart({ indexes: [1, 2, 3] }); 125 | 126 | gid('cart-item').should('have.length', 3); 127 | gid('clear-cart-button').click(); 128 | gid('cart-item').should('have.length', 0); 129 | }); 130 | }); 131 | 132 | context('Store > Product List', () => { 133 | it('should display "0 Products" when no product is returned', () => { 134 | cy.visit('/'); 135 | gid('product-card').should('have.length', 0); 136 | g('body').contains('0 Products'); 137 | }); 138 | 139 | it('should display "1 Product" when 1 product is returned', () => { 140 | server.create('product'); 141 | 142 | cy.visit('/'); 143 | gid('product-card').should('have.length', 1); 144 | g('body').contains('1 Product'); 145 | }); 146 | 147 | it('should display "10 Products" when 10 products are returned', () => { 148 | server.createList('product', 10); 149 | 150 | cy.visit('/'); 151 | gid('product-card').should('have.length', 10); 152 | g('body').contains('10 Products'); 153 | }); 154 | }); 155 | 156 | context('Store > Search for Products', () => { 157 | it('should type in the search field', () => { 158 | cy.visit('/'); 159 | 160 | g('input[type="search"]') 161 | .type('Some text here') 162 | .should('have.value', 'Some text here'); 163 | }); 164 | 165 | it('should return 1 product when "Relógio bonito" is used as search term', () => { 166 | server.create('product', { 167 | title: 'Relógio bonito', 168 | }); 169 | server.createList('product', 10); 170 | 171 | cy.visit('/'); 172 | g('input[type="search"]').type('Relógio bonito'); 173 | gid('search-form').submit(); 174 | gid('product-card').should('have.length', 1); 175 | }); 176 | 177 | it('should not return any product', () => { 178 | server.createList('product', 10); 179 | 180 | cy.visit('/'); 181 | g('input[type="search"]').type('Relógio bonito'); 182 | gid('search-form').submit(); 183 | gid('product-card').should('have.length', 0); 184 | g('body').contains('0 Products'); 185 | }); 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | Cypress.Commands.add('getByTestId', (selector) => { 2 | return cy.get(`[data-testid="${selector}"]`); 3 | }); 4 | 5 | Cypress.Commands.add('addToCart', (mode) => { 6 | cy.getByTestId('product-card').as('productCards'); 7 | 8 | const click = (index) => 9 | cy.get('@productCards').eq(index).find('button').click({ force: true }); 10 | 11 | const addByIndex = () => { 12 | click(mode.index); 13 | }; 14 | 15 | const addByIndexes = () => { 16 | for (const index of mode.indexes) { 17 | click(index); 18 | } 19 | }; 20 | 21 | const addAll = () => { 22 | cy.get('@productCards').then(($elements) => { 23 | let i = 0; 24 | while (i < $elements.length) { 25 | click(i); 26 | i++; 27 | } 28 | }); 29 | }; 30 | 31 | if (mode.index) { 32 | addByIndex(); 33 | } else if (!!mode.indexes && Array.isArray(mode.indexes)) { 34 | addByIndexes(); 35 | } else if (!!mode.indexes && mode.indexes === 'all') { 36 | addAll(); 37 | } else { 38 | throw new Error( 39 | 'Please provide a valid input for cy.addToCart()\r\nPossible values are Array, number or "all"' 40 | ); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /cypress/support/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Cypress { 2 | interface Chainable { 3 | getByTestId(): Chainable; 4 | addToCart(): Chainable; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | import './commands'; 2 | 3 | Cypress.on('window:before:load', (win) => { 4 | win.handleFromCypress = function (request) { 5 | return fetch(request.url, { 6 | method: request.method, 7 | headers: request.requestHeaders, 8 | body: request.requestBody, 9 | }).then((res) => { 10 | const content = res.headers 11 | .get('content-type') 12 | .includes('application/json') 13 | ? res.json() 14 | : res.text(); 15 | return new Promise((resolve) => { 16 | content.then((body) => resolve([res.status, res.headers, body])); 17 | }); 18 | }); 19 | }; 20 | }); 21 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "baseUrl": "../node_modules", 5 | "types": ["cypress", "./support"] 6 | }, 7 | "include": ["**/*.*"] 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | moduleNameMapper: { 4 | '^@/(.*)$': '/$1', 5 | '^~/(.*)$': '/$1', 6 | '^vue$': 'vue/dist/vue.common.js', 7 | }, 8 | testPathIgnorePatterns: ['/cypress/'], 9 | moduleFileExtensions: ['js', 'vue', 'json'], 10 | transform: { 11 | '^.+\\.js$': 'babel-jest', 12 | '.*\\.(vue)$': 'vue-jest', 13 | }, 14 | collectCoverage: false, 15 | collectCoverageFrom: [ 16 | '/components/**/*.vue', 17 | '/pages/**/*.vue', 18 | '/layouts/**/*.vue', 19 | '/managers/**/*.js', 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /layouts/default.integration.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import flushPromises from 'flush-promises'; 3 | import axios from 'axios'; 4 | import DefaultLayout from '@/layouts/default'; 5 | import Cart from '@/components/Cart'; 6 | import { CartManager } from '@/managers/CartManager'; 7 | import { makeServer } from '@/miragejs/server'; 8 | 9 | jest.mock('axios', () => ({ 10 | post: jest.fn(() => ({ order: { id: 111 } })), 11 | setHeader: jest.fn(), 12 | })); 13 | 14 | const cartManager = new CartManager(); 15 | 16 | function mountComponent( 17 | providedCartManager = cartManager, 18 | providedAxios = axios 19 | ) { 20 | return mount(DefaultLayout, { 21 | mocks: { 22 | $cart: providedCartManager, 23 | $axios: providedAxios, 24 | }, 25 | stubs: { 26 | Nuxt: true, 27 | }, 28 | }); 29 | } 30 | 31 | describe('Default Layout', () => { 32 | let server; 33 | let products; 34 | 35 | beforeEach(() => { 36 | server = makeServer({ environment: 'test' }); 37 | products = server.createList('product', 2); 38 | }); 39 | 40 | afterEach(() => { 41 | server.shutdown(); 42 | jest.clearAllMocks(); 43 | }); 44 | 45 | it('should set email header on Axios when Cart emmits checkout event', async () => { 46 | const wrapper = mountComponent(); 47 | const cartComponent = wrapper.findComponent(Cart).vm; 48 | const email = 'vedovelli@gmail.com'; 49 | 50 | await cartComponent.$emit('checkout', { email }); 51 | 52 | expect(axios.setHeader).toHaveBeenCalledTimes(1); 53 | expect(axios.setHeader).toHaveBeenCalledWith('email', email); 54 | }); 55 | 56 | it('should call Axios.post with the right endpoint and send products', async () => { 57 | jest.spyOn(cartManager, 'getState').mockReturnValue({ 58 | items: products, 59 | }); 60 | 61 | jest.spyOn(cartManager, 'clearProducts'); 62 | 63 | const wrapper = mountComponent(cartManager); 64 | const cartComponent = wrapper.findComponent(Cart).vm; 65 | const email = 'vedovelli@gmail.com'; 66 | 67 | await cartComponent.$emit('checkout', { email }); 68 | 69 | expect(axios.post).toHaveBeenCalledTimes(1); 70 | expect(axios.post).toHaveBeenCalledWith('/api/order', { products }); 71 | }); 72 | 73 | it('should call cartManager.clearProducts on success', async () => { 74 | jest.spyOn(cartManager, 'getState').mockReturnValue({ 75 | items: products, 76 | }); 77 | 78 | jest.spyOn(cartManager, 'clearProducts'); 79 | 80 | const wrapper = mountComponent(cartManager); 81 | const cartComponent = wrapper.findComponent(Cart).vm; 82 | const email = 'vedovelli@gmail.com'; 83 | 84 | await cartComponent.$emit('checkout', { email }); 85 | 86 | expect(cartManager.clearProducts).toHaveBeenCalledTimes(1); 87 | }); 88 | 89 | it('should display error notice when Axios.post fails', async () => { 90 | jest.spyOn(cartManager, 'getState').mockReturnValue({ 91 | items: products, 92 | }); 93 | 94 | jest.spyOn(axios, 'post').mockRejectedValue({}); 95 | 96 | const wrapper = mountComponent(cartManager, axios); 97 | const cartComponent = wrapper.findComponent(Cart).vm; 98 | 99 | await cartComponent.$emit('checkout', { email: 'vedovelli@gmail.com' }); 100 | await flushPromises(); 101 | 102 | expect(wrapper.find('[data-testid="error-message"]').exists()).toBe(true); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /layouts/default.unit.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import DefaultLayout from '@/layouts/default'; 3 | import Cart from '@/components/Cart'; 4 | import { CartManager } from '@/managers/CartManager'; 5 | 6 | describe('Default Layout', () => { 7 | const mountLayout = () => { 8 | const wrapper = mount(DefaultLayout, { 9 | mocks: { 10 | $cart: new CartManager(), 11 | }, 12 | stubs: { 13 | Nuxt: true, 14 | }, 15 | }); 16 | 17 | return { wrapper }; 18 | }; 19 | 20 | it('should mount Cart', () => { 21 | const { wrapper } = mountLayout(); 22 | expect(wrapper.findComponent(Cart).exists()).toBe(true); 23 | }); 24 | 25 | it('should toggle Cart visibility', async () => { 26 | const { wrapper } = mountLayout(); 27 | const button = wrapper.find('[data-testid="toggle-button"]'); 28 | 29 | await button.trigger('click'); 30 | expect(wrapper.vm.isCartOpen).toBe(true); 31 | await button.trigger('click'); 32 | expect(wrapper.vm.isCartOpen).toBe(false); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 121 | 122 | 164 | -------------------------------------------------------------------------------- /managers/CartManager.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | export default { 4 | install: (Vue) => { 5 | /* istanbul ignore next */ 6 | Vue.prototype.$cart = new CartManager(); 7 | }, 8 | }; 9 | 10 | const initialState = { 11 | open: false, 12 | items: [], 13 | }; 14 | 15 | export class CartManager { 16 | state; 17 | 18 | constructor() { 19 | this.state = Vue.observable(initialState); 20 | } 21 | 22 | getState() { 23 | return this.state; 24 | } 25 | 26 | open() { 27 | this.state.open = true; 28 | 29 | return this.getState(); 30 | } 31 | 32 | close() { 33 | this.state.open = false; 34 | 35 | return this.getState(); 36 | } 37 | 38 | productIsInTheCart(product) { 39 | return !!this.state.items.find(({ id }) => id === product.id); 40 | } 41 | 42 | hasProducts() { 43 | return this.state.items.length > 0; 44 | } 45 | 46 | addProduct(product) { 47 | if (!this.productIsInTheCart(product)) { 48 | this.state.items.push(product); 49 | } 50 | 51 | return this.getState(); 52 | } 53 | 54 | removeProduct(productId) { 55 | this.state.items = [ 56 | ...this.state.items.filter((product) => product.id !== productId), 57 | ]; 58 | 59 | return this.getState(); 60 | } 61 | 62 | clearProducts() { 63 | this.state.items = []; 64 | 65 | return this.getState(); 66 | } 67 | 68 | clearCart() { 69 | this.clearProducts(); 70 | this.close(); 71 | 72 | return this.getState(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /managers/CartManager.unit.spec.js: -------------------------------------------------------------------------------- 1 | import { CartManager } from '@/managers/CartManager'; 2 | import { makeServer } from '@/miragejs/server'; 3 | 4 | describe('CartManager', () => { 5 | let server; 6 | let manager; 7 | 8 | beforeEach(() => { 9 | manager = new CartManager(); 10 | server = makeServer({ environment: 'test' }); 11 | }); 12 | 13 | afterEach(() => { 14 | server.shutdown(); 15 | }); 16 | 17 | it('should return the state', () => { 18 | const product = server.create('product'); 19 | manager.open(); 20 | manager.addProduct(product); 21 | const state = manager.getState(); 22 | 23 | expect(state).toEqual({ 24 | items: [product], 25 | open: true, 26 | }); 27 | }); 28 | 29 | it('should set cart to open', () => { 30 | const state = manager.open(); 31 | 32 | expect(state.open).toBe(true); 33 | }); 34 | 35 | it('should set cart to closed', () => { 36 | const state = manager.close(); 37 | 38 | expect(state.open).toBe(false); 39 | }); 40 | 41 | it('should add product to the cart only once', () => { 42 | const product = server.create('product'); 43 | manager.addProduct(product); 44 | const state = manager.addProduct(product); 45 | 46 | expect(state.items).toHaveLength(1); 47 | }); 48 | 49 | it('should remove product from the cart', () => { 50 | const product = server.create('product'); 51 | const state = manager.removeProduct(product.id); 52 | 53 | expect(state.items).toHaveLength(0); 54 | }); 55 | 56 | it('should clear products', () => { 57 | const product1 = server.create('product'); 58 | const product2 = server.create('product'); 59 | 60 | manager.addProduct(product1); 61 | manager.addProduct(product2); 62 | 63 | const state = manager.clearProducts(); 64 | 65 | expect(state.items).toHaveLength(0); 66 | }); 67 | 68 | it('should clear cart', () => { 69 | const product1 = server.create('product'); 70 | const product2 = server.create('product'); 71 | 72 | manager.addProduct(product1); 73 | manager.addProduct(product2); 74 | manager.open(); 75 | 76 | const state = manager.clearCart(); 77 | 78 | expect(state.items).toHaveLength(0); 79 | expect(state.open).toBe(false); 80 | }); 81 | 82 | it('should return true if cart is not empty', () => { 83 | const product1 = server.create('product'); 84 | const product2 = server.create('product'); 85 | 86 | manager.addProduct(product1); 87 | manager.addProduct(product2); 88 | 89 | expect(manager.hasProducts()).toBe(true); 90 | }); 91 | 92 | it('should return true if product is already in the cart', () => { 93 | const product = server.create('product'); 94 | manager.addProduct(product); 95 | 96 | expect(manager.productIsInTheCart(product)).toBe(true); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /middleware/README.md: -------------------------------------------------------------------------------- 1 | # MIDDLEWARE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your application middleware. 6 | Middleware let you define custom functions that can be run before rendering either a page or a group of pages. 7 | 8 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware). 9 | -------------------------------------------------------------------------------- /miragejs/.gitignore: -------------------------------------------------------------------------------- 1 | TODO.txt 2 | -------------------------------------------------------------------------------- /miragejs/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /miragejs/README.md: -------------------------------------------------------------------------------- 1 | # Mirage JS Starter Kit 2 | 3 | [From Mirage website](https://miragejs.com/): 4 | 5 | > Mirage JS is an API mocking library that lets you build, test and share a complete working JavaScript application without having to rely on any backend services. 6 | 7 | Their documentation is straight to the point but it only helps you setting up a simple server without any suggestion of folder organization. This starter kit keeps things separate into their own folders, introducing separation of concerns and making it easier to reason about all the parts. 8 | 9 | --- 10 | 11 | ## How to use it 12 | 13 | ### 1. Having started a React or Vue.js project, copy all the files to the src folder: 14 | 15 | ``` 16 | cd src && npx degit vedovelli/miragejs-starter-kit miragejs 17 | ``` 18 | 19 | [What is **degit**?](https://github.com/Rich-Harris/degit#readme) 20 | 21 | This will create the `miragejs` folder inside `src`. You can use any folder name you find best. 22 | 23 | **IMPORTANT**: do NOT omit the folder name in the degit command otherwise all starter kit's folders will be created inside your src folder, messing up your project organization. 24 | 25 | ### 2. Make sure all dependencies are installed: 26 | 27 | ``` 28 | npm install --save-dev miragejs faker 29 | ``` 30 | 31 | ### 3. Make your project aware of Mirage JS: 32 | 33 | **React** 34 | 35 | ``` 36 | import React from "react"; 37 | import ReactDOM from "react-dom"; 38 | import App from "./App"; 39 | 40 | if (process.env.NODE_ENV === "development") { 41 | // You can't use import in a conditional so we're using require() so no 42 | // Mirage JS code will ever reach your production build. 43 | require('./miragejs/server').makeServer(); 44 | } 45 | 46 | ReactDOM.render(, document.getElementById("root")); 47 | ``` 48 | 49 | **Vue.js** 50 | 51 | ``` 52 | import Vue from "vue"; 53 | import App from "./App.vue"; 54 | import router from "./router"; 55 | import store from "./store"; 56 | 57 | if (process.env.NODE_ENV === "development") { 58 | // You can't use import in a conditional so we're using require() so no 59 | // Mirage JS code will ever reach your production build. 60 | require('./miragejs/server').makeServer(); 61 | } 62 | 63 | Vue.config.productionTip = false; 64 | 65 | new Vue({ 66 | router, 67 | store, 68 | render: h => h(App) 69 | }).$mount("#app"); 70 | ``` 71 | 72 | ### 4. Calling the API 73 | 74 | Inside any component of your application and using your favorite HTTP request's library make a call to `api/users`. You will receive back a list of 10 objects with the following shape: 75 | 76 | ``` 77 | {id: "1", name: "Some name", mobile: "+1 555 525636"} 78 | ``` 79 | 80 | Additionally if you call `api/products` you'll receive back a list of 3 objects with the following shape: 81 | 82 | ``` 83 | { 84 | id: "1", 85 | name: "Javascript coffee mug", 86 | description: "We are nothing without coffee", 87 | price: 3.5 88 | }, 89 | ``` 90 | 91 | Those routes operate with a `resource` meaning they accept all HTTP verbs involved in a CRUD operation. 92 | 93 | To get all the messages associated with an user make a call to `api/messages?userId=`. 94 | 95 | ### 5. Adding your own content 96 | 97 | Lastly tweak `factories`, `fixtures` and `seeds` to accommodate your own needs. 98 | 99 | --- 100 | 101 | ## Folder structure 102 | 103 | - `factories`: contains the blueprints for the content to be generated by mirage. It uses [faker](https://github.com/Marak/Faker.js#readme) to generate random but credible content; 104 | - `fixtures`: contains predefined data to be served by Mirage. Use it if you have content to be served from JSON files; 105 | - `models`: contains all models for the database entities. Every time you create a new resource or a new fixture, it is necessary to create a new model; 106 | - `routes`: contains the routes for your API; 107 | - `seeds`: contains the seeds for the data. They determine how many records should be generated and stored in the database. For that purpose they use the Factories. 108 | 109 | ## Example projects 110 | 111 | To help you see it in action there are 2 example projects. For each one of them all instructions above were followed and the data is displayed in the interface. 112 | 113 | **React**: [https://github.com/vedovelli/miragejs-starter-kit-react-example](https://github.com/vedovelli/miragejs-starter-kit-react-example) 114 | 115 | **Vue.js**: [https://github.com/vedovelli/miragejs-starter-kit-vuejs-example](https://github.com/vedovelli/miragejs-starter-kit-vuejs-example) 116 | -------------------------------------------------------------------------------- /miragejs/factories/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Mirage JS guide on Factories: https://miragejs.com/docs/data-layer/factories 3 | */ 4 | 5 | import user from './user'; 6 | import message from './message'; 7 | import product from './product'; 8 | 9 | /* 10 | * factories are contained in a single object, that's why we 11 | * destructure what's coming from users and the same should 12 | * be done for all future factories 13 | */ 14 | export default { 15 | ...user, 16 | ...message, 17 | ...product, 18 | }; 19 | -------------------------------------------------------------------------------- /miragejs/factories/message.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Mirage JS guide on Factories: https://miragejs.com/docs/data-layer/factories 3 | */ 4 | import { Factory } from 'miragejs'; 5 | 6 | /* 7 | * Faker Github repository: https://github.com/Marak/Faker.js#readme 8 | */ 9 | import faker from 'faker'; 10 | 11 | export default { 12 | message: Factory.extend({ 13 | content() { 14 | return faker.fake('{{lorem.paragraph}}'); 15 | }, 16 | date() { 17 | const date = new Date(faker.fake('{{date.past}}')); 18 | return date.toLocaleDateString(); 19 | }, 20 | }), 21 | }; 22 | -------------------------------------------------------------------------------- /miragejs/factories/product.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Mirage JS guide on Factories: https://miragejs.com/docs/data-layer/factories 3 | */ 4 | import { Factory } from 'miragejs'; 5 | 6 | /* 7 | * Faker Github repository: https://github.com/Marak/Faker.js#readme 8 | */ 9 | import faker from 'faker'; 10 | 11 | import { randomNumber } from './utils'; 12 | 13 | const images = [ 14 | 'https://images.unsplash.com/photo-1495856458515-0637185db551?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80', 15 | 'https://images.unsplash.com/photo-1524592094714-0f0654e20314?ixlib=rb-1.2.1&auto=format&fit=crop&w=689&q=80', 16 | 'https://images.unsplash.com/photo-1532667449560-72a95c8d381b?ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80', 17 | 'https://images.unsplash.com/photo-1542496658-e33a6d0d50f6?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80', 18 | 'https://images.unsplash.com/photo-1434056886845-dac89ffe9b56?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80', 19 | 'https://images.unsplash.com/photo-1526045431048-f857369baa09?ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80', 20 | 'https://images.unsplash.com/photo-1495857000853-fe46c8aefc30?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80', 21 | 'https://images.unsplash.com/photo-1444881421460-d838c3b98f95?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=889&q=80', 22 | 'https://images.unsplash.com/photo-1495856458515-0637185db551?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80', 23 | 'https://images.unsplash.com/photo-1524592094714-0f0654e20314?ixlib=rb-1.2.1&auto=format&fit=crop&w=689&q=80', 24 | 'https://images.unsplash.com/photo-1532667449560-72a95c8d381b?ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80', 25 | 'https://images.unsplash.com/photo-1542496658-e33a6d0d50f6?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80', 26 | 'https://images.unsplash.com/photo-1434056886845-dac89ffe9b56?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80', 27 | 'https://images.unsplash.com/photo-1526045431048-f857369baa09?ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80', 28 | 'https://images.unsplash.com/photo-1495857000853-fe46c8aefc30?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80', 29 | 'https://images.unsplash.com/photo-1444881421460-d838c3b98f95?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=889&q=80', 30 | ]; 31 | 32 | export default { 33 | product: Factory.extend({ 34 | title() { 35 | return faker.fake('{{lorem.words}}'); 36 | }, 37 | price() { 38 | return faker.fake('{{commerce.price}}'); 39 | }, 40 | image() { 41 | return images[randomNumber(10)]; 42 | }, 43 | }), 44 | }; 45 | -------------------------------------------------------------------------------- /miragejs/factories/user.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Mirage JS guide on Factories: https://miragejs.com/docs/data-layer/factories 3 | */ 4 | import { Factory } from 'miragejs'; 5 | 6 | /* 7 | * Faker Github repository: https://github.com/Marak/Faker.js#readme 8 | */ 9 | import faker from 'faker'; 10 | import { randomNumber } from './utils'; 11 | 12 | export default { 13 | user: Factory.extend({ 14 | name() { 15 | return faker.fake('{{name.findName}}'); 16 | }, 17 | mobile() { 18 | return faker.fake('{{phone.phoneNumber}}'); 19 | }, 20 | afterCreate(user, server) { 21 | const messages = server.createList('message', randomNumber(10), { user }); 22 | 23 | user.update({ messages }); 24 | }, 25 | }), 26 | }; 27 | -------------------------------------------------------------------------------- /miragejs/factories/utils.js: -------------------------------------------------------------------------------- 1 | export const randomNumber = (quantity) => Math.floor(Math.random() * quantity) + 1; 2 | -------------------------------------------------------------------------------- /miragejs/models/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Mirage JS guide on Models: https://miragejs.com/docs/data-layer/models 3 | */ 4 | 5 | import { Model, hasMany, belongsTo } from 'miragejs'; 6 | 7 | /* 8 | * Everytime you create a new resource you have 9 | * to create a new Model and add it here. It is 10 | * true for Factories and for Fixtures. 11 | * 12 | * Mirage JS guide on Relationships: https://miragejs.com/docs/main-concepts/relationships/ 13 | */ 14 | export default { 15 | user: Model.extend({ 16 | messages: hasMany(), 17 | }), 18 | messages: Model.extend({ 19 | user: belongsTo(), 20 | }), 21 | product: Model, 22 | }; 23 | -------------------------------------------------------------------------------- /miragejs/routes/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Mirage JS guide on Routes: https://miragejs.com/docs/route-handlers/functions 3 | */ 4 | 5 | // import { Response } from 'miragejs'; 6 | 7 | export default function routes() { 8 | this.namespace = 'api'; 9 | // this.timing = 5000; 10 | 11 | /* 12 | * A resource comprises all operations for a CRUD 13 | * operation. .get(), .post(), .put() and delete(). 14 | * Mirage JS guide on Resource: https://miragejs.com/docs/route-handlers/shorthands#resource-helper 15 | */ 16 | this.resource('users'); 17 | this.resource('products'); 18 | // this.get('products', () => { 19 | // return new Response(500, {}, 'O server morreu!'); 20 | // }); 21 | 22 | /* 23 | * From your component use fetch('api/messages?userId=') 24 | * replacing with a real ID 25 | */ 26 | this.get('messages', (schema, request) => { 27 | const { 28 | queryParams: { userId }, 29 | } = request; 30 | 31 | return schema.messages.where({ userId }); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /miragejs/seeds/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Mirage JS guide on Seeds: https://miragejs.com/docs/data-layer/factories#in-development 3 | */ 4 | 5 | const usersSeeder = (server) => { 6 | /* 7 | * This will create in the in memory DB 10 objects 8 | * of the Factory `user`. Moreover it creates a 9 | * random number of messages and assign to each 10 | * and every user, making use of relationships. 11 | */ 12 | server.createList('user', 10); 13 | }; 14 | 15 | const productsSeeder = (server) => { 16 | server.createList('product', 24); 17 | }; 18 | 19 | export default function seeds(server) { 20 | usersSeeder(server); 21 | productsSeeder(server); 22 | } 23 | -------------------------------------------------------------------------------- /miragejs/server.js: -------------------------------------------------------------------------------- 1 | import { Server } from 'miragejs'; 2 | import factories from './factories'; 3 | import routes from './routes'; 4 | import models from './models'; 5 | import seeds from './seeds'; 6 | 7 | const config = (environment) => { 8 | const config = { 9 | environment, 10 | factories, 11 | models, 12 | routes, 13 | seeds, 14 | }; 15 | 16 | if (process.env.NODE_ENV !== 'development' && process.env.USE_API) { 17 | config.urlPrefix = 'http://localhost:5000'; 18 | } 19 | 20 | return config; 21 | }; 22 | 23 | export function makeServer({ environment = 'development' } = {}) { 24 | return new Server(config(environment)); 25 | } 26 | -------------------------------------------------------------------------------- /modulo2.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "workbench.colorCustomizations": { 9 | "titleBar.activeBackground": "#f4bb00", 10 | "titleBar.activeForeground": "#000000", 11 | "titleBar.inactiveBackground": "#f4bb00", 12 | "activityBar.background": "#f4bb00", 13 | "activityBar.foreground": "#000000", 14 | "activityBarBadge.background": "#0072f4", 15 | "activityBarBadge.foreground": "#ffffff" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | target: 'static', 3 | ssr: false, 4 | // Global page headers (https://go.nuxtjs.dev/config-head) 5 | head: { 6 | title: 'watch-store', 7 | meta: [ 8 | { charset: 'utf-8' }, 9 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 10 | { hid: 'description', name: 'description', content: '' }, 11 | ], 12 | link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], 13 | }, 14 | 15 | // Global CSS (https://go.nuxtjs.dev/config-css) 16 | css: [], 17 | 18 | // Plugins to run before rendering page (https://go.nuxtjs.dev/config-plugins) 19 | plugins: ['@/plugins/miragejs', '@/plugins/cart'], 20 | 21 | // Auto import components (https://go.nuxtjs.dev/config-components) 22 | components: false, 23 | 24 | // Modules for dev and build (recommended) (https://go.nuxtjs.dev/config-modules) 25 | buildModules: ['@nuxtjs/eslint-module', '@nuxtjs/tailwindcss'], 26 | 27 | // Modules (https://go.nuxtjs.dev/config-modules) 28 | modules: [ 29 | // https://go.nuxtjs.dev/axios 30 | '@nuxtjs/axios', 31 | // https://go.nuxtjs.dev/pwa 32 | '@nuxtjs/pwa', 33 | ], 34 | 35 | env: { 36 | USE_API: !!process.env.USE_API, 37 | }, 38 | 39 | // Axios module configuration (https://go.nuxtjs.dev/config-axios) 40 | axios: {}, 41 | 42 | // Build Configuration (https://go.nuxtjs.dev/config-build) 43 | build: {}, 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "watch-store", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxt", 7 | "dev:api": "USE_API=true nuxt", 8 | "build": "nuxt build", 9 | "start": "nuxt start", 10 | "generate": "nuxt generate", 11 | "lint:js": "eslint --ext .js,.vue --ignore-path .gitignore .", 12 | "lint": "yarn lint:js", 13 | "cypress:open": "cypress open", 14 | "cypress:run": "cypress run", 15 | "test": "jest", 16 | "test:watch": "jest --watchAll", 17 | "test:coverage": "jest --coverage", 18 | "test:e2e": "start-server-and-test dev 3000 cypress:open", 19 | "test:e2e:headless": "start-server-and-test dev 3000 cypress:run", 20 | "prepare": "husky install" 21 | }, 22 | "lint-staged": { 23 | "*.{js,vue}": "eslint" 24 | }, 25 | "dependencies": { 26 | "@nuxtjs/axios": "^5.13.6", 27 | "@nuxtjs/pwa": "^3.3.5", 28 | "core-js": "^3.20.3", 29 | "nuxt": "^2.15.8" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.16.7", 33 | "@babel/eslint-parser": "^7.16.5", 34 | "@nuxtjs/eslint-config": "^8.0.0", 35 | "@nuxtjs/eslint-module": "^3.0.2", 36 | "@nuxtjs/tailwindcss": "^4.2.1", 37 | "@types/jest": "^27.4.0", 38 | "@vue/test-utils": "^1.3.0", 39 | "babel-core": "^7.0.0-bridge.0", 40 | "babel-jest": "^27.4.6", 41 | "babel-preset-vue": "^2.0.2", 42 | "cypress": "^9.2.1", 43 | "eslint": "^8.7.0", 44 | "eslint-config-prettier": "^8.3.0", 45 | "eslint-plugin-cypress": "^2.12.1", 46 | "eslint-plugin-nuxt": "^3.1.0", 47 | "eslint-plugin-prettier": "^4.0.0", 48 | "faker": "^5.5.3", 49 | "flush-promises": "^1.0.2", 50 | "husky": "^7.0.4", 51 | "jest": "^27.4.7", 52 | "jsdom": "^19.0.0", 53 | "lint-staged": "^12.1.7", 54 | "miragejs": "^0.1.43", 55 | "prettier": "^2.5.1", 56 | "pretty-quick": "^3.1.3", 57 | "regenerator-runtime": "^0.13.9", 58 | "start-server-and-test": "^1.14.0", 59 | "vue-eslint-parser": "^8.0.1", 60 | "vue-jest": "^3.0.4" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pages/ProductList.integration.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import Vue from 'vue'; 3 | import axios from 'axios'; 4 | import ProductList from '.'; 5 | import ProductCard from '@/components/ProductCard'; 6 | import Search from '@/components/Search'; 7 | import { makeServer } from '@/miragejs/server'; 8 | 9 | /** 10 | * O Jest substitui o método get por sua própria 11 | * função, a qual ele tem total conhecimento. 12 | * Isso possibilita fazer assertions tais como 13 | * quantas vezes o método foi executado e com quais 14 | * parametros. 15 | */ 16 | jest.mock('axios', () => ({ 17 | get: jest.fn(), 18 | })); 19 | 20 | describe('ProductList - integration', () => { 21 | let server; 22 | 23 | /** 24 | * Este método é executado antes de cada teste 25 | */ 26 | beforeEach(() => { 27 | /** 28 | * Cria uma instancia do MirageJS Server antes da 29 | * execução de cada teste. 30 | */ 31 | server = makeServer({ environment: 'test' }); 32 | }); 33 | 34 | afterEach(() => { 35 | /** 36 | * Desliga a instância do MirageJS server depois 37 | * da execução de cada teste. 38 | */ 39 | server.shutdown(); 40 | 41 | /** 42 | * Faz o reset de tudo o que aconteceu com os mocks 43 | * durante o teste. Por exemplo: zera a contagem 44 | * de quantas vezes o mock foi executado. 45 | */ 46 | jest.clearAllMocks(); 47 | }); 48 | 49 | /** 50 | * Retorna uma lista de produtos. A lista é criada pelo 51 | * server MirageJS. Permite criar produtos com dados 52 | * específicos junto com os dados gerados pelo Faker. 53 | * Basta passar no parametro overrides uma lista de 54 | * objetos com as propriedades que se quer. 55 | */ 56 | const getProducts = (quantity = 10, overrides = []) => { 57 | let overrideList = []; 58 | 59 | if (overrides.length > 0) { 60 | overrideList = overrides.map((override) => 61 | server.create('product', override) 62 | ); 63 | } 64 | 65 | return [...server.createList('product', quantity), ...overrideList]; 66 | }; 67 | 68 | /** 69 | * Monta o componente ProductList, fornecendo as dependências 70 | * bem como a lista de produtos. Permite passar diferentes 71 | * quantidades e também solicitar produtos com dados fixos. 72 | * Por fim, permite também informar se o método axios.get 73 | * retornará uma Promise.resolve() ou Promise.reject(). 74 | */ 75 | const mountProductList = async ( 76 | quantity = 10, 77 | overrides = [], 78 | shouldReject = false 79 | ) => { 80 | const products = getProducts(quantity, overrides); 81 | 82 | if (shouldReject) { 83 | axios.get.mockReturnValue(Promise.reject(new Error('error'))); 84 | } else { 85 | axios.get.mockReturnValue(Promise.resolve({ data: { products } })); 86 | } 87 | 88 | const wrapper = mount(ProductList, { 89 | mocks: { 90 | $axios: axios, 91 | }, 92 | }); 93 | 94 | await Vue.nextTick(); 95 | 96 | /** 97 | * O método retona um objeto com o wrapper e também 98 | * com qualquer outra informação que possa ser necessária 99 | * para que o teste funcione como se deve. 100 | */ 101 | return { wrapper, products }; 102 | }; 103 | 104 | it('should mount the component', async () => { 105 | const { wrapper } = await mountProductList(); 106 | expect(wrapper.vm).toBeDefined(); 107 | }); 108 | 109 | it('should mount the Search component', async () => { 110 | const { wrapper } = await mountProductList(); 111 | expect(wrapper.findComponent(Search)).toBeDefined(); 112 | }); 113 | 114 | it('should call axios.get on component mount', async () => { 115 | await mountProductList(); 116 | 117 | expect(axios.get).toHaveBeenCalledTimes(1); 118 | expect(axios.get).toHaveBeenCalledWith('/api/products'); 119 | }); 120 | 121 | it('should mount the ProductCard component 10 times', async () => { 122 | const { wrapper } = await mountProductList(); 123 | 124 | const cards = wrapper.findAllComponents(ProductCard); 125 | 126 | expect(cards).toHaveLength(10); 127 | }); 128 | 129 | it('should filter the product list when a search is performed', async () => { 130 | // Arrange 131 | const { wrapper } = await mountProductList(10, [ 132 | { 133 | title: 'Meu relógio amado', 134 | }, 135 | { 136 | title: 'Meu outro relógio estimado', 137 | }, 138 | ]); 139 | 140 | // Act 141 | const search = wrapper.findComponent(Search); 142 | search.find('input[type="search"]').setValue('relógio'); 143 | await search.find('form').trigger('submit'); 144 | 145 | // Assert 146 | const cards = wrapper.findAllComponents(ProductCard); 147 | expect(wrapper.vm.searchTerm).toEqual('relógio'); 148 | expect(cards).toHaveLength(2); 149 | }); 150 | 151 | it('should filter the product list when a search is performed', async () => { 152 | // Arrange 153 | const { wrapper } = await mountProductList(10, [ 154 | { 155 | title: 'Meu outro relógio estimado', 156 | }, 157 | ]); 158 | 159 | // Act 160 | const search = wrapper.findComponent(Search); 161 | search.find('input[type="search"]').setValue('relógio'); 162 | await search.find('form').trigger('submit'); 163 | search.find('input[type="search"]').setValue(''); 164 | await search.find('form').trigger('submit'); 165 | 166 | // Assert 167 | const cards = wrapper.findAllComponents(ProductCard); 168 | expect(wrapper.vm.searchTerm).toEqual(''); 169 | expect(cards).toHaveLength(11); 170 | }); 171 | 172 | it('should display error message when promise rejects', async () => { 173 | const { wrapper } = await mountProductList(1, {}, true); 174 | 175 | expect(wrapper.text()).toContain('Problemas ao carregar a lista!'); 176 | }); 177 | 178 | it('should display the total quantity of products', async () => { 179 | const { wrapper } = await mountProductList(27); 180 | const label = wrapper.find('[data-testid="total-quantity-label"]'); 181 | 182 | expect(label.text()).toEqual('27 Products'); 183 | }); 184 | 185 | it('should display product (singular) when there is only 1 product', async () => { 186 | const { wrapper } = await mountProductList(1); 187 | const label = wrapper.find('[data-testid="total-quantity-label"]'); 188 | 189 | expect(label.text()).toEqual('1 Product'); 190 | }); 191 | }); 192 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 70 | -------------------------------------------------------------------------------- /plugins/cart.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import CartManagerPlugin from '@/managers/CartManager'; 3 | 4 | Vue.use(CartManagerPlugin); 5 | -------------------------------------------------------------------------------- /plugins/miragejs.js: -------------------------------------------------------------------------------- 1 | import { createServer, Response } from 'miragejs'; 2 | 3 | if (process.env.NODE_ENV === 'development' && !process.env.USE_API) { 4 | require('@/miragejs/server').makeServer(); 5 | } 6 | 7 | if (window.Cypress) { 8 | // If your app makes requests to domains other than / (the current domain), add them 9 | // here so that they are also proxied from your app to the handleFromCypress function. 10 | // For example: let otherDomains = ["https://my-backend.herokuapp.com/"] 11 | const otherDomains = []; 12 | const methods = ['get', 'put', 'patch', 'post', 'delete']; 13 | 14 | createServer({ 15 | urlPrefix: 'http://localhost:5000', 16 | environment: 'test', 17 | routes() { 18 | for (const domain of ['/', ...otherDomains]) { 19 | for (const method of methods) { 20 | this[method](`${domain}*`, async (schema, request) => { 21 | const [status, headers, body] = await window.handleFromCypress( 22 | request 23 | ); 24 | return new Response(status, headers, body); 25 | }); 26 | } 27 | } 28 | 29 | // If your central server has any calls to passthrough(), you'll need to duplicate them here 30 | // this.passthrough('https://analytics.google.com') 31 | }, 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | # STATIC 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your static files. 6 | Each file inside this directory is mapped to `/`. 7 | Thus you'd want to delete this README.md before deploying to production. 8 | 9 | Example: `/static/robots.txt` is mapped as `/robots.txt`. 10 | 11 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static). 12 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vedovelli/curso-javascript-testes-modulo-2/c7baa9e36c018432c62d1c37ab8b88cbe9f65ad7/static/favicon.ico -------------------------------------------------------------------------------- /static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vedovelli/curso-javascript-testes-modulo-2/c7baa9e36c018432c62d1c37ab8b88cbe9f65ad7/static/icon.png -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Vuex Store files. 6 | Vuex Store option is implemented in the Nuxt.js framework. 7 | 8 | Creating a file in this directory automatically activates the option in the framework. 9 | 10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store). 11 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | }; 12 | --------------------------------------------------------------------------------