diff --git a/openspec/specs/pasarela-proponer-local/spec.md b/openspec/specs/pasarela-proponer-local/spec.md
index 474fd42..b15a5cb 100644
--- a/openspec/specs/pasarela-proponer-local/spec.md
+++ b/openspec/specs/pasarela-proponer-local/spec.md
@@ -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.
diff --git a/src/components/ModalPasarela.jsx b/src/components/ModalPasarela.jsx
index 817dd13..0fc2bcc 100644
--- a/src/components/ModalPasarela.jsx
+++ b/src/components/ModalPasarela.jsx
@@ -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 },
diff --git a/src/components/PasoDatosLocal.jsx b/src/components/PasoDatosLocal.jsx
new file mode 100644
index 0000000..530c109
--- /dev/null
+++ b/src/components/PasoDatosLocal.jsx
@@ -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 (
+
+ {algunoInferido && (
+
+
+
+ Rellenados automáticamente desde la ubicación: compruébalos y corrige lo que haga falta.
+
+
+ Opcional: se rellena automáticamente desde la ubicación; corrígela si hace falta.
+
+
+
+ );
+}
diff --git a/src/components/PasoNombre.jsx b/src/components/PasoNombre.jsx
deleted file mode 100644
index d783049..0000000
--- a/src/components/PasoNombre.jsx
+++ /dev/null
@@ -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 (
-
- {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."}
-
-
- );
-}
diff --git a/src/components/PasoProvincia.jsx b/src/components/PasoProvincia.jsx
deleted file mode 100644
index b989546..0000000
--- a/src/components/PasoProvincia.jsx
+++ /dev/null
@@ -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 (
-
-
-
-
- Si se rellenó automáticamente desde la ubicación, verifica que es correcta.
-
-
- );
-}
diff --git a/tests/component/ModalPasarela.spec.jsx b/tests/component/ModalPasarela.spec.jsx
index 7ae5fa4..ea5b7fc 100644
--- a/tests/component/ModalPasarela.spec.jsx
+++ b/tests/component/ModalPasarela.spec.jsx
@@ -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();
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();
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();
- 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();
});
diff --git a/tests/component/PasosFormulario.spec.jsx b/tests/component/PasosFormulario.spec.jsx
index 6637316..8e633a0 100644
--- a/tests/component/PasosFormulario.spec.jsx
+++ b/tests/component/PasosFormulario.spec.jsx
@@ -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();
});
});