Merge branch 'main' into quest

This commit is contained in:
Lin, Hung-Shin authored and GitHub committed 2023-11-23 23:40:03 +08:00
commit 93e2485dee
18 files changed
+315 -29

No files matched your search

+3
View File
@@ -1,5 +1,8 @@
# Linux Odyssey # Linux Odyssey
[![Production](https://github.com/linux-odyssey/linux-odyssey/actions/workflows/production.yml/badge.svg)](https://github.com/linux-odyssey/linux-odyssey/actions/workflows/production.yml)
[![Staging](https://github.com/linux-odyssey/linux-odyssey/actions/workflows/staging.yml/badge.svg)](https://github.com/linux-odyssey/linux-odyssey/actions/workflows/staging.yml)
## Requirements ## Requirements
- NodeJS - NodeJS
+1
View File
@@ -26,6 +26,7 @@
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-router": "^4.2.4", "vue-router": "^4.2.4",
"vue-toastification": "^2.0.0-rc.5", "vue-toastification": "^2.0.0-rc.5",
"vue3-lottie": "^3.2.0",
"xterm": "^5.2.1", "xterm": "^5.2.1",
"xterm-addon-attach": "^0.8.0", "xterm-addon-attach": "^0.8.0",
"xterm-addon-fit": "^0.7.0" "xterm-addon-fit": "^0.7.0"
+48 -5
View File
@@ -1,12 +1,36 @@
<script setup> <script setup>
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { ref } from 'vue' import { ref } from 'vue'
import { surveyUrl } from '../config' import { Vue3Lottie } from 'vue3-lottie'
// import { surveyUrl } from '../config'
import CompleteAnimation from '../img/complete-animation.json'
const showModal = ref(true) // Controls whether the modal is visible or not const showModal = ref(true) // Controls whether the modal is visible or not
const router = useRouter()
const toast = useToast()
const closeModal = () => { const closeModal = () => {
showModal.value = false showModal.value = false
} }
const defineComponent = () => ({
components: {
Vue3Lottie,
},
setup() {
return {
CompleteAnimation,
}
},
})
const backtoMap = async () => {
try {
router.push({ name: 'map' })
} catch (err) {
// useToast().error('Failed to load map')
toast.error('無法讀取地圖')
console.error(err)
}
}
// Simulate completing a quest to show the modal // Simulate completing a quest to show the modal
setTimeout(() => { setTimeout(() => {
@@ -18,6 +42,7 @@ setTimeout(() => {
<div v-if="showModal" class="modal"> <div v-if="showModal" class="modal">
<div class="modal-content bg-bg-secondary rounded-lg"> <div class="modal-content bg-bg-secondary rounded-lg">
<span class="close text-text" @click="closeModal">&times;</span> <span class="close text-text" @click="closeModal">&times;</span>
<h2 <h2
id="QuestCompleted" id="QuestCompleted"
class="text-2xl text-text mb-5 w-full inline-flex justify-center" class="text-2xl text-text mb-5 w-full inline-flex justify-center"
@@ -25,16 +50,34 @@ setTimeout(() => {
<!-- Quest Completed! --> <!-- Quest Completed! -->
關卡完成! 關卡完成!
</h2> </h2>
<p class="text-base text-text"> <div class="z-10 right-1/2 top-1/2" @animationend="defineComponent">
<Vue3Lottie
:animationData="CompleteAnimation"
:height="200"
:width="200"
/>
</div>
<p class="text-base text-text text-center">恭喜你完成這一關!</p>
<p class="text-base text-text text-center">
接下來回到地圖,迎接新的挑戰吧!
</p>
<!-- <p class="text-base text-text">
您已成功完成挑戰,真的非常感謝您的參與!<br /> 您已成功完成挑戰,真的非常感謝您的參與!<br />
如果您能撥冗填寫這份問卷,我們將不勝感激,這對我們非常重要! 如果您能撥冗填寫這份問卷,我們將不勝感激,這對我們非常重要!
</p> </p> -->
<a <!-- <a
id="SurveyButton" id="SurveyButton"
:href="surveyUrl" :href="surveyUrl"
target="_blank" target="_blank"
class="inline-flex justify-center rounded-lg text-base font-black py-2 mt-3 bg-text-primary w-full" class="inline-flex justify-center rounded-lg text-base font-black py-2 mt-3 bg-text-primary w-full"
>填寫問卷</a >填寫問卷</a
> -->
<a
id="BacktoMap"
target="_blank"
@click="backtoMap"
class="inline-flex justify-center rounded-lg text-base font-black py-2 mt-3 bg-text-primary w-full"
>回到地圖</a
> >
</div> </div>
</div> </div>
+39 -4
View File
@@ -1,9 +1,14 @@
<script setup> <script setup>
import { defineProps, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { logout } from '../utils/auth' import { logout } from '../utils/auth'
import { bugReportUrl } from '../config' import { bugReportUrl } from '../config'
import sessionStore, { reset } from '../store/session' import { reset } from '../store/session'
import userProfileStore, {
loadUserProfile,
resetUserProfile,
} from '../store/userProfile'
const router = useRouter() const router = useRouter()
@@ -12,6 +17,7 @@ const toast = useToast()
const handleLogout = async () => { const handleLogout = async () => {
try { try {
await logout() await logout()
resetUserProfile()
reset() reset()
router.push({ name: 'login' }) router.push({ name: 'login' })
} catch (err) { } catch (err) {
@@ -29,6 +35,22 @@ const handleMap = async () => {
console.error(err) console.error(err)
} }
} }
defineProps({
title: {
type: String,
required: true,
},
})
onMounted(async () => {
try {
await loadUserProfile()
} catch (err) {
// toast.error('Failed to load user profile')
toast.error('無法讀取使用者資料')
}
})
</script> </script>
<template> <template>
@@ -47,13 +69,19 @@ const handleMap = async () => {
class="text-border h-full w-full" class="text-border h-full w-full"
/> />
</button> --> </button> -->
<p <button
id="HeaderText" id="HeaderText"
class="text-text-primary inline-block font-bold whitespace-nowrap" class="text-text-primary inline-block font-bold whitespace-nowrap"
style="font-size: 2vh" style="font-size: 2vh"
v-if="sessionStore.quest"
> >
{{ sessionStore.quest.title }} Linux Odyssey
</button>
<p
id="HeaderText"
class="text-text inline-block font-bold whitespace-nowrap"
style="font-size: 2vh"
>
{{ title }}
</p> </p>
</div> </div>
<div class="h-full w-full flex gap-3 items-center justify-end"> <div class="h-full w-full flex gap-3 items-center justify-end">
@@ -75,6 +103,13 @@ const handleMap = async () => {
class="text-text-primary h-full w-full" class="text-text-primary h-full w-full"
/> />
</button> --> </button> -->
<p
id="UsernameText"
class="text-text inline-block whitespace-nowrap"
style="font-size: 2vh"
>
{{ userProfileStore.username }}
</p>
<button title="Map" @click="handleMap" class="h-5 w-5"> <button title="Map" @click="handleMap" class="h-5 w-5">
<font-awesome-icon <font-awesome-icon
:icon="['fas', 'map']" :icon="['fas', 'map']"
+10 -2
View File
@@ -1,3 +1,4 @@
<!-- eslint-disable no-console -->
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -34,6 +35,7 @@ async function getQuests() {
return res.data return res.data
} catch (err) { } catch (err) {
console.error(err) console.error(err)
throw err throw err
} }
} }
@@ -93,9 +95,15 @@ const genOption = (nodes, edges) => ({
itemStyle: { itemStyle: {
color: (params) => { color: (params) => {
const { const {
data: { completed }, data: { completed, unlocked },
} = params } = params
return completed ? '#ADADB5' : '#454552' if (completed) {
return '#00ff00'
}
if (unlocked) {
return '#ADADB5'
}
return '454552'
}, },
}, },
}, },
+1
View File
@@ -0,0 +1 @@
{"v":"5.5.9","fr":60,"ip":0,"op":90,"w":640,"h":640,"nm":"Lottie_petard","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","parent":2,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[320,320,0],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[358.25,238.75],[313.75,278],[456.813,424.5]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":3},{"ddd":0,"ind":2,"ty":0,"nm":"Pre-comp 1","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.47,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[318,312,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.47,"y":1},"o":{"x":0.167,"y":0.987},"t":6.5,"s":[328,316,0],"to":[0,0,0],"ti":[0,0,0]},{"t":45,"s":[318,312,0]}],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"confetti","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-61.57,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[345.257,314.37,0],"to":[0,0,0],"ti":[0,0,0]},{"t":15,"s":[272.458,139.571,0]}],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[-54,54,100],"ix":6}},"ao":0,"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"confetti","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-61.57,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[345.257,314.37,0],"to":[0,0,0],"ti":[0,0,0]},{"t":15,"s":[207.458,186.571,0]}],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[-54,54,100],"ix":6}},"ao":0,"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"confetti","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-61.57,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[345.257,314.37,0],"to":[0,0,0],"ti":[0,0,0]},{"t":15,"s":[274.458,236.571,0]}],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[-54,54,100],"ix":6}},"ao":0,"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"confetti","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-61.57,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[345.257,314.37,0],"to":[0,0,0],"ti":[0,0,0]},{"t":15,"s":[224.458,267.571,0]}],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[54,54,100],"ix":6}},"ao":0,"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"confetti","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-61.57,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[345.257,314.37,0],"to":[0,0,0],"ti":[0,0,0]},{"t":15,"s":[326.458,202.571,0]}],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[54,54,100],"ix":6}},"ao":0,"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"confetti","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-61.57,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[345.257,314.37,0],"to":[0,0,0],"ti":[0,0,0]},{"t":15,"s":[314.458,272.571,0]}],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[54,54,100],"ix":6}},"ao":0,"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 4","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[319.25,320.25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,104.369,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-27.614,0],[0,-27.614],[27.568,-1.605],[0,27.614]],"o":[[27.614,0],[0,27.614],[-26.718,1.555],[0,-27.614]],"v":[[-9,-26.75],[41,23.25],[-11.439,70.948],[-59,23.25]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.949020026712,0.886274988511,0.019608000213,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9,0.840928201114,0.008999999364,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[8,-36.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[89.096,20.801],"ix":3},"r":{"a":0,"k":-45,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60Line truncated
+4
View File
@@ -5,6 +5,8 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import './library' import './library'
/* import Vue Toastification */ /* import Vue Toastification */
import Toast, { POSITION } from 'vue-toastification' import Toast, { POSITION } from 'vue-toastification'
/* import Vue Lottie */
import Vue3Lottie from 'vue3-lottie'
import 'vue-toastification/dist/index.css' import 'vue-toastification/dist/index.css'
import App from './App.vue' import App from './App.vue'
@@ -22,4 +24,6 @@ const options = {
app.use(Toast, options) app.use(Toast, options)
app.use(Vue3Lottie, { name: 'Vue3Lottie' })
app.mount('#app') app.mount('#app')
+6
View File
@@ -4,6 +4,7 @@ import LoginPage from './views/LoginPage.vue'
import RegisterPage from './views/RegisterPage.vue' import RegisterPage from './views/RegisterPage.vue'
import ChooseUsernamePage from './views/ChooseUsernamePage.vue' import ChooseUsernamePage from './views/ChooseUsernamePage.vue'
import QuestMapPage from './views/QuestMapPage.vue' import QuestMapPage from './views/QuestMapPage.vue'
import LeaderboardPage from './views/LeaderboardPage.vue'
import { isLoggedIn } from './utils/auth' import { isLoggedIn } from './utils/auth'
const routes = [ const routes = [
@@ -45,6 +46,11 @@ const routes = [
component: QuestMapPage, component: QuestMapPage,
meta: { requiresAuth: true }, meta: { requiresAuth: true },
}, },
{
path: '/leaderboard',
name: 'leaderboard',
component: LeaderboardPage,
},
] ]
const router = createRouter({ const router = createRouter({
+30
View File
@@ -0,0 +1,30 @@
import { reactive } from 'vue'
import api from '../utils/api'
const store = reactive({
username: '',
email: '',
progress: {},
})
export async function loadUserProfile() {
try {
const res = await api.get('/users/me')
const { username, email, progress } = res.data
store.username = username
store.email = email
store.progress = progress
return store
} catch (err) {
console.error(err)
throw new Error(`Failed to load user profile: ${err.message}`)
}
}
export function resetUserProfile() {
store.username = ''
store.email = ''
store.progress = {}
}
export default store
+1 -1
View File
@@ -50,7 +50,7 @@ onMounted(async () => {
<template> <template>
<!-- game header --> <!-- game header -->
<div class="h-[6vh] w-full"> <div class="h-[6vh] w-full">
<GameHeaderPart /> <GameHeaderPart :title="sessionStore.quest?.title" />
</div> </div>
<!-- current status indicator --> <!-- current status indicator -->
<input <input
+81
View File
@@ -0,0 +1,81 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useToast } from 'vue-toastification'
import DynamicBackground from '../components/DynamicBackground.vue'
import GameHeaderPart from '../components/GameHeaderPart.vue'
import api from '../utils/api'
const leaderboard = ref([])
const toast = useToast()
async function getLeaderboard() {
try {
const res = await api.get('/leaderboard')
leaderboard.value = res.data
} catch (err) {
console.error(err)
toast.error('讀取排行榜失敗')
}
}
onMounted(async () => {
await getLeaderboard()
})
</script>
<template>
<div class="w-screen h-screen bg-bg">
<DynamicBackground class="w-full h-full" />
<div
class="w-screen h-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
>
<div class="h-[6vh] w-full">
<GameHeaderPart title="排行榜" />
</div>
<div class="w-fit h-[94vh] m-3">
<div
class="h-full bg-bg border-8 border-bg-primary flex flex-col items-center rounded-3xl p-10"
>
<h1 class="text-text-primary text-3xl mb-5">排行榜</h1>
<div class="max-h-300 overflow-auto">
<table class="w-full text-sm text-left text-text">
<thead
class="text-xs text-text-secondary uppercase bg-bg-secondary"
>
<tr>
<th scope="col" class="px-6 py-3">使用者名稱</th>
<th scope="col" class="px-6 py-3">分數</th>
<th scope="col" class="px-6 py-3">完成關卡</th>
</tr>
</thead>
<tbody>
<tr
v-for="user in leaderboard"
:key="user.username"
class="border-b bg-bg"
>
<th
scope="row"
class="px-6 py-4 font-medium text-text-primary whitespace-nowrap"
>
{{ user.username }}
</th>
<td class="px-6 py-4">{{ user.score }}</td>
<td class="px-6 py-4 text-text-secondary">
<span
class="mr-2"
v-for="quest in user.completedQuests"
:key="quest"
>
{{ quest }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</template>
+4 -4
View File
@@ -5,14 +5,14 @@ import GameHeaderPart from '../components/GameHeaderPart.vue'
<template> <template>
<div class="h-[6vh] w-full"> <div class="h-[6vh] w-full">
<GameHeaderPart /> <GameHeaderPart title="關卡地圖" />
<p <!-- <p
class="text-text-primary absolute z-1 font-bold inline-block tracking-widest" class="text-text-primary absolute z-1 font-bold inline-block tracking-widest"
style="left: 1.2%; top: 1.5%; height: 100%; font-size: 2vh" style="left: 1.2%; top: 1.5%; height: 100%; font-size: 2vh"
> >
<!-- Quest Map --> Quest Map
Unix編年史 Unix編年史
</p> </p> -->
</div> </div>
<div class="flex flex-col justify-center items-center w-full h-[94vh]"> <div class="flex flex-col justify-center items-center w-full h-[94vh]">
<QuestMap /> <QuestMap />
@@ -0,0 +1,12 @@
import { asyncHandler } from '../../middleware/error.js'
import { leaderboard } from '../../models/userManager.js'
export default asyncHandler(async (req, res) => {
const users = await leaderboard()
const response = users.map((user) => ({
username: user.user.username,
completedQuests: user.completedQuests,
score: user.score * 1000,
}))
res.json(response)
})
+3
View File
@@ -7,6 +7,7 @@ import authRouter from './authRouter.js'
import authRequired from '../../middleware/authRequired.js' import authRequired from '../../middleware/authRequired.js'
import userRouter from './userRouter.js' import userRouter from './userRouter.js'
import config from '../../config.js' import config from '../../config.js'
import leaderboardController from '../controllers/leaderboardController.js'
const router = Router() const router = Router()
@@ -48,4 +49,6 @@ router.get('/bug-report', (req, res) => {
res.redirect(config.bugReportUrl) res.redirect(config.bugReportUrl)
}) })
router.get('/leaderboard', leaderboardController)
export default router export default router
+43
View File
@@ -22,3 +22,46 @@ export async function createUser(username, email, { password, socialLogin }) {
return user return user
} }
export function leaderboard() {
return UserProfile.aggregate([
{
$project: {
user: true, // Include the user field in the projection
completedQuests: {
$map: {
input: {
$filter: {
input: { $objectToArray: '$progress' }, // Convert the progress map to an array
as: 'prog',
cond: { $eq: ['$$prog.v.completed', true] }, // Filter for completed quests
},
},
as: 'completedProg',
in: '$$completedProg.k', // Extract the quest IDs
},
},
},
},
{
$addFields: {
score: { $size: '$completedQuests' }, // Calculate the size of the completedQuests array
},
},
{
$match: {
score: { $gt: 0 }, // Filter out users who haven't completed any quests
},
},
{ $sort: { score: -1 } },
{
$lookup: {
from: 'users',
localField: 'user',
foreignField: '_id',
as: 'user',
},
},
{ $unwind: '$user' },
])
}
+3 -3
View File
@@ -79,7 +79,7 @@ describe('example helloworld app', () => {
cy.PrepareForGame() cy.PrepareForGame()
}) })
it('Check Header', () => { it('Check Header', () => {
cy.get('#HeaderText').should('contain', title).and('be.visible') cy.get('#HeaderText').should('contain', 'Linux Odyssey').and('be.visible')
cy.findByRole('link', { name: 'Bug Report' }).should('be.visible') cy.findByRole('link', { name: 'Bug Report' }).should('be.visible')
cy.findByRole('button', { name: 'Sign Out' }).should('be.visible') cy.findByRole('button', { name: 'Sign Out' }).should('be.visible')
}) })
@@ -123,8 +123,8 @@ describe('example helloworld app', () => {
it('Typing in Terminal', () => { it('Typing in Terminal', () => {
cy.typeInCommand('12345{enter}') cy.typeInCommand('12345{enter}')
cy.get('@Terminaltextbox') cy.get('@Terminaltextbox')
.should('contain', 'zsh: command not found: 12345') .should('contain', '12345')
.and('contain', '12345') .and('contain', 'zsh: command not found: 12345', { timeout: 50000 })
}) })
it('Complete the Game(relating UI)', () => { it('Complete the Game(relating UI)', () => {
// Stage1 // Stage1
+7 -4
View File
@@ -9,12 +9,19 @@ 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
cy.url().should('satisfy', (elements) => {
const text = elements
return text.includes('login') || text.includes('register')
})
cy.LoginWithPassword( cy.LoginWithPassword(
Cypress.env('defaultAccount'), Cypress.env('defaultAccount'),
Cypress.env('defaultPassword') Cypress.env('defaultPassword')
) )
// make sure the map page is loaded
cy.url().should('include', '/map') cy.url().should('include', '/map')
cy.visit('/game/get-started') cy.visit('/game/get-started')
cy.url().should('include', '/game/get-started')
cy.get('.xterm-screen', { timeout: 10000 }) cy.get('.xterm-screen', { timeout: 10000 })
.as('Terminaltextbox') .as('Terminaltextbox')
.should('be.visible') .should('be.visible')
@@ -24,10 +31,6 @@ Cypress.Commands.add('PrepareForGame', () => {
'contain', 'contain',
'commander:~ $' 'commander:~ $'
) )
cy.get('@Terminaltextbox', { timeout: 150000 }).should(
'contain',
'commander:~ $'
)
}) })
Cypress.Commands.add('typeInCommand', (command) => { Cypress.Commands.add('typeInCommand', (command) => {
cy.get('.xterm-screen', { timeout: 150000 }).type(command) cy.get('.xterm-screen', { timeout: 150000 }).type(command)
+19 -6
View File
@@ -1500,12 +1500,7 @@ astral-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
async@^3.2.0: async@^3.2.0, async@^3.2.3:
version "3.2.5"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==
async@^3.2.3:
version "3.2.5" version "3.2.5"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==
@@ -4556,6 +4551,11 @@ locate-path@^6.0.0:
dependencies: dependencies:
p-locate "^5.0.0" p-locate "^5.0.0"
lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash.includes@^4.3.0: lodash.includes@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
@@ -4636,6 +4636,11 @@ long-timeout@0.1.1:
resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514"
integrity sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w== integrity sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==
lottie-web@5.12.2:
version "5.12.2"
resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.12.2.tgz#579ca9fe6d3fd9e352571edd3c0be162492f68e5"
integrity sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==
lru-cache@^5.1.1: lru-cache@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -6620,6 +6625,14 @@ vue-toastification@^2.0.0-rc.5:
resolved "https://registry.yarnpkg.com/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz#92798604d806ae473cfb76ed776fae294280f8f8" resolved "https://registry.yarnpkg.com/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz#92798604d806ae473cfb76ed776fae294280f8f8"
integrity sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA== integrity sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==
vue3-lottie@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/vue3-lottie/-/vue3-lottie-3.2.0.tgz#7f95831d3483dc304c680a9b2cac6ed098070a2a"
integrity sha512-PtNXbv7iD4XGmK8CXd71aLWmILIR6P6VzLMDw6ZzmDAeuJZuJ0MdOVnn50LOARgq+cOFJsn/2z+7tZh0F9T9qw==
dependencies:
lodash-es "^4.17.21"
lottie-web "5.12.2"
vue@^3.3.4: vue@^3.3.4:
version "3.3.4" version "3.3.4"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.4.tgz#8ed945d3873667df1d0fcf3b2463ada028f88bd6" resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.4.tgz#8ed945d3873667df1d0fcf3b2463ada028f88bd6"