├── .dockerignore ├── .env.example ├── .eslintrc ├── .github └── workflows │ ├── check-diff-size.yml │ ├── enforce-linting.yml │ ├── reset-server.yml │ ├── run-client-tests.yml │ ├── run-e2e.yml │ └── run-server-tests.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── client ├── .eslintrc ├── index.html ├── package.json ├── public │ ├── favicon.ico │ └── icon.svg ├── src │ ├── App.jsx │ ├── App.test.jsx │ ├── index.css │ ├── index.jsx │ └── tests │ │ └── setupTests.js └── vite.config.js ├── data ├── example_data.csv └── example_response.json ├── db └── initdb.sql ├── e2e ├── .eslintrc ├── package.json ├── playwright.config.js └── tests │ ├── features.spec.js │ └── utils.js ├── guides ├── code-quality │ └── readme.md ├── db-setup │ └── readme.md ├── e2e-tests │ └── readme.md ├── setup │ └── readme.md └── testing │ └── readme.md ├── netlify.toml ├── package-lock.json ├── package.json └── server ├── .eslintrc ├── api.js ├── api.test.js ├── app.js ├── db.js ├── functions └── app.mjs ├── package-lock.json ├── package.json ├── server.js └── test └── jest-setup.js /.dockerignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .env 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://cyf:password@localhost:5432/cyf 2 | TEST_DATABASE_URL=postgres://cyf:password@localhost:5432/cyf_test 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true, 6 | "commonjs": true, 7 | "jest/globals": true 8 | }, 9 | "extends": ["@codeyourfuture/standard", "prettier"], 10 | "parserOptions": { 11 | "ecmaVersion": 2021, 12 | "sourceType": "module" 13 | }, 14 | "plugins": ["jest"], 15 | "root": true 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/check-diff-size.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | 7 | jobs: 8 | check-diff: 9 | name: Check diff size 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: "Check that diff isn't too large" 18 | env: 19 | GH_TOKEN: ${{ github.token }} 20 | run: | 21 | FILES_CHANGED=$(gh pr diff ${{github.event.pull_request.number}} --name-only | wc -l) 22 | echo "Number of files changed in this PR: ${FILES_CHANGED}" 23 | if [ $FILES_CHANGED -gt 20 ] 24 | then 25 | echo "The diff is too large. You've changed ${FILES_CHANGED} in this PR!" 26 | exit 1 27 | else 28 | echo "The diff looks nice and small - good work! 😎" 29 | exit 0 30 | fi 31 | -------------------------------------------------------------------------------- /.github/workflows/enforce-linting.yml: -------------------------------------------------------------------------------- 1 | name: enforce-linting 2 | 3 | run-name: Enforce lint passes on committed files 4 | 5 | on: 6 | workflow_dispatch: 7 | #pull_request: 8 | 9 | jobs: 10 | run-linter: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: "20" 17 | cache: "npm" 18 | - run: npm install 19 | - run: npm exec prettier -- --check . 20 | - run: npm exec eslint . 21 | -------------------------------------------------------------------------------- /.github/workflows/reset-server.yml: -------------------------------------------------------------------------------- 1 | name: Reset Server 2 | run-name: Reset deployed application occasionally 3 | on: 4 | workflow_dispatch: 5 | #schedule: 6 | # - cron: "*/30 * * * *" 7 | 8 | jobs: 9 | pingServer: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - run: 'curl --request POST https://.netlify.app/api/videos/reset -H "Content-Type: application/json" --data "{\"code\":\"$RESET_CODE\"}" || true' 13 | env: 14 | RESET_CODE: ${{ secrets.RESET_CODE }} 15 | -------------------------------------------------------------------------------- /.github/workflows/run-client-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-client-tests 2 | 3 | run-name: Enforce tests pass on committed files 4 | 5 | on: 6 | workflow_dispatch: 7 | #pull_request: 8 | 9 | jobs: 10 | run-client-tests: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: "20" 17 | cache: "npm" 18 | - run: cp .env.example .env 19 | - run: npm install 20 | - run: npm run test:client 21 | -------------------------------------------------------------------------------- /.github/workflows/run-e2e.yml: -------------------------------------------------------------------------------- 1 | name: run-e2e 2 | 3 | run-name: Enforce playwright end-to-end tests pass on committed files 4 | 5 | on: 6 | workflow_dispatch: 7 | #pull_request: 8 | 9 | jobs: 10 | run-features: 11 | runs-on: ubuntu-latest 12 | services: 13 | postgres: 14 | image: postgres 15 | env: 16 | POSTGRES_PASSWORD: password 17 | POSTGRES_USER: cyf 18 | # Set health checks to wait until postgres has started 19 | options: >- 20 | --health-cmd pg_isready 21 | --health-interval 10s 22 | --health-timeout 5s 23 | --health-retries 5 24 | ports: 25 | - 5432:5432 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: "20" 31 | cache: "npm" 32 | - run: cp .env.example .env 33 | - run: npm install 34 | - run: npx playwright install --with-deps 35 | - run: npm run test:e2e 36 | env: 37 | NODE_ENV: test 38 | TEST_DATABASE_URL: postgres://cyf:password@localhost:5432/cyf 39 | -------------------------------------------------------------------------------- /.github/workflows/run-server-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-server-tests 2 | 3 | run-name: Enforce tests pass on committed files 4 | 5 | on: 6 | workflow_dispatch: 7 | #pull_request: 8 | 9 | jobs: 10 | run-server-tests: 11 | runs-on: ubuntu-latest 12 | services: 13 | postgres: 14 | image: postgres 15 | env: 16 | POSTGRES_PASSWORD: password 17 | POSTGRES_USER: cyf 18 | # Set health checks to wait until postgres has started 19 | options: >- 20 | --health-cmd pg_isready 21 | --health-interval 10s 22 | --health-timeout 5s 23 | --health-retries 5 24 | ports: 25 | - 5432:5432 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: "20" 31 | cache: "npm" 32 | - run: cp .env.example .env 33 | - run: npm install 34 | - run: npm run test:server 35 | env: 36 | TEST_DATABASE_URL: postgres://cyf:password@localhost:5432/cyf 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | e2e/blob-report/ 2 | e2e/playwright/.cache/ 3 | e2e/playwright-report/ 4 | e2e/test-results/ 5 | node_modules/ 6 | server/static/ 7 | .env 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.8.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "endOfLine": "lf", 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "es5", 8 | "useTabs": true 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2024 Code your future 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Video Recommendation App 2 | 3 | ## Background 4 | 5 | To apply to The Launch you must show you can meaningfully contribute to a technical project. This means that you must **clearly demonstrate** that you can create and deploy full stack applications. If you cannot yet complete this full stack assessment, you are not ready to apply to The Launch. 6 | 7 | ## Challenge 8 | 9 | In this project, you will be building up a small application that allows you to share your favourite YouTube videos with the world. We will begin with a very small MVP (Minimal Viable Product), and build on top of it to make it nicer and more useful. 10 | 11 | ## User stories 👩🏽‍💻 12 | 13 | Most of the core features for this project have been captured as **user story** issues. You can view all the user story issues on the project planning board: [User story issues](https://github.com/orgs/CodeYourFuture/projects/169/views/3). User story issues define a particular feature through a user story. They also link through to other issues you'll need to implement for this user story to be complete. 14 | 15 | ## Requirements 16 | 17 | All the requirements for this project are captured as issues that you can find on this planning board: [Full Stack Assessment Planner](https://github.com/orgs/CodeYourFuture/projects/169/views/2) 18 | 19 | > [!TIP] 20 | > Some of the issues are optional which means that you can build a working project without them. However, make something really impressive: complete as much as you can. We value excellence and so do employers. 21 | 22 | ### Week 1 - Minimal Viable Product 23 | 24 | [Week 1 issues](https://github.com/orgs/CodeYourFuture/projects/169/views/2?filterQuery=sprint%3A1) 25 | 26 | ### Week 2 - Additional features 27 | 28 | [Week 2 issues](https://github.com/orgs/CodeYourFuture/projects/169/views/2?filterQuery=sprint%3A2) 29 | 30 | ### Week 3 - Finalizing project 31 | 32 | [Week 3 issues](https://github.com/orgs/CodeYourFuture/projects/169/views/2?filterQuery=sprint%3A3) 33 | 34 | ### Week 4 - Stretch goals 35 | 36 | Use extra time this week to implement missing required and optional features from previous weeks. 37 | [Week 4 issues](https://github.com/orgs/CodeYourFuture/projects/169/views/2?filterQuery=sprint%3A4) 38 | 39 | ## Sample Solution 40 | 41 | Here is an example solution for both frontend and backend, including all optional features: 42 | 43 | https://cyf-fsa-solution.netlify.app/ 44 | 45 | > [!NOTE] 46 | > You can design the website to look however you like. 47 | 48 | ## Using this project for the launch project and other portfolio pieces 49 | 50 | While you are free to use this codebase for your future projects we recommend against it. This project is set up in a way to make it easy to understand, but lacks a lot of features that would be otherwise helpful. 51 | 52 | For launch projects and future portfolio pieces, look at the [Code Your Future Starter Kit](https://github.com/CodeYourFuture/cyf-final-project-starter-kit). This assessment project is a simplified version of the starter kit, with a lot of the features removed to keep it light and more understandable. In fact, some of the challenges in this project are to re-add these features yourself, like support for linting. 53 | 54 | Since these features will already be present in the starter kit, it will be a much better starting point. And since it uses the same libraries and setup that you will learn here, you should feel immediately familiar with it. 55 | -------------------------------------------------------------------------------- /client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { "browser": true, "es2020": true }, 3 | "extends": [ 4 | "plugin:jsx-a11y/recommended", 5 | "plugin:react/recommended", 6 | "plugin:react/jsx-runtime", 7 | "plugin:react-hooks/recommended", 8 | "prettier" 9 | ], 10 | "overrides": [ 11 | { 12 | "files": ["src/**/*.test.jsx", "src/setupTests.js"], 13 | "env": { "vitest/env": true }, 14 | "extends": [ 15 | "plugin:testing-library/react", 16 | "plugin:vitest/recommended" 17 | ] 18 | } 19 | ], 20 | "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, 21 | "settings": { "react": { "version": "18.2" } }, 22 | "plugins": ["react-refresh"], 23 | "rules": { 24 | "testing-library/no-render-in-lifecycle": ["error", {"allowTestingFrameworkSetupHook": "beforeEach"}], 25 | "react-refresh/only-export-components": [ 26 | "warn", 27 | { "allowConstantExport": true } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Video Recommendations 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@video-recommendations/client", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Video Recommendations app - Frontend", 6 | "type": "module", 7 | "scripts": { 8 | "build": "vite build --emptyOutDir", 9 | "dev": "vite", 10 | "test": "vitest --run", 11 | "test:cover": "npm run test -- --coverage", 12 | "test:watch": "vitest --watch" 13 | }, 14 | "dependencies": { 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-router-dom": "^6.21.2" 18 | }, 19 | "devDependencies": { 20 | "@testing-library/jest-dom": "^6.3.0", 21 | "@testing-library/react": "^14.1.2", 22 | "@testing-library/user-event": "^14.5.2", 23 | "@types/react": "^18.2.47", 24 | "@types/react-dom": "^18.2.18", 25 | "@vitejs/plugin-react-swc": "^3.5.0", 26 | "@vitest/coverage-v8": "^1.2.0", 27 | "jsdom": "^23.2.0", 28 | "msw": "^2.1.4", 29 | "undici": "^6.3.0", 30 | "vite": "^5.0.11", 31 | "vitest": "^1.2.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeYourFuture/Full-Stack-Project-Assessment/e443210280054ba20c80372f6efa9d3396da453f/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /client/src/App.jsx: -------------------------------------------------------------------------------- 1 | const App = () => { 2 | return ( 3 | <> 4 |

