Saltar al contenido
Development

Siete arreglos pequeños en Deep Cut Atlas, y un test que casi se borra

Por Victor Da Luz
swiftiostestingdev-logdeep-cut-atlas

Pasé una sesión entera avanzando por el backlog de Deep Cut Atlas en orden de tamaño, lo más pequeño y seguro primero, fusionando cada arreglo a main antes de empezar el siguiente. Se resolvieron siete issues: un ícono equivocado en un estado vacío, unos chevrons sueltos que no llevaban a ningún lado, portadas que no respetaban Dynamic Type, una sección de Configuración en el orden equivocado, un chip de filtro que no parecía tocable, tres pequeñas inconsistencias de texto, y un error real en cómo una hoja de detalle verificaba si el álbum ya estaba en la biblioteca. La mayor parte fue mecánica. Dos de esos arreglos dejaron una lección que vale la pena anotar.

Las portadas de tamaño fijo no escalan con el texto

Ocho vistas de esta app muestran la portada de un álbum junto a un título y un nombre de artista. Las ocho usaban un simple .frame(width: 56, height: 56) (o 48, o 72, según la vista). Eso funciona bien con el tamaño de texto por defecto. Al activar un ajuste de Dynamic Type más grande en Accesibilidad, el texto crece, pero la portada se queda exactamente del mismo tamaño, así que una pared de texto termina junto a una estampilla.

El arreglo es un cambio de una línea por vista: cambiar el literal del frame por una propiedad @ScaledMetric sembrada con ese mismo literal.

@ScaledMetric private var artworkSize: CGFloat = 56
...
.frame(width: artworkSize, height: artworkSize)

@ScaledMetric lee la categoría actual de Dynamic Type y escala el valor automáticamente, así que en el tamaño por defecto se renderiza exactamente igual que antes (sin riesgo de regresión) y en tamaños más grandes agranda la portada de forma proporcional al texto que tiene al lado. Esto se verificó con xcrun simctl ui <udid> content_size accessibility-extra-extra-extra-large, que cambia el simulador al tamaño de texto de accesibilidad más grande sin tocar la interfaz de la app Configuración, y después se tomó una captura. La portada creció visiblemente junto al título y al nombre del artista en las ocho vistas.

El test que protegía en silencio la cosa equivocada

Este llevó el resto de la sesión. La hoja de detalle de una canción muestra “already in your library” cuando ese álbum ya fue agregado, y deshabilita el botón de agregar. La verificación que decidía esto, isAlbumInLibrary, era un let guardado que se pasaba al inicializador del view model, congelado en el momento en que se construía la hoja. Si el escaneo único de toda la biblioteca de la app todavía estaba corriendo cuando se abría la hoja, la hoja creía para siempre que el álbum no era propio, incluso después de que el escaneo terminara y dijera lo contrario. El mismo tipo de error que se había arreglado antes en la lista de sugerencias, solo que en una propiedad distinta.

El arreglo sigue el mismo patrón: convertir el valor guardado en una propiedad computada que lee en vivo del view model padre.

var isAlbumInLibrary: Bool {
    history.isAlbumInLibrary(track)
}

Sin parámetro de init, sin instantánea, sin forma de que quede desactualizado. Como es computada y lee una propiedad @Observable, SwiftUI vuelve a renderizar la hoja automáticamente cada vez que cambia la respuesta del padre.

Ese cambio rompió un test existente. Ese test construía el view model directamente y pasaba isAlbumInLibrary: false a mano para simular un escenario específico, uno donde el álbum termina siendo propio a mitad de un agregado en curso. Una vez que la propiedad dejó de ser un argumento del constructor, ese atajo no tenía dónde ir, y el movimiento obvio era simplemente borrar la aserción, ya que el código que probaba ya no existía en esa forma.

Antes de hacer eso se leyó el comentario propio del test, y describía una carrera real que seguía existiendo después del arreglo, solo que más angosta: el escaneo de biblioteca termina en la ventana exacta entre el momento en que el botón de agregar decide que es seguro tocarlo y la propia reverificación de la operación de agregar sobre esos mismos datos en caché unas líneas después, más adentro de una cadena de llamadas async. El arreglo cambió cómo se podía llegar a esa ventana. No la cerró.

Así que en vez de borrar el test, se reconstruyó. Necesitaba dos puntos de bloqueo abiertos a la vez en lugar de uno: el escaneo de biblioteca (ya simulable) y la propia búsqueda de playlist del flujo de agregado, que no tenía ningún gancho para pausarla, así que se agregó uno. Con ambos controlados, el test detiene el escaneo a mitad de vuelo, abre la hoja y confirma que el botón se lee como agregable, inicia el agregado, lo detiene en su propio await, libera el escaneo para que la respuesta real quede disponible, y después libera el agregado y verifica que reporte “already in your library,” no “added.” Determinista, no un test de esperar-y-tener-suerte.

La lección que quedó de esto, y que se anotó como nota de patrón en la base de conocimiento: cuando un arreglo rompe un test, hay que leer qué dice el comentario de ese test que está protegiendo antes de tocarlo. Si el escenario que nombra sigue siendo alcanzable, el arreglo cambió el camino para llegar ahí, no si el error todavía es posible. Hay que reconstruir la reproducción contra el nuevo mecanismo. No bajar en silencio la cobertura solo porque desapareció la vieja forma de dispararlo.

El resto, en breve

El estado vacío de “Not Interested” usaba un ícono de pulgar arriba; ahora es pulgar abajo. Se quitó un chevron de dos filas de lista que no navegaban a ningún lado al tocarlas. Se movió la sección de filtros por defecto en Configuración debajo de las secciones que la gente realmente cambia día a día. Los chips de filtro se veían igual estuvieran activos o no, se agregó una marca de verificación para el estado activo, y se le dio al filtro “Compilation” su nombre completo en vez de mostrar la misma abreviación “Comp” que usa la insignia. Y tres pequeños arreglos de texto: un mensaje de playlist vacía que ahora dice algo específico en vez de un “nothing here” genérico, y dos botones “Add to Playlist” renombrados para que coincidan con el nombre real de la función, “To Check Out.”

Lecturas relacionadas