diff --git a/client/public/index.html b/client/public/index.html new file mode 100644 index 0000000..f996e58 --- /dev/null +++ b/client/public/index.html @@ -0,0 +1,17 @@ + + + + + + + + Hetic VS. EEMI + + + +
+ + + diff --git a/client/public/robots.txt b/client/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/client/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/client/src/App.vue b/client/src/App.vue index d8ed3c9..43c2c1f 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -24,4 +24,19 @@ } } } + +h3 { + margin: 40px 0 0; +} +ul { + list-style-type: none; + padding: 0; +} +li { + display: inline-block; + margin: 0 10px; +} +a { + color: #42b983; +} diff --git a/client/src/assets/logo.png b/client/src/assets/logo.png deleted file mode 100644 index f3d2503..0000000 Binary files a/client/src/assets/logo.png and /dev/null differ diff --git a/client/src/components/Quiz.vue b/client/src/components/Quiz.vue new file mode 100644 index 0000000..6c164e4 --- /dev/null +++ b/client/src/components/Quiz.vue @@ -0,0 +1,53 @@ + + + \ No newline at end of file diff --git a/client/src/components/ScoreScreen.vue b/client/src/components/ScoreScreen.vue index 2259dd2..a6c995a 100644 --- a/client/src/components/ScoreScreen.vue +++ b/client/src/components/ScoreScreen.vue @@ -1,18 +1,23 @@ - - + \ No newline at end of file diff --git a/client/src/router.ts b/client/src/router.ts index 37eeef5..d318c83 100644 --- a/client/src/router.ts +++ b/client/src/router.ts @@ -1,6 +1,7 @@ import Vue from 'vue'; import Router from 'vue-router'; import Home from './views/Home.vue'; +import ScoreScreen from './views/ScoreScreen.vue'; Vue.use(Router); @@ -13,5 +14,14 @@ export default new Router({ name: 'home', component: Home, }, + { + path: '/end', + name: 'scoreScreen', + component: ScoreScreen, + }, + { + path: '*', + redirect: { name: 'home' }, + }, ], }); diff --git a/client/src/store/modules/questions.ts b/client/src/store/modules/questions.ts index dcec3e2..53c504b 100644 --- a/client/src/store/modules/questions.ts +++ b/client/src/store/modules/questions.ts @@ -21,15 +21,6 @@ IStoreQuestions, questions: [], }, getters: { - index(state): number { - return state.index; - }, - score(state): number { - return state.score; - }, - questions(state): IQuestion[] { - return state.questions; - }, currentQuestion(state): IQuestion | {} { return state.questions.length ? state.questions[state.index] : {}; }, diff --git a/client/src/store/modules/schools.ts b/client/src/store/modules/schools.ts index a66f508..b9be5b0 100644 --- a/client/src/store/modules/schools.ts +++ b/client/src/store/modules/schools.ts @@ -13,11 +13,7 @@ const schools: Module = { state: { schools: [], }, - getters: { - schools(state): ISchool[] { - return state.schools; - }, - }, + getters: {}, mutations: { setSchools(state, payload): ISchool[] { return (state.schools = payload); diff --git a/client/src/views/Home.vue b/client/src/views/Home.vue index 8dd76e4..19eab76 100644 --- a/client/src/views/Home.vue +++ b/client/src/views/Home.vue @@ -1,68 +1,17 @@ diff --git a/client/tests/e2e/specs/home.js b/client/tests/e2e/specs/home.js index c6c4c16..f0e6cd6 100644 --- a/client/tests/e2e/specs/home.js +++ b/client/tests/e2e/specs/home.js @@ -1,90 +1,132 @@ -describe("Home page", () => { - it("should have content displayed", () => { - cy.server({ status: 200 }); - cy.route("/schools", { +describe('Home page', () => { + describe('content', () => { + it('should display quizz component', () => { + cy.get('#quizz_input').should('be.visible'); + }); + + it('should display title', () => { + cy.get('h1#title').should('be.visible'); + cy.contains('h1#title', 'HETIC vs EEMI'); + }); + + it('should display progress', () => { + cy.get('span.progress').should('be.visible'); + cy.contains('span.progress', '1/2'); + }); + + it('should display choice text', () => { + cy.get('p').should('be.visible'); + cy.contains('p', 'Plus Éemien ou Héticien ?'); + }); + + it('should display question', () => { + cy.server(); + cy.route('/questions', { + questions: [{ text: 'test_cypress', answer: 2 }], + }); + + cy.get('h2#question_text').should('be.visible'); + cy.contains('h2#question_text', 'test_cypress'); + }); + + it('should display schools', () => { + cy.server(); + cy.route('/schools', { + schools: [ + { id: 1, name: 'school1' }, + { id: 2, name: 'school2' }, + { id: 3, name: 'school3' }, + ], + }); + + cy.get('.choice-btn').should('be.visible'); + cy.get('.choice-btn').should('have.length', 2); + cy.get('.choice-btn') + .first() + .should('have.text', 'school1'); + }); + }); + + it('should finish game with score 0', () => { + cy.server(); + cy.route('/schools', { schools: [ - { id: 1, name: "school1" }, - { id: 2, name: "school2" }, - { id: 3, name: "school3" } - ] + { id: 1, name: 'school1' }, + { id: 2, name: 'school2' }, + ], }); - cy.route("/questions", { - questions: [{ text: "test_cypress", answer: 2 }] + cy.route('/questions', { + questions: [ + { text: 'test_cypress', answer: 2 }, + { text: 'test_cypress', answer: 2 }, + ], }); - cy.visit("/"); + cy.visit('/'); - // Elements are visible - cy.get("h1#title").should("be.visible"); - cy.get("h2#question_text").should("be.visible"); - cy.get("span.progress").should("be.visible"); - cy.get("p").should("be.visible"); - cy.get(".choice-btn").should("be.visible"); - cy.get("#quizz_input").should("be.visible"); - cy.get("#score_screen").should("not.be.visible"); + cy.contains('#question_text', 'test_cypress'); - // Elements contain right content - cy.contains("#title", "HETIC vs EEMI"); - cy.get("h2#question_text").should("not.be.empty"); - cy.contains("p", "Plus Éemien ou Héticien ?"); - cy.get(".choice-btn").should("have.length", 3); - cy.get(".choice-btn") - .first() - .should("have.text", "school1"); - }); - - it("should finish game with score 0", () => { - cy.server({ status: 200 }); - cy.route("/schools", { - schools: [{ id: 1, name: "school1" }, { id: 2, name: "school2" }] - }); - cy.route("/questions", { - questions: [{ text: "test_cypress", answer: 2 }] - }); - - cy.visit("/"); - - cy.contains("#question_text", "test_cypress"); - - cy.get(".choice-btn") + cy.get('.choice-btn') .first() .click(); - cy.contains("Score: 0/1"); + cy.get('.choice-btn') + .first() + .click(); + + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/end'); + }); + + cy.contains('Score: 0/2'); cy.contains("T'as pas lu les questions avoues."); - cy.contains("Recommencer"); + cy.contains('Recommencer'); - cy.get("#quizz_input").should("not.be.visible"); // Quizz is hidden - cy.get("#score_screen").should("be.visible"); // Score screen is displayed + cy.get('#quizz_input').should('not.be.visible'); // Quizz is hidden + cy.get('#score_screen').should('be.visible'); // Score screen is displayed }); - it("should finish game with score 1 and retry", () => { - cy.server({ status: 200 }); - cy.route("/schools", { - schools: [{ id: 1, name: "school1" }, { id: 2, name: "school2" }] + it('should finish game with score 1 and retry', () => { + cy.server(); + cy.route('/schools', { + schools: [ + { id: 1, name: 'school1' }, + { id: 2, name: 'school2' }, + ], }); - cy.route("/questions", { - questions: [{ text: "test_cypress", answer: 1 }] + cy.route('/questions', { + questions: [ + { text: 'test_cypress', answer: 1 }, + { text: 'test_cypress', answer: 1 }, + ], }); - cy.visit("/"); + cy.visit('/'); - cy.contains("#question_text", "test_cypress"); + cy.contains('#question_text', 'test_cypress'); - cy.get(".choice-btn") + cy.get('.choice-btn') .first() .click(); - cy.contains("Score: 1/1"); + cy.get('.choice-btn') + .first() + .click(); + + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/end'); + }); + + cy.contains('Score: 2/2'); cy.contains("Bon bah c'est pas tip top tout ça."); - cy.contains("Recommencer"); + cy.contains('Recommencer'); - cy.get("#quizz_input").should("not.be.visible"); // Quizz is hidden - cy.get("#score_screen").should("be.visible"); // Score screen is displayed + cy.get('#quizz_input').should('not.be.visible'); // Quizz is hidden + cy.get('#score_screen').should('be.visible'); // Score screen is displayed - cy.get(".replay-btn").click(); + cy.get('.replay-btn').click(); - cy.get("#quizz_input").should("be.visible"); // Quizz is visible - cy.get("#score_screen").should("not.be.visible"); // Score screen is hidden + cy.get('#quizz_input').should('be.visible'); // Quizz is visible + cy.get('#score_screen').should('not.be.visible'); // Score screen is hidden }); }); diff --git a/client/tests/unit/components/Quiz.spec.ts b/client/tests/unit/components/Quiz.spec.ts new file mode 100644 index 0000000..03793ff --- /dev/null +++ b/client/tests/unit/components/Quiz.spec.ts @@ -0,0 +1,124 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import Vuex from 'vuex'; +import Quiz from '../../../src/components/Quiz.vue'; +import router from '@/router'; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +const checkAnswerMock = jest.fn(); + +let isLastQuestionMock = false; +let $store: any; +let wrapper: any; + +describe('Component - Quiz', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + + $store = new Vuex.Store({ + modules: { + questions: { + namespaced: true, + state: { + index: 0, + score: 0, + questions: [{ answer: 1, text: 'test' }], + }, + getters: { + currentQuestion: () => ({ + text: 'currentQuestion', + }), + checkAnswer: () => checkAnswerMock, + isLastQuestion: () => isLastQuestionMock, + }, + }, + schools: { + namespaced: true, + state: { + schools: [{ id: 1, name: 'school1' }], + }, + getters: {}, + }, + }, + }); + + wrapper = shallowMount(Quiz, { + mocks: { $store }, + }); + }); + + it('should display question', () => { + expect(wrapper.find('#question_text').text()).toBe('currentQuestion'); + }); + + it('should display question', () => { + expect(wrapper.find('span.progress').text()).toBe('(1/1)'); + }); + + it('should only increase index', () => { + const dispatchMock = spyOn($store, 'dispatch'); + const routerMock = spyOn(router, 'push'); + + checkAnswerMock.mockImplementation(() => false); + isLastQuestionMock = false; + + wrapper + .findAll('.choice-btn') + .at(0) + .trigger('click'); + + expect(checkAnswerMock).toBeCalledTimes(1); + expect(checkAnswerMock).toBeCalledWith({ answer: 1 }); + + expect(dispatchMock).toBeCalledTimes(1); + expect(dispatchMock).toBeCalledWith('questions/increaseIndex'); + + expect(routerMock).toBeCalledTimes(0); + }); + + it('should increase score and index', () => { + const dispatchMock = spyOn($store, 'dispatch'); + const routerMock = spyOn(router, 'push'); + + checkAnswerMock.mockImplementation(() => true); + isLastQuestionMock = false; + + wrapper + .findAll('.choice-btn') + .at(0) + .trigger('click'); + + expect(checkAnswerMock).toBeCalledTimes(1); + expect(checkAnswerMock).toBeCalledWith({ answer: 1 }); + + expect(dispatchMock).toBeCalledTimes(2); + expect(dispatchMock).nthCalledWith(1, 'questions/increaseScore'); + expect(dispatchMock).nthCalledWith(2, 'questions/increaseIndex'); + + expect(routerMock).toBeCalledTimes(0); + }); + + it('should to score screen', () => { + const dispatchMock = spyOn($store, 'dispatch'); + const routerMock = spyOn(router, 'push'); + + checkAnswerMock.mockImplementation(() => false); + isLastQuestionMock = true; + + wrapper + .findAll('.choice-btn') + .at(0) + .trigger('click'); + + expect(checkAnswerMock).toBeCalledTimes(1); + expect(checkAnswerMock).toBeCalledWith({ answer: 1 }); + + expect(dispatchMock).toBeCalledTimes(0); + + expect(routerMock).toBeCalledTimes(1); + expect(routerMock).toBeCalledWith({ name: 'scoreScreen' }); + }); +}); diff --git a/client/tests/unit/components/ScoreScreen.spec.ts b/client/tests/unit/components/ScoreScreen.spec.ts new file mode 100644 index 0000000..423d72f --- /dev/null +++ b/client/tests/unit/components/ScoreScreen.spec.ts @@ -0,0 +1,55 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import Vuex from 'vuex'; +import ScoreScreen from '../../../src/components/ScoreScreen.vue'; +import router from '@/router'; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +let $store: any; + +let wrapper: any; + +describe('Component - ScoreScreen', () => { + beforeEach(() => { + $store = new Vuex.Store({ + modules: { + questions: { + namespaced: true, + state: { + index: 0, + score: 0, + questions: [], + }, + }, + }, + }); + wrapper = shallowMount(ScoreScreen, { + mocks: { $store }, + propsData: { + score: 2, + count: 5, + }, + }); + }); + + it('should display message based on score', () => { + expect(wrapper.find('h3').text()).toBe( + 'Bon bah c\'est pas tip top tout ça.', + ); + }); + + it('should call reset state function', () => { + const dispatchMock = spyOn($store, 'dispatch'); + const routerMock = spyOn(router, 'push'); + + wrapper.find('button.replay-btn').trigger('click'); + + expect(dispatchMock).toBeCalledTimes(1); + expect(dispatchMock).toBeCalledWith('questions/resetState'); + + expect(routerMock).toBeCalledTimes(1); + expect(routerMock).toBeCalledWith({ name: 'home' }); + }); +}); diff --git a/client/tests/unit/store/questions.spec.ts b/client/tests/unit/store/questions.spec.ts index d084bd9..e269342 100644 --- a/client/tests/unit/store/questions.spec.ts +++ b/client/tests/unit/store/questions.spec.ts @@ -21,36 +21,6 @@ describe('Store - Questions', () => { }); describe('Getters', () => { - describe('#index', () => { - it('should get index', () => { - state.index = 5; - - const index = getters.index(state, null, null, null); - - expect(index).toEqual(5); - }); - }); - - describe('#score', () => { - it('should get score', () => { - state.score = 5; - - const score = getters.score(state, null, null, null); - - expect(score).toEqual(5); - }); - }); - - describe('#questions', () => { - it('should get questions', () => { - state.questions = [{ text: 'test', answer: 1 }]; - - const data = getters.questions(state, null, null, null); - - expect(data).toStrictEqual(state.questions); - }); - }); - describe('#currentQuestion', () => { it('should get current question', () => { state.index = 1; @@ -77,7 +47,12 @@ describe('Store - Questions', () => { it('should check answer and get true', () => { state.questions = [{ text: 'test', answer: 1 }]; - const isCorrect = getters.checkAnswer(state, null, null, null)({ + const isCorrect = getters.checkAnswer( + state, + null, + null, + null, + )({ answer: 1, }); @@ -87,7 +62,12 @@ describe('Store - Questions', () => { it('should check answer and get false', () => { state.questions = [{ text: 'test', answer: 2 }]; - const isCorrect = getters.checkAnswer(state, null, null, null)({ + const isCorrect = getters.checkAnswer( + state, + null, + null, + null, + )({ answer: 1, }); diff --git a/client/tests/unit/store/schools.spec.ts b/client/tests/unit/store/schools.spec.ts index 8be088b..7f37786 100644 --- a/client/tests/unit/store/schools.spec.ts +++ b/client/tests/unit/store/schools.spec.ts @@ -16,19 +16,6 @@ describe('Store - Questions', () => { }; }); - describe('Getters', () => { - describe('#schools', () => { - it('should get schools', () => { - state.schools = [{ id: '1', name: 'test' }, { id: '2', name: 'test2' }]; - - const data = getters.schools(state, null, null, null); - - expect(data).toStrictEqual(state.schools); - expect(data.length).toBe(2); - }); - }); - }); - describe('Mutations', () => { describe('#setSchools', () => { it('should set schools state', () => { diff --git a/client/tests/unit/views/Home.spec.ts b/client/tests/unit/views/Home.spec.ts index 75883e8..15f0a39 100644 --- a/client/tests/unit/views/Home.spec.ts +++ b/client/tests/unit/views/Home.spec.ts @@ -4,7 +4,7 @@ import Home from '../../../src/views/Home.vue'; import axios from 'axios'; import config from '@/config'; import questions from '@/store/modules/questions'; -import schools from '@/store/modules/questions'; +import schools from '@/store/modules/schools'; const localVue = createLocalVue(); @@ -18,17 +18,13 @@ const $store = new Vuex.Store({ }); const axiosMock = jest .spyOn(axios, 'get') - .mockResolvedValue({ data: { questions: [] } } as any); -const wrapper = shallowMount(Home, { mocks: { $store } }); + .mockResolvedValueOnce({ data: { questions: [] } } as any) + .mockResolvedValueOnce({ data: { schools: [] } } as any); -describe.skip('Views - Home', () => { - it('should ', () => { - const storeDispatchMock = spyOn($store, 'dispatch'); +shallowMount(Home, { mocks: { $store } }); - // wrapper.find('.todoList__removeDone').trigger('click'); - // expect(storeDispatchMock).toBeCalledTimes(2); - // expect(storeDispatchMock).toHaveBeenNthCalledWith(1, 'fetchQuestions'); - // expect(storeDispatchMock).toHaveBeenNthCalledWith(2, 'fetchSchools'); +describe('Views - Home', () => { + it('should fetch questions and shools', () => { expect(axiosMock).toBeCalledTimes(2); expect(axiosMock).toHaveBeenNthCalledWith(1, `${config.apiUrl}/questions`); expect(axiosMock).toHaveBeenNthCalledWith(2, `${config.apiUrl}/schools`); diff --git a/client/tests/unit/views/ScoreScreen.spec.ts b/client/tests/unit/views/ScoreScreen.spec.ts new file mode 100644 index 0000000..88f771b --- /dev/null +++ b/client/tests/unit/views/ScoreScreen.spec.ts @@ -0,0 +1,53 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import Vuex from 'vuex'; +import ScoreScreen from '../../../src/views/ScoreScreen.vue'; +import router from '@/router'; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +let wrapper: any; + +const mountIt = (index = 0, score = 0, questions = []) => { + wrapper = shallowMount(ScoreScreen, { + mocks: { + $store: new Vuex.Store({ + modules: { + questions: { + namespaced: true, + state: { + index, + score, + questions, + }, + }, + }, + }), + }, + }); +}; + +describe('Views - ScoreScreen', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + it('should redirect to home on index 0', () => { + const routerMock = spyOn(router, 'push'); + + mountIt(); + + expect(routerMock).toBeCalledTimes(1); + expect(routerMock).toBeCalledWith({ name: 'home' }); + }); + + it('should not redirect to home', () => { + mountIt(1); + + const routerMock = spyOn(router, 'push'); + + expect(routerMock).toBeCalledTimes(0); + }); +}); diff --git a/package-lock.json b/package-lock.json index c3e7bd3..fdc245f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "HETICvsEEMI", + "name": "hetic-vs-eemi", "version": "1.0.0", "lockfileVersion": 1, "requires": true, diff --git a/package.json b/package.json index 00b326a..0ded26c 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "HETICvsEEMI", + "name": "hetic-vs-eemi", "version": "1.0.0", "description": "Plus Eemien ou Héticien ? Le jeu qui ne fait pas rire les élèves.", "main": "index.js", @@ -16,8 +16,6 @@ "type": "git", "url": "git+https://github.com/sundowndev/HETICvsEEMI.git" }, - "keywords": [], - "author": "", "license": "ISC", "bugs": { "url": "https://github.com/sundowndev/HETICvsEEMI/issues" diff --git a/server/package.json b/server/package.json index 272cba9..f630c43 100644 --- a/server/package.json +++ b/server/package.json @@ -2,7 +2,6 @@ "name": "server", "private": false, "version": "1.0.0", - "description": "", "main": "index.js", "dependencies": { "compression": "^1.7.4", @@ -10,11 +9,7 @@ "express": "^4.17.1", "morgan": "^1.9.1" }, - "devDependencies": {}, "scripts": { "start": "node index.js" - }, - "keywords": [], - "author": "", - "license": "ISC" + } }