Files
kanban/frontend/e2e/archive.spec.ts
T
egutierrez 9b503f0555 feat(kanban): archive automatico para cards Done +30 dias (issue 0092)
Adds an archive layer separate from the trash. Cards in is_done columns
that have been there for more than 30 days are auto-archived on the next
board load (throttled to once every 30 minutes). Archived cards leave
the board but stay in the DB and are listed in a new sidebar drawer
"Hecho (archivo)" below the existing Papelera, with a one-click restore.

Schema (migration 012_card_archived.sql):
- ALTER TABLE cards ADD COLUMN archived_at TEXT;
- NULL = active, ISO timestamp = archived. Independent from deleted_at.

Backend:
- Card.ArchivedAt + JSON; ListCardsWithTime filters archived_at IS NULL.
- New methods: ArchiveCard, UnarchiveCard, ListArchivedCards,
  AutoArchiveDoneOlderThan.
- New endpoints: GET /api/archive, POST /api/cards/:id/archive,
  POST /api/cards/:id/unarchive.
- handleGetBoard invokes maybeAutoArchive (atomic throttle, 30 min sweep,
  30 day cutoff). Errors logged but never block the board response.

Frontend:
- Card type + api.ts add the new field and helpers.
- App.tsx state for archive list, reload, archive/unarchive handlers.
- New sidebar drawer with toggle, count badge, restore button.
- KanbanCard gains an "Archivar" menu item (gated on isDone +
  onArchive prop) for manual archiving of any done card.

Tests:
- Playwright e2e/archive.spec.ts: manual archive via menu, drawer
  toggle, unarchive. Picks a done card via /api/board introspection so
  it stays stable regardless of board state.
- Auto-archive of >30d cards: not under e2e (real time travel needed);
  covered by code review of the SQL query and the throttle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:57:14 +02:00

58 lines
2.5 KiB
TypeScript

import { test, expect } from "@playwright/test";
import { pw_kanban_login } from "../../../../frontend/functions/browser/pw_kanban_login";
const USER = process.env.KANBAN_USER || "e2e_user";
const PWD = process.env.KANBAN_PWD || "e2e_test_pw_2026";
/**
* Issue 0092: cards en columnas DONE con >30 dias se mueven al cajon "Hecho".
* Test cubre: archivar via menu manual, listar archivo, des-archivar.
*/
test.describe("kanban archive (issue 0092)", () => {
test("archiva una done card via menu y la des-archiva desde el cajon", async ({ page }) => {
await page.goto("/");
await pw_kanban_login(page, { username: USER, password: PWD });
// Pick a card from a done column (queried directly from the API).
const board = await page.request.get("/api/board").then((r) => r.json());
const doneCol = (board.columns as Array<{ id: string; is_done: boolean }>).find((c) => c.is_done);
if (!doneCol) test.skip(true, "no done column in board");
const cardInDone = (board.cards as Array<{ id: string; column_id: string }>).find(
(c) => c.column_id === doneCol!.id
);
if (!cardInDone) test.skip(true, "no card in a done column");
const targetId = cardInDone!.id;
const cardSel = `[data-card-id="${targetId}"]`;
const card = page.locator(cardSel).first();
await expect(card).toBeVisible();
// Open the per-card menu. Use dispatchEvent so we ignore viewport scroll constraints.
await card.locator('button[aria-label="Acciones"]').dispatchEvent("click");
const archiveItem = page.getByRole("menuitem", { name: /Archivar/i }).first();
await expect(archiveItem).toBeVisible();
await archiveItem.click();
// Card disappears from board.
await expect(card).toHaveCount(0, { timeout: 5000 });
// Archive drawer toggle visible + opens.
const archiveToggle = page.locator('[data-test="archive-toggle"]');
await archiveToggle.scrollIntoViewIfNeeded();
await archiveToggle.dispatchEvent("click");
// Archived row appears in the drawer.
const archivedRow = page.locator(`[data-archived-card-id="${targetId}"]`);
await expect(archivedRow).toBeVisible({ timeout: 5000 });
// Restore from archive (force click — sidebar can be scrollable / off-viewport).
await archivedRow.locator("button").first().dispatchEvent("click");
// Back on board.
await expect(page.locator(cardSel).first()).toBeVisible({ timeout: 5000 });
// No longer in archive.
await expect(archivedRow).toHaveCount(0);
});
});