Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49e965b248 | ||
|
|
30b7cc884e | ||
|
|
d0fa037010 | ||
|
|
00eaa1d209 | ||
|
|
0c5651a6c0 |
@@ -11,17 +11,19 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
name: Unitarias + componente + build
|
||||
runs-on: ubuntu-latest
|
||||
# El runner disponible es self-hosted (AlmaLinux, Node 20 del sistema);
|
||||
# las deps soportan Node >=18/20 (jsdom@26), así que no hace falta setup-node.
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Instalar dependencias
|
||||
run: npm ci
|
||||
# better-sqlite3 (v12) requiere C++20 y el GCC 8 base de AlmaLinux 8
|
||||
# no lo soporta: se activa gcc-toolset-12 igual que en deploy.yml
|
||||
shell: bash
|
||||
run: |
|
||||
source /opt/rh/gcc-toolset-12/enable 2>/dev/null || echo "WARN: gcc-toolset-12 no encontrado"
|
||||
npm ci
|
||||
|
||||
- name: Pruebas unitarias y de componente (offline)
|
||||
run: npm test
|
||||
@@ -31,19 +33,17 @@ jobs:
|
||||
|
||||
integracion:
|
||||
name: Integración (Nominatim real, opt-in)
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: self-hosted
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
continue-on-error: true # depende de un servicio externo: informativa
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Instalar dependencias
|
||||
run: npm ci
|
||||
shell: bash
|
||||
run: |
|
||||
source /opt/rh/gcc-toolset-12/enable 2>/dev/null || echo "WARN: gcc-toolset-12 no encontrado"
|
||||
npm ci
|
||||
|
||||
- name: Pruebas de integración (API en memoria + Nominatim)
|
||||
run: npm run test:integration
|
||||
@@ -26,7 +26,7 @@ El sistema SHALL presentar la propuesta de un local como una pasarela (wizard) d
|
||||
- **THEN** la pasarela se cierra y se descarta el borrador en curso
|
||||
|
||||
### Requirement: Orden de las pantallas empezando por Ubicación
|
||||
La pasarela SHALL presentar las pantallas en este orden: 1) Ubicación, 2) Nombre, 3) Provincia, 4) Categoría, 5) Tipo específico (opcional), 6) Revisión y envío.
|
||||
La pasarela SHALL presentar las pantallas en este orden: 1) Ubicación, 2) Datos del local (nombre, provincia y dirección inferidos, juntos en una misma pantalla), 3) Categoría, 4) Tipo específico (opcional), 5) Revisión y envío.
|
||||
|
||||
#### Scenario: Primera pantalla
|
||||
- **WHEN** se abre la pasarela
|
||||
@@ -36,12 +36,12 @@ La pasarela SHALL presentar las pantallas en este orden: 1) Ubicación, 2) Nombr
|
||||
- **WHEN** el usuario está en la pantalla de Tipo específico
|
||||
- **THEN** puede continuar sin seleccionar ninguna subcategoría
|
||||
|
||||
### Requirement: Inferencia de Nombre y Provincia desde la Ubicación
|
||||
El sistema SHALL pre-rellenar los campos Nombre y Provincia a partir de la ubicación introducida en la primera pantalla (geocodificación inversa), permitiendo al usuario corregirlos.
|
||||
### Requirement: Inferencia de Nombre, Provincia y Dirección desde la Ubicación
|
||||
El sistema SHALL pre-rellenar los campos Nombre, Provincia y Dirección a partir de la ubicación introducida en la primera pantalla (geocodificación inversa), mostrándolos juntos en la pantalla de Datos del local y permitiendo al usuario corregirlos (incluida la dirección).
|
||||
|
||||
#### Scenario: Nombre y provincia pre-rellenados
|
||||
#### Scenario: Datos pre-rellenados en su pantalla conjunta
|
||||
- **WHEN** el usuario introduce una ubicación válida en la primera pantalla y esta se resuelve con éxito
|
||||
- **THEN** al llegar a las pantallas de Nombre y Provincia ambos campos aparecen pre-rellenados con los valores inferidos
|
||||
- **THEN** al llegar a la pantalla de Datos del local los campos nombre, provincia y dirección aparecen pre-rellenados con los valores inferidos
|
||||
|
||||
#### Scenario: Valores corregibles
|
||||
- **WHEN** los valores inferidos no son correctos
|
||||
@@ -49,7 +49,7 @@ El sistema SHALL pre-rellenar los campos Nombre y Provincia a partir de la ubica
|
||||
|
||||
#### Scenario: Degradación sin inferencia
|
||||
- **WHEN** la inferencia falla o devuelve campos vacíos
|
||||
- **THEN** las pantallas de Nombre y Provincia aparecen vacías y el usuario los rellena manualmente, sin bloquear la pasarela
|
||||
- **THEN** la pantalla de Datos del local aparece vacía y el usuario la rellena manualmente, sin bloquear la pasarela
|
||||
|
||||
### Requirement: Sin campo de comentario ni puntuación
|
||||
La pasarela SHALL NOT incluir el campo de comentario/descripción ni el campo de puntuación; la propuesta se envía sin esos datos.
|
||||
|
||||
Generated
+686
-1273
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -24,9 +24,9 @@
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"concurrently": "^8.0.0",
|
||||
"jsdom": "^30.0.1",
|
||||
"jsdom": "^26.1.0",
|
||||
"supertest": "^7.2.2",
|
||||
"vite": "^5.2.0",
|
||||
"vitest": "^4.1.10"
|
||||
"vitest": "^3.2.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useEsMovil from "../hooks/useEsMovil.js";
|
||||
import PasoUbicacion from "./PasoUbicacion.jsx";
|
||||
import PasoNombre from "./PasoNombre.jsx";
|
||||
import PasoProvincia from "./PasoProvincia.jsx";
|
||||
import PasoDatosLocal from "./PasoDatosLocal.jsx";
|
||||
import PasoCategoria from "./PasoCategoria.jsx";
|
||||
import PasoSubcategoria from "./PasoSubcategoria.jsx";
|
||||
import PasoResumen from "./PasoResumen.jsx";
|
||||
@@ -12,8 +11,7 @@ import PasoResumen from "./PasoResumen.jsx";
|
||||
// gestiona su propio botón de envío en PasoResumen.
|
||||
const PASOS = [
|
||||
{ id: "ubicacion", titulo: "Ubicación", Componente: PasoUbicacion, puedeContinuar: (f) => f.lat != null && f.lng != null },
|
||||
{ id: "nombre", titulo: "Nombre", Componente: PasoNombre, puedeContinuar: (f) => (f.nombre || "").trim().length >= 2 },
|
||||
{ id: "provincia", titulo: "Provincia", Componente: PasoProvincia, puedeContinuar: (f) => !!f.provincia },
|
||||
{ id: "datos", titulo: "Datos del local", Componente: PasoDatosLocal, puedeContinuar: (f) => (f.nombre || "").trim().length >= 2 && !!f.provincia },
|
||||
{ id: "categoria", titulo: "Categoría", Componente: PasoCategoria, puedeContinuar: (f) => !!f.categoria },
|
||||
{ id: "subcategoria", titulo: "Tipo específico", Componente: PasoSubcategoria, puedeContinuar: () => true },
|
||||
{ id: "resumen", titulo: "Revisión y envío", Componente: PasoResumen, puedeContinuar: () => true },
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { inputStyle } from "../styles/shared.js";
|
||||
import { PROVINCIAS } from "../constants/provincias.js";
|
||||
|
||||
const etiqueta = { fontSize: 12, color: "var(--color-text-secondary)", display: "block", marginBottom: 4 };
|
||||
|
||||
// Paso 2 de la pasarela: los datos inferidos desde la ubicación (nombre,
|
||||
// provincia y dirección), pre-rellenados y editables en una misma pantalla.
|
||||
// Si la inferencia falló o no casó con el listado, llegan vacíos y se
|
||||
// rellenan a mano, sin bloquear la pasarela.
|
||||
export default function PasoDatosLocal({ form, setForm }) {
|
||||
const algunoInferido = !!(form.nombre || form.provincia || form.direccion);
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{algunoInferido && (
|
||||
<div role="status" style={{
|
||||
display: "flex", alignItems: "flex-start", gap: 6,
|
||||
background: "#EAFAF1", color: "#1E8449",
|
||||
padding: "6px 10px", borderRadius: "var(--border-radius-md)", fontSize: 12,
|
||||
}}>
|
||||
<i className="ti ti-sparkles" aria-hidden="true" style={{ fontSize: 14, flexShrink: 0, marginTop: 1 }}></i>
|
||||
<span style={{ lineHeight: 1.4 }}>
|
||||
Rellenados automáticamente desde la ubicación: compruébalos y corrige lo que haga falta.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="paso-nombre" style={etiqueta}>
|
||||
Nombre del local <span style={{ color: "#8B0000" }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="paso-nombre"
|
||||
style={inputStyle}
|
||||
autoComplete="off"
|
||||
placeholder="Bar El Olivo, Clínica San José..."
|
||||
value={form.nombre}
|
||||
onChange={(e) => setForm((f) => ({ ...f, nombre: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="paso-provincia" style={etiqueta}>
|
||||
Provincia <span style={{ color: "#8B0000" }}>*</span>
|
||||
</label>
|
||||
<select
|
||||
id="paso-provincia"
|
||||
style={inputStyle}
|
||||
value={form.provincia}
|
||||
onChange={(e) => setForm((f) => ({ ...f, provincia: e.target.value }))}
|
||||
>
|
||||
<option value="">Selecciona provincia...</option>
|
||||
{PROVINCIAS.map((p) => <option key={p} value={p}>{p}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="paso-direccion" style={etiqueta}>Dirección</label>
|
||||
<input
|
||||
id="paso-direccion"
|
||||
style={inputStyle}
|
||||
autoComplete="off"
|
||||
placeholder="Calle, número, ciudad"
|
||||
value={form.direccion}
|
||||
onChange={(e) => setForm((f) => ({ ...f, direccion: e.target.value }))}
|
||||
aria-describedby="paso-direccion-ayuda"
|
||||
/>
|
||||
<p id="paso-direccion-ayuda" style={{ fontSize: 11, color: "var(--color-text-tertiary)", margin: "4px 0 0" }}>
|
||||
Opcional: se rellena automáticamente desde la ubicación; corrígela si hace falta.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { inputStyle } from "../styles/shared.js";
|
||||
|
||||
// Paso 2 de la pasarela: nombre del local, pre-rellenado con lo inferido
|
||||
// desde la ubicación (editable). Si la inferencia falló, llega vacío.
|
||||
export default function PasoNombre({ form, setForm }) {
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor="paso-nombre" style={{ fontSize: 12, color: "var(--color-text-secondary)", display: "block", marginBottom: 4 }}>
|
||||
Nombre del local <span style={{ color: "#8B0000" }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="paso-nombre"
|
||||
style={inputStyle}
|
||||
autoComplete="off"
|
||||
placeholder="Bar El Olivo, Clínica San José..."
|
||||
value={form.nombre}
|
||||
onChange={(e) => setForm((f) => ({ ...f, nombre: e.target.value }))}
|
||||
/>
|
||||
<p style={{ fontSize: 11, color: "var(--color-text-tertiary)", margin: "6px 0 0" }}>
|
||||
{form.nombre
|
||||
? "Si el nombre se rellenó automáticamente desde la ubicación, comprueba que es correcto antes de continuar."
|
||||
: "El nombre con el que la gente conoce el negocio."}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { inputStyle } from "../styles/shared.js";
|
||||
import { PROVINCIAS } from "../constants/provincias.js";
|
||||
|
||||
// Paso 3 de la pasarela: provincia, pre-rellenada con lo inferido desde la
|
||||
// ubicación (si la inferencia falló o no casó con el listado, llega vacía).
|
||||
export default function PasoProvincia({ form, setForm }) {
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor="paso-provincia" style={{ fontSize: 12, color: "var(--color-text-secondary)", display: "block", marginBottom: 4 }}>
|
||||
Provincia <span style={{ color: "#8B0000" }}>*</span>
|
||||
</label>
|
||||
<select
|
||||
id="paso-provincia"
|
||||
style={inputStyle}
|
||||
value={form.provincia}
|
||||
onChange={(e) => setForm((f) => ({ ...f, provincia: e.target.value }))}
|
||||
>
|
||||
<option value="">Selecciona provincia...</option>
|
||||
{PROVINCIAS.map((p) => <option key={p} value={p}>{p}</option>)}
|
||||
</select>
|
||||
<p style={{ fontSize: 11, color: "var(--color-text-tertiary)", margin: "6px 0 0" }}>
|
||||
Si se rellenó automáticamente desde la ubicación, verifica que es correcta.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -24,7 +24,15 @@ const matchMediaMock = (esMovil) => (_query) => ({
|
||||
|
||||
// En pantallas anchas la tarjeta acompaña a la columna de 720px (mismo borde
|
||||
// que el contenido); en estrechas queda a 16px del borde de la ventana.
|
||||
const PATRON_ALINEADO_CUERPO = /left:\s*max\(16px,\s*[^;]*100vw[^;]*720px[^;]*16px[^;]*\)/;
|
||||
// (la serialización exacta de max()/calc() varía entre versiones de jsdom,
|
||||
// así que se comprueba por contenido y no por sintaxis exacta)
|
||||
function assertAlineadoAlCuerpo(styleAttr) {
|
||||
const left = (styleAttr.match(/left:\s*([^;]+)/) || [])[1];
|
||||
expect(left, "debe usar left con max()").toMatch(/max\(/);
|
||||
expect(left).toContain("100vw");
|
||||
expect(left).toContain("720px");
|
||||
expect(left).toContain("16px");
|
||||
}
|
||||
|
||||
function contenedorFijo() {
|
||||
const fijos = [...document.querySelectorAll("div")].filter((d) => d.style.position === "fixed");
|
||||
@@ -53,7 +61,7 @@ describe("BannerMovil (PC y móvil): abajo a la izquierda, alineado al cuerpo",
|
||||
|
||||
const contenedor = contenedorFijo();
|
||||
expect(contenedor.style.bottom).toBe("0px");
|
||||
expect(contenedor.getAttribute("style")).toMatch(PATRON_ALINEADO_CUERPO);
|
||||
assertAlineadoAlCuerpo(contenedor.getAttribute("style"));
|
||||
expect(contenedor.style.right).toBe(""); // nunca a la derecha, ahí vive el FAB
|
||||
// en PC la tarjeta es más grande
|
||||
const tarjeta = screen.getByRole("button", { name: "Cerrar aviso de instalación" }).parentElement;
|
||||
@@ -65,7 +73,7 @@ describe("BannerMovil (PC y móvil): abajo a la izquierda, alineado al cuerpo",
|
||||
render(<BannerMovil />);
|
||||
const contenedor = contenedorFijo();
|
||||
// misma fórmula: con vw < 720px el navegador la evalúa a 16px
|
||||
expect(contenedor.getAttribute("style")).toMatch(PATRON_ALINEADO_CUERPO);
|
||||
assertAlineadoAlCuerpo(contenedor.getAttribute("style"));
|
||||
expect(contenedor.style.bottom).toBe("0px");
|
||||
expect(contenedor.style.right).toBe("");
|
||||
const tarjeta = screen.getByRole("button", { name: "Cerrar aviso de instalación" }).parentElement;
|
||||
|
||||
@@ -59,8 +59,13 @@ describe("BotonFlotante", () => {
|
||||
const contenedor = boton.closest("div").parentElement; // button → relativo → fijo
|
||||
expect(contenedor.style.position).toBe("fixed");
|
||||
expect(contenedor.style.bottom).toBe("0px");
|
||||
// en pantallas anchas acompaña a la columna de 720px; mínimo 16px
|
||||
expect(contenedor.getAttribute("style")).toMatch(/right:\s*max\(16px,\s*[^;]*100vw[^;]*720px[^;]*16px[^;]*\)/);
|
||||
// en pantallas anchas acompaña a la columna de 720px; mínimo 16px.
|
||||
// (la serialización exacta de max()/calc() varía entre versiones de jsdom)
|
||||
const right = (contenedor.getAttribute("style").match(/right:\s*([^;]+)/) || [])[1];
|
||||
expect(right).toMatch(/max\(/);
|
||||
expect(right).toContain("100vw");
|
||||
expect(right).toContain("720px");
|
||||
expect(right).toContain("16px");
|
||||
// en PC el FAB mide 64px (en móvil 54)
|
||||
expect(boton.style.width).toBe("64px");
|
||||
expect(boton.style.height).toBe("64px");
|
||||
|
||||
@@ -30,8 +30,13 @@ describe("BotonProponerArriba (solo PC)", () => {
|
||||
|
||||
expect(boton.style.position).toBe("fixed");
|
||||
expect(boton.style.top).toBe("20px");
|
||||
// alineado al borde derecho de la columna de 720px; mínimo 16px
|
||||
expect(boton.getAttribute("style")).toMatch(/right:\s*max\(16px,\s*[^;]*100vw[^;]*720px[^;]*16px[^;]*\)/);
|
||||
// alineado al borde derecho de la columna de 720px; mínimo 16px.
|
||||
// (la serialización exacta de max()/calc() varía entre versiones de jsdom)
|
||||
const right = (boton.getAttribute("style").match(/right:\s*([^;]+)/) || [])[1];
|
||||
expect(right).toMatch(/max\(/);
|
||||
expect(right).toContain("100vw");
|
||||
expect(right).toContain("720px");
|
||||
expect(right).toContain("16px");
|
||||
|
||||
fireEvent.click(boton);
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -20,6 +20,7 @@ const FORM_RESUELTO = {
|
||||
proveedorUbicacion: "coordenadas",
|
||||
lat: 41.3851, lng: 2.1734,
|
||||
nombre: "Bar X", provincia: "Barcelona",
|
||||
direccion: "Calle Mayor 5, Alcalá de Henares",
|
||||
categoria: "Restauración", subcategoria: "Tapas y raciones",
|
||||
};
|
||||
|
||||
@@ -41,14 +42,13 @@ const propsBase = {
|
||||
const siguiente = () => screen.getByRole("button", { name: "Siguiente" });
|
||||
|
||||
describe("ModalPasarela", () => {
|
||||
it("arranca en la pantalla de Ubicación con las 6 pantallas declaradas", () => {
|
||||
it("arranca en la pantalla de Ubicación con las 5 pantallas declaradas", () => {
|
||||
render(<ModalPasarela {...propsBase} form={FORM_VACIO} />);
|
||||
expect(screen.getByLabelText(/ubicación del local/i)).toBeTruthy();
|
||||
expect(screen.getByText(/paso 1 de 6/i)).toBeTruthy();
|
||||
expect(screen.getAllByRole("generic", { hidden: true })).toBeTruthy();
|
||||
// indicador de progreso: 6 barras
|
||||
expect(screen.getByText(/paso 1 de 5/i)).toBeTruthy();
|
||||
// indicador de progreso: 5 barras
|
||||
const barras = document.querySelectorAll('[aria-hidden="true"] span[style*="width: 16px"]');
|
||||
expect(barras.length).toBe(6);
|
||||
expect(barras.length).toBe(5);
|
||||
});
|
||||
|
||||
it("Siguiente deshabilitado sin resolución de ubicación válida", () => {
|
||||
@@ -56,12 +56,14 @@ describe("ModalPasarela", () => {
|
||||
expect(siguiente().disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("Siguiente habilitado con datos válidos y navega; Atrás conserva los datos", () => {
|
||||
it("Siguiente habilitado con datos válidos y navega; los datos inferidos comparten pantalla; Atrás conserva", () => {
|
||||
render(<ModalPasarela {...propsBase} form={FORM_RESUELTO} />);
|
||||
expect(siguiente().disabled).toBe(false);
|
||||
fireEvent.click(siguiente());
|
||||
// paso 2: nombre pre-rellenado con lo inferido
|
||||
// paso 2: datos del local (nombre, provincia y dirección) en la misma pantalla
|
||||
expect(screen.getByLabelText(/nombre del local/i).value).toBe("Bar X");
|
||||
expect(screen.getByLabelText(/provincia/i).value).toBe("Barcelona");
|
||||
expect(screen.getByLabelText(/^dirección/i).value).toBe("Calle Mayor 5, Alcalá de Henares");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /atrás/i }));
|
||||
// vuelve a ubicación con la entrada conservada
|
||||
@@ -105,7 +107,7 @@ describe("ModalPasarela", () => {
|
||||
|
||||
it("en el paso final muestra el resumen con su propio botón de envío", () => {
|
||||
render(<ModalPasarela {...propsBase} form={FORM_RESUELTO} />);
|
||||
for (let i = 0; i < 5; i++) fireEvent.click(siguiente());
|
||||
for (let i = 0; i < 4; i++) fireEvent.click(siguiente());
|
||||
expect(screen.getByRole("button", { name: /enviar propuesta/i })).toBeTruthy();
|
||||
expect(screen.queryByRole("button", { name: "Siguiente" })).toBeNull();
|
||||
});
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { useState } from "react";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import PasoNombre from "../../src/components/PasoNombre.jsx";
|
||||
import PasoProvincia from "../../src/components/PasoProvincia.jsx";
|
||||
import PasoDatosLocal from "../../src/components/PasoDatosLocal.jsx";
|
||||
import PasoCategoria from "../../src/components/PasoCategoria.jsx";
|
||||
import PasoSubcategoria from "../../src/components/PasoSubcategoria.jsx";
|
||||
import { PROVINCIAS } from "../../src/constants/provincias.js";
|
||||
@@ -12,8 +11,7 @@ import { CATEGORIAS, NOMBRES_CATEGORIAS } from "../../src/constants/categorias.j
|
||||
afterEach(cleanup);
|
||||
|
||||
// Harness con useState real: los pasos escriben a través de setForm y el
|
||||
// estado resultante se lee desde fuera (más fiel que espiar el updater,
|
||||
// cuya evaluación diferida pierde el valor del evento en React 18).
|
||||
// estado resultante se lee desde fuera (más fiel que espiar el updater).
|
||||
function renderPaso(Componente, inicial) {
|
||||
const estado = { form: inicial };
|
||||
function Wrapper() {
|
||||
@@ -25,35 +23,38 @@ function renderPaso(Componente, inicial) {
|
||||
return estado;
|
||||
}
|
||||
|
||||
describe("PasoNombre", () => {
|
||||
it("muestra el nombre inferido y es editable", () => {
|
||||
const estado = renderPaso(PasoNombre, { nombre: "Bar El Olivo" });
|
||||
const input = screen.getByLabelText(/nombre del local/i);
|
||||
expect(input.value).toBe("Bar El Olivo");
|
||||
fireEvent.change(input, { target: { value: "Bar La Plaza" } });
|
||||
describe("PasoDatosLocal (datos inferidos en una pantalla)", () => {
|
||||
it("muestra los tres campos pre-rellenados con lo inferido, en la misma pantalla", () => {
|
||||
renderPaso(PasoDatosLocal, { nombre: "Puerta del Sol", provincia: "Madrid", direccion: "Puerta del Sol, 28013 Madrid" });
|
||||
expect(screen.getByLabelText(/nombre del local/i).value).toBe("Puerta del Sol");
|
||||
expect(screen.getByLabelText(/provincia/i).value).toBe("Madrid");
|
||||
expect(screen.getByLabelText(/^dirección/i).value).toBe("Puerta del Sol, 28013 Madrid");
|
||||
// aviso de que vienen de la inferencia
|
||||
expect(screen.getByText(/Rellenados automáticamente desde la ubicación/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("todos editables: nombre (input), provincia (select PROVINCIAS), dirección (input)", () => {
|
||||
const estado = renderPaso(PasoDatosLocal, { nombre: "Bar X", provincia: "Barcelona", direccion: "Calle 1" });
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/nombre del local/i), { target: { value: "Bar La Plaza" } });
|
||||
expect(estado.form.nombre).toBe("Bar La Plaza");
|
||||
});
|
||||
|
||||
it("vacío si la inferencia falló", () => {
|
||||
renderPaso(PasoNombre, { nombre: "" });
|
||||
expect(screen.getByLabelText(/nombre del local/i).value).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PasoProvincia", () => {
|
||||
it("es un select con el listado PROVINCIAS y la provincia pre-rellenada", () => {
|
||||
renderPaso(PasoProvincia, { provincia: "La Coruña" });
|
||||
const select = screen.getByLabelText(/provincia/i);
|
||||
expect(select.value).toBe("La Coruña");
|
||||
const opciones = [...select.options].map((o) => o.value);
|
||||
expect(opciones).toEqual(["", ...PROVINCIAS]);
|
||||
fireEvent.change(select, { target: { value: "Madrid" } });
|
||||
// (la aserción de estado se cubre con el harness en el resto de pasos)
|
||||
fireEvent.change(select, { target: { value: "Gerona" } });
|
||||
expect(estado.form.provincia).toBe("Gerona");
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/^dirección/i), { target: { value: "Calle Nueva 2" } });
|
||||
expect(estado.form.direccion).toBe("Calle Nueva 2");
|
||||
});
|
||||
|
||||
it("sin coincidencia queda vacío (selección manual)", () => {
|
||||
renderPaso(PasoProvincia, { provincia: "" });
|
||||
it("vacíos y sin aviso de inferencia cuando la inferencia falló", () => {
|
||||
renderPaso(PasoDatosLocal, { nombre: "", provincia: "", direccion: "" });
|
||||
expect(screen.getByLabelText(/nombre del local/i).value).toBe("");
|
||||
expect(screen.getByLabelText(/provincia/i).value).toBe("");
|
||||
expect(screen.getByLabelText(/^dirección/i).value).toBe("");
|
||||
expect(screen.queryByText(/Rellenados automáticamente/i)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user