Add end-to-end tests for chore and project functionalities, enhance auth flow, and update package version
This commit is contained in:
174
e2e/tests/add-task-modal.spec.js
Normal file
174
e2e/tests/add-task-modal.spec.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import { expect, test } from '../fixtures/auth.js'
|
||||
import { API_URL } from '../global-setup.js'
|
||||
|
||||
// Fetch the full chore list for the authenticated user via the API — used to
|
||||
// verify a task's persisted fields without relying on list-page badge markup.
|
||||
async function fetchChores(page) {
|
||||
const token = await page.evaluate(() => localStorage.getItem('token'))
|
||||
const apiRes = await fetch(`${API_URL}/api/v1/chores/`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(apiRes.ok).toBe(true)
|
||||
const { res: chores } = await apiRes.json()
|
||||
return chores
|
||||
}
|
||||
|
||||
test.describe('AddTaskModal (quick-add)', () => {
|
||||
// All tests in this suite run as the pre-authenticated E2E user
|
||||
test.use({
|
||||
storageState: '.auth/state.json',
|
||||
})
|
||||
|
||||
test('creates a task from plain text title only and it appears in the list', async ({
|
||||
page,
|
||||
}) => {
|
||||
const taskName = `E2E Quick Task ${Date.now()}`
|
||||
|
||||
// ── Open the quick-add modal from the chores list ───────────────────────
|
||||
await page.goto('/chores')
|
||||
await page.getByTestId('open-add-task-modal').click()
|
||||
|
||||
const dialog = page.getByRole('dialog', { name: 'Create new task' })
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
// ── Type a plain title (no parseable tokens) and submit with Enter ──────
|
||||
const input = dialog.locator('textarea')
|
||||
await input.fill(taskName)
|
||||
await input.press('Enter')
|
||||
|
||||
// ── Modal closes and the task shows up in the list ──────────────────────
|
||||
await expect(dialog).toBeHidden()
|
||||
await expect(page.getByText(taskName)).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
// ── Verify it exists via the API too ─────────────────────────────────────
|
||||
const chores = await fetchChores(page)
|
||||
const created = chores.find(c => c.name === taskName)
|
||||
expect(created).toBeDefined()
|
||||
})
|
||||
|
||||
test('smart-parses a due date and priority from typed text', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Kept lowercase: parsePriority's cleanup step lowercases the whole
|
||||
// sentence when a priority token matches, so an already-lowercase prefix
|
||||
// sidesteps that quirk and round-trips unchanged.
|
||||
const taskName = `e2e parse task ${Date.now()}`
|
||||
|
||||
await page.goto('/chores')
|
||||
await page.getByTestId('open-add-task-modal').click()
|
||||
|
||||
const dialog = page.getByRole('dialog', { name: 'Create new task' })
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
const input = dialog.locator('textarea')
|
||||
await input.fill(`${taskName} !p1 tomorrow`)
|
||||
|
||||
// ── Confirm the smart parse is reflected in the pickers before saving ───
|
||||
await expect(
|
||||
dialog.getByRole('button', { name: 'P1', exact: true }),
|
||||
).toBeVisible()
|
||||
// The due-date trigger only ever reads "Due" in its empty state, so once
|
||||
// parsing lands a date the button relabels itself away from that text.
|
||||
await expect(
|
||||
dialog.getByRole('button', { name: 'Due', exact: true }),
|
||||
).toHaveCount(0)
|
||||
|
||||
await input.press('Enter')
|
||||
|
||||
await expect(dialog).toBeHidden()
|
||||
await expect(page.getByText(taskName)).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
// ── Verify the parsed priority + due date persisted via the API ─────────
|
||||
const chores = await fetchChores(page)
|
||||
const created = chores.find(c => c.name === taskName)
|
||||
expect(created).toBeDefined()
|
||||
expect(created.priority).toBe(1)
|
||||
expect(created.nextDueDate).toBeTruthy()
|
||||
})
|
||||
|
||||
test('creates a task using the picker UI directly, skipping smart-parse tokens', async ({
|
||||
page,
|
||||
}) => {
|
||||
const taskName = `E2E Picker Task ${Date.now()}`
|
||||
|
||||
await page.goto('/chores')
|
||||
await page.getByTestId('open-add-task-modal').click()
|
||||
|
||||
const dialog = page.getByRole('dialog', { name: 'Create new task' })
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
// Plain title only — none of "due date"/"priority"/"repeat" tokens.
|
||||
await dialog.locator('textarea').fill(taskName)
|
||||
|
||||
// ── Set the due date via the picker (Due → Tomorrow → Apply) ────────────
|
||||
await dialog.getByRole('button', { name: 'Due', exact: true }).click()
|
||||
const dueDateDialog = page.getByRole('dialog', { name: 'Due Date' })
|
||||
await dueDateDialog
|
||||
.getByRole('checkbox', { name: 'Tomorrow', exact: true })
|
||||
.click()
|
||||
await dueDateDialog
|
||||
.getByRole('button', { name: 'Apply', exact: true })
|
||||
.click()
|
||||
|
||||
// ── Set the priority via the picker (Priority → P2) ──────────────────────
|
||||
await dialog.getByRole('button', { name: 'Priority', exact: true }).click()
|
||||
await page.getByRole('button', { name: 'P2', exact: true }).click()
|
||||
await expect(
|
||||
dialog.getByRole('button', { name: 'P2', exact: true }),
|
||||
).toBeVisible()
|
||||
|
||||
// ── Set the repeat schedule via the picker (Repeat → Daily → Apply) ─────
|
||||
await dialog.getByRole('button', { name: 'Repeat', exact: true }).click()
|
||||
const repeatDialog = page.getByRole('dialog', { name: 'Repeat Schedule' })
|
||||
await repeatDialog
|
||||
.getByRole('checkbox', { name: 'Daily', exact: true })
|
||||
.click()
|
||||
await repeatDialog
|
||||
.getByRole('button', { name: 'Apply', exact: true })
|
||||
.click()
|
||||
|
||||
await dialog.getByRole('button', { name: 'Create', exact: true }).click()
|
||||
|
||||
await expect(dialog).toBeHidden()
|
||||
await expect(page.getByText(taskName)).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
// ── Verify the picker-set values persisted via the API ───────────────────
|
||||
const chores = await fetchChores(page)
|
||||
const created = chores.find(c => c.name === taskName)
|
||||
expect(created).toBeDefined()
|
||||
expect(created.priority).toBe(2)
|
||||
expect(created.frequencyType).toBe('daily')
|
||||
expect(created.nextDueDate).toBeTruthy()
|
||||
})
|
||||
|
||||
test('Create button is disabled until the title is non-empty', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/chores')
|
||||
await page.getByTestId('open-add-task-modal').click()
|
||||
|
||||
const dialog = page.getByRole('dialog', { name: 'Create new task' })
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
const createButton = dialog.getByRole('button', {
|
||||
name: 'Create',
|
||||
exact: true,
|
||||
})
|
||||
const input = dialog.locator('textarea')
|
||||
|
||||
// ── Empty title ───────────────────────────────────────────────────────
|
||||
await expect(createButton).toBeDisabled()
|
||||
|
||||
// ── Whitespace-only title still counts as empty ─────────────────────────
|
||||
await input.fill(' ')
|
||||
await expect(createButton).toBeDisabled()
|
||||
|
||||
// ── Real text enables the button ─────────────────────────────────────────
|
||||
await input.fill('E2E Enable Check')
|
||||
await expect(createButton).toBeEnabled()
|
||||
|
||||
// No task should be created by this test — back out via Cancel.
|
||||
await dialog.getByRole('button', { name: 'Cancel', exact: true }).click()
|
||||
await expect(dialog).toBeHidden()
|
||||
})
|
||||
})
|
||||
@@ -31,7 +31,7 @@ test.describe('Auth – Sign Up', () => {
|
||||
await page.locator('#email').fill('short@donetick.test')
|
||||
await page.locator('#password').fill('ValidPass123!')
|
||||
await page.locator('#displayName').fill('Short User')
|
||||
await page.getByRole('button', { name: 'Sign Up' }).click()
|
||||
await page.getByRole('button', { name: 'Create account' }).click()
|
||||
|
||||
await expect(
|
||||
page.getByText('Username must be at least 4 characters'),
|
||||
@@ -52,7 +52,7 @@ test.describe('Auth – Login', () => {
|
||||
|
||||
test('shows an error for wrong password', async ({ page }) => {
|
||||
await page.goto('/login')
|
||||
await page.locator('#email').fill('e2e.user')
|
||||
await page.locator('#username').fill('e2e.user')
|
||||
await page.locator('#password').fill('WrongPassword!')
|
||||
await page.getByRole('button', { name: 'Sign In' }).click()
|
||||
|
||||
|
||||
150
e2e/tests/chore-edit.spec.js
Normal file
150
e2e/tests/chore-edit.spec.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import { expect, test } from '../fixtures/auth.js'
|
||||
import { API_URL } from '../global-setup.js'
|
||||
|
||||
test.describe('ChoreEdit', () => {
|
||||
// All tests in this suite run as the pre-authenticated E2E user
|
||||
test.use({
|
||||
storageState: '.auth/state.json',
|
||||
})
|
||||
|
||||
test('blocks save and shows an error when the name is left blank', async ({
|
||||
page,
|
||||
}) => {
|
||||
// ── Navigate to the create chore page, leave the form at defaults ──────
|
||||
await page.goto('/chores/create')
|
||||
|
||||
// ── Attempt to save without a name ──────────────────────────────────────
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
|
||||
// ── The validation error is shown and we never leave the create page ───
|
||||
await expect(page.getByText('Name is required').first()).toBeVisible({
|
||||
timeout: 5_000,
|
||||
})
|
||||
await expect(page).toHaveURL(/\/chores\/create/)
|
||||
})
|
||||
|
||||
test('days-of-the-week repeat: blocks save with no days selected, then succeeds once days are chosen', async ({
|
||||
page,
|
||||
}) => {
|
||||
const choreName = `E2E Weekly Chore ${Date.now()}`
|
||||
|
||||
// ── Navigate to the create chore page ────────────────────────────────
|
||||
await page.goto('/chores/create')
|
||||
|
||||
// ── Fill in the chore name ────────────────────────────────────────────
|
||||
await page.locator('input').first().fill(choreName)
|
||||
|
||||
// ── Enable recurrence and switch to a custom "days of the week" schedule
|
||||
await page.getByLabel('Repeat this task').click()
|
||||
await page.getByLabel('Custom').click()
|
||||
await page.getByLabel('Days of the Week').click()
|
||||
|
||||
// ── Attempt to save with zero days selected ─────────────────────────────
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await expect(
|
||||
page.getByText('Please select at least one day of the week').first(),
|
||||
).toBeVisible({ timeout: 5_000 })
|
||||
await expect(page).toHaveURL(/\/chores\/create/)
|
||||
|
||||
// ── Select Monday and Wednesday, then save successfully ────────────────
|
||||
await page.getByLabel('Monday').click()
|
||||
await page.getByLabel('Wednesday').click()
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
|
||||
await page.waitForURL('**/chores', { timeout: 15_000 })
|
||||
await expect(page.getByText(choreName)).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
// ── Verify the chore exists in the API with the selected days ─────────
|
||||
const token = await page.evaluate(() => localStorage.getItem('token'))
|
||||
const apiRes = await fetch(`${API_URL}/api/v1/chores/`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(apiRes.ok).toBe(true)
|
||||
|
||||
const { res: chores } = await apiRes.json()
|
||||
const created = chores.find(c => c.name === choreName)
|
||||
expect(created).toBeDefined()
|
||||
expect(created.frequencyType).toBe('days_of_the_week')
|
||||
expect([...(created.frequencyMetadata?.days || [])].sort()).toEqual([
|
||||
'monday',
|
||||
'wednesday',
|
||||
])
|
||||
})
|
||||
|
||||
test('creates a chore, edits it after reload, and toggles Anyone assignment', async ({
|
||||
page,
|
||||
}) => {
|
||||
const originalName = `E2E Edit Chore ${Date.now()}`
|
||||
const updatedName = `${originalName} Updated`
|
||||
const updatedDescription = `Updated by e2e ${Date.now()}`
|
||||
// One week out, formatted as YYYY-MM-DD for the native date input
|
||||
const updatedDueDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
|
||||
.toISOString()
|
||||
.slice(0, 10)
|
||||
|
||||
// ── Create a simple one-off chore with a due date ───────────────────────
|
||||
await page.goto('/chores/create')
|
||||
await page.locator('input').first().fill(originalName)
|
||||
|
||||
// Toggle "Anyone" on then off again — confirm it snaps back to the
|
||||
// default self-assignment before we submit.
|
||||
await page.getByLabel('Anyone').click()
|
||||
await expect(page.getByLabel('Anyone')).toBeChecked()
|
||||
await page.getByLabel('Anyone').click()
|
||||
await expect(page.getByLabel('Anyone')).not.toBeChecked()
|
||||
|
||||
// Give it a due date so it's a simple one-off chore
|
||||
await page.getByLabel('Give this task a due date').click()
|
||||
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await page.waitForURL('**/chores', { timeout: 15_000 })
|
||||
await expect(page.getByText(originalName)).toBeVisible({ timeout: 10_000 })
|
||||
|
||||
// ── Look up the created chore via the API (id + self-assignment state) ─
|
||||
const token = await page.evaluate(() => localStorage.getItem('token'))
|
||||
const listRes = await fetch(`${API_URL}/api/v1/chores/`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(listRes.ok).toBe(true)
|
||||
const { res: chores } = await listRes.json()
|
||||
const created = chores.find(c => c.name === originalName)
|
||||
expect(created).toBeDefined()
|
||||
// Default self-assignment: exactly one assignee, matching assignedTo.
|
||||
expect(created.assignees?.length).toBe(1)
|
||||
expect(created.assignedTo).toBe(created.assignees[0].userId)
|
||||
|
||||
// ── Edit: change name, description, due date, and switch to Anyone ─────
|
||||
await page.goto(`/chores/${created.id}/edit`)
|
||||
|
||||
await page.locator('input').first().fill(updatedName)
|
||||
await page.locator('.ql-editor').fill(updatedDescription)
|
||||
await page.locator('input[type="date"]').fill(updatedDueDate)
|
||||
await page.getByLabel('Anyone').click()
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
await page.waitForURL('**/chores', { timeout: 15_000 })
|
||||
|
||||
// ── Reload the edit page and confirm the new values persisted in the UI
|
||||
await page.goto(`/chores/${created.id}/edit`)
|
||||
await expect(page.locator('input').first()).toHaveValue(updatedName)
|
||||
await expect(page.locator('.ql-editor')).toContainText(updatedDescription)
|
||||
await expect(page.locator('input[type="date"]')).toHaveValue(updatedDueDate)
|
||||
await expect(page.getByLabel('Anyone')).toBeChecked()
|
||||
|
||||
// ── Confirm persisted via the API too ───────────────────────────────────
|
||||
const detailRes = await fetch(`${API_URL}/api/v1/chores/${created.id}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(detailRes.ok).toBe(true)
|
||||
const { res: updated } = await detailRes.json()
|
||||
expect(updated.name).toBe(updatedName)
|
||||
expect(updated.description).toContain(updatedDescription)
|
||||
// Due-date persistence is already confirmed via the reloaded date input
|
||||
// above; comparing the raw UTC `nextDueDate` string here would be
|
||||
// timezone-fragile since the app stores it as an end-of-local-day UTC
|
||||
// instant.
|
||||
// "Anyone" assignment: no specific assignees, no fixed assignedTo.
|
||||
expect(updated.assignees ?? []).toHaveLength(0)
|
||||
expect(updated.assignedTo).toBeFalsy()
|
||||
})
|
||||
})
|
||||
164
e2e/tests/projects-filters.spec.js
Normal file
164
e2e/tests/projects-filters.spec.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import { expect, test } from '../fixtures/auth.js'
|
||||
import { API_URL } from '../global-setup.js'
|
||||
|
||||
test.describe('Projects & Filters – Create and navigate', () => {
|
||||
// All tests in this suite run as the pre-authenticated E2E user
|
||||
test.use({
|
||||
storageState: '.auth/state.json',
|
||||
})
|
||||
|
||||
test('creates a project via the modal, tracks its task count, and filters chores by it', async ({
|
||||
page,
|
||||
}) => {
|
||||
const projectName = `E2E Project ${Date.now()}`
|
||||
const choreName = `E2E Project Chore ${Date.now()}`
|
||||
|
||||
// ── Create the project via the modal ─────────────────────────────────
|
||||
await page.goto('/projects')
|
||||
await page.getByTestId('open-add-project-modal').click()
|
||||
|
||||
const projectDialog = page.getByRole('dialog')
|
||||
await projectDialog.getByLabel('Project Name').fill(projectName)
|
||||
await projectDialog.getByRole('button', { name: 'Create' }).click()
|
||||
|
||||
// ── Verify the project appears in the list with a 0 task count ───────
|
||||
// Scope to the row: the name Typography's parent Box also holds the
|
||||
// "N tasks" chip as a sibling, so one level up covers both.
|
||||
const projectRow = page
|
||||
.getByText(projectName, { exact: true })
|
||||
.locator('xpath=..')
|
||||
await expect(projectRow.getByText('0 tasks')).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
// ── Look up the created project via the API to get its id ────────────
|
||||
const token = await page.evaluate(() => localStorage.getItem('token'))
|
||||
const projectsRes = await fetch(`${API_URL}/api/v1/projects`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(projectsRes.ok).toBe(true)
|
||||
const projects = await projectsRes.json()
|
||||
const project = projects.find(p => p.name === projectName)
|
||||
expect(project).toBeDefined()
|
||||
|
||||
// ── Create a chore inside the project directly via the API ───────────
|
||||
// Cheaper than driving the full chore-create UI, and this project is
|
||||
// brand new so its task count is guaranteed to go from 0 to 1.
|
||||
const choreRes = await fetch(`${API_URL}/api/v1/chores/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: choreName,
|
||||
frequencyType: 'once',
|
||||
assignStrategy: 'no_assignee',
|
||||
projectId: project.id,
|
||||
}),
|
||||
})
|
||||
expect(choreRes.ok).toBe(true)
|
||||
|
||||
// ── Reload so the project list reflects the new task count ───────────
|
||||
await page.reload()
|
||||
await expect(projectRow.getByText('1 tasks')).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
// ── Click the project card and confirm navigation + filtered results ─
|
||||
await page.getByText(projectName, { exact: true }).click()
|
||||
await page.waitForURL(new RegExp(`/chores\\?project=${project.id}(&|$)`), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
await expect(page.getByText(choreName)).toBeVisible({ timeout: 10_000 })
|
||||
})
|
||||
|
||||
test('creates a filter via AdvancedFilterBuilder, tracks its task count, and filters chores by it', async ({
|
||||
page,
|
||||
}) => {
|
||||
const filterProjectName = `E2E Filter Project ${Date.now()}`
|
||||
const filterName = `E2E Filter ${Date.now()}`
|
||||
const choreName = `E2E Filter Chore ${Date.now()}`
|
||||
|
||||
// ── Seed a throwaway project via the API to use as the filter's ──────
|
||||
// condition target. A brand-new project has zero chores, so the
|
||||
// filter's task count is guaranteed to start at 0 regardless of
|
||||
// whatever else has accumulated in the shared test database.
|
||||
await page.goto('/filters')
|
||||
const token = await page.evaluate(() => localStorage.getItem('token'))
|
||||
const createProjectRes = await fetch(`${API_URL}/api/v1/projects`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ name: filterProjectName }),
|
||||
})
|
||||
expect(createProjectRes.ok).toBe(true)
|
||||
const { res: filterProject } = await createProjectRes.json()
|
||||
|
||||
// Reload so the modal's project list (fetched on page load) includes it
|
||||
await page.reload()
|
||||
|
||||
// ── Create the filter via AdvancedFilterBuilder ───────────────────────
|
||||
await page.getByTestId('open-add-filter-modal').click()
|
||||
|
||||
const filterDialog = page.getByRole('dialog')
|
||||
await filterDialog
|
||||
.getByPlaceholder('e.g. Overdue tasks for Alice')
|
||||
.fill(filterName)
|
||||
// Condition: Projects is <the seeded project> — one condition is enough
|
||||
await filterDialog
|
||||
.getByRole('button', { name: filterProjectName, exact: true })
|
||||
.click()
|
||||
await filterDialog.getByRole('button', { name: 'Save Filter' }).click()
|
||||
|
||||
// ── Verify the filter appears in the list with a 0 task count ────────
|
||||
// The name Typography sits inside a name-row Box, itself inside the
|
||||
// content Box that also holds the "N tasks" chip — two levels up.
|
||||
const filterRow = page
|
||||
.getByText(filterName, { exact: true })
|
||||
.locator('xpath=../..')
|
||||
await expect(filterRow.getByText('0 tasks')).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
// ── Look up the created filter via the API to get its id ─────────────
|
||||
const filtersRes = await fetch(`${API_URL}/api/v1/filters`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(filtersRes.ok).toBe(true)
|
||||
const filters = await filtersRes.json()
|
||||
const filter = filters.find(f => f.name === filterName)
|
||||
expect(filter).toBeDefined()
|
||||
|
||||
// ── Create a chore matching the filter's project condition ───────────
|
||||
const choreRes = await fetch(`${API_URL}/api/v1/chores/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: choreName,
|
||||
frequencyType: 'once',
|
||||
assignStrategy: 'no_assignee',
|
||||
projectId: filterProject.id,
|
||||
}),
|
||||
})
|
||||
expect(choreRes.ok).toBe(true)
|
||||
|
||||
// ── Reload so the filter list reflects the new task count ────────────
|
||||
await page.reload()
|
||||
await expect(filterRow.getByText('1 tasks')).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
|
||||
// ── Click the filter card and confirm navigation + filtered results ──
|
||||
await page.getByText(filterName, { exact: true }).click()
|
||||
await page.waitForURL(new RegExp(`/chores\\?filterId=${filter.id}(&|$)`), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
await expect(page.getByText(choreName)).toBeVisible({ timeout: 10_000 })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user