├── .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 |
2 |
78 |
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 |
2 |
3 |
4 |
![]()
9 |
10 |
{{ product.title }}
11 |
17 |
18 |
35 |
{{
36 | quantity
37 | }}
38 |
57 |
58 |
59 |
60 |
${{ product.price }}
61 |
62 |
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 |
2 |
3 |
7 |
26 |
27 |
28 |
{{ product.title }}
29 | ${{ product.price }}
30 |
31 |
32 |
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 |
2 |
25 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
26 |
NY
27 |
28 |
31 | Brand
32 |
33 |
34 |
53 |
54 |
55 |
67 |
68 |
69 |
70 |
99 |
100 |
101 |
107 |
{{ errorMessage }}
108 |
109 |
119 |
120 |
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 |
2 |
3 |
4 |
5 |
Wrist Watch
6 |
{{ quantityLabel }}
11 |
21 |
22 | {{ errorMessage }}
23 |
24 |
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 |
--------------------------------------------------------------------------------