Compare commits

..

10 Commits

Author SHA1 Message Date
13ad4d7439 Feat: docker compose done 2025-04-18 20:23:21 +08:00
YuBin, Hsu
947c10f231 Merge pull request #7 from gnsJhenJie/patch-1
Fix typo in todo.spec.ts
2025-03-25 19:31:57 +08:00
Jhen-Jie Hsieh
d6e7ed73c2 fix: Fix typo
Fix typo in comment
2025-03-23 22:58:02 +08:00
yubintw
ca12654443 docs: update README.md to reflect changes from Cypress to Playwright for end-to-end testing 2025-03-18 23:30:54 +08:00
yubintw
a84687681d test: add playground lab to practice 2025-03-18 23:24:32 +08:00
yubintw
692da86eae chore: add VSCode extensions recommendations and update devcontainer configuration 2025-03-15 19:39:33 +08:00
yubintw
bc51179cd4 build: update dependencies 2025-03-15 19:39:19 +08:00
yubintw
d8d08b9c7b test: uninstall cypress, add playwright 2025-03-15 19:22:09 +08:00
yubintw
e4bacf8745 docs: update README.md to format code blocks and improve clarity 2025-02-27 00:01:21 +08:00
YuBin, Hsu
a5c4cca7a6 build: add raw option for concurrently 2024-03-27 13:48:38 +00:00
23 changed files with 4902 additions and 8781 deletions

View File

@@ -12,7 +12,19 @@
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [6080, 5901],
"forwardPorts": [
6080,
5901
],
"customizations": {
"vscode": {
"extensions": [
"streetsidesoftware.code-spell-checker",
"EditorConfig.EditorConfig",
"dbaeumer.vscode-eslint"
]
}
},
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"streetsidesoftware.code-spell-checker",
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint"
]
}

View File

@@ -1,5 +1,6 @@
{
"cSpell.words": [
"fabonacci",
"fastify",
"todos"
]

View File

@@ -6,7 +6,7 @@ Fastify Server
### Set your environment variable
```
```bash
cd backend
cp .env.sample .env
```
@@ -14,23 +14,26 @@ cp .env.sample .env
### Development
Run a mongo container
```
```bash
docker run -d -p 27017:27017 mongo
```
Install dependencies
```
```bash
npm install
```
Start development mode
```
```bash
npm run dev
```
### Run the test
```
```bash
npm run test
```
@@ -41,21 +44,28 @@ React (by vite)
### Development
Install dependencies
```
```bash
npm install
```
Start development mode
```
```bash
cd frontend
npm run dev
```
Visit
http://localhost:5173
Visit <http://localhost:5173>
### Run cypress test
### Run playwright test
```bash
npm run test:e2e
```
show playwright report
```bash
npx playwright show-report
```
npm run cy:test
```

31
backend/Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
FROM node:22 AS deps
WORKDIR /app
COPY ./package.json ./package-lock.json .
RUN npm ci --omit=dev
#####
FROM node:22 AS devdeps
WORKDIR /app
COPY ./package.json ./package-lock.json ./tsconfig.json .
RUN npm ci
#####
FROM node:22 AS build
WORKDIR /app
COPY . /app
COPY --from=devdeps /app/node_modules /app/node_modules
RUN npm run build
#####
FROM gcr.io/distroless/nodejs22-debian12
WORKDIR /app
COPY --from=build /app /app
COPY --from=deps /app/node_modules /app/node_modules
CMD ["dist/index.js"]

4708
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,24 +5,24 @@
"main": "index.js",
"scripts": {
"test": "vitest run --coverage",
"build": "tsc",
"build": "npx tsc",
"start": "node dist/index.js",
"dev": "concurrently \"tsc -w \" \"nodemon dist/index.js\""
"dev": "concurrently --raw \"tsc -w \" \"nodemon dist/index.js\""
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"fastify": "^4.26.2",
"mongoose": "^8.2.3"
"fastify": "^5.2.1",
"mongoose": "^8.12.1"
},
"devDependencies": {
"@types/node": "^20.11.30",
"@vitest/coverage-v8": "^1.4.0",
"concurrently": "^8.2.2",
"dotenv": "^16.4.5",
"nodemon": "^3.1.0",
"typescript": "^5.4.3",
"vitest": "^1.4.0"
"@types/node": "^22.13.10",
"@vitest/coverage-v8": "^3.0.8",
"concurrently": "^9.1.2",
"dotenv": "^16.4.7",
"nodemon": "^3.1.9",
"typescript": "^5.8.2",
"vitest": "^3.0.8"
}
}

