feat: complete the thirteen-stage learning experience and E2E journey
Frontend CI / unit (test, type-check, build) (push) Successful in 7m21s
Frontend CI / browser E2E (frontend + pinned backend) (push) Successful in 11m38s

Play levels 7-13 in the app and pin E2E to the local thirteen-stage backend commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-22 19:42:04 +03:30
co-authored by Cursor
parent bb29cc6bdb
commit c53a67831d
38 changed files with 3764 additions and 8 deletions
+1 -1
View File
@@ -2,6 +2,6 @@
"repositoryUrl": "https://get.feep.ir/gold_land/gold_land_server.git",
"checkoutPath": "backend-nest",
"localRef": "",
"pinnedRef": "dfb20ca92cb13f1d25c6aca64b6256c09176e603",
"pinnedRef": "866be223956ef5d3961b0d835b36adedfeb35c82",
"notes": "CI uses E2E_BACKEND_REF when set, otherwise pinnedRef (full SHA only). Never falls back to main. Local sibling: E2E_BACKEND_ROOT=../backend-nest and skip ci:checkout-backend."
}
+120
View File
@@ -0,0 +1,120 @@
import { test, expect } from '@playwright/test'
import { fetchMe, registerViaUi, uniqueUsername } from './helpers/auth'
const perfectLessons: Record<number, Array<Record<string, unknown>>> = {
1: [
{ questionId: 'money-q1', answerId: 'option-a' },
{ questionId: 'money-q2', answerId: 'true' },
{ questionId: 'money-q3', answerId: 'option-a' },
{ questionId: 'money-q4', answerId: 'true' },
{ questionId: 'money-q5', answerId: 'option-b' },
],
2: [
{ questionId: 'saving-q1', answerId: 'option-b' },
{ questionId: 'saving-q2', answerId: 'true' },
{ questionId: 'saving-q3', answerId: 'option-a' },
{ questionId: 'saving-q4', answerId: 'true' },
{ questionId: 'saving-q5', answerId: 'option-a' },
],
3: [
{ questionId: 'needs-q1', answerId: 'option-a' },
{ questionId: 'needs-q2', answerId: 'true' },
{ questionId: 'needs-q3', orderedItemIds: ['water', 'food', 'medicine', 'shoes', 'toy', 'crown'] },
{ questionId: 'needs-q4', answerId: 'option-a' },
{ questionId: 'needs-q5', answerId: 'true' },
],
4: [
{ questionId: 'gold-q1', answerId: 'option-a' },
{ questionId: 'gold-q2', answerId: 'true' },
{ questionId: 'gold-q3', answerId: 'option-a' },
{ questionId: 'gold-q4', answerId: 'false' },
{ questionId: 'gold-q5', answerId: 'option-b' },
],
5: [
{ questionId: 'market-q1', answerId: 'option-a' },
{ questionId: 'market-q2', answerId: 'false' },
{ questionId: 'market-q3', answerId: 'option-a' },
{ questionId: 'market-q4', answerId: 'true' },
{ questionId: 'market-q5', answerId: 'option-b' },
],
6: [
{ questionId: 'inflation-q1', answerId: 'option-b' },
{ questionId: 'inflation-q2', answerId: 'true' },
{ questionId: 'inflation-q3', answerId: 'option-a' },
{ questionId: 'inflation-q4', answerId: 'false' },
{ questionId: 'inflation-q5', answerId: 'option-b' },
],
}
async function authHeader(page: import('@playwright/test').Page) {
const token = await page.evaluate(() => localStorage.getItem('gold-land-auth-token'))
if (!token) throw new Error('missing token')
return { authorization: `Bearer ${token}` }
}
test('budget mission from the map, then replay without extra reward', async ({ page }) => {
const user = uniqueUsername('budget')
await registerViaUi(page, user, 'pass1234', 'بودجه')
const headers = await authHeader(page)
for (let levelId = 1; levelId <= 6; levelId += 1) {
const start = await page.request.post(`/api/gold-land/levels/${levelId}/start`, { headers })
expect(start.ok()).toBeTruthy()
const done = await page.request.post(`/api/gold-land/levels/${levelId}/complete`, {
headers,
data: { answers: perfectLessons[levelId] },
})
expect(done.ok()).toBeTruthy()
}
await page.goto('/map')
await page.getByRole('button', { name: 'مرحله 7: بودجه‌بندی' }).click()
await page.getByRole('button', { name: 'شروع بازی' }).click()
await page.getByRole('button', { name: 'ساخت بودجه' }).click()
await page.getByRole('button', { name: 'بررسی برنامه' }).click()
await expect(page.getByRole('button', { name: 'تأیید برنامه' })).toBeDisabled()
await expect(page.getByText('رفت‌وآمد حداقل ۲۰ سکه')).toBeVisible()
await page.getByRole('button', { name: 'اصلاح برنامه' }).click()
for (let i = 0; i < 4; i += 1) await page.getByRole('button', { name: 'افزایش رفت‌وآمد' }).click()
for (let i = 0; i < 5; i += 1) await page.getByRole('button', { name: 'افزایش آب و غذا' }).click()
for (let i = 0; i < 4; i += 1) await page.getByRole('button', { name: 'افزایش پس‌انداز برای هدف' }).click()
await page.getByRole('button', { name: 'بررسی برنامه' }).click()
await page.getByRole('button', { name: 'تأیید برنامه' }).click()
await expect(page.getByText('هزینه رفت‌وآمد ۱۰ سکه بیشتر شد')).toBeVisible()
await page.getByRole('button', { name: 'بازتنظیم بودجه' }).click()
await expect(page.getByRole('button', { name: 'ثبت نتیجه' })).toBeDisabled()
await expect(page.getByText('حداقل ۳۰ سکه')).toBeVisible()
for (let i = 0; i < 2; i += 1) await page.getByRole('button', { name: 'افزایش رفت‌وآمد' }).click()
await expect(page.getByRole('button', { name: 'ثبت نتیجه' })).toBeEnabled()
const before = await fetchMe(page)
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await expect(page.getByText('پاداش:')).toBeVisible()
await page.getByRole('button', { name: 'بازگشت به نقشه' }).click()
await page.waitForURL(/\/map/)
await page.reload()
const after = await fetchMe(page)
expect(after.progress.mapLevels.find((level) => level.id === 7)?.stars).toBe(3)
expect(after.user.coins).toBe(before.user.coins + 18 + 9)
await page.getByRole('button', { name: 'مرحله 7: بودجه‌بندی' }).click()
await page.getByRole('button', { name: 'مرور مجدد' }).click()
await page.getByRole('button', { name: 'ساخت بودجه' }).click()
for (let i = 0; i < 4; i += 1) await page.getByRole('button', { name: 'افزایش رفت‌وآمد' }).click()
for (let i = 0; i < 5; i += 1) await page.getByRole('button', { name: 'افزایش آب و غذا' }).click()
for (let i = 0; i < 4; i += 1) await page.getByRole('button', { name: 'افزایش پس‌انداز برای هدف' }).click()
await page.getByRole('button', { name: 'بررسی برنامه' }).click()
await page.getByRole('button', { name: 'تأیید برنامه' }).click()
await page.getByRole('button', { name: 'بازتنظیم بودجه' }).click()
for (let i = 0; i < 2; i += 1) await page.getByRole('button', { name: 'افزایش رفت‌وآمد' }).click()
const replayBefore = await fetchMe(page)
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await expect(page.getByText('پاداش تازه‌ای اضافه نشد')).toBeVisible()
const replayAfter = await fetchMe(page)
expect(replayAfter.user.coins).toBe(replayBefore.user.coins)
expect(replayAfter.user.xp).toBe(replayBefore.user.xp)
await page.goto('/map')
await expect(page.getByRole('button', { name: 'مرحله 9: سبد سرمایه‌گذاری' })).toBeDisabled()
})
+112
View File
@@ -0,0 +1,112 @@
import { test, expect, type Page } from '@playwright/test'
import { fetchMe, registerViaUi, uniqueUsername } from './helpers/auth'
const correct = [
'مبادله را آسان می‌کند و مقایسه قیمت را ممکن می‌سازد',
'مبلغی متناسب با شرایط را منظم کنار بگذاریم',
'اول آب آشامیدنی را تأمین کنیم',
'طلا یک دارایی است و قیمتش می‌تواند بالا یا پایین برود',
'خیر',
'با همان پول، مقدار کمتری از آن کالاها می‌توان خرید',
'رفت‌وآمد ۳۰، غذا ۲۵، پس‌انداز ۲۰، تفریح ۱۵، ذخیره ۱۰',
'پول لازم را وارد پیشنهاد نامطمئن با احتمال زیان نکنیم',
'خیر؛ چند گزینه ممکن است هم‌زمان افت کنند',
'۱۲۱',
'ب، چون ۴۵ در برابر ۵۰ است',
'۶ هفته',
]
const wrongTail = [
'همان پول ضروری را برای سود بیشتر وارد پیشنهاد نامطمئن کنیم',
'بله، با چند گزینه دیگر زیان ممکن نیست',
'۱۲۰',
'الف، چون عدد روی کارت کوچک‌تر است',
'۸ هفته',
]
async function finishThroughGoal(page: Page, headers: { authorization: string }) {
const lessons: Record<number, unknown> = {
1: [{ questionId: 'money-q1', answerId: 'option-a' }, { questionId: 'money-q2', answerId: 'true' }, { questionId: 'money-q3', answerId: 'option-a' }, { questionId: 'money-q4', answerId: 'true' }, { questionId: 'money-q5', answerId: 'option-b' }],
2: [{ questionId: 'saving-q1', answerId: 'option-b' }, { questionId: 'saving-q2', answerId: 'true' }, { questionId: 'saving-q3', answerId: 'option-a' }, { questionId: 'saving-q4', answerId: 'true' }, { questionId: 'saving-q5', answerId: 'option-a' }],
3: [{ questionId: 'needs-q1', answerId: 'option-a' }, { questionId: 'needs-q2', answerId: 'true' }, { questionId: 'needs-q3', orderedItemIds: ['water', 'food', 'medicine', 'shoes', 'toy', 'crown'] }, { questionId: 'needs-q4', answerId: 'option-a' }, { questionId: 'needs-q5', answerId: 'true' }],
4: [{ questionId: 'gold-q1', answerId: 'option-a' }, { questionId: 'gold-q2', answerId: 'true' }, { questionId: 'gold-q3', answerId: 'option-a' }, { questionId: 'gold-q4', answerId: 'false' }, { questionId: 'gold-q5', answerId: 'option-b' }],
5: [{ questionId: 'market-q1', answerId: 'option-a' }, { questionId: 'market-q2', answerId: 'false' }, { questionId: 'market-q3', answerId: 'option-a' }, { questionId: 'market-q4', answerId: 'true' }, { questionId: 'market-q5', answerId: 'option-b' }],
6: [{ questionId: 'inflation-q1', answerId: 'option-b' }, { questionId: 'inflation-q2', answerId: 'true' }, { questionId: 'inflation-q3', answerId: 'option-a' }, { questionId: 'inflation-q4', answerId: 'false' }, { questionId: 'inflation-q5', answerId: 'option-b' }],
}
for (let levelId = 1; levelId <= 6; levelId += 1) {
expect((await page.request.post(`/api/gold-land/levels/${levelId}/start`, { headers })).ok()).toBeTruthy()
expect((await page.request.post(`/api/gold-land/levels/${levelId}/complete`, { headers, data: { answers: lessons[levelId] } })).ok()).toBeTruthy()
}
const later = [
{ answers: [{ questionId: 'initial', allocations: { transport: 20, food: 25, savings: 20, fun: 0 } }, { questionId: 'final', allocations: { transport: 30, food: 25, savings: 20, fun: 0 } }] },
{ answers: [{ questionId: 'q-loss', answerId: 'b' }, { questionId: 'q-certain', answerId: 'no' }, { questionId: 'q-need', answerId: 'keep' }, { questionId: 'q-compare', answerId: 'range' }, { questionId: 'q-promise', answerId: 'check' }] },
{ answers: [{ questionId: 'portfolio', holdings: { sun: 40, rain: 30, wind: 30 } }, { questionId: 'q-one', answerId: 'depend' }, { questionId: 'q-loss', answerId: 'no' }, { questionId: 'q-name', answerId: 'no' }, { questionId: 'q-best', answerId: 'no' }, { questionId: 'q-approach', answerId: 'balanced' }] },
{ answers: [{ questionId: 'compound-q1', answerId: 'on-balance' }, { questionId: 'compound-q2', answerId: '121' }, { questionId: 'compound-q3', answerId: 'from-110' }, { questionId: 'compound-q4', answerId: 'compound' }, { questionId: 'compound-q5', answerId: 'false' }] },
{ answers: [{ questionId: 'notebook-choice', answerId: 'b' }, { questionId: 'notebook-why', answerId: 'fits-and-cheaper' }, { questionId: 'discount-choice', answerId: 'b' }, { questionId: 'discount-why', answerId: 'pay-amount' }, { questionId: 'info-choice', answerId: 'ask' }, { questionId: 'info-why', answerId: 'incomplete' }] },
{ answers: [{ questionId: 'plan', plan: { initialWeekly: 15, exceptionWeekly: 5, continueWeekly: 15 } }, { questionId: 'q-goal', answerId: 'specific' }, { questionId: 'q-left', answerId: '90' }, { questionId: 'q-weeks', answerId: '6' }, { questionId: 'q-need', answerId: 'adjust' }, { questionId: 'q-change', answerId: 'update' }] },
]
for (let index = 0; index < later.length; index += 1) {
const levelId = index + 7
expect((await page.request.post(`/api/gold-land/levels/${levelId}/start`, { headers })).ok()).toBeTruthy()
expect((await page.request.post(`/api/gold-land/levels/${levelId}/complete`, { headers, data: later[index] })).ok()).toBeTruthy()
}
}
async function answerExam(page: Page, labels: string[]) {
for (let index = 0; index < labels.length; index += 1) {
await page.getByText(labels[index]!, { exact: true }).click()
const name = index === labels.length - 1 ? 'مرور پاسخ‌ها' : 'بعدی'
await page.getByRole('button', { name }).click()
}
}
test('castle exam from the map', async ({ page }) => {
const user = uniqueUsername('castle')
await registerViaUi(page, user, 'pass1234', 'قلعه')
const token = await page.evaluate(() => localStorage.getItem('gold-land-auth-token'))
await finishThroughGoal(page, { authorization: `Bearer ${token}` })
await page.goto('/map')
await page.getByRole('button', { name: 'مرحله 13: آزمون قلعه' }).click()
await page.getByRole('button', { name: 'شروع آزمون' }).click()
await page.getByRole('button', { name: 'شروع سؤال‌ها' }).click()
await expect(page.getByText('پایه‌های پول')).toBeVisible()
const beforeFail = await fetchMe(page)
await answerExam(page, [...correct.slice(0, 7), ...wrongTail])
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await expect(page.getByText('این تلاش کامل نشد')).toBeVisible()
await expect(page.getByRole('button', { name: 'مرحله بعد' })).toHaveCount(0)
const afterFail = await fetchMe(page)
expect(afterFail.user.coins).toBe(beforeFail.user.coins)
await page.getByRole('button', { name: 'تلاش مجدد' }).click()
await answerExam(page, correct)
const before = await fetchMe(page)
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await expect(page.getByText('آفرین! مسیر آموزشی قلعه را کامل کردی')).toBeVisible()
await expect(page.getByText('پاداش:')).toBeVisible()
await expect(page.getByRole('button', { name: 'مرحله بعد' })).toHaveCount(0)
await page.getByRole('button', { name: 'بازگشت به نقشه' }).click()
await page.reload()
const after = await fetchMe(page)
expect(after.progress.mapLevels.find((level) => level.id === 13)?.stars).toBe(3)
expect(after.user.coins).toBe(before.user.coins + 50 + 9)
await page.getByRole('button', { name: 'مرحله 13: آزمون قلعه' }).click()
await page.getByRole('button', { name: 'مرور مجدد' }).click()
await page.getByRole('button', { name: 'شروع سؤال‌ها' }).click()
await answerExam(page, correct)
const replayBefore = await fetchMe(page)
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await expect(page.getByText('پاداش تازه‌ای اضافه نشد')).toBeVisible()
const replayAfter = await fetchMe(page)
expect(replayAfter.user.coins).toBe(replayBefore.user.coins)
await page.getByRole('button', { name: 'تلاش مجدد' }).click()
await answerExam(page, [...correct.slice(0, 7), ...wrongTail])
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await expect(page.getByText('این تلاش کامل نشد')).toBeVisible()
await expect(page.getByText('ستاره ذخیره‌شده همچنان 3 از ۳ است')).toBeVisible()
await expect(page.getByText('آفرین! مسیر آموزشی قلعه را کامل کردی')).toHaveCount(0)
const kept = await fetchMe(page)
expect(kept.progress.mapLevels.find((level) => level.id === 13)?.stars).toBe(3)
expect(kept.user.coins).toBe(replayAfter.user.coins)
})
+149
View File
@@ -0,0 +1,149 @@
import { test, expect } from '@playwright/test'
import { fetchMe, registerViaUi, uniqueUsername } from './helpers/auth'
const lessons: Record<number, Array<Record<string, unknown>>> = {
1: [
{ questionId: 'money-q1', answerId: 'option-a' },
{ questionId: 'money-q2', answerId: 'true' },
{ questionId: 'money-q3', answerId: 'option-a' },
{ questionId: 'money-q4', answerId: 'true' },
{ questionId: 'money-q5', answerId: 'option-b' },
],
2: [
{ questionId: 'saving-q1', answerId: 'option-b' },
{ questionId: 'saving-q2', answerId: 'true' },
{ questionId: 'saving-q3', answerId: 'option-a' },
{ questionId: 'saving-q4', answerId: 'true' },
{ questionId: 'saving-q5', answerId: 'option-a' },
],
3: [
{ questionId: 'needs-q1', answerId: 'option-a' },
{ questionId: 'needs-q2', answerId: 'true' },
{ questionId: 'needs-q3', orderedItemIds: ['water', 'food', 'medicine', 'shoes', 'toy', 'crown'] },
{ questionId: 'needs-q4', answerId: 'option-a' },
{ questionId: 'needs-q5', answerId: 'true' },
],
4: [
{ questionId: 'gold-q1', answerId: 'option-a' },
{ questionId: 'gold-q2', answerId: 'true' },
{ questionId: 'gold-q3', answerId: 'option-a' },
{ questionId: 'gold-q4', answerId: 'false' },
{ questionId: 'gold-q5', answerId: 'option-b' },
],
5: [
{ questionId: 'market-q1', answerId: 'option-a' },
{ questionId: 'market-q2', answerId: 'false' },
{ questionId: 'market-q3', answerId: 'option-a' },
{ questionId: 'market-q4', answerId: 'true' },
{ questionId: 'market-q5', answerId: 'option-b' },
],
6: [
{ questionId: 'inflation-q1', answerId: 'option-b' },
{ questionId: 'inflation-q2', answerId: 'true' },
{ questionId: 'inflation-q3', answerId: 'option-a' },
{ questionId: 'inflation-q4', answerId: 'false' },
{ questionId: 'inflation-q5', answerId: 'option-b' },
],
}
async function answerCompound(page: import('@playwright/test').Page) {
const choices = [
'رشد دوره بعد بر موجودی جدید، شامل رشد قبلی، محاسبه می‌شود',
'۱۲۱ سکه',
'چون ۱۰٪ از ۱۱۰ محاسبه شده است',
'مرکب؛ ۱۳۳٫۱ در برابر ۱۳۰',
]
for (const label of choices) {
await page.getByRole('button', { name: label, exact: true }).click()
await page.getByRole('button', { name: 'بعدی' }).click()
}
await page.getByRole('button', { name: 'نادرست', exact: true }).click()
await page.getByRole('button', { name: 'بعدی' }).click()
}
test('compound interest lesson from the map', async ({ page }) => {
const user = uniqueUsername('cmp')
await registerViaUi(page, user, 'pass1234', 'مرکب')
const token = await page.evaluate(() => localStorage.getItem('gold-land-auth-token'))
const headers = { authorization: `Bearer ${token}` }
for (let levelId = 1; levelId <= 6; levelId += 1) {
expect((await page.request.post(`/api/gold-land/levels/${levelId}/start`, { headers })).ok()).toBeTruthy()
expect((await page.request.post(`/api/gold-land/levels/${levelId}/complete`, {
headers,
data: { answers: lessons[levelId] },
})).ok()).toBeTruthy()
}
expect((await page.request.post('/api/gold-land/levels/7/start', { headers })).ok()).toBeTruthy()
expect((await page.request.post('/api/gold-land/levels/7/complete', {
headers,
data: {
answers: [
{ questionId: 'initial', allocations: { transport: 20, food: 25, savings: 20, fun: 0 } },
{ questionId: 'final', allocations: { transport: 30, food: 25, savings: 20, fun: 0 } },
],
},
})).ok()).toBeTruthy()
expect((await page.request.post('/api/gold-land/levels/8/start', { headers })).ok()).toBeTruthy()
expect((await page.request.post('/api/gold-land/levels/8/complete', {
headers,
data: {
answers: [
{ questionId: 'q-loss', answerId: 'b' },
{ questionId: 'q-certain', answerId: 'no' },
{ questionId: 'q-need', answerId: 'keep' },
{ questionId: 'q-compare', answerId: 'range' },
{ questionId: 'q-promise', answerId: 'check' },
],
},
})).ok()).toBeTruthy()
expect((await page.request.post('/api/gold-land/levels/9/start', { headers })).ok()).toBeTruthy()
expect((await page.request.post('/api/gold-land/levels/9/complete', {
headers,
data: {
answers: [
{ questionId: 'portfolio', holdings: { sun: 40, rain: 30, wind: 30 } },
{ questionId: 'q-one', answerId: 'depend' },
{ questionId: 'q-loss', answerId: 'no' },
{ questionId: 'q-name', answerId: 'no' },
{ questionId: 'q-best', answerId: 'no' },
{ questionId: 'q-approach', answerId: 'balanced' },
],
},
})).ok()).toBeTruthy()
await page.goto('/map')
await page.getByRole('button', { name: 'مرحله 10: سود مرکب' }).click()
await page.getByRole('button', { name: 'شروع درس' }).click()
await page.getByRole('button', { name: 'بعدی' }).click()
await page.getByRole('button', { name: 'دوره بعد' }).click()
await page.getByRole('button', { name: 'دوره بعد' }).click()
await page.getByRole('button', { name: 'دوره بعد' }).click()
await expect(page.getByText('۱۳۳٫۱ سکه').first()).toBeVisible()
await expect(page.getByText('۱۲۱ + ۱۲٫۱ = ۱۳۳٫۱')).toBeVisible()
await page.getByRole('button', { name: 'دوره قبل' }).click()
await page.getByRole('button', { name: 'بعدی' }).click()
await answerCompound(page)
const before = await fetchMe(page)
await page.getByRole('button', { name: 'اتمام درس' }).click()
await expect(page.getByText('۱۰٪ آن ۱۱ سکه')).toBeVisible()
await page.getByRole('button', { name: 'بازگشت به نقشه' }).click()
await page.reload()
const after = await fetchMe(page)
expect(after.progress.mapLevels.find((level) => level.id === 10)?.stars).toBe(3)
expect(after.user.coins).toBe(before.user.coins + 18 + 9)
await page.getByRole('button', { name: 'مرحله 10: سود مرکب' }).click()
await page.getByRole('button', { name: 'مرور مجدد' }).click()
await page.getByRole('button', { name: 'بعدی' }).click()
await page.getByRole('button', { name: 'بعدی' }).click()
await answerCompound(page)
const replayBefore = await fetchMe(page)
await page.getByRole('button', { name: 'اتمام درس' }).click()
await expect(page.getByText('امتیاز')).toBeVisible()
const replayAfter = await fetchMe(page)
expect(replayAfter.user.coins).toBe(replayBefore.user.coins)
await page.goto('/map')
await expect(page.getByRole('button', { name: 'مرحله 12: هدف مالی' })).toBeDisabled()
})
+89
View File
@@ -0,0 +1,89 @@
import { test, expect, type Page } from '@playwright/test'
import { fetchMe, registerViaUi, uniqueUsername } from './helpers/auth'
async function finishThroughShop(page: Page, headers: { authorization: string }) {
const lessons: Record<number, unknown> = {
1: [{ questionId: 'money-q1', answerId: 'option-a' }, { questionId: 'money-q2', answerId: 'true' }, { questionId: 'money-q3', answerId: 'option-a' }, { questionId: 'money-q4', answerId: 'true' }, { questionId: 'money-q5', answerId: 'option-b' }],
2: [{ questionId: 'saving-q1', answerId: 'option-b' }, { questionId: 'saving-q2', answerId: 'true' }, { questionId: 'saving-q3', answerId: 'option-a' }, { questionId: 'saving-q4', answerId: 'true' }, { questionId: 'saving-q5', answerId: 'option-a' }],
3: [{ questionId: 'needs-q1', answerId: 'option-a' }, { questionId: 'needs-q2', answerId: 'true' }, { questionId: 'needs-q3', orderedItemIds: ['water', 'food', 'medicine', 'shoes', 'toy', 'crown'] }, { questionId: 'needs-q4', answerId: 'option-a' }, { questionId: 'needs-q5', answerId: 'true' }],
4: [{ questionId: 'gold-q1', answerId: 'option-a' }, { questionId: 'gold-q2', answerId: 'true' }, { questionId: 'gold-q3', answerId: 'option-a' }, { questionId: 'gold-q4', answerId: 'false' }, { questionId: 'gold-q5', answerId: 'option-b' }],
5: [{ questionId: 'market-q1', answerId: 'option-a' }, { questionId: 'market-q2', answerId: 'false' }, { questionId: 'market-q3', answerId: 'option-a' }, { questionId: 'market-q4', answerId: 'true' }, { questionId: 'market-q5', answerId: 'option-b' }],
6: [{ questionId: 'inflation-q1', answerId: 'option-b' }, { questionId: 'inflation-q2', answerId: 'true' }, { questionId: 'inflation-q3', answerId: 'option-a' }, { questionId: 'inflation-q4', answerId: 'false' }, { questionId: 'inflation-q5', answerId: 'option-b' }],
}
for (let levelId = 1; levelId <= 6; levelId += 1) {
expect((await page.request.post(`/api/gold-land/levels/${levelId}/start`, { headers })).ok()).toBeTruthy()
expect((await page.request.post(`/api/gold-land/levels/${levelId}/complete`, { headers, data: { answers: lessons[levelId] } })).ok()).toBeTruthy()
}
const later = [
{ answers: [{ questionId: 'initial', allocations: { transport: 20, food: 25, savings: 20, fun: 0 } }, { questionId: 'final', allocations: { transport: 30, food: 25, savings: 20, fun: 0 } }] },
{ answers: [{ questionId: 'q-loss', answerId: 'b' }, { questionId: 'q-certain', answerId: 'no' }, { questionId: 'q-need', answerId: 'keep' }, { questionId: 'q-compare', answerId: 'range' }, { questionId: 'q-promise', answerId: 'check' }] },
{ answers: [{ questionId: 'portfolio', holdings: { sun: 40, rain: 30, wind: 30 } }, { questionId: 'q-one', answerId: 'depend' }, { questionId: 'q-loss', answerId: 'no' }, { questionId: 'q-name', answerId: 'no' }, { questionId: 'q-best', answerId: 'no' }, { questionId: 'q-approach', answerId: 'balanced' }] },
{ answers: [{ questionId: 'compound-q1', answerId: 'on-balance' }, { questionId: 'compound-q2', answerId: '121' }, { questionId: 'compound-q3', answerId: 'from-110' }, { questionId: 'compound-q4', answerId: 'compound' }, { questionId: 'compound-q5', answerId: 'false' }] },
{ answers: [{ questionId: 'notebook-choice', answerId: 'b' }, { questionId: 'notebook-why', answerId: 'fits-and-cheaper' }, { questionId: 'discount-choice', answerId: 'b' }, { questionId: 'discount-why', answerId: 'pay-amount' }, { questionId: 'info-choice', answerId: 'ask' }, { questionId: 'info-why', answerId: 'incomplete' }] },
]
for (let index = 0; index < later.length; index += 1) {
const levelId = index + 7
expect((await page.request.post(`/api/gold-land/levels/${levelId}/start`, { headers })).ok()).toBeTruthy()
expect((await page.request.post(`/api/gold-land/levels/${levelId}/complete`, { headers, data: later[index] })).ok()).toBeTruthy()
}
}
async function answerGoal(page: Page) {
const labels = [
'خرید بسته مشخص با قیمت ۱۲۰ و برنامه پس‌انداز',
'۹۰ سکه',
'۶ هفته',
'نیاز را تأمین کن و مبلغ پس‌انداز یا زمان هدف را اصلاح کن',
'برنامه و زمان را دوباره بررسی و به‌روز کن',
]
for (let index = 0; index < labels.length; index += 1) {
await page.getByText(labels[index]!, { exact: true }).click()
if (index < labels.length - 1) await page.getByRole('button', { name: 'بعدی' }).click()
else await page.getByRole('button', { name: 'مرور پاسخ‌ها' }).click()
}
}
async function buildPlan(page: Page) {
await page.getByRole('button', { name: 'ساخت برنامه' }).click()
await page.getByRole('button', { name: '15 سکه در هفته' }).click()
await expect(page.getByText('برآورد زمان: 6 هفته')).toBeVisible()
await page.getByRole('button', { name: 'تأیید برنامه اولیه' }).click()
await page.getByRole('button', { name: 'هفته دوم: 5 سکه' }).click()
await page.getByRole('button', { name: 'ادامه: 15 سکه' }).click()
await expect(page.getByText('کل زمان: 7 هفته')).toBeVisible()
await expect(page.getByText('موجودی نهایی برآوردی: 125 سکه')).toBeVisible()
await page.getByRole('button', { name: 'رفتن به سؤال‌ها' }).click()
await answerGoal(page)
}
test('financial goal from the map', async ({ page }) => {
const user = uniqueUsername('goal')
await registerViaUi(page, user, 'pass1234', 'هدف')
const token = await page.evaluate(() => localStorage.getItem('gold-land-auth-token'))
await finishThroughShop(page, { authorization: `Bearer ${token}` })
await page.goto('/map')
await page.getByRole('button', { name: 'مرحله 12: هدف مالی' }).click()
await page.getByRole('button', { name: 'شروع چالش' }).click()
await buildPlan(page)
const before = await fetchMe(page)
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await expect(page.getByText('پاداش:')).toBeVisible()
await page.getByRole('button', { name: 'بازگشت به نقشه' }).click()
await page.reload()
const after = await fetchMe(page)
expect(after.progress.mapLevels.find((level) => level.id === 12)?.stars).toBe(3)
expect(after.user.coins).toBe(before.user.coins + 20 + 9)
await page.getByRole('button', { name: 'مرحله 12: هدف مالی' }).click()
await page.getByRole('button', { name: 'مرور مجدد' }).click()
await buildPlan(page)
const replayBefore = await fetchMe(page)
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await expect(page.getByText('پاداش تازه‌ای اضافه نشد')).toBeVisible()
const replayAfter = await fetchMe(page)
expect(replayAfter.user.coins).toBe(replayBefore.user.coins)
await page.goto('/map')
await page.getByRole('button', { name: 'مرحله 13: آزمون قلعه' }).click()
await expect(page.getByRole('button', { name: 'شروع آزمون' })).toBeEnabled()
})
+242
View File
@@ -0,0 +1,242 @@
import { test, expect, type Page } from '@playwright/test'
import { fetchMe, loginViaUi, logoutViaProfile, registerViaUi, uniqueUsername } from './helpers/auth'
test.describe.configure({ mode: 'serial' })
async function openLevel(page: Page, name: string, start: string) {
await page.goto('/map')
await page.getByRole('button', { name }).click()
await page.getByRole('button', { name: start }).click()
await page.getByText('در حال بارگذاری').waitFor({ state: 'detached', timeout: 30_000 }).catch(() => undefined)
}
async function clickAnswer(page: Page, label: string) {
if (label === 'درست' || label === 'نادرست') {
await page.getByRole('button', { name: label, exact: true }).click()
return
}
const button = page.getByRole('button', { name: label, exact: true })
if (await button.count()) {
await button.first().click()
return
}
await page.getByText(label, { exact: true }).click()
}
async function finishLesson(page: Page, labels: string[]) {
let cursor = 0
for (let step = 0; step < 30; step += 1) {
if (await page.getByText('امتیاز').first().isVisible().catch(() => false)) return
if (await page.getByText('خوراکی ضروری').isVisible().catch(() => false)) {
for (let move = 0; move < 4; move += 1) {
await page.locator('.sort-item').filter({ hasText: 'خوراکی ضروری' }).getByRole('button', { name: '↑' }).click()
}
}
if (cursor < labels.length) {
const label = labels[cursor]!
const visible = await page.getByText(label, { exact: true }).isVisible().catch(() => false)
if (visible) {
await clickAnswer(page, label)
cursor += 1
}
}
const finish = page.getByRole('button', { name: 'اتمام درس' })
if (await finish.isVisible().catch(() => false) && await finish.isEnabled().catch(() => false)) {
await finish.click()
await page.getByText('امتیاز').first().waitFor()
return
}
const next = page.getByRole('button', { name: 'بعدی' })
if (await next.isEnabled().catch(() => false)) await next.click()
}
throw new Error('lesson did not finish')
}
async function starsOf(page: Page, levelId: number) {
const me = await fetchMe(page)
return me.progress.mapLevels.find((level) => level.id === levelId)?.stars ?? 0
}
test('full thirteen-level journey from the interface', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'desktop', 'full journey runs once on desktop')
test.setTimeout(600_000)
const errors: string[] = []
page.on('pageerror', (error) => errors.push(error.message))
page.on('console', (message) => {
if (message.type() === 'error') errors.push(message.text())
})
const user = uniqueUsername('path')
await registerViaUi(page, user, 'pass1234', 'مسیر')
const lessons: Array<{ name: string; labels: string[] }> = [
{ name: 'مرحله 1: پول چیه؟', labels: ['مبادله آسان‌تر کالاها', 'درست', 'چون همه همیشه همان چیز را می‌خواهند', 'درست', 'پول وسیلهٔ مبادله و نگه‌داری ارزش است'] },
{ name: 'مرحله 2: پس‌انداز', labels: ['بخشی از پول را برای بعد کنار گذاشتن', 'درست', 'برای هدفش برنامه نداشت و همه را خرج کرد', 'درست', 'هدف آینده چیزی است که برایش برنامه می‌ریزیم'] },
{ name: 'مرحله 3: نیاز و خواسته', labels: ['چیزی که برای زندگی سالم لازم است', 'درست', 'اول نیازها را برطرف کنیم', 'درست'] },
{ name: 'مرحله 4: طلا چیه؟', labels: ['چون واحد استاندارد وزن طلا است', 'درست', 'زیورآلات و صنعت', 'نادرست', 'طلا دارایی با ارزش است که قیمتش می‌تواند تغییر کند'] },
{ name: 'مرحله 5: بازار طلا', labels: ['قیمت بالا و پایین می‌شود', 'نادرست', 'چون ممکن است وقت کافی برای فکر کردن نداشته باشیم', 'درست', 'فکر کردن و پراکنده نکردن تمام پول در یک دارایی'] },
{ name: 'مرحله 6: تورم', labels: ['افزایش عمومی قیمت‌ها در طول زمان', 'درست', 'قدرت خرید پول کم شده', 'نادرست', 'ممکن است ارزش واقعی‌اش کم شود'] },
]
for (let index = 0; index < lessons.length; index += 1) {
const lesson = lessons[index]!
await openLevel(page, lesson.name, 'شروع درس')
await finishLesson(page, lesson.labels)
await page.getByRole('button', { name: 'بازگشت به نقشه' }).click()
expect(await starsOf(page, index + 1)).toBeGreaterThan(0)
if (index < 5) {
await expect(page.getByRole('button', { name: lessons[index + 1]!.name })).toBeEnabled()
}
}
await page.reload()
expect(await starsOf(page, 6)).toBeGreaterThan(0)
await openLevel(page, 'مرحله 7: بودجه‌بندی', 'شروع بازی')
await page.getByRole('button', { name: 'ساخت بودجه' }).click()
for (let index = 0; index < 4; index += 1) await page.getByRole('button', { name: 'افزایش رفت‌وآمد' }).click()
for (let index = 0; index < 5; index += 1) await page.getByRole('button', { name: 'افزایش آب و غذا' }).click()
for (let index = 0; index < 4; index += 1) await page.getByRole('button', { name: 'افزایش پس‌انداز برای هدف' }).click()
await page.getByRole('button', { name: 'بررسی برنامه' }).click()
await page.getByRole('button', { name: 'تأیید برنامه' }).click()
await page.getByRole('button', { name: 'بازتنظیم بودجه' }).click()
for (let index = 0; index < 2; index += 1) await page.getByRole('button', { name: 'افزایش رفت‌وآمد' }).click()
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await page.getByRole('button', { name: 'بازگشت به نقشه' }).click()
expect(await starsOf(page, 7)).toBeGreaterThan(0)
await openLevel(page, 'مرحله 8: ریسک و بازده', 'شروع چالش')
await page.getByRole('button', { name: 'دیدن دو پیشنهاد' }).click()
await page.getByRole('button', { name: 'رفتن به فعالیت‌ها' }).click()
const risk = [
'پیشنهاد ب',
'خیر، ۳۶ فقط یکی از نتیجه‌های ممکن است',
'این پول ضروری را وارد پیشنهادهای نامطمئن نکن',
'پیشنهاد ب امکان نتیجه بالاتر و زیان بیشتری دارد؛ سود بیشتر قطعی نیست',
'وعده را تضمین معتبر فرض نکن، بررسی کن و از بزرگسال قابل اعتماد کمک بگیر',
]
for (let index = 0; index < risk.length; index += 1) {
await page.getByText(risk[index]!, { exact: true }).click()
await page.getByRole('button', { name: index === risk.length - 1 ? 'مرور پاسخ‌ها' : 'بعدی' }).click()
}
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await page.getByRole('button', { name: 'بازگشت به نقشه' }).click()
expect(await starsOf(page, 8)).toBeGreaterThan(0)
await openLevel(page, 'مرحله 9: سبد سرمایه‌گذاری', 'شروع بازی')
await page.getByRole('button', { name: 'ساخت سبد' }).click()
for (let index = 0; index < 4; index += 1) await page.getByRole('button', { name: 'افزایش آفتاب' }).click()
for (let index = 0; index < 3; index += 1) await page.getByRole('button', { name: 'افزایش باران' }).click()
for (let index = 0; index < 3; index += 1) await page.getByRole('button', { name: 'افزایش باد' }).click()
await page.getByRole('button', { name: 'دیدن سه سناریو' }).click()
await page.getByRole('button', { name: 'رفتن به سؤال‌ها' }).click()
const portfolio = [
'نتیجه بیشتر به همان گزینه وابسته می‌شود',
'خیر؛ افت همگانی می‌تواند چند گزینه را هم‌زمان کم کند',
'خیر؛ رفتار و وابستگی آن‌ها مهم است',
'خیر؛ یک سناریو آینده را تضمین نمی‌کند',
'توجه به هدف، زمان نیاز به پول و ریسک؛ در مسائل واقعی کمک از بزرگسال قابل اعتماد',
]
for (let index = 0; index < portfolio.length; index += 1) {
await page.getByText(portfolio[index]!, { exact: true }).click()
await page.getByRole('button', { name: index === portfolio.length - 1 ? 'مرور پاسخ‌ها' : 'بعدی' }).click()
}
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await page.getByRole('button', { name: 'بازگشت به نقشه' }).click()
expect(await starsOf(page, 9)).toBeGreaterThan(0)
await openLevel(page, 'مرحله 10: سود مرکب', 'شروع درس')
await page.getByRole('button', { name: 'بعدی' }).click()
await page.getByRole('button', { name: 'دوره بعد' }).click()
await page.getByRole('button', { name: 'دوره بعد' }).click()
await page.getByRole('button', { name: 'دوره بعد' }).click()
await page.getByRole('button', { name: 'بعدی' }).click()
const compound = [
'رشد دوره بعد بر موجودی جدید، شامل رشد قبلی، محاسبه می‌شود',
'۱۲۱ سکه',
'چون ۱۰٪ از ۱۱۰ محاسبه شده است',
'مرکب؛ ۱۳۳٫۱ در برابر ۱۳۰',
]
for (const label of compound) {
await page.getByRole('button', { name: label, exact: true }).click()
await page.getByRole('button', { name: 'بعدی' }).click()
}
await page.getByRole('button', { name: 'نادرست', exact: true }).click()
await page.getByRole('button', { name: 'بعدی' }).click()
await page.getByRole('button', { name: 'اتمام درس' }).click()
await page.getByRole('button', { name: 'بازگشت به نقشه' }).click()
expect(await starsOf(page, 10)).toBeGreaterThan(0)
await openLevel(page, 'مرحله 11: خرید هوشمند', 'شروع بازی')
await page.getByRole('button', { name: 'شروع خریدها' }).click()
const shop = [
['دفتر ۶۰برگ، ۴۰ سکه، تحویل ۵ سکه', 'نیاز را برآورده می‌کند و هزینه نهایی‌اش بین گزینه‌های مناسب کمتر است'],
['مداد با قیمت نهایی ۲۸، بدون تخفیف', 'برای کالای یکسان، مبلغ نهایی پرداختی مهم است؛ درصد تخفیف به‌تنهایی کافی نیست'],
['فعلاً نخر و اندازه و هزینه نهایی را بپرس، با کمک بزرگسال قابل اعتماد', 'اطلاعات لازم برای تصمیم و مقایسه هنوز کامل نیست'],
]
for (let index = 0; index < shop.length; index += 1) {
await page.getByText(shop[index]![0]!, { exact: true }).click()
await page.getByText(shop[index]![1]!, { exact: true }).click()
await page.getByRole('button', { name: index === shop.length - 1 ? 'مرور انتخاب‌ها' : 'مأموریت بعد' }).click()
}
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await page.getByRole('button', { name: 'بازگشت به نقشه' }).click()
expect(await starsOf(page, 11)).toBeGreaterThan(0)
await openLevel(page, 'مرحله 12: هدف مالی', 'شروع چالش')
await page.getByRole('button', { name: 'ساخت برنامه' }).click()
await page.getByRole('button', { name: '15 سکه در هفته' }).click()
await page.getByRole('button', { name: 'تأیید برنامه اولیه' }).click()
await page.getByRole('button', { name: 'هفته دوم: 5 سکه' }).click()
await page.getByRole('button', { name: 'ادامه: 15 سکه' }).click()
await page.getByRole('button', { name: 'رفتن به سؤال‌ها' }).click()
const goal = [
'خرید بسته مشخص با قیمت ۱۲۰ و برنامه پس‌انداز',
'۹۰ سکه',
'۶ هفته',
'نیاز را تأمین کن و مبلغ پس‌انداز یا زمان هدف را اصلاح کن',
'برنامه و زمان را دوباره بررسی و به‌روز کن',
]
for (let index = 0; index < goal.length; index += 1) {
await page.getByText(goal[index]!, { exact: true }).click()
await page.getByRole('button', { name: index === goal.length - 1 ? 'مرور پاسخ‌ها' : 'بعدی' }).click()
}
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await page.getByRole('button', { name: 'بازگشت به نقشه' }).click()
expect(await starsOf(page, 12)).toBeGreaterThan(0)
await page.reload()
expect(await starsOf(page, 12)).toBeGreaterThan(0)
await expect(page.getByRole('button', { name: 'مرحله 14', exact: false })).toHaveCount(0)
await openLevel(page, 'مرحله 13: آزمون قلعه', 'شروع آزمون')
await page.getByRole('button', { name: 'شروع سؤال‌ها' }).click()
const castle = [
'مبادله را آسان می‌کند و مقایسه قیمت را ممکن می‌سازد',
'مبلغی متناسب با شرایط را منظم کنار بگذاریم',
'اول آب آشامیدنی را تأمین کنیم',
'طلا یک دارایی است و قیمتش می‌تواند بالا یا پایین برود',
'خیر',
'با همان پول، مقدار کمتری از آن کالاها می‌توان خرید',
'رفت‌وآمد ۳۰، غذا ۲۵، پس‌انداز ۲۰، تفریح ۱۵، ذخیره ۱۰',
'پول لازم را وارد پیشنهاد نامطمئن با احتمال زیان نکنیم',
'خیر؛ چند گزینه ممکن است هم‌زمان افت کنند',
'۱۲۱',
'ب، چون ۴۵ در برابر ۵۰ است',
'۶ هفته',
]
for (let index = 0; index < castle.length; index += 1) {
await page.getByText(castle[index]!, { exact: true }).click()
await page.getByRole('button', { name: index === castle.length - 1 ? 'مرور پاسخ‌ها' : 'بعدی' }).click()
}
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await expect(page.getByText('آفرین! مسیر آموزشی قلعه را کامل کردی')).toBeVisible()
await expect(page.getByRole('button', { name: 'مرحله بعد' })).toHaveCount(0)
await page.getByRole('button', { name: 'بازگشت به نقشه' }).click()
expect(await starsOf(page, 13)).toBeGreaterThan(0)
await logoutViaProfile(page)
await loginViaUi(page, user, 'pass1234')
await page.goto('/map')
for (let levelId = 1; levelId <= 13; levelId += 1) {
expect(await starsOf(page, levelId)).toBeGreaterThan(0)
}
expect(errors.filter((error) => !/favicon|Download the React DevTools/i.test(error))).toEqual([])
})
+152
View File
@@ -0,0 +1,152 @@
import { test, expect } from '@playwright/test'
import { fetchMe, registerViaUi, uniqueUsername } from './helpers/auth'
const lessons: Record<number, Array<Record<string, unknown>>> = {
1: [
{ questionId: 'money-q1', answerId: 'option-a' },
{ questionId: 'money-q2', answerId: 'true' },
{ questionId: 'money-q3', answerId: 'option-a' },
{ questionId: 'money-q4', answerId: 'true' },
{ questionId: 'money-q5', answerId: 'option-b' },
],
2: [
{ questionId: 'saving-q1', answerId: 'option-b' },
{ questionId: 'saving-q2', answerId: 'true' },
{ questionId: 'saving-q3', answerId: 'option-a' },
{ questionId: 'saving-q4', answerId: 'true' },
{ questionId: 'saving-q5', answerId: 'option-a' },
],
3: [
{ questionId: 'needs-q1', answerId: 'option-a' },
{ questionId: 'needs-q2', answerId: 'true' },
{ questionId: 'needs-q3', orderedItemIds: ['water', 'food', 'medicine', 'shoes', 'toy', 'crown'] },
{ questionId: 'needs-q4', answerId: 'option-a' },
{ questionId: 'needs-q5', answerId: 'true' },
],
4: [
{ questionId: 'gold-q1', answerId: 'option-a' },
{ questionId: 'gold-q2', answerId: 'true' },
{ questionId: 'gold-q3', answerId: 'option-a' },
{ questionId: 'gold-q4', answerId: 'false' },
{ questionId: 'gold-q5', answerId: 'option-b' },
],
5: [
{ questionId: 'market-q1', answerId: 'option-a' },
{ questionId: 'market-q2', answerId: 'false' },
{ questionId: 'market-q3', answerId: 'option-a' },
{ questionId: 'market-q4', answerId: 'true' },
{ questionId: 'market-q5', answerId: 'option-b' },
],
6: [
{ questionId: 'inflation-q1', answerId: 'option-b' },
{ questionId: 'inflation-q2', answerId: 'true' },
{ questionId: 'inflation-q3', answerId: 'option-a' },
{ questionId: 'inflation-q4', answerId: 'false' },
{ questionId: 'inflation-q5', answerId: 'option-b' },
],
}
async function answerPortfolio(page: import('@playwright/test').Page) {
const choices = [
'نتیجه بیشتر به همان گزینه وابسته می‌شود',
'خیر؛ افت همگانی می‌تواند چند گزینه را هم‌زمان کم کند',
'خیر؛ رفتار و وابستگی آن‌ها مهم است',
'خیر؛ یک سناریو آینده را تضمین نمی‌کند',
'توجه به هدف، زمان نیاز به پول و ریسک؛ در مسائل واقعی کمک از بزرگسال قابل اعتماد',
]
for (let index = 0; index < choices.length; index += 1) {
await page.getByText(choices[index]!, { exact: true }).click()
if (index < choices.length - 1) await page.getByRole('button', { name: 'بعدی' }).click()
else await page.getByRole('button', { name: 'مرور پاسخ‌ها' }).click()
}
}
test('portfolio game from the map', async ({ page }) => {
const user = uniqueUsername('port')
await registerViaUi(page, user, 'pass1234', 'سبد')
const token = await page.evaluate(() => localStorage.getItem('gold-land-auth-token'))
const headers = { authorization: `Bearer ${token}` }
for (let levelId = 1; levelId <= 6; levelId += 1) {
expect((await page.request.post(`/api/gold-land/levels/${levelId}/start`, { headers })).ok()).toBeTruthy()
expect((await page.request.post(`/api/gold-land/levels/${levelId}/complete`, {
headers,
data: { answers: lessons[levelId] },
})).ok()).toBeTruthy()
}
expect((await page.request.post('/api/gold-land/levels/7/start', { headers })).ok()).toBeTruthy()
expect((await page.request.post('/api/gold-land/levels/7/complete', {
headers,
data: {
answers: [
{ questionId: 'initial', allocations: { transport: 20, food: 25, savings: 20, fun: 0 } },
{ questionId: 'final', allocations: { transport: 30, food: 25, savings: 20, fun: 0 } },
],
},
})).ok()).toBeTruthy()
expect((await page.request.post('/api/gold-land/levels/8/start', { headers })).ok()).toBeTruthy()
expect((await page.request.post('/api/gold-land/levels/8/complete', {
headers,
data: {
answers: [
{ questionId: 'q-loss', answerId: 'b' },
{ questionId: 'q-certain', answerId: 'no' },
{ questionId: 'q-need', answerId: 'keep' },
{ questionId: 'q-compare', answerId: 'range' },
{ questionId: 'q-promise', answerId: 'check' },
],
},
})).ok()).toBeTruthy()
await page.goto('/map')
await page.getByRole('button', { name: 'مرحله 9: سبد سرمایه‌گذاری' }).click()
await page.getByRole('button', { name: 'شروع بازی' }).click()
await page.getByRole('button', { name: 'ساخت سبد' }).click()
await expect(page.getByRole('button', { name: 'دیدن سه سناریو' })).toBeDisabled()
const sunUp = page.getByRole('button', { name: 'افزایش آفتاب' })
const rainUp = page.getByRole('button', { name: 'افزایش باران' })
const windUp = page.getByRole('button', { name: 'افزایش باد' })
for (let index = 0; index < 4; index += 1) await sunUp.click()
for (let index = 0; index < 3; index += 1) await rainUp.click()
for (let index = 0; index < 3; index += 1) await windUp.click()
await page.getByRole('button', { name: 'دیدن سه سناریو' }).click()
await expect(page.getByText('سود 5 سکه')).toBeVisible()
await expect(page.getByText('زیان 5 سکه')).toBeVisible()
await expect(page.getByText('زیان 10 سکه').first()).toBeVisible()
await page.getByRole('button', { name: 'تغییر ترکیب' }).click()
await page.getByRole('button', { name: 'کاهش آفتاب' }).click()
await page.getByRole('button', { name: 'افزایش باد' }).click()
await page.getByRole('button', { name: 'دیدن سه سناریو' }).click()
await page.getByRole('button', { name: 'رفتن به سؤال‌ها' }).click()
await answerPortfolio(page)
const before = await fetchMe(page)
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await expect(page.getByText('پاداش:')).toBeVisible()
await expect(page.getByText('زیان را حذف یا سود را تضمین نمی‌کند')).toBeVisible()
await page.getByRole('button', { name: 'بازگشت به نقشه' }).click()
await page.reload()
const after = await fetchMe(page)
expect(after.progress.mapLevels.find((level) => level.id === 9)?.stars).toBe(3)
expect(after.user.coins).toBe(before.user.coins + 20 + 9)
await page.getByRole('button', { name: 'مرحله 9: سبد سرمایه‌گذاری' }).click()
await page.getByRole('button', { name: 'مرور مجدد' }).click()
await page.getByRole('button', { name: 'ساخت سبد' }).click()
for (let index = 0; index < 10; index += 1) await page.getByRole('button', { name: 'افزایش آفتاب' }).click()
await page.getByRole('button', { name: 'دیدن سه سناریو' }).click()
await page.getByRole('button', { name: 'رفتن به سؤال‌ها' }).click()
await answerPortfolio(page)
const replayBefore = await fetchMe(page)
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await expect(page.getByText('پاداش تازه‌ای اضافه نشد')).toBeVisible()
const replayAfter = await fetchMe(page)
expect(replayAfter.user.coins).toBe(replayBefore.user.coins)
await page.goto('/map')
await expect(page.getByRole('button', { name: 'مرحله 11: خرید هوشمند' })).toBeDisabled()
})
+123
View File
@@ -0,0 +1,123 @@
import { test, expect } from '@playwright/test'
import { fetchMe, registerViaUi, uniqueUsername } from './helpers/auth'
const lessons: Record<number, Array<Record<string, unknown>>> = {
1: [
{ questionId: 'money-q1', answerId: 'option-a' },
{ questionId: 'money-q2', answerId: 'true' },
{ questionId: 'money-q3', answerId: 'option-a' },
{ questionId: 'money-q4', answerId: 'true' },
{ questionId: 'money-q5', answerId: 'option-b' },
],
2: [
{ questionId: 'saving-q1', answerId: 'option-b' },
{ questionId: 'saving-q2', answerId: 'true' },
{ questionId: 'saving-q3', answerId: 'option-a' },
{ questionId: 'saving-q4', answerId: 'true' },
{ questionId: 'saving-q5', answerId: 'option-a' },
],
3: [
{ questionId: 'needs-q1', answerId: 'option-a' },
{ questionId: 'needs-q2', answerId: 'true' },
{ questionId: 'needs-q3', orderedItemIds: ['water', 'food', 'medicine', 'shoes', 'toy', 'crown'] },
{ questionId: 'needs-q4', answerId: 'option-a' },
{ questionId: 'needs-q5', answerId: 'true' },
],
4: [
{ questionId: 'gold-q1', answerId: 'option-a' },
{ questionId: 'gold-q2', answerId: 'true' },
{ questionId: 'gold-q3', answerId: 'option-a' },
{ questionId: 'gold-q4', answerId: 'false' },
{ questionId: 'gold-q5', answerId: 'option-b' },
],
5: [
{ questionId: 'market-q1', answerId: 'option-a' },
{ questionId: 'market-q2', answerId: 'false' },
{ questionId: 'market-q3', answerId: 'option-a' },
{ questionId: 'market-q4', answerId: 'true' },
{ questionId: 'market-q5', answerId: 'option-b' },
],
6: [
{ questionId: 'inflation-q1', answerId: 'option-b' },
{ questionId: 'inflation-q2', answerId: 'true' },
{ questionId: 'inflation-q3', answerId: 'option-a' },
{ questionId: 'inflation-q4', answerId: 'false' },
{ questionId: 'inflation-q5', answerId: 'option-b' },
],
}
async function answerChallenge(page: import('@playwright/test').Page) {
const choices = [
'پیشنهاد ب',
'خیر، ۳۶ فقط یکی از نتیجه‌های ممکن است',
'این پول ضروری را وارد پیشنهادهای نامطمئن نکن',
'پیشنهاد ب امکان نتیجه بالاتر و زیان بیشتری دارد؛ سود بیشتر قطعی نیست',
'وعده را تضمین معتبر فرض نکن، بررسی کن و از بزرگسال قابل اعتماد کمک بگیر',
]
for (let index = 0; index < choices.length; index += 1) {
await page.getByText(choices[index]!, { exact: true }).click()
if (index < choices.length - 1) {
await page.getByRole('button', { name: 'بعدی' }).click()
} else {
await page.getByRole('button', { name: 'مرور پاسخ‌ها' }).click()
}
}
}
test('risk and return challenge from the map', async ({ page }) => {
const user = uniqueUsername('risk')
await registerViaUi(page, user, 'pass1234', 'ریسک')
const token = await page.evaluate(() => localStorage.getItem('gold-land-auth-token'))
const headers = { authorization: `Bearer ${token}` }
for (let levelId = 1; levelId <= 6; levelId += 1) {
expect((await page.request.post(`/api/gold-land/levels/${levelId}/start`, { headers })).ok()).toBeTruthy()
expect((await page.request.post(`/api/gold-land/levels/${levelId}/complete`, {
headers,
data: { answers: lessons[levelId] },
})).ok()).toBeTruthy()
}
const budget = {
answers: [
{ questionId: 'initial', allocations: { transport: 20, food: 25, savings: 20, fun: 0 } },
{ questionId: 'final', allocations: { transport: 30, food: 25, savings: 20, fun: 0 } },
],
}
expect((await page.request.post('/api/gold-land/levels/7/start', { headers })).ok()).toBeTruthy()
expect((await page.request.post('/api/gold-land/levels/7/complete', { headers, data: budget })).ok()).toBeTruthy()
await page.goto('/map')
await page.getByRole('button', { name: 'مرحله 8: ریسک و بازده' }).click()
await page.getByRole('button', { name: 'شروع چالش' }).click()
await page.getByRole('button', { name: 'دیدن دو پیشنهاد' }).click()
await expect(page.getByText('کارت 1 از 4: 8 سکه')).toBeVisible()
await expect(page.getByText('کارت 4 از 4: 36 سکه')).toBeVisible()
await expect(page.getByText('کارت 3 از 4: 22 سکه')).toBeVisible()
await page.getByRole('button', { name: 'رفتن به فعالیت‌ها' }).click()
await answerChallenge(page)
const before = await fetchMe(page)
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await expect(page.getByText('پاداش:')).toBeVisible()
await expect(page.getByText('امکان سود بیشتر، تضمین سود نیست')).toBeVisible()
await page.getByRole('button', { name: 'بازگشت به نقشه' }).click()
await page.reload()
const after = await fetchMe(page)
expect(after.progress.mapLevels.find((level) => level.id === 8)?.stars).toBe(3)
expect(after.user.coins).toBe(before.user.coins + 20 + 9)
await page.getByRole('button', { name: 'مرحله 8: ریسک و بازده' }).click()
await page.getByRole('button', { name: 'مرور مجدد' }).click()
await page.getByRole('button', { name: 'دیدن دو پیشنهاد' }).click()
await page.getByRole('button', { name: 'رفتن به فعالیت‌ها' }).click()
await answerChallenge(page)
const replayBefore = await fetchMe(page)
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await expect(page.getByText('پاداش تازه‌ای اضافه نشد')).toBeVisible()
const replayAfter = await fetchMe(page)
expect(replayAfter.user.coins).toBe(replayBefore.user.coins)
await page.goto('/map')
await expect(page.getByRole('button', { name: 'مرحله 10: سود مرکب' })).toBeDisabled()
})
+135
View File
@@ -0,0 +1,135 @@
import { test, expect } from '@playwright/test'
import { fetchMe, registerViaUi, uniqueUsername } from './helpers/auth'
async function completePrior(page: import('@playwright/test').Page, headers: { authorization: string }) {
const lessons: Record<number, Array<Record<string, unknown>>> = {
1: [
{ questionId: 'money-q1', answerId: 'option-a' },
{ questionId: 'money-q2', answerId: 'true' },
{ questionId: 'money-q3', answerId: 'option-a' },
{ questionId: 'money-q4', answerId: 'true' },
{ questionId: 'money-q5', answerId: 'option-b' },
],
2: [
{ questionId: 'saving-q1', answerId: 'option-b' },
{ questionId: 'saving-q2', answerId: 'true' },
{ questionId: 'saving-q3', answerId: 'option-a' },
{ questionId: 'saving-q4', answerId: 'true' },
{ questionId: 'saving-q5', answerId: 'option-a' },
],
3: [
{ questionId: 'needs-q1', answerId: 'option-a' },
{ questionId: 'needs-q2', answerId: 'true' },
{ questionId: 'needs-q3', orderedItemIds: ['water', 'food', 'medicine', 'shoes', 'toy', 'crown'] },
{ questionId: 'needs-q4', answerId: 'option-a' },
{ questionId: 'needs-q5', answerId: 'true' },
],
4: [
{ questionId: 'gold-q1', answerId: 'option-a' },
{ questionId: 'gold-q2', answerId: 'true' },
{ questionId: 'gold-q3', answerId: 'option-a' },
{ questionId: 'gold-q4', answerId: 'false' },
{ questionId: 'gold-q5', answerId: 'option-b' },
],
5: [
{ questionId: 'market-q1', answerId: 'option-a' },
{ questionId: 'market-q2', answerId: 'false' },
{ questionId: 'market-q3', answerId: 'option-a' },
{ questionId: 'market-q4', answerId: 'true' },
{ questionId: 'market-q5', answerId: 'option-b' },
],
6: [
{ questionId: 'inflation-q1', answerId: 'option-b' },
{ questionId: 'inflation-q2', answerId: 'true' },
{ questionId: 'inflation-q3', answerId: 'option-a' },
{ questionId: 'inflation-q4', answerId: 'false' },
{ questionId: 'inflation-q5', answerId: 'option-b' },
],
}
for (let levelId = 1; levelId <= 6; levelId += 1) {
expect((await page.request.post(`/api/gold-land/levels/${levelId}/start`, { headers })).ok()).toBeTruthy()
expect((await page.request.post(`/api/gold-land/levels/${levelId}/complete`, { headers, data: { answers: lessons[levelId] } })).ok()).toBeTruthy()
}
const posts: Array<[string, unknown]> = [
['7', { answers: [
{ questionId: 'initial', allocations: { transport: 20, food: 25, savings: 20, fun: 0 } },
{ questionId: 'final', allocations: { transport: 30, food: 25, savings: 20, fun: 0 } },
] }],
['8', { answers: [
{ questionId: 'q-loss', answerId: 'b' },
{ questionId: 'q-certain', answerId: 'no' },
{ questionId: 'q-need', answerId: 'keep' },
{ questionId: 'q-compare', answerId: 'range' },
{ questionId: 'q-promise', answerId: 'check' },
] }],
['9', { answers: [
{ questionId: 'portfolio', holdings: { sun: 40, rain: 30, wind: 30 } },
{ questionId: 'q-one', answerId: 'depend' },
{ questionId: 'q-loss', answerId: 'no' },
{ questionId: 'q-name', answerId: 'no' },
{ questionId: 'q-best', answerId: 'no' },
{ questionId: 'q-approach', answerId: 'balanced' },
] }],
['10', { answers: [
{ questionId: 'compound-q1', answerId: 'on-balance' },
{ questionId: 'compound-q2', answerId: '121' },
{ questionId: 'compound-q3', answerId: 'from-110' },
{ questionId: 'compound-q4', answerId: 'compound' },
{ questionId: 'compound-q5', answerId: 'false' },
] }],
]
for (const [levelId, data] of posts) {
expect((await page.request.post(`/api/gold-land/levels/${levelId}/start`, { headers })).ok()).toBeTruthy()
expect((await page.request.post(`/api/gold-land/levels/${levelId}/complete`, { headers, data })).ok()).toBeTruthy()
}
}
async function answerShop(page: import('@playwright/test').Page) {
const picks = [
['دفتر ۶۰برگ، ۴۰ سکه، تحویل ۵ سکه', 'نیاز را برآورده می‌کند و هزینه نهایی‌اش بین گزینه‌های مناسب کمتر است'],
['مداد با قیمت نهایی ۲۸، بدون تخفیف', 'برای کالای یکسان، مبلغ نهایی پرداختی مهم است؛ درصد تخفیف به‌تنهایی کافی نیست'],
['فعلاً نخر و اندازه و هزینه نهایی را بپرس، با کمک بزرگسال قابل اعتماد', 'اطلاعات لازم برای تصمیم و مقایسه هنوز کامل نیست'],
]
for (let index = 0; index < picks.length; index += 1) {
if (index === 2) await expect(page.getByText('نامشخص').first()).toBeVisible()
await page.getByText(picks[index]![0]!, { exact: true }).click()
await page.getByText(picks[index]![1]!, { exact: true }).click()
await page.getByRole('button', { name: index === picks.length - 1 ? 'مرور انتخاب‌ها' : 'مأموریت بعد' }).click()
}
}
test('smart shop from the map', async ({ page }) => {
const user = uniqueUsername('shop')
await registerViaUi(page, user, 'pass1234', 'خرید')
const token = await page.evaluate(() => localStorage.getItem('gold-land-auth-token'))
await completePrior(page, { authorization: `Bearer ${token}` })
await page.goto('/map')
await page.getByRole('button', { name: 'مرحله 11: خرید هوشمند' }).click()
await page.getByRole('button', { name: 'شروع بازی' }).click()
await page.getByRole('button', { name: 'شروع خریدها' }).click()
await expect(page.getByText('بودجه:')).toBeVisible()
await expect(page.getByText('جمع نهایی: ۴۵ سکه')).toBeVisible()
await answerShop(page)
const before = await fetchMe(page)
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await expect(page.getByText('پاداش:')).toBeVisible()
await page.getByRole('button', { name: 'بازگشت به نقشه' }).click()
await page.reload()
const after = await fetchMe(page)
expect(after.progress.mapLevels.find((level) => level.id === 11)?.stars).toBe(3)
expect(after.user.coins).toBe(before.user.coins + 18 + 9)
await page.getByRole('button', { name: 'مرحله 11: خرید هوشمند' }).click()
await page.getByRole('button', { name: 'مرور مجدد' }).click()
await page.getByRole('button', { name: 'شروع خریدها' }).click()
await answerShop(page)
const replayBefore = await fetchMe(page)
await page.getByRole('button', { name: 'ثبت نتیجه' }).click()
await expect(page.getByText('پاداش تازه‌ای اضافه نشد')).toBeVisible()
const replayAfter = await fetchMe(page)
expect(replayAfter.user.coins).toBe(replayBefore.user.coins)
await page.goto('/map')
await expect(page.getByRole('button', { name: 'مرحله 13: آزمون قلعه' })).toBeDisabled()
})