Video Recommendations

5 | 6 | ); 7 | }; 8 | 9 | export default App; 10 | -------------------------------------------------------------------------------- /client/src/App.test.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen, waitFor } from "@testing-library/react"; 2 | import userEvent from "@testing-library/user-event"; 3 | import { http, HttpResponse } from "msw"; 4 | 5 | import { server } from "./tests/setupTests.js"; 6 | 7 | import App from "./App.jsx"; 8 | 9 | describe("Main Page", () => { 10 | /** @type {import("@testing-library/user-event").UserEvent} */ 11 | let user; 12 | 13 | beforeEach(async () => { 14 | // Here we create a fake backend that will always return two videos when calling the /api/videos endpoint 15 | server.use( 16 | http.get("/api/videos", () => 17 | HttpResponse.json([ 18 | { 19 | id: 1, 20 | title: "Never Gonna Give You Up", 21 | url: "https://www.youtube.com/watch?v=ABCDEFGHIJK", 22 | }, 23 | { 24 | id: 2, 25 | title: "Other Title", 26 | url: "https://www.youtube.com/watch?v=KJIHGFEDCBA", 27 | }, 28 | ]) 29 | ) 30 | ); 31 | 32 | // Let's render our app 33 | render(); 34 | 35 | // Let's wait for one of the videos to appear 36 | await screen.findByText("Never Gonna Give You Up"); 37 | user = userEvent.setup(); 38 | }); 39 | 40 | it("Renders the videos", async () => { 41 | // calculate how many iframes there are on the website 42 | const videoContainers = screen.getAllByText( 43 | (_, e) => e.tagName.toLowerCase() === "iframe" 44 | ); 45 | 46 | // We have two videos, so the amount should be two 47 | expect(videoContainers).toHaveLength(2); 48 | }); 49 | 50 | it("Removes the video when asked to do", async () => { 51 | // we create another fake backend that listens on the delete call, and returns success 52 | server.use( 53 | http.delete( 54 | "/api/videos/1", 55 | () => new HttpResponse(null, { status: 204 }) 56 | ) 57 | ); 58 | 59 | // we find the delete button on the website 60 | const deleteButton = screen.getAllByRole("button", { 61 | name: "Remove video", 62 | })[0]; 63 | 64 | // then we click it 65 | await user.click(deleteButton); 66 | 67 | // wait for the video to get deleted from the page 68 | await waitFor(() => 69 | expect( 70 | screen.getAllByRole("button", { name: "Remove video" }) 71 | ).toHaveLength(1) 72 | ); 73 | 74 | // we calculate the number of videos after the call 75 | const videoContainers = screen.getAllByText( 76 | (_, e) => e.tagName.toLowerCase() === "iframe" 77 | ); 78 | 79 | // this should now be only 1 80 | expect(videoContainers).toHaveLength(1); 81 | }); 82 | 83 | it("Adds a new video when asked to do", async () => { 84 | const title = "New Title"; 85 | const url = "https://www.youtube.com/watch?v=CDEYRFUTURE"; 86 | 87 | // we set up a fake backend that allows us to send a new video. It only allows one specific title and url however 88 | server.use( 89 | http.post("/api/videos", async ({ request }) => { 90 | const data = await request.json(); 91 | if (data.title !== title || data.url !== url) { 92 | return HttpResponse.json({ success: false }, { status: 400 }); 93 | } 94 | return HttpResponse.json({ id: 3, title, url }); 95 | }) 96 | ); 97 | 98 | // we fill in the form 99 | await user.type(screen.getByRole("textbox", { name: "Title:" }), title); 100 | await user.type(screen.getByRole("textbox", { name: "Url:" }), url); 101 | 102 | // then click submit 103 | await user.click(screen.getByRole("button", { name: "Submit" })); 104 | 105 | // wait for the new video to appear 106 | await screen.findByText(title); 107 | 108 | // afterwards we calculate the number of videos on the page 109 | const videoContainers = screen.getAllByText( 110 | (_, e) => e.tagName.toLowerCase() === "iframe" 111 | ); 112 | 113 | // this should now be three 114 | expect(videoContainers).toHaveLength(3); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | } 5 | 6 | h1 { 7 | text-align: center; 8 | } 9 | -------------------------------------------------------------------------------- /client/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { BrowserRouter } from "react-router-dom"; 4 | 5 | import App from "./App.jsx"; 6 | import "./index.css"; 7 | 8 | ReactDOM.createRoot(document.getElementById("root")).render( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /client/src/tests/setupTests.js: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | import { setupServer } from "msw/node"; 3 | import { setGlobalOrigin } from "undici"; 4 | import { beforeAll, beforeEach, afterAll } from "vitest"; 5 | 6 | export const server = setupServer(); 7 | 8 | setGlobalOrigin(window.location.href); // see mswjs/msw#1625 9 | 10 | beforeAll(() => server.listen()); 11 | 12 | beforeEach(() => server.resetHandlers()); 13 | 14 | afterAll(() => server.close()); 15 | -------------------------------------------------------------------------------- /client/vite.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | import { defineConfig } from "vite"; 3 | import react from "@vitejs/plugin-react-swc"; 4 | 5 | const serverPort = process.env.SERVER_PORT ?? "3100"; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | build: { 10 | outDir: "../server/static", 11 | }, 12 | plugins: [react()], 13 | server: { 14 | port: process.env.PORT, 15 | proxy: { 16 | "/api": `http://localhost:${serverPort}`, 17 | "/healthz": `http://localhost:${serverPort}`, 18 | }, 19 | }, 20 | test: { 21 | environment: "jsdom", 22 | globals: true, 23 | setupFiles: ["src/tests/setupTests.js"], 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /data/example_data.csv: -------------------------------------------------------------------------------- 1 | "Never Gonna Give You Up","https://www.youtube.com/watch?v=dQw4w9WgXcQ" 2 | "The Coding Train","https://www.youtube.com/watch?v=HerCR8bw_GE" 3 | "Mac & Cheese | Basics with Babish","https://www.youtube.com/watch?v=FUeyrEN14Rk" 4 | "Videos for Cats to Watch - 8 Hour Bird Bonanza","https://www.youtube.com/watch?v=xbs7FT7dXYc" 5 | "The Complete London 2012 Opening Ceremony | London 2012 Olympic Games","https://www.youtube.com/watch?v=4As0e4de-rI" 6 | "Learn Unity - Beginner's Game Development Course","https://www.youtube.com/watch?v=gB1F9G0JXOo" 7 | "Cracking Enigma in 2021 - Computerphile","https://www.youtube.com/watch?v=RzWB5jL5RX0" 8 | "Coding Adventure: Chess AI","https://www.youtube.com/watch?v=U4ogK0MIzqk" 9 | "Coding Adventure: Ant and Slime Simulations","https://www.youtube.com/watch?v=X-iSQQgOd1A" 10 | "Why the Tour de France is so brutal","https://www.youtube.com/watch?v=ZacOS8NBK6U" 11 | -------------------------------------------------------------------------------- /data/example_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "title": "Never Gonna Give You Up", 5 | "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" 6 | }, 7 | { 8 | "id": 2, 9 | "title": "The Coding Train", 10 | "url": "https://www.youtube.com/watch?v=HerCR8bw_GE" 11 | }, 12 | { 13 | "id": 3, 14 | "title": "Mac & Cheese | Basics with Babish", 15 | "url": "https://www.youtube.com/watch?v=FUeyrEN14Rk" 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /db/initdb.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS videos CASCADE; 2 | 3 | CREATE TABLE videos ( 4 | title VARCHAR, 5 | src VARCHAR 6 | ); 7 | 8 | INSERT INTO videos (title,src) VALUES ('Never Gonna Give You Up','https://www.youtube.com/watch?v=dQw4w9WgXcQ'); 9 | -- you can insert more rows using example data from the example_data.csv file -------------------------------------------------------------------------------- /e2e/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:playwright/recommended"], 3 | "ignorePatterns": ["/playwright-report/", "/test-results/"] 4 | } 5 | -------------------------------------------------------------------------------- /e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@video-recommendations/e2e", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Playwright end-to-end tests", 6 | "type": "module", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "cross-env NODE_ENV=test playwright test", 10 | "test:ui": "cross-env NODE_ENV=test playwright test --ui", 11 | "test:headed": "cross-env NODE_ENV=test playwright test --headed" 12 | }, 13 | "keywords": [], 14 | "author": "Code Your Future ", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@playwright/test": "^1.41.1", 18 | "eslint-plugin-playwright": "^0.22.1", 19 | "playwright": "^1.40.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /e2e/playwright.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | import { dirname, join } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import { defineConfig, devices } from "@playwright/test"; 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)); 7 | 8 | export default defineConfig({ 9 | testDir: "./tests", 10 | /* Do not run tests in files in parallel due to database usage */ 11 | fullyParallel: false, 12 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 13 | forbidOnly: !!process.env.CI, 14 | /* Retry on CI only */ 15 | retries: process.env.CI ? 2 : 0, 16 | /* Opt out of parallel tests to make sure we can assert better on the database. */ 17 | workers: 1, 18 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 19 | reporter: "html", 20 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 21 | use: { 22 | /* Base URL to use in actions like `await page.goto('/')`. */ 23 | baseURL: "http://localhost:3000", 24 | screenshot: "only-on-failure", 25 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 26 | trace: "on-first-retry", 27 | }, 28 | 29 | /* Configure projects for major browsers */ 30 | projects: [ 31 | { 32 | name: "chromium", 33 | use: { ...devices["Desktop Chrome"] }, 34 | }, 35 | { 36 | name: "firefox", 37 | use: { ...devices["Desktop Firefox"] }, 38 | }, 39 | { 40 | name: "webkit", 41 | use: { ...devices["Desktop Safari"] }, 42 | }, 43 | 44 | /* Test against mobile viewports. */ 45 | // { 46 | // name: 'Mobile Chrome', 47 | // use: { ...devices['Pixel 5'] }, 48 | // }, 49 | // { 50 | // name: 'Mobile Safari', 51 | // use: { ...devices['iPhone 12'] }, 52 | // }, 53 | 54 | /* Test against branded browsers. */ 55 | // { 56 | // name: 'Microsoft Edge', 57 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 58 | // }, 59 | // { 60 | // name: 'Google Chrome', 61 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 62 | // }, 63 | ], 64 | 65 | /* Run your local dev server before starting the tests */ 66 | webServer: process.env.PLAYWRIGHT_BASE_URL 67 | ? false 68 | : { 69 | command: "npm run dev", 70 | cwd: join(__dirname, ".."), 71 | url: "http://localhost:3000", 72 | reuseExistingServer: !process.env.CI, 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /e2e/tests/features.spec.js: -------------------------------------------------------------------------------- 1 | import db from "../../server/db"; 2 | import resetDatabase from "./utils"; 3 | import { test, expect } from "@playwright/test"; 4 | 5 | async function openWebsite(page) { 6 | // Open URL 7 | await page.goto("http://localhost:3000"); 8 | // Wait for title to appear 9 | await expect(page.getByText("Video Recommendations")).toBeVisible(); 10 | } 11 | 12 | async function findVideoByTitle(page, title) { 13 | // Find the title on the screen 14 | await expect(page.getByText(title)).toBeVisible(); 15 | 16 | const titleComponent = page.getByText(title); 17 | // Go up a couple levels to find the encompassing component of the video 18 | // You might need to change this if the structure of your video components differ 19 | const videoParent = titleComponent.locator("xpath=./../.."); 20 | 21 | return videoParent; 22 | } 23 | 24 | test.describe("Videos", () => { 25 | test.beforeEach(async () => { 26 | await resetDatabase(); 27 | }); 28 | 29 | test("Level 130 requirements - display videos", async ({ page }) => { 30 | // Given I open the website 31 | await openWebsite(page); 32 | 33 | // Then I am able to see the video entries 34 | await expect(page.getByText("Never Gonna Give You Up")).toBeVisible(); 35 | }); 36 | 37 | test("Level 200 requirements - videos in iframe", async ({ page }) => { 38 | // Given I have a video from the database 39 | const videoResults = await db.query("SELECT * FROM videos LIMIT 1"); 40 | 41 | // And I open up the website 42 | await openWebsite(page); 43 | 44 | // Then I am able to see the video's title 45 | const videoParent = await findVideoByTitle( 46 | page, 47 | videoResults.rows[0].title 48 | ); 49 | 50 | // And I am able to see the embedded video 51 | const videoIframe = videoParent.locator("iframe"); 52 | await expect(videoIframe).toHaveAttribute( 53 | "src", 54 | "https://www.youtube.com/embed/" + videoResults.rows[0].url.slice(-11) 55 | ); 56 | }); 57 | 58 | test("Level 210 requirements - add new video", async ({ page }) => { 59 | // Given I open up the website 60 | await openWebsite(page); 61 | 62 | // Then I am able to see the upload section 63 | await expect(page.getByText("Submit a new video")).toBeVisible(); 64 | 65 | // And I am able to fill in the details 66 | await page.getByLabel("Title").fill("The New Title"); 67 | await page 68 | .getByLabel("Url") 69 | .fill("https://www.youtube.com/watch?v=ABCDEFGHIJK"); 70 | 71 | // When I submit the details 72 | await page.getByRole("button", { name: "Submit" }).click(); 73 | 74 | // Then I am able to see the video's title to appear 75 | const videoParent = await findVideoByTitle(page, "The New Title"); 76 | 77 | // And I am able to see the embedded video 78 | const videoIframe = videoParent.locator("iframe"); 79 | await expect(videoIframe).toHaveAttribute( 80 | "src", 81 | "https://www.youtube.com/embed/ABCDEFGHIJK" 82 | ); 83 | 84 | // And I can see the new video in the database 85 | const dbResponse = await db.query( 86 | "SELECT * FROM videos WHERE title = $1 AND url = $2", 87 | ["The New Title", "https://www.youtube.com/watch?v=ABCDEFGHIJK"] 88 | ); 89 | expect(dbResponse.rows.length).toBe(1); 90 | }); 91 | 92 | test("Level 220 requirements - delete video", async ({ page }) => { 93 | // Given I have a video from the database 94 | const videoResults = await db.query("SELECT * FROM videos LIMIT 1"); 95 | 96 | // And I open up the website 97 | await openWebsite(page); 98 | 99 | // Then I am able to see the video's title 100 | const videoParent = await findVideoByTitle( 101 | page, 102 | videoResults.rows[0].title 103 | ); 104 | 105 | // Then I am able to see a button that removes the video 106 | const deleteButton = videoParent.getByText("Remove video"); 107 | 108 | // When I remove the video when pressing the button 109 | deleteButton.click(); 110 | 111 | // Then the video gets removed from the screen 112 | await expect(page.getByText(videoResults.rows[0].title)).toHaveCount(0); 113 | 114 | // And the video gets removed from the database 115 | const videoResultsAfterDelete = await db.query( 116 | "SELECT * FROM videos WHERE id = $1", 117 | [videoResults.rows[0].id] 118 | ); 119 | expect(videoResultsAfterDelete.rows.length).toBe(0); 120 | }); 121 | 122 | test("Level 300 requirements - ratings", async ({ page }) => { 123 | // Given I have a video from the database 124 | const videoResults = await db.query("SELECT * FROM videos LIMIT 1"); 125 | 126 | // And I open up the website 127 | await openWebsite(page); 128 | 129 | // Then I am able to see the video's title 130 | const videoParent = await findVideoByTitle( 131 | page, 132 | videoResults.rows[0].title 133 | ); 134 | 135 | // And I am able to see a button that adds a vote to the video 136 | const upVoteButton = videoParent.getByText("Up Vote"); 137 | 138 | // And the current rating 139 | await expect( 140 | videoParent.getByText(new RegExp(`^${videoResults.rows[0].rating}$`)) 141 | ).toBeVisible(); 142 | 143 | // When I upvote the video when pressing the button 144 | upVoteButton.click(); 145 | 146 | // Then the vote will update on the screen 147 | await expect( 148 | videoParent.getByText(new RegExp(`^${videoResults.rows[0].rating + 1}$`)) 149 | ).toBeVisible(); 150 | 151 | // And the video will update in the database 152 | const videoResultsAfterUpvote = await db.query( 153 | "SELECT * FROM videos WHERE id = $1", 154 | [videoResults.rows[0].id] 155 | ); 156 | expect(videoResultsAfterUpvote.rows[0].rating).toBe( 157 | videoResults.rows[0].rating + 1 158 | ); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /e2e/tests/utils.js: -------------------------------------------------------------------------------- 1 | import { dirname } from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | import { readFile } from "fs/promises"; 4 | import path from "path"; 5 | import db from "../../server/db"; 6 | 7 | const __dirname = dirname(fileURLToPath(import.meta.url)); 8 | 9 | async function resetDatabase() { 10 | // Make sure we load up the test database, but only if we started the app in test mode, not to accidentally delete prod data 11 | if (process.env.NODE_ENV === "test") { 12 | const schemaSql = await readFile( 13 | path.resolve(__dirname, "../../db/initdb.sql"), 14 | "utf8" 15 | ); 16 | await db.query(schemaSql); 17 | } 18 | } 19 | 20 | resetDatabase(); 21 | 22 | export default resetDatabase; 23 | -------------------------------------------------------------------------------- /guides/code-quality/readme.md: -------------------------------------------------------------------------------- 1 | # Code quality improvements (Optional) 2 | 3 | While the following work items are completely optional, they will help you to make fewer mistakes as you continue with future requirements. It is generally a good idea to have them when working on projects of any size. 4 | 5 | ## 1) Proper error handling 6 | 7 | You should make your system to be designed with error handling in mind. For example if the database cannot be accessed when you call `GET /api/videos`, then your backend endpoint should return a properly formatted error message with a HTTP `500` error code. 8 | 9 | Here is an example response: 10 | 11 | ```json 12 | { "success": false, "error": "Could not connect to the database" } 13 | ``` 14 | 15 | **Note:** You can design how you return error messages differently than the above example. You could also try and merge the error and non-error response styles into one. For example the standard `200` response on the same endpoint could be something like the following: 16 | 17 | ```json 18 | {"success":true,"videos":[(...)]} 19 | ``` 20 | 21 | Once you do this on the backend you should also change your frontend, to make sure it handles errors that are received. For example if your frontend receives an error like above it might show a message like `"Could not connect to the database, please reload the page at a later time"`. Remember, real users don't look in the console, so you'll need to work how where you want to display this error to the users so they see it. 22 | 23 | **Note:** Once you add this feature, make sure you keep handling errors properly during the week 2 and week 3 requirements as well. 24 | 25 | ## 2) Prettying and linting 26 | 27 | It is also usually a good idea to make sure that your code is formatted based on a single standard throughout, and also passes basic checks. There are two projects that can usually help you with that: 28 | 29 | - `prettier` is a formatter that makes sure that your code is formatted the same way throughout. For example all files use `tab` characters for indenting. 30 | - `eslint` is a linter that checks the code for common coding mistakes and warns you if it encounters any of them. It can also automatically fix some mistakes. 31 | 32 | Let's set up both of them! 33 | 34 | ### `prettier` 35 | 36 | First install prettier into your `package.json` file: 37 | 38 | ```sh 39 | npm install prettier --save-dev 40 | ``` 41 | 42 | Next you will need a `.prettierrc` file in the root directory. We have already provided one for your convenience. 43 | 44 | You can now run prettier to check your files: 45 | 46 | ```sh 47 | npm exec prettier -- --check . 48 | ``` 49 | 50 | And also to automatically fix them: 51 | 52 | ```sh 53 | npm exec prettier -- --write . 54 | ``` 55 | 56 | If you don't want to type out these commands you can add them as `scripts` into your `package.json` file. For example you can add a line like: 57 | 58 | ```json 59 | "prettier": "prettier --write ." 60 | ``` 61 | 62 | to the scripts section, and then you'll be able to automatically pretty your files by typing: 63 | 64 | ```sh 65 | npm run prettier 66 | ``` 67 | 68 | ### `eslint` 69 | 70 | Installing `eslint` is similar, but to get the most out of it you will need to install multiple projects: 71 | 72 | ```sh 73 | npm install eslint eslint-config-prettier eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-refresh eslint-plugin-react-hooks eslint-plugin-n eslint-plugin-jest eslint-plugin-jest-dom eslint-plugin-vitest eslint-plugin-testing-library @codeyourfuture/eslint-config-standard --save-dev 74 | ``` 75 | 76 | This will install `eslint`, and a couple plugins that help you with validating tests, JSX code and React components, like proper `useEffect` usage. 77 | 78 | You will also need to have multiple `.eslintrc.json` files, one for each project, tailored to that project's needs. These have also all been provided already. 79 | 80 | Once you have everything in place you can run the linter to check for common code mistakes: 81 | 82 | ```sh 83 | npm exec eslint . 84 | ``` 85 | 86 | You can also ask it to fix any issues it can automatically fix: 87 | 88 | ```sh 89 | npm exec eslint -- --fix . 90 | ``` 91 | 92 | Same as for `prettier`, you might want to add these commands to your `package.json` for easier access. 93 | 94 | ### Checks during PRs 95 | 96 | It is a good idea to enforce running both the linter and prettier during PRs. One way to do that is to make sure GitHub runs these checks for every PR, blocking the PR in case the code doesn't pass these checks. We have already prepared the `.github/workflows/enforce-linting.yml` file that you can use to run checks on GitHub manually. If you also uncomment the `pull_request` line, you will enable GitHub to run these checks automatically on every PR. 97 | 98 | To confirm, the top of the file should look like this: 99 | 100 | ```yaml 101 | on: 102 | workflow_dispatch: 103 | pull_request: 104 | ``` 105 | 106 | **Note:** Make sure to have a full read of this file and try to figure out what it does. 107 | -------------------------------------------------------------------------------- /guides/db-setup/readme.md: -------------------------------------------------------------------------------- 1 | # Local database setup ⚙️ 2 | 3 | ## Initialising the Database 4 | 5 | ### Create a local database 6 | 7 | Use the `createdb` command to create a new database called `videorec`: 8 | 9 | ```bash 10 | createdb videorec 11 | ``` 12 | 13 | ### Create tables and load data 14 | 15 | Create tables and load the database with initial data: 16 | 17 | ```bash 18 | psql -d videorec < db/initdb.sql 19 | ``` 20 | 21 | #### Summary 22 | 23 | 1. `psql` : Use the PostgreSQL command-line interface. 24 | 2. `-d` : This flag marks the next argument as the database name. 25 | 3. `videorec` : The name of the database you want to populate. 26 | 4. `<` : The following file will be used as input. 27 | 5. `db/initdb.sql` : The path to the SQL file to populate the database. 28 | 29 | Depending how postgresql was installed for you, you might need to add some connectivity details to both createdb and psql: `psql -h localhost -U username -d videorec < db/initdb.sql` In this example you ask postgres to connect to your local database through localhost and use username as the user. 30 | 31 | ##### Check 📝 32 | 33 | Double-check the command you just used was successful. What should you expect to see in your local database after running this command. 34 | 35 | ### Backup your database 36 | 37 | Now, let's create a compressed archive of your database for safekeeping. Use the `pg_dump` command with the following options: 38 | 39 | ```bash 40 | pg_dump -h localhost -U username videorec > videorec_backup.sql.gz 41 | ``` 42 | 43 | ##### Explanation 44 | 45 | - `pg_dump`: This command is designed specifically for creating PostgreSQL database backups. 46 | - `-h localhost` (Optional): Specify the host (`localhost` in most cases) if your PostgreSQL installation differs. 47 | - `-U username` (Optional): Include your username if required for connection. 48 | - `videorec`: This is the name of the database you want to back up. 49 | - `> videorec_backup.sql.gz`:\*\* This defines the filename and format for the backup. The `>` redirects the output to a file, and `.sql.gz` indicates a gzipped SQL archive. 50 | 51 | **Verification:** Check your terminal or file explorer to confirm the existence of the backup file (`videorec_backup.sql.gz`). 52 | 53 | #### Stretch: Customize Backup Location 54 | 55 | - You can modify the output filename and location to suit your preference. For example: 56 | 57 | ```bash 58 | pg_dump -h localhost -U username videorec > backups/videorec_backup_$(date +"%Y-%m-%d").sql.gz 59 | ``` 60 | 61 | - This command incorporates the current date in the filename for easy identification and versioning. 62 | 63 | ### Removing, Re-initializing, and Restoring 64 | 65 | Now that we have a backup, let's practice removing and re-initializing the database: 66 | 67 | #### 1. Drop the Database 68 | 69 | Use the `dropdb` command followed by the database name (`videorec`) to remove the database: 70 | 71 | ```bash 72 | dropdb videorec 73 | ``` 74 | 75 | **Confirmation:** Verify that the database is gone by trying to connect to it with `psql -d videorec`. You should receive an error message indicating the database doesn't exist. 76 | 77 | #### 2. Recreate the Database 78 | 79 | Use the same `createdb` command from before to create a new empty database with the same name (`videorec`): 80 | 81 | ```bash 82 | createdb videorec 83 | ``` 84 | 85 | #### 3. Restore from Backup 86 | 87 | Use `psql` with the `-f` flag to specify the backup file and restore the data into the newly created database: 88 | 89 | ```bash 90 | psql -d videorec -f videorec_backup.sql.gz 91 | ``` 92 | 93 | #### 4. Verify 94 | 95 | Connect to the database (`psql -d videorec`) to confirm the tables and data have been restored successfully. 96 | 97 | ## Sample data 98 | 99 | You will need some example video data in your database. Please check [the example data](./data/example_data.csv), and modify your `initdb.sql`. Add the relevant `INSERT INTO` calls that will add all of this example data every time you run `initdb.sql`. 100 | -------------------------------------------------------------------------------- /guides/e2e-tests/readme.md: -------------------------------------------------------------------------------- 1 | # End-to-end tests 🧪 2 | 3 | While unit and integration tests usually only check part of the stack (like the frontend tests only check the frontend but don't connect to the backend), end-to-end (E2E) tests are specifically designed to check the interaction of the entirety of the stack - frontend, backend and database. 4 | 5 | ## Playwright 6 | 7 | The biggest E2E frameworks are Selenium, Cypress and Playwright. Each of them allow you to run a browser instance and automate what happens inside it. For this requirement, we picked Playwright as the framework. 8 | 9 | For example you can write Playwright code that opens up your application, then clicks the "Remove video" button on the page, checking that the video is indeed removed from the website. 10 | 11 | ## Setup 12 | 13 | Just with the other tests, we have helped you get started by setting up a test runner for your feature tests, and adding a couple tests to `e2e/tests/features.test.js`. These tests would go through the website and check that you can do all of the required features. 14 | 15 | **Note:** for this to work you need to make sure you have setup the test backend database. If you haven't done so please refer to that section to set up your test database first. 16 | 17 | To run your end-to-end tests there are three steps you need to do: 18 | 19 | 1. Install Playwright browsers 20 | 21 | Playwright will use its own browsers instead of the one installed on your computer. To install the browsers type 22 | 23 | ```sh 24 | npx playwright install 25 | ``` 26 | 27 | You'll only need to do this once. 28 | 29 | 2. Stop any running server 30 | 31 | Playwright will start up your backend and frontend automatically, but it will not be able to do that properly if you're already running them 32 | 33 | 3. Start the feature tests: 34 | 35 | ```sh 36 | npm run test:e2e 37 | ``` 38 | 39 | This command will run the test in the background. If you want to check what Playwright does in the browser you can use 40 | 41 | ```sh 42 | npm run test:e2e:headed 43 | ``` 44 | 45 | This will start up the browsers in the foreground. Both options will then click around very quickly trying to run the features - adding and deleting videos as well as ranking existing videos up and down. 46 | 47 | (Playwright also has a full UI mode where you can play around with your tests. This is accessible via `npm run test:e2e:ui`) 48 | 49 | **Note:** just like with the other pre-created tests this might fail depending on how you have implemented your frontend, backend and database parts. Please check the test code and update it to make sure it runs successfully. Some changes that you need to do will be similar to the level 299 frontend tests - like if you don't have a `"Remove video"` button but have something else you need to change the code to find that button. Similarly, the current test assumes that the HTML structure of your video components look like the one below: 50 | 51 | ```html 52 |
53 |

54 | The title of the Video 55 |

56 | 57 |
58 | ``` 59 | 60 | Here to get from the title to the video container you need to go two levels up. First from `` to `

`, then from this `

` to `
`. If your website is structured differently you will need to update this in the tests. 61 | 62 | ## Enable PR tests 63 | 64 | Tests are useful to run every time you create a PR to make sure you don't accidentally add or change code that breaks existing functionality. You can go to `.github/workflows/run-e2e.yml` and remove the comment from the line that says `pull_request:`. This will run the `npm run test:e2e` call every time you create a new PR blocking merging in case tests fail. 65 | 66 | ## Add new test cases 67 | 68 | You might want to add tests to cover some additional scenarios. For example if you have opted into doing the ordering feature you might want to add a test that checks that sorting by ascending and descending really updates the page to sort accordingly. 69 | 70 | ## Next Level 71 | 72 | Once finished please check if you have missed any optional features before, and finish them. If you have done all of them, then congratulations, you have finished this exercise 100%! 73 | 74 | Feel free to use this project as part of your portfolio. You might also use it as a base project to start with testing out new ideas, frameworks or deployment services. 75 | -------------------------------------------------------------------------------- /guides/setup/readme.md: -------------------------------------------------------------------------------- 1 | ## Setting up the project 🧰 2 | 3 | We have set up an initial project for you to help with some of the complexities of the assessment. This project requires Node.JS version 20 or later. Make sure you have at least Node version 20 running: 4 | 5 | ```sh 6 | node -v 7 | ``` 8 | 9 | The version number should be `v20.6.0` or higher: 10 | 11 | ``` 12 | v20.8.0 13 | ``` 14 | 15 | If this is lower, like `v18.18.2`, or `v20.5.5` you will need to install a more recent version of Node.JS. Refer to the [node installation guides](https://nodejs.org/en/download/package-manager) for your operating system. For Ubuntu based systems [NodeSource](https://github.com/nodesource/distributions) is usually a good way to do it. 16 | 17 | Once you have confirmed that your node version is recent enough you can install the requirements. Change directory to the root of your monorepo. Then run the following commands: 18 | 19 | ```sh 20 | npm install 21 | ``` 22 | 23 | Then to start up your frontend and backend locally, you can run 24 | 25 | ```sh 26 | npm run dev 27 | ``` 28 | 29 | To confirm your server is running go to 30 | 31 | ```url 32 | http://127.0.0.1:3100/health 33 | ``` 34 | 35 | in your browser. If the server is running you'll see a message saying 36 | 37 | ```json 38 | OK 39 | ``` 40 | 41 | ## Before you commit your changes 42 | 43 | Read this [article on .gitignore](https://sabe.io/blog/git-ignore-node_modules). We have set up a basic `.gitignore` file for you. 44 | 45 | ## Frontend setup 46 | 47 | Since we are using a monorepo, you can launch the frontend the same way you launch the backend. Run the following command in the root of your repo: 48 | 49 | ```sh 50 | npm run dev 51 | ``` 52 | 53 | The code you put under `client/src` will then be accessible on http://localhost:3000 54 | 55 | ## Monorepo 56 | 57 | The project is set up to run as a monorepo, where both the [`client`](../../client/) and [`server`](../../server/) source code live in the same git repository and are linked together. When using monorepos, there is some boilerplate code required to make sure both the frontend and the backend application can work at the same time and are accessible on the same URL. To kickstart your development we have set up this boilerplate code for you already. Feel free to look at the code, but generally you won't need to edit them if you follow the proposals in this guide. 58 | 59 | If you are interested you can read more about what they do in the following places: 60 | 61 | - [Client vite settings](../client/vite.config.js) 62 | - [Server frontend middleware](../server/app.js) 63 | -------------------------------------------------------------------------------- /guides/testing/readme.md: -------------------------------------------------------------------------------- 1 | # Automated tests (Optional) 2 | 3 | The below points are improvement ideas that you can add to your project to become a great project. If you want this project to be part of your portfolio we suggest that you implement at least some of the optional features. You don't need to do all of them during this week, you are free to revisit this list later 4 | 5 | ## 1) Backend unit tests 6 | 7 | Automated tests are code that, when run, will check that your code adheres to some pre-set conditions, usually your acceptance criteria. With the help of automated tests you can make sure that even if you add new features to your code, your existing features remain working. Breaking existing features when adding new ones is very common in the engineering world, there is a word for it as well: "regression". 8 | 9 | Unit tests only check a small subset (a unit) of your application at a time. These tests can usually be both written and run quickly so they are a good way of determining that the application is doing what it should be doing. 10 | 11 | You can test your backend in isolation, your frontend in isolation and you can also have tests that check both of them at the same time. For this level, we will start by adding tests for your backend. 12 | 13 | ### Setting up backend unit tests ⚙️ 14 | 15 | We have helped you get started by setting up a test runner for your backend systems and adding a couple tests to `server/api.test.js`. These tests would check that the list and delete endpoints works as expected. 16 | 17 | First you need to set up the tests. To do that make sure you create a separate database for tests. For example let's call it `videorec_test`: 18 | 19 | ```sh 20 | createdb videorec_test 21 | ``` 22 | 23 | Afterwards edit your `.env` file and change the `TEST_DATABASE_URL` to point to this database. 24 | 25 | **Note:** we are using a different database to make sure that we don't accidentally modify your normal database every time when you run your tests. 26 | 27 | Once you have set up the database settings you can now run the tests by calling 28 | 29 | ```sh 30 | npm run test:server 31 | ``` 32 | 33 | Check the response, it will tell you if the tests have succeeded or not. It is possible that you will need to update the test to cater for how you have implemented your backend. 34 | 35 | ### Transactional tests 36 | 37 | The test runner above is set up to have transactional tests. What this means is that whenever you start the testing session, the code will reset your (test) database using `initdb.sql`. Afterwards it will run each test in a database transaction, rolling it back at the end of the test. Database transactions are a feature of most relational databases that allows you to run SQL commands in a temporary environment first, and only if you are happy with the results will they be saved to the database. It's a bit like editing a file and not saving it until you are happy with the result. 38 | 39 | We use this feature during test runs. We effectively ask the database to never save our changes at the end of the tests. This allows each test case to start with the exact same, initial database as every other test. For example when testing the video removal features, during the test the video will be removed, but once the tests are run, the database is reset, and subsequent tests will still be able to access the deleted video. 40 | 41 | ### Enable PR tests 42 | 43 | Tests are made much more useful if they run every time you create a PR. This way GitHub will make sure to run them and let you know if your latest code is not working anymore based on the tests. To enable backend tests you should to `.github/workflows/run-server-tests.yml` and remove the comment from the line that says `pull_request:`. This will run the `npm run test:server` call every time you create a new PR blocking merging in case tests fail. 44 | 45 | To confirm, the top of the file should look like this: 46 | 47 | ```yaml 48 | on: 49 | workflow_dispatch: 50 | pull_request: 51 | ``` 52 | 53 | This will allow GitHub to run the defined action on each pull request, and you can also run it ad-hoc from the "Actions" page whenever you wish. 54 | 55 | **Note:** Make sure to have a full read of this file and try to figure out what it does. 56 | 57 | ### Adding additional tests 58 | 59 | Based on the existing tests in the system you could try and create one that checks the add video functionality. It should verify that videos are added if the request is formatted properly, but fail if the request is missing the title, or the URL is incorrect. 60 | 61 | ## 2) Frontend unit tests 62 | 63 | Similar to the backend unit tests we can create tests that verify that the frontend is working as expected in an automated way. 64 | 65 | ### Setting up frontend unit tests ⚙️ 66 | 67 | To setup the frontend tests we have helped you get started by adding a couple tests to `client/test/App.test.js`. These would check that the page, after loading, contains enough `