feat(kanban): requester input empty + keyboard nav (issue 0088)

CardForm: drop pre-fill of requester from logged user; Enter inside the
Autocomplete no longer submits the form (Mantine handles dropdown
selection; arrows + Enter pick option without closing modal). Submit
remains via "Crear" button or Ctrl+Enter from description.

Adds data-field="requester" and data-test="add-card" selectors for stable
e2e queries.

Tests:
- vitest component test (CardForm.test.tsx): empty input, Enter does not
  submit, submit only via button. Dropdown arrow nav covered by e2e
  (jsdom portal handling is brittle).
- Playwright e2e (requester-input.spec.ts) using new browser capability
  group (pw_kanban_login, pw_keyboard_sequence) from registry.
- seed_e2e_user CLI to create deterministic test user against
  operations.db (bcrypt via standard backend hash).

Setup additions (frontend/):
- vitest + @testing-library + jsdom devDeps
- @playwright/test devDep + playwright.config.ts
- src/test/setup.ts polyfills jsdom for Mantine (matchMedia,
  visualViewport, document.fonts, ResizeObserver)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 12:57:00 +02:00
parent a34a8142cc
commit eb1c13d82c
14 changed files with 1224 additions and 61 deletions
+71
View File
@@ -0,0 +1,71 @@
import { describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MantineProvider } from "@mantine/core";
import { CardForm } from "./CardForm";
function renderForm(overrides: Partial<Parameters<typeof CardForm>[0]> = {}) {
const onSubmit = vi.fn();
const onCancel = vi.fn();
render(
<MantineProvider>
<CardForm
requesterOptions={["Alice", "Anna", "Bob", "Enmanuel"]}
onSubmit={onSubmit}
onCancel={onCancel}
{...overrides}
/>
</MantineProvider>
);
return { onSubmit, onCancel };
}
describe("CardForm — requester input (issue 0088)", () => {
it("solicitante entra vacio cuando initial.requester no se pasa", () => {
renderForm();
const requesterInput = (document.querySelector('input[data-field="requester"]') as HTMLInputElement) as HTMLInputElement;
expect(requesterInput.value).toBe("");
});
it("Enter dentro del requester NO dispara onSubmit (dropdown cerrado o abierto)", async () => {
const user = userEvent.setup();
const { onSubmit } = renderForm();
// Necesita un titulo valido para que un eventual submit no se ignore por el guard.
const title = screen.getByLabelText(/Tarea/i);
await user.type(title, "Mi tarea");
const requester = (document.querySelector('input[data-field="requester"]') as HTMLInputElement);
await user.click(requester);
await user.keyboard("{Enter}");
expect(onSubmit).not.toHaveBeenCalled();
await user.type(requester, "An");
await user.keyboard("{Enter}");
expect(onSubmit).not.toHaveBeenCalled();
});
// Navegacion ArrowDown + Enter del dropdown la maneja Mantine internamente.
// Validar eso en jsdom es fragil (portals + virtual focus). Cubierto en e2e
// Playwright donde corre browser real.
it("submit solo via boton Crear", async () => {
const user = userEvent.setup();
const { onSubmit } = renderForm({ submitLabel: "Crear" });
const title = screen.getByLabelText(/Tarea/i);
await user.type(title, "Mi tarea");
const requester = (document.querySelector('input[data-field="requester"]') as HTMLInputElement);
await user.type(requester, "Anna");
await user.click(screen.getByRole("button", { name: /Crear/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledTimes(1);
});
expect(onSubmit.mock.calls[0][0]).toMatchObject({
title: "Mi tarea",
requester: "Anna",
});
});
});
+9 -7
View File
@@ -2,6 +2,11 @@ import { Autocomplete, Button, Group, Select, Stack, TagsInput, Textarea } from
import { FormEvent, KeyboardEvent, useState } from "react";
import type { User } from "../types";
// CardForm: Solicitante (Autocomplete) intencionadamente NO hace submit en Enter.
// Enter dentro del Autocomplete deja que Mantine seleccione el item resaltado del
// dropdown sin cerrar el formulario. Submit solo via boton "Crear" o Ctrl+Enter
// en descripcion. Ver issue 0088.
export interface CardFormValues {
requester: string;
title: string;
@@ -48,12 +53,6 @@ export function CardForm({
});
};
const enterSubmit = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
submit();
}
};
const textareaEnter = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
@@ -89,9 +88,12 @@ export function CardForm({
data={requesterOptions}
tabIndex={2}
autoComplete="off"
onKeyDown={enterSubmit}
data-field="requester"
placeholder="Empieza a escribir y elige uno existente"
limit={10}
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
<Textarea
label="Descripcion"
+1
View File
@@ -455,6 +455,7 @@ function KanbanColumnImpl({
onClick={() => onAddCard(column.id)}
mt="xs"
fullWidth
data-test="add-card"
>
Anadir tarjeta
</Button>