- Campo universal de ubicación (Google Maps/Waze/OSM/coordenadas/dirección) sobre proveedores extensibles (src/utils/ubicacion) con resolver e inferencia inversa Nominatim (nombre/provincia pre-rellenados, alias cooficiales) - FAB flotante abajo a la derecha que abre la pasarela en un modal accesible (Escape, foco contenido, responsive); sin descripción ni puntuación; avisos de duplicado y filtro de palabras dentro del modal - Aviso PWA reposicionado: tarjeta abajo a la izquierda en móvil, botón compacto arriba a la izquierda en PC (no tapa el FAB) - Suite de pruebas: 88 unitarias/componente (vitest + RTL, offline) y 10 de integración (supertest + SQLite :memory: y Nominatim real opt-in) - CI: workflow de Gitea Actions con npm test + build en push/PR e integración manual
11 KiB
1. Proveedores de ubicación (núcleo extensible)
- 1.1 Crear
src/utils/ubicacion/ProveedorUbicacion.js: clase base con el contrato documentado (id,detecta(entrada),extrae(entrada)que lanza "sin implementar") y helpers compartidos (normalizar(),emparejarProvincia()contraPROVINCIAS, fetch a Nominatim con cabeceras y manejo de errores que devuelvenullen fallo) - 1.2 Implementar
proveedorCoordenadas.js: detecta pares "lat, lng" (coma o punto, signo, espacios) y devuelve{ proveedor, lat, lng } - 1.3 Implementar
proveedorGoogleMaps.js: detecta URLs de Google Maps y extrae coordenadas de los formatos@lat,lng,!3d…!4d…y?q=lat,lng(migrandoparsearEnlaceGoogleMapsdegeo.js) - 1.4 Implementar
proveedorWaze.js: detectawaze.comy extrae dell=lat,lng; si solo hayq=texto, delega la resolución en la búsqueda de Nominatim marcando el proveedor como waze - 1.5 Implementar
proveedorOSM.js: detectaopenstreetmap.orgy extrae demlat/mlony del fragmento#map=zoom/lat/lng - 1.6 Implementar
proveedorNominatim.js: búsqueda por texto (/search, migrandogeocodificarDireccion) como fallback de dirección, y métodoreverse(lat, lng)(/reverseconaddressdetails=1&namedetails=1) que devuelve{ nombre, provincia, direccion }con provincia normalizada al listado canónimo (vacía si no casa) - 1.7 Crear
src/utils/ubicacion/index.js: arrayPROVEEDORESordenado por prioridad (coordenadas → Google → Waze → OSM → Nominatim), funciónresolverUbicacion(entrada)que usa el primerdetectaque acepte, yinferirDesdeCoords(lat, lng)que envuelve el reverse de Nominatim con manejo de errores - 1.8 Ajustar
src/utils/geo.js: conservar exportaciones usadas porLocalCard/mapa (enlaceGoogleMapsDesdeLocal,enlaceWazeDesdeLocal,distanciaKm,formatearDistancia,buscarLugares) y reubicar lo migrado en los proveedores, sin romper imports existentes
2. Pantallas de la pasarela
- 2.1 Crear
PasoUbicacion.jsx: input universal con debounce (~600ms) que llama aresolverUbicacion; al resolver muestra chip con el proveedor detectado y coordenadas; llama ainferirDesdeCoordsy pre-rellenanombre/provincia/direccionen elform; muestra avisos de entrada no reconocida o error de red; "Siguiente" solo habilitado con resolución válida - 2.2 Crear
PasoNombre.jsxyPasoProvincia.jsx: inputs simples pre-rellenados con lo inferido (editables); provincia como select del listadoPROVINCIAS; vacíos si la inferencia falló - 2.3 Crear
PasoCategoria.jsxyPasoSubcategoria.jsx: selects conCATEGORIAS/NOMBRES_CATEGORIAS; subcategoría opcional (permite continuar sin elegir), solo accesible tras elegir categoría - 2.4 Crear
PasoResumen.jsx: resumen legible de todos los datos, checkbox de aceptación de privacidad (enlace alModalPrivacidadexistente) y botón "Enviar propuesta" deshabilitado sin aceptación; renderiza avisos de palabra prohibida y de duplicado (con "Enviar de todos modos"/"Revisar datos") dentro del modal
3. Modal de la pasarela y FAB
- 3.1 Crear
ModalPasarela.jsx: overlay siguiendo el patrón deModalPrivacidad(role="dialog",aria-modal, cierre por fondo/✕/Escape), cabecera con título e indicador de progreso, cuerpo con la pantalla activa y pie con botones Atrás/Siguiente; en móvil panel a ancho completo y casi todo el alto (hookuseEsMovilconmatchMedia), en escritorio centradomaxWidth≈480px; foco inicial en el primer control de cada pantalla - 3.2 Crear
BotonFlotante.jsx: FAB fijo abajo a la derecha (z-index< modal,padding-bottom: env(safe-area-inset-bottom)), con tooltip propio al hover/foco ("Añade un nuevo local al directorio") posicionado a la izquierda yaria-labelen el botón - 3.3 Integrar en
App.jsx: eliminar el botón "Proponer local" de la cabecera y el bloque de formulario en línea; renderizarBotonFlotante(abre el modal) yModalPasarela; sustituirmostrarFormpor estadopasarelaAbierta; actualizar el texto del pie "Proponlo arriba" para referirse al botón flotante
4. Envío de la propuesta
- 4.1 Actualizar
FORM_VACIOy el flujo de envío enApp.jsx: quitardescripcionypuntuaciondel formulario y del payload deenviarPropuesta;formValidosin requisito de puntuación; conservar filtro de palabras,comprobarDuplicado, fallback de coordenadas por provincia y confirmaciónenviadotras cerrar el modal; estados "Comprobando…/Enviando…" dentro del modal
5. Verificación
-
5.1 Probar manualmente los formatos de entrada del campo universal: enlace Google Maps (@/, !3d!4d, ?q=), Waze (ll= y q=), OSM (mlat/mlon y #map=), coordenadas sueltas y dirección en texto; verificar aviso con entrada no reconocida
Verificado a nivel de módulo contra los proveedores reales (con red): los 17 casos pasan, incluidos Waze
q=delegando en Nominatim y entradas no reconocidas devolviendonull(dispara el aviso del paso). Falta el clic-through visual en navegador. -
5.2 Verificar inferencia: nombre y provincia pre-rellenados desde coordenadas de varias comunidades (normalización de acentos), y flujo completo de propuesta sin comentario ni puntuación con aviso de duplicado y de palabra prohibida dentro del modal
Verificado
npm test(82/82: 51 unitarias + 31 componente) ynpm run test:integration(10/10 con red real). Además se cubrió programáticamente gran parte del contenido: avisos de duplicado/palabra prohibida enPasoResumen.spec.jsx, flujo de propuesta sin comentario/puntuación contra el backend real enapiLocal.spec.js, y normalización multi-comunidad ennominatim.spec.js. Pendiente solo el clic-through visual en navegador con backend levantado. -
5.3 Verificar responsive y accesibilidad: FAB visible sin tapar contenido en viewport estrecho y con barra PWA; tooltip en hover y foco; modal navegable con teclado, Escape cierra, foco contenido; enviar aceptación de privacidad obligatoria
Cobertura programática añadida: tooltip en foco/hover (
BotonFlotante.spec.jsx), Escape/✕/foco contenido/Tab wrap (ModalPasarela.spec.jsx), privacidad obligatoria (PasoResumen.spec.jsx). Pendiente solo la verificación visual en dispositivo/viewport estrecho y barra PWA (safe-area). -
5.4 Verificar no-regresión: tarjetas, mapa, favoritos, reportes y panel admin siguen funcionando (imports de
geo.jsintactos);npm run buildsin erroresnpm run buildOK (65 módulos); imports degeo.jsintactos en LocalCard, AdminPanel, App y AutocompletarLocal; sin referencias muertas (mostrarForm/CampoUbicacion/formValido).
6. Infraestructura de pruebas
- 6.1 Añadir dependencias y configuración de pruebas:
vitest+jsdom+@testing-library/react(+@testing-library/dom) +supertestcomo devDeps;vitest.config.mjs(unitarias/componente, offline) yvitest.config.integration.mjs; scriptsnpm test,npm run test:watch,npm run test:integration - 6.2 Hacer el servidor comprobable sin cambiar su comportamiento: ruta de BD configurable (
process.env.LOCALESP_DB, admite:memory:) yserver/index.jsexportandoappescuchando solo al ejecutarse como script (supertestsin puerto)
7. Pruebas unitarias (proveedores de ubicación)
- 7.1
ProveedorUbicacion(base):normalizar(),emparejarProvincia()(acentos, mayúsculas, artículos, alias cooficiales, sin coincidencia →""),extrae()de la base lanza "sin implementar",detecta()base no acepta nada,fetchNominatimdevuelvenullsifetchlanza o la respuesta no es ok - 7.2
proveedorCoordenadas: pares con coma y punto decimal, separador,/;/espacio, signo, espacios; latitud fuera de rango →null; texto que no es un par →detectafalse - 7.3
proveedorGoogleMaps: formatos@lat,lng,!3d…!4d…,?q=lat,lng; prioridad@sobre!3d!4d(comportamiento migrado deparsearEnlaceGoogleMaps); enlace acortadodetectaperoextrae→null; URL ajena →detectafalse - 7.4
proveedorWaze:ll=lat,lngok;llmalformado →null;q=textodelega en la búsqueda Nominatim (fetch simulado) marcandoproveedor: "waze";qsin resultados →null - 7.5
proveedorOSM: prioridadmlat/mlonsobre#map=;#map=zoom/lat/lng; sin ninguno →null - 7.6
proveedorNominatim:detectarechaza URLs y textos cortos y acepta direcciones;desdeResultadonormaliza provincia (Bizkaia→Vizcaya, A Coruña→La Coruña, desconocida→"");extraesin resultados →null - 7.7
resolverUbicacion/inferirDesdeCoords: orden de prioridad dePROVEEDORES, fallback de texto, proveedor que lanza → se degrada y prueba el siguiente, entrada vacía →null;ETIQUETAS_PROVEEDORcubre todos los ids registrados
8. Pruebas de componente (pasarela)
- 8.1
PasoUbicacion: debounce ~600ms (fake timers, no resuelve antes), chip con proveedor detectado y coordenadas, aviso de entrada no reconocida, pre-relleno delform(nombre/provincia/dirección/lat/lng/enlace según proveedor), aviso cuando la inferencia inversa falla - 8.2
PasoNombre/PasoProvincia/PasoCategoria/PasoSubcategoria: pre-rellenados con lo inferido y editables; provincia como select dePROVINCIAS; subcategoría deshabilitada sin categoría y opcional (se puede continuar) - 8.3
PasoResumen: resumen con los datos del form; botón "Enviar propuesta" deshabilitado sin aceptar privacidad y habilitado al marcar; abreModalPrivacidadvía enlace; avisos de palabra prohibida y de duplicado con "Enviar de todos modos"/"Revisar datos"; estados "Comprobando…/Enviando…" - 8.4
ModalPasarela: arranca en Ubicación; "Siguiente" deshabilitado sin dato válido y habilitado con él; "Atrás" conserva datos; Escape y ✕ cierran (onCerrar); foco inicial en el primer control de cada pantalla y contención de Tab dentro del modal - 8.5
BotonFlotante:aria-labeldescriptivo, tooltip oculto por defecto y visible al enfocar (y oculto al desenfocar),onClickal pulsar
9. Pruebas de integración (backends reales)
- 9.1 API local con
supertest+ BD:memory::POST /api/propuestassindescripcion/puntuacion→ 201 con defaults (puntuacion0, estado pendiente);POST /api/check-duplicadodetecta por nombre normalizado (acentos/mayúsculas) + misma provincia; nombre igual en otra provincia y lejos → no duplicado;GET /api/localesarranca limpio - 9.2 Nominatim real (suite opt-in
npm run test:integration, auto-skip sin red, ≤1 req/s): búsqueda por dirección → coordenadas + provincia canónica; reverse de Madrid y Bilbao → provincia canónica; reverse fuera de España → provincia vacía