Feat automation test - Transfer to Typescript (#104)

- Update Readme for new build-up commands (still not the latest version)
- Add Cpress optional OAuth activation
- Transfer testing files into Typescript
This commit is contained in:
Lin, Hung-Shin authored and GitHub committed 2024-02-28 23:29:24 +08:00
commit fb69cb4e10
24 files changed
+145 -64

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
*/node_modules **/node_modules
.git .git
*/dist */dist
+4
View File
@@ -0,0 +1,4 @@
TESTING=true
TESTING_USERNAME=alex
TESTING_PASSWORD=Alex1234
SECRET_KEY=b1d2e917-17b9-4fce-9055-082dee6ec5d6
+2 -1
View File
@@ -63,9 +63,10 @@ module.exports = {
}, },
overrides: [ 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: { rules: {
'import/extensions': 'off', // Turn off the rule for test files 'import/extensions': 'off', // Turn off the rule for test files
'import/no-extraneous-dependencies': 'off',
}, },
}, },
], ],
+12 -6
View File
@@ -17,15 +17,22 @@
## Development ## Development
Run everything (frontend, backend, db, swagger, quests): Run mongoDB first:
docker compose build docker compose -f docker-compose.dev.yml up -d db
docker compose up -d
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: 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 ### 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`. 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: list sessions:
yarn cli list yarn cli list
+1 -1
View File
@@ -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/utils/package.json ./packages/utils/package.json
COPY packages/constants/package.json ./packages/constants/package.json COPY packages/constants/package.json ./packages/constants/package.json
COPY app/package.json ./app/package.json COPY app/package.json ./app/package.json
RUN yarn install RUN yarn install
# Dev stage # Dev stage
FROM base as dev FROM base as dev
+1
View File
@@ -74,6 +74,7 @@ services:
db: db:
image: mongo image: mongo
restart: always
networks: networks:
- internal - internal
ports: ports:
+6 -4
View File
@@ -12,6 +12,7 @@ services:
- BASE_URL=http://app-test - BASE_URL=http://app-test
- TESTING_USERNAME=alex - TESTING_USERNAME=alex
- TESTING_PASSWORD=Alex1234 - TESTING_PASSWORD=Alex1234
# - DEBUG=cypress:*
depends_on: depends_on:
- app-test - app-test
@@ -23,8 +24,9 @@ services:
target: prod target: prod
networks: networks:
internal: internal:
ports: # Only open the port for debugging
- 5173:80 # ports:
# - 5173:80
depends_on: depends_on:
- backend - backend
@@ -71,8 +73,8 @@ services:
- MONGO_USER=user - MONGO_USER=user
- MONGO_PASS=user-password - MONGO_PASS=user-password
- MONGO_DB=odyssey-test - MONGO_DB=odyssey-test
ports: # ports:
- 27017:27017 # - 27017:27017
base: base:
image: linuxodyssey/quest-base image: linuxodyssey/quest-base
+1
View File
@@ -30,6 +30,7 @@
"build:quests": "yarn workspace server build:quests" "build:quests": "yarn workspace server build:quests"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/node": "^20.10.4", "@types/node": "^20.10.4",
"@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/eslint-plugin": "^6.14.0",
+2 -1
View File
@@ -1,4 +1,5 @@
import File from '../src/file' import { describe, test, expect } from '@jest/globals'
import File from '../src/file.js'
describe('File', () => { describe('File', () => {
let file: File let file: File
@@ -1,3 +1,4 @@
import { describe, test, expect } from '@jest/globals'
import FileGraph from '../src/fileGraph.js' import FileGraph from '../src/fileGraph.js'
import FileNode from '../src/fileNode.js' import FileNode from '../src/fileNode.js'
@@ -1,3 +1,4 @@
import { describe, test, expect } from '@jest/globals'
import { DuplicateItemError, ParentNotExistsError } from '../src/errors.js' import { DuplicateItemError, ParentNotExistsError } from '../src/errors.js'
import FileNode from '../src/fileNode.js' import FileNode from '../src/fileNode.js'
import { FileObject } from '../src/file.js' import { FileObject } from '../src/file.js'
+1
View File
@@ -1,3 +1,4 @@
import { test, expect } from '@jest/globals'
import { isValidEmail, isValidUsername } from '../src/auth.js' import { isValidEmail, isValidUsername } from '../src/auth.js'
test('check valid username', () => { test('check valid username', () => {
+1
View File
@@ -1,3 +1,4 @@
import { describe, it, expect } from '@jest/globals'
import { buildFileCheckCmd, File } from '../src/commands.js' import { buildFileCheckCmd, File } from '../src/commands.js'
describe('buildFileCheckCmd function', () => { describe('buildFileCheckCmd function', () => {
+1
View File
@@ -1,3 +1,4 @@
import { describe, it, expect } from '@jest/globals'
import DAG from '../src/dag.js' import DAG from '../src/dag.js'
describe('dag layers', () => { describe('dag layers', () => {
+3
View File
@@ -4,6 +4,9 @@ import { get } from './utils/env.js'
// Import dotenv and load ../.env // Import dotenv and load ../.env
dotenv.config({ path: '../.env' }) dotenv.config({ path: '../.env' })
if (process.env.NODE_ENV !== 'production') {
dotenv.config({ path: '../.env.dev' })
}
function getTrustProxies(key: string): string[] { function getTrustProxies(key: string): string[] {
const value = process.env[key] const value = process.env[key]
+1 -1
View File
@@ -3,7 +3,7 @@ FROM cypress/browsers
WORKDIR /e2e WORKDIR /e2e
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./
COPY tests/package.json ./tests/package.json COPY tests/package.json ./tests/package.json
RUN yarn install RUN yarn install --frozen-lockfile
COPY quests ./quests COPY quests ./quests
COPY tests ./tests COPY tests ./tests
@@ -1,20 +1,27 @@
const { defineConfig } = require('cypress') import { defineConfig } from 'cypress'
const process = require('process') import process from 'process'
require('dotenv').config({ path: '../.env' }) import dotenv from 'dotenv'
import fs from 'fs' // Import fs module
dotenv.config({ path: '../.env.dev' })
dotenv.config({ path: '../.env' })
const getConfig = (key, defaultValue) => { const getConfig = (key, defaultValue) => {
return process.env[key] || defaultValue return process.env[key] || defaultValue
} }
const getOrFail = (key) => { const getOrFail = (key) => {
const value = getConfig(key) const value = getConfig(key, null)
if (!value) { if (!value) {
throw new Error(`Missing required environment variable ${key}`) throw new Error(`Missing required environment variable ${key}`)
} }
return value return value
} }
module.exports = defineConfig({ // Check if the .env file exists
const EnableoAuth = fs.existsSync('../.env')
export default defineConfig({
e2e: { e2e: {
baseUrl: getConfig('BASE_URL', 'http://localhost:5173'), baseUrl: getConfig('BASE_URL', 'http://localhost:5173'),
viewportWidth: 1920, viewportWidth: 1920,
@@ -28,13 +35,13 @@ module.exports = defineConfig({
defaultAccount: getOrFail('TESTING_USERNAME'), defaultAccount: getOrFail('TESTING_USERNAME'),
defaultPassword: getOrFail('TESTING_PASSWORD'), defaultPassword: getOrFail('TESTING_PASSWORD'),
isCHVersion: true, isCHVersion: true,
EnableoAuth, // Add the EnableoAuth variable here
// google_username: process.env.GOOGLE_USERNAME, // google_username: process.env.GOOGLE_USERNAME,
// google_password: process.env.GOOGLE_PASSWORD, // google_password: process.env.GOOGLE_PASSWORD,
// google_client_id: process.env.GOOGLE_CLIENT_ID, // google_client_id: process.env.GOOGLE_CLIENT_ID,
// google_client_secret: process.env.GOOGLE_CLIENT_SECRET,
// github_username: process.env.GITHUB_USERNAME, // github_username: process.env.GITHUB_USERNAME,
// github_password: process.env.GITHUB_PASSWORD, // github_password: process.env.GITHUB_PASSWORD,
// github_client_id: process.env.GITHUB_CLIENT_ID, // github_client_id: process.env.GITHUB_CLIENT_ID,
// github_client_secret: process.env.GITHUB_CLIENT_SECRET,
}, },
}) })
@@ -1,4 +1,3 @@
/// <reference types="cypress" />
export default function checkLoginUI() { export default function checkLoginUI() {
cy.get('#title').should('contain', 'Linux Odyssey').and('be.visible') cy.get('#title').should('contain', 'Linux Odyssey').and('be.visible')
cy.CheckPlaceholder('#password', '密碼', 'Password') cy.CheckPlaceholder('#password', '密碼', 'Password')
@@ -37,17 +36,19 @@ describe('example helloworld app', () => {
cy.CheckTextElement('#SwitchtoRegister', '註冊', 'Sign up') cy.CheckTextElement('#SwitchtoRegister', '註冊', 'Sign up')
}) })
it('Check social account UI', () => { it('Check social account UI', () => {
cy.CheckTextElement( if (Cypress.env('EnableoAuth')) {
'#GoogleLogin', cy.CheckTextElement(
'以 Google 繼續', '#GoogleLogin',
'Continue with Google' '以 Google 繼續',
) 'Continue with Google'
cy.CheckTextElement('#or', '或', 'or') )
cy.CheckTextElement( cy.CheckTextElement('#or', '或', 'or')
'#GitHubLogin', cy.CheckTextElement(
'以 GitHub 繼續', '#GitHubLogin',
'Continue with GitHub' '以 GitHub 繼續',
) 'Continue with GitHub'
)
}
}) })
it('Check login fail', () => { it('Check login fail', () => {
cy.LoginWithPassword('dddd', '123456') cy.LoginWithPassword('dddd', '123456')
@@ -96,20 +97,18 @@ describe('example helloworld app', () => {
cy.CheckTextElement('#tasks', '任務', 'Tasks:') cy.CheckTextElement('#tasks', '任務', 'Tasks:')
}) })
it('Check Terminal', () => { it('Check Terminal', () => {
cy.get('#terminal', { timeout: 20000 }).within(($terminal) => { // cy.get('#terminal', { timeout: 20000 }).within(($terminal) => {
cy.get($terminal).should('be.visible') // cy.get($terminal).should('be.visible')
cy.get('svg[data-icon="terminal"]').should('be.visible') cy.get('#terminal', { timeout: 20000 }).should('be.visible')
cy.CheckTextElement('#Terminal', '終端機', 'Terminal') cy.get('svg[data-icon="terminal"]').should('be.visible')
cy.get('@Terminaltextbox') cy.CheckTextElement('#Terminal', '終端機', 'Terminal')
.should('be.visible') cy.get('@Terminaltextbox')
.and('contain', 'commander:~ $') .should('be.visible')
}) .and('contain', 'commander:~ $')
}) })
it('Check Hint Part', () => { it('Check Hint Part', () => {
cy.get('#hint', { timeout: 20000 }).within(($hint) => { cy.get('#hint', { timeout: 20000 }).should('be.visible')
cy.get($hint).should('be.visible') cy.get('svg[data-icon="lightbulb"]').should('be.visible')
cy.get('svg[data-icon="lightbulb"]').should('be.visible')
})
cy.CheckTextElement('#hint', '提示', 'Hint') cy.CheckTextElement('#hint', '提示', 'Hint')
}) })
it('Check File TreeChart', () => { it('Check File TreeChart', () => {
@@ -1,4 +1,3 @@
/// <reference types="cypress" />
describe('Complete quests with only commands', () => { describe('Complete quests with only commands', () => {
beforeEach(() => { beforeEach(() => {
cy.PrepareForGame() cy.PrepareForGame()
@@ -1,7 +1,38 @@
/* eslint-disable no-undef */
/* eslint-disable no-unused-vars */
/// <reference types="cypress" />
import '@testing-library/cypress/add-commands' import '@testing-library/cypress/add-commands'
declare const expect: Chai.ExpectStatic
declare global {
namespace Cypress {
interface Chainable {
LoginWithPassword(username: string, password: string): Chainable<string>
PrepareForGame(): Chainable<void>
InitTerminal(): Chainable<void>
typeInCommand(command: string): Chainable<string>
getQuestInfo(id: string): Chainable<JQuery<HTMLParagraphElement>>
checkHint(index: number, total: number): Chainable<number> // 修改類型
checkTaskInit(): Chainable<void>
waitUntilActive(last?: boolean): Chainable<void> // 注意小寫boolean,並且變量名應該是小寫
checkPending(): Chainable<void>
CompleteStageWithCommands(stagename: string): Chainable<string>
CheckTextElement(
id: string,
chText: string,
enText: string
): Chainable<string>
CheckPlaceholder(
id: string,
chText: string,
enText: string
): Chainable<string>
CheckTreeElement(element: string): Chainable<string>
}
}
}
Cypress.Commands.add('LoginWithPassword', (username, password) => { Cypress.Commands.add('LoginWithPassword', (username, password) => {
cy.clearAllCookies({ domain: null }) cy.clearAllCookies()
cy.visit('/login') cy.visit('/login')
cy.get('#username').type(username) cy.get('#username').type(username)
cy.get('#password').type(password) cy.get('#password').type(password)
@@ -10,7 +41,7 @@ Cypress.Commands.add('LoginWithPassword', (username, password) => {
Cypress.Commands.add('PrepareForGame', () => { Cypress.Commands.add('PrepareForGame', () => {
cy.visit('/') cy.visit('/')
// make sure the login page is loaded // make sure the login page is loaded
cy.url().should('satisfy', (elements) => { cy.url().should('satisfy', (elements: string) => {
const text = elements const text = elements
return text.includes('login') || text.includes('register') return text.includes('login') || text.includes('register')
}) })
@@ -70,9 +101,9 @@ Cypress.Commands.add('waitUntilActive', (last = false) => {
return return
} }
if (value === 'pending' && !last) { if (value === 'pending' && !last) {
cy.log(last) cy.log(`${last}`)
cy.typeInCommand('{enter}') 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.CheckTextElement('#reset', '重來', 'Reset').click()
cy.checkTaskInit() cy.checkTaskInit()
cy.InitTerminal() cy.InitTerminal()
cy.readFile(`../quests/${stagename}/answer.sh`, 'utf-8').as('answers') cy.readFile(`../quests/${stagename}/answer.sh`, 'utf-8').then((answers) => {
cy.get('@answers').then((answers) => {
const answerarr = answers.split('\n') const answerarr = answers.split('\n')
for (const element of answerarr) { for (const element of answerarr) {
cy.typeInCommand(`${element}{enter}`) cy.typeInCommand(`${element}{enter}`)
@@ -109,13 +139,11 @@ Cypress.Commands.add('CompleteStageWithCommands', (stagename) => {
}) })
}) })
Cypress.Commands.add('CheckTextElement', (id, chText, enText) => { Cypress.Commands.add('CheckTextElement', (id, chText, enText) => {
cy.get(id, { timeout: 50000 }).within(($element) => { if (Cypress.env('isCHVersion')) {
if (Cypress.env('isCHVersion')) { cy.get(id, { timeout: 50000 }).should('contain', chText).and('be.visible')
cy.get($element).should('contain', chText).and('be.visible') } else {
} else { cy.get(id, { timeout: 50000 }).should('contain', enText).and('be.visible')
cy.get($element).should('contain', enText).and('be.visible') }
}
})
}) })
Cypress.Commands.add('CheckPlaceholder', (id, chText, enText) => { Cypress.Commands.add('CheckPlaceholder', (id, chText, enText) => {
cy.get(id) cy.get(id)
File renamed without changes.
+9 -3
View File
@@ -3,14 +3,20 @@
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"main": "index.js", "main": "index.js",
"type": "commonjs",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"test": "TESTING_USERNAME=alex TESTING_PASSWORD=Alex1234 cypress run", "test": "yarn cypress run",
"test:open": "TESTING_USERNAME=alex TESTING_PASSWORD=Alex1234 cypress open" "test:open": "yarn cypress open"
}, },
"dependencies": { "dependencies": {
"@testing-library/cypress": "^10.0.1", "@testing-library/cypress": "^10.0.1",
"cypress": "^13.5.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"
} }
} }
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"outDir": "dist",
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node", "@testing-library/cypress"],
"esModuleInterop": true
},
"include": ["**/*.ts"],
"exclude": ["dist"]
}
+8 -1
View File
@@ -1043,6 +1043,13 @@
"@types/docker-modem" "*" "@types/docker-modem" "*"
"@types/node" "*" "@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": "@types/express-serve-static-core@^4.17.33":
version "4.17.43" version "4.17.43"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz#10d8444be560cb789c4735aea5eac6e5af45df54" 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" resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.9.tgz#b3f362f24b99f53498c75d43ecbd784b0b3ad65e"
integrity sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ== 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" version "16.4.5"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==