Files
collective-lists/apps/web/tests/e2e/tasks.test.ts
Oier Bravo Urtasun ab8ac08226 fix(tasks): match /lists + /notes create pattern
Two concrete problems the user hit:
  1) /tasks overview had no visible "create" button — only an enter-to-submit
     input hidden behind a decorative Plus icon.
  2) /tasks/[id] add-task form had no visible submit button either.

Both now mirror the /lists + /notes pattern:

  /tasks — new "New list" button in the masthead (bg-slate-900 primary
    style matching "New list" on /lists and "New note" on /notes). Click
    creates an empty task_list and navigates to /tasks/[id] where the
    user names it inline via the new big editable title input
    (autosaves after NAME_SAVE_DEBOUNCE_MS, 500 ms — same hook as
    /lists/[id] and /notes/[id]).
  /tasks/[id] — sticky add-task footer gains a filled "+" submit button
    on the right; clicking it or pressing Enter both fire handleAdd.
    Input is now wrapped in the same rounded-lg card used on /lists/[id]
    so the two screens look identical.

i18n
  tasks_new_list = "New list" / "Nueva lista" (en / es).

Tests
  tasks.test.ts helper updated to drive the new button-then-title flow
  (same shape as lists.test.ts). T-UI-01..03 all pass.

just test-all → exit 0, 228 green, 2 skipped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 02:25:14 +02:00

112 lines
4.0 KiB
TypeScript

/**
* T-series (UI) — Fase 3.1
*
* Task lists overview at /tasks; per-list detail at /tasks/[id]. Members can
* create, complete, edit, delete, and reorder tasks. Guests see them but
* cannot mutate. Completed tasks sink to the bottom and show "Completed by …".
*/
import { test, expect, type Page } from '@playwright/test';
import { USERS } from '../fixtures/users.js';
import { loginAs } from '../fixtures/login.js';
const NEW_LIST_PLACEHOLDER = /new task list|nueva lista de tareas/i;
const NEW_TASK_PLACEHOLDER = /add task|añadir tarea/i;
async function gotoTasksClean(page: Page) {
await page.goto('/tasks');
await expect(
page.getByRole('button', { name: /new list|nueva lista/i })
).toBeVisible({ timeout: 15_000 });
}
async function createTaskList(page: Page, name: string) {
// Masthead "New list" button → creates empty list and navigates to the detail.
await page.getByRole('button', { name: /new list|nueva lista/i }).click();
await expect(page).toHaveURL(/\/tasks\/[0-9a-f-]+$/, { timeout: 10_000 });
const titleInput = page.getByPlaceholder(NEW_LIST_PLACEHOLDER);
await expect(titleInput).toBeVisible({ timeout: 10_000 });
await titleInput.fill(name);
await titleInput.blur();
await page.waitForTimeout(900); // autosave debounce
}
test.describe('Tasks — member (Borja)', () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, USERS.borja);
});
test('T-UI-01: create list, add task, complete it, see "Completed by …" tooltip', async ({
page
}) => {
await gotoTasksClean(page);
const listName = `Tasks ${Date.now()}`;
await createTaskList(page, listName);
const taskInput = page.getByPlaceholder(NEW_TASK_PLACEHOLDER);
await expect(taskInput).toBeVisible({ timeout: 5_000 });
const taskTitle = `Buy milk ${Date.now()}`;
await taskInput.fill(taskTitle);
await taskInput.press('Enter');
const row = page.locator('[role="listitem"]').filter({ hasText: taskTitle });
await expect(row).toBeVisible({ timeout: 5_000 });
// Toggle complete
await row.getByRole('button', { name: /toggle task|alternar tarea/i }).click();
// Title is decorated as completed (line-through). Check via aria.
await expect(
page
.locator('[role="listitem"]')
.filter({ hasText: taskTitle })
.getByRole('button', { name: /uncheck task|desmarcar tarea/i })
).toBeVisible({ timeout: 5_000 });
// "Completed by Borja" tooltip / metadata visible somewhere on the row.
await expect(
page.locator('[role="listitem"]').filter({ hasText: taskTitle }).getByText(/borja/i)
).toBeVisible({ timeout: 5_000 });
});
test('T-UI-02: delete a task removes it from the list', async ({ page }) => {
await gotoTasksClean(page);
const listName = `Tasks-del ${Date.now()}`;
await createTaskList(page, listName);
const taskInput = page.getByPlaceholder(NEW_TASK_PLACEHOLDER);
const taskTitle = `Delete me ${Date.now()}`;
await taskInput.fill(taskTitle);
await taskInput.press('Enter');
const row = page.locator('[role="listitem"]').filter({ hasText: taskTitle });
await expect(row).toBeVisible({ timeout: 5_000 });
await row.getByRole('button', { name: /delete task|eliminar tarea/i }).click();
await expect(
page.locator('[role="listitem"]').filter({ hasText: taskTitle })
).toHaveCount(0, { timeout: 5_000 });
});
});
test.describe('Tasks — guest (David, read-only)', () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, USERS.david);
});
test('T-UI-03: guest can navigate to /tasks but cannot create lists', async ({ page }) => {
await page.goto('/tasks');
// Either the create input is hidden, or it exists but typing into it
// must NOT produce a committed list (RLS blocks the insert).
const input = page.getByPlaceholder(NEW_LIST_PLACEHOLDER);
if (await input.isVisible({ timeout: 2_000 }).catch(() => false)) {
const name = `David sneaky ${Date.now()}`;
await input.fill(name);
await input.press('Enter');
await page.waitForTimeout(1_500);
await expect(
page.getByRole('heading', { name: new RegExp(`^${name}$`) })
).not.toBeVisible({ timeout: 2_000 });
}
});
});