diff --git a/.dockerignore b/.dockerignore index d571957..2f03a01 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,3 @@ -*/node_modules +**/node_modules .git */dist \ No newline at end of file diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..9bd9af8 --- /dev/null +++ b/.env.dev @@ -0,0 +1,4 @@ +TESTING=true +TESTING_USERNAME=alex +TESTING_PASSWORD=Alex1234 +SECRET_KEY=b1d2e917-17b9-4fce-9055-082dee6ec5d6 \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index afc6b85..d177747 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -63,9 +63,10 @@ module.exports = { }, overrides: [ { - files: ['**/tests/**/*.js', '**/tests/**/*.ts'], // Adjust the pattern to match your test files + files: ['**/tests/**/*.js', '**/tests/**/*.ts', '**/*.test.ts'], // Adjust the pattern to match your test files rules: { 'import/extensions': 'off', // Turn off the rule for test files + 'import/no-extraneous-dependencies': 'off', }, }, ], diff --git a/README.md b/README.md index 870d116..fd56c86 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,22 @@ ## Development -Run everything (frontend, backend, db, swagger, quests): +Run mongoDB first: - docker compose build - docker compose up -d + docker compose -f docker-compose.dev.yml up -d db + +The db will automatically restarts by Docker. + +Build up dependencies: + + yarn build:deps + +Run dev server (frontend, backend): + + yarn dev If you want to enable social login, you should have OAuth client token in a `.env` file: - docker compose --env-file .env up -d - ### Build Quest Images In each quest folder, there is a `game.yml` file, which is the quest definition. And a optional `Dockerfile` to overwrite the base image. If you want to include additional files, put them in a `home` folder. The files in `home` will **not** be copied to the image automatically, you have to copy them in the `Dockerfile`. @@ -144,4 +151,3 @@ connect to other host: list sessions: yarn cli list - \ No newline at end of file diff --git a/app/Dockerfile b/app/Dockerfile index f8806ac..506854c 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -7,7 +7,7 @@ COPY packages/file-graph/package.json ./packages/file-graph/package.json COPY packages/utils/package.json ./packages/utils/package.json COPY packages/constants/package.json ./packages/constants/package.json COPY app/package.json ./app/package.json -RUN yarn install +RUN yarn install # Dev stage FROM base as dev diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3c98144..b35aba1 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -74,6 +74,7 @@ services: db: image: mongo + restart: always networks: - internal ports: diff --git a/docker-compose.testing.yml b/docker-compose.testing.yml index 18f0f97..5c67018 100644 --- a/docker-compose.testing.yml +++ b/docker-compose.testing.yml @@ -12,6 +12,7 @@ services: - BASE_URL=http://app-test - TESTING_USERNAME=alex - TESTING_PASSWORD=Alex1234 + # - DEBUG=cypress:* depends_on: - app-test @@ -23,8 +24,9 @@ services: target: prod networks: internal: - ports: - - 5173:80 + # Only open the port for debugging + # ports: + # - 5173:80 depends_on: - backend @@ -71,8 +73,8 @@ services: - MONGO_USER=user - MONGO_PASS=user-password - MONGO_DB=odyssey-test - ports: - - 27017:27017 + # ports: + # - 27017:27017 base: image: linuxodyssey/quest-base diff --git a/package.json b/package.json index 1f8d8f7..04954b1 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "build:quests": "yarn workspace server build:quests" }, "devDependencies": { + "@jest/globals": "^29.7.0", "@types/jest": "^29.5.12", "@types/node": "^20.10.4", "@typescript-eslint/eslint-plugin": "^6.14.0", diff --git a/packages/file-graph/tests/file.test.ts b/packages/file-graph/tests/file.test.ts index 0524f93..57eba95 100644 --- a/packages/file-graph/tests/file.test.ts +++ b/packages/file-graph/tests/file.test.ts @@ -1,4 +1,5 @@ -import File from '../src/file' +import { describe, test, expect } from '@jest/globals' +import File from '../src/file.js' describe('File', () => { let file: File diff --git a/packages/file-graph/tests/fileGraph.test.ts b/packages/file-graph/tests/fileGraph.test.ts index fd21202..c9ff417 100644 --- a/packages/file-graph/tests/fileGraph.test.ts +++ b/packages/file-graph/tests/fileGraph.test.ts @@ -1,3 +1,4 @@ +import { describe, test, expect } from '@jest/globals' import FileGraph from '../src/fileGraph.js' import FileNode from '../src/fileNode.js' diff --git a/packages/file-graph/tests/fileNode.test.ts b/packages/file-graph/tests/fileNode.test.ts index 3ccb3e1..14c6818 100644 --- a/packages/file-graph/tests/fileNode.test.ts +++ b/packages/file-graph/tests/fileNode.test.ts @@ -1,3 +1,4 @@ +import { describe, test, expect } from '@jest/globals' import { DuplicateItemError, ParentNotExistsError } from '../src/errors.js' import FileNode from '../src/fileNode.js' import { FileObject } from '../src/file.js' diff --git a/packages/utils/tests/auth.test.ts b/packages/utils/tests/auth.test.ts index 28593f7..84eff27 100644 --- a/packages/utils/tests/auth.test.ts +++ b/packages/utils/tests/auth.test.ts @@ -1,3 +1,4 @@ +import { test, expect } from '@jest/globals' import { isValidEmail, isValidUsername } from '../src/auth.js' test('check valid username', () => { diff --git a/packages/utils/tests/commands.test.ts b/packages/utils/tests/commands.test.ts index 7730678..77d02d1 100644 --- a/packages/utils/tests/commands.test.ts +++ b/packages/utils/tests/commands.test.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from '@jest/globals' import { buildFileCheckCmd, File } from '../src/commands.js' describe('buildFileCheckCmd function', () => { diff --git a/packages/utils/tests/dag.test.ts b/packages/utils/tests/dag.test.ts index ff631c0..fbe860d 100644 --- a/packages/utils/tests/dag.test.ts +++ b/packages/utils/tests/dag.test.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from '@jest/globals' import DAG from '../src/dag.js' describe('dag layers', () => { diff --git a/server/src/config.ts b/server/src/config.ts index adc0863..96377a1 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -4,6 +4,9 @@ import { get } from './utils/env.js' // Import dotenv and load ../.env dotenv.config({ path: '../.env' }) +if (process.env.NODE_ENV !== 'production') { + dotenv.config({ path: '../.env.dev' }) +} function getTrustProxies(key: string): string[] { const value = process.env[key] diff --git a/tests/Dockerfile b/tests/Dockerfile index 1dc0d4a..d5c96d2 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -3,7 +3,7 @@ FROM cypress/browsers WORKDIR /e2e COPY package.json yarn.lock ./ COPY tests/package.json ./tests/package.json -RUN yarn install +RUN yarn install --frozen-lockfile COPY quests ./quests COPY tests ./tests diff --git a/tests/cypress.config.js b/tests/cypress.config.ts similarity index 69% rename from tests/cypress.config.js rename to tests/cypress.config.ts index b2bb72e..ce86fe3 100644 --- a/tests/cypress.config.js +++ b/tests/cypress.config.ts @@ -1,20 +1,27 @@ -const { defineConfig } = require('cypress') -const process = require('process') -require('dotenv').config({ path: '../.env' }) +import { defineConfig } from 'cypress' +import process from 'process' +import dotenv from 'dotenv' +import fs from 'fs' // Import fs module + +dotenv.config({ path: '../.env.dev' }) +dotenv.config({ path: '../.env' }) const getConfig = (key, defaultValue) => { return process.env[key] || defaultValue } const getOrFail = (key) => { - const value = getConfig(key) + const value = getConfig(key, null) if (!value) { throw new Error(`Missing required environment variable ${key}`) } return value } -module.exports = defineConfig({ +// Check if the .env file exists +const EnableoAuth = fs.existsSync('../.env') + +export default defineConfig({ e2e: { baseUrl: getConfig('BASE_URL', 'http://localhost:5173'), viewportWidth: 1920, @@ -28,13 +35,13 @@ module.exports = defineConfig({ defaultAccount: getOrFail('TESTING_USERNAME'), defaultPassword: getOrFail('TESTING_PASSWORD'), isCHVersion: true, + EnableoAuth, // Add the EnableoAuth variable here // google_username: process.env.GOOGLE_USERNAME, // google_password: process.env.GOOGLE_PASSWORD, // google_client_id: process.env.GOOGLE_CLIENT_ID, - // google_client_secret: process.env.GOOGLE_CLIENT_SECRET, + // github_username: process.env.GITHUB_USERNAME, // github_password: process.env.GITHUB_PASSWORD, // github_client_id: process.env.GITHUB_CLIENT_ID, - // github_client_secret: process.env.GITHUB_CLIENT_SECRET, }, }) diff --git a/tests/cypress/e2e/UItests.cy.js b/tests/cypress/e2e/UItests.cy.ts similarity index 85% rename from tests/cypress/e2e/UItests.cy.js rename to tests/cypress/e2e/UItests.cy.ts index c2ecfd7..1ea8646 100644 --- a/tests/cypress/e2e/UItests.cy.js +++ b/tests/cypress/e2e/UItests.cy.ts @@ -1,4 +1,3 @@ -/// export default function checkLoginUI() { cy.get('#title').should('contain', 'Linux Odyssey').and('be.visible') cy.CheckPlaceholder('#password', '密碼', 'Password') @@ -37,17 +36,19 @@ describe('example helloworld app', () => { cy.CheckTextElement('#SwitchtoRegister', '註冊', 'Sign up') }) it('Check social account UI', () => { - cy.CheckTextElement( - '#GoogleLogin', - '以 Google 繼續', - 'Continue with Google' - ) - cy.CheckTextElement('#or', '或', 'or') - cy.CheckTextElement( - '#GitHubLogin', - '以 GitHub 繼續', - 'Continue with GitHub' - ) + if (Cypress.env('EnableoAuth')) { + cy.CheckTextElement( + '#GoogleLogin', + '以 Google 繼續', + 'Continue with Google' + ) + cy.CheckTextElement('#or', '或', 'or') + cy.CheckTextElement( + '#GitHubLogin', + '以 GitHub 繼續', + 'Continue with GitHub' + ) + } }) it('Check login fail', () => { cy.LoginWithPassword('dddd', '123456') @@ -96,20 +97,18 @@ describe('example helloworld app', () => { cy.CheckTextElement('#tasks', '任務', 'Tasks:') }) it('Check Terminal', () => { - cy.get('#terminal', { timeout: 20000 }).within(($terminal) => { - cy.get($terminal).should('be.visible') - cy.get('svg[data-icon="terminal"]').should('be.visible') - cy.CheckTextElement('#Terminal', '終端機', 'Terminal') - cy.get('@Terminaltextbox') - .should('be.visible') - .and('contain', 'commander:~ $') - }) + // cy.get('#terminal', { timeout: 20000 }).within(($terminal) => { + // cy.get($terminal).should('be.visible') + cy.get('#terminal', { timeout: 20000 }).should('be.visible') + cy.get('svg[data-icon="terminal"]').should('be.visible') + cy.CheckTextElement('#Terminal', '終端機', 'Terminal') + cy.get('@Terminaltextbox') + .should('be.visible') + .and('contain', 'commander:~ $') }) it('Check Hint Part', () => { - cy.get('#hint', { timeout: 20000 }).within(($hint) => { - cy.get($hint).should('be.visible') - cy.get('svg[data-icon="lightbulb"]').should('be.visible') - }) + cy.get('#hint', { timeout: 20000 }).should('be.visible') + cy.get('svg[data-icon="lightbulb"]').should('be.visible') cy.CheckTextElement('#hint', '提示', 'Hint') }) it('Check File TreeChart', () => { diff --git a/tests/cypress/e2e/completeQuests.cy.js b/tests/cypress/e2e/completeQuests.cy.ts similarity index 96% rename from tests/cypress/e2e/completeQuests.cy.js rename to tests/cypress/e2e/completeQuests.cy.ts index 3ef0aab..57ba8e2 100644 --- a/tests/cypress/e2e/completeQuests.cy.js +++ b/tests/cypress/e2e/completeQuests.cy.ts @@ -1,4 +1,3 @@ -/// describe('Complete quests with only commands', () => { beforeEach(() => { cy.PrepareForGame() diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.ts similarity index 70% rename from tests/cypress/support/commands.js rename to tests/cypress/support/commands.ts index 9428779..b77cd88 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.ts @@ -1,7 +1,38 @@ +/* eslint-disable no-undef */ +/* eslint-disable no-unused-vars */ +/// import '@testing-library/cypress/add-commands' +declare const expect: Chai.ExpectStatic +declare global { + namespace Cypress { + interface Chainable { + LoginWithPassword(username: string, password: string): Chainable + PrepareForGame(): Chainable + InitTerminal(): Chainable + typeInCommand(command: string): Chainable + getQuestInfo(id: string): Chainable> + checkHint(index: number, total: number): Chainable // 修改類型 + checkTaskInit(): Chainable + waitUntilActive(last?: boolean): Chainable // 注意小寫boolean,並且變量名應該是小寫 + checkPending(): Chainable + CompleteStageWithCommands(stagename: string): Chainable + CheckTextElement( + id: string, + chText: string, + enText: string + ): Chainable + CheckPlaceholder( + id: string, + chText: string, + enText: string + ): Chainable + CheckTreeElement(element: string): Chainable + } + } +} Cypress.Commands.add('LoginWithPassword', (username, password) => { - cy.clearAllCookies({ domain: null }) + cy.clearAllCookies() cy.visit('/login') cy.get('#username').type(username) cy.get('#password').type(password) @@ -10,7 +41,7 @@ Cypress.Commands.add('LoginWithPassword', (username, password) => { Cypress.Commands.add('PrepareForGame', () => { cy.visit('/') // make sure the login page is loaded - cy.url().should('satisfy', (elements) => { + cy.url().should('satisfy', (elements: string) => { const text = elements return text.includes('login') || text.includes('register') }) @@ -70,9 +101,9 @@ Cypress.Commands.add('waitUntilActive', (last = false) => { return } if (value === 'pending' && !last) { - cy.log(last) + cy.log(`${last}`) cy.typeInCommand('{enter}') - cy.waitUntilActive() + cy.waitUntilActive(last) // Pass `last` parameter } }) }) @@ -90,8 +121,7 @@ Cypress.Commands.add('CompleteStageWithCommands', (stagename) => { cy.CheckTextElement('#reset', '重來', 'Reset').click() cy.checkTaskInit() cy.InitTerminal() - cy.readFile(`../quests/${stagename}/answer.sh`, 'utf-8').as('answers') - cy.get('@answers').then((answers) => { + cy.readFile(`../quests/${stagename}/answer.sh`, 'utf-8').then((answers) => { const answerarr = answers.split('\n') for (const element of answerarr) { cy.typeInCommand(`${element}{enter}`) @@ -109,13 +139,11 @@ Cypress.Commands.add('CompleteStageWithCommands', (stagename) => { }) }) Cypress.Commands.add('CheckTextElement', (id, chText, enText) => { - cy.get(id, { timeout: 50000 }).within(($element) => { - if (Cypress.env('isCHVersion')) { - cy.get($element).should('contain', chText).and('be.visible') - } else { - cy.get($element).should('contain', enText).and('be.visible') - } - }) + if (Cypress.env('isCHVersion')) { + cy.get(id, { timeout: 50000 }).should('contain', chText).and('be.visible') + } else { + cy.get(id, { timeout: 50000 }).should('contain', enText).and('be.visible') + } }) Cypress.Commands.add('CheckPlaceholder', (id, chText, enText) => { cy.get(id) diff --git a/tests/cypress/support/e2e.js b/tests/cypress/support/e2e.ts similarity index 100% rename from tests/cypress/support/e2e.js rename to tests/cypress/support/e2e.ts diff --git a/tests/package.json b/tests/package.json index 18332d0..885c84b 100644 --- a/tests/package.json +++ b/tests/package.json @@ -3,14 +3,20 @@ "version": "1.0.0", "private": true, "main": "index.js", + "type": "commonjs", "license": "MIT", "scripts": { - "test": "TESTING_USERNAME=alex TESTING_PASSWORD=Alex1234 cypress run", - "test:open": "TESTING_USERNAME=alex TESTING_PASSWORD=Alex1234 cypress open" + "test": "yarn cypress run", + "test:open": "yarn cypress open" }, "dependencies": { "@testing-library/cypress": "^10.0.1", "cypress": "^13.5.1", - "dotenv": "^16.3.1" + "dotenv": "^16.3.1", + "ts-node": "^10.9.2" + }, + "devDependencies": { + "@types/dotenv": "^8.2.0", + "typescript": "^5.3.3" } } diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 0000000..6f9a664 --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "outDir": "dist", + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress", "node", "@testing-library/cypress"], + "esModuleInterop": true + }, + "include": ["**/*.ts"], + "exclude": ["dist"] +} diff --git a/yarn.lock b/yarn.lock index 01fe85a..26908b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1043,6 +1043,13 @@ "@types/docker-modem" "*" "@types/node" "*" +"@types/dotenv@^8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-8.2.0.tgz#5cd64710c3c98e82d9d15844375a33bf1b45d053" + integrity sha512-ylSC9GhfRH7m1EUXBXofhgx4lUWmFeQDINW5oLuS+gxWdfUeW4zJdeVTYVkexEW+e2VUvlZR2kGnGGipAWR7kw== + dependencies: + dotenv "*" + "@types/express-serve-static-core@^4.17.33": version "4.17.43" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz#10d8444be560cb789c4735aea5eac6e5af45df54" @@ -2777,7 +2784,7 @@ dompurify@^3.0.6: resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.9.tgz#b3f362f24b99f53498c75d43ecbd784b0b3ad65e" integrity sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ== -dotenv@^16.3.1, dotenv@^16.4.1: +dotenv@*, dotenv@^16.3.1, dotenv@^16.4.1: version "16.4.5" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==