Skip to content
Snippets Groups Projects
Commit 43a3011b authored by Vilde Min Vikan's avatar Vilde Min Vikan
Browse files

Merge branch 'merge-branch' into 'master'

Merge tests and read me file to master

See merge request !14
parents 11d54f63 6609dddd
No related branches found
No related tags found
1 merge request!14Merge tests and read me file to master
Showing
with 611 additions and 59 deletions
......@@ -2,6 +2,19 @@
The frontend component of the Trivio quiz application is built using Vue.js and Vite. It provides a user interface for interacting with the backend APIs. Unit testing is performed using vitest, while integration testing is conducted using Cypress.
## Content
- [Functionality](#functionality)
- [Security]("#security")
- [Installation](#installation)
- [Usage](#usage)
- [Tests](#tests)
- [Dependencies](#dependencies)
- [Future work](#future-work)
- [Authors](#authors)
# Frontend Trivio
The frontend component of the Trivio quiz application is built using Vue.js and Vite. It provides a user interface for interacting with the backend APIs. Unit testing is performed using vitest, while integration testing is conducted using Cypress.
## Content
- [Functionality](#functionality)
- [Security]("#security")
......@@ -44,6 +57,42 @@ git clone https://gitlab.stud.idi.ntnu.no/team_fullastack/frontend_fullstack.git
cd frontend_fullstack
```
4. Install dependencies:
```bash
npm install
```
## Functionality
User
- Sign in and log in
- Discover quizzes made by other users
- Take quizzes
- Get score after quiz
- Watch previous quiz scores
- Make a quiz
- Collaborate on quizzes
- Contact admin
- Change username and password
Admin
- Access to all users
- See all trivios
## Security
Trivio uses JWT-Tokens for user authentication during API calls. Upon user login, the backend validates the provided email and encoded password comparing them with existing user records. A JTO token is generated using JWTSBuilder, incorporating the user's id and expiration time. This token serves as the user's authentication credential for making API calls.
## Installation
1. Make sure you have Node.js and npm installed
2. Clone the repository
```bash
git clone https://gitlab.stud.idi.ntnu.no/team_fullastack/frontend_fullstack.git
```
3. Navigate to frontend_fullstack:
```bash
cd frontend_fullstack
```
4. Install dependencies:
```bash
npm install
......@@ -60,6 +109,20 @@ This will start the application locally and display the URL where it can be acce
## Tests
### Cypress
1. Ensure that both the backend and frontend applications are running.
2. Run Cypress interaction tests:
```bash
npm run test:e2e
```
### Vitest
1. Run Vitest unit tests:
```bash
npm run test:unit
```
## Dependencies
- Vue.js
- Vite
......@@ -71,6 +134,15 @@ This will start the application locally and display the URL where it can be acce
## Future work
### Mobile Responsive UI
Implement a mobile-friendly user interface to ensure optimal user experience across various devices and screen sizes. This involves optimizing layouts, font sizes, and interactive elements for smaller screens.
### Additional Admin Functionalities
Expand the capabilities of the admin dashboard to provide more control and management options.
### Performance
As the project developed, we adjusted our functionalities to meet requirements and time constraints. Consequently, some aspects of our database queries and server-side logic may not be optimized for performance. To enhance the efficiency of our application, future work will prioritize optimizing these areas, resulting in improved server-side performance.
## Authors
Gia Hy Nguyen
Tini Tran
......
......@@ -3,6 +3,6 @@ import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
baseUrl: 'http://localhost:4173'
baseUrl: 'http://localhost:5173'
}
})
// https://on.cypress.io/api
describe('My First Test', () => {
it('visits the app root url', () => {
cy.visit('/')
cy.contains('h1', 'You did it!')
})
})
describe('Login Form', () => {
beforeEach(() => {
cy.visit('/')
})
it('should log in with valid credentials', () => {
// Enter valid username and password
cy.get('#username').type('test')
cy.get('#password').type('password')
// Click the login button
cy.get('#signinButton').click()
// Ensure redirected to the homepage or appropriate route after successful login
cy.url().should('include', '/homepage')
})
it('should display error message with invalid password', () => {
// Enter password
cy.get('#username').type('test')
cy.get('#password').type('pass')
// Click the login button
cy.get('#signinButton').click()
// Check if error message is displayed
cy.get('#loginStatus').should('have.text', 'Login failed')
})
})
\ No newline at end of file
describe('Create Account Form', () => {
beforeEach(() => {
cy.visit('/createAccount')
})
it('should sign in with valid credentials', () => {
// Enter valid username and password
cy.get('#username').type('account')
cy.get('#password').type('password')
cy.get('#email').type('account@mail.com')
// Click the sign-in button
cy.get('#signinButton').click()
// Ensure redirected to the dashboard or appropriate route after successful sign-in
cy.url().should('include', '/homepage')
})
it('should display error message with invalid email', () => {
// Enter valid username and password, but invalid email
cy.get('#username').type('username')
cy.get('#password').type('password')
cy.get('#email').type('InvalidEmail')
// Click the sign-in button
cy.get('#signinButton').click()
// Check if error message is displayed
cy.get('#loginStatus').should('have.text', 'Please enter a valid email address')
})
it('should display error message when username is empty', () => {
// Enter empty username
cy.get('#password').type('password')
cy.get('#email').type('username@mail.com')
// Click the sign-in button
cy.get('#signinButton').click()
// Check if error message is displayed
cy.get('#loginStatus').should('have.text', 'Please enter a username')
})
it('should display error message when password is empty', () => {
// Enter empty password
cy.get('#username').type('username')
cy.get('#email').type('username@mail.com')
// Click the sign-in button
cy.get('#signinButton').click()
// Check if error message is displayed
cy.get('#loginStatus').should('have.text', 'Please enter a password')
})
})
\ No newline at end of file
describe('Contact Form', () => {
beforeEach(() => {
cy.login('test', 'password')
cy.visit('/homepage/contact')
})
it('should submit the form with valid input', () => {
// Fill in the form fields
cy.get('#name').type('John Doe');
cy.get('#email').type('john.doe@example.com');
cy.get('#message').type('This is a test message');
// Submit the form
cy.get('#submit_button').click();
// Check for success message
cy.get('.response').should('contain.text', 'Message sent. Thanks for feedback!');
});
it('should display error message if email field is empty', () => {
// Submit the form without filling in the email field
cy.get('#name').type('John Doe');
cy.get('#message').type('This is a test message');
cy.get('#submit_button').click();
// Check for error message
cy.get('.response').should('contain.text', 'Email cannot be empty');
});
it('should display error message if message field is empty', () => {
// Submit the form without filling in the message field
cy.get('#name').type('John Doe');
cy.get('#email').type('john.doe@example.com');
cy.get('#submit_button').click();
// Check for error message
cy.get('.response').should('contain.text', 'Message cannot be empty');
});
})
describe('Discovery View', () => {
beforeEach(() => {
cy.login('test', 'password')
})
it('should display trivios', () => {
// Check if card container exists
cy.get('.card-container').should('exist')
// Check if at least one TrivioCard component is visible inside the card container
cy.get('.card-container .box', { timeout: 5000 }).should('exist')
})
it('should display trivio with correct tags', () => {
// Click "Add Tag" button
cy.get('.addTag').click()
// Click the first tag input box and enter "Capital"
cy.get('.tag-box input').first().click().type('Capital')
// Press Enter after typing the first tag
cy.get('.tag-box input').first().type('{enter}')
// Check if at least one TrivioCard component is visible inside the card container
cy.get('.card-container .box', { timeout: 5000 }).should('exist')
})
it('should not display any trivio with incorrect tags', () => {
// Click "Add Tag" button
cy.get('.addTag').click()
// Click the first tag input box and enter "InvalidTag"
cy.get('.tag-box input').first().click().type('InvalidTag')
// Press Enter after typing the first tag
cy.get('.tag-box input').first().type('{enter}')
// Check if no TrivioCard component is visible inside the card container
cy.get('.card-container .box', { timeout: 5000 }).should('not.exist')
})
it('should display trivio with correct difficulty', () => {
// Select "Medium" in the Difficulty dropdown
cy.get('.FilterOptions select').eq(1).select('Easy')
// Assert that "Medium" is selected in the Difficulty dropdown
cy.get('.FilterOptions select').eq(1).should('have.value', 'Easy')
// Check if at least one TrivioCard component is visible inside the card container
cy.get('.card-container .box', { timeout: 5000 }).should('exist')
})
it('should display trivio with incorrect difficulty', () => {
// Select "Medium" in the Difficulty dropdown
cy.get('.FilterOptions select').eq(1).select('Medium')
// Assert that "Medium" is selected in the Difficulty dropdown
cy.get('.FilterOptions select').eq(1).should('have.value', 'Medium')
// Check if at least one TrivioCard component is visible inside the card container
cy.get('.card-container .box', { timeout: 5000 }).should('not.exist')
})
it('should navigate to start page when a TrivioCard is clicked', () => {
// Click on the first TrivioCard
cy.get('.card-container .box').first().click()
// Assert that the URL has changed to the expected route
cy.url().should('include', '/homepage/start')
})
})
describe('EditTrivio Component', () => {
beforeEach(() => {
cy.login('test', 'password')
cy.visit('/homepage/trivios')
cy.get('.card-container .box').first().click()
cy.get('.user-button-one').first().click()
})
it('should display the component with correct initial data', () => {
// Check if the component title is displayed
cy.contains('h1', 'Edit Trivio').should('exist')
// Check if the initial input fields are displayed
cy.get('.header').should('exist')
cy.get('.input-boxes').should('exist')
cy.get('.questions').should('exist')
cy.get('.add-box').should('exist')
// Check if the difficulty, category, and visibility dropdowns are displayed
cy.get('.option').should('have.length', 3)
// Check if the add question button is displayed
cy.get('.add-question').should('exist')
})
it('should appear correct alert message and route to start page when clicking save', () => {
// Click the save button
cy.get('.top-button').contains('Save').click()
// Check if the alert box is displayed with the correct message
cy.on('window:alert', (alertMessage) => {
expect(alertMessage).to.equal('Trivio Updated!')
})
// Ensure that the router navigates back to the start page after saving
cy.url().should('include', '/start')
})
})
describe('History Page', () => {
beforeEach(() => {
cy.login('test', 'password')
cy.visit('/homepage/history')
})
it('should display history', () => {
// Check if history is displayed
cy.get('.History').should('be.visible')
// Check if trivio titles are visible
cy.get('.filter select#titleFilter').should('be.visible')
// Check if pagination buttons are visible
cy.get('.pagination button').should('be.visible')
})
})
\ No newline at end of file
describe('MakeTrivio Component', () => {
beforeEach(() => {
cy.login('test', 'password')
cy.visit('/homepage/create-trivio')
})
it('should display the component with correct initial data', () => {
// Check if the component title is displayed
cy.contains('h1', 'Create new quiz').should('exist')
// Check if the initial input fields are displayed
cy.get('.header').should('exist')
cy.get('.input-boxes').should('exist')
cy.get('.questions').should('exist')
cy.get('.add-box').should('exist')
// Check if the difficulty, category, and visibility dropdowns are displayed
cy.get('.option').should('have.length', 3)
// Check if the add question button is displayed
cy.get('.add-question').should('exist')
})
it('should add a question when the add question button is clicked', () => {
// Click the add question button
cy.get('.add-question').click()
// Check if the question input boxes are displayed
cy.get('.questions').should('have.length', 1)
// Check if the answer input boxes are displayed
cy.get('.answer').should('have.length', 4)
})
it('should route to my trivios page when clicking create', () => {
// Click the save button
cy.get('.top-button').contains('Create').click()
// Ensure that the router navigates back to the start page after saving
cy.url().should('include', '/trivios')
})
})
describe('My Trivios Component', () => {
beforeEach(() => {
cy.login('test', 'password')
cy.visit('/homepage/trivios')
})
it('should display the filter options', () => {
// Check if category and difficulty filter options are visible
cy.get('.FilterOptions').should('be.visible')
cy.get('.option').should('have.length', 4)
})
it('should display the tags section', () => {
// Check if the tags section is visible
cy.get('.tag-section').should('be.visible')
// Check if the "Add Tag" button is visible
cy.contains('.addTag', 'Add Tag +').should('be.visible')
})
it('should display pagination', () => {
// Check if the pagination section is visible
cy.get('.pagination').should('be.visible')
cy.get('.pagination').find('button').should('exist')
})
})
describe('Profile Component', () => {
beforeEach(() => {
cy.login('test', 'password');
cy.visit('/homepage/profile');
});
it('fetches user information on mount', () => {
// Check if user information is displayed correctly
cy.get('#username').should('have.value', 'test');
cy.get('#email').should('have.value', 'username@mail.com');
});
it('should allow editing user information', () => {
// Type new username and email
cy.get('#username').clear().type('newUsername')
cy.get('#email').clear().type('newEmail@example.com')
// Save changes
cy.get('.user-info-header .save-button').click()
// Check if changes are saved
cy.get('#username').should('have.value', 'newUsername')
cy.get('#email').should('have.value', 'newEmail@example.com')
})
});
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
declare global {
namespace Cypress {
interface Chainable {
/**
* Custom command to log in.
* @example cy.login('username', 'password')
*/
login(username: string, password: string): Chainable<void>;
}
}
}
Cypress.Commands.add('login', (username, password) => {
cy.visit('/login') // Adjust the URL as needed
cy.get('#username').type(username)
cy.get('#password').type(password)
cy.get('#signinButton').click()
cy.url().should('include', '/homepage')
})
export { }
......@@ -8,8 +8,8 @@
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"test:unit": "vitest",
"test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'",
"test:e2e": "start-server-and-test preview http://localhost:5173 'cypress run --e2e'",
"test:e2e:dev": "start-server-and-test 'vite dev --port 5173' http://localhost:5173 'cypress open --e2e'",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
......
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld', () => {
it('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
})
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils';
import HistoryBox from '@/components/HistoryBox.vue';
describe('HistoryBox', () => {
it('renders with correct props', () => {
// Mock props
const props = {
title: 'Sample Quiz',
image: '/sample-image.png',
numberOfQuestion: 10,
username: 'John Doe',
difficulty: 'Easy',
score: 8,
date: '2024-04-10',
};
// Mount the component with mock props
const wrapper = mount(HistoryBox, {
props,
});
// Assert that the rendered component contains the correct information
expect(wrapper.find('.quiz-title').text()).toBe(props.title);
expect(wrapper.find('.difficulty').text()).toContain(`Difficulty: ${props.difficulty}`);
expect(wrapper.find('.username').text()).toBe(props.username);
expect(wrapper.find('.score-text').text()).toContain(`${props.score}/${props.numberOfQuestion}`);
expect(wrapper.find('.date').text()).toBe(props.date);
// Assert image source
const imageSrc = wrapper.find('.quiz-box img').attributes('src');
if (props.image) {
expect(imageSrc).toBe(props.image);
} else {
expect(imageSrc).toBe('/src/assets/trivio.svg');
}
});
});
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils';
import QuestionBox from '@/components/QuestionBox.vue';
describe('QuestionBox', () => {
it('renders props correctly', () => {
// Define props
const props = {
image: '/path/to/image.jpg',
questionNumber: 1,
tags: ['tag1', 'tag2'],
question: ['Sample question line 1', 'Sample question line 2'],
};
// Mount the component with props
const wrapper = mount(QuestionBox, {
props,
});
// Assert that props are rendered correctly
expect(wrapper.find('img').attributes('src')).toBe(props.image);
expect(wrapper.find('label[for="number"]').text()).toBe(props.questionNumber.toString());
// Construct the expected question string
const expectedQuestion = JSON.stringify(props.question, null, 2); // Use JSON.stringify
const receivedQuestion = wrapper.find('label[for="question"]').text();
expect(receivedQuestion).toBe(expectedQuestion);
});
it('renders default image if image prop is not provided', () => {
const wrapper = mount(QuestionBox, {
props: {
questionNumber: 1,
tags: ['tag1', 'tag2'],
question: ['Sample question line 1', 'Sample question line 2'],
},
});
expect(wrapper.find('img').attributes('src')).toBe('/src/assets/trivio.svg');
});
});
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils';
import TrivioCard from '@/components/TrivioCard.vue';
describe('TrivioCard', () => {
it('renders props correctly', () => {
// Define props
const props = {
title: 'Sample Quiz',
image: '/path/to/image.jpg',
category: 'Sample Category',
numberOfQuestion: 10,
username: 'sampleUser',
difficulty: 'Easy',
media: '/path/to/media.jpg',
};
// Mount the component with props
const wrapper = mount(TrivioCard, {
props,
});
// Assert that props are rendered correctly
expect(wrapper.find('.title').text()).toBe(props.title);
expect(wrapper.find('.category').text()).toContain(props.category);
expect(wrapper.find('.question').text()).toContain(`${props.numberOfQuestion} Questions`);
expect(wrapper.find('.user').text()).toBe(props.username);
expect(wrapper.find('.difficulty').text()).toBe(props.difficulty);
expect(wrapper.find('img').attributes('src')).toBe(props.media);
});
it('renders default image if image prop is not provided', () => {
// Mount the component without image prop
const wrapper = mount(TrivioCard, {
props: {
title: 'Sample Quiz',
category: 'Sample Category',
numberOfQuestion: 10,
username: 'sampleUser',
difficulty: 'Easy',
},
});
// Assert that default image is rendered
expect(wrapper.find('img').attributes('src')).toBe('/src/assets/trivio.svg');
});
});
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils';
import UserSchema from '@/components/UserSchema.vue';
describe('UserSchema Component', () => {
it('renders correctly with default props', async () => {
// Mount the component
const wrapper = mount(UserSchema);
// Assert that the component renders correctly
expect(wrapper.exists()).toBe(true);
expect(wrapper.find('h1').text()).toBe('Login');
expect(wrapper.find('#username').exists()).toBe(true);
expect(wrapper.find('#password').exists()).toBe(true);
expect(wrapper.find('#email').exists()).toBe(false); // Email input should not be rendered by default
expect(wrapper.find('button').text()).toBe('Login');
expect(wrapper.find('#loginStatus').text()).toBe('');
});
it('renders correctly with custom props', async () => {
// Mount the component with custom props
const wrapper = mount(UserSchema, {
props: {
buttonText: 'Sign Up',
headerText: 'Create Account',
status: 'Invalid username or password',
},
});
// Assert that the component renders correctly with custom props
expect(wrapper.exists()).toBe(true);
expect(wrapper.find('h1').text()).toBe('Create Account');
expect(wrapper.find('#username').exists()).toBe(true);
expect(wrapper.find('#password').exists()).toBe(true);
expect(wrapper.find('#email').exists()).toBe(true); // Email input should be rendered with custom props
expect(wrapper.find('button').text()).toBe('Sign Up');
expect(wrapper.find('#loginStatus').text()).toBe('Invalid username or password');
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment