Add end-to-end tests for parent item management
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m31s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m31s
- Implement tests for creating, editing, and deleting chores, kindness acts, and penalties. - Add tests to verify conversion of default items to user items and restoration of system defaults upon deletion. - Ensure proper cancellation of creation and editing actions. - Create a comprehensive plan document outlining the test scenarios and expected behaviors.
This commit is contained in:
75
frontend/vue-app/e2e/mode_parent/tasks/chore-cancel.spec.ts
Normal file
75
frontend/vue-app/e2e/mode_parent/tasks/chore-cancel.spec.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
// spec: frontend/vue-app/e2e/plans/parent-item-management.plan.md
|
||||
// seed: e2e/seed.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Chore creation/edit cancellation', () => {
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name === 'chromium-no-pin', 'Requires parent-authenticated mode')
|
||||
})
|
||||
|
||||
test('Cancel chore creation', async ({ page }) => {
|
||||
// 1. From /parent/tasks/chores, start creating a chore
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.getByRole('button', { name: 'Create Chore' }).click()
|
||||
// expect: Create Chore form appears
|
||||
await expect(page.locator('text=Chore Name')).toBeVisible()
|
||||
|
||||
// 2. Fill in 'Test' name and '5' points then cancel
|
||||
await page.evaluate(() => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', 'Test')
|
||||
setVal('#points', '5')
|
||||
})
|
||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||
// expect: User is returned to the chores list (exact match to avoid custom copy)
|
||||
await expect(page.getByText('Clean your mess', { exact: true })).toBeVisible()
|
||||
// expect: No chore named 'Test' exists
|
||||
await expect(page.locator('text=Test')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('Cancel chore edit', async ({ page }) => {
|
||||
// 1. Locate a chore and open its edit form
|
||||
await page.goto('/parent/tasks/chores')
|
||||
// ensure item exists
|
||||
if (!(await page.getByText('Wash dishes', { exact: true }).count())) {
|
||||
await page.getByRole('button', { name: 'Create Chore' }).click()
|
||||
await page.evaluate(() => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', 'Wash dishes')
|
||||
setVal('#points', '10')
|
||||
})
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
}
|
||||
await page.getByText('Wash dishes', { exact: true }).click()
|
||||
|
||||
// 2. Modify the name then cancel
|
||||
await page.evaluate(() => {
|
||||
const el = document.querySelector('#name')
|
||||
if (el) {
|
||||
el.value = 'Should not save'
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
})
|
||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||
// expect: Navigation returns to list (exact match)
|
||||
await expect(page.getByText('Clean your mess', { exact: true })).toBeVisible()
|
||||
// expect: Original values remain unchanged
|
||||
await expect(page.getByText('Wash dishes', { exact: true })).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
// spec: frontend/vue-app/e2e/plans/parent-item-management.plan.md
|
||||
// seed: e2e/seed.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('Convert a default chore to a user item by editing', async ({ page }) => {
|
||||
// 1. Locate a default chore such as 'Clean your mess' in the chores list
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await expect(page.locator('text=Clean your mess')).toBeVisible()
|
||||
// expect: Item is visible and has no delete button
|
||||
await expect(
|
||||
page.locator('text=Clean your mess >> .. >> button[aria-label="Delete item"]'),
|
||||
).toHaveCount(0)
|
||||
|
||||
// 2. Click the default chore itself to edit
|
||||
await page.click('text=Clean your mess')
|
||||
// expect: Edit form opens with the default values
|
||||
await expect(page.locator('input#name').inputValue()).resolves.toBe('Clean your mess')
|
||||
|
||||
// 3. Change some properties (name and points) so Save becomes enabled
|
||||
await page.evaluate(() => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', 'Clean your mess (custom)')
|
||||
setVal('#points', '20')
|
||||
})
|
||||
// expect: Save button is enabled because form is dirty
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
|
||||
|
||||
// 4. Save the update
|
||||
await page.click('button:has-text("Save")')
|
||||
// expect: The chore now shows as editable and includes a delete option
|
||||
await expect(
|
||||
page.locator('text=Clean your mess >> .. >> button[aria-label="Delete item"]'),
|
||||
).toBeVisible()
|
||||
// expect: Item behaves like a custom chore
|
||||
})
|
||||
141
frontend/vue-app/e2e/mode_parent/tasks/chore-create-edit.spec.ts
Normal file
141
frontend/vue-app/e2e/mode_parent/tasks/chore-create-edit.spec.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
// spec: frontend/vue-app/e2e/plans/parent-item-management.plan.md
|
||||
// seed: e2e/seed.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Chore management', () => {
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name === 'chromium-no-pin', 'Requires parent-authenticated mode')
|
||||
})
|
||||
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
test('Create a new chore (parent mode)', async ({ page }) => {
|
||||
const suffix = Date.now()
|
||||
const name = `Wash dishes ${suffix}`
|
||||
// 1. Navigate to /parent/tasks/chores
|
||||
await page.goto('/parent/tasks/chores')
|
||||
// expect: The parent dashboard loads and shows the chores list
|
||||
await expect(page.getByText('Clean your mess', { exact: true })).toBeVisible()
|
||||
|
||||
// 2. Click the 'Create Chore' FAB
|
||||
await page.getByRole('button', { name: 'Create Chore' }).click()
|
||||
// expect: The Create Chore form is displayed with fields for name and points
|
||||
await expect(page.locator('text=Chore Name')).toBeVisible()
|
||||
|
||||
// 3. Enter the chore name and points using DOM events
|
||||
await page.evaluate((name) => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', name)
|
||||
setVal('#points', '10')
|
||||
}, name)
|
||||
// expect: Submit/Create button becomes enabled
|
||||
await expect(page.getByRole('button', { name: 'Create' })).toBeEnabled()
|
||||
|
||||
// 5. Click the Create button
|
||||
await page.click('button:has-text("Create")')
|
||||
// expect: No validation errors are shown
|
||||
await expect(page.locator('.error')).toHaveCount(0)
|
||||
// expect: Navigation returns to /parent/chores (or dashboard)
|
||||
await expect(page).toHaveURL(/\/parent/)
|
||||
// expect: The new chore appears in the chores list
|
||||
await expect(page.locator(`text=${name}`)).toBeVisible()
|
||||
})
|
||||
|
||||
test('Edit an existing chore', async ({ page }) => {
|
||||
const suffix = Date.now()
|
||||
const original = `Wash dishes ${suffix}`
|
||||
const updated = `Wash car ${suffix}`
|
||||
// 1. Ensure there is at least one chore in the list (create one if necessary)
|
||||
await page.goto('/parent/tasks/chores')
|
||||
if (!(await page.locator(`text=${original}`).count())) {
|
||||
await page.getByRole('button', { name: 'Create Chore' }).click()
|
||||
await page.evaluate((name) => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', name)
|
||||
setVal('#points', '10')
|
||||
}, original)
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
}
|
||||
// expect: The chore appears in the list
|
||||
await expect(page.locator(`text=${original}`)).toBeVisible()
|
||||
|
||||
// 2. Click the chore row to edit
|
||||
await page.click(`text=${original}`)
|
||||
// expect: Edit Chore form appears (loaded with current values)
|
||||
await expect(page.locator('text=Chore Name')).toBeVisible()
|
||||
|
||||
// 3. Change the Name to 'Wash car' and Points to '15' via DOM events
|
||||
await page.evaluate((name) => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', name)
|
||||
setVal('#points', '15')
|
||||
}, updated)
|
||||
// expect: Fields are updated with the new values
|
||||
|
||||
// 4. Click Save/Update
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
|
||||
await page.click('button:has-text("Save")')
|
||||
// expect: No errors are displayed
|
||||
await expect(page.locator('.error')).toHaveCount(0)
|
||||
// expect: Navigation returns to chores list
|
||||
await expect(page).toHaveURL(/\/parent/)
|
||||
// expect: The chore now reads updated name with 15 points
|
||||
await expect(page.locator(`text=${updated}`)).toBeVisible()
|
||||
})
|
||||
|
||||
test('Delete a chore', async ({ page }) => {
|
||||
const suffix = Date.now()
|
||||
const name = `Wash car ${suffix}`
|
||||
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.getByRole('button', { name: 'Create Chore' }).click()
|
||||
await page.evaluate((name) => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', name)
|
||||
setVal('#points', '15')
|
||||
}, name)
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page.locator(`text=${name}`)).toBeVisible()
|
||||
|
||||
// delete using row-local button then confirm via modal
|
||||
await page
|
||||
.locator(`text=${name}`)
|
||||
.first()
|
||||
.locator('..')
|
||||
.getByRole('button', { name: 'Delete' })
|
||||
.click()
|
||||
// modal appears with a warning message
|
||||
await expect(page.locator('text=Are you sure you want to delete')).toBeVisible()
|
||||
// click the red danger button inside the modal (labelled Delete)
|
||||
await page.locator('button.btn-danger:has-text("Delete")').click()
|
||||
await expect(page.locator(`text=${name}`)).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
// spec: frontend/vue-app/e2e/plans/parent-item-management.plan.md
|
||||
// seed: e2e/seed.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('Edit default chore "Take out trash" and verify system chore restoration on delete', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/parent/tasks/chores')
|
||||
|
||||
// Cleanup: if a previous run left a modified 'Take out trash' (with delete icon), remove it first
|
||||
while (
|
||||
(await page
|
||||
.getByText('Take out trash', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]')
|
||||
.count()) > 0
|
||||
) {
|
||||
await page
|
||||
.getByText('Take out trash', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]')
|
||||
.first()
|
||||
.click()
|
||||
await page.getByRole('button', { name: 'Delete', exact: true }).click()
|
||||
}
|
||||
|
||||
// 1. Verify 'Take out trash' is the system default (visible, no delete icon)
|
||||
await expect(page.getByText('Take out trash', { exact: true })).toBeVisible()
|
||||
await expect(
|
||||
page
|
||||
.getByText('Take out trash', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]'),
|
||||
).toHaveCount(0)
|
||||
|
||||
// 2. Click 'Take out trash' to open the edit form and change points
|
||||
await page.getByText('Take out trash', { exact: true }).click()
|
||||
await expect(page.locator('input#name').inputValue()).resolves.toBe('Take out trash')
|
||||
|
||||
await page.evaluate(() => {
|
||||
const setVal = (sel: string, val: string) => {
|
||||
const el = document.querySelector(sel) as HTMLInputElement | null
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#points', '5')
|
||||
})
|
||||
|
||||
// expect: Save button becomes enabled because the form is dirty
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
|
||||
// 3. Verify exactly one 'Take out trash' is in the list and it now has a delete icon
|
||||
await expect(page.getByText('Take out trash', { exact: true })).toHaveCount(1)
|
||||
await expect(
|
||||
page
|
||||
.getByText('Take out trash', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]'),
|
||||
).toBeVisible()
|
||||
// expect: points display reflects the updated value
|
||||
await expect(
|
||||
page.getByText('Take out trash', { exact: true }).locator('..').locator('.value'),
|
||||
).toHaveText('5 pts')
|
||||
|
||||
// 4. Delete the modified 'Take out trash' and verify the system default is restored
|
||||
await page
|
||||
.getByText('Take out trash', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]')
|
||||
.click()
|
||||
await expect(page.locator('text=Are you sure')).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Delete', exact: true }).click()
|
||||
|
||||
// expect: 'Take out trash' is still on the list (system default restored)
|
||||
await expect(page.getByText('Take out trash', { exact: true })).toBeVisible()
|
||||
// expect: no delete icon (it's a system default again)
|
||||
await expect(
|
||||
page
|
||||
.getByText('Take out trash', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]'),
|
||||
).toHaveCount(0)
|
||||
// expect: original points (20 pts) are restored
|
||||
await expect(
|
||||
page.getByText('Take out trash', { exact: true }).locator('..').locator('.value'),
|
||||
).toHaveText('20 pts')
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
// spec: frontend/vue-app/e2e/plans/parent-item-management.plan.md
|
||||
// seed: e2e/seed.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Kindness act cancellation', () => {
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name === 'chromium-no-pin', 'Requires parent-authenticated mode')
|
||||
})
|
||||
|
||||
test('Cancel kindness act creation', async ({ page }) => {
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.click('text=Kindness Acts')
|
||||
await page.getByRole('button', { name: 'Create Kindness Act' }).click()
|
||||
await page.evaluate(() => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', 'Test')
|
||||
setVal('#points', '5')
|
||||
})
|
||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||
await expect(page.locator('text=Should not save')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('Cancel kindness act edit', async ({ page }) => {
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.click('text=Kindness Acts')
|
||||
if (!(await page.getByText('Share toys', { exact: true }).count())) {
|
||||
await page.getByRole('button', { name: 'Create Kindness Act' }).click()
|
||||
await page.evaluate(() => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', 'Share toys')
|
||||
setVal('#points', '5')
|
||||
})
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
}
|
||||
// open edit by clicking row
|
||||
await page.getByText('Share toys', { exact: true }).click()
|
||||
await page.evaluate(() => {
|
||||
const el = document.querySelector('#name')
|
||||
if (el) {
|
||||
el.value = 'Never saved'
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
})
|
||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||
await expect(page.getByText('Share toys', { exact: true })).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
// spec: frontend/vue-app/e2e/plans/parent-item-management.plan.md
|
||||
// seed: e2e/seed.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('Convert a default kindness act to a user item by editing', async ({ page }) => {
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.click('text=Kindness Acts')
|
||||
// find a default act
|
||||
await expect(page.locator('text=Be good for the day')).toBeVisible()
|
||||
await expect(
|
||||
page.locator('text=Be good for the day >> .. >> button[aria-label="Delete item"]'),
|
||||
).toHaveCount(0)
|
||||
|
||||
// edit it — rename to avoid name collision with kindness-delete-default running in parallel
|
||||
await page.click('text=Be good for the day')
|
||||
await page.locator('#name').fill('Be good today (edited)')
|
||||
await page.locator('#points').fill('7')
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
// renamed item should now be deletable
|
||||
await expect(
|
||||
page
|
||||
.getByText('Be good today (edited)', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]'),
|
||||
).toBeVisible()
|
||||
// clean up: delete the created user item so other tests see a clean default state
|
||||
await page
|
||||
.getByText('Be good today (edited)', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]')
|
||||
.click()
|
||||
await page.getByRole('button', { name: 'Delete', exact: true }).click()
|
||||
})
|
||||
@@ -0,0 +1,136 @@
|
||||
// spec: frontend/vue-app/e2e/plans/parent-item-management.plan.md
|
||||
// seed: e2e/seed.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Kindness act management', () => {
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name === 'chromium-no-pin', 'Requires parent-authenticated mode')
|
||||
})
|
||||
|
||||
// avoid parallel execution within this file; shared backend state and SSE events
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
test('Create a new kindness act (parent mode)', async ({ page }) => {
|
||||
// use a unique name so repeated runs don't collide
|
||||
const suffix = Date.now()
|
||||
const name = `Share toys ${suffix}`
|
||||
|
||||
// 1. navigate once and switch to kindness tab
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.click('text=Kindness Acts')
|
||||
await expect(page.locator('text=Kindness Acts')).toBeVisible()
|
||||
|
||||
// 2. open form
|
||||
await page.getByRole('button', { name: 'Create Kindness Act' }).click()
|
||||
await expect(page.locator('text=Name')).toBeVisible()
|
||||
|
||||
// 3‑4. fill using evaluate helper
|
||||
await page.evaluate((name) => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', name)
|
||||
setVal('#points', '5')
|
||||
}, name)
|
||||
await expect(page.getByRole('button', { name: 'Create' })).toBeEnabled()
|
||||
|
||||
// 5. submit and assert the new row is visible
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page.locator('.error')).toHaveCount(0)
|
||||
await expect(page.locator(`text=${name}`).first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('Edit an existing kindness act', async ({ page }) => {
|
||||
const suffix = Date.now()
|
||||
const original = `Share toys ${suffix}`
|
||||
const updated = `Help with homework ${suffix}`
|
||||
|
||||
// navigate and create fresh item
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.click('text=Kindness Acts')
|
||||
await page.getByRole('button', { name: 'Create Kindness Act' }).click()
|
||||
await page.evaluate((name) => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', name)
|
||||
setVal('#points', '5')
|
||||
}, original)
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page.locator(`text=${original}`).first()).toBeVisible()
|
||||
|
||||
// 2. open edit by clicking first matching row
|
||||
await page.locator(`text=${original}`).first().click()
|
||||
// wait for edit form to appear
|
||||
await expect(page.locator('text=Name')).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible()
|
||||
|
||||
// 3. update values once form is ready
|
||||
await page.evaluate((name) => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', name)
|
||||
setVal('#points', '8')
|
||||
}, updated)
|
||||
|
||||
// 4. save and verify (wait until button is enabled)
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
await expect(page.locator(`text=${updated}`).first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('Delete a kindness act', async ({ page }) => {
|
||||
const suffix = Date.now()
|
||||
const name = `Help with homework ${suffix}`
|
||||
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.click('text=Kindness Acts')
|
||||
// create fresh item
|
||||
await page.getByRole('button', { name: 'Create Kindness Act' }).click()
|
||||
await page.evaluate((name) => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', name)
|
||||
setVal('#points', '8')
|
||||
}, name)
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page.locator(`text=${name}`).first()).toBeVisible()
|
||||
|
||||
// click the delete button on the first matching row
|
||||
await page
|
||||
.locator(`text=${name}`)
|
||||
.first()
|
||||
.locator('..')
|
||||
.getByRole('button', { name: 'Delete' })
|
||||
.click()
|
||||
// confirmation modal should appear
|
||||
await expect(
|
||||
page.locator('text=Are you sure you want to delete this kindness act?'),
|
||||
).toBeVisible()
|
||||
// click the red danger button inside the modal
|
||||
await page.locator('button.btn-danger:has-text("Delete")').click()
|
||||
await expect(page.locator(`text=${name}`)).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
// spec: frontend/vue-app/e2e/plans/parent-item-management.plan.md
|
||||
// seed: e2e/seed.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('Edit default kindness act "Be good for the day" and verify system act restoration on delete', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.getByText('Kindness Acts').click()
|
||||
|
||||
// Cleanup: if a previous run left a modified 'Be good for the day' (with delete icon), remove it first
|
||||
while (
|
||||
(await page
|
||||
.getByText('Be good for the day', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]')
|
||||
.count()) > 0
|
||||
) {
|
||||
await page
|
||||
.getByText('Be good for the day', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]')
|
||||
.first()
|
||||
.click()
|
||||
await page.getByRole('button', { name: 'Delete', exact: true }).click()
|
||||
}
|
||||
|
||||
// 1. Verify 'Be good for the day' is the system default (visible, no delete icon)
|
||||
await expect(page.getByText('Be good for the day', { exact: true })).toBeVisible()
|
||||
await expect(
|
||||
page
|
||||
.getByText('Be good for the day', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]'),
|
||||
).toHaveCount(0)
|
||||
|
||||
// 2. Click 'Be good for the day' to open the edit form and change points
|
||||
await page.getByText('Be good for the day', { exact: true }).click()
|
||||
await expect(page.locator('input#name').inputValue()).resolves.toBe('Be good for the day')
|
||||
|
||||
await page.evaluate(() => {
|
||||
const setVal = (sel: string, val: string) => {
|
||||
const el = document.querySelector(sel) as HTMLInputElement | null
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#points', '3')
|
||||
})
|
||||
|
||||
// expect: Save button becomes enabled because the form is dirty
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
|
||||
// 3. Verify exactly one 'Be good for the day' is in the list and it now has a delete icon
|
||||
await expect(page.getByText('Be good for the day', { exact: true })).toHaveCount(1)
|
||||
await expect(
|
||||
page
|
||||
.getByText('Be good for the day', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]'),
|
||||
).toBeVisible()
|
||||
// expect: points display reflects the updated value
|
||||
await expect(
|
||||
page.getByText('Be good for the day', { exact: true }).locator('..').locator('.value'),
|
||||
).toHaveText('3 pts')
|
||||
|
||||
// 4. Delete the modified 'Be good for the day' and verify the system default is restored
|
||||
await page
|
||||
.getByText('Be good for the day', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]')
|
||||
.click()
|
||||
await expect(page.locator('text=Are you sure')).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Delete', exact: true }).click()
|
||||
|
||||
// expect: 'Be good for the day' is still on the list (system default restored)
|
||||
await expect(page.getByText('Be good for the day', { exact: true })).toBeVisible()
|
||||
// expect: no delete icon (it's a system default again)
|
||||
await expect(
|
||||
page
|
||||
.getByText('Be good for the day', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]'),
|
||||
).toHaveCount(0)
|
||||
// expect: original points (15 pts) are restored
|
||||
await expect(
|
||||
page.getByText('Be good for the day', { exact: true }).locator('..').locator('.value'),
|
||||
).toHaveText('15 pts')
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
// spec: frontend/vue-app/e2e/plans/parent-item-management.plan.md
|
||||
// seed: e2e/seed.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Penalty creation/edit cancellation', () => {
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name === 'chromium-no-pin', 'Requires parent-authenticated mode')
|
||||
})
|
||||
|
||||
test('Cancel penalty creation', async ({ page }) => {
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.click('text=Penalties')
|
||||
await page.getByRole('button', { name: 'Create Penalty' }).click()
|
||||
await page.evaluate(() => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', 'Should not save')
|
||||
setVal('#points', '10')
|
||||
})
|
||||
// cancel before submitting the form
|
||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||
await expect(page.locator('text=Should not save')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('Cancel penalty edit', async ({ page }) => {
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.click('text=Penalties')
|
||||
if (!(await page.getByText('No screen time', { exact: true }).count())) {
|
||||
await page.getByRole('button', { name: 'Create Penalty' }).click()
|
||||
await page.locator('#name').fill('No screen time')
|
||||
await page.locator('#points').fill('10')
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page.getByText('No screen time', { exact: true })).toBeVisible()
|
||||
}
|
||||
// open edit by clicking row
|
||||
await page.getByText('No screen time', { exact: true }).click()
|
||||
await page.evaluate(() => {
|
||||
const el = document.querySelector('#name')
|
||||
if (el) {
|
||||
el.value = 'Never saved'
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
})
|
||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||
await expect(page.getByText('No screen time', { exact: true })).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
// spec: frontend/vue-app/e2e/plans/parent-item-management.plan.md
|
||||
// seed: e2e/seed.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('Convert a default penalty to a user item by editing', async ({ page }) => {
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.click('text=Penalties')
|
||||
// locate default penalty
|
||||
await expect(page.locator('text=Fighting')).toBeVisible()
|
||||
await expect(page.locator('text=Fighting >> .. >> button[aria-label="Delete item"]')).toHaveCount(
|
||||
0,
|
||||
)
|
||||
// edit it (click the item itself)
|
||||
await page.getByText('Fighting', { exact: true }).click()
|
||||
await page.locator('#name').fill('Fighting (custom)')
|
||||
await page.locator('#points').fill('15')
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
// now should have delete option
|
||||
await expect(
|
||||
page.locator('text=Fighting >> .. >> button[aria-label="Delete item"]'),
|
||||
).toBeVisible()
|
||||
})
|
||||
@@ -0,0 +1,114 @@
|
||||
// spec: frontend/vue-app/e2e/plans/parent-item-management.plan.md
|
||||
// seed: e2e/seed.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Penalty management', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name === 'chromium-no-pin', 'Requires parent-authenticated mode')
|
||||
})
|
||||
|
||||
test('Create a new penalty (parent mode)', async ({ page }) => {
|
||||
const name = `No screen time ${Date.now()}`
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.click('text=Penalties')
|
||||
await expect(page.locator('text=Penalties')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Create Penalty' }).click()
|
||||
await page.evaluate((n) => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', n)
|
||||
setVal('#points', '30')
|
||||
}, name)
|
||||
await expect(page.getByRole('button', { name: 'Create' })).toBeEnabled()
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page.locator(`text=${name}`)).toBeVisible()
|
||||
})
|
||||
|
||||
test('Edit an existing penalty', async ({ page }) => {
|
||||
const suffix = Date.now()
|
||||
const original = `No screen time ${suffix}`
|
||||
const updated = `No dessert ${suffix}`
|
||||
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.click('text=Penalties')
|
||||
|
||||
// create the item to edit
|
||||
await page.getByRole('button', { name: 'Create Penalty' }).click()
|
||||
await page.evaluate((n) => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', n)
|
||||
setVal('#points', '30')
|
||||
}, original)
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page.locator(`text=${original}`)).toBeVisible()
|
||||
|
||||
// open edit by clicking the row itself
|
||||
await page.click(`text=${original}`)
|
||||
// wait for the edit form to finish loading data from the API
|
||||
await expect(page.locator('#name')).toHaveValue(original)
|
||||
|
||||
await page.evaluate((n) => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', n)
|
||||
setVal('#points', '20')
|
||||
}, updated)
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
await expect(page.locator(`text=${updated}`)).toBeVisible()
|
||||
})
|
||||
|
||||
test('Delete a penalty', async ({ page }) => {
|
||||
const name = `No dessert ${Date.now()}`
|
||||
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.click('text=Penalties')
|
||||
|
||||
await page.getByRole('button', { name: 'Create Penalty' }).click()
|
||||
await page.evaluate((n) => {
|
||||
const setVal = (sel, val) => {
|
||||
const el = document.querySelector(sel)
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#name', n)
|
||||
setVal('#points', '20')
|
||||
}, name)
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(page.locator(`text=${name}`)).toBeVisible()
|
||||
|
||||
await page
|
||||
.locator(`text=${name}`)
|
||||
.locator('..')
|
||||
.getByRole('button', { name: 'Delete item' })
|
||||
.click()
|
||||
await page.locator('button.btn-danger:has-text("Delete")').click()
|
||||
await expect(page.locator(`text=${name}`)).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
// spec: frontend/vue-app/e2e/plans/parent-item-management.plan.md
|
||||
// seed: e2e/seed.spec.ts
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('Edit default penalty "Fighting" and verify system penalty restoration on delete', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/parent/tasks/chores')
|
||||
await page.getByText('Penalties').click()
|
||||
|
||||
// Cleanup: if a previous run left a modified 'Fighting' (with delete icon), remove it first
|
||||
while (
|
||||
(await page
|
||||
.getByText('Fighting', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]')
|
||||
.count()) > 0
|
||||
) {
|
||||
await page
|
||||
.getByText('Fighting', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]')
|
||||
.first()
|
||||
.click()
|
||||
await page.getByRole('button', { name: 'Delete', exact: true }).click()
|
||||
}
|
||||
|
||||
// 1. Verify 'Fighting' is the system default (visible, no delete icon)
|
||||
await expect(page.getByText('Fighting', { exact: true })).toBeVisible()
|
||||
await expect(
|
||||
page
|
||||
.getByText('Fighting', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]'),
|
||||
).toHaveCount(0)
|
||||
|
||||
// 2. Click 'Fighting' to open the edit form and change points
|
||||
await page.getByText('Fighting', { exact: true }).click()
|
||||
await expect(page.locator('input#name').inputValue()).resolves.toBe('Fighting')
|
||||
|
||||
await page.evaluate(() => {
|
||||
const setVal = (sel: string, val: string) => {
|
||||
const el = document.querySelector(sel) as HTMLInputElement | null
|
||||
if (el) {
|
||||
el.value = val
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
setVal('#points', '3')
|
||||
})
|
||||
|
||||
// expect: Save button becomes enabled because the form is dirty
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
|
||||
// 3. Verify exactly one 'Fighting' is in the list and it now has a delete icon
|
||||
await expect(page.getByText('Fighting', { exact: true })).toHaveCount(1)
|
||||
await expect(
|
||||
page
|
||||
.getByText('Fighting', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]'),
|
||||
).toBeVisible()
|
||||
// expect: points display reflects the updated value
|
||||
await expect(
|
||||
page.getByText('Fighting', { exact: true }).locator('..').locator('.value'),
|
||||
).toHaveText('3 pts')
|
||||
|
||||
// 4. Delete the modified 'Fighting' and verify the system default is restored
|
||||
await page
|
||||
.getByText('Fighting', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]')
|
||||
.click()
|
||||
await expect(page.locator('text=Are you sure')).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Delete', exact: true }).click()
|
||||
|
||||
// expect: 'Fighting' is still on the list (system default restored)
|
||||
await expect(page.getByText('Fighting', { exact: true })).toBeVisible()
|
||||
// expect: no delete icon (it's a system default again)
|
||||
await expect(
|
||||
page
|
||||
.getByText('Fighting', { exact: true })
|
||||
.locator('..')
|
||||
.locator('button[aria-label="Delete item"]'),
|
||||
).toHaveCount(0)
|
||||
// expect: original points (10 pts) are restored
|
||||
await expect(
|
||||
page.getByText('Fighting', { exact: true }).locator('..').locator('.value'),
|
||||
).toHaveText('10 pts')
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('Simple chore creation test', async ({ page }) => {
|
||||
// Navigate to chores
|
||||
await page.goto('/parent/tasks/chores')
|
||||
|
||||
// Click the Create Chore FAB
|
||||
await page.getByRole('button', { name: 'Create Chore' }).click()
|
||||
|
||||
// Wait for form to load
|
||||
await expect(page.locator('text=Chore Name')).toBeVisible()
|
||||
|
||||
// Fill form using custom evaluation
|
||||
await page.evaluate(() => {
|
||||
// Fill name
|
||||
const nameInput = document.querySelector('input[type="text"], input:not([type])')
|
||||
if (nameInput) {
|
||||
nameInput.value = 'Simple Chore Test'
|
||||
nameInput.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
nameInput.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
|
||||
// Fill points
|
||||
const pointsInput = document.querySelector('input[type="number"]')
|
||||
if (pointsInput) {
|
||||
pointsInput.value = '5'
|
||||
pointsInput.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
pointsInput.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
|
||||
// Click first image to select it
|
||||
const firstImage = document.querySelector('img[alt*="Image"]')
|
||||
if (firstImage) {
|
||||
firstImage.click()
|
||||
}
|
||||
})
|
||||
|
||||
// Wait a moment for validation to trigger
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// Click Create button
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
|
||||
// Verify we're back on the list page and item was created
|
||||
// locate the name element, then move up to the row container
|
||||
const choreName = page.locator('text=Simple Chore Test').first()
|
||||
const choreRow = choreName.locator('..')
|
||||
await expect(choreRow).toBeVisible()
|
||||
// the row container should display the correct points value
|
||||
await expect(choreRow.locator('text=5 pts')).toBeVisible()
|
||||
})
|
||||
Reference in New Issue
Block a user