10
backend/src/utils/math.ts Normal file
View File

@@ -0,0 +1,10 @@
export function myCustomAdd(a: number, b: number): number {
return a + b
}
export function fabonacci(n: number): number {
if (n === 1 || n === 2) {
return 1
}
return fabonacci(n - 1) + fabonacci(n - 2)
}

View File

@@ -0,0 +1,35 @@
import { describe, test, expect, it } from 'vitest'
import { myCustomAdd, fabonacci } from '../src/utils/math'
import { fail } from 'assert'
describe('my testing playground', () => {
test('it works', () => {
const expected = true
const actual = false
expect(actual).toBe(expected)
})
describe('add function testing', () => {
it('should return 3 when add 1 and 2', () => {
expect(myCustomAdd(1, 2)).toBe(3)
})
it('should return 5 when add 2 and 3', () => {
// TODO: fix the test
fail('not implemented')
})
})
describe('fabonacci testing', () => {
it('should return 1 when n is 1', () => {
expect(fabonacci(1)).toBe(1)
})
it('should return 1 when n is 2', () => {
// TODO: fix the test
fail('not implemented')
})
it('should return 2 when n is 3', () => {
// TODO: fix the test
fail('not implemented')
})
})
})

View File

@@ -10,7 +10,7 @@ describe('Todo API Testing', () => {
})
test('Given an empty array return from repo function, When send a GET request to /api/v1/todos, Then it should response an empty array', async () => {
// assert: stub the repo function to return an empty array
// arrange: stub the repo function to return an empty array
vi.spyOn(TodoRepo, 'findAllTodos').mockImplementation(async () => [])
// act: send a GET request to /api/v1/todos

24
docker-compose.yml Normal file
View File

@@ -0,0 +1,24 @@
---
services:
db:
image: mongo:8.0
volumes:
- mongo:/data/db
backend:
build:
context: ./backend
environment:
PORT: 8888
HOST: 0.0.0.0
MONGO_CONNECTION_STRING: mongodb://db:27017/todo
depends_on:
- db
frontend:
build:
context: ./frontend
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
ports:
- 8080:80
volumes:
mongo: {}

6
frontend/.gitignore vendored
View File

@@ -22,3 +22,9 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

21
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM node:22 AS devdeps
WORKDIR /app
COPY ./package.json ./package-lock.json .
RUN npm ci
#####
FROM node:22 AS build
WORKDIR /app
COPY . /app
COPY --from=devdeps /app/node_modules /app/node_modules
RUN npm run build
#####
FROM nginx:1.27.5-alpine-slim
WORKDIR /app
COPY --from=build /app/dist /usr/share/nginx/html

View File

@@ -1,10 +0,0 @@
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
baseUrl: 'http://localhost:5173'
}
})

View File

@@ -1,6 +0,0 @@
describe('My Todo Website Test', () => {
it('Should load my website "/" and contain title "My Todos"', () => {
cy.visit('/')
cy.contains('My Todos')
})
})

View File

@@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -1,37 +0,0 @@
/// <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>
// }
// }
// }

View File

@@ -1,20 +0,0 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +1,34 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build": "npx tsc && npx vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"cy:test": "cypress run --headed --no-exit"
"test:e2e": "playwright test"
},
"dependencies": {
"axios": "^1.6.8",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"axios": "^1.8.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.2",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
"cypress": "^13.7.1",
"eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"jsdom": "^24.0.0",
"typescript": "^5.2.2",
"vite": "^5.0.8"
"@playwright/test": "^1.51.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^14.3.1",
"@types/node": "^22.13.10",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^8.57.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.19",
"jsdom": "^26.0.0",
"typescript": "^5.8.2",
"vite": "^6.2.2"
}
}

View File

@@ -0,0 +1,79 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

View File

@@ -0,0 +1,18 @@
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click();
// Expects page to have a heading with the name of Installation.
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

47
nginx.conf Normal file
View File

@@ -0,0 +1,47 @@
server {
listen 80;
server_name localhost;
#access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /api {
proxy_pass http://backend:8888;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}