Author SHA1 Message Date
edgar.friendly 49e965b248 feat(pasarela): datos inferidos juntos en una pantalla "Datos del local"
deploy-branch / deploy (push) Successful in 2m18s
deploy-branch / teardown (push) Skipped
tests / Unitarias + componente + build (pull_request) Successful in 2m23s
tests / Integración (Nominatim real, opt-in) (pull_request) Skipped
tests / Unitarias + componente + build (push) Successful in 2m30s
tests / Integración (Nominatim real, opt-in) (push) Skipped
- Nuevo PasoDatosLocal con nombre, provincia y dirección (ahora editable)
  en la misma pantalla, con aviso cuando vienen de la inferencia
- El wizard pasa de 6 a 5 pasos: Ubicación → Datos del local → Categoría
  → Tipo específico → Resumen
- Eliminados PasoNombre y PasoProvincia (absorbidos)
- Spec principal actualizada al nuevo orden de pantallas
2026-09-12 19:00:33 +02:00
edgar.friendly 30b7cc884e ci: verificación de statuses tras reinicio de runner y gitea
deploy-branch / deploy (push) Successful in 2m13s
deploy-branch / teardown (push) Skipped
tests / Unitarias + componente + build (push) Successful in 2m41s
tests / Integración (Nominatim real, opt-in) (push) Skipped
tests / Unitarias + componente + build (pull_request) Successful in 2m37s
tests / Integración (Nominatim real, opt-in) (pull_request) Skipped
2026-09-10 18:33:12 +02:00
edgar.friendly d0fa037010 ci(tests): activa gcc-toolset-12 para compilar better-sqlite3 con npm ci
deploy-branch / deploy (push) Successful in 2m35s
deploy-branch / teardown (push) Skipped
tests / Unitarias + componente + build (push) Successful in 2m47s
tests / Integración (Nominatim real, opt-in) (push) Skipped
2026-08-18 07:42:42 +02:00
edgar.friendly 00eaa1d209 ci: mueve tests.yml a .gitea/workflows (ubicación que este Gitea ejecuta)
deploy-branch / deploy (push) Successful in 2m37s
deploy-branch / teardown (push) Skipped
tests / Unitarias + componente + build (push) Failing after 1m48s
tests / Integración (Nominatim real, opt-in) (push) Skipped
2026-08-18 07:30:59 +02:00
edgar.friendly 0c5651a6c0 fix(ci): compatibilidad con el runner self-hosted (npm 10 / Node 20)
deploy-branch / deploy (push) Successful in 2m43s
deploy-branch / teardown (push) Skipped
- vitest 4 traía vite 8 anidado con esbuild 0.28: el lockfile quedaba
  incompleto para npm ci y el npm 10.8.2 del runner fallaba con
  "Missing @esbuild/* from lock file" / EBADPLATFORM. vitest 3 usa el
  vite 5 del proyecto como peer: sin árbol anidado, esbuild solo 0.21.5
- jsdom 30 exige Node >=22.22 (el runner tiene 20.20): bajado a jsdom 26
- tests.yml sobre el runner real (self-hosted, Node del sistema) en vez
  de ubuntu-latest, que no existe como label
- aserciones de max()/calc() tolerantes a la serialización de jsdom 26
2026-08-18 07:27:19 +02:00
13 changed files with 844 additions and 1391 deletions
@@ -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.
+686 -1273
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -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"
}
}
+2 -4
View File
@@ -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 },
+73
View File
@@ -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>
);
}
-26
View File
@@ -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>
);
}
-26
View File
@@ -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>
);
}
+11 -3
View File
@@ -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;
+7 -2
View File
@@ -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");
+7 -2
View File
@@ -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);
+10 -8
View File
@@ -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();
});
+26 -25
View File
@@ -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();
});
});