feat(fase-10): reset list button on detail view (10.7)

A list shown at status=completed used to have its only Reset action
buried in the 3-dot menu, forcing the user back to /lists to start
over. Now the title row of /lists/[id] renders a prominent
"Reset list" pill button when status === 'completed' — reusing the
existing handleReset() handler, no new logic. The 3-dot menu entry
stays for the active and archived views.

RST-01 seeds a completed list with a checked item, clicks the new
button, verifies status flips back to active + the item gets
unchecked.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 02:26:41 +02:00
parent 10415bfca8
commit 2924b94329
2 changed files with 88 additions and 0 deletions

View File

@@ -690,6 +690,22 @@
>
{m.list_status_archived()}
</span>
{:else if list?.status === 'completed'}
<!-- Fase 10.7: prominent reset CTA on the completed-list view.
The 3-dot menu already exposes the same action, but landing
on a list you just finished and not seeing a way to start
over without bouncing back to /lists is the UX trap. -->
<button
type="button"
data-testid="detail-reset-button"
onclick={handleReset}
class="shrink-0 inline-flex items-center gap-1.5 rounded-full bg-slate-900 px-3 py-1
text-xs font-semibold text-white hover:bg-slate-800
dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-200"
>
<RotateCcw size={12} strokeWidth={2} />
{m.list_reset()}
</button>
{/if}
</div>

View File

@@ -0,0 +1,72 @@
/**
* RST-series: reset list from /lists/[id] (Fase 10.7).
*
* The completed-list detail view exposes a prominent "Reset list" button
* next to the title. Clicking it brings the list back to status='active'
* without leaving the page.
*/
import { test, expect } from '@playwright/test';
import { USERS } from '../fixtures/users.js';
import { loginAs } from '../fixtures/login.js';
import { closePool, sql, COLLECTIVE_ID, BORJA_ID } from '@colectivo/test-utils';
test.describe.configure({ mode: 'serial' });
const created: { id: string | null } = { id: null };
test.describe('Reset list from detail view (10.7)', () => {
test.beforeEach(async () => {
// Seed a completed list with one checked item.
const r = await sql(
`INSERT INTO public.shopping_lists (collective_id, name, status, completed_at, created_by)
VALUES ($1, 'Reset-from-detail', 'completed', now(), $2)
RETURNING id`,
[COLLECTIVE_ID, BORJA_ID]
);
created.id = r.rows[0].id as string;
await sql(
`INSERT INTO public.shopping_items (list_id, name, sort_order, created_by, is_checked, checked_by, checked_at)
VALUES ($1, 'Milk', 1, $2, true, $2, now())`,
[created.id, BORJA_ID]
);
});
test.afterEach(async () => {
if (created.id) {
await sql('DELETE FROM public.shopping_lists WHERE id = $1', [created.id]);
created.id = null;
}
});
test.afterAll(async () => {
await closePool();
});
test('RST-01: completed list shows a Reset button that brings it back to active', async ({
page
}) => {
await loginAs(page, USERS.borja);
await page.goto(`/lists/${created.id}`);
const resetBtn = page.getByTestId('detail-reset-button');
await expect(resetBtn).toBeVisible({ timeout: 15_000 });
await resetBtn.click();
// Button should disappear once the list is back to active.
await expect(resetBtn).toHaveCount(0, { timeout: 5_000 });
const row = await sql(
'SELECT status::text AS status, completed_at FROM public.shopping_lists WHERE id = $1',
[created.id]
);
expect(row.rows[0].status).toBe('active');
expect(row.rows[0].completed_at).toBeNull();
// Items uncheked.
const items = await sql(
'SELECT bool_and(NOT is_checked) AS all_unchecked FROM public.shopping_items WHERE list_id = $1',
[created.id]
);
expect(items.rows[0].all_unchecked).toBe(true);
});
});