diff --git a/backend/tests/integration/test_comprobante.py b/backend/tests/integration/test_comprobante.py
index fff95e5..cca63c4 100644
--- a/backend/tests/integration/test_comprobante.py
+++ b/backend/tests/integration/test_comprobante.py
@@ -98,3 +98,28 @@ def test_comprobante_errores(client, url, cabeceras, status):
def test_numero_de_factura():
assert numero_factura(7) == "KEC-00000007"
+
+
+# ---------------------------------------------------------------- asientos en la factura
+
+
+def _con_asientos(*asientos):
+ base = _detalle()
+ return ReservaDetalle(base.reserva, base.vuelo, tuple(asientos))
+
+
+def test_la_factura_lista_los_asientos_de_la_reserva():
+ datos = armar_datos_factura(_con_asientos("1A", "2B"), ANA)
+ assert datos["detalles"][0]["asientos"] == "1A, 2B"
+ raiz = ElementTree.fromstring(generar_factura_xml(datos).encode("utf-8"))
+ producto = raiz.find("Detalle/producto")
+ assert [c.tag for c in producto] == ["Nombre", "Asientos", "Cantidad", "PrecioUnitario", "Subtotal"]
+ assert producto.findtext("Asientos") == "1A, 2B"
+
+
+def test_los_caracteres_especiales_del_cliente_no_rompen_el_xml():
+ cliente = un_usuario(id=1, nombre="Ana & Co');
+ });
+
+ test('los montos se leen en centavos exactos, sin decimales flotantes', () {
+ final f = Factura.desdeXml(facturaXml(unitario: '0.10', subtotal: '0.30', iva: '0.05', total: '0.35', cantidad: '3'));
+ expect((f.precioUnitarioCentavos, f.subtotalCentavos, f.ivaCentavos, f.totalCentavos), (10, 30, 5, 35));
+ });
+
+ test('todo lo que no sea una factura válida es InvalidResponseFailure', () {
+ for (final malo in [
+ '',
+ 'esto no es xml',
+ '',
+ '',
+ '',
+ '{"json": true}',
+ facturaXml(total: 'mucho'),
+ facturaXml(cantidad: 'dos'),
+ facturaXml(fecha: '11/11/2026'),
+ facturaXml(porcentaje: '1500'),
+ facturaXml(numero: ''),
+ facturaXml(correo: ''),
+ facturaXml().replaceAll('Confirmada', ''),
+ facturaXml().replaceAll('', '').replaceAll('', ''),
+ ']>&e;',
+ ]) {
+ expect(() => Factura.desdeXml(malo), throwsA(isA()), reason: malo.length > 60 ? malo.substring(0, 60) : malo);
+ }
+ });
+
+ test('un XML enorme no rompe el lector', () {
+ final grande = facturaXml(descripcion: 'Pasaje ${'x' * 200000}');
+ expect(Factura.desdeXml(grande).descripcion.length, greaterThan(200000));
+ });
+ });
+}
diff --git a/frontend/test/pages/admin_y_paginacion_test.dart b/frontend/test/pages/admin_y_paginacion_test.dart
index 066920d..12870a8 100644
--- a/frontend/test/pages/admin_y_paginacion_test.dart
+++ b/frontend/test/pages/admin_y_paginacion_test.dart
@@ -152,7 +152,7 @@ void main() {
group('panel de administración', () {
testWidgets('el cliente no ve el botón del panel y la ruta lo devuelve a la lista', (tester) async {
await _abrir(tester, {'GET /vuelos': sinVuelos});
- expect(find.byTooltip('Panel de administración'), findsNothing);
+ expect(find.text('Administración'), findsNothing);
_irA(tester, AppRoutes.admin);
await tester.pumpAndSettle();
@@ -163,7 +163,7 @@ void main() {
testWidgets('el administrador abre el panel y ve el IVA vigente y el historial', (tester) async {
await _abrir(tester, {'GET /vuelos': sinVuelos, 'GET /iva': respuestaIva}, usuario: usuarioAdmin);
- await tester.tap(find.byTooltip('Panel de administración'));
+ await tester.tap(find.text('Administración'));
await tester.pumpAndSettle();
expect(find.byType(AdminPage), findsOneWidget);
diff --git a/frontend/test/pages/comprobante_test.dart b/frontend/test/pages/comprobante_test.dart
new file mode 100644
index 0000000..bfbba52
--- /dev/null
+++ b/frontend/test/pages/comprobante_test.dart
@@ -0,0 +1,184 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:vuelos_app/config/app_router.dart';
+import 'package:vuelos_app/pages/comprobante_page.dart';
+import 'package:vuelos_app/pages/login_page.dart';
+import 'package:vuelos_app/pages/vuelos_page.dart';
+import 'package:vuelos_app/services/api_exception.dart';
+
+import '../helpers/app_helper.dart';
+import '../helpers/fakes.dart';
+
+Future _abrir(WidgetTester tester, Map rutas, {Object usuario = usuarioAna}) async {
+ final client = RouteFakeRestClient({
+ 'GET /auth/perfil': {'usuario': usuario},
+ 'GET /vuelos': respuestaVuelos,
+ 'GET /iva': respuestaIva,
+ 'GET /reservas/58': reservaJson(),
+ ...rutas,
+ });
+ await abrirApp(tester, client: client, tokens: InMemoryTokenStorage('jwt'));
+ return client;
+}
+
+Future _ir(WidgetTester tester, String ruta) async {
+ ProviderScope.containerOf(tester.element(find.byType(MaterialApp))).read(appRouterProvider).push(ruta);
+ await tester.pumpAndSettle();
+}
+
+void main() {
+ group('desde la reserva', () {
+ testWidgets('el botón "Ver comprobante" abre la factura', (tester) async {
+ await _abrir(tester, {'GET /reservas/58/comprobante': facturaXml()});
+ await _ir(tester, AppRoutes.reserva(58));
+
+ await tester.tap(find.text('Ver comprobante (factura XML)'));
+ await tester.pumpAndSettle();
+
+ expect(find.byType(ComprobantePage), findsOneWidget);
+ });
+
+ testWidgets('también se ofrece en una reserva cancelada (queda como anulada)', (tester) async {
+ await _abrir(tester, {
+ 'GET /reservas/58': reservaJson(estado: 'cancelada'),
+ 'GET /reservas/58/comprobante': facturaXml(estado: 'Cancelada'),
+ });
+ await _ir(tester, AppRoutes.reserva(58));
+ await tester.tap(find.text('Ver comprobante (factura XML)'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Anulada'), findsOneWidget);
+ });
+ });
+
+ group('la factura', () {
+ testWidgets('muestra número, fecha, cliente, asientos, montos e IVA', (tester) async {
+ await _abrir(tester, {'GET /reservas/58/comprobante': facturaXml()});
+ await _ir(tester, AppRoutes.comprobante(58));
+
+ expect(find.text('Factura KEC-00000058'), findsOneWidget);
+ expect(find.textContaining('Emitida el'), findsOneWidget);
+ expect(find.text('Ana Torres'), findsOneWidget);
+ expect(find.text('ana@correo.com'), findsOneWidget);
+ expect(find.textContaining('Pasaje KT101'), findsOneWidget);
+ expect(find.text('Asiento 1A'), findsOneWidget);
+ expect(find.text('Asiento 1B'), findsOneWidget);
+ expect(find.text(r'2 × $89.00'), findsOneWidget);
+ expect(find.text(r'$178.00'), findsWidgets);
+ expect(find.text('IVA (15 %)'), findsOneWidget);
+ expect(find.text(r'$26.70'), findsOneWidget);
+ expect(find.text(r'$204.70'), findsOneWidget);
+ expect(find.text('Confirmada'), findsOneWidget);
+ });
+
+ testWidgets('pide el comprobante a la ruta correcta', (tester) async {
+ final api = await _abrir(tester, {'GET /reservas/58/comprobante': facturaXml()});
+ await _ir(tester, AppRoutes.comprobante(58));
+ expect(api.rutasSolicitadas, contains('GET /reservas/58/comprobante'));
+ });
+
+ testWidgets('un nombre con caracteres raros se ve como texto, sin interpretarse', (tester) async {
+ await _abrir(tester, {'GET /reservas/58/comprobante': facturaXml(nombre: 'Ana <b>Negrita</b> & Co')});
+ await _ir(tester, AppRoutes.comprobante(58));
+ expect(find.text('Ana Negrita & Co'), findsOneWidget);
+ });
+ });
+
+ group('el panel Factura XML', () {
+ testWidgets('muestra el XML tal como lo entrega el servidor', (tester) async {
+ await _abrir(tester, {'GET /reservas/58/comprobante': facturaXml()});
+ await _ir(tester, AppRoutes.comprobante(58));
+
+ await tester.tap(find.text('Factura XML'));
+ await tester.pumpAndSettle();
+
+ expect(find.textContaining('', findRichText: true), findsOneWidget);
+ expect(find.textContaining('KEC-00000058', findRichText: true), findsOneWidget);
+ expect(find.text('Copiar XML'), findsOneWidget);
+ expect(find.text('Factura KEC-00000058'), findsNothing); // la vista legible ya no está
+ });
+
+ testWidgets('Copiar XML lo deja en el portapapeles y avisa', (tester) async {
+ String? copiado;
+ tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (llamada) async {
+ if (llamada.method == 'Clipboard.setData') copiado = (llamada.arguments as Map)['text'] as String?;
+ return null;
+ });
+ addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null));
+ final xml = facturaXml();
+ await _abrir(tester, {'GET /reservas/58/comprobante': xml});
+ await _ir(tester, AppRoutes.comprobante(58));
+ await tester.tap(find.text('Factura XML'));
+ await tester.pumpAndSettle();
+
+ await tester.tap(find.text('Copiar XML'));
+ await tester.pump();
+
+ expect(copiado, xml);
+ expect(find.text('XML copiado al portapapeles.'), findsOneWidget);
+ });
+
+ testWidgets('se puede volver a la vista de la factura', (tester) async {
+ await _abrir(tester, {'GET /reservas/58/comprobante': facturaXml()});
+ await _ir(tester, AppRoutes.comprobante(58));
+ await tester.tap(find.text('Factura XML'));
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('Factura').first);
+ await tester.pumpAndSettle();
+ expect(find.text('Factura KEC-00000058'), findsOneWidget);
+ });
+ });
+
+ group('cuando algo falla', () {
+ testWidgets('reserva ajena o inexistente (404): muestra el mensaje y permite reintentar', (tester) async {
+ var intento = 0;
+ await _abrir(tester, {
+ 'GET /reservas/58/comprobante': (_) => ++intento == 1 ? throw const ServerFailure('La reserva solicitada no existe.', statusCode: 404) : facturaXml(),
+ });
+ await _ir(tester, AppRoutes.comprobante(58));
+ expect(find.text('La reserva solicitada no existe.'), findsOneWidget);
+
+ await tester.tap(find.text('Reintentar'));
+ await tester.pumpAndSettle();
+ expect(find.text('Factura KEC-00000058'), findsOneWidget);
+ });
+
+ testWidgets('sin conexión: avisa y permite reintentar', (tester) async {
+ await _abrir(tester, {'GET /reservas/58/comprobante': const NetworkFailure()});
+ await _ir(tester, AppRoutes.comprobante(58));
+ expect(find.text('No se pudo conectar con el servidor.'), findsOneWidget);
+ expect(find.text('Reintentar'), findsOneWidget);
+ });
+
+ testWidgets('si el servidor manda algo que no es una factura, lo dice sin romperse', (tester) async {
+ await _abrir(tester, {'GET /reservas/58/comprobante': 'error de un proxy'});
+ await _ir(tester, AppRoutes.comprobante(58));
+ expect(find.text('El servidor envió una respuesta no válida.'), findsOneWidget);
+ expect(tester.takeException(), isNull);
+ });
+
+ testWidgets('un 401 cierra la sesión y lleva al login', (tester) async {
+ await _abrir(tester, {'GET /reservas/58/comprobante': const UnauthorizedFailure()});
+ await _ir(tester, AppRoutes.comprobante(58));
+ expect(find.byType(LoginPage), findsOneWidget);
+ });
+
+ testWidgets('el administrador no tiene comprobantes: vuelve a la lista', (tester) async {
+ final api = await _abrir(tester, {}, usuario: usuarioAdmin);
+ await _ir(tester, AppRoutes.comprobante(58));
+ expect(find.byType(VuelosPage), findsOneWidget);
+ expect(api.rutasSolicitadas.where((r) => r.contains('comprobante')), isEmpty);
+ });
+
+ testWidgets('ids absurdos en la URL no llegan al servidor', (tester) async {
+ final api = await _abrir(tester, {});
+ for (final id in ['0', 'abc', '99999999999999999999', '-1']) {
+ await _ir(tester, '/reservas/$id/comprobante');
+ expect(find.byType(ComprobantePage), findsNothing, reason: id);
+ }
+ expect(api.rutasSolicitadas.where((r) => r.contains('comprobante')), isEmpty);
+ });
+ });
+}
diff --git a/frontend/test/pages/navegacion_test.dart b/frontend/test/pages/navegacion_test.dart
new file mode 100644
index 0000000..34e42a6
--- /dev/null
+++ b/frontend/test/pages/navegacion_test.dart
@@ -0,0 +1,261 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:vuelos_app/config/app_router.dart';
+import 'package:vuelos_app/pages/admin_page.dart';
+import 'package:vuelos_app/pages/estado_api_page.dart';
+import 'package:vuelos_app/pages/login_page.dart';
+import 'package:vuelos_app/pages/reserva_detalle_page.dart';
+import 'package:vuelos_app/pages/reservas_page.dart';
+import 'package:vuelos_app/pages/seleccion_asientos_page.dart';
+import 'package:vuelos_app/pages/vuelo_detalle_page.dart';
+import 'package:vuelos_app/pages/vuelos_page.dart';
+import 'package:vuelos_app/widgets/migas.dart';
+
+import '../helpers/app_helper.dart';
+import '../helpers/fakes.dart';
+
+Future _abrir(WidgetTester tester, {Object usuario = usuarioAna, Map rutas = const {}}) async {
+ final client = RouteFakeRestClient({
+ 'GET /auth/perfil': {'usuario': usuario},
+ 'GET /vuelos': respuestaVuelos,
+ 'GET /vuelos/3': vueloQuitoGuayaquil,
+ 'GET /vuelos/3/asientos': mapaJson(),
+ 'GET /iva': respuestaIva,
+ 'GET /reservas': paginaDeReservas(total: 1, pagina: 1),
+ 'GET /reservas/58': reservaJson(),
+ 'GET /health': healthOk,
+ ...rutas,
+ });
+ await abrirApp(tester, client: client, tokens: InMemoryTokenStorage('jwt'));
+ return client;
+}
+
+Future _ir(WidgetTester tester, String ruta) async {
+ ProviderScope.containerOf(tester.element(find.byType(MaterialApp))).read(appRouterProvider).push(ruta);
+ await tester.pumpAndSettle();
+}
+
+Finder get _barraInferior => find.byType(NavigationBar);
+
+void main() {
+ group('barra de navegación inferior', () {
+ testWidgets('el cliente ve Vuelos y Mis reservas (y no Administración)', (tester) async {
+ await _abrir(tester);
+ expect(find.descendant(of: _barraInferior, matching: find.text('Vuelos')), findsOneWidget);
+ expect(find.descendant(of: _barraInferior, matching: find.text('Mis reservas')), findsOneWidget);
+ expect(find.descendant(of: _barraInferior, matching: find.text('Administración')), findsNothing);
+ });
+
+ testWidgets('el administrador ve Vuelos y Administración (y no Mis reservas)', (tester) async {
+ await _abrir(tester, usuario: usuarioAdmin);
+ expect(find.descendant(of: _barraInferior, matching: find.text('Vuelos')), findsOneWidget);
+ expect(find.descendant(of: _barraInferior, matching: find.text('Administración')), findsOneWidget);
+ expect(find.descendant(of: _barraInferior, matching: find.text('Mis reservas')), findsNothing);
+ });
+
+ testWidgets('resalta la sección en la que está el usuario', (tester) async {
+ await _abrir(tester);
+ expect(tester.widget(_barraInferior).selectedIndex, 0);
+ await _ir(tester, AppRoutes.reserva(58));
+ expect(tester.widget(_barraInferior).selectedIndex, 1); // el detalle de una reserva es de "Mis reservas"
+ });
+
+ testWidgets('desde un detalle profundo se llega a cualquier sección con un toque', (tester) async {
+ await _abrir(tester);
+ await _ir(tester, AppRoutes.reserva(58));
+ expect(find.byType(ReservaDetallePage), findsOneWidget);
+
+ await tester.tap(find.descendant(of: _barraInferior, matching: find.text('Vuelos')));
+ await tester.pumpAndSettle();
+ expect(find.byType(VuelosPage), findsOneWidget);
+
+ await tester.tap(find.descendant(of: _barraInferior, matching: find.text('Mis reservas')));
+ await tester.pumpAndSettle();
+ expect(find.byType(ReservasPage), findsOneWidget);
+ });
+
+ testWidgets('el administrador va a su panel desde la barra', (tester) async {
+ await _abrir(tester, usuario: usuarioAdmin);
+ await tester.tap(find.descendant(of: _barraInferior, matching: find.text('Administración')));
+ await tester.pumpAndSettle();
+ expect(find.byType(AdminPage), findsOneWidget);
+ });
+
+ testWidgets('el mapa de asientos NO tiene barra inferior (es un paso a medio camino, con su propio resumen)', (tester) async {
+ await _abrir(tester);
+ await _ir(tester, AppRoutes.asientosDelVuelo(3));
+ expect(find.byType(SeleccionAsientosPage), findsOneWidget);
+ expect(_barraInferior, findsNothing);
+ });
+ });
+
+ group('inicio desde cualquier parte', () {
+ testWidgets('tocar el logo lleva al inicio', (tester) async {
+ await _abrir(tester);
+ await _ir(tester, AppRoutes.reserva(58));
+ expect(find.byType(VuelosPage), findsNothing);
+
+ await tester.tap(find.byTooltip('Ir al inicio'));
+ await tester.pumpAndSettle();
+ expect(find.byType(VuelosPage), findsOneWidget);
+ });
+
+ testWidgets('también desde el mapa de asientos', (tester) async {
+ await _abrir(tester);
+ await _ir(tester, AppRoutes.asientosDelVuelo(3));
+ await tester.tap(find.byTooltip('Ir al inicio'));
+ await tester.pumpAndSettle();
+ expect(find.byType(VuelosPage), findsOneWidget);
+ });
+
+ testWidgets('el botón Volver regresa a la pantalla anterior', (tester) async {
+ await _abrir(tester);
+ await tester.tap(find.text('KT101'));
+ await tester.pumpAndSettle();
+ expect(find.byType(VueloDetallePage), findsOneWidget);
+
+ await tester.tap(find.byTooltip('Volver'));
+ await tester.pumpAndSettle();
+ expect(find.byType(VuelosPage), findsOneWidget);
+ });
+ });
+
+ group('migas de pan', () {
+ testWidgets('en el detalle del vuelo: Vuelos › KT101, y "Vuelos" regresa', (tester) async {
+ await _abrir(tester);
+ await tester.tap(find.text('KT101'));
+ await tester.pumpAndSettle();
+ expect(find.byType(Migas), findsOneWidget);
+ expect(find.descendant(of: find.byType(Migas), matching: find.text('KT101')), findsOneWidget);
+
+ await tester.tap(find.descendant(of: find.byType(Migas), matching: find.text('Vuelos')));
+ await tester.pumpAndSettle();
+ expect(find.byType(VuelosPage), findsOneWidget);
+ });
+
+ testWidgets('en una reserva: Mis reservas › KEC-…, y "Mis reservas" regresa', (tester) async {
+ await _abrir(tester);
+ await _ir(tester, AppRoutes.reserva(58));
+ expect(find.descendant(of: find.byType(Migas), matching: find.text('KEC-00000058')), findsOneWidget);
+
+ await tester.tap(find.descendant(of: find.byType(Migas), matching: find.text('Mis reservas')));
+ await tester.pumpAndSettle();
+ expect(find.byType(ReservasPage), findsOneWidget);
+ });
+
+ testWidgets('en el mapa de asientos se puede volver al vuelo desde las migas', (tester) async {
+ await _abrir(tester);
+ await _ir(tester, AppRoutes.asientosDelVuelo(3));
+ await tester.tap(find.descendant(of: find.byType(Migas), matching: find.text('KT101')));
+ await tester.pumpAndSettle();
+ expect(find.byType(VueloDetallePage), findsOneWidget);
+ });
+ });
+
+ group('menú de cuenta', () {
+ testWidgets('muestra quién está conectado y su rol', (tester) async {
+ await _abrir(tester);
+ await tester.tap(find.byWidgetPredicate((w) => w is PopupMenuButton));
+ await tester.pumpAndSettle();
+ expect(find.text('Ana Torres'), findsOneWidget);
+ expect(find.text('ana@correo.com'), findsOneWidget);
+ expect(find.text('Cliente'), findsOneWidget);
+ });
+
+ testWidgets('el administrador figura como Administrador', (tester) async {
+ await _abrir(tester, usuario: usuarioAdmin);
+ await tester.tap(find.byWidgetPredicate((w) => w is PopupMenuButton));
+ await tester.pumpAndSettle();
+ expect(find.text('Administrador'), findsOneWidget);
+ });
+
+ testWidgets('lleva al estado del servicio', (tester) async {
+ await _abrir(tester);
+ await tester.tap(find.byWidgetPredicate((w) => w is PopupMenuButton));
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('Estado del servicio'));
+ await tester.pumpAndSettle();
+ expect(find.byType(EstadoApiPage), findsOneWidget);
+ });
+
+ testWidgets('cerrar sesión pide confirmación: "Seguir conectado" la conserva', (tester) async {
+ final api = await _abrir(tester);
+ await tester.tap(find.byWidgetPredicate((w) => w is PopupMenuButton));
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('Cerrar sesión'));
+ await tester.pumpAndSettle();
+ expect(find.text('¿Cerrar sesión?'), findsOneWidget);
+
+ await tester.tap(find.text('Seguir conectado'));
+ await tester.pumpAndSettle();
+ expect(find.byType(VuelosPage), findsOneWidget);
+ expect(api.llamadas.where((l) => l.ruta == '/auth/logout'), isEmpty);
+ });
+ });
+
+ group('salir del mapa de asientos con cambios', () {
+ Future elegir(WidgetTester tester, String asiento) async {
+ await tester.tap(find.text(asiento));
+ await tester.pump();
+ }
+
+ testWidgets('sin asientos elegidos, "Volver" sale sin preguntar', (tester) async {
+ await _abrir(tester);
+ await _ir(tester, AppRoutes.asientosDelVuelo(3));
+ await tester.tap(find.byTooltip('Volver'));
+ await tester.pumpAndSettle();
+ expect(find.byType(SeleccionAsientosPage), findsNothing);
+ expect(find.text('¿Salir sin guardar?'), findsNothing);
+ });
+
+ testWidgets('con asientos elegidos pregunta antes de salir; "Seguir eligiendo" los conserva', (tester) async {
+ await _abrir(tester);
+ await _ir(tester, AppRoutes.asientosDelVuelo(3));
+ await elegir(tester, '1A');
+
+ await tester.tap(find.byTooltip('Volver'));
+ await tester.pumpAndSettle();
+ expect(find.text('¿Salir sin guardar?'), findsOneWidget);
+
+ await tester.tap(find.text('Seguir eligiendo'));
+ await tester.pumpAndSettle();
+ expect(find.byType(SeleccionAsientosPage), findsOneWidget);
+ expect(find.text('Asientos (1 de 10): 1A'), findsOneWidget);
+ });
+
+ testWidgets('con asientos elegidos, "Salir" abandona la selección', (tester) async {
+ await _abrir(tester);
+ await _ir(tester, AppRoutes.asientosDelVuelo(3));
+ await elegir(tester, '1A');
+ await tester.tap(find.byTooltip('Volver'));
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('Salir'));
+ await tester.pumpAndSettle();
+ expect(find.byType(SeleccionAsientosPage), findsNothing);
+ });
+
+ testWidgets('al cambiar los asientos de una reserva también avisa si hay cambios sin guardar', (tester) async {
+ await _abrir(tester, rutas: {'GET /vuelos/3/asientos': mapaJson(ocupados: ['1A', '1B'])});
+ await _ir(tester, AppRoutes.asientosDeLaReserva(58));
+ await tester.tap(find.byTooltip('Volver'));
+ await tester.pumpAndSettle();
+ expect(find.text('¿Salir sin guardar?'), findsNothing); // sin cambios: sale directo
+
+ await _ir(tester, AppRoutes.asientosDeLaReserva(58));
+ await elegir(tester, '2A');
+ await tester.tap(find.byTooltip('Volver'));
+ await tester.pumpAndSettle();
+ expect(find.text('¿Salir sin guardar?'), findsOneWidget);
+ expect(find.text('Los cambios en sus asientos no se guardarán.'), findsOneWidget);
+ });
+ });
+
+ testWidgets('sin sesión no hay barra de navegación ni menú (login)', (tester) async {
+ final client = RouteFakeRestClient({});
+ await abrirApp(tester, client: client);
+ expect(find.byType(LoginPage), findsOneWidget);
+ expect(_barraInferior, findsNothing);
+ expect(find.byWidgetPredicate((w) => w is PopupMenuButton), findsNothing);
+ });
+}
diff --git a/frontend/test/pages/seleccion_asientos_test.dart b/frontend/test/pages/seleccion_asientos_test.dart
index cad3b27..6b17a98 100644
--- a/frontend/test/pages/seleccion_asientos_test.dart
+++ b/frontend/test/pages/seleccion_asientos_test.dart
@@ -43,7 +43,7 @@ void main() {
await _ir(tester, AppRoutes.asientosDelVuelo(3));
expect(find.byType(SeleccionAsientosPage), findsOneWidget);
- expect(find.text('KT101'), findsOneWidget);
+ expect(find.text('KT101'), findsWidgets); // en las migas y en la cabecera
expect(find.text('Disponible'), findsOneWidget);
expect(find.text('Seleccionado'), findsOneWidget);
expect(find.text('Ocupado'), findsOneWidget);
diff --git a/frontend/test/pages/vuelos_page_test.dart b/frontend/test/pages/vuelos_page_test.dart
index 7194809..be609f9 100644
--- a/frontend/test/pages/vuelos_page_test.dart
+++ b/frontend/test/pages/vuelos_page_test.dart
@@ -137,7 +137,7 @@ void main() {
expect(find.text('42 libres de 80'), findsOneWidget);
expect(find.text('Precio por persona'), findsOneWidget);
- await tester.tap(find.byTooltip('Volver a los vuelos'));
+ await tester.tap(find.byTooltip('Volver'));
await tester.pumpAndSettle();
expect(find.byType(VuelosPage), findsOneWidget);
});