├── .eslintignore
├── config
├── prod.env.js
├── dev.env.js
└── index.js
├── src
├── assets
│ ├── baturro.gif
│ ├── stones.svg
│ └── stones-small-red-white_letters.svg
├── statics
│ └── stones.png
├── app
│ ├── services
│ │ ├── MapService.js
│ │ ├── jota-instances.js
│ │ ├── jota-commands.js
│ │ ├── __mocks__
│ │ │ ├── JotaRouter.js
│ │ │ ├── jota-api.js
│ │ │ ├── gigs-sample.js
│ │ │ └── create-fake-gig.js
│ │ ├── jota-payloads.js
│ │ ├── __tests__
│ │ │ ├── jota-payloads.spec.js
│ │ │ ├── jota-api.integration.spec.js
│ │ │ └── date-utils.spec.js
│ │ ├── jota-api.js
│ │ ├── JotaRouter.js
│ │ └── date-utils.js
│ ├── shared-components
│ │ ├── LoadSpinner.vue
│ │ ├── FormButton.vue
│ │ ├── __test__
│ │ │ ├── codely
│ │ │ │ ├── TestingSlots.spec.js
│ │ │ │ └── TestingClickButton.spec.js
│ │ │ ├── LoadSpinner.spec.js
│ │ │ └── FormButton.spec.js
│ │ ├── BackToTopButton.vue
│ │ ├── Toolbar.vue
│ │ ├── TextInput.vue
│ │ ├── DateTimeInput.vue
│ │ └── SideBar.vue
│ ├── fake-backend
│ │ ├── __mocks__
│ │ │ └── JotaCore.js
│ │ └── JotaCore.js
│ ├── pages
│ │ ├── NewGig
│ │ │ ├── customValidations.js
│ │ │ ├── __test__
│ │ │ │ ├── customValidations.spec.js
│ │ │ │ ├── codely
│ │ │ │ │ ├── TestingSubmit.spec.js
│ │ │ │ │ └── TestingValidations.spec.js
│ │ │ │ └── NewGig.spec.js
│ │ │ └── NewGig.vue
│ │ ├── Days
│ │ │ ├── GigRowFunctional.vue
│ │ │ ├── __test__
│ │ │ │ ├── GigRow.spec.js
│ │ │ │ ├── codely
│ │ │ │ │ ├── TestingContainer.spec.js
│ │ │ │ │ └── TestingRouteNavigation.spec.js
│ │ │ │ ├── Day.spec.js
│ │ │ │ └── Days.spec.js
│ │ │ ├── GigRow.vue
│ │ │ ├── Day.vue
│ │ │ └── Days.vue
│ │ ├── GigDetail
│ │ │ ├── __test__
│ │ │ │ ├── codely
│ │ │ │ │ ├── TestingWithBackendStubbedByJest.spec.js
│ │ │ │ │ ├── TestingRouterWithManualMock.spec.js
│ │ │ │ │ └── TestingWithBackendAsProp.spec.js
│ │ │ │ └── GigDetail.spec.js
│ │ │ └── GigDetail.vue
│ │ └── Error404.vue
│ └── GlobalComponentsLoader.js
├── setupJest.js
├── index.html
├── themes
│ ├── app.ios.styl
│ ├── app.mat.styl
│ ├── quasar.variables.styl
│ └── app.variables.styl
├── jestGlobalMocks.js
├── App.vue
├── main.js
└── router.js
├── .gitignore
├── .editorconfig
├── .babelrc
├── jest.config.js
├── .stylintrc
├── .eslintrc.js
├── test
├── helpers.js
└── render-utils.js
├── README.md
└── package.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | config/*.js
3 | dist/*.js
4 |
--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | NODE_ENV: '"production"'
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/baturro.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingstones/codelytv-vue-basic/HEAD/src/assets/baturro.gif
--------------------------------------------------------------------------------
/src/statics/stones.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingstones/codelytv-vue-basic/HEAD/src/statics/stones.png
--------------------------------------------------------------------------------
/src/app/services/MapService.js:
--------------------------------------------------------------------------------
1 | export function openMap(url) {
2 | window.open(url, '_blank', 'location=yes')
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/services/jota-instances.js:
--------------------------------------------------------------------------------
1 | import { GigService, Matcher } from '../fake-backend/JotaCore'
2 |
3 | export const gigService = new GigService(new Matcher())
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .thumbs.db
3 | node_modules/
4 | dist/
5 | npm-debug.log*
6 | cordova/platforms
7 | cordova/plugins
8 | .idea
9 | cordova
10 | .quasar
11 | /coverage
--------------------------------------------------------------------------------
/config/dev.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var prodEnv = require('./prod.env')
3 |
4 | module.exports = merge(prodEnv, {
5 | NODE_ENV: '"development"'
6 | })
7 |
--------------------------------------------------------------------------------
/src/app/services/jota-commands.js:
--------------------------------------------------------------------------------
1 | // this commands have the same name that they have in our backend
2 | export const RETRIEVE_DAYS = 'retrieve_days'
3 | export const CREATE_GIG = 'create_gig'
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/src/setupJest.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 | import { sleep } from '../test/render-utils';
3 |
4 | //Avoid weird async error with Quasar buttons
5 | beforeEach(async ()=> await sleep(1))
6 |
--------------------------------------------------------------------------------
/src/app/services/__mocks__/JotaRouter.js:
--------------------------------------------------------------------------------
1 | const getParam = () => 'any id'
2 | const navigateToAllGigs = jest.fn()
3 | const navigateToGig = jest.fn()
4 | module.exports = {
5 | JotaRouter: () => {
6 | return { getParam, navigateToAllGigs, navigateToGig }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/shared-components/LoadSpinner.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
12 |
13 |
15 |
--------------------------------------------------------------------------------
/src/app/fake-backend/__mocks__/JotaCore.js:
--------------------------------------------------------------------------------
1 | import { fakeGigsByDay } from '../../services/__mocks__/gigs-sample'
2 |
3 | class GigService {
4 | retrieveNextGigs() {
5 | return Promise.resolve(fakeGigsByDay)
6 | }
7 | }
8 |
9 | class Matcher {}
10 |
11 | module.exports = { GigService, Matcher }
12 |
--------------------------------------------------------------------------------
/src/app/pages/NewGig/customValidations.js:
--------------------------------------------------------------------------------
1 | import { withParams } from 'vuelidate/lib'
2 |
3 | export const isFutureDatetime = withParams({ type: 'isFutureDatetime' }, selectedDatetime => {
4 | if (selectedDatetime === '') return true
5 | let selected = new Date(selectedDatetime).getTime()
6 | let now = Date.now()
7 | return selected > now
8 | })
9 |
--------------------------------------------------------------------------------
/src/app/services/jota-payloads.js:
--------------------------------------------------------------------------------
1 | export function createGigPayload(title, dateTime) {
2 | return {
3 | title: title,
4 | day: dateTime.substring(0, 10),
5 | description: 'Fake gig created by jota-api',
6 | place: 'Fake place',
7 | image_url: 'https://pbs.twimg.com/profile_images/727103317168394240/fY7WRP5T_400x400.jpg'
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/shared-components/FormButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | Saving gig...
8 |
9 |
10 |
11 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/src/app/services/__mocks__/jota-api.js:
--------------------------------------------------------------------------------
1 | import { fakeGigsByDay, FIRST_GIG } from './gigs-sample'
2 |
3 | const createGig = jest.fn((payload) => {
4 | return Promise.resolve(payload)
5 | })
6 |
7 | const retrieveAGig = jest.fn((gigId) => {
8 | return Promise.resolve(FIRST_GIG)
9 | })
10 |
11 | const retrieveDays = jest.fn(() => {
12 | return Promise.resolve(fakeGigsByDay)
13 | })
14 |
15 | module.exports = { createGig, retrieveDays, retrieveAGig }
16 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "modules": false,
5 | "targets": {
6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
7 | }
8 | }],
9 | "stage-2"
10 | ],
11 | "plugins": ["transform-runtime", "dynamic-import-node"],
12 | "env": {
13 | "test": {
14 | "presets": ["env", "stage-2"],
15 | "plugins": [
16 | ["module-resolver", {
17 | "root": ["./src"]
18 | }]]
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/services/__tests__/jota-payloads.spec.js:
--------------------------------------------------------------------------------
1 | import { stubNow } from '../../../../test/helpers'
2 | import { createGigPayload } from '../jota-payloads'
3 |
4 | describe('jota payloads', () => {
5 |
6 | beforeEach(() => stubNow('2017-09-18'))
7 |
8 | it('Builds create Gig Payload', () => {
9 | const payload = createGigPayload('a title', '2017-09-18T19:32')
10 |
11 | expect(payload.title).toBe('a title')
12 | expect(payload.day).toBe('2017-09-18')
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/app/shared-components/__test__/codely/TestingSlots.spec.js:
--------------------------------------------------------------------------------
1 | import FormButton from '@/app/shared-components/FormButton.vue'
2 | import { renderComponent } from '@test/render-utils'
3 |
4 | describe('FormButton.vue', () => {
5 | it('renders label inside slot', async () => {
6 |
7 | const utils = renderComponent(FormButton, {
8 | slots: { default: 'CLICK ME' }
9 | })
10 |
11 | expect(await utils.findByText(/click me/i)).toBeInTheDocument()
12 |
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/app/shared-components/BackToTopButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
22 |
23 |
25 |
--------------------------------------------------------------------------------
/src/app/pages/Days/GigRowFunctional.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{props.gig.title}}
7 | {{props.gig.place}}
8 |
9 |
10 |
11 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/src/app/pages/Days/__test__/GigRow.spec.js:
--------------------------------------------------------------------------------
1 | import GigRow from '@/app/pages/Days/GigRow.vue'
2 | import { renderComponent } from '@test/render-utils'
3 | import {screen} from '@testing-library/vue'
4 |
5 | describe('GigRow', () => {
6 |
7 | it('renders gig content', async () => {
8 | const GIG = {id: 'an id', title: 'a title', place: 'a place'}
9 |
10 | renderComponent(GigRow, { props: {gig: GIG} })
11 |
12 | expect(await screen.findByText('a title')).toBeInTheDocument()
13 | expect(await screen.findByText('a place')).toBeInTheDocument()
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Jota JS
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/app/pages/Days/__test__/codely/TestingContainer.spec.js:
--------------------------------------------------------------------------------
1 | import Days from '@/app/pages/Days/Days.vue'
2 | import { renderComponent } from '@test/render-utils'
3 | import { FIRST_DAY } from '../../../../services/__mocks__/gigs-sample'
4 |
5 | jest.mock('@/app/services/jota-api')
6 |
7 | test('renders all gigs titles for the first day of gigs', async() => {
8 | const {screen} = renderComponent(Days)
9 |
10 | const FIRST_DAY_GIG_TITLES = FIRST_DAY.gigs.map(gig => gig.title)
11 | FIRST_DAY_GIG_TITLES.forEach(async text => {
12 | expect(await screen.findByText(text)).toBeInTheDocument()
13 | })
14 | })
--------------------------------------------------------------------------------
/src/app/shared-components/Toolbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Jotas JS
8 |
9 |
10 |
11 | SEARCH
12 |
13 |
14 |
15 |
16 |
25 |
26 |
29 |
--------------------------------------------------------------------------------
/src/app/pages/Days/__test__/Day.spec.js:
--------------------------------------------------------------------------------
1 | import Day from '@/app/pages/Days/Day.vue'
2 | import { FIRST_DAY } from '../../../services/__mocks__/gigs-sample'
3 | import { localizedFromIso } from '../../../services/date-utils'
4 | import { renderComponent } from '@test/render-utils'
5 |
6 | describe('Day', () => {
7 |
8 | it('renders gig date in spanish format', async() => {
9 | const {findByText} = await renderComponent(Day, {
10 | props: { day: FIRST_DAY, isLoading: false, onClick: jest.fn }
11 | })
12 |
13 | expect(await findByText(localizedFromIso(FIRST_DAY.date))).toBeInTheDocument()
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/app/pages/Days/GigRow.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{gig.title}}
7 | {{gig.place}}
8 |
9 |
10 |
11 |
12 |
13 |
18 |
19 |
25 |
--------------------------------------------------------------------------------
/src/app/shared-components/__test__/LoadSpinner.spec.js:
--------------------------------------------------------------------------------
1 | import LoadSpinner from '@/app/shared-components/LoadSpinner.vue'
2 | import { render } from '@testing-library/vue'
3 |
4 | describe('LoadSpinner.vue', () => {
5 |
6 | it('shows loading', async () => {
7 | const {queryByText} = render(LoadSpinner, {props: { isLoading: false }})
8 |
9 | expect(queryByText(/Loading.../i)).not.toBeInTheDocument()
10 | })
11 |
12 | it('does not show loading', async () => {
13 | const {findByText} = render(LoadSpinner, {props: { isLoading: true }})
14 |
15 | expect(await findByText((/Loading.../i))).toBeInTheDocument()
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/src/app/pages/GigDetail/__test__/codely/TestingWithBackendStubbedByJest.spec.js:
--------------------------------------------------------------------------------
1 | import GigDetail from '@/app/pages/GigDetail/GigDetail.vue'
2 | import { renderComponent } from '@test/render-utils'
3 | import { FIRST_GIG } from '../../../../services/__mocks__/gigs-sample'
4 |
5 | jest.mock('@/app/services/jota-api')
6 |
7 | it('renders details from a Gig', async () => {
8 | const router = {currentRoute : {params: {id: FIRST_GIG.id}}}
9 |
10 | const {screen} = renderComponent(GigDetail, {router})
11 |
12 | expect(await screen.findByText(FIRST_GIG.title)).toBeInTheDocument()
13 | expect(await screen.findByText(FIRST_GIG.place)).toBeInTheDocument()
14 | })
15 |
--------------------------------------------------------------------------------
/src/themes/app.ios.styl:
--------------------------------------------------------------------------------
1 | // This file is included in the build if src/main.js imports it.
2 | // Otherwise the default iOS CSS file is bundled.
3 | // Check "DEFAULT / CUSTOM STYLE" in src/main.js
4 |
5 | // App Shared Variables
6 | // --------------------------------------------------
7 | // Shared Stylus variables go in the app.variables.styl file
8 | @import 'app.variables'
9 |
10 | // Quasar iOS Design Stylus
11 | // --------------------------------------------------
12 | // Custom App variables must be declared before importing Quasar.
13 | // Quasar will use its default values when a custom variable isn't provided.
14 | @import '~quasar-framework/dist/quasar.ios.styl'
15 |
--------------------------------------------------------------------------------
/src/themes/app.mat.styl:
--------------------------------------------------------------------------------
1 | // This file is included in the build if src/main.js imports it.
2 | // Otherwise the default Material CSS file is bundled.
3 | // Check "DEFAULT / CUSTOM STYLE" in src/main.js
4 |
5 | // App Shared Variables
6 | // --------------------------------------------------
7 | // Shared Stylus variables go in the app.variables.styl file
8 | @import 'app.variables'
9 |
10 | // Quasar Material Design Stylus
11 | // --------------------------------------------------
12 | // Custom App variables must be declared before importing Quasar.
13 | // Quasar will use its default values when a custom variable isn't provided.
14 | @import '~quasar-framework/dist/quasar.mat.styl'
15 |
--------------------------------------------------------------------------------
/src/app/pages/GigDetail/__test__/GigDetail.spec.js:
--------------------------------------------------------------------------------
1 | import GigDetail from '@/app/pages/GigDetail/GigDetail.vue'
2 | import { renderComponent } from '@test/render-utils'
3 | import { FIRST_GIG } from '../../../services/__mocks__/gigs-sample'
4 |
5 | jest.mock('@/app/services/jota-api')
6 |
7 | describe('Gig Detail', () => {
8 |
9 | it('renders details from a Gig', async () => {
10 | const router = {currentRoute : {params: {id: FIRST_GIG.id}}}
11 |
12 | const {findByText} = await renderComponent(GigDetail, {router})
13 |
14 | expect(await findByText(FIRST_GIG.title)).toBeInTheDocument()
15 | expect(await findByText(FIRST_GIG.place)).toBeInTheDocument()
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/src/app/pages/GigDetail/__test__/codely/TestingRouterWithManualMock.spec.js:
--------------------------------------------------------------------------------
1 | import GigDetail from '@/app/pages/GigDetail/GigDetail.vue'
2 | import { renderComponent } from '@test/render-utils'
3 | import { FIRST_GIG } from '../../../../services/__mocks__/gigs-sample'
4 |
5 | jest.mock('@/app/services/jota-api')
6 |
7 | test('renders details from a Gig', async () => {
8 | const router = {currentRoute : {params: {id: FIRST_GIG.id}}}
9 |
10 | const {screen} = renderComponent(GigDetail,
11 | {
12 | router
13 | } )
14 |
15 | expect(await screen.findByText(FIRST_GIG.title)).toBeInTheDocument()
16 | expect(await screen.findByText(FIRST_GIG.place)).toBeInTheDocument()
17 | })
18 |
19 |
--------------------------------------------------------------------------------
/src/app/pages/NewGig/__test__/customValidations.spec.js:
--------------------------------------------------------------------------------
1 | import { isFutureDatetime } from '../customValidations'
2 |
3 | describe('isFutureDatetime', () => {
4 |
5 | beforeEach(() => {
6 | Date.now = jest.fn(()=> new Date('2017-10-12T04:41:20'))
7 | })
8 |
9 | it('with tomorrow', () => {
10 | expect(isFutureDatetime('2017-10-13T00:00:00')).toBe(true)
11 | })
12 |
13 | it('with today', () => {
14 | expect(isFutureDatetime('2017-10-12T00:00:00')).toBe(false)
15 | })
16 |
17 | it('with yesterday', () => {
18 | expect(isFutureDatetime('2017-10-11T00:00:00')).toBe(false)
19 | })
20 |
21 | it('with empty date', () => {
22 | expect(isFutureDatetime('')).toBe(true)
23 | })
24 |
25 | })
26 |
--------------------------------------------------------------------------------
/src/jestGlobalMocks.js:
--------------------------------------------------------------------------------
1 | const mock = () => {
2 | let storage = {}
3 | return {
4 | getItem: key => key in storage ? storage[key] : null,
5 | setItem: (key, value) => storage[key] = value || '',
6 | removeItem: key => delete storage[key],
7 | clear: () => storage = {},
8 | }
9 | }
10 |
11 | Object.defineProperty(window, 'localStorage', {value: mock()})
12 | Object.defineProperty(window, 'sessionStorage', {value: mock()})
13 | Object.defineProperty(window, 'getComputedStyle', {
14 | value: () => ['-webkit-appearance']
15 | })
16 |
17 | window.getComputedStyle = function(el1, el2) {
18 | return [
19 | 'transitionDuration'
20 | ]
21 | }
22 |
23 | Object.defineProperty(window, 'localStorage', {value: mock()})
24 |
25 |
--------------------------------------------------------------------------------
/src/themes/quasar.variables.styl:
--------------------------------------------------------------------------------
1 | //
2 | // Webpack alias "variables" points to this file.
3 | // So you can import it in your app's *.vue files
4 | // inside the
12 |
13 |
14 | // First we load app's Stylus variables
15 | @import 'app.variables'
16 |
17 | // Then we load Quasar Stylus variables.
18 | // Any variables defined in "app.variables.styl"
19 | // will override Quasar's ones.
20 | //
21 | // NOTICE that we only import Core Quasar Variables
22 | // like colors, media breakpoints, and so.
23 | // No component variable will be included.
24 | @import '~quasar/dist/core.variables'
25 |
--------------------------------------------------------------------------------
/src/app/services/jota-api.js:
--------------------------------------------------------------------------------
1 | import { gigService } from './jota-instances'
2 |
3 | let fakeDays = gigService.retrieveNextGigs()
4 |
5 | const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
6 |
7 | export function retrieveDays() {
8 | return delay(1500).then(() => fakeDays)
9 | }
10 |
11 | export function retrieveAGig(gigId) {
12 | return delay(1000).then(() => gigService.retrieveAGig(gigId))
13 | }
14 |
15 | export function createGig(payload) {
16 | addGigToFakeSample(payload)
17 | return delay(1000).then(() => payload)
18 | }
19 |
20 | function addGigToFakeSample(gig) {
21 | if (fakeDays[gig.day]) {
22 | fakeDays[gig.day].gigs.unshift(gig)
23 | }
24 | else {
25 | fakeDays[gig.day] = { date: gig.day, gigs: [gig] }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/pages/Days/Day.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{dateLabel}}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
25 |
26 |
33 |
--------------------------------------------------------------------------------
/src/app/pages/GigDetail/__test__/codely/TestingWithBackendAsProp.spec.js:
--------------------------------------------------------------------------------
1 | import GigDetail from '@/app/pages/GigDetail/GigDetail.vue'
2 | import { renderComponent } from '@test/render-utils'
3 | import { createFakeGig } from '../../../../services/__mocks__/create-fake-gig'
4 |
5 | test('renders details from a Gig', async () => {
6 | const GIG = createFakeGig()
7 | const retrieveAGigStub = jest.fn(() => Promise.resolve(GIG))
8 | const router = {currentRoute : {params: {id: GIG.id}}}
9 |
10 | const {screen} = renderComponent(GigDetail,
11 | {
12 | props: {retrieveAGig: retrieveAGigStub},
13 | router
14 | } )
15 |
16 | expect(await screen.findByText(GIG.title)).toBeInTheDocument()
17 | expect(await screen.findByText(GIG.place)).toBeInTheDocument()
18 | })
19 |
20 |
--------------------------------------------------------------------------------
/src/app/services/JotaRouter.js:
--------------------------------------------------------------------------------
1 | import VueRouter from 'vue-router'
2 |
3 | export const JotaRouter = (router) => {
4 | if (!router) router = new VueRouter()
5 | return { navigateToGig, navigateToCreateGig, navigateToAllGigs, getParam }
6 |
7 | function navigateToGig(gigId) {
8 | console.log('ENTRO POR AQUI CO')
9 | router.push('gig/' + gigId)
10 | }
11 |
12 | function navigateToCreateGig() {
13 | router.push({path: '/newGig'})
14 | }
15 |
16 | function navigateToAllGigs() {
17 | router.push({path: '/all'})
18 | }
19 |
20 | function getParam(id) {
21 | if (!router) return ''
22 | return router.currentRoute.params[id]
23 | }
24 | }
25 |
26 | export const jotaRouterMixin = {
27 | created: function () {
28 | this.jotaRouter = JotaRouter(this.$router)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/pages/Days/Days.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
33 |
34 |
36 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
30 |
31 |
36 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleFileExtensions: [
3 | "js",
4 | "json",
5 | "vue"
6 | ],
7 | coverageReporters: ['json', 'html', 'text', 'text-summary', 'lcov'],
8 | collectCoverageFrom: [
9 | '/src/**/*.js',
10 | '!**/node_modules/**',
11 | '!**/vendor/**',
12 | ],
13 | moduleNameMapper: {
14 | "^@/(.*)$": "/src/$1",
15 | "^@test/(.*)$": "/test/$1"
16 | },
17 | setupFilesAfterEnv: ['/src/setupJest.js'],
18 | transform: {
19 | "^.+\\.js$": "/node_modules/babel-jest",
20 | ".*\\.(vue)$": "/node_modules/vue-jest"
21 | },
22 | transformIgnorePatterns: [
23 | "node_modules/(?!quasar-framework)"
24 | ],
25 | testPathIgnorePatterns: [
26 | "/node_modules/",
27 | "test/e2e"
28 | ],
29 | // snapshotSerializers: [
30 | // "/node_modules/jest-serializer-vue"
31 | // ]
32 | }
--------------------------------------------------------------------------------
/.stylintrc:
--------------------------------------------------------------------------------
1 | {
2 | "blocks": "never",
3 | "brackets": "never",
4 | "colons": "never",
5 | "colors": "always",
6 | "commaSpace": "always",
7 | "commentSpace": "always",
8 | "cssLiteral": "never",
9 | "depthLimit": false,
10 | "duplicates": true,
11 | "efficient": "always",
12 | "extendPref": false,
13 | "globalDupe": true,
14 | "indentPref": 2,
15 | "leadingZero": "never",
16 | "maxErrors": false,
17 | "maxWarnings": false,
18 | "mixed": false,
19 | "namingConvention": false,
20 | "namingConventionStrict": false,
21 | "none": "never",
22 | "noImportant": false,
23 | "parenSpace": "never",
24 | "placeholder": false,
25 | "prefixVarsWithDollar": "always",
26 | "quotePref": "single",
27 | "semicolons": "never",
28 | "sortOrder": false,
29 | "stackedProperties": "never",
30 | "trailingWhitespace": "never",
31 | "universal": "never",
32 | "valid": true,
33 | "zeroUnits": "never",
34 | "zIndexNormalize": false
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/services/__tests__/jota-api.integration.spec.js:
--------------------------------------------------------------------------------
1 | import { createGig, retrieveAGig, retrieveDays } from '../jota-api'
2 | import { isoToday } from '../date-utils'
3 | import { createGigPayload } from '../jota-payloads'
4 |
5 | describe('jota api', () => {
6 |
7 | it('Returns valid gigs', async () => {
8 | const days = await retrieveDays()
9 |
10 | expect(days).toBeInstanceOf(Object)
11 | expect(days[isoToday()].gigs.length).toBeGreaterThan(0)
12 | // We should also check gig structure has not changed
13 | })
14 |
15 | it('Returns a gig by id', async () => {
16 | const gig = await retrieveAGig(1)
17 |
18 | expect(gig).toBeInstanceOf(Object)
19 | expect(typeof gig.title).toBe('string')
20 | })
21 |
22 | it('Creates a gig', async () => {
23 | const gig = await createGig(createGigPayload('a title', '2007-10-12T12:00'))
24 |
25 | expect(gig).toBeInstanceOf(Object)
26 | expect(typeof gig.title).toBe('string')
27 | expect(gig.title).toBe('a title')
28 | expect(gig.day).toBe('2007-10-12')
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/src/app/shared-components/__test__/codely/TestingClickButton.spec.js:
--------------------------------------------------------------------------------
1 | import FormButton from '@/app/shared-components/FormButton.vue'
2 | import { renderComponent } from '@test/render-utils'
3 | import userEvent from '@testing-library/user-event'
4 | import { render } from '@testing-library/vue'
5 | import { mount } from '@vue/test-utils'
6 |
7 | describe('When clicking', () => {
8 |
9 | it('calls callback if enabled', async () => {
10 |
11 | const clickSpy = jest.fn()
12 | const utils = renderComponent(FormButton, {
13 | props: {onClick: clickSpy},
14 | slots: {default: 'Click Me'}
15 | })
16 |
17 | userEvent.click(await utils.findByText(/Click me/i))
18 |
19 | expect(clickSpy).toHaveBeenCalled()
20 | })
21 |
22 | it('does not call callback if disabled', async () => {
23 |
24 | const clickSpy = jest.fn()
25 | const utils = renderComponent(FormButton, {
26 | props: {onClick: clickSpy, disabled: true},
27 | slots: {default: 'Click Me'}
28 | })
29 |
30 | expect(clickSpy).not.toHaveBeenCalled()
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: 'babel-eslint',
4 | parserOptions: {
5 | sourceType: 'module'
6 | },
7 | env: {
8 | browser: true,
9 | jest: true
10 | },
11 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
12 | extends: [
13 | 'standard'
14 | ],
15 | // required to lint *.vue files
16 | plugins: [
17 | 'html',
18 | 'import'
19 | ],
20 | globals: {
21 | 'cordova': true,
22 | 'DEV': true,
23 | 'PROD': true,
24 | '__THEME': true,
25 | // defined in src/setupJest.js
26 | keepsSnapshot: true,
27 | },
28 | // add your custom rules here
29 | 'rules': {
30 | 'space-before-function-paren': ['off'],
31 | // allow paren-less arrow functions
32 | 'arrow-parens': 0,
33 | 'one-var': 0,
34 | 'import/first': 0,
35 | 'import/named': 2,
36 | 'import/namespace': 2,
37 | 'import/default': 2,
38 | 'import/export': 2,
39 | // allow debugger during development
40 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
41 | 'brace-style': [2, 'stroustrup', { 'allowSingleLine': true }]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/themes/app.variables.styl:
--------------------------------------------------------------------------------
1 | // This file is included in the build if src/main.js imports
2 | // either app.mat.styl or app.ios.styl.
3 | // Check "DEFAULT / CUSTOM STYLE" in src/main.js
4 |
5 | // App Shared Variables
6 | // --------------------------------------------------
7 | // To customize the look and feel of this app, you can override
8 | // the Stylus variables found in Quasar's source Stylus files. Setting
9 | // variables before Quasar's Stylus will use these variables rather than
10 | // Quasar's default Stylus variable values. Stylus variables specific
11 | // to the themes belong in either the app.ios.styl or app.mat.styl files.
12 |
13 |
14 | // App Shared Color Variables
15 | // --------------------------------------------------
16 | // It's highly recommended to change the default colors
17 | // to match your app's branding.
18 |
19 | $primary = #795548
20 | $secondary = #FF5722
21 |
22 | $neutral = #E0E1E2
23 | $positive = #21BA45
24 | $negative = #DB2828
25 | $info = #31CCEC
26 | $warning = #F2C037
27 |
28 | $layout-aside-background = #FFFFFF
29 | $body-background = #FFFFFF
30 | $item-font-size = 1.1rem
31 |
32 | //$body-color = $secondary
33 | //$toolbar-color = red
34 | // $item-stripe-color = yellow
35 | //$item-highlight-color = alpha($secondary, 30%)
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | // === DEFAULT / CUSTOM STYLE ===
2 | // WARNING! always comment out ONE of the two require() calls below.
3 | // 1. use next line to activate CUSTOM STYLE (./src/themes)
4 | import { jotaRouterMixin } from './app/services/JotaRouter'
5 |
6 | require(`./themes/app.${__THEME}.styl`)
7 | // 2. or, use next line to activate DEFAULT QUASAR STYLE
8 | // require(`quasar/dist/quasar.${__THEME}.css`)
9 | // ==============================
10 |
11 | // Uncomment the following lines if you need IE11/Edge support
12 | // require(`quasar/dist/quasar.ie`)
13 | // require(`quasar/dist/quasar.ie.${__THEME}.css`)
14 |
15 | // Import all transitions (it will include unused ones KB's)
16 | import 'quasar-extras/animate'
17 |
18 | import Vue from 'vue'
19 | import Quasar from 'quasar-framework'
20 | import Vuelidate from 'vuelidate'
21 | import router from './router'
22 |
23 | Vue.use(Quasar)
24 | Vue.config.productionTip = false
25 | Vue.use(Vuelidate)
26 | Vue.mixin(jotaRouterMixin)
27 |
28 | if (__THEME === 'mat') {
29 | require('quasar-extras/roboto-font')
30 | }
31 | import 'quasar-extras/material-icons'
32 | import 'quasar-extras/ionicons'
33 | // import 'quasar-extras/fontawesome'
34 |
35 | Quasar.start(() => {
36 | /* eslint-disable no-new */
37 | new Vue({
38 | el: '#q-app',
39 | router,
40 | render: h => h(require('./App').default)
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/src/app/services/date-utils.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment')
2 | require('moment/locale/es')
3 | const ISO_FORMAT = 'YYYY-MM-DD'
4 |
5 | // Default locale:
6 | let locale = 'es'
7 | export function setLocale(localeCode) {
8 | locale = localeCode
9 | }
10 |
11 | export function isoToday() {
12 | return moment().format(ISO_FORMAT)
13 | }
14 |
15 | export function isoTomorrow() {
16 | return moment().add(1, 'days').format(ISO_FORMAT)
17 | }
18 |
19 | export function isoFuture(daysFromNow) {
20 | return moment().add(daysFromNow, 'days').format(ISO_FORMAT)
21 | }
22 |
23 | export function localizedToday() {
24 | return localize(moment())
25 | }
26 |
27 | export function localizedTomorrow() {
28 | return localize(moment().add(1, 'days'))
29 | }
30 |
31 | export function localizedFromIso(isoDate) {
32 | return localize(moment(isoDate, ISO_FORMAT))
33 | }
34 |
35 | function localize(date) {
36 | return removeLeadingZeros(toTitleCase(date.locale(locale).format(`dddd, DD [${localizedSeparator(locale)}] MMMM`)))
37 | }
38 |
39 | function removeLeadingZeros(formattedDate) {
40 | return formattedDate.replace(/ 0+/g, ' ')
41 | }
42 |
43 | function localizedSeparator(locale) {
44 | if (locale === 'en') return 'of'
45 | return 'de'
46 | }
47 |
48 | function toTitleCase(str) {
49 | return str.replace(/\w\S*/g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() })
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/pages/Days/__test__/codely/TestingRouteNavigation.spec.js:
--------------------------------------------------------------------------------
1 | import Days from '@/app/pages/Days/Days.vue'
2 | import { renderComponent } from '@test/render-utils'
3 | import { FIRST_DAY } from '../../../../services/__mocks__/gigs-sample'
4 | import {screen} from '@testing-library/vue'
5 | import userEvent from '@testing-library/user-event'
6 |
7 | jest.mock('@/app/services/jota-api')
8 |
9 | describe('Should allow', () => {
10 |
11 | it('navigating to first gig detail', async () => {
12 | const FIRST_GIG = FIRST_DAY.gigs[0]
13 | const navigateToGigSpy = jest.fn()
14 | const screen = await renderDays({navigateToGig: navigateToGigSpy})
15 |
16 | await screen.openGig(FIRST_GIG.title)
17 |
18 | expect(navigateToGigSpy).toHaveBeenCalledWith(FIRST_GIG.id)
19 | })
20 |
21 | it('navigating to second gig detail', async () => {
22 | const SECOND_GIG = FIRST_DAY.gigs[1]
23 | const navigateToGigSpy = jest.fn()
24 | const screen = await renderDays({navigateToGig: navigateToGigSpy})
25 |
26 | await screen.openGig(SECOND_GIG.title)
27 |
28 | expect(navigateToGigSpy).toHaveBeenCalledWith(SECOND_GIG.id)
29 | })
30 | })
31 |
32 | async function renderDays(jotaRouterInstance) {
33 | await renderComponent(Days, {
34 | jotaRouter: ()=> jotaRouterInstance
35 | })
36 | const openGig = async title => userEvent.click((await screen.findByText(title)))
37 |
38 | return {...screen, openGig}
39 | }
--------------------------------------------------------------------------------
/src/app/services/__tests__/date-utils.spec.js:
--------------------------------------------------------------------------------
1 | import {
2 | localizedToday, isoToday, isoTomorrow, localizedFromIso, localizedTomorrow,
3 | setLocale
4 | } from '../date-utils'
5 | import { stubNow } from '../../../../test/helpers'
6 |
7 | describe('date utils', () => {
8 |
9 | beforeEach(() => stubNow('2017-09-02'))
10 |
11 | it('Prints today in ISO', () => {
12 | expect(isoToday()).toBe('2017-09-02')
13 | })
14 |
15 | it('Prints tomorrow in ISO', () => {
16 | expect(isoTomorrow()).toBe('2017-09-03')
17 | })
18 |
19 | describe('When locale is english', () => {
20 |
21 | beforeEach(() => setLocale('en'))
22 |
23 | it('Prints todays date', () => {
24 | expect(localizedToday()).toBe('Saturday, 2 Of September')
25 | })
26 |
27 | it('Prints tomorrows date', () => {
28 | expect(localizedTomorrow()).toBe('Sunday, 3 Of September')
29 | })
30 | })
31 |
32 | describe('When locale is spanish', () => {
33 |
34 | beforeEach(() => setLocale('es'))
35 |
36 | it('Prints todays date', () => {
37 | expect(localizedToday()).toBe('Sábado, 2 De Septiembre')
38 | })
39 |
40 | it('Prints tomorrows date', () => {
41 | expect(localizedTomorrow()).toBe('Domingo, 3 De Septiembre')
42 | })
43 |
44 | it('translate ISO date', () => {
45 | expect(localizedFromIso('2017-09-18')).toBe('Lunes, 18 De Septiembre')
46 | expect(localizedFromIso('2017-11-04')).toBe('Sábado, 4 De Noviembre')
47 | })
48 | })
49 | })
50 |
--------------------------------------------------------------------------------
/src/app/pages/Error404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Oops. Nothing here...
6 |
7 |
14 | Go back
15 |
16 |
22 | Go home
23 |
24 |
25 |
26 |
27 |
28 |
29 |
44 |
45 |
63 |
--------------------------------------------------------------------------------
/src/app/shared-components/__test__/FormButton.spec.js:
--------------------------------------------------------------------------------
1 | import FormButton from '@/app/shared-components/FormButton.vue'
2 | import { renderComponent } from '@test/render-utils'
3 | import userEvent from '@testing-library/user-event'
4 |
5 | describe('FormButton.vue', () => {
6 |
7 | const SLOT_CONTENT = 'click me'
8 |
9 | it('renders label', async () => {
10 |
11 | const {findByText} = renderComponent(FormButton, {
12 | props: {isLoading: false, disabled: false},
13 | slots: {default: SLOT_CONTENT}
14 | })
15 |
16 | expect(await findByText(SLOT_CONTENT)).toBeInTheDocument()
17 | })
18 |
19 | describe('When clicking', () => {
20 |
21 | it('calls callback if enabled', async () => {
22 | const clickSpy = jest.fn()
23 | const {findByText} = renderComponent(FormButton, {
24 | props: {isLoading: false, disabled: false, onClick: clickSpy},
25 | slots: {default: SLOT_CONTENT}
26 | })
27 |
28 | userEvent.click(await findByText(SLOT_CONTENT))
29 |
30 | expect(clickSpy).toHaveBeenCalled()
31 | })
32 |
33 | it('does not call callback if disabled', async () => {
34 | const clickSpy = jest.fn()
35 | const {findByText} = renderComponent(FormButton, {
36 | props: {isLoading: false, disabled: true, onClick: clickSpy},
37 | slots: {default: SLOT_CONTENT}
38 | })
39 |
40 | userEvent.click(await findByText(SLOT_CONTENT))
41 |
42 | expect(clickSpy).not.toHaveBeenCalled()
43 | })
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/test/helpers.js:
--------------------------------------------------------------------------------
1 | import VueRouter from 'vue-router'
2 | import { mount as _mount, shallow as _shallow } from '@vue/test-utils'
3 |
4 | export function resolvedPromise(promiseResult) {
5 | return jest.fn(() => Promise.resolve(promiseResult))
6 | }
7 |
8 | export function rejectedPromise(promiseError) {
9 | return jest.fn(() => Promise.reject(promiseError))
10 | }
11 |
12 | export function stubNow(isoDate) {
13 | Date.now = jest.fn(() => new Date(isoDate).valueOf())
14 | }
15 |
16 | export function stubDate(isoDate) {
17 | Date.now = jest.fn(() => new Date(isoDate).valueOf())
18 | }
19 |
20 | export function Wrap(component) {
21 |
22 | return {mount, shallow, withProps, withSlots, withRouter, withStore, config}
23 |
24 | function withProps(props) {
25 | this.props = props
26 | return this
27 | }
28 |
29 | function withStore(store) {
30 | this.store = store
31 | this.router = new VueRouter()
32 | sync(this.store, this.router)
33 | return this
34 | }
35 |
36 | function withRouter(router) {
37 | this.router = router
38 | return this
39 | }
40 |
41 | function withSlots(slots) {
42 | this.slots = slots
43 | return this
44 | }
45 |
46 | function mount() {
47 | return _mount(component, this.config())
48 | }
49 |
50 | function shallow() {
51 | return _shallow(component, this.config())
52 | }
53 |
54 | function config() {
55 | return { propsData: this.props, slots: this.slots, router: this.router, store: this.store }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/app/shared-components/TextInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
12 | {{validationMessages[key]}}
13 |
14 |
15 |
16 |
17 |
18 |
55 |
60 |
--------------------------------------------------------------------------------
/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter from 'vue-router'
3 |
4 | Vue.use(VueRouter)
5 |
6 | function load (component) {
7 | // '@' is aliased to src/,
8 | // We will use lady loading when the app needs it
9 | // for now is not worthy to add code splitting
10 | return () => import(`@/app/pages/${component}.vue`)
11 | }
12 | import Days from '@/app/pages/Days/Days.vue'
13 | import NewGig from '@/app/pages/NewGig/NewGig'
14 | import GigDetail from '@/app/pages/GigDetail/GigDetail'
15 |
16 | export const NEW_GIG_PATH = '/newGig'
17 | export const ALL_GIGS_PATH = '/all'
18 |
19 | const router = new VueRouter({
20 | /*
21 | * NOTE! VueRouter "history" mode DOESN'T works for Cordova builds,
22 | * it is only to be used only for websites.
23 | *
24 | * If you decide to go with "history" mode, please also open /config/index.js
25 | * and set "build.publicPath" to something other than an empty string.
26 | * Example: '/' instead of current ''
27 | *
28 | * If switching back to default "hash" mode, don't forget to set the
29 | * build publicPath back to '' so Cordova builds work again.
30 | */
31 | // mode: 'history',
32 | routes:
33 | [
34 | { path: '/', component: Days, title: 'root' },
35 | { path: ALL_GIGS_PATH, component: Days, title: 'all' },
36 | { path: '/gig/:id', component: GigDetail, title: 'gig' },
37 | { path: NEW_GIG_PATH, component: NewGig, title: 'newGig' },
38 | { path: '*', component: load('Error404') }
39 | ],
40 | scrollBehavior (to, from, savedPosition) {
41 | return { x: 0, y: 0 }
42 | }
43 | })
44 |
45 | export default router
46 |
--------------------------------------------------------------------------------
/src/app/shared-components/DateTimeInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
16 | {{validationMessages[key]}}
17 |
18 |
19 |
20 |
21 |
58 |
63 |
--------------------------------------------------------------------------------
/src/app/shared-components/SideBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ¡A la jota jotaaa!
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Admin
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
49 |
50 |
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JotasJS Basic
2 |
3 | > Demo project for the CodelyTV Pro video course `Building an app with VueJS and Jest using TDD` you may find in: [Crea una app con VueJS y Jest aplicando TDD](https://pro.codely.tv/library/crea-una-app-con-vuejs-y-jest-aplicando-tdd/65211/path/)
4 |
5 | _Note that although all the videos and course contents are in Spanish, this repo is only available in English._
6 |
7 | ## 🦎🦎🦎🦎🦎🦎🦎🦎🦎🦎 DISCLAIMER (Jan 2021) 🦎🦎🦎🦎🦎
8 |
9 | This project is very old and tests were a bit outdated 😓
10 |
11 | We just migrated every test to Vue Testing Library and set `testing-library` as the default branch 🔥
12 |
13 | (You can always jump to `jotas` branch if you want to check the old fashioned tests).
14 |
15 | ## Getting Started!
16 |
17 | ``` bash
18 | # install dependencies
19 | $ yarn install
20 | or
21 | $ npm install
22 |
23 | # run tests
24 | $ yarn unit
25 | or
26 | $ npm run unit
27 |
28 | # run tests in watch mode
29 | $ yarn unit:watch
30 | or
31 | $ npm run unit:watch
32 |
33 | # run tests in with coverage
34 | $ yarn unit:coverage
35 | or
36 | $ npm run unit:coverage
37 |
38 | # serve with hot reload at localhost:8080
39 | $ yarn dev mat
40 | or
41 | $ npm run dev mat
42 |
43 |
44 | # 'mat' was the material theme, try iOS with
45 | $ yarn dev ios
46 | or
47 | $ npm run dev ios
48 |
49 | # build for production with minification
50 | $ quasar build
51 | ```
52 |
53 | ## Going Mobile!
54 |
55 | You may want to wrap the App into a native mobile App. Given you already have Cordova and an Android or iOS SDK installed in your system, run:
56 |
57 |
58 | ```
59 | quasar wrap cordova
60 | cordova platform add android
61 | cordova run android
62 | ```
63 |
64 | For full details, take a look to the Quasar [guide](http://quasar-framework.org/guide/cordova-wrapper.html).
65 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 |
3 | module.exports = {
4 | // Webpack aliases
5 | aliases: {
6 | quasar: path.resolve(__dirname, '../node_modules/quasar-framework/'),
7 | src: path.resolve(__dirname, '../src'),
8 | assets: path.resolve(__dirname, '../src/assets'),
9 | '@': path.resolve(__dirname, '../src'),
10 | variables: path.resolve(__dirname, '../src/themes/quasar.variables.styl')
11 | },
12 |
13 | // Progress Bar Webpack plugin format
14 | // https://github.com/clessg/progress-bar-webpack-plugin#options
15 | progressFormat: ' [:bar] ' + ':percent'.bold + ' (:msg)',
16 |
17 | // Default theme to build with ('ios' or 'mat')
18 | defaultTheme: 'mat',
19 |
20 | build: {
21 | env: require('./prod.env'),
22 | publicPath: '',
23 | productionSourceMap: false,
24 |
25 | // Remove unused CSS
26 | // Disable it if it has side-effects for your specific app
27 | purifyCSS: true
28 | },
29 | dev: {
30 | env: require('./dev.env'),
31 | cssSourceMap: true,
32 | // auto open browser or not
33 | openBrowser: true,
34 | publicPath: '/',
35 | port: 8080,
36 |
37 | // If for example you are using Quasar Play
38 | // to generate a QR code then on each dev (re)compilation
39 | // you need to avoid clearing out the console, so set this
40 | // to "false", otherwise you can set it to "true" to always
41 | // have only the messages regarding your last (re)compilation.
42 | clearConsoleOnRebuild: false,
43 |
44 | // Proxy your API if using any.
45 | // Also see /build/script.dev.js and search for "proxy api requests"
46 | // https://github.com/chimurai/http-proxy-middleware
47 | proxyTable: {}
48 | }
49 | }
50 |
51 | /*
52 | * proxyTable example:
53 | *
54 | proxyTable: {
55 | // proxy all requests starting with /api
56 | '/api': {
57 | target: 'https://some.address.com/api',
58 | changeOrigin: true,
59 | pathRewrite: {
60 | '^/api': ''
61 | }
62 | }
63 | }
64 | */
65 |
--------------------------------------------------------------------------------
/src/app/pages/NewGig/__test__/codely/TestingSubmit.spec.js:
--------------------------------------------------------------------------------
1 | import NewGig from '@/app/pages/NewGig/NewGig.vue'
2 | import { createGig as createGigSpy } from '@/app/services/jota-api'
3 | import { createGigPayload } from '@/app/services/jota-payloads'
4 | import { renderComponent, tomorrow, tomorrowDayOfMonth } from '@test/render-utils'
5 | import userEvent from '@testing-library/user-event'
6 | jest.mock('@/app/services/jota-api')
7 |
8 | describe('When clicking save button', () => {
9 | it('calls backend with appropriate command', async () => {
10 |
11 | const {typeGigName, setGigDate, findCreateGigButton} = await renderNewGig()
12 |
13 | typeGigName(nameWithValidLength())
14 | await setGigDate(tomorrowDayOfMonth())
15 |
16 | userEvent.click(await findCreateGigButton())
17 | expect(createGigSpy).toHaveBeenCalledWith(createGigPayload(nameWithValidLength(), tomorrow().toISOString()))
18 | })
19 | })
20 |
21 | async function renderNewGig() {
22 | const {screen} = renderComponent(NewGig)
23 |
24 | //it would be much better to use a label but for now q-input does not bind label with input
25 | // (we should modify q-input to force that binding or maybe using aria-label as a workaround)
26 | const nameInput = (await screen.findAllByRole('textbox'))[0]
27 |
28 | const typeGigName = (name)=> {
29 | userEvent.type(nameInput, name)
30 | }
31 |
32 | const setGigDate = async (dayText) => {
33 | userEvent.click(await screen.findByText(/Date and time/i))
34 | userEvent.click(await screen.findByText(dayText))
35 | userEvent.click(await screen.findByText(/set/i))
36 | //Wait for date set and rendered
37 | await screen.findByText(/\//i)
38 | }
39 |
40 | const findCreateGigButton = async ()=> (await screen.findByText(/Create Gig/i)).closest('button')
41 |
42 | return {screen, typeGigName, setGigDate, findCreateGigButton}
43 | }
44 |
45 | function nameWithValidLength() {
46 | return nameWithLength(5)
47 | }
48 |
49 | function nameWithLength(length) {
50 | return 'x'.repeat(length)
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/services/__mocks__/gigs-sample.js:
--------------------------------------------------------------------------------
1 | export const fakeGigsByDay =
2 | {
3 | '2017-09-18': {
4 | 'date': '2017-09-18',
5 | 'gigs': [
6 | {
7 | 'id': '123456',
8 | 'title': 'Anarchy in the Jota S',
9 | 'lat_lng': '41.641851935961654,-0.8751129897638315',
10 | 'address': 'BCN',
11 | 'description': 'Coding Stones cantando Joticas',
12 | 'price': '',
13 | 'image_url': 'http://www.zaragoza.es/cont/paginas/actividades/imagen/web_320x480px.png',
14 | 'place': 'SCBCN',
15 | 'slug': 'stones',
16 | 'schedule_id': 5280,
17 | 'hour': '19:30',
18 | 'day': '2017-09-18',
19 | 'schedule': [
20 | {
21 | 'id': 5280,
22 | 'day': '2017-09-18',
23 | 'hour': '19:30',
24 | 'gig_id': 5927,
25 | 'created_at': '2017-09-07T15:57:35.419Z',
26 | 'updated_at': '2017-09-07T15:57:35.419Z'
27 | }
28 | ]
29 | },
30 | {
31 | 'id': '2222222',
32 | 'title': 'Jotas de Codely TV',
33 | 'lat_lng': '41.641851935961654,-0.8751129897638315',
34 | 'address': 'BCN',
35 | 'description': 'Javi y Rafa cantando jotas',
36 | 'price': '',
37 | 'image_url': 'http://www.wtf.es/wtf.png',
38 | 'affiliate_url': null,
39 | 'place': 'Around the world',
40 | 'slug': 'stones',
41 | 'schedule_id': 5280,
42 | 'hour': '19:30',
43 | 'day': '2017-09-18',
44 | 'schedule': [
45 | {
46 | 'id': 5280,
47 | 'day': '2017-09-18',
48 | 'hour': '19:30',
49 | 'gig_id': 5927,
50 | 'created_at': '2017-09-07T15:57:35.419Z',
51 | 'updated_at': '2017-09-07T15:57:35.419Z'
52 | }
53 | ]
54 | }
55 | ]
56 | }
57 | }
58 |
59 | export const FIRST_DAY = fakeGigsByDay[Object.keys(fakeGigsByDay)[0]]
60 | export const FIRST_GIG = FIRST_DAY.gigs[0]
61 | export const DAY_LIST = Object.values(fakeGigsByDay)
62 |
--------------------------------------------------------------------------------
/src/app/pages/NewGig/__test__/codely/TestingValidations.spec.js:
--------------------------------------------------------------------------------
1 | import NewGig from '@/app/pages/NewGig/NewGig.vue'
2 | import DateTimeInput from '@/app/shared-components/DateTimeInput.vue'
3 | import { renderComponent, yesterdayDayOfMonth } from '@test/render-utils'
4 | import userEvent from '@testing-library/user-event'
5 | import Vue from 'vue'
6 |
7 | jest.mock('@/app/services/jota-api')
8 |
9 | describe('shows validation error', () => {
10 |
11 | it('when title is too short', async() => {
12 | const {typeGigName, screen} = await renderNewGig()
13 |
14 | typeGigName(tooShortName())
15 |
16 | expect(await screen.findByText('Minimum 5 characters.')).toBeInTheDocument()
17 | })
18 |
19 | it('when datetime is in the past', async () => {
20 |
21 | const {screen, setGigDate} = await renderNewGig()
22 |
23 | await setGigDate(yesterdayDayOfMonth())
24 |
25 | expect(await screen.findByText('You cannot set a gig in a past date :(')).toBeInTheDocument()
26 | })
27 | })
28 |
29 | async function renderNewGig() {
30 | const {screen} = renderComponent(NewGig)
31 |
32 | //it would be much better to use a label but for now q-input does not bind label with input
33 | // (we should modify q-input to force that binding or maybe using aria-label as a workaround)
34 | const nameInput = (await screen.findAllByRole('textbox'))[0]
35 |
36 | const typeGigName = (name)=> {
37 | userEvent.type(nameInput, name)
38 | }
39 |
40 | const setGigDate = async (dayText) => {
41 | userEvent.click(await screen.findByText(/Date and time/i))
42 | userEvent.click(await screen.findByText(dayText))
43 | userEvent.click(await screen.findByText(/set/i))
44 | //Wait for date set and rendered
45 | await screen.findByText(/\//i)
46 | }
47 |
48 | const findCreateGigButton = async ()=> (await screen.findByText(/Create Gig/i)).closest('button')
49 |
50 | return {screen, typeGigName, setGigDate, findCreateGigButton}
51 | }
52 |
53 | function tooShortName() {
54 | return nameWithLength(3)
55 | }
56 |
57 | function nameWithLength(length) {
58 | return 'x'.repeat(length)
59 | }
60 |
61 | function wait() {
62 | return Vue.nextTick()
63 | }
64 |
--------------------------------------------------------------------------------
/src/app/fake-backend/JotaCore.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable eqeqeq */
2 | import { createFakeDays } from '../services/__mocks__/create-fake-gig'
3 |
4 | export class Gig {
5 | constructor(args) {
6 | Object.assign(this, args)
7 | }
8 | }
9 |
10 | export class GigService {
11 | constructor(matcher) {
12 | this._gigs = []
13 | this._gigs_by_day = []
14 | this._matcher = matcher
15 | }
16 |
17 | retrieveNextGigs() {
18 | const days = createFakeDays()
19 | Object.keys(days).forEach((date) => this._gigs.push(...days[date].gigs))
20 | return days
21 | }
22 |
23 | retrieveAGig(id) {
24 | return new Promise((resolve, reject) => {
25 | let gig = this._gigs.find((gig) => { return (gig.id == parseInt(id)) })
26 |
27 | if (gig) {
28 | resolve(gig)
29 | }
30 | else {
31 | throw Error('Gig not found')
32 | }
33 | })
34 | }
35 |
36 | searchGigsGroupedByDay(term) {
37 | return new Promise((resolve, reject) => {
38 | const daysWithGigs = []
39 | this._gigs_by_day.forEach((day) => {
40 | const gigs = day.gigs
41 | .filter((gig) => {
42 | return this._gigIsMatching(gig, term)
43 | }).map((gig) => {
44 | return new Gig(gig)
45 | })
46 |
47 | if (gigs.length > 0) {
48 | daysWithGigs.push({day: day.day, gigs: gigs})
49 | }
50 | })
51 | resolve(daysWithGigs)
52 | })
53 | }
54 |
55 | searchGigs(term) {
56 | return new Promise((resolve, reject) => {
57 | let matches = []
58 | this._gigs.forEach((gig) => {
59 | if (this._gigIsMatching(gig, term)) {
60 | matches.push(new Gig(gig))
61 | }
62 | })
63 | resolve(matches)
64 | })
65 | }
66 |
67 | _gigIsMatching(gig, term) {
68 | return this._matcher.hasTheTerm(gig.title, term) || this._matcher.hasTheTerm(gig.place, term)
69 | }
70 | }
71 |
72 | export class Matcher {
73 | constructor() {
74 | this.FROM = 'ÃÀÁÄÂÈÉËÊÌÍÏÎÒÓÖÔÙÚÜÛ'
75 | this.TO = 'AAAAAEEEEIIIIOOOOUUUU'
76 | }
77 |
78 | hasTheTerm(text, term) {
79 | if (text) {
80 | text = this.normalize(text.toUpperCase())
81 | term = this.normalize(term.toUpperCase())
82 | return (text.indexOf(term) > -1)
83 | }
84 | }
85 |
86 | normalize(aString) {
87 | this.FROM.split('').forEach((changeFrom, index) => {
88 | const changeTo = this.TO[index]
89 | aString = aString.replace(changeFrom, changeTo)
90 | })
91 | return aString
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/app/pages/NewGig/NewGig.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Add a New Gig to Jota JS!
5 |
6 |
7 |
YOYOYOYOYOYO: {{$v}}
8 |
9 |
16 |
24 |
25 |
27 | Create Gig
28 |
29 |
30 |
31 |
32 |
33 |
86 |
--------------------------------------------------------------------------------
/src/app/pages/Days/__test__/Days.spec.js:
--------------------------------------------------------------------------------
1 | import Days from '@/app/pages/Days/Days.vue'
2 | import { FIRST_DAY, DAY_LIST } from '../../../services/__mocks__/gigs-sample'
3 | import { localizedFromIso } from '../../../services/date-utils'
4 | import {within} from '@testing-library/vue'
5 | import { renderComponent } from '@test/render-utils'
6 | import userEvent from '@testing-library/user-event'
7 | jest.mock('@/app/services/jota-api')
8 |
9 | describe('Days', () => {
10 | const FIRST_DAY_GIG_TITLES = FIRST_DAY.gigs.map(gig => gig.title)
11 |
12 | it('renders all gigs in the first day', async() => {
13 | const screen = renderComponent(Days)
14 |
15 | FIRST_DAY_GIG_TITLES.forEach(async text => {
16 | expect(await screen.findByText(text)).toBeInTheDocument()
17 | })
18 | })
19 |
20 | describe('When clicking buttons', () => {
21 |
22 | it('navigates to first gig detail', async () => {
23 | const FIRST_GIG = FIRST_DAY.gigs[0]
24 | const navigateToGigSpy = jest.fn()
25 | const screen = await renderDays({navigateToGig: navigateToGigSpy})
26 |
27 | await screen.openGig(FIRST_GIG.title)
28 |
29 | expect(navigateToGigSpy).toHaveBeenCalledWith(FIRST_GIG.id)
30 | })
31 |
32 | it('navigates to second gig detail', async () => {
33 | const SECOND_GIG = FIRST_DAY.gigs[1]
34 | const navigateToGigSpy = jest.fn()
35 | const screen = await renderDays({navigateToGig: navigateToGigSpy})
36 |
37 | await screen.openGig(SECOND_GIG.title)
38 |
39 | expect(navigateToGigSpy).toHaveBeenCalledWith(SECOND_GIG.id)
40 | })
41 | })
42 |
43 | // /* Different examples of more accurate tests that need
44 | // to explicitly run over the DOM structure
45 | // */
46 | it('render days in localized format', async () => {
47 | const screen = await renderDays()
48 | DAY_LIST.forEach(async (day) => {
49 | expect(await screen.findByText(localizedFromIso(day.date))).toBeInTheDocument()
50 | })
51 | })
52 |
53 | it('render gigs for each day', async () => {
54 | const screen = await renderDays()
55 | DAY_LIST.forEach(day => {
56 | const gigTitlesInDay = day.gigs.map((gig) => gig.title + ' ' + gig.place)
57 | const dayElement = screen.getByText(localizedFromIso(day.date))
58 | gigTitlesInDay.forEach(async title => {
59 | expect(
60 | await within(dayElement).findByText(title)
61 | ).toBeInTheDocument()
62 | })
63 | })
64 | })
65 | })
66 |
67 | async function renderDays(jotaRouterInstance) {
68 | const screen = await renderComponent(Days, {
69 | jotaRouter: ()=> jotaRouterInstance
70 | })
71 | const openGig = async title => userEvent.click((await screen.findByText(title)))
72 |
73 | return {...screen, openGig}
74 | }
75 |
--------------------------------------------------------------------------------
/src/app/pages/GigDetail/GigDetail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 | {{gig.title}}
10 |
11 |
12 |
13 |
{{gig.price}}
14 |
15 |
16 | {{localized(gig.day)}} a las {{gig.hour}} h.
17 |
18 | Add it to your calendar
19 |
20 |
{{gig.place}}
21 |
22 |
23 | How to get there?
24 |
25 |
26 | {{gig.description}}
27 |
28 | Share it with your friends
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
75 |
76 |
91 |
--------------------------------------------------------------------------------
/test/render-utils.js:
--------------------------------------------------------------------------------
1 | import {prettyDOM, render} from '@testing-library/vue';
2 | import {createLocalVue} from '@vue/test-utils';
3 | import Vuex from 'vuex';
4 | import Vuelidate from 'vuelidate'
5 | import Quasar from 'quasar-framework'
6 | import { jotaRouterMixin } from '@/app/services/JotaRouter'
7 | import { registerGlobalComponents } from '@/app/GlobalComponentsLoader'
8 | import {screen} from '@testing-library/vue'
9 |
10 | // import Router from 'vue-router';
11 | // import {routes} from '@/router';
12 |
13 | export const sleep = (ms) => {
14 | return new Promise(resolve => setTimeout(resolve, ms))
15 | };
16 |
17 | export function debug(doc = document) {
18 | // Using global document when no param passed
19 | const maxLines = 100000
20 | console.log(prettyDOM(doc, maxLines))
21 | }
22 |
23 | export const resolvePromises = () =>
24 | new Promise((resolve) => setImmediate(resolve))
25 |
26 | const defaultRouter = {
27 | replace: jest.fn(),
28 | push: jest.fn()
29 | };
30 |
31 | export const buildDefaultRoute = () => ({
32 | query: {},
33 | name: 'mocked-route'
34 | });
35 |
36 | export const yesterdayDayOfMonth = () => {
37 | const date = new Date()
38 | date.setDate(date.getDate() - 1)
39 | return date.getDate().toString()
40 | }
41 |
42 | export const tomorrow = () => {
43 | const date = new Date()
44 | date.setDate(date.getDate() + 1)
45 | return date
46 | }
47 |
48 | //Acertijo
49 | export const tomorrowDayOfMonth = () => tomorrow().getDate().toString()
50 |
51 | export function renderComponent (component, setup={}) {
52 | const localVue = createLocalVue()
53 | const defaultSetup = {
54 | localVue,
55 | mocks: {
56 | $route: setup.route || buildDefaultRoute(),
57 | $router: setup.router || defaultRouter
58 | },
59 | // Stubbing some components to simplify test rendering
60 | stubs: {
61 | 'router-view': true,
62 | }
63 | }
64 |
65 | initializePlugins(localVue)
66 |
67 | const mixin = setup.jotaRouter ? buildMixinWithMockedJotaRouter(setup.jotaRouter) : jotaRouterMixin
68 | initializeJotaRouterMixin(localVue, mixin)
69 |
70 | const utils = render(component, {
71 | ...defaultSetup, ...setup,
72 | });
73 | return {...utils, debug, screen}
74 | }
75 |
76 | function initializePlugins(vueInstance) {
77 | vueInstance.use(Vuelidate)
78 | vueInstance.use(Quasar)
79 | // http://forum.quasar-framework.org/topic/278/turn-off-the-annoying-vue-production-tip/7
80 | vueInstance.config.productionTip = false
81 | vueInstance.use(Vuex)
82 | // vueInstance.use(VueRouter)
83 | registerGlobalComponents(vueInstance)
84 | }
85 |
86 | function initializeJotaRouterMixin(vueInstance, jotaRouterMixin) {
87 | vueInstance.mixin(jotaRouterMixin)
88 | }
89 |
90 | export function buildMixinWithMockedJotaRouter(jotaRouter) {
91 | return {
92 | created: function () {
93 | this.jotaRouter = jotaRouter({})
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/src/app/GlobalComponentsLoader.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import {
3 | QList,
4 | QListHeader,
5 | QItem,
6 | QBtn,
7 | QToolbar,
8 | QToolbarTitle,
9 | QInput,
10 | QField,
11 | QDatetime,
12 | QAjaxBar,
13 | QIcon,
14 | QCard,
15 | QCardSeparator,
16 | QCardMedia,
17 | QCardMain,
18 | QCardTitle,
19 | QTransition,
20 | QSideLink,
21 | QScrollArea,
22 | QItemSide,
23 | QItemMain,
24 | QItemTile,
25 | QItemSeparator,
26 | QLayout,
27 | QSpinner,
28 | QSpinnerMat,
29 | QFixedPosition
30 | } from 'quasar-framework'
31 | import LoadSpinner from '@/app/shared-components/LoadSpinner.vue'
32 | import FormButton from '@/app/shared-components/FormButton.vue'
33 | import BackToTopButton from '@/app/shared-components/BackToTopButton.vue'
34 | import SideBar from '@/app/shared-components/SideBar.vue'
35 | import Toolbar from '@/app/shared-components/Toolbar.vue'
36 | import GigRow from '@/app/pages/Days/GigRow.vue'
37 | import Day from '@/app/pages/Days/Day.vue'
38 | import TextInput from '@/app/shared-components/TextInput.vue'
39 | import DateTimeInput from '@/app/shared-components/DateTimeInput.vue'
40 |
41 | export function registerGlobalComponents(vueInstance = Vue) {
42 | vueInstance.component('LoadSpinner', LoadSpinner)
43 | vueInstance.component('Day', Day)
44 | vueInstance.component('GigRow', GigRow)
45 | vueInstance.component('QList', QList)
46 | vueInstance.component('QListHeader', QListHeader)
47 | vueInstance.component('QItem', QItem)
48 | vueInstance.component('QIcon', QIcon)
49 | vueInstance.component('QBtn', QBtn)
50 | vueInstance.component('QToolbar', QToolbar)
51 | vueInstance.component('QToolbarTitle', QToolbarTitle)
52 | vueInstance.component('QInput', QInput)
53 | vueInstance.component('QField', QField)
54 | vueInstance.component('QDatetime', QDatetime)
55 | vueInstance.component('QCard', QCard)
56 | vueInstance.component('QCardMedia', QCardMedia)
57 | vueInstance.component('QCardSeparator', QCardSeparator)
58 | vueInstance.component('QCardMain', QCardMain)
59 | vueInstance.component('QCardTitle', QCardTitle)
60 | vueInstance.component('QAjaxBar', QAjaxBar)
61 | vueInstance.component('QTransition', QTransition)
62 | vueInstance.component('QSideLink', QSideLink)
63 | vueInstance.component('QScrollArea', QScrollArea)
64 | vueInstance.component('QItemSide', QItemSide)
65 | vueInstance.component('QItemTile', QItemTile)
66 | vueInstance.component('QItemMain', QItemMain)
67 | vueInstance.component('QItemSeparator', QItemSeparator)
68 | vueInstance.component('QLayout', QLayout)
69 | vueInstance.component('QSpinner', QSpinner)
70 | vueInstance.component('QSpinnerMat', QSpinnerMat)
71 | vueInstance.component('QFixedPosition', QFixedPosition)
72 | vueInstance.component('FormButton', FormButton)
73 | vueInstance.component('BackToTopButton', BackToTopButton)
74 | vueInstance.component('Toolbar', Toolbar)
75 | vueInstance.component('TextInput', TextInput)
76 | vueInstance.component('DateTimeInput', DateTimeInput)
77 | vueInstance.component('SideBar', SideBar)
78 | }
79 |
--------------------------------------------------------------------------------
/src/app/services/__mocks__/create-fake-gig.js:
--------------------------------------------------------------------------------
1 | import faker from 'faker'
2 | import { isoFuture } from '../date-utils'
3 |
4 | let UUID = 0
5 |
6 | export function createFakeGig(date) {
7 | UUID += 1
8 | return {
9 | id: (UUID).toString(),
10 | title: randomTitle(),
11 | description: faker.lorem.paragraph(15),
12 | image_url: randomImageUrl(),
13 | lat_lng: `${faker.address.latitude()},${faker.address.longitude()}`,
14 | address: fakeAddress(),
15 | price: randomNumber(5, 60) + '€',
16 | place: randomPlace(),
17 | hour: randomNumber(4, 11) + 'PM',
18 | day: date
19 | }
20 | }
21 |
22 | function fakeAddress() {
23 | return faker.address.streetAddress() +
24 | '
' + faker.address.city() + ', ' + faker.address.country() +
25 | '
' + faker.address.zipCode()
26 | }
27 |
28 | function randomNumber(min, max) {
29 | return faker.random.number({min, max})
30 | }
31 |
32 | export function createFakeDay(date) {
33 | return {
34 | date: date,
35 | gigs: Array(randomNumber(1, 10)).fill().map(() => createFakeGig(date))
36 | }
37 | }
38 |
39 | export function createFakeDays() {
40 | let days = {}
41 | Array(randomNumber(3, 10)).fill().forEach((number, index) => {
42 | days[isoFuture(index)] = createFakeDay(isoFuture(index))
43 | })
44 | return days
45 | }
46 |
47 | function randomImageUrl() {
48 | return imageUrls[randomNumber(0, imageUrls.length - 1)]
49 | }
50 |
51 | const imageUrls = [
52 | 'https://i.pinimg.com/736x/0e/de/f8/0edef8dd970d352a95a8caeaa55aa81c--marina-dancing.jpg',
53 | 'https://i.pinimg.com/564x/95/83/8b/95838bc6e43c5229b7a677a78c70e09a.jpg',
54 | 'https://i.pinimg.com/564x/08/d5/dd/08d5ddd4bb0c8a59b7a4021ba87a38e8.jpg',
55 | 'https://i.pinimg.com/564x/31/c7/4e/31c74e89d905b300e3832b439ce7ffc0.jpg',
56 | 'https://i.pinimg.com/564x/97/0b/73/970b736b7d75062ab9a525ad5f3c3497.jpg',
57 | 'https://i.pinimg.com/564x/cf/b7/b6/cfb7b6fc2000dfdbb9f4aeb8800f7e87.jpg',
58 | 'https://i.pinimg.com/564x/59/bb/59/59bb5945d68e8d913b2130f3628949b9.jpg'
59 | ]
60 |
61 | function randomPlace() {
62 | return places[randomNumber(0, places.length - 1)]
63 | }
64 |
65 | const places = [
66 | 'Sala Razzmatazz',
67 | 'Codely Headquarters',
68 | 'Pabellón Principe Felipe',
69 | 'Estadio Vicente Calderón',
70 | 'Palacio Municipal de los Deportes Huesca',
71 | 'Joy Slava',
72 | 'Sala Z',
73 | 'Estadio de Wembley',
74 | 'CBGB New York',
75 | 'Teatro Principal de Zaragoza'
76 | ]
77 |
78 | function randomTitle() {
79 | return firstTerm[randomNumber(0, firstTerm.length - 1)] + ' ' +
80 | secondTerm[randomNumber(0, secondTerm.length - 1)] + ' ' +
81 | thirdTerm[randomNumber(0, thirdTerm.length - 1)]
82 | }
83 |
84 | const firstTerm = [
85 | 'Grupo', 'Rondalla', 'Agrupación', 'Amigos', 'Folklore', 'Colectivo', 'Banda', 'Ronda',
86 | 'Rondalla', 'Escuela', 'Dulzaineros'
87 | ]
88 |
89 | const secondTerm = [
90 | 'Folclore', 'Baluarte', 'Danzantes', 'Joteros', 'Jota', 'Baile', 'Bandurrias', 'Copla'
91 | ]
92 |
93 | const thirdTerm = [
94 | 'de Zaragoza', 'de Aragón', 'Turolense', 'Oscense', 'Fematera', 'Rondadera'
95 | ]
96 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "JotaQuasar",
3 | "productName": "Jota JS",
4 | "version": "0.0.1",
5 | "description": "Jota Sample with Quasar",
6 | "author": "Coding Stones",
7 | "scripts": {
8 | "clean": "node build/script.clean.js",
9 | "dev": "node build/script.dev.js",
10 | "build": "node build/script.build.js",
11 | "lint": "eslint --ext .js,.vue src",
12 | "unit": "jest --config=jest.config.js",
13 | "unit:watch": "jest --config=jest.config.js --watchAll",
14 | "unit:coverage": "jest --config=jest.config.js --coverage"
15 | },
16 | "dependencies": {
17 | "babel-runtime": "^6.25.0",
18 | "faker": "^4.1.0",
19 | "fetch-jsonp": "^1.1.3",
20 | "js-beautify": "^1.7.3",
21 | "moment": "^2.19.1",
22 | "quasar-extras": "0.x",
23 | "quasar-framework": "^0.14.6",
24 | "vue": "^2.5.2",
25 | "vue-router": "^3.0.1",
26 | "vuelidate": "^0.6.1",
27 | "vuex": "^3.0.0",
28 | "vuex-router-sync": "^5.0.0"
29 | },
30 | "devDependencies": {
31 | "@testing-library/jest-dom": "^5.11.9",
32 | "@testing-library/user-event": "^12.6.0",
33 | "@testing-library/vue": "^5.6.1",
34 | "@vue/test-utils": "^1.0.0-beta.26",
35 | "autoprefixer": "^6.4.0",
36 | "babel-core": "^6.0.0",
37 | "babel-eslint": "^7.0.0",
38 | "babel-jest": "^21.0.2",
39 | "babel-loader": "^7.0.0",
40 | "babel-plugin-dynamic-import-node": "^1.1.0",
41 | "babel-plugin-module-resolver": "^2.7.1",
42 | "babel-plugin-transform-runtime": "^6.0.0",
43 | "babel-preset-env": "^1.6.0",
44 | "babel-preset-es2015": "^6.0.0",
45 | "babel-preset-stage-2": "^6.0.0",
46 | "colors": "^1.1.2",
47 | "connect-history-api-fallback": "^1.1.0",
48 | "css-loader": "^0.28.0",
49 | "es6-promise": "^4.1.1",
50 | "eslint": "^4.7.2",
51 | "eslint-config-standard": "^10.2.1",
52 | "eslint-friendly-formatter": "^3.0.0",
53 | "eslint-loader": "^1.9.0",
54 | "eslint-plugin-html": "^3.1.1",
55 | "eslint-plugin-import": "^2.7.0",
56 | "eslint-plugin-node": "^5.1.1",
57 | "eslint-plugin-promise": "^3.5.0",
58 | "eslint-plugin-standard": "^3.0.1",
59 | "eventsource-polyfill": "^0.9.6",
60 | "express": "^4.15.5",
61 | "extract-text-webpack-plugin": "^3.0.0",
62 | "file-loader": "^0.11.1",
63 | "friendly-errors-webpack-plugin": "^1.1.3",
64 | "glob": "^7.1.2",
65 | "html-webpack-plugin": "^2.30.1",
66 | "http-proxy-middleware": "^0.17.0",
67 | "jest": "^26.6.3",
68 | "json-loader": "^0.5.7",
69 | "module-resolver": "^1.0.0",
70 | "opn": "^5.0.0",
71 | "optimize-css-assets-webpack-plugin": "^3.0.0",
72 | "postcss-loader": "^1.0.0",
73 | "progress-bar-webpack-plugin": "^1.10.0",
74 | "purify-css": "^1.2.6",
75 | "shelljs": "^0.7.0",
76 | "stylus": "^0.54.5",
77 | "stylus-loader": "^3.0.1",
78 | "synchronous-promise": "^1.0.17",
79 | "url-loader": "^0.5.7",
80 | "vue-jest": "^1.0.2",
81 | "vue-loader": "~13.3.0",
82 | "vue-server-renderer": "^2.5.2",
83 | "vue-style-loader": "^3.0.3",
84 | "vue-template-compiler": "^2.5.2",
85 | "webpack": "^3.5.2",
86 | "webpack-dev-middleware": "^1.12.0",
87 | "webpack-hot-middleware": "^2.18.2",
88 | "webpack-merge": "^4.1.0"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/app/pages/NewGig/__test__/NewGig.spec.js:
--------------------------------------------------------------------------------
1 | import NewGig from '@/app/pages/NewGig/NewGig.vue'
2 | import userEvent from '@testing-library/user-event'
3 | import { createGig as createGigSpy } from '../../../services/jota-api'
4 | jest.mock('@/app/services/jota-api')
5 | import { createGigPayload } from '../../../services/jota-payloads'
6 | import { renderComponent, sleep, tomorrow, tomorrowDayOfMonth, yesterdayDayOfMonth } from '@test/render-utils'
7 | import { fireEvent } from '@testing-library/vue'
8 |
9 | describe('shows validation error', () => {
10 |
11 | describe('when validating title', () => {
12 |
13 | xit('and title is cleared', async () => {
14 | const {typeGigName, clearGigName, findByText, findAllByRole, findByTestId} = await renderNewGig()
15 | typeGigName('a')
16 | clearGigName()
17 | sleep(1000)
18 | // typeGigName({keyCode: '46'})
19 | const input = (await findAllByRole('textbox'))[0]
20 |
21 | console.log('after clear', input)
22 | console.log('after clear value', input.value)
23 |
24 | // console.log(await (await findByTestId('debug')).innerHTML)
25 |
26 | expect(await findByText('Name is required')).toBeInTheDocument()
27 | })
28 |
29 | it('and title is too short', async() => {
30 | const {typeGigName, screen} = await renderNewGig()
31 |
32 | typeGigName(tooShortName())
33 |
34 | expect(await screen.findByText('Minimum 5 characters.')).toBeInTheDocument()
35 | })
36 |
37 | it('and title is too long', async () => {
38 | const {typeGigName, screen} = await renderNewGig()
39 |
40 | typeGigName(tooLongName())
41 |
42 | expect(await screen.findByText('Maximum 20 characters.')).toBeInTheDocument()
43 | })
44 | })
45 |
46 | describe('when validating datetime', () => {
47 | it('and datetime is cleared', async () => {
48 |
49 | const {screen, setGigDate, clearGigDate} = await renderNewGig()
50 |
51 | await setGigDate(tomorrowDayOfMonth())
52 |
53 | await clearGigDate()
54 |
55 | expect(await screen.findByText('Date and time of gig are required.')).toBeInTheDocument()
56 | })
57 |
58 | it('and datetime is in the past', async () => {
59 |
60 | const {screen, setGigDate} = await renderNewGig()
61 |
62 | await setGigDate(yesterdayDayOfMonth())
63 |
64 | expect(await screen.findByText('You cannot set a gig in a past date :(')).toBeInTheDocument()
65 | })
66 | })
67 | })
68 |
69 | describe('Create Gig button', () => {
70 | it('is disabled by default', async () => {
71 |
72 | const {findCreateGigButton} = await renderNewGig()
73 |
74 | expect(await findCreateGigButton()).toHaveClass('disabled')
75 | })
76 |
77 | it('is disabled when form not fully filled', async () => {
78 | const {typeGigName, findCreateGigButton} = await renderNewGig()
79 |
80 | typeGigName(nameWithValidLength())
81 |
82 | expect(await findCreateGigButton()).toHaveClass('disabled')
83 | })
84 |
85 | it('is disabled when form has errors', async () => {
86 | const {typeGigName, findCreateGigButton} = await renderNewGig()
87 |
88 | typeGigName(tooShortName())
89 |
90 | expect(await findCreateGigButton()).toHaveClass('disabled')
91 | })
92 |
93 | it('is enabled when form is fully filled without errors', async () => {
94 | const {typeGigName, setGigDate, findCreateGigButton} = await renderNewGig()
95 |
96 | typeGigName(nameWithValidLength())
97 | await setGigDate(tomorrowDayOfMonth())
98 |
99 | expect(await findCreateGigButton()).not.toHaveClass('disabled');
100 | })
101 | })
102 |
103 | describe('When clicking create gig button', () => {
104 | it('calls backend with appropriate command', async () => {
105 | const {typeGigName, setGigDate, findCreateGigButton} = await renderNewGig()
106 |
107 | typeGigName(nameWithValidLength())
108 | await setGigDate(tomorrowDayOfMonth())
109 |
110 | userEvent.click(await findCreateGigButton())
111 | expect(createGigSpy).toHaveBeenCalledWith(createGigPayload(nameWithValidLength(), tomorrow().toISOString()))
112 | })
113 | })
114 |
115 | async function renderNewGig() {
116 | const {screen} = renderComponent(NewGig)
117 |
118 | //it would be much better to use a label but for now q-input does not bind label with input
119 | // (we should modify q-input to force that binding or maybe using aria-label as a workaround)
120 | const nameInput = (await screen.findAllByRole('textbox'))[0]
121 |
122 | const typeGigName = (name)=> {
123 | userEvent.type(nameInput, name)
124 | }
125 |
126 | const clearGigName = () => {
127 | userEvent.clear(nameInput)
128 | fireEvent.blur(nameInput)
129 | }
130 |
131 | const setGigDate = async (dayText) => {
132 | userEvent.click(await screen.findByText(/Date and time/i))
133 | userEvent.click(await screen.findByText(dayText))
134 | userEvent.click(await screen.findByText(/set/i))
135 | //Wait for date set and rendered
136 | // await findByText(/\//i)
137 | await sleep(100)
138 | }
139 |
140 | const clearGigDate = async ()=> {
141 | userEvent.click(await screen.findByText(/\//i))
142 | userEvent.click(await screen.findByText(/clear/i))
143 | }
144 |
145 | const findCreateGigButton = async ()=> (await screen.findByText(/Create Gig/i)).closest('button')
146 |
147 | return {screen, typeGigName, clearGigName, setGigDate, clearGigDate, findCreateGigButton}
148 | }
149 |
150 | function nameWithValidLength() {
151 | return nameWithLength(5)
152 | }
153 |
154 | function tooShortName() {
155 | return nameWithLength(3)
156 | }
157 |
158 | function tooLongName() {
159 | return nameWithLength(21)
160 | }
161 |
162 | function nameWithLength(length) {
163 | return 'x'.repeat(length)
164 | }
165 |
--------------------------------------------------------------------------------
/src/assets/stones.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/src/assets/stones-small-red-white_letters.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------