feat: update reward version to 1.0.13 and modify dialog button labels for clarity
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m31s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m31s
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
# file: config/version.py
|
# file: config/version.py
|
||||||
import os
|
import os
|
||||||
|
|
||||||
BASE_VERSION = "1.0.12" # update manually when releasing features
|
BASE_VERSION = "1.0.13" # update manually when releasing features
|
||||||
|
|
||||||
def get_full_version() -> str:
|
def get_full_version() -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "FZULMCV2SqB1TurS_72i4vCGjYXmL4rQzTvtKFODS-c",
|
"value": "mY6Ehfjz0l0gIgI1-zrA_HMfYQiNmgnNy6g7v2GoT3M",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1784993494.828813,
|
"expires": 1785468152.319767,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiJiMTkyZDgzZS1mMTQyLTRhZjQtYjAyNC02YzdiNzFmNzFlYjYiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzcyMTgzOTR9.7JsdzTZDZd8qErp-GiAaYfU92jCb1L5ROGZXpEaz-dc",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiI0YmI3ZTQ3ZS0wNGYxLTQyMDctYjZjYy0yNDM3NDVlOGQ0ZDMiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3Nzc3MDI5NTJ9.614NMFQB7JcIJ4k4cqKyxlpcyHmt2Hn4PbjCbvLMJ2A",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1777218394.827404,
|
"expires": 1777702952.31857,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
@@ -27,13 +27,13 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1777217494503}"
|
"value": "{\"type\":\"logout\",\"at\":1777692151952}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1777390295394}"
|
"value": "{\"expiresAt\":1777864952730}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -2,20 +2,20 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "tTh7qrpLh9RjQNVxmjhfQi0VOQwbLa4PJ8xfjlMC9P8",
|
"value": "fmbL31vJQXFd7NDUWy7eGlRZZoGh0BZipf8CqFOb8zw",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1784993494.703215,
|
"expires": 1785468152.214735,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1kZWxldGVAdGVzdC5jb20iLCJ1c2VyX2lkIjoiMGY4YzE2YzUtN2ZlNy00ZGZiLWE4ZmMtMTc3NjQ5MDkxOWI4IiwidG9rZW5fdmVyc2lvbiI6MCwiZXhwIjoxNzc3MjE4Mzk0fQ.zSLSZsWZb9N1En9mWCqXxM9d__U7JldwT8pCKaoxzUc",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1kZWxldGVAdGVzdC5jb20iLCJ1c2VyX2lkIjoiNzc2YjBkYjctYzYyNy00ODBiLTkzZDYtMTRlMTE4MDQ3NTE5IiwidG9rZW5fdmVyc2lvbiI6MCwiZXhwIjoxNzc3NzAyOTUyfQ.I4sfKTo0nsJgKvwDRSztxtyZpWw-oN1y3L4yCtr_CRo",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1777218394.702339,
|
"expires": 1777702952.21406,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
@@ -27,13 +27,13 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1777217494445}"
|
"value": "{\"type\":\"logout\",\"at\":1777692151844}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1777390295182}"
|
"value": "{\"expiresAt\":1777864952556}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -2,20 +2,20 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "iBgMbKeAn8p8X9iS6eumIZHEqKN0yqMKZ09V6nk7s-U",
|
"value": "YitpHdwm093Mavj6iTYmJOLGLOIKoozGkmYXgmJMTBM",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1784993490.715252,
|
"expires": 1785468147.453462,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiJhM2ZlZjRmNS01MTc4LTRlYWEtOWU0NC1jZjk2MGJiMGQ3ZTkiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3NzcyMTgzOTB9.PtdzVvtN6rMWPOTS6OfGc8tVSrihktnT2GhVIwKollc",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiI4ZGNmOWU5Ni1lM2I3LTRkNzYtOWQ1NS01NmE4MjU5ZWQ5NzUiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3Nzc3MDI5NDd9.zSetTnajvus3N5uJDBqxRYXfLksoU9ZmkWzqCS0GUvc",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1777218390.714722,
|
"expires": 1777702947.452943,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
@@ -27,13 +27,13 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1777217490520}"
|
"value": "{\"type\":\"logout\",\"at\":1777692147220}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1777390290893}"
|
"value": "{\"expiresAt\":1777864947663}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -108,8 +108,8 @@ test.describe('Reward Notification — In-App Denial', () => {
|
|||||||
await expect(card).toHaveClass(/item-ready/, { timeout: 3000 })
|
await expect(card).toHaveClass(/item-ready/, { timeout: 3000 })
|
||||||
await card.click()
|
await card.click()
|
||||||
|
|
||||||
// Look for a Deny button in the dialog
|
// Look for the reject action in the dialog
|
||||||
const denyBtn = page.getByRole('button', { name: /deny/i })
|
const denyBtn = page.getByRole('button', { name: 'Reject', exact: true })
|
||||||
await expect(denyBtn).toBeVisible({ timeout: 5000 })
|
await expect(denyBtn).toBeVisible({ timeout: 5000 })
|
||||||
await denyBtn.click()
|
await denyBtn.click()
|
||||||
await expect(denyBtn).not.toBeVisible({ timeout: 5000 })
|
await expect(denyBtn).not.toBeVisible({ timeout: 5000 })
|
||||||
@@ -127,7 +127,7 @@ test.describe('Reward Notification — In-App Denial', () => {
|
|||||||
await card.click()
|
await card.click()
|
||||||
await expect(card).toHaveClass(/item-ready/, { timeout: 3000 })
|
await expect(card).toHaveClass(/item-ready/, { timeout: 3000 })
|
||||||
await card.click()
|
await card.click()
|
||||||
const denyBtn = page.getByRole('button', { name: /deny/i })
|
const denyBtn = page.getByRole('button', { name: 'Reject', exact: true })
|
||||||
await expect(denyBtn).toBeVisible({ timeout: 5000 })
|
await expect(denyBtn).toBeVisible({ timeout: 5000 })
|
||||||
await denyBtn.click()
|
await denyBtn.click()
|
||||||
await expect(denyBtn).not.toBeVisible({ timeout: 5000 })
|
await expect(denyBtn).not.toBeVisible({ timeout: 5000 })
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ async function createReward(
|
|||||||
for (const r of (await pre.json()).rewards ?? []) {
|
for (const r of (await pre.json()).rewards ?? []) {
|
||||||
if (r.name === name) await request.delete(`/api/reward/${r.id}`)
|
if (r.name === name) await request.delete(`/api/reward/${r.id}`)
|
||||||
}
|
}
|
||||||
await request.put('/api/reward/add', { data: { name, description: 'E2E pending-warn test', cost } })
|
await request.put('/api/reward/add', {
|
||||||
|
data: { name, description: 'E2E pending-warn test', cost },
|
||||||
|
})
|
||||||
const list = await request.get('/api/reward/list')
|
const list = await request.get('/api/reward/list')
|
||||||
return (await list.json()).rewards?.find((r: any) => r.name === name)?.id ?? ''
|
return (await list.json()).rewards?.find((r: any) => r.name === name)?.id ?? ''
|
||||||
}
|
}
|
||||||
@@ -160,9 +162,7 @@ test.describe('Pending reward warning dialog', () => {
|
|||||||
|
|
||||||
test('Pending reward card shows PENDING banner', async ({ page }) => {
|
test('Pending reward card shows PENDING banner', async ({ page }) => {
|
||||||
await page.goto(`/parent/${childId}`)
|
await page.goto(`/parent/${childId}`)
|
||||||
const card = rewardSection(page)
|
const card = rewardSection(page).locator('.item-card').filter({ hasText: PENDING_REWARD_NAME })
|
||||||
.locator('.item-card')
|
|
||||||
.filter({ hasText: PENDING_REWARD_NAME })
|
|
||||||
await card.waitFor({ state: 'visible' })
|
await card.waitFor({ state: 'visible' })
|
||||||
await expect(card.locator('.pending')).toHaveText('PENDING')
|
await expect(card.locator('.pending')).toHaveText('PENDING')
|
||||||
})
|
})
|
||||||
@@ -177,12 +177,12 @@ test.describe('Pending reward warning dialog', () => {
|
|||||||
|
|
||||||
await activateItem(card)
|
await activateItem(card)
|
||||||
|
|
||||||
// Pending reward warning dialog appears (has a "No" button)
|
// Pending reward warning dialog appears
|
||||||
await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'No', exact: true }).click()
|
await page.getByRole('button', { name: 'Reject', exact: true }).click()
|
||||||
|
|
||||||
// Dialog dismissed; task confirmation dialog must NOT have appeared
|
// Dialog dismissed; task confirmation dialog must NOT have appeared
|
||||||
await expect(page.getByRole('button', { name: 'No', exact: true })).not.toBeVisible()
|
await expect(page.getByRole('button', { name: 'Reject', exact: true })).not.toBeVisible()
|
||||||
await expect(page.getByRole('button', { name: 'Cancel' })).not.toBeVisible()
|
await expect(page.getByRole('button', { name: 'Cancel' })).not.toBeVisible()
|
||||||
// Points unchanged
|
// Points unchanged
|
||||||
await expect(page.locator('.points .value')).toHaveText(String(pointsBefore))
|
await expect(page.locator('.points .value')).toHaveText(String(pointsBefore))
|
||||||
@@ -204,8 +204,8 @@ test.describe('Pending reward warning dialog', () => {
|
|||||||
await activateItem(card)
|
await activateItem(card)
|
||||||
|
|
||||||
// Pending reward warning dialog
|
// Pending reward warning dialog
|
||||||
await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'Yes' }).click()
|
await page.getByRole('button', { name: 'Approve', exact: true }).click()
|
||||||
|
|
||||||
// After confirming, the task confirmation dialog appears (has "Cancel" button)
|
// After confirming, the task confirmation dialog appears (has "Cancel" button)
|
||||||
await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible({ timeout: 3000 })
|
await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible({ timeout: 3000 })
|
||||||
@@ -219,7 +219,9 @@ test.describe('Pending reward warning dialog', () => {
|
|||||||
|
|
||||||
// ── kindness ──────────────────────────────────────────────────────────────
|
// ── kindness ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
test('Kindness activation — Cancel on pending dialog leaves state unchanged', async ({ page }) => {
|
test('Kindness activation — Cancel on pending dialog leaves state unchanged', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
await page.goto(`/parent/${childId}`)
|
await page.goto(`/parent/${childId}`)
|
||||||
const card = kindnessSection(page).locator('.item-card').filter({ hasText: KIND_NAME })
|
const card = kindnessSection(page).locator('.item-card').filter({ hasText: KIND_NAME })
|
||||||
await card.waitFor({ state: 'visible' })
|
await card.waitFor({ state: 'visible' })
|
||||||
@@ -227,10 +229,10 @@ test.describe('Pending reward warning dialog', () => {
|
|||||||
|
|
||||||
await activateItem(card)
|
await activateItem(card)
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'No', exact: true }).click()
|
await page.getByRole('button', { name: 'Reject', exact: true }).click()
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'No', exact: true })).not.toBeVisible()
|
await expect(page.getByRole('button', { name: 'Reject', exact: true })).not.toBeVisible()
|
||||||
await expect(page.getByRole('button', { name: 'Cancel' })).not.toBeVisible()
|
await expect(page.getByRole('button', { name: 'Cancel' })).not.toBeVisible()
|
||||||
await expect(page.locator('.points .value')).toHaveText(String(pointsBefore))
|
await expect(page.locator('.points .value')).toHaveText(String(pointsBefore))
|
||||||
})
|
})
|
||||||
@@ -245,8 +247,8 @@ test.describe('Pending reward warning dialog', () => {
|
|||||||
|
|
||||||
await activateItem(card)
|
await activateItem(card)
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'Yes' }).click()
|
await page.getByRole('button', { name: 'Approve', exact: true }).click()
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible({ timeout: 3000 })
|
await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible({ timeout: 3000 })
|
||||||
await page.getByRole('button', { name: 'Yes' }).click()
|
await page.getByRole('button', { name: 'Yes' }).click()
|
||||||
@@ -266,10 +268,10 @@ test.describe('Pending reward warning dialog', () => {
|
|||||||
|
|
||||||
await activateItem(card)
|
await activateItem(card)
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'No', exact: true }).click()
|
await page.getByRole('button', { name: 'Reject', exact: true }).click()
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'No', exact: true })).not.toBeVisible()
|
await expect(page.getByRole('button', { name: 'Reject', exact: true })).not.toBeVisible()
|
||||||
await expect(page.getByRole('button', { name: 'Cancel' })).not.toBeVisible()
|
await expect(page.getByRole('button', { name: 'Cancel' })).not.toBeVisible()
|
||||||
await expect(page.locator('.points .value')).toHaveText(String(pointsBefore))
|
await expect(page.locator('.points .value')).toHaveText(String(pointsBefore))
|
||||||
})
|
})
|
||||||
@@ -284,8 +286,8 @@ test.describe('Pending reward warning dialog', () => {
|
|||||||
|
|
||||||
await activateItem(card)
|
await activateItem(card)
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'Yes' }).click()
|
await page.getByRole('button', { name: 'Approve', exact: true }).click()
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible({ timeout: 3000 })
|
await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible({ timeout: 3000 })
|
||||||
await page.getByRole('button', { name: 'Yes' }).click()
|
await page.getByRole('button', { name: 'Yes' }).click()
|
||||||
@@ -320,10 +322,10 @@ test.describe('Pending reward warning dialog', () => {
|
|||||||
await activateItem(otherCard)
|
await activateItem(otherCard)
|
||||||
|
|
||||||
// Pending reward warning dialog appears when another reward has a pending request
|
// Pending reward warning dialog appears when another reward has a pending request
|
||||||
await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'No', exact: true }).click()
|
await page.getByRole('button', { name: 'Reject', exact: true }).click()
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'No', exact: true })).not.toBeVisible()
|
await expect(page.getByRole('button', { name: 'Reject', exact: true })).not.toBeVisible()
|
||||||
await expect(page.getByRole('button', { name: 'Cancel' })).not.toBeVisible()
|
await expect(page.getByRole('button', { name: 'Cancel' })).not.toBeVisible()
|
||||||
await expect(page.locator('.points .value')).toHaveText(String(pointsBefore))
|
await expect(page.locator('.points .value')).toHaveText(String(pointsBefore))
|
||||||
// PENDING banner still visible — reward request was not cancelled
|
// PENDING banner still visible — reward request was not cancelled
|
||||||
@@ -346,11 +348,11 @@ test.describe('Pending reward warning dialog', () => {
|
|||||||
|
|
||||||
await activateItem(otherCard)
|
await activateItem(otherCard)
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'No', exact: true })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Reject', exact: true })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'Yes' }).click()
|
await page.getByRole('button', { name: 'Approve', exact: true }).click()
|
||||||
|
|
||||||
// Pending dialog closes; reward confirm does NOT auto-open (other reward must be re-clicked)
|
// Pending dialog closes; reward confirm does NOT auto-open (other reward must be re-clicked)
|
||||||
await expect(page.getByRole('button', { name: 'No', exact: true })).not.toBeVisible()
|
await expect(page.getByRole('button', { name: 'Reject', exact: true })).not.toBeVisible()
|
||||||
await expect(page.getByRole('button', { name: 'Cancel' })).not.toBeVisible()
|
await expect(page.getByRole('button', { name: 'Cancel' })).not.toBeVisible()
|
||||||
|
|
||||||
// The pending reward request was cancelled — PENDING banner should disappear
|
// The pending reward request was cancelled — PENDING banner should disappear
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ test.describe('Reward redemption', () => {
|
|||||||
|
|
||||||
await activateItem(card)
|
await activateItem(card)
|
||||||
await expect(page.getByRole('button', { name: 'Yes' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Yes' })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
await page.getByRole('button', { name: 'No', exact: true }).click()
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Yes' })).not.toBeVisible()
|
await expect(page.getByRole('button', { name: 'Yes' })).not.toBeVisible()
|
||||||
await expect(page.locator('.points .value')).toHaveText(String(before))
|
await expect(page.locator('.points .value')).toHaveText(String(before))
|
||||||
|
|||||||
@@ -187,11 +187,11 @@ test.describe('Reward edit cost', () => {
|
|||||||
await card.getByTitle('Edit custom value').click()
|
await card.getByTitle('Edit custom value').click()
|
||||||
|
|
||||||
// PendingRewardDialog should appear warning about the pending state
|
// PendingRewardDialog should appear warning about the pending state
|
||||||
await expect(page.locator('.modal-message')).toContainText('pending')
|
await expect(page.getByText(/currently pending/i)).toBeVisible()
|
||||||
|
|
||||||
// Clicking "No" closes the dialog without opening the override modal
|
// Clicking "Reject" closes the dialog without opening the override modal
|
||||||
await page.getByRole('button', { name: 'No', exact: true }).click()
|
await page.getByRole('button', { name: 'Reject', exact: true }).click()
|
||||||
await expect(page.locator('.modal-message')).not.toBeVisible()
|
await expect(page.getByText(/currently pending/i)).not.toBeVisible()
|
||||||
await expect(page.getByRole('button', { name: 'Save' })).not.toBeVisible()
|
await expect(page.getByRole('button', { name: 'Save' })).not.toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -209,10 +209,10 @@ test.describe('Reward edit cost', () => {
|
|||||||
await card.getByTitle('Edit custom value').click()
|
await card.getByTitle('Edit custom value').click()
|
||||||
|
|
||||||
// PendingRewardDialog visible
|
// PendingRewardDialog visible
|
||||||
await expect(page.locator('.modal-message')).toContainText('pending')
|
await expect(page.getByText(/currently pending/i)).toBeVisible()
|
||||||
|
|
||||||
// Confirming cancels the pending request and opens the override modal
|
// Confirming cancels the pending request and opens the override modal
|
||||||
await page.getByRole('button', { name: 'Yes' }).click()
|
await page.getByRole('button', { name: 'Approve', exact: true }).click()
|
||||||
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible({ timeout: 5000 })
|
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible({ timeout: 5000 })
|
||||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -452,6 +452,7 @@ function isChoreScheduledToday(item: ChildTask): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isChoreExpired(item: ChildTask): boolean {
|
function isChoreExpired(item: ChildTask): boolean {
|
||||||
|
if (item.pending_status === 'pending') return false
|
||||||
if (!item.schedule) return false
|
if (!item.schedule) return false
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
const due = getDueTimeToday(item.schedule, today)
|
const due = getDueTimeToday(item.schedule, today)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
import { computed, ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
import ModalDialog from '../shared/ModalDialog.vue'
|
import ModalDialog from '../shared/ModalDialog.vue'
|
||||||
import ScheduleModal from '../shared/ScheduleModal.vue'
|
import ScheduleModal from '../shared/ScheduleModal.vue'
|
||||||
import PendingRewardDialog from './PendingRewardDialog.vue'
|
import PendingRewardDialog from './PendingRewardDialog.vue'
|
||||||
@@ -95,6 +95,14 @@ const kebabBtnRefs = ref<Map<string, HTMLElement>>(new Map())
|
|||||||
const showScheduleModal = ref(false)
|
const showScheduleModal = ref(false)
|
||||||
const scheduleTarget = ref<ChildTask | null>(null)
|
const scheduleTarget = ref<ChildTask | null>(null)
|
||||||
|
|
||||||
|
const pendingDialogReward = computed<Reward | RewardStatus | null>(() => {
|
||||||
|
if (pendingEditOverrideTarget.value?.type === 'reward') {
|
||||||
|
return pendingEditOverrideTarget.value.entity as Reward
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedReward.value
|
||||||
|
})
|
||||||
|
|
||||||
// Expiry timers
|
// Expiry timers
|
||||||
const expiryTimers = ref<number[]>([])
|
const expiryTimers = ref<number[]>([])
|
||||||
|
|
||||||
@@ -449,6 +457,7 @@ function isChoreScheduledToday(item: ChildTask): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isChoreExpired(item: ChildTask): boolean {
|
function isChoreExpired(item: ChildTask): boolean {
|
||||||
|
if (item.pending_status === 'pending') return false
|
||||||
if (!item.schedule) return false
|
if (!item.schedule) return false
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
if (!isScheduledToday(item.schedule, now)) return false
|
if (!isScheduledToday(item.schedule, now)) return false
|
||||||
@@ -777,6 +786,7 @@ const triggerTask = (task: ChildTask) => {
|
|||||||
selectedTask.value = task
|
selectedTask.value = task
|
||||||
const pendingRewardIds = getPendingRewardIds()
|
const pendingRewardIds = getPendingRewardIds()
|
||||||
if (pendingRewardIds.length > 0) {
|
if (pendingRewardIds.length > 0) {
|
||||||
|
selectedReward.value = null
|
||||||
showPendingRewardDialog.value = true
|
showPendingRewardDialog.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -841,13 +851,13 @@ const confirmTriggerTask = async () => {
|
|||||||
|
|
||||||
const triggerReward = (reward: RewardStatus) => {
|
const triggerReward = (reward: RewardStatus) => {
|
||||||
if (reward.points_needed > 0) return
|
if (reward.points_needed > 0) return
|
||||||
|
selectedReward.value = reward
|
||||||
// If there is a pending reward and it's not this one, show the pending dialog
|
// If there is a pending reward and it's not this one, show the pending dialog
|
||||||
const pendingRewardIds = getPendingRewardIds()
|
const pendingRewardIds = getPendingRewardIds()
|
||||||
if (pendingRewardIds.length > 0 && !reward.redeeming) {
|
if (pendingRewardIds.length > 0 && !reward.redeeming) {
|
||||||
showPendingRewardDialog.value = true
|
showPendingRewardDialog.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selectedReward.value = reward
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showRewardConfirm.value = true
|
showRewardConfirm.value = true
|
||||||
}, 150)
|
}, 150)
|
||||||
@@ -1158,6 +1168,16 @@ function goToAssignRewards() {
|
|||||||
<!-- Pending Reward Dialog -->
|
<!-- Pending Reward Dialog -->
|
||||||
<PendingRewardDialog
|
<PendingRewardDialog
|
||||||
v-if="showPendingRewardDialog"
|
v-if="showPendingRewardDialog"
|
||||||
|
:reward-name="pendingDialogReward?.name"
|
||||||
|
:child-name="pendingDialogReward ? child?.name : undefined"
|
||||||
|
:image-url="pendingDialogReward?.image_url ?? null"
|
||||||
|
:subtitle="
|
||||||
|
pendingDialogReward
|
||||||
|
? pendingDialogReward.points_needed === 0
|
||||||
|
? 'Reward Ready!'
|
||||||
|
: pendingDialogReward.points_needed + ' more points'
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
:message="
|
:message="
|
||||||
pendingEditOverrideTarget
|
pendingEditOverrideTarget
|
||||||
? 'This reward is currently pending. Changing its cost will cancel the pending request. Would you like to proceed?'
|
? 'This reward is currently pending. Changing its cost will cancel the pending request. Would you like to proceed?'
|
||||||
@@ -1168,6 +1188,7 @@ function goToAssignRewards() {
|
|||||||
() => {
|
() => {
|
||||||
showPendingRewardDialog = false
|
showPendingRewardDialog = false
|
||||||
pendingEditOverrideTarget = null
|
pendingEditOverrideTarget = null
|
||||||
|
selectedReward = null
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<ModalDialog title="Warning!" @backdrop-click="$emit('cancel')">
|
<ModalDialog @backdrop-click="$emit('cancel')">
|
||||||
<div class="modal-message">
|
<div class="approve-dialog">
|
||||||
{{ message }}
|
<img v-if="imageUrl" :src="imageUrl" alt="Reward" class="reward-image" />
|
||||||
</div>
|
<p v-if="rewardName" class="item-label">{{ rewardName }}</p>
|
||||||
<div class="modal-actions">
|
<p v-if="subtitle" class="subtitle">{{ subtitle }}</p>
|
||||||
<button @click="$emit('confirm')" class="btn btn-primary">Yes</button>
|
<p v-if="rewardName && childName" class="message">
|
||||||
<button @click="$emit('cancel')" class="btn btn-secondary">No</button>
|
Redeem this reward for <strong>{{ childName }}</strong
|
||||||
|
>?
|
||||||
|
</p>
|
||||||
|
<p class="message">{{ message }}</p>
|
||||||
|
<div class="actions">
|
||||||
|
<button @click="$emit('confirm')" class="btn btn-primary">Approve</button>
|
||||||
|
<button @click="$emit('cancel')" class="btn btn-secondary">Reject</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -16,9 +23,17 @@ import ModalDialog from '../shared/ModalDialog.vue'
|
|||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
message?: string
|
message?: string
|
||||||
|
rewardName?: string
|
||||||
|
childName?: string
|
||||||
|
imageUrl?: string | null
|
||||||
|
subtitle?: string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
message: 'A reward is currently pending. It will be cancelled. Would you like to proceed?',
|
message: 'A reward is currently pending. It will be cancelled. Would you like to proceed?',
|
||||||
|
rewardName: undefined,
|
||||||
|
childName: undefined,
|
||||||
|
imageUrl: null,
|
||||||
|
subtitle: undefined,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,9 +44,58 @@ defineEmits<{
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.modal-message {
|
.approve-dialog {
|
||||||
margin-bottom: 1.2rem;
|
text-align: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-image {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--info-image-bg);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-label {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--dialog-child-name);
|
||||||
|
margin-bottom: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: var(--modal-message-color, #333);
|
font-weight: 600;
|
||||||
|
color: var(--dialog-child-name);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--dialog-message);
|
||||||
|
margin-bottom: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message:last-of-type {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions button {
|
||||||
|
padding: 0.7rem 1.8rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
transition: background 0.18s;
|
||||||
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<ModalDialog
|
<ModalDialog v-if="reward" @backdrop-click="$emit('cancel')">
|
||||||
v-if="reward"
|
<div class="approve-dialog">
|
||||||
:imageUrl="reward.image_url"
|
<img v-if="reward.image_url" :src="reward.image_url" alt="Reward" class="reward-image" />
|
||||||
:title="reward.name"
|
<p class="item-label">{{ reward.name }}</p>
|
||||||
:subtitle="reward.points_needed === 0 ? 'Reward Ready!' : reward.points_needed + ' more points'"
|
<p class="subtitle">
|
||||||
dialogMaxWidth="760px"
|
{{ reward.points_needed === 0 ? 'Reward Ready!' : reward.points_needed + ' more points' }}
|
||||||
@backdrop-click="$emit('cancel')"
|
</p>
|
||||||
>
|
<p class="message">
|
||||||
<div class="modal-message">
|
Redeem this reward for <strong>{{ childName }}</strong
|
||||||
Redeem this reward for <span class="child-name">{{ childName }}</span
|
>?
|
||||||
>?
|
</p>
|
||||||
</div>
|
<div class="actions">
|
||||||
<div class="modal-actions">
|
<button @click="$emit('confirm')" class="btn btn-primary">Yes</button>
|
||||||
<button @click="$emit('confirm')" class="btn btn-primary">Yes</button>
|
<button v-if="reward.redeeming" @click="$emit('deny')" class="btn btn-secondary">
|
||||||
<button v-if="reward?.redeeming" @click="$emit('deny')" class="btn btn-danger">Deny</button>
|
Reject
|
||||||
<button @click="$emit('cancel')" class="btn btn-secondary">Cancel</button>
|
</button>
|
||||||
|
<button v-else @click="$emit('cancel')" class="btn btn-secondary">No</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -36,14 +38,54 @@ defineEmits<{
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.modal-message {
|
.approve-dialog {
|
||||||
margin-bottom: 1.2rem;
|
text-align: center;
|
||||||
font-size: 1rem;
|
padding: 0.5rem;
|
||||||
color: var(--modal-message-color, #333);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.child-name {
|
.reward-image {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--info-image-bg);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-label {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--dialog-child-name);
|
||||||
|
margin-bottom: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary, #333);
|
color: var(--dialog-child-name);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--dialog-message);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions button {
|
||||||
|
padding: 0.7rem 1.8rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
transition: background 0.18s;
|
||||||
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user