From 43ffb3da5affbd8b1d751a57b6a06759a9d2a167 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 11 Aug 2026 00:55:17 -0400 Subject: [PATCH 1/2] Add end-to-end tests for chore and project functionalities, enhance auth flow, and update package version --- e2e/debug.mjs | 19 +++ e2e/fixtures/auth.js | 15 ++- e2e/run-e2e.sh | 5 +- e2e/tests/add-task-modal.spec.js | 174 +++++++++++++++++++++++++ e2e/tests/auth.spec.js | 4 +- e2e/tests/chore-edit.spec.js | 150 +++++++++++++++++++++ e2e/tests/projects-filters.spec.js | 164 +++++++++++++++++++++++ package-lock.json | 4 +- src/views/Authorization/AuthFields.jsx | 4 +- src/views/Filters/FilterView.jsx | 1 + src/views/Projects/ProjectView.jsx | 1 + src/views/components/AddTaskModal.jsx | 8 +- 12 files changed, 535 insertions(+), 14 deletions(-) create mode 100644 e2e/debug.mjs create mode 100644 e2e/tests/add-task-modal.spec.js create mode 100644 e2e/tests/chore-edit.spec.js create mode 100644 e2e/tests/projects-filters.spec.js diff --git a/e2e/debug.mjs b/e2e/debug.mjs new file mode 100644 index 0000000..c575e8f --- /dev/null +++ b/e2e/debug.mjs @@ -0,0 +1,19 @@ +import { chromium } from '@playwright/test' +import { readFile } from 'fs/promises' + +const state = JSON.parse(await readFile('.auth/state.json', 'utf8')) +const browser = await chromium.launch() +const context = await browser.newContext({ storageState: state }) +const page = await context.newPage() +page.on('console', msg => console.log('[console]', msg.type(), msg.text())) +page.on('pageerror', err => console.log('[pageerror]', err.message, '\n', err.stack)) + +await page.goto('http://localhost:5173/chores/create') +await page.getByTestId('chore-name-input').fill('Debug Chore ' + Date.now()) +await page.getByLabel('Repeat this task').click() +await page.getByLabel('Daily').click() +await page.getByRole('button', { name: 'Create' }).click() +await page.waitForURL('**/chores', { timeout: 15000 }) +await page.waitForTimeout(2000) + +await browser.close() diff --git a/e2e/fixtures/auth.js b/e2e/fixtures/auth.js index 13df6f6..06b1747 100644 --- a/e2e/fixtures/auth.js +++ b/e2e/fixtures/auth.js @@ -6,7 +6,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) /** * Fill and submit the signup form through the UI. - * After successful signup the app auto-logs in and redirects to /chores. + * After successful signup the app auto-logs in and walks through the + * onboarding flow (/circle-setup, then /ready) before landing on /chores. */ export async function signUpViaUI(page, { username, email, password, displayName }) { await page.goto('/signup') @@ -14,7 +15,14 @@ export async function signUpViaUI(page, { username, email, password, displayName await page.locator('#email').fill(email) await page.locator('#password').fill(password) await page.locator('#displayName').fill(displayName) - await page.getByRole('button', { name: 'Sign Up' }).click() + await page.getByRole('button', { name: 'Create account' }).click() + + await page.waitForURL('**/circle-setup', { timeout: 10_000 }) + await page.getByRole('button', { name: 'Continue' }).click() + + await page.waitForURL('**/ready', { timeout: 10_000 }) + await page.getByRole('button', { name: 'Continue' }).click() + await page.waitForURL('**/chores', { timeout: 10_000 }) } @@ -24,8 +32,7 @@ export async function signUpViaUI(page, { username, email, password, displayName */ export async function loginViaUI(page, { username, password }) { await page.goto('/login') - // The username input on the login page has id="email" (quirk of the form) - await page.locator('#email').fill(username) + await page.locator('#username').fill(username) await page.locator('#password').fill(password) await page.getByRole('button', { name: 'Sign In' }).click() await page.waitForURL('**/chores', { timeout: 10_000 }) diff --git a/e2e/run-e2e.sh b/e2e/run-e2e.sh index 9bc4c56..086146a 100755 --- a/e2e/run-e2e.sh +++ b/e2e/run-e2e.sh @@ -4,6 +4,7 @@ # (2022/5180) so it never touches dev servers you may have running elsewhere # (e.g. another worktree on the default 2021/5173). set -uo pipefail +set -m # each backgrounded job gets its own process group, so we can kill it as a unit (portable alternative to setsid, which isn't available on macOS) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" FRONTEND_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" @@ -70,7 +71,7 @@ kill_port "$FRONTEND_PORT" echo "--- Starting backend on :$BACKEND_PORT (log: $LOG_DIR/backend.log) ---" ( cd "$BACKEND_DIR" - exec setsid env \ + exec env \ DT_NAME=e2e-frontend-repo \ DT_IS_DONE_TICK_DOT_COM=false \ DT_IS_USER_CREATION_DISABLED=false \ @@ -93,7 +94,7 @@ BACKEND_PID=$! echo "--- Starting frontend on :$FRONTEND_PORT (log: $LOG_DIR/frontend.log) ---" ( cd "$FRONTEND_DIR" - exec setsid env VITE_APP_API_URL="$BACKEND_URL" npx vite --port "$FRONTEND_PORT" --strictPort + exec env VITE_APP_API_URL="$BACKEND_URL" npx vite --port "$FRONTEND_PORT" --strictPort ) > "$LOG_DIR/frontend.log" 2>&1 & FRONTEND_PID=$! diff --git a/e2e/tests/add-task-modal.spec.js b/e2e/tests/add-task-modal.spec.js new file mode 100644 index 0000000..7aefae9 --- /dev/null +++ b/e2e/tests/add-task-modal.spec.js @@ -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() + }) +}) diff --git a/e2e/tests/auth.spec.js b/e2e/tests/auth.spec.js index bf657dd..9c049a7 100644 --- a/e2e/tests/auth.spec.js +++ b/e2e/tests/auth.spec.js @@ -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() diff --git a/e2e/tests/chore-edit.spec.js b/e2e/tests/chore-edit.spec.js new file mode 100644 index 0000000..df791e4 --- /dev/null +++ b/e2e/tests/chore-edit.spec.js @@ -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() + }) +}) diff --git a/e2e/tests/projects-filters.spec.js b/e2e/tests/projects-filters.spec.js new file mode 100644 index 0000000..b9f0658 --- /dev/null +++ b/e2e/tests/projects-filters.spec.js @@ -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 — 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 }) + }) +}) diff --git a/package-lock.json b/package-lock.json index acffd35..360424e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "donetick", - "version": "1.2.38", + "version": "1.2.45", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "donetick", - "version": "1.2.38", + "version": "1.2.45", "hasInstallScript": true, "dependencies": { "@capacitor-community/in-app-review": "^8.0.0", diff --git a/src/views/Authorization/AuthFields.jsx b/src/views/Authorization/AuthFields.jsx index e63ad36..be3ca93 100644 --- a/src/views/Authorization/AuthFields.jsx +++ b/src/views/Authorization/AuthFields.jsx @@ -34,7 +34,7 @@ export const AuthField = ({ label, error, helper, children, ...formProps }) => ( ) export const AuthTextField = ({ label, error, helper, sx, ...inputProps }) => ( - + ) @@ -49,7 +49,7 @@ export const AuthPasswordField = ({ const [visible, setVisible] = useState(false) return ( - + { }} > { }} > { chore.frequencyMetadata = frequency.frequencyMetadata chore.frequency = frequency.frequency } - if (!frequency && dueDate) { - // Use RFC3339/ISO-8601 format expected by backend. + if (dueDate) { + // Use RFC3339/ISO-8601 format expected by backend. The backend only + // derives NextDueDate from what's sent on create (handler.go never + // computes it from frequencyType), so this must be sent whether or + // not the task also repeats — otherwise a recurring task created with + // a due date lands with nextDueDate: null. chore.nextDueDate = new Date(dueDate).toISOString() } if (hasReminders && (frequency || dueDate)) { From dbd1fa72c4e02fa1243e84f68f2582f39e723f1b Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 11 Aug 2026 00:55:40 -0400 Subject: [PATCH 2/2] remove e2e workflow --- .github/workflows/e2e.yml | 138 -------------------------------------- 1 file changed, 138 deletions(-) delete mode 100644 .github/workflows/e2e.yml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml deleted file mode 100644 index f73d178..0000000 --- a/.github/workflows/e2e.yml +++ /dev/null @@ -1,138 +0,0 @@ -name: E2E Tests - -on: - push: - branches: [main] - paths: - - 'src/**' - - 'e2e/**' - - '.github/workflows/e2e.yml' - pull_request: - branches: [main] - paths: - - 'src/**' - - 'e2e/**' - - '.github/workflows/e2e.yml' - workflow_dispatch: - -jobs: - e2e: - runs-on: ubuntu-latest - timeout-minutes: 20 - - steps: - - uses: actions/checkout@v4 - - # ── Node / frontend ──────────────────────────────────────────────────── - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'npm' - - - name: Install frontend dependencies - run: | - npm ci - # Platform-specific native packages required on Linux - npm install --force \ - @rollup/rollup-linux-x64-gnu@4.34.9 \ - @swc/core-linux-x64-gnu - - # ── Playwright ──────────────────────────────────────────────────────── - - name: Install Playwright dependencies - run: npm ci - working-directory: e2e - - - name: Install Playwright browsers - run: npx playwright install --with-deps chromium - working-directory: e2e - - # ── Backend binary ──────────────────────────────────────────────────── - - name: Get latest donetick release tag - id: release - run: | - TAG=$(curl -fsSL https://api.github.com/repos/donetick/donetick/releases/latest \ - | grep '"tag_name"' | head -1 \ - | sed 's/.*"tag_name": "\(.*\)".*/\1/') - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "Using backend tag: $TAG" - - - name: Cache donetick binary - id: cache-binary - uses: actions/cache@v4 - with: - path: .donetick-bin/ - key: donetick-${{ steps.release.outputs.tag }}-linux-x86_64 - - - name: Download and extract donetick binary - if: steps.cache-binary.outputs.cache-hit != 'true' - run: | - mkdir -p .donetick-bin - curl -fsSL \ - "https://github.com/donetick/donetick/releases/download/${{ steps.release.outputs.tag }}/donetick_Linux_x86_64.tar.gz" \ - -o .donetick-bin/donetick_Linux_x86_64.tar.gz - tar -xzf .donetick-bin/donetick_Linux_x86_64.tar.gz -C .donetick-bin - chmod +x .donetick-bin/donetick - - - name: Write backend config - run: | - mkdir -p .donetick-bin/config - cat > .donetick-bin/config/selfhosted.yaml << 'EOF' - name: "selfhosted" - is_done_tick_dot_com: false - is_user_creation_disabled: false - database: - type: "sqlite" - migration: true - jwt: - secret: "e2e_test_secret_32chars_long_ok!" - session_time: 168h - max_refresh: 1440h - server: - port: 2021 - read_timeout: 10s - write_timeout: 10s - rate_period: 60s - rate_limit: 300 - cors_allow_origins: - - "http://localhost:5173" - - "http://localhost:4173" - serve_frontend: false - logging: - level: "warn" - encoding: "json" - development: false - EOF - - - name: Start donetick backend - working-directory: .donetick-bin - env: - DT_ENV: selfhosted - DT_SQLITE_PATH: /tmp/donetick-e2e.db - run: ./donetick & - - # ── Frontend ────────────────────────────────────────────────────────── - # Build with --mode development so .env.development is loaded, - # which sets VITE_APP_API_URL=http://localhost:2021 - - name: Build frontend - run: npx vite build --mode development - - - name: Serve frontend - run: npx vite preview --port 5173 & - - # ── Tests ───────────────────────────────────────────────────────────── - - name: Run Playwright tests - working-directory: e2e - env: - E2E_FRONTEND_URL: http://localhost:5173 - E2E_API_URL: http://localhost:2021 - run: npx playwright test - - - name: Upload test artifacts - if: failure() - uses: actions/upload-artifact@v4 - with: - name: playwright-report - path: | - e2e/playwright-report/ - e2e/test-results/ - retention-days: 7