[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"technologies":3,"\u002Fblog\u002Fflutter-golden-tests":167,"team-members":2508,"blog-topics":2849,"mdc--3lbxge-key":2891,"glossary":2933},[4,10,15,21,26,32,38,43,48,53,58,63,68,73,77,82,87,92,97,103,108,113,118,123,128,133,138,143,147,152,157,162],{"title":5,"description":6,"slug":7,"category":8,"icon":9},"Android","Todas las apps que entregamos llegan a Android: APIs de plataforma, ejecución en segundo plano, permisos y los requisitos de tienda que traen consigo.","android","framework","logos:android-icon",{"title":11,"description":12,"slug":13,"category":8,"icon":14},"Angular","El framework de frontend en el que trabajamos cuando un producto ya es Angular: una estructura con todo incluido que encaja en apps grandes y de vida larga.","angular","logos:angular-icon",{"title":16,"description":17,"slug":18,"category":19,"icon":20},"Dart","El lenguaje en el que está escrita cada app Flutter que entregamos: null safety robusto, pattern matching de verdad y un compilador que apunta a nativo y a la web.","dart","language","logos:dart",{"title":22,"description":23,"slug":24,"category":8,"icon":25},"Django","El framework de Python que usamos cuando un producto necesita administración, autenticación y un modelo de datos real desde el primer día.","django","logos:django-icon",{"title":27,"description":28,"slug":29,"category":30,"icon":31},"Docker","Cada servicio que construimos se publica como contenedor, así que lo que corre en un portátil, en CI y en producción es un solo artefacto y no tres.","docker","infrastructure","logos:docker-icon",{"title":33,"description":34,"slug":35,"category":36,"icon":37},"Fastlane","La automatización de releases móviles: firma, builds y subidas a las tiendas desde CI, no desde el portátil de un ingeniero el día del lanzamiento.","fastlane","tools","logos:fastlane",{"title":39,"description":40,"slug":41,"category":8,"icon":42},"Flutter","Nuestro stack móvil principal desde 2018: una base de código Dart que llega a iOS, Android, web y escritorio sin un equipo por plataforma.","flutter","logos:flutter",{"title":44,"description":45,"slug":46,"category":36,"icon":47},"Git","Todos los proyectos que tocamos viven en Git: pull requests revisadas, CI en cada rama y un historial que sigue teniendo sentido meses después.","git","logos:git-icon",{"title":49,"description":50,"slug":51,"category":19,"icon":52},"Go","Nuestro lenguaje de backend para APIs en tiempo real y servicios bajo carga: binarios pequeños, compilaciones rápidas y concurrencia que sigue siendo legible.","go","logos:gopher",{"title":54,"description":55,"slug":56,"category":30,"icon":57},"Google Cloud","La nube donde corren nuestras cargas de producción: Kubernetes gestionado, almacenamiento y red, sin servidores hechos a mano que nadie quiere mantener.","gcp","logos:google-cloud",{"title":59,"description":60,"slug":61,"category":36,"icon":62},"Gradle","El sistema de build por el que pasa cada release de Android: product flavours, configuraciones de firma y el cableado de dependencias bajo una app Flutter.","gradle","logos:gradle",{"title":64,"description":65,"slug":66,"category":30,"icon":67},"Helm","Cómo empaquetamos un despliegue de Kubernetes: servicio, configuración, secretos e ingress como una unidad versionada que se puede promover y revertir.","helm","logos:helm",{"title":69,"description":70,"slug":71,"category":19,"icon":72},"Kotlin","A lo que recurrimos cuando una app Flutter necesita Android de verdad por debajo: platform channels, trabajo en segundo plano e integraciones con SDK nativos.","kotlin","logos:kotlin-icon",{"title":74,"description":75,"slug":76,"category":8,"icon":72},"Kotlin Multiplatform","Compartir la lógica de negocio entre iOS y Android mientras cada plataforma conserva su UI nativa: la alternativa cuando Flutter no encaja.","kmp",{"title":78,"description":79,"slug":80,"category":30,"icon":81},"Kubernetes","Cómo ejecutamos servicios en producción: despliegues con Helm, rollouts reversibles y escalado que no necesita a nadie a las 3 de la mañana.","kubernetes","logos:kubernetes",{"title":83,"description":84,"slug":85,"category":30,"icon":86},"NATS","Mensajería ligera entre servicios: publicación\u002Fsuscripción y petición\u002Frespuesta sin el peso operativo de un clúster de broker completo.","nats","logos:nats-icon",{"title":88,"description":89,"slug":90,"category":8,"icon":91},"Nuxt","Vue con renderizado en servidor, enrutado y SEO resueltos: así hacemos sitios y apps web que deben ser rápidas e indexables en la primera carga.","nuxt","logos:nuxt-icon",{"title":93,"description":94,"slug":95,"category":19,"icon":96},"PHP","Donde trabajamos con un backend PHP ya existente: extenderlo, integrarse con él y construir los clientes móviles y web a los que tiene que servir.","php","logos:php",{"title":98,"description":99,"slug":100,"category":101,"icon":102},"PostgreSQL","Nuestra base de datos por defecto, salvo que un producto dé una razón concreta para no usarla: del diseño del esquema al ajuste de índices.","postgres","database","logos:postgresql",{"title":104,"description":105,"slug":106,"category":19,"icon":107},"Python","Nuestro lenguaje para backends, trabajo con datos e integraciones de IA, incluidos los bindings de Python que publicamos para nuestras propias herramientas en Rust.","python","logos:python",{"title":109,"description":110,"slug":111,"category":8,"icon":112},"React","La librería de frontend en la que trabajamos cuando un producto ya es React: componentes, hooks y el ecosistema que ha crecido a su alrededor.","react","logos:react",{"title":114,"description":115,"slug":116,"category":101,"icon":117},"Redis","Donde ponemos los datos que tienen que ser rápidos y se pueden reconstruir: cachés, sesiones, límites de tasa y las colas tras las rutas lentas de un producto.","redis","logos:redis",{"title":119,"description":120,"slug":121,"category":19,"icon":122},"Ruby","El lenguaje de nuestra automatización de releases móviles: lanes de Fastlane, acciones a medida y el pegamento de CI que sube builds a las tiendas.","ruby","logos:ruby",{"title":124,"description":125,"slug":126,"category":19,"icon":127},"Rust","A donde vamos cuando importan tanto el rendimiento como la corrección: renderizado de documentos, herramientas de CLI y servicios que tienen que ser rápidos y predecibles.","rust","simple-icons:rust",{"title":129,"description":130,"slug":131,"category":101,"icon":132},"SQLite","La base de datos que viaja dentro de la app: cachés locales, almacenamiento offline-first y todo lo que tiene que seguir funcionando sin red.","sqlite","logos:sqlite",{"title":134,"description":135,"slug":136,"category":8,"icon":137},"Strapi","Un CMS headless para cuando los editores necesitan ser dueños del contenido: una administración de verdad y una API limpia, sin construirlas desde cero.","strapi","logos:strapi-icon",{"title":139,"description":140,"slug":141,"category":19,"icon":142},"Swift","A lo que recurrimos cuando una app Flutter necesita iOS de verdad por debajo: platform channels, integraciones con SDK nativos, widgets y App Clips.","swift","logos:swift",{"title":144,"description":145,"slug":146,"category":8,"icon":142},"SwiftUI","El framework de UI de Apple, donde construimos las superficies nativas que una app Flutter no puede tener: widgets, App Clips y extensiones.","swiftui",{"title":148,"description":149,"slug":150,"category":8,"icon":151},"Tailwind CSS","Cómo damos estilo a cada frontend que construimos: clases de utilidad y design tokens en lugar de una hoja de estilos que solo crece y nunca se borra.","tailwind","logos:tailwindcss-icon",{"title":153,"description":154,"slug":155,"category":19,"icon":156},"TypeScript","Lo predeterminado para todo lo que escribimos para el navegador y para servicios Node: tipos que detectan errores de integración antes de llegar a una revisión.","typescript","logos:typescript-icon",{"title":158,"description":159,"slug":160,"category":8,"icon":161},"Vue","Nuestro framework de frontend para paneles de administración, cuadros de mando y apps web que deben seguir siendo mantenibles durante años.","vue","logos:vue",{"title":163,"description":164,"slug":165,"category":36,"icon":166},"Whisper","El modelo de reconocimiento de voz de código abierto de OpenAI: el motor de transcripción detrás de la entrada por voz, que funciona como servicio de backend y también en el propio dispositivo.","whisper","simple-icons:openai",{"id":168,"title":169,"author":170,"body":171,"description":177,"extension":2492,"hero":2493,"meta":2496,"metaDescription":2497,"metaTitle":2498,"navigation":360,"ogDescription":2499,"path":2500,"seo":2501,"slug":2502,"stem":2503,"summary":2504,"timestamp":2505,"topic":41,"updated":2506,"__hash__":2507},"blog_es\u002Fblog\u002Fflutter-golden-tests.md","Tus golden tests pasan por el motivo equivocado","maxim",{"type":172,"value":173,"toc":2472},"minimark",[174,178,181,189,198,203,274,278,281,426,442,445,463,473,477,482,492,499,825,836,843,847,864,875,879,917,920,924,946,976,983,992,1106,1109,1117,1121,1124,1142,1151,1157,1209,1212,1222,1748,1778,1787,1791,1794,1871,1874,1877,1897,1900,1904,1907,1920,1926,2156,2171,2177,2181,2184,2187,2228,2234,2238,2245,2257,2261,2264,2267,2271,2431,2435,2439,2452,2468],[175,176,177],"p",{},"La última suite de goldens que heredamos tenía 214 imágenes. Todo en verde, durante unos ocho meses, a través de varios cientos de pull requests. Hasta que alguien abrió uno de los PNG para ver qué protegía.",[175,179,180],{},"Cada glifo era un rectángulo negro.",[175,182,183,184,188],{},"La suite se había escrito copiando el primer tutorial que apareció, y eso no es una crítica: el tutorial funciona, en el sentido de que ",[185,186,187],"code",{},"flutter test --update-goldens"," produce archivos y la siguiente ejecución los compara. Solo que compara lo que no es: una maquetación de cajas rellenas, con las sombras aplanadas en bloques duros, en una máquina que nadie había fijado. Cambia una palabra por otra de la misma longitud, cambia la familia tipográfica, y los píxeles no se mueven. La suite no estaba midiendo la interfaz. Estaba midiendo que nada se había movido.",[175,190,191,192,197],{},"Ese es el problema completo de los ",[193,194,196],"term",{"slug":195},"golden-test","golden tests"," en un ejemplo: son a la vez la red de regresión más barata de Flutter y la falsificación más convincente. Una marca verde se ve idéntica en los dos casos.",[199,200,202],"h2",{"id":201},"lo-esencial","Lo esencial",[204,205,206,214,220,230,236,245,258,264],"ul",{},[207,208,209,213],"li",{},[210,211,212],"strong",{},"Un golden es una instantánea del árbol de render a un tamaño lógico, un tema, un idioma y una escala de texto — y nada más."," No es una captura de tu app en un teléfono.",[207,215,216,219],{},[210,217,218],{},"El entorno de test no trae las fuentes de la app."," El texto sin familia disponible se dibuja como cajas sólidas, a propósito, para que la maquetación sea determinista. Sin cargar fuentes estás haciendo golden tests de geometría y llamándolo test visual.",[207,221,222,225,226,229],{},[210,223,224],{},"Las sombras se aplanan en bloques duros"," (",[185,227,228],{},"debugDisableShadows","): el framework descarta el desenfoque. La elevación sigue en la imagen, pero el radio de desenfoque, la propagación y el degradado no — así que las regresiones de sombra son invisibles.",[207,231,232,235],{},[210,233,234],{},"Los píxeles dependen del motor, del sistema anfitrión y de la versión de Flutter."," Una suite generada en macOS falla en CI Linux. Fija el toolchain y genera en la misma plataforma donde corre CI.",[207,237,238,244],{},[210,239,240,243],{},[185,241,242],{},"tester.view"," es la superficie, no el dispositivo."," La superficie por defecto son 800×600 píxeles lógicos con una relación de píxeles de 3; sin fijar un tamaño, cada pantalla de móvil se renderiza a una resolución que ningún usuario tiene.",[207,246,247,253,254,257],{},[210,248,249,252],{},[185,250,251],{},"pumpAndSettle"," se cuelga con un spinner"," — un indicador de progreso indeterminado programa fotogramas para siempre. Los estados de carga necesitan ",[185,255,256],{},"pump(duration)",".",[207,259,260,263],{},[210,261,262],{},"La matriz se multiplica."," 12 pantallas × 2 temas × 3 anchos × 2 escalas de texto × 2 idiomas son 288 imágenes, todas regeneradas en cada actualización de Flutter. Elige los ejes que de verdad vas a revisar.",[207,265,266,269,270,273],{},[210,267,268],{},"El valor de una suite de goldens es la probabilidad de que alguien mire el diff."," Todo lo demás es fontanería. Un commit con ",[185,271,272],{},"--update-goldens"," que nadie revisó convierte un test en un archivo de bloqueo.",[199,275,277],{"id":276},"qué-hace-realmente-un-golden-test","Qué hace realmente un golden test",[175,279,280],{},"Un test, una imagen, una comparación:",[282,283,287],"pre",{"className":284,"code":285,"language":18,"meta":286,"style":286},"language-dart shiki shiki-themes material-theme-lighter github-light github-dark","testWidgets('cart is empty', (tester) async {\n  await pumpScreen(tester, const CartScreen(items: []));\n\n  await expectLater(\n    find.byType(CartScreen),\n    matchesGoldenFile('goldens\u002Fcart_empty.light.390.ts1.0.en.png'),\n  );\n});\n","",[185,288,289,320,355,362,373,395,410,418],{"__ignoreMap":286},[290,291,294,298,302,306,310,313,317],"span",{"class":292,"line":293},"line",1,[290,295,297],{"class":296},"sGLFI","testWidgets",[290,299,301],{"class":300},"su5hD","(",[290,303,305],{"class":304},"s_sjI","'cart is empty'",[290,307,309],{"class":308},"sP7_E",",",[290,311,312],{"class":300}," (tester) ",[290,314,316],{"class":315},"sVHd0","async",[290,318,319],{"class":300}," {\n",[290,321,323,326,329,332,334,338,342,345,349,352],{"class":292,"line":322},2,[290,324,325],{"class":315},"  await",[290,327,328],{"class":296}," pumpScreen",[290,330,331],{"class":300},"(tester",[290,333,309],{"class":308},[290,335,337],{"class":336},"sbsja"," const",[290,339,341],{"class":340},"sZMiF"," CartScreen",[290,343,344],{"class":300},"(items",[290,346,348],{"class":347},"smGrS",":",[290,350,351],{"class":300}," []))",[290,353,354],{"class":308},";\n",[290,356,358],{"class":292,"line":357},3,[290,359,361],{"emptyLinePlaceholder":360},true,"\n",[290,363,365,367,370],{"class":292,"line":364},4,[290,366,325],{"class":315},[290,368,369],{"class":296}," expectLater",[290,371,372],{"class":300},"(\n",[290,374,376,379,381,384,386,389,392],{"class":292,"line":375},5,[290,377,378],{"class":300},"    find",[290,380,257],{"class":308},[290,382,383],{"class":296},"byType",[290,385,301],{"class":300},[290,387,388],{"class":340},"CartScreen",[290,390,391],{"class":300},")",[290,393,394],{"class":308},",\n",[290,396,398,401,403,406,408],{"class":292,"line":397},6,[290,399,400],{"class":296},"    matchesGoldenFile",[290,402,301],{"class":300},[290,404,405],{"class":304},"'goldens\u002Fcart_empty.light.390.ts1.0.en.png'",[290,407,391],{"class":300},[290,409,394],{"class":308},[290,411,413,416],{"class":292,"line":412},7,[290,414,415],{"class":300},"  )",[290,417,354],{"class":308},[290,419,421,424],{"class":292,"line":420},8,[290,422,423],{"class":300},"})",[290,425,354],{"class":308},[175,427,428,430,431,434,435,438,439,257],{},[185,429,187],{}," escribe ese PNG y pasa. Cada ejecución posterior lo decodifica y lo compara píxel a píxel con lo que acaba de renderizar el motor. Sin tolerancia ni hash perceptual: igualdad exacta, hasta que instales un ",[185,432,433],{},"goldenFileComparator"," propio. Cuando falla, el comparador por defecto deja cuatro artefactos en un directorio ",[185,436,437],{},"failures\u002F"," junto al archivo de test: la referencia, el render nuevo, un diff aislado y un diff enmascarado. (Si los tamaños no coinciden escribe solo los dos primeros.) Esos son tu revisión del diff, y su sitio es ",[185,440,441],{},".gitignore",[175,443,444],{},"Dos detalles de ese fragmento importan más de lo que parece.",[175,446,447,455,456,459,460,462],{},[210,448,449,452,453,257],{},[185,450,451],{},"find.byType(CartScreen)"," no captura ",[185,454,388],{}," El matcher sube desde el elemento encontrado hasta el ancestro ",[185,457,458],{},"RepaintBoundary"," más cercano y captura esa capa. Normalmente ese ancestro es la app entera, y por eso los goldens salen tantas veces como una imagen de toda la ventana con el banner de debug en una esquina. Envuelve el sujeto en un ",[185,461,458],{}," y capturarás exactamente lo que querías.",[175,464,465,468,469,472],{},[210,466,467],{},"La imagen renderizada no es tu app."," Es ",[185,470,471],{},"flutter_tester"," en tu máquina anfitriona: sin fuentes del sistema, sin interfaz del sistema, sin teclado, con una red simulada que solo devuelve 400, con las sombras reducidas a bloques duros y en la superficie que le tocara al test. Un golden que pasa te dice que este árbol, con este tamaño y estas entradas, no ha cambiado. Eso vale mucho — y es mucho más estrecho que «la interfaz funciona».",[199,474,476],{"id":475},"las-cinco-razones-por-las-que-tu-primer-golden-te-miente","Las cinco razones por las que tu primer golden te miente",[478,479,481],"h3",{"id":480},"_1-no-hay-fuentes-así-que-cada-glifo-es-una-caja","1. No hay fuentes, así que cada glifo es una caja",[175,483,484,487,488,491],{},[185,485,486],{},"flutter test"," se ejecuta sin las fuentes declaradas en ",[185,489,490],{},"pubspec.yaml",". El texto sin familia disponible cae en una fuente de test cuyos glifos son todos rectángulos rellenos: un diseño que hace la maquetación determinista y prescinde de archivos de fuentes, y que además significa que el PNG de tu repositorio no contiene ni una letra legible.",[175,493,494,495,498],{},"La solución es ",[185,496,497],{},"test\u002Fflutter_test_config.dart",", que Flutter ejecuta antes de las suites de ese directorio. Todo lo que declares ahí — carga de fuentes, un comparador propio, configuración del reloj — se aplica a cada test que haya debajo:",[282,500,502],{"className":284,"code":501,"language":18,"meta":286,"style":286},"\u002F\u002F test\u002Fflutter_test_config.dart\nimport 'dart:async';\nimport 'package:flutter\u002Fservices.dart';\n\nFuture\u003Cvoid> testExecutable(FutureOr\u003Cvoid> Function() testMain) async {\n  await _loadFonts();\n  await testMain();\n}\n\nFuture\u003Cvoid> _loadFonts() async {\n  \u002F\u002F The family name must match what the theme asks for. If `fontFamily` is\n  \u002F\u002F null, register the same faces as 'Roboto' — that is what Material falls\n  \u002F\u002F back to, and a mismatch loads nothing and fails silently.\n  \u002F\u002F Both faces are declared under `assets:` in pubspec.yaml, and `flutter test`\n  \u002F\u002F can load them from there — a pure `dart test` cannot.\n  final text = FontLoader('Roboto')\n    ..addFont(rootBundle.load('assets\u002Ffonts\u002FRoboto-Regular.ttf'))\n    ..addFont(rootBundle.load('assets\u002Ffonts\u002FRoboto-Medium.ttf'));\n\n  \u002F\u002F Icons are fonts too. Skip this and every `Icon` in every golden is a box.\n  final icons = FontLoader('MaterialIcons')\n    ..addFont(rootBundle.load('assets\u002Ffonts\u002FMaterialIcons-Regular.otf'));\n\n  await Future.wait([text.load(), icons.load()]);\n}\n",[185,503,504,510,521,530,534,572,584,595,600,605,626,632,638,644,650,656,679,704,727,732,738,757,779,784,820],{"__ignoreMap":286},[290,505,506],{"class":292,"line":293},[290,507,509],{"class":508},"sutJx","\u002F\u002F test\u002Fflutter_test_config.dart\n",[290,511,512,516,519],{"class":292,"line":322},[290,513,515],{"class":514},"sw1J6","import",[290,517,518],{"class":304}," 'dart:async'",[290,520,354],{"class":308},[290,522,523,525,528],{"class":292,"line":357},[290,524,515],{"class":514},[290,526,527],{"class":304}," 'package:flutter\u002Fservices.dart'",[290,529,354],{"class":308},[290,531,532],{"class":292,"line":364},[290,533,361],{"emptyLinePlaceholder":360},[290,535,536,539,542,545,548,551,553,556,558,560,562,565,568,570],{"class":292,"line":375},[290,537,538],{"class":340},"Future",[290,540,541],{"class":300},"\u003C",[290,543,544],{"class":336},"void",[290,546,547],{"class":300},"> ",[290,549,550],{"class":296},"testExecutable",[290,552,301],{"class":300},[290,554,555],{"class":340},"FutureOr",[290,557,541],{"class":300},[290,559,544],{"class":336},[290,561,547],{"class":300},[290,563,564],{"class":340},"Function",[290,566,567],{"class":300},"() testMain) ",[290,569,316],{"class":315},[290,571,319],{"class":300},[290,573,574,576,579,582],{"class":292,"line":397},[290,575,325],{"class":315},[290,577,578],{"class":296}," _loadFonts",[290,580,581],{"class":300},"()",[290,583,354],{"class":308},[290,585,586,588,591,593],{"class":292,"line":412},[290,587,325],{"class":315},[290,589,590],{"class":296}," testMain",[290,592,581],{"class":300},[290,594,354],{"class":308},[290,596,597],{"class":292,"line":420},[290,598,599],{"class":300},"}\n",[290,601,603],{"class":292,"line":602},9,[290,604,361],{"emptyLinePlaceholder":360},[290,606,608,610,612,614,616,619,622,624],{"class":292,"line":607},10,[290,609,538],{"class":340},[290,611,541],{"class":300},[290,613,544],{"class":336},[290,615,547],{"class":300},[290,617,618],{"class":296},"_loadFonts",[290,620,621],{"class":300},"() ",[290,623,316],{"class":315},[290,625,319],{"class":300},[290,627,629],{"class":292,"line":628},11,[290,630,631],{"class":508},"  \u002F\u002F The family name must match what the theme asks for. If `fontFamily` is\n",[290,633,635],{"class":292,"line":634},12,[290,636,637],{"class":508},"  \u002F\u002F null, register the same faces as 'Roboto' — that is what Material falls\n",[290,639,641],{"class":292,"line":640},13,[290,642,643],{"class":508},"  \u002F\u002F back to, and a mismatch loads nothing and fails silently.\n",[290,645,647],{"class":292,"line":646},14,[290,648,649],{"class":508},"  \u002F\u002F Both faces are declared under `assets:` in pubspec.yaml, and `flutter test`\n",[290,651,653],{"class":292,"line":652},15,[290,654,655],{"class":508},"  \u002F\u002F can load them from there — a pure `dart test` cannot.\n",[290,657,659,662,665,668,671,673,676],{"class":292,"line":658},16,[290,660,661],{"class":336},"  final",[290,663,664],{"class":300}," text ",[290,666,667],{"class":347},"=",[290,669,670],{"class":340}," FontLoader",[290,672,301],{"class":300},[290,674,675],{"class":304},"'Roboto'",[290,677,678],{"class":300},")\n",[290,680,682,685,688,691,693,696,698,701],{"class":292,"line":681},17,[290,683,684],{"class":308},"    ..",[290,686,687],{"class":296},"addFont",[290,689,690],{"class":300},"(rootBundle",[290,692,257],{"class":308},[290,694,695],{"class":296},"load",[290,697,301],{"class":300},[290,699,700],{"class":304},"'assets\u002Ffonts\u002FRoboto-Regular.ttf'",[290,702,703],{"class":300},"))\n",[290,705,707,709,711,713,715,717,719,722,725],{"class":292,"line":706},18,[290,708,684],{"class":308},[290,710,687],{"class":296},[290,712,690],{"class":300},[290,714,257],{"class":308},[290,716,695],{"class":296},[290,718,301],{"class":300},[290,720,721],{"class":304},"'assets\u002Ffonts\u002FRoboto-Medium.ttf'",[290,723,724],{"class":300},"))",[290,726,354],{"class":308},[290,728,730],{"class":292,"line":729},19,[290,731,361],{"emptyLinePlaceholder":360},[290,733,735],{"class":292,"line":734},20,[290,736,737],{"class":508},"  \u002F\u002F Icons are fonts too. Skip this and every `Icon` in every golden is a box.\n",[290,739,741,743,746,748,750,752,755],{"class":292,"line":740},21,[290,742,661],{"class":336},[290,744,745],{"class":300}," icons ",[290,747,667],{"class":347},[290,749,670],{"class":340},[290,751,301],{"class":300},[290,753,754],{"class":304},"'MaterialIcons'",[290,756,678],{"class":300},[290,758,760,762,764,766,768,770,772,775,777],{"class":292,"line":759},22,[290,761,684],{"class":308},[290,763,687],{"class":296},[290,765,690],{"class":300},[290,767,257],{"class":308},[290,769,695],{"class":296},[290,771,301],{"class":300},[290,773,774],{"class":304},"'assets\u002Ffonts\u002FMaterialIcons-Regular.otf'",[290,776,724],{"class":300},[290,778,354],{"class":308},[290,780,782],{"class":292,"line":781},23,[290,783,361],{"emptyLinePlaceholder":360},[290,785,787,789,792,794,797,800,802,804,806,808,811,813,815,818],{"class":292,"line":786},24,[290,788,325],{"class":315},[290,790,791],{"class":340}," Future",[290,793,257],{"class":308},[290,795,796],{"class":296},"wait",[290,798,799],{"class":300},"([text",[290,801,257],{"class":308},[290,803,695],{"class":296},[290,805,581],{"class":300},[290,807,309],{"class":308},[290,809,810],{"class":300}," icons",[290,812,257],{"class":308},[290,814,695],{"class":296},[290,816,817],{"class":300},"()])",[290,819,354],{"class":308},[290,821,823],{"class":292,"line":822},25,[290,824,599],{"class":300},[175,826,827,828,831,832,835],{},"Dos cosas fallan aquí en silencio en vez de ruidosamente. Un nombre de familia equivocado carga una fuente que ningún widget pide, así que no cambia nada. Y ",[185,829,830],{},"google_fonts"," descarga sus tipos en tiempo de ejecución, cosa que el entorno de test no hace: guarda en el repositorio los ",[185,833,834],{},".ttf"," que realmente distribuyes o tus goldens se quedarán esperando para siempre una llamada de red que nunca ocurre.",[175,837,838,839,842],{},"Hay una consecuencia más sutil, y es la que golpea a los equipos que se saltan esta sección: ",[210,840,841],{},"con glifos de caja no puedes ver qué dice el texto, y desaparece toda propiedad que la fuente de test normaliza."," La familia, el peso, el kerning, la forma del glifo y la ascensión y descendencia reales de la fuente se sustituyen por cuadrados idénticos, así que cambiar de Roboto a Inter dibuja las mismas cajas, y un cambio de copy con el mismo número de caracteres no mueve nada. El número de caracteres y el tamaño de fuente sí mueven las cajas — una cadena rusa más larga produce una barra más ancha —, así que el desbordamiento y los cambios de tamaño no son invisibles. Solo lo son las cosas que hacen legible la tipografía en producción.",[478,844,846],{"id":845},"_2-las-sombras-se-aplanan-así-que-el-desenfoque-no-existe","2. Las sombras se aplanan, así que el desenfoque no existe",[175,848,849,851,852,855,856,859,860,863],{},[185,850,228],{}," no elimina las sombras: las ",[210,853,854],{},"sustituye por bloques sólidos",". ",[185,857,858],{},"BoxShadow.toPaint()"," descarta su máscara de desenfoque, y una superficie Material pinta su elevación como un contorno duro (",[185,861,862],{},"strokeWidth: elevation * 2","), así que en el golden queda una forma sin desenfoque en el color de la sombra. El binding de test lo activa porque el renderizado de sombras no está garantizado como idéntico píxel a píxel entre versiones, ni siquiera entre ejecuciones.",[175,865,866,867,870,871,874],{},"La consecuencia es más estrecha que «sin sombras» y más engañosa: la tarjeta sigue ahí, pero su radio de desenfoque, su propagación y su degradado no, así que una regresión de sombra — un desenfoque que se duplicó, una propagación que rompió la maquetación — se renderiza idéntica en el golden. Los propios tests del framework de Flutter ponen ",[185,868,869],{},"debugDisableShadows = false"," cuando prueban sombras concretamente, y lo restauran al final del test; el binding comprueba que el flag ha vuelto a su valor por defecto (",[185,872,873],{},"true","), con el que trabaja el resto de la suite. Ese es el planteamiento correcto: devuelve las sombras reales solo donde la sombra es lo que se está probando, acepta allí la inestabilidad y no toques el flag en ningún otro sitio.",[478,876,878],{"id":877},"_3-toda-imagen-de-red-es-un-placeholder","3. Toda imagen de red es un placeholder",[175,880,881,884,885,888,889,892,893,896,897,900,901,904,905,908,909,912,913,916],{},[185,882,883],{},"Image.network"," en un test llama a un ",[185,886,887],{},"HttpClient"," simulado que devuelve un 400 vacío. Lo que pasa después depende del widget: sin ",[185,890,891],{},"errorBuilder",", el widget ",[185,894,895],{},"Image"," relanza el error de carga en debug y el test falla con ",[185,898,899],{},"NetworkImageLoadException","; con uno, el golden registra tu placeholder en silencio. ",[185,902,903],{},"cached_network_image"," y compañía necesitan su propio doble. O envuelves las llamadas en ",[185,906,907],{},"HttpOverrides"," (los tutoriales antiguos lo hacen así), o haces lo que sobrevive a un refactor: recibir un ",[185,910,911],{},"ImageProvider"," en el widget y pasarle en el test un ",[185,914,915],{},"MemoryImage"," de un PNG de 1×1.",[175,918,919],{},"Los avatares y las fotos de producto son casi todo el peso visual de una pantalla real, así que una suite donde todos son placeholders está probando la maquetación alrededor de las imágenes y nada de las imágenes.",[478,921,923],{"id":922},"_4-el-reloj-el-idioma-y-el-azar-no-son-tuyos","4. El reloj, el idioma y el azar no son tuyos",[175,925,926,929,930,933,934,937,938,941,942,945],{},[185,927,928],{},"DateTime.now()"," dentro de un widget, el ",[185,931,932],{},"DateFormat"," de ",[185,935,936],{},"intl"," sin una fecha fija, un ",[185,939,940],{},"Random"," sin semilla, un ",[185,943,944],{},"uuid"," generado durante el build: cada uno produce un golden que pasa hoy y falla el segundo martes de algún mes, o que falla solo en CI, donde el idioma de la máquina es otro.",[175,947,948,949,952,953,956,957,960,961,964,965,968,969,972,973,975],{},"La solución no está en el test sino en el widget: recibe una función ",[185,950,951],{},"now"," o un ",[185,954,955],{},"Clock",", recibe un ",[185,958,959],{},"Locale",", recibe una semilla. Merece la pena hacerlo igualmente, porque un widget que lee el reloj del sistema directamente es un widget que no puedes testear en ninguna capa, ni golden ni de otro tipo. Declara ",[185,962,963],{},"Intl.defaultLocale"," explícitamente en ",[185,966,967],{},"flutter_test_config.dart"," y eliminarás toda una clase de fallos del tipo «en mi máquina funciona, en CI sale otro idioma»; para fechas que no sean en inglés, llama también a ",[185,970,971],{},"initializeDateFormatting(locale)",", porque ",[185,974,932],{}," lanza una excepción para un idioma del que no tiene datos.",[478,977,979,980,982],{"id":978},"_5-pumpandsettle-no-vuelve-nunca-en-un-estado-de-carga","5. ",[185,981,251],{}," no vuelve nunca en un estado de carga",[175,984,985,987,988,991],{},[185,986,251],{}," avanza fotogramas hasta que no queda ninguno programado. Un ",[185,989,990],{},"CircularProgressIndicator"," indeterminado programa fotogramas mientras gira, así que la llamada agota el tiempo con el famoso «pumpAndSettle timed out», y el golden del estado de carga — el estado que QA nunca mira porque no hay manera de mantener la app en él — resulta ser el que no puedes escribir.",[282,993,995],{"className":284,"code":994,"language":18,"meta":286,"style":286},"\u002F\u002F Not this: it waits for an animation that never ends.\n\u002F\u002F await tester.pumpAndSettle();\n\nawait pumpScreen(tester, const CartScreen.loading());\nawait tester.pump(const Duration(milliseconds: 100)); \u002F\u002F one fixed phase of the spinner\nawait expectLater(find.byType(CartScreen), matchesGoldenFile('goldens\u002Fcart_loading.light.390.ts1.0.en.png'));\n",[185,996,997,1002,1007,1011,1036,1073],{"__ignoreMap":286},[290,998,999],{"class":292,"line":293},[290,1000,1001],{"class":508},"\u002F\u002F Not this: it waits for an animation that never ends.\n",[290,1003,1004],{"class":292,"line":322},[290,1005,1006],{"class":508},"\u002F\u002F await tester.pumpAndSettle();\n",[290,1008,1009],{"class":292,"line":357},[290,1010,361],{"emptyLinePlaceholder":360},[290,1012,1013,1016,1018,1020,1022,1024,1026,1028,1031,1034],{"class":292,"line":364},[290,1014,1015],{"class":315},"await",[290,1017,328],{"class":296},[290,1019,331],{"class":300},[290,1021,309],{"class":308},[290,1023,337],{"class":336},[290,1025,341],{"class":340},[290,1027,257],{"class":308},[290,1029,1030],{"class":296},"loading",[290,1032,1033],{"class":300},"())",[290,1035,354],{"class":308},[290,1037,1038,1040,1043,1045,1048,1050,1053,1056,1059,1061,1065,1067,1070],{"class":292,"line":375},[290,1039,1015],{"class":315},[290,1041,1042],{"class":300}," tester",[290,1044,257],{"class":308},[290,1046,1047],{"class":296},"pump",[290,1049,301],{"class":300},[290,1051,1052],{"class":336},"const",[290,1054,1055],{"class":340}," Duration",[290,1057,1058],{"class":300},"(milliseconds",[290,1060,348],{"class":347},[290,1062,1064],{"class":1063},"srdBf"," 100",[290,1066,724],{"class":300},[290,1068,1069],{"class":308},";",[290,1071,1072],{"class":508}," \u002F\u002F one fixed phase of the spinner\n",[290,1074,1075,1077,1079,1082,1084,1086,1088,1090,1092,1094,1097,1099,1102,1104],{"class":292,"line":397},[290,1076,1015],{"class":315},[290,1078,369],{"class":296},[290,1080,1081],{"class":300},"(find",[290,1083,257],{"class":308},[290,1085,383],{"class":296},[290,1087,301],{"class":300},[290,1089,388],{"class":340},[290,1091,391],{"class":300},[290,1093,309],{"class":308},[290,1095,1096],{"class":296}," matchesGoldenFile",[290,1098,301],{"class":300},[290,1100,1101],{"class":304},"'goldens\u002Fcart_loading.light.390.ts1.0.en.png'",[290,1103,724],{"class":300},[290,1105,354],{"class":308},[175,1107,1108],{},"Ahora el golden captura el spinner a 100 ms, de forma consistente, para siempre. Está bien: lo que pruebas es que la maquetación de alrededor aguanta mientras algo indeterminado gira, no el spinner en sí.",[1110,1111,1114],"info-box",{":leading-icon":1112,":title":1113},"lucide:alert-triangle","Una suite verde con 214 archivos no es una suite sólida",[175,1115,1116],{},"Todo lo anterior produce el mismo síntoma: tests que pasan comprobando menos de lo que crees. Nada de eso aparece como fallo, como aviso ni como caída de cobertura. Una suite de goldens es particular en que su modo de fallo es el silencio, así que la única forma de confiar en ella es abrir un PNG y comprobar que contiene lo que crees que contiene. Hazlo una vez, el primer día, para cada tipo distinto de captura de la suite. Son diez minutos y son los diez minutos más rentables de toda la configuración.",[199,1118,1120],{"id":1119},"la-configuración-que-hace-reproducibles-los-goldens","La configuración que hace reproducibles los goldens",[175,1122,1123],{},"Cuatro decisiones, todas aburridas, todas se toman una vez:",[175,1125,1126,1129,1130,1133,1134,1137,1138,1141],{},[210,1127,1128],{},"Fija el toolchain."," Un ",[185,1131,1132],{},".fvmrc"," en el repositorio, y que CI lea el mismo archivo. Actualizar Flutter cambia el renderizado de texto, los temas por defecto, los renombrados de ",[185,1135,1136],{},"CardTheme"," a ",[185,1139,1140],{},"CardThemeData"," y el pintado de sombras, así que todos los goldens del repositorio cambian a la vez. Eso no es una razón para no actualizar; es una razón para que la actualización sea un commit deliberado que no contiene nada más que los goldens regenerados, y que lo dice en el mensaje.",[175,1143,1144,1147,1148,1150],{},[210,1145,1146],{},"Genera en la plataforma donde corre CI."," El antialiasing y el hinting del texto difieren entre macOS y Linux: el mismo widget al mismo tamaño produce píxeles distintos. Una suite generada en el Mac de un desarrollador fallará en un runner Linux, y el «arreglo» habitual es un ",[185,1149,272],{}," donde no debería estar. Ejecuta también los goldens en Linux en local, en el mismo contenedor que CI si el equipo es mixto.",[175,1152,1153,1156],{},[210,1154,1155],{},"No dejes nunca que CI regenere."," La protección es una línea:",[282,1158,1162],{"className":1159,"code":1160,"language":1161,"meta":286,"style":286},"language-yaml shiki shiki-themes material-theme-lighter github-light github-dark","- uses: subosito\u002Fflutter-action@v2\n  with:\n    flutter-version-file: .fvmrc   # one version, repo and CI\n    cache: true\n- run: flutter test\n# A \"helpful\" --update-goldens in CI or a stale golden written by a bad merge\n# would make the suite green while comparing nothing. Fail instead. `git diff`\n# alone misses untracked files, which is exactly what a regenerated golden is.\n- run: test -z \"$(git status --porcelain -- '*.png')\"\n","yaml",[185,1163,1164,1169,1174,1179,1184,1189,1194,1199,1204],{"__ignoreMap":286},[290,1165,1166],{"class":292,"line":293},[290,1167,1168],{},"- uses: subosito\u002Fflutter-action@v2\n",[290,1170,1171],{"class":292,"line":322},[290,1172,1173],{},"  with:\n",[290,1175,1176],{"class":292,"line":357},[290,1177,1178],{},"    flutter-version-file: .fvmrc   # one version, repo and CI\n",[290,1180,1181],{"class":292,"line":364},[290,1182,1183],{},"    cache: true\n",[290,1185,1186],{"class":292,"line":375},[290,1187,1188],{},"- run: flutter test\n",[290,1190,1191],{"class":292,"line":397},[290,1192,1193],{},"# A \"helpful\" --update-goldens in CI or a stale golden written by a bad merge\n",[290,1195,1196],{"class":292,"line":412},[290,1197,1198],{},"# would make the suite green while comparing nothing. Fail instead. `git diff`\n",[290,1200,1201],{"class":292,"line":420},[290,1202,1203],{},"# alone misses untracked files, which is exactly what a regenerated golden is.\n",[290,1205,1206],{"class":292,"line":602},[290,1207,1208],{},"- run: test -z \"$(git status --porcelain -- '*.png')\"\n",[175,1210,1211],{},"Esa última línea no es paranoia. La forma más común de que una suite de goldens deje de comprobar algo es un paso de CI añadido a las dos de la mañana durante un build en rojo, que actualiza los goldens en vez de fallar.",[175,1213,1214,1217,1218,1221],{},[210,1215,1216],{},"Fija la superficie explícitamente."," La superficie por defecto son 800×600 píxeles lógicos con relación de píxeles 3. Prueba en ella una pantalla de móvil y obtendrás la maquetación de una tableta pequeña, con el ",[185,1219,1220],{},"MediaQuery"," de la app insistiendo en lo contrario. Un solo helper lo resuelve para todos los tests, y si no puedes leer el nombre del golden y saber en qué cinco ejes se renderizó, el nombre está mal:",[282,1223,1225],{"className":284,"code":1224,"language":18,"meta":286,"style":286},"\u002F\u002F test\u002Fhelpers\u002Fgolden.dart\nFuture\u003Cvoid> pumpScreen(\n  WidgetTester tester,\n  Widget child, {\n  Size size = const Size(390, 844), \u002F\u002F iPhone 14 — pick your own baselines\n  double textScale = 1,\n  Locale locale = const Locale('en'),\n  Brightness brightness = Brightness.light,\n}) async {\n  tester.view.physicalSize = size * 3;   \u002F\u002F logical size × device pixel ratio\n  tester.view.devicePixelRatio = 3;\n  addTearDown(tester.view.reset);        \u002F\u002F never leak a view into the next test\n\n  await tester.pumpWidget(\n    MaterialApp(\n      debugShowCheckedModeBanner: false,\n      theme: brightness == Brightness.dark ? AppTheme.dark : AppTheme.light,\n      locale: locale,\n      \u002F\u002F Without the app's own delegates a locale it does not support resolves\n      \u002F\u002F back to English.\n      localizationsDelegates: AppLocalizations.delegates,\n      supportedLocales: AppLocalizations.supportedLocales,\n      home: RepaintBoundary(\n        child: Builder(\n          builder: (context) => MediaQuery(\n            \u002F\u002F Merge, don't replace: a fresh MediaQueryData has size zero,\n            \u002F\u002F devicePixelRatio 1 and no padding.\n            data: MediaQuery.of(context).copyWith(\n              textScaler: TextScaler.linear(textScale),\n            ),\n            child: child,\n          ),\n        ),\n      ),\n    ),\n  );\n  await tester.pump();\n}\n",[185,1226,1227,1232,1247,1256,1268,1300,1315,1339,1359,1368,1398,1417,1438,1442,1455,1462,1475,1514,1526,1531,1536,1553,1569,1581,1593,1611,1617,1623,1648,1669,1677,1689,1697,1705,1713,1721,1728,1743],{"__ignoreMap":286},[290,1228,1229],{"class":292,"line":293},[290,1230,1231],{"class":508},"\u002F\u002F test\u002Fhelpers\u002Fgolden.dart\n",[290,1233,1234,1236,1238,1240,1242,1245],{"class":292,"line":322},[290,1235,538],{"class":340},[290,1237,541],{"class":300},[290,1239,544],{"class":336},[290,1241,547],{"class":300},[290,1243,1244],{"class":296},"pumpScreen",[290,1246,372],{"class":300},[290,1248,1249,1252,1254],{"class":292,"line":357},[290,1250,1251],{"class":340},"  WidgetTester",[290,1253,1042],{"class":300},[290,1255,394],{"class":308},[290,1257,1258,1261,1264,1266],{"class":292,"line":364},[290,1259,1260],{"class":340},"  Widget",[290,1262,1263],{"class":300}," child",[290,1265,309],{"class":308},[290,1267,319],{"class":300},[290,1269,1270,1273,1276,1278,1280,1283,1285,1288,1290,1293,1295,1297],{"class":292,"line":375},[290,1271,1272],{"class":340},"  Size",[290,1274,1275],{"class":300}," size ",[290,1277,667],{"class":347},[290,1279,337],{"class":336},[290,1281,1282],{"class":340}," Size",[290,1284,301],{"class":300},[290,1286,1287],{"class":1063},"390",[290,1289,309],{"class":308},[290,1291,1292],{"class":1063}," 844",[290,1294,391],{"class":300},[290,1296,309],{"class":308},[290,1298,1299],{"class":508}," \u002F\u002F iPhone 14 — pick your own baselines\n",[290,1301,1302,1305,1308,1310,1313],{"class":292,"line":397},[290,1303,1304],{"class":340},"  double",[290,1306,1307],{"class":300}," textScale ",[290,1309,667],{"class":347},[290,1311,1312],{"class":1063}," 1",[290,1314,394],{"class":308},[290,1316,1317,1320,1323,1325,1327,1330,1332,1335,1337],{"class":292,"line":412},[290,1318,1319],{"class":340},"  Locale",[290,1321,1322],{"class":300}," locale ",[290,1324,667],{"class":347},[290,1326,337],{"class":336},[290,1328,1329],{"class":340}," Locale",[290,1331,301],{"class":300},[290,1333,1334],{"class":304},"'en'",[290,1336,391],{"class":300},[290,1338,394],{"class":308},[290,1340,1341,1344,1347,1349,1352,1354,1357],{"class":292,"line":420},[290,1342,1343],{"class":340},"  Brightness",[290,1345,1346],{"class":300}," brightness ",[290,1348,667],{"class":347},[290,1350,1351],{"class":340}," Brightness",[290,1353,257],{"class":308},[290,1355,1356],{"class":300},"light",[290,1358,394],{"class":308},[290,1360,1361,1364,1366],{"class":292,"line":602},[290,1362,1363],{"class":300},"}) ",[290,1365,316],{"class":315},[290,1367,319],{"class":300},[290,1369,1370,1373,1375,1378,1380,1383,1385,1387,1390,1393,1395],{"class":292,"line":607},[290,1371,1372],{"class":300},"  tester",[290,1374,257],{"class":308},[290,1376,1377],{"class":300},"view",[290,1379,257],{"class":308},[290,1381,1382],{"class":300},"physicalSize ",[290,1384,667],{"class":347},[290,1386,1275],{"class":300},[290,1388,1389],{"class":347},"*",[290,1391,1392],{"class":1063}," 3",[290,1394,1069],{"class":308},[290,1396,1397],{"class":508},"   \u002F\u002F logical size × device pixel ratio\n",[290,1399,1400,1402,1404,1406,1408,1411,1413,1415],{"class":292,"line":628},[290,1401,1372],{"class":300},[290,1403,257],{"class":308},[290,1405,1377],{"class":300},[290,1407,257],{"class":308},[290,1409,1410],{"class":300},"devicePixelRatio ",[290,1412,667],{"class":347},[290,1414,1392],{"class":1063},[290,1416,354],{"class":308},[290,1418,1419,1422,1424,1426,1428,1430,1433,1435],{"class":292,"line":634},[290,1420,1421],{"class":296},"  addTearDown",[290,1423,331],{"class":300},[290,1425,257],{"class":308},[290,1427,1377],{"class":300},[290,1429,257],{"class":308},[290,1431,1432],{"class":300},"reset)",[290,1434,1069],{"class":308},[290,1436,1437],{"class":508},"        \u002F\u002F never leak a view into the next test\n",[290,1439,1440],{"class":292,"line":640},[290,1441,361],{"emptyLinePlaceholder":360},[290,1443,1444,1446,1448,1450,1453],{"class":292,"line":646},[290,1445,325],{"class":315},[290,1447,1042],{"class":300},[290,1449,257],{"class":308},[290,1451,1452],{"class":296},"pumpWidget",[290,1454,372],{"class":300},[290,1456,1457,1460],{"class":292,"line":652},[290,1458,1459],{"class":340},"    MaterialApp",[290,1461,372],{"class":300},[290,1463,1464,1467,1469,1473],{"class":292,"line":658},[290,1465,1466],{"class":300},"      debugShowCheckedModeBanner",[290,1468,348],{"class":347},[290,1470,1472],{"class":1471},"s39Yj"," false",[290,1474,394],{"class":308},[290,1476,1477,1480,1482,1484,1487,1489,1491,1494,1497,1500,1502,1504,1506,1508,1510,1512],{"class":292,"line":681},[290,1478,1479],{"class":300},"      theme",[290,1481,348],{"class":347},[290,1483,1346],{"class":300},[290,1485,1486],{"class":347},"==",[290,1488,1351],{"class":340},[290,1490,257],{"class":308},[290,1492,1493],{"class":300},"dark ",[290,1495,1496],{"class":347},"?",[290,1498,1499],{"class":340}," AppTheme",[290,1501,257],{"class":308},[290,1503,1493],{"class":300},[290,1505,348],{"class":347},[290,1507,1499],{"class":340},[290,1509,257],{"class":308},[290,1511,1356],{"class":300},[290,1513,394],{"class":308},[290,1515,1516,1519,1521,1524],{"class":292,"line":706},[290,1517,1518],{"class":300},"      locale",[290,1520,348],{"class":347},[290,1522,1523],{"class":300}," locale",[290,1525,394],{"class":308},[290,1527,1528],{"class":292,"line":729},[290,1529,1530],{"class":508},"      \u002F\u002F Without the app's own delegates a locale it does not support resolves\n",[290,1532,1533],{"class":292,"line":734},[290,1534,1535],{"class":508},"      \u002F\u002F back to English.\n",[290,1537,1538,1541,1543,1546,1548,1551],{"class":292,"line":740},[290,1539,1540],{"class":300},"      localizationsDelegates",[290,1542,348],{"class":347},[290,1544,1545],{"class":340}," AppLocalizations",[290,1547,257],{"class":308},[290,1549,1550],{"class":300},"delegates",[290,1552,394],{"class":308},[290,1554,1555,1558,1560,1562,1564,1567],{"class":292,"line":759},[290,1556,1557],{"class":300},"      supportedLocales",[290,1559,348],{"class":347},[290,1561,1545],{"class":340},[290,1563,257],{"class":308},[290,1565,1566],{"class":300},"supportedLocales",[290,1568,394],{"class":308},[290,1570,1571,1574,1576,1579],{"class":292,"line":781},[290,1572,1573],{"class":300},"      home",[290,1575,348],{"class":347},[290,1577,1578],{"class":340}," RepaintBoundary",[290,1580,372],{"class":300},[290,1582,1583,1586,1588,1591],{"class":292,"line":786},[290,1584,1585],{"class":300},"        child",[290,1587,348],{"class":347},[290,1589,1590],{"class":340}," Builder",[290,1592,372],{"class":300},[290,1594,1595,1598,1600,1603,1606,1609],{"class":292,"line":822},[290,1596,1597],{"class":300},"          builder",[290,1599,348],{"class":347},[290,1601,1602],{"class":300}," (context) ",[290,1604,1605],{"class":347},"=>",[290,1607,1608],{"class":340}," MediaQuery",[290,1610,372],{"class":300},[290,1612,1614],{"class":292,"line":1613},26,[290,1615,1616],{"class":508},"            \u002F\u002F Merge, don't replace: a fresh MediaQueryData has size zero,\n",[290,1618,1620],{"class":292,"line":1619},27,[290,1621,1622],{"class":508},"            \u002F\u002F devicePixelRatio 1 and no padding.\n",[290,1624,1626,1629,1631,1633,1635,1638,1641,1643,1646],{"class":292,"line":1625},28,[290,1627,1628],{"class":300},"            data",[290,1630,348],{"class":347},[290,1632,1608],{"class":340},[290,1634,257],{"class":308},[290,1636,1637],{"class":296},"of",[290,1639,1640],{"class":300},"(context)",[290,1642,257],{"class":308},[290,1644,1645],{"class":296},"copyWith",[290,1647,372],{"class":300},[290,1649,1651,1654,1656,1659,1661,1664,1667],{"class":292,"line":1650},29,[290,1652,1653],{"class":300},"              textScaler",[290,1655,348],{"class":347},[290,1657,1658],{"class":340}," TextScaler",[290,1660,257],{"class":308},[290,1662,1663],{"class":296},"linear",[290,1665,1666],{"class":300},"(textScale)",[290,1668,394],{"class":308},[290,1670,1672,1675],{"class":292,"line":1671},30,[290,1673,1674],{"class":300},"            )",[290,1676,394],{"class":308},[290,1678,1680,1683,1685,1687],{"class":292,"line":1679},31,[290,1681,1682],{"class":300},"            child",[290,1684,348],{"class":347},[290,1686,1263],{"class":300},[290,1688,394],{"class":308},[290,1690,1692,1695],{"class":292,"line":1691},32,[290,1693,1694],{"class":300},"          )",[290,1696,394],{"class":308},[290,1698,1700,1703],{"class":292,"line":1699},33,[290,1701,1702],{"class":300},"        )",[290,1704,394],{"class":308},[290,1706,1708,1711],{"class":292,"line":1707},34,[290,1709,1710],{"class":300},"      )",[290,1712,394],{"class":308},[290,1714,1716,1719],{"class":292,"line":1715},35,[290,1717,1718],{"class":300},"    )",[290,1720,394],{"class":308},[290,1722,1724,1726],{"class":292,"line":1723},36,[290,1725,415],{"class":300},[290,1727,354],{"class":308},[290,1729,1731,1733,1735,1737,1739,1741],{"class":292,"line":1730},37,[290,1732,325],{"class":315},[290,1734,1042],{"class":300},[290,1736,257],{"class":308},[290,1738,1047],{"class":296},[290,1740,581],{"class":300},[290,1742,354],{"class":308},[290,1744,1746],{"class":292,"line":1745},38,[290,1747,599],{"class":300},[175,1749,1750,1751,1754,1755,1757,1758,1761,1762,1765,1766,1769,1770,1773,1774,1777],{},"Dos detalles son fáciles de estropear. ",[185,1752,1753],{},"MediaQueryData(...)"," en vez de ",[185,1756,1645],{}," construye un objeto nuevo con ",[185,1759,1760],{},"size: Size.zero"," y ",[185,1763,1764],{},"devicePixelRatio: 1",", así que ",[185,1767,1768],{},"MediaQuery.sizeOf"," miente a todo lo que haya debajo. Y un ",[185,1771,1772],{},"MaterialApp"," reconstruido dentro del helper resuelve cualquier idioma que no soporte de vuelta al inglés salvo que le pases los delegados de la app (que es lo que aquí representa ",[185,1775,1776],{},"AppLocalizations","); si no, el eje de idioma prueba inglés en silencio.",[175,1779,1780,1783,1784,1786],{},[185,1781,1782],{},"addTearDown(tester.view.reset)"," no es opcional. ",[185,1785,242],{}," es global para el archivo de test, así que un test que pone un ancho de 320pt se filtra a todos los siguientes, y el fallo aparece atribuido a un test inocente posterior dentro del mismo archivo.",[199,1788,1790],{"id":1789},"qué-convertir-en-golden-y-cuánto-cuesta-la-matriz","Qué convertir en golden y cuánto cuesta la matriz",[175,1792,1793],{},"La trampa es que la matriz parece gratis. No lo es: cada imagen es un archivo que regenerar, un archivo que revisar y un archivo que añadirá una línea al diff de tu siguiente pull request no relacionado.",[1795,1796,1797,1813],"table",{},[1798,1799,1800],"thead",{},[1801,1802,1803,1807,1810],"tr",{},[1804,1805,1806],"th",{},"Capa",[1804,1808,1809],{},"Qué cubre",[1804,1811,1812],{},"Imágenes",[1814,1815,1816,1828,1839,1850,1861],"tbody",{},[1801,1817,1818,1822,1825],{},[1819,1820,1821],"td",{},"Componentes del sistema de diseño — botón, campo, badge, tarjeta, list tile",[1819,1823,1824],{},"Cada estado × claro\u002Foscuro, a un ancho",[1819,1826,1827],{},"20–40",[1801,1829,1830,1833,1836],{},[1819,1831,1832],{},"Las cinco pantallas que importan",[1819,1834,1835],{},"3 anchos (320\u002F390\u002F768) × claro\u002Foscuro",[1819,1837,1838],{},"30",[1801,1840,1841,1844,1847],{},[1819,1842,1843],{},"Estados vacíos, de error y de carga",[1819,1845,1846],{},"Los estados que nadie abre la app para ver",[1819,1848,1849],{},"6–10",[1801,1851,1852,1855,1858],{},[1819,1853,1854],{},"Escala de texto 2.0, oscuro, 320pt",[1819,1856,1857],{},"Los dos componentes más densos del sistema de diseño",[1819,1859,1860],{},"4",[1801,1862,1863,1866,1869],{},[1819,1864,1865],{},"Idioma",[1819,1867,1868],{},"El idioma con cadenas más largas, un ancho, las dos pantallas con más riesgo de desbordarse",[1819,1870,1860],{},[175,1872,1873],{},"Eso es una suite de unas 60–90 imágenes: unos veinte segundos de CI y un pull request cuyo diff visual una persona puede realmente mirar. La versión que lo multiplica todo por todos los ejes — 12 pantallas × 2 temas × 3 anchos × 2 escalas × 2 idiomas — son 288 imágenes, y nadie revisa 288 imágenes.",[175,1875,1876],{},"Hay tres cosas que no vale la pena convertir en golden:",[204,1878,1879,1885,1891],{},[207,1880,1881,1884],{},[210,1882,1883],{},"Animaciones y transiciones."," Un golden captura un fotograma en un instante fijo. Te dice que el fotograma de los 100 ms se ve igual, no que la animación sea fluida, y el valor marginal sobre los estados inicial y final es casi cero.",[207,1886,1887,1890],{},[210,1888,1889],{},"Cualquier cosa con contenido real dentro."," Nombres de producto largos, precios, fechas, IDs generados: cada uno es una razón para que el golden cambie por un motivo no visual. Haz golden del esqueleto con fixtures fijas y prueba el renderizado de contenido con un widget test que afirme sobre el texto, no sobre píxeles.",[207,1892,1893,1896],{},[210,1894,1895],{},"Flujos completos."," Tres pantallas en una imagen se revisan tres veces peor que una pantalla por imagen.",[175,1898,1899],{},"Y lo más revelador que se puede convertir en golden, en nuestra experiencia, no es una pantalla: es el sistema de diseño a 320pt, en modo oscuro, con el texto al 200 %. Esa combinación es donde se rompen las apps reales, es casi imposible de comprobar a mano en todos los componentes, y es exactamente para lo que sirve un golden.",[199,1901,1903],{"id":1902},"lo-que-los-goldens-no-comprueban","Lo que los goldens no comprueban",[175,1905,1906],{},"Un golden es una imagen. No sabe nada de significado, y conviene corregir explícitamente unos cuantos supuestos habituales.",[175,1908,1909,1912,1913,1916,1917,1919],{},[210,1910,1911],{},"Comportamiento."," Un botón conectado al callback equivocado se dibuja idéntico. También un formulario que ha dejado de validar, una lista que perdió su controlador de scroll, un diálogo que no se cierra. Eso son widget tests — ",[185,1914,1915],{},"tap",", ",[185,1918,1047],{},", comprobar qué pasó — y ninguna cantidad de píxeles los sustituye.",[175,1921,1922,1925],{},[210,1923,1924],{},"Accesibilidad."," El árbol de semántica es invisible para una captura: etiquetas que faltan, roles equivocados, un área táctil reducida a 24pt, contraste por debajo de 4.5:1 para texto de cuerpo (3:1 para texto grande). Flutter trae las comprobaciones y son baratas:",[282,1927,1929],{"className":284,"code":1928,"language":18,"meta":286,"style":286},"testWidgets('cart meets a11y guidelines', (tester) async {\n  final semantics = tester.ensureSemantics();\n  addTearDown(semantics.dispose);\n\n  await pumpScreen(tester, const CartScreen(items: []));\n\n  \u002F\u002F Tap targets (48dp Android \u002F 44pt iOS) and text contrast.\n  await expectLater(tester, meetsGuideline(androidTapTargetGuideline));\n  await expectLater(tester, meetsGuideline(textContrastGuideline));\n\n  \u002F\u002F The label and role the golden cannot see. `isSemantics` checks only the\n  \u002F\u002F values you pass; `matchesSemantics` would also require every other flag\n  \u002F\u002F to be false, and a real Material button sets `hasEnabledState`.\n  \u002F\u002F (`containsSemantics` is the same matcher on Flutter before 3.40.)\n  expect(\n    tester.getSemantics(find.byType(CheckoutButton)),\n    isSemantics(label: 'Checkout', isButton: true, hasTapAction: true),\n  );\n});\n",[185,1930,1931,1948,1968,1982,1986,2008,2012,2017,2035,2052,2056,2061,2066,2071,2076,2083,2108,2144,2150],{"__ignoreMap":286},[290,1932,1933,1935,1937,1940,1942,1944,1946],{"class":292,"line":293},[290,1934,297],{"class":296},[290,1936,301],{"class":300},[290,1938,1939],{"class":304},"'cart meets a11y guidelines'",[290,1941,309],{"class":308},[290,1943,312],{"class":300},[290,1945,316],{"class":315},[290,1947,319],{"class":300},[290,1949,1950,1952,1955,1957,1959,1961,1964,1966],{"class":292,"line":322},[290,1951,661],{"class":336},[290,1953,1954],{"class":300}," semantics ",[290,1956,667],{"class":347},[290,1958,1042],{"class":300},[290,1960,257],{"class":308},[290,1962,1963],{"class":296},"ensureSemantics",[290,1965,581],{"class":300},[290,1967,354],{"class":308},[290,1969,1970,1972,1975,1977,1980],{"class":292,"line":357},[290,1971,1421],{"class":296},[290,1973,1974],{"class":300},"(semantics",[290,1976,257],{"class":308},[290,1978,1979],{"class":300},"dispose)",[290,1981,354],{"class":308},[290,1983,1984],{"class":292,"line":364},[290,1985,361],{"emptyLinePlaceholder":360},[290,1987,1988,1990,1992,1994,1996,1998,2000,2002,2004,2006],{"class":292,"line":375},[290,1989,325],{"class":315},[290,1991,328],{"class":296},[290,1993,331],{"class":300},[290,1995,309],{"class":308},[290,1997,337],{"class":336},[290,1999,341],{"class":340},[290,2001,344],{"class":300},[290,2003,348],{"class":347},[290,2005,351],{"class":300},[290,2007,354],{"class":308},[290,2009,2010],{"class":292,"line":397},[290,2011,361],{"emptyLinePlaceholder":360},[290,2013,2014],{"class":292,"line":412},[290,2015,2016],{"class":508},"  \u002F\u002F Tap targets (48dp Android \u002F 44pt iOS) and text contrast.\n",[290,2018,2019,2021,2023,2025,2027,2030,2033],{"class":292,"line":420},[290,2020,325],{"class":315},[290,2022,369],{"class":296},[290,2024,331],{"class":300},[290,2026,309],{"class":308},[290,2028,2029],{"class":296}," meetsGuideline",[290,2031,2032],{"class":300},"(androidTapTargetGuideline))",[290,2034,354],{"class":308},[290,2036,2037,2039,2041,2043,2045,2047,2050],{"class":292,"line":602},[290,2038,325],{"class":315},[290,2040,369],{"class":296},[290,2042,331],{"class":300},[290,2044,309],{"class":308},[290,2046,2029],{"class":296},[290,2048,2049],{"class":300},"(textContrastGuideline))",[290,2051,354],{"class":308},[290,2053,2054],{"class":292,"line":607},[290,2055,361],{"emptyLinePlaceholder":360},[290,2057,2058],{"class":292,"line":628},[290,2059,2060],{"class":508},"  \u002F\u002F The label and role the golden cannot see. `isSemantics` checks only the\n",[290,2062,2063],{"class":292,"line":634},[290,2064,2065],{"class":508},"  \u002F\u002F values you pass; `matchesSemantics` would also require every other flag\n",[290,2067,2068],{"class":292,"line":640},[290,2069,2070],{"class":508},"  \u002F\u002F to be false, and a real Material button sets `hasEnabledState`.\n",[290,2072,2073],{"class":292,"line":646},[290,2074,2075],{"class":508},"  \u002F\u002F (`containsSemantics` is the same matcher on Flutter before 3.40.)\n",[290,2077,2078,2081],{"class":292,"line":652},[290,2079,2080],{"class":296},"  expect",[290,2082,372],{"class":300},[290,2084,2085,2088,2090,2093,2095,2097,2099,2101,2104,2106],{"class":292,"line":658},[290,2086,2087],{"class":300},"    tester",[290,2089,257],{"class":308},[290,2091,2092],{"class":296},"getSemantics",[290,2094,1081],{"class":300},[290,2096,257],{"class":308},[290,2098,383],{"class":296},[290,2100,301],{"class":300},[290,2102,2103],{"class":340},"CheckoutButton",[290,2105,724],{"class":300},[290,2107,394],{"class":308},[290,2109,2110,2113,2116,2118,2121,2123,2126,2128,2131,2133,2136,2138,2140,2142],{"class":292,"line":681},[290,2111,2112],{"class":296},"    isSemantics",[290,2114,2115],{"class":300},"(label",[290,2117,348],{"class":347},[290,2119,2120],{"class":304}," 'Checkout'",[290,2122,309],{"class":308},[290,2124,2125],{"class":300}," isButton",[290,2127,348],{"class":347},[290,2129,2130],{"class":1471}," true",[290,2132,309],{"class":308},[290,2134,2135],{"class":300}," hasTapAction",[290,2137,348],{"class":347},[290,2139,2130],{"class":1471},[290,2141,391],{"class":300},[290,2143,394],{"class":308},[290,2145,2146,2148],{"class":292,"line":706},[290,2147,415],{"class":300},[290,2149,354],{"class":308},[290,2151,2152,2154],{"class":292,"line":729},[290,2153,423],{"class":300},[290,2155,354],{"class":308},[175,2157,2158,2161,2162,2166,2167,2170],{},[210,2159,2160],{},"Renderizado en dispositivo real."," Los goldens corren sobre el motor anfitrión, así que no te dicen qué hace ",[193,2163,2165],{"slug":2164},"impeller","Impeller"," en iOS con el mismo árbol ni cómo difiere el shaping de texto del sistema. Si esa diferencia importa, es ",[185,2168,2169],{},"integration_test"," en un dispositivo con una captura tomada allí: más lento, más inestable y lo único que responde a la pregunta.",[175,2172,2173,2176],{},[210,2174,2175],{},"Jank."," El presupuesto de fotograma se mide en un build de perfil sobre un dispositivo real, no se deduce de un PNG.",[199,2178,2180],{"id":2179},"las-reglas-de-las-que-depende-que-haya-valido-la-pena","Las reglas de las que depende que haya valido la pena",[175,2182,2183],{},"El valor de la suite es proporcional a la probabilidad de que un ser humano mire el diff. Ese es todo el retorno de la inversión, y es una propiedad cultural, no técnica.",[175,2185,2186],{},"Así que, en concreto:",[2188,2189,2190,2198,2204,2213,2219],"ol",{},[207,2191,2192,2197],{},[210,2193,2194,2196],{},[185,2195,272],{}," solo en un commit cuyo mensaje nombre el cambio visual."," «Update goldens» como mensaje significa que el diff no se revisó. «Update goldens: el botón de pago mide 48pt y el estado de error usa la nueva paleta» significa que sí.",[207,2199,2200,2203],{},[210,2201,2202],{},"Un diff de golden en una pantalla que no tocaste es un hallazgo, no ruido."," Es la suite haciendo su trabajo. Agruparlo en un «chore: update goldens» es apagar la suite.",[207,2205,2206,2209,2210,2212],{},[210,2207,2208],{},"Si quien revisa no puede ver las imágenes, la fontanería falló."," Sube los artefactos de ",[185,2211,437],{}," desde CI, o adjunta el diff a la pull request. Una revisión que consiste en «créeme, los píxeles cambiaron» no es una revisión.",[207,2214,2215,2218],{},[210,2216,2217],{},"Borra los goldens que aprobarías sin mirar."," Una suite de cuarenta imágenes que se revisan todas gana a cuatrocientas que se aprueban en un clic. La métrica no es la cobertura; es la cobertura revisada.",[207,2220,2221,2224,2225,2227],{},[210,2222,2223],{},"Cuando los goldens cambian en pull requests no relacionados, para y busca la causa",": una versión de Flutter sin fijar, una fuente que no se carga, un ",[185,2226,928],{}," que llegó a un widget. La inestabilidad de los goldens casi nunca es del mecanismo; es un no determinismo real que ahora conoces.",[2229,2230],"checklist",{":groups":2231,":subtitle":2232,":title":2233},"[{\"title\": \"Hacer honesto el renderizado\", \"items\": [\"Fuentes de la app cargadas en test\u002Fflutter_test_config.dart, con el nombre de familia que el tema pide de verdad\", \"La fuente de iconos cargada también: si no, cada Icon es una caja\", \"Imágenes de red sustituidas por fixtures en memoria\", \"Reloj, idioma y semillas inyectados en vez de leídos del entorno\", \"Estados de carga renderizados con pump(duration) y no con pumpAndSettle\"]}, {\"title\": \"Hacerlo reproducible\", \"items\": [\"Versión de Flutter fijada en el repositorio y leída por CI\", \"Goldens generados en Linux, la misma plataforma donde corre CI\", \"Un paso de CI que falla si algún golden está modificado o sin seguimiento, para que nada se regenere en silencio\", \"failures\u002F ignorado en git y subido como artefacto de build para la revisión\", \"La superficie de test fijada explícitamente: tamaño, relación de píxeles, escala de texto, idioma\"]}, {\"title\": \"Hacerlo revisable\", \"items\": [\"Cada golden envuelto en un RepaintBoundary para capturar el widget que se pretende\", \"Los cinco ejes codificados en el nombre del archivo: sujeto, tema, ancho, escala de texto, idioma\", \"Menos de ~90 imágenes, o una razón declarada para más\", \"Una regla de que las actualizaciones de goldens son un commit propio con una razón visual declarada\", \"Alguien ha abierto al menos un PNG de cada tipo y ha confirmado que contiene texto\"]}]","De un repositorio vacío a una suite fiable, en un día o dos.","La checklist de la suite de goldens",[199,2235,2237],{"id":2236},"lo-que-cuesta-a-los-seis-meses","Lo que cuesta a los seis meses",[175,2239,2240,2241,2244],{},"Montar la suite es un día, quizá dos, la mayor parte en fuentes y fontanería de CI. Escribir cada golden son minutos. El coste recurrente es el que nadie calcula: ",[210,2242,2243],{},"cada actualización de Flutter regenera cada imagen."," Con la matriz de arriba son 60–90 PNG en un commit, dos a cuatro veces al año, cada uno con su propia justificación. Si no puedes permitirte esa revisión, no puedes permitirte la suite, porque una regeneración sin revisar es indistinguible de no tener test, solo que tarda más.",[175,2246,2247,2248,2251,2252,257],{},"A cambio obtienes las regresiones que ningún otro tipo de test atrapa: el estado vacío que perdió su padding en modo oscuro, la traducción rusa que se salió de un botón de 320pt, el sistema de diseño derivando porque alguien cambió un ",[185,2249,2250],{},"TextStyle"," en el tema. Y el caso que nos hizo prestar atención desde el principio: un agente o un compañero cambiando un componente compartido y siendo sinceramente incapaz de ver las cuatro pantallas que rompió. Un golden es lo más parecido que tiene Flutter a darle ojos a un cambio de código, que es exactamente el argumento que hicimos en ",[2253,2254,2256],"a",{"href":2255},"\u002Fblog\u002Fai-agents-struggle-with-flutter","por qué los agentes de IA se atascan con Flutter",[199,2258,2260],{"id":2259},"la-versión-corta","La versión corta",[175,2262,2263],{},"Un golden test es una comparación de píxeles contra un PNG, y todo lo que lo hace útil tiene que ver con las condiciones de esa comparación. Carga tus fuentes, carga tu fuente de iconos, fija la superficie, fija el toolchain, regenera en la plataforma que usa CI y no dejes nunca que una máquina apruebe el diff.",[175,2265,2266],{},"Y luego mantén la suite lo bastante pequeña como para que una persona siga mirándola. El modo de fallo de los golden tests no es un test inestable: son 214 imágenes que pasan todas porque comparan cajas con cajas, y una marca verde que lleva ocho meses contando la misma historia cómoda.",[199,2268,2270],{"id":2269},"fuentes","Fuentes",[204,2272,2273,2288,2298,2312,2331,2341,2359,2376,2387,2397,2407,2417],{},[207,2274,2275,2278,2279,2281,2282],{},[185,2276,2277],{},"matchesGoldenFile"," — qué captura el matcher y dónde se engancha ",[185,2280,433],{},": ",[2253,2283,2287],{"href":2284,"rel":2285},"https:\u002F\u002Fapi.flutter.dev\u002Fflutter\u002Fflutter_test\u002FmatchesGoldenFile.html",[2286],"nofollow","api.flutter.dev",[207,2289,2290,2293,2294],{},[185,2291,2292],{},"FontLoader"," — registrar una fuente por nombre de familia dentro de un test: ",[2253,2295,2287],{"href":2296,"rel":2297},"https:\u002F\u002Fapi.flutter.dev\u002Fflutter\u002Fservices\u002FFontLoader-class.html",[2286],[207,2299,2300,2302,2303,2306,2307],{},[185,2301,228],{}," — declarado en ",[185,2304,2305],{},"painting\u002Fdebug.dart",", activado por el binding de test y reactivado por los propios tests de sombras de Flutter: ",[2253,2308,2311],{"href":2309,"rel":2310},"https:\u002F\u002Fgithub.com\u002Fflutter\u002Fflutter\u002Fblob\u002Fmaster\u002Fpackages\u002Fflutter\u002Flib\u002Fsrc\u002Fpainting\u002Fdebug.dart",[2286],"github.com\u002Fflutter\u002Fflutter",[207,2313,2314,2317,2318,1916,2321,1761,2324,2281,2327],{},[185,2315,2316],{},"TestFlutterView"," — ",[185,2319,2320],{},"physicalSize",[185,2322,2323],{},"devicePixelRatio",[185,2325,2326],{},"reset()",[2253,2328,2287],{"href":2329,"rel":2330},"https:\u002F\u002Fapi.flutter.dev\u002Fflutter\u002Fflutter_test\u002FTestFlutterView-class.html",[2286],[207,2332,2333,2336,2337],{},[185,2334,2335],{},"WidgetTester.pumpAndSettle"," — «repeatedly calls pump with the given duration until there are no longer any frames scheduled»: ",[2253,2338,2287],{"href":2339,"rel":2340},"https:\u002F\u002Fapi.flutter.dev\u002Fflutter\u002Fflutter_test\u002FWidgetTester\u002FpumpAndSettle.html",[2286],[207,2342,2343,2317,2346,1916,2349,1916,2352,2281,2355],{},[185,2344,2345],{},"AccessibilityGuideline",[185,2347,2348],{},"androidTapTargetGuideline",[185,2350,2351],{},"iOSTapTargetGuideline",[185,2353,2354],{},"textContrastGuideline",[2253,2356,2287],{"href":2357,"rel":2358},"https:\u002F\u002Fapi.flutter.dev\u002Fflutter\u002Fflutter_test\u002FAccessibilityGuideline-class.html",[2286],[207,2360,2361,2362,2365,2366,1916,2371],{},"Goldens del framework triados en Skia Gold, y el repositorio ",[185,2363,2364],{},"flutter\u002Fgoldens"," con los que no pueden usarlo: ",[2253,2367,2370],{"href":2368,"rel":2369},"https:\u002F\u002Fskia.org\u002Fdocs\u002Fdev\u002Ftesting\u002Fskiagold\u002F",[2286],"skia.org",[2253,2372,2375],{"href":2373,"rel":2374},"https:\u002F\u002Fgithub.com\u002Fflutter\u002Fgoldens",[2286],"github.com\u002Fflutter\u002Fgoldens",[207,2377,2378,2381,2382],{},[185,2379,2380],{},"alchemist"," — carpetas de goldens por plataforma, goldens agrupados de varios widgets, configuración consciente de CI: ",[2253,2383,2386],{"href":2384,"rel":2385},"https:\u002F\u002Fpub.dev\u002Fpackages\u002Falchemist",[2286],"pub.dev",[207,2388,2389,2392,2393],{},[185,2390,2391],{},"golden_screenshot"," — tests de captura con previsualizaciones de dispositivo y goldens por plataforma: ",[2253,2394,2386],{"href":2395,"rel":2396},"https:\u002F\u002Fpub.dev\u002Fpackages\u002Fgolden_screenshot",[2286],[207,2398,2399,2402,2403],{},[185,2400,2401],{},"golden_toolkit"," — el paquete que aún recomiendan casi todos los tutoriales de golden tests; descontinuado, última versión en febrero de 2023: ",[2253,2404,2386],{"href":2405,"rel":2406},"https:\u002F\u002Fpub.dev\u002Fpackages\u002Fgolden_toolkit",[2286],[207,2408,2409,2412,2413],{},[185,2410,2411],{},"spot"," — widget tests tipo golden que comparan un árbol de widgets serializado en vez de píxeles, cuando la inestabilidad de píxeles es el problema mayor: ",[2253,2414,2386],{"href":2415,"rel":2416},"https:\u002F\u002Fpub.dev\u002Fpackages\u002Fspot",[2286],[207,2418,2419,2421,2422,2425,2426],{},[185,2420,2169],{}," — capturas tomadas en un dispositivo real (llama antes a ",[185,2423,2424],{},"convertFlutterSurfaceToImage()"," en Android), la única respuesta a «cómo se ve en un teléfono»: ",[2253,2427,2430],{"href":2428,"rel":2429},"https:\u002F\u002Fgithub.com\u002Fflutter\u002Fflutter\u002Fblob\u002Fmaster\u002Fpackages\u002Fintegration_test\u002Flib\u002Fintegration_test.dart",[2286],"integration_test.dart",[199,2432,2434],{"id":2433},"preguntas-frecuentes","Preguntas frecuentes",[2436,2437],"questions",{":items":2438},"[{\"title\": \"¿Qué es un golden test en Flutter?\", \"text\": \"Un golden test renderiza un widget y compara el resultado píxel a píxel con un PNG guardado en el repositorio. El PNG se crea ejecutando flutter test con el flag update-goldens, y cada ejecución posterior falla si el renderizado difiere. En Flutter es la forma más barata de atrapar regresiones visuales, porque corre en el mismo proceso de test que los widget tests normales y tarda segundos.\"}, {\"title\": \"¿Por qué el texto se ve como cajas negras en mis golden files?\", \"text\": \"Porque el entorno de test no carga las fuentes declaradas en tu pubspec. El texto sin familia disponible cae en una fuente de test cuyos glifos son rectángulos rellenos, lo que mantiene la maquetación determinista y evita necesitar archivos de fuentes. Registra las fuentes reales con FontLoader en test\u002Fflutter_test_config.dart, incluida la fuente de iconos, y los goldens dibujarán texto de verdad.\"}, {\"title\": \"¿Por qué los golden tests pasan en local y fallan en CI?\", \"text\": \"Los píxeles dependen del sistema operativo anfitrión y de la versión de Flutter, no solo de tu árbol de widgets. El antialiasing y el hinting del texto difieren entre macOS y Linux, así que una suite generada en el portátil de un desarrollador no coincide con un runner Linux. Genera los goldens en la misma plataforma y con la misma versión fijada de Flutter que usa CI, y trata una actualización de Flutter como un commit que regenera todas las imágenes a propósito.\"}, {\"title\": \"¿Hay que commitear los golden files a git?\", \"text\": \"Sí. Todo el valor de un golden está en que su cambio se vea en la revisión, y eso exige que la imagen viva en el repositorio junto al código, en la misma rama, cambiando en el mismo commit que lo que cambió el renderizado. La excepción son los artefactos de fallo que escribe el comparador: la referencia, el render nuevo y las dos imágenes de diff van al gitignore y a los artefactos de integración continua.\"}, {\"title\": \"¿Es pumpAndSettle la forma correcta de renderizar un estado de carga en un golden?\", \"text\": \"No. pumpAndSettle avanza hasta que no queda ningún fotograma programado, y un CircularProgressIndicator indeterminado programa fotogramas para siempre, así que la llamada agota el tiempo. Usa tester.pump con una duración explícita para capturar una fase fija del spinner. Lo que estás probando es que la maquetación alrededor del spinner aguanta; el spinner en sí no es lo que cambia.\"}, {\"title\": \"¿Sustituyen los golden tests a los widget tests?\", \"text\": \"No, cubren cosas distintas. Un golden afirma sobre píxeles, así que no puede decirte que un botón llama al callback equivocado, que la validación dejó de ejecutarse o que un diálogo no se cierra, porque todo eso se renderiza igual. El comportamiento lo cubren los widget tests, y la semántica y las áreas táctiles las cubren aserciones como meetsGuideline, que una captura no ve en absoluto.\"}, {\"title\": \"¿Cuántos golden tests debería tener una app Flutter?\", \"text\": \"Los suficientes para que una persona siga mirando los diffs, lo que en la práctica son unas 60 a 90 imágenes: los componentes del sistema de diseño en sus estados y en ambos temas, las pocas pantallas que importan en tres anchos, y los estados vacíos, de error y de carga. Multiplicar cada pantalla por cada tema, ancho, escala de texto e idioma produce cientos de imágenes que se aprueban en un clic, y una suite que se aprueba en un clic no está probando nada.\"}, {\"title\": \"¿Qué paquete de golden tests debería usar?\", \"text\": \"Empieza con lo que trae flutter_test: matchesGoldenFile, un flutter_test_config.dart para cargar fuentes y un pequeño helper que fija el tamaño de la superficie. Paquetes como alchemist, golden_screenshot y spot añaden carpetas de goldens por plataforma, previsualizaciones de dispositivo o comparación por árbol de widgets, y merecen la pena cuando te topas con el problema que resuelve cada uno. golden_toolkit, que casi todos los tutoriales siguen recomendando, se descontinuó en 2023.\"}]",[175,2440,2441,2442,2446,2447,2451],{},"Si estás montando la capa de tests desde cero, las decisiones que la rodean están en nuestro repaso del ",[2253,2443,2445],{"href":2444},"\u002Fblog\u002Fflutter-app-development-process","proceso de desarrollo de una app Flutter",", y el problema de modelo de estado que impide testear media interfaz está en ",[2253,2448,2450],{"href":2449},"\u002Fblog\u002Fimpossible-states-dart-sealed-classes","cinco estados que no pueden existir",". El tema es el mismo en los tres: una comprobación que pasa no es lo mismo que una comprobación que mira.",[2453,2454,2455],"post-credits",{},[175,2456,2457,2458,2462,2463,2467],{},"Maxim es ingeniero de software senior en ",[2253,2459,2461],{"href":2460},"\u002F","Nerdy Production",", una agencia Flutter-first que construye y mantiene apps en fintech, salud y retail. También hacemos ",[2253,2464,2466],{"href":2465},"\u002Fservices\u002Fflutter-app-development","desarrollo de apps en Flutter"," para equipos que prefieren saber qué compara su suite de tests antes de la próxima release y no después.",[2469,2470,2471],"style",{},"html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZMiF, html code.shiki .sZMiF{--shiki-light:#E2931D;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sw1J6, html code.shiki .sw1J6{--shiki-light:#F76D47;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s39Yj, html code.shiki .s39Yj{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":286,"searchDepth":322,"depth":322,"links":2473},[2474,2475,2476,2484,2485,2486,2487,2488,2489,2490,2491],{"id":201,"depth":322,"text":202},{"id":276,"depth":322,"text":277},{"id":475,"depth":322,"text":476,"children":2477},[2478,2479,2480,2481,2482],{"id":480,"depth":357,"text":481},{"id":845,"depth":357,"text":846},{"id":877,"depth":357,"text":878},{"id":922,"depth":357,"text":923},{"id":978,"depth":357,"text":2483},"5. pumpAndSettle no vuelve nunca en un estado de carga",{"id":1119,"depth":322,"text":1120},{"id":1789,"depth":322,"text":1790},{"id":1902,"depth":322,"text":1903},{"id":2179,"depth":322,"text":2180},{"id":2236,"depth":322,"text":2237},{"id":2259,"depth":322,"text":2260},{"id":2269,"depth":322,"text":2270},{"id":2433,"depth":322,"text":2434},"md",{"type":2494,"src":2495},"image","\u002Fblog\u002Fflutter-golden-tests.webp",{},"Los golden tests son la red de regresión visual más barata de Flutter y la más fácil de falsear: fuentes, CI fijado, la matriz que compensa y revisión.","Golden tests en Flutter que no te engañan","Cada glifo un rectángulo negro, sombras aplanadas en bloques duros, píxeles atados al sistema anfitrión. Qué compara de verdad un golden test y cómo hacerlo honesto.","\u002Fblog\u002Fflutter-golden-tests",{"title":169,"description":177},"flutter-golden-tests","blog\u002Fflutter-golden-tests","Los golden tests son el test de interfaz con más retorno en Flutter y el más fácil de convertir en un mero trámite. El primero que escribas dibujará cada glifo como un rectángulo negro, porque el entorno de test no tiene fuentes; las sombras se aplanan en bloques duros; y los píxeles dependen del sistema operativo anfitrión y de la versión de Flutter, así que una suite generada en un Mac falla en CI Linux. Aquí tienes la configuración que hace honestos los goldens, la matriz por la que vale la pena pagar, lo que no pueden comprobar y las reglas de revisión de las que depende que toda la suite justifique su mantenimiento.","2026-11-10T12:00:00Z",null,"X57Hw1eNEo8yXwoyaCnpmqZsnXcKq-3aYBctQUuXIps",[2509,2565,2612,2661,2751,2803],{"id":2510,"bio":2511,"expertise":2516,"extension":1161,"links":2532,"meta":2536,"metaDescription":2537,"name":2542,"ogDescription":2545,"photo":2550,"role":2551,"seniority":2556,"skills":2557,"slug":2558,"specialization":2559,"stem":2558,"__hash__":2564},"team_members\u002Fdima.yaml",{"en":2512,"ru":2513,"es":2514,"nl":2515},"Dima has been building with [Flutter](\u002Ftechnologies\u002Fflutter) since 2021, and specializes in :term[state management]{slug=\"state-management\"} and app architecture.\n\nHe has hands-on experience with real-time communication protocols — :term{slug=\"webrtc\"} for audio and video, and :term{slug=\"xmpp\"} for messaging — which makes him comfortable with the network-heavy, stateful features many teams struggle to get right.\n","Дима работает с [Flutter](\u002Ftechnologies\u002Fflutter) с 2021 года и специализируется на :term[state management]{slug=\"state-management\"} и архитектуре приложений.\n\nУ него есть практический опыт с протоколами реального времени — :term{slug=\"webrtc\"} для аудио и видео и :term{slug=\"xmpp\"} для обмена сообщениями, — поэтому ему близки сетевые stateful-фичи, которые многим командам даются с трудом.\n","Dima trabaja con [Flutter](\u002Ftechnologies\u002Fflutter) desde 2021 y se especializa en :term[gestión de estado]{slug=\"state-management\"} y arquitectura de aplicaciones.\n\nTiene experiencia práctica con protocolos de comunicación en tiempo real —:term{slug=\"webrtc\"} para audio y vídeo, y :term{slug=\"xmpp\"} para mensajería—, lo que le hace sentirse cómodo con esas funcionalidades con mucho estado y mucha red que a muchos equipos se les atragantan.\n","Dima werkt sinds 2021 met [Flutter](\u002Ftechnologies\u002Fflutter) en is gespecialiseerd in :term[state management]{slug=\"state-management\"} en applicatiearchitectuur.\n\nHij heeft praktijkervaring met protocollen voor realtime communicatie — :term{slug=\"webrtc\"} voor audio en video, en :term{slug=\"xmpp\"} voor berichten — waardoor hij zich thuis voelt bij de netwerkzware functies met veel staat waar veel teams moeite mee hebben.\n",[2517,2520,2525,2527],{"en":2518,"ru":2518,"es":2519,"nl":2518},"State management","Gestión de estado",{"en":2521,"ru":2522,"es":2523,"nl":2524},"Application architecture","Архитектура приложений","Arquitectura de aplicaciones","Applicatiearchitectuur",{"en":2526,"ru":2526,"es":2526,"nl":2526},"WebRTC",{"en":2528,"ru":2529,"es":2530,"nl":2531},"XMPP messaging","Обмен сообщениями по XMPP","Mensajería XMPP","Berichten via XMPP",[2533],{"type":2534,"address":2535},"email","konopatov@nerdy.pro",{},{"en":2538,"ru":2539,"es":2540,"nl":2541},"Lead Flutter developer at Nerdy Production, building real-time apps since 2021 — state management, app architecture, WebRTC and XMPP.","Ведущий Flutter-разработчик Nerdy Production: приложения реального времени с 2021 года — state management, архитектура, WebRTC и XMPP.","Lead de desarrollo Flutter en Nerdy Production, construyendo apps en tiempo real desde 2021: gestión de estado, arquitectura de aplicaciones, WebRTC y XMPP.","Lead Flutter-developer bij Nerdy Production, bouwt sinds 2021 realtime-apps — state management, applicatiearchitectuur, WebRTC en XMPP.",{"en":2543,"ru":2544,"es":2543,"nl":2543},"Dima","Дима",{"en":2546,"ru":2547,"es":2548,"nl":2549},"Lead Flutter developer at Nerdy Production — real-time apps since 2021, and the state management that keeps them from falling over.","Ведущий Flutter-разработчик Nerdy Production: приложения реального времени с 2021 года и state management, на котором они держатся.","Lead de desarrollo Flutter en Nerdy Production: apps en tiempo real desde 2021 y la gestión de estado que evita que se caigan.","Lead Flutter-developer bij Nerdy Production — realtime-apps sinds 2021, en het state management dat ze overeind houdt.","\u002Fteam\u002Fdima.webp",{"en":2552,"ru":2553,"es":2554,"nl":2555},"Lead Flutter Developer","Ведущий Flutter-разработчик","Lead de desarrollo Flutter","Lead Flutter-developer","lead",[41,18,155,160,90,111,150,95],"dima",{"en":2560,"ru":2561,"es":2562,"nl":2563},"Real-time apps, state management, and architecture","Приложения реального времени, state management и архитектура","Apps en tiempo real, gestión de estado y arquitectura","Realtime-apps, state management en architectuur","1TuxUt3zjdVkQUvMf9dBITG872C0exrdZyyoKw1beiE",{"id":2566,"bio":2567,"expertise":2572,"extension":1161,"links":2506,"meta":2583,"metaDescription":2584,"name":2589,"ogDescription":2592,"photo":2597,"role":2598,"seniority":2603,"skills":2604,"slug":2605,"specialization":2606,"stem":2605,"__hash__":2611},"team_members\u002Fmasha.yaml",{"en":2568,"ru":2569,"es":2570,"nl":2571},"Masha builds [Flutter](\u002Ftechnologies\u002Fflutter) apps where design and copy are treated as one job.\n\nShe specializes in UI\u002FUX — turning product requirements into clean, usable interfaces — and in the writing inside the app, from onboarding flows to the microcopy that makes a screen make sense. The result is apps that feel considered, not just functional.\n","Маша делает приложения на [Flutter](\u002Ftechnologies\u002Fflutter), где дизайн и текст — одна задача.\n\nОна специализируется на UI\u002FUX, превращая продуктовые требования в чистые и удобные интерфейсы, и на текстах внутри приложения — от онбординга до микрокопирайта, который делает экран понятным. В итоге приложения получаются продуманными, а не просто рабочими.\n","Masha crea apps en [Flutter](\u002Ftechnologies\u002Fflutter) donde el diseño y el texto se tratan como un mismo trabajo.\n\nSe especializa en UI\u002FUX —convertir requisitos de producto en interfaces limpias y usables— y en la escritura dentro de la app, desde los flujos de onboarding hasta el microcopy que hace que una pantalla se entienda. El resultado son apps que se sienten pensadas, no solo funcionales.\n","Masha bouwt [Flutter](\u002Ftechnologies\u002Fflutter)-apps waarin ontwerp en tekst als één taak worden behandeld.\n\nZe is gespecialiseerd in UI\u002FUX — productwensen omzetten in heldere, bruikbare interfaces — en in de teksten binnen de app, van onboarding tot de microteksten die een scherm begrijpelijk maken. Het resultaat zijn apps die doordacht aanvoelen en niet alleen werken.\n",[2573,2578],{"en":2574,"ru":2575,"es":2576,"nl":2577},"UI\u002FUX design","UI\u002FUX-дизайн","Diseño UI\u002FUX","UI\u002FUX-ontwerp",{"en":2579,"ru":2580,"es":2581,"nl":2582},"Product copywriting","Продуктовый копирайтинг","Redacción de producto","Productteksten",{},{"en":2585,"ru":2586,"es":2587,"nl":2588},"Flutter developer at Nerdy Production working where design meets copy — UI\u002FUX, onboarding flows, and the microcopy that makes a screen make sense.","Flutter-разработчик Nerdy Production на стыке дизайна и текста: UI\u002FUX, онбординг и микрокопирайт, который делает экран понятным.","Desarrolladora Flutter en Nerdy Production, donde el diseño se encuentra con el texto: UI\u002FUX, onboarding y el microcopy que hace que una pantalla se entienda.","Flutter-developer bij Nerdy Production op het snijvlak van ontwerp en tekst — UI\u002FUX, onboarding, en de microteksten die een scherm begrijpelijk maken.",{"en":2590,"ru":2591,"es":2590,"nl":2590},"Masha","Маша",{"en":2593,"ru":2594,"es":2595,"nl":2596},"Flutter developer at Nerdy Production working where design meets copy — onboarding flows and the microcopy that makes a screen make sense.","Flutter-разработчик Nerdy Production на стыке дизайна и текста: онбординг и микрокопирайт, который делает экран понятным.","Desarrolladora Flutter en Nerdy Production, donde el diseño se encuentra con el texto: onboarding y el microcopy que hace entender una pantalla.","Flutter-developer bij Nerdy Production op het snijvlak van ontwerp en tekst — onboarding en de microteksten die een scherm begrijpelijk maken.","\u002Fteam\u002Fmasha.webp",{"en":2599,"ru":2600,"es":2601,"nl":2602},"Flutter Developer","Flutter-разработчик","Desarrolladora Flutter","Flutter-developer","middle",[41,18],"masha",{"en":2607,"ru":2608,"es":2609,"nl":2610},"UI\u002FUX and copywriting for Flutter apps","UI\u002FUX и копирайтинг для Flutter-приложений","UI\u002FUX y redacción para apps Flutter","UI\u002FUX en teksten voor Flutter-apps","z3c64iWR39RIrn8wE0elSy00IfCITYgNkbgp_G_sslk",{"id":2613,"bio":2614,"expertise":2619,"extension":1161,"links":2630,"meta":2633,"metaDescription":2634,"name":2639,"ogDescription":2642,"photo":2647,"role":2648,"seniority":2653,"skills":2654,"slug":170,"specialization":2655,"stem":170,"__hash__":2660},"team_members\u002Fmaxim.yaml",{"en":2615,"ru":2616,"es":2617,"nl":2618},"Maxim is a polyglot engineer who moves comfortably across [Go](\u002Ftechnologies\u002Fgo), [Flutter](\u002Ftechnologies\u002Fflutter), [Python](\u002Ftechnologies\u002Fpython), and [TypeScript](\u002Ftechnologies\u002Ftypescript), which lets him own a feature from the backend to the screen.\n\nHe came up at Ozon, one of Russia's largest marketplaces, where scale makes reliability non-negotiable — and it shows in his work: testing is a first-class part of how he builds, not an afterthought bolted on at the end.\n","Максим — полиглот-инженер, свободно работающий с [Go](\u002Ftechnologies\u002Fgo), [Flutter](\u002Ftechnologies\u002Fflutter), [Python](\u002Ftechnologies\u002Fpython) и [TypeScript](\u002Ftechnologies\u002Ftypescript), что позволяет ему вести фичу от бэкенда до экрана.\n\nОн вырос в Ozon, одном из крупнейших маркетплейсов России, где масштаб делает надёжность обязательной, — и это видно в его работе: тестирование для него первоклассная часть разработки, а не то, что прикручивают в конце.\n","Maxim es un ingeniero políglota que se mueve con soltura entre [Go](\u002Ftechnologies\u002Fgo), [Flutter](\u002Ftechnologies\u002Fflutter), [Python](\u002Ftechnologies\u002Fpython) y [TypeScript](\u002Ftechnologies\u002Ftypescript), lo que le permite hacerse cargo de una funcionalidad desde el backend hasta la pantalla.\n\nSe formó en Ozon, uno de los mayores marketplaces de Rusia, donde la escala hace que la fiabilidad no sea negociable, y se le nota: el testing es para él una parte de primera clase de cómo construye, no un añadido al final.\n","Maxim is een polyglotte engineer die zich moeiteloos beweegt tussen [Go](\u002Ftechnologies\u002Fgo), [Flutter](\u002Ftechnologies\u002Fflutter), [Python](\u002Ftechnologies\u002Fpython) en [TypeScript](\u002Ftechnologies\u002Ftypescript), waardoor hij een functie van de backend tot het scherm kan dragen.\n\nHij is opgegroeid bij Ozon, een van de grootste marktplaatsen van Rusland, waar schaal betrouwbaarheid onderhandelbaar maakt noch toestaat — en dat zie je terug in zijn werk: testen is bij hem een volwaardig onderdeel van hoe hij bouwt, geen bijzaak die er aan het eind bij komt.\n",[2620,2625],{"en":2621,"ru":2622,"es":2623,"nl":2624},"Automated testing","Автоматизированное тестирование","Testing automatizado","Geautomatiseerd testen",{"en":2626,"ru":2627,"es":2628,"nl":2629},"High-load backend systems","Высоконагруженные бэкенд-системы","Sistemas backend de alta carga","Backendsystemen met hoge belasting",[2631],{"type":2534,"address":2632},"maxim@nerdy.pro",{},{"en":2635,"ru":2636,"es":2637,"nl":2638},"Senior engineer at Nerdy Production across Go, Flutter, Python and TypeScript — from backend to screen, with automated testing built in.","Старший инженер Nerdy Production: Go, Flutter, Python и TypeScript — от бэкенда до экрана, с автотестами как частью разработки.","Ingeniero senior en Nerdy Production con Go, Flutter, Python y TypeScript: del backend a la pantalla, con testing automatizado incorporado.","Senior engineer bij Nerdy Production met Go, Flutter, Python en TypeScript — van backend tot scherm, met geautomatiseerd testen ingebouwd.",{"en":2640,"ru":2641,"es":2640,"nl":2640},"Maxim","Максим",{"en":2643,"ru":2644,"es":2645,"nl":2646},"Senior engineer at Nerdy Production working from backend to screen — Go, Flutter, Python and TypeScript, with the tests written as he goes.","Старший инженер Nerdy Production, работающий от бэкенда до экрана: Go, Flutter, Python и TypeScript — и тесты, которые пишутся по ходу дела.","Ingeniero senior en Nerdy Production, del backend a la pantalla: Go, Flutter, Python y TypeScript, con los tests escritos sobre la marcha.","Senior engineer bij Nerdy Production, van backend tot scherm — Go, Flutter, Python en TypeScript, met de tests die hij onderweg schrijft.","\u002Fteam\u002Fmax.webp",{"en":2649,"ru":2650,"es":2651,"nl":2652},"Senior Software Engineer","Старший инженер-программист","Ingeniero de software senior","Senior software engineer","senior",[41,18,51,106,155,13,150],{"en":2656,"ru":2657,"es":2658,"nl":2659},"Backend and Flutter engineering with a testing focus","Бэкенд и Flutter с фокусом на тестирование","Ingeniería backend y Flutter con foco en testing","Backend- en Flutter-engineering met focus op testen","a-8Kx1i-PQXIGU92_Rimqr3bPEur6ZwMfPKQWr2uk4I",{"id":2662,"bio":2663,"expertise":2668,"extension":1161,"links":2710,"meta":2722,"metaDescription":2723,"name":2728,"ogDescription":2731,"photo":2736,"role":2737,"seniority":2742,"skills":2743,"slug":2744,"specialization":2745,"stem":2744,"__hash__":2750},"team_members\u002Fnixan.yaml",{"en":2664,"ru":2665,"es":2666,"nl":2667},"Ilya founded Nerdy Production and leads its engineering. He has been building software since 2010 and shipping production Flutter since 2018.\n\nBefore that he was CTO of QIWI, one of Russia's largest payment platforms, where he ran roughly 12 engineering teams spanning web products down to card processing, :term{slug=\"pci-dss\"} scope, and contactless payments — including building contactless card payments on Android via :term[Host Card Emulation]{slug=\"host-card-emulation\"} over ISO\u002FIEC 14443, with EMV Contactless (Visa PayWave) on top.\n\nHe was also a principal developer at Yandex, where he worked on Yandex.Auto — taking native Android deep into the vehicle, with heavy CAN-bus integration through a custom CAN shield — and a principal at Evotor, whose point-of-sale devices run on a forked :term{slug=\"aosp\"}, giving him a low-level view of Android most app developers never touch.\n\nToday he leads delivery on the agency's flagship apps — from the chart-heavy fintech UI of [ExtraETF](\u002Fportfolio\u002Fextraetf) to the fully custom design system of [Arcana](\u002Fportfolio\u002Farcana). He writes most of the essays on this blog and maintains the agency's open-source work, including the [dxpdf](\u002Fopen-source\u002Fdxpdf) DOCX-to-PDF engine.\n\nHe works across [Flutter](\u002Ftechnologies\u002Fflutter), native iOS and Android, [Go](\u002Ftechnologies\u002Fgo), [Rust](\u002Ftechnologies\u002Frust), [TypeScript](\u002Ftechnologies\u002Ftypescript), [Kotlin](\u002Ftechnologies\u002Fkotlin), [Kubernetes](\u002Ftechnologies\u002Fkubernetes), and [Docker](\u002Ftechnologies\u002Fdocker), with a focus on app architecture, cross-platform delivery, and building teams that ship.\n","Илья основал Nerdy Production и руководит инженерной командой. Он занимается разработкой с 2010 года и выпускает продакшн-приложения на Flutter с 2018-го.\n\nДо этого он был CTO QIWI — одной из крупнейших платёжных платформ России, — где руководил примерно 12 инженерными командами: от веб-продуктов до карточного процессинга, зоны :term{slug=\"pci-dss\"} и бесконтактных платежей, включая бесконтактную оплату картой на Android через :term[Host Card Emulation]{slug=\"host-card-emulation\"} поверх ISO\u002FIEC 14443, с платёжным протоколом EMV Contactless (Visa PayWave).\n\nОн также был принципал-разработчиком в Яндексе, где работал над Яндекс.Авто, уводя нативный Android глубоко в автомобиль, с серьёзной интеграцией по шине CAN через собственный CAN-шилд, и принципалом в Эвоторе, чьи кассовые устройства работают на форке :term{slug=\"aosp\"}, что дало ему низкоуровневый взгляд на Android, недоступный большинству прикладных разработчиков.\n\nСейчас он ведёт поставку флагманских приложений агентства — от насыщенного графиками финтех-интерфейса [ExtraETF](\u002Fportfolio\u002Fextraetf) до полностью кастомной дизайн-системы [Arcana](\u002Fportfolio\u002Farcana). Он пишет большую часть материалов этого блога и поддерживает open-source агентства, включая движок [dxpdf](\u002Fopen-source\u002Fdxpdf) для конвертации DOCX в PDF.\n\nРаботает с [Flutter](\u002Ftechnologies\u002Fflutter), нативными iOS и Android, [Go](\u002Ftechnologies\u002Fgo), [Rust](\u002Ftechnologies\u002Frust), [TypeScript](\u002Ftechnologies\u002Ftypescript), [Kotlin](\u002Ftechnologies\u002Fkotlin), [Kubernetes](\u002Ftechnologies\u002Fkubernetes) и [Docker](\u002Ftechnologies\u002Fdocker); его фокус — архитектура приложений, кросс-платформенная поставка и построение команд, которые доводят продукт до релиза.\n","Ilya fundó Nerdy Production y dirige su ingeniería. Lleva construyendo software desde 2010 y entregando Flutter en producción desde 2018.\n\nAntes fue CTO de QIWI, una de las mayores plataformas de pago de Rusia, donde dirigió alrededor de 12 equipos de ingeniería que abarcaban desde productos web hasta procesamiento de tarjetas, alcance :term{slug=\"pci-dss\"} y pagos contactless, incluida la construcción de pagos contactless con tarjeta en Android mediante :term[Host Card Emulation]{slug=\"host-card-emulation\"} sobre ISO\u002FIEC 14443, con EMV Contactless (Visa PayWave) por encima.\n\nTambién fue principal developer en Yandex, donde trabajó en Yandex.Auto —llevando Android nativo hasta el interior del vehículo, con una integración intensiva por bus CAN a través de un CAN shield propio— y principal en Evotor, cuyos terminales de punto de venta funcionan sobre un fork de :term{slug=\"aosp\"}, lo que le dio una visión de bajo nivel de Android que la mayoría de desarrolladores de apps nunca llega a tocar.\n\nHoy lidera la entrega de las apps insignia de la agencia, desde la interfaz fintech cargada de gráficos de [ExtraETF](\u002Fportfolio\u002Fextraetf) hasta el sistema de diseño totalmente a medida de [Arcana](\u002Fportfolio\u002Farcana). Escribe la mayoría de los artículos de este blog y mantiene el trabajo de código abierto de la agencia, incluido el motor de conversión de DOCX a PDF [dxpdf](\u002Fopen-source\u002Fdxpdf).\n\nTrabaja con [Flutter](\u002Ftechnologies\u002Fflutter), iOS y Android nativos, [Go](\u002Ftechnologies\u002Fgo), [Rust](\u002Ftechnologies\u002Frust), [TypeScript](\u002Ftechnologies\u002Ftypescript), [Kotlin](\u002Ftechnologies\u002Fkotlin), [Kubernetes](\u002Ftechnologies\u002Fkubernetes) y [Docker](\u002Ftechnologies\u002Fdocker), con foco en arquitectura de aplicaciones, entrega multiplataforma y la construcción de equipos que entregan.\n","Ilya richtte Nerdy Production op en leidt de engineering. Hij bouwt software sinds 2010 en levert sinds 2018 Flutter in productie.\n\nDaarvoor was hij CTO van QIWI, een van de grootste betaalplatforms van Rusland, waar hij zo'n 12 engineeringteams aanstuurde die reikten van webproducten tot kaartverwerking, :term{slug=\"pci-dss\"}-scope en contactloos betalen — waaronder het bouwen van contactloze kaartbetalingen op Android via :term[Host Card Emulation]{slug=\"host-card-emulation\"} over ISO\u002FIEC 14443, met EMV Contactless (Visa PayWave) daarbovenop.\n\nHij was ook principal developer bij Yandex, waar hij aan Yandex.Auto werkte — native Android diep de auto in brengen, met zware integratie over de CAN-bus via een eigen CAN-shield — en principal bij Evotor, waarvan de kassa-apparaten op een fork van :term{slug=\"aosp\"} draaien, wat hem een blik op Android op laag niveau gaf die de meeste app-ontwikkelaars nooit krijgen.\n\nVandaag leidt hij de oplevering van de vlaggenschipapps van het bureau — van de grafiekzware fintech-UI van [ExtraETF](\u002Fportfolio\u002Fextraetf) tot het volledig eigen designsysteem van [Arcana](\u002Fportfolio\u002Farcana). Hij schrijft de meeste artikelen op deze blog en onderhoudt het opensourcewerk van het bureau, waaronder de DOCX-naar-PDF-motor [dxpdf](\u002Fopen-source\u002Fdxpdf).\n\nHij werkt met [Flutter](\u002Ftechnologies\u002Fflutter), native iOS en Android, [Go](\u002Ftechnologies\u002Fgo), [Rust](\u002Ftechnologies\u002Frust), [TypeScript](\u002Ftechnologies\u002Ftypescript), [Kotlin](\u002Ftechnologies\u002Fkotlin), [Kubernetes](\u002Ftechnologies\u002Fkubernetes) en [Docker](\u002Ftechnologies\u002Fdocker), met de nadruk op applicatiearchitectuur, cross-platform oplevering en het bouwen van teams die opleveren.\n",[2669,2674,2679,2684,2689,2694,2699,2704,2706,2708],{"en":2670,"ru":2671,"es":2672,"nl":2673},"iOS development","Разработка под iOS","Desarrollo iOS","iOS-ontwikkeling",{"en":2675,"ru":2676,"es":2677,"nl":2678},"Software architecture","Архитектура ПО","Arquitectura de software","Softwarearchitectuur",{"en":2680,"ru":2681,"es":2682,"nl":2683},"Engineering team leadership","Руководство инженерными командами","Liderazgo de equipos de ingeniería","Leidinggeven aan engineeringteams",{"en":2685,"ru":2686,"es":2687,"nl":2688},"Payment systems","Платёжные системы","Sistemas de pago","Betaalsystemen",{"en":2690,"ru":2691,"es":2692,"nl":2693},"Card processing","Карточный процессинг","Procesamiento de tarjetas","Kaartverwerking",{"en":2695,"ru":2696,"es":2697,"nl":2698},"PCI-DSS compliance","Соответствие PCI-DSS","Cumplimiento de PCI-DSS","Naleving van PCI-DSS",{"en":2700,"ru":2701,"es":2702,"nl":2703},"NFC and contactless payments","NFC и бесконтактные платежи","NFC y pagos contactless","NFC en contactloos betalen",{"en":2705,"ru":2705,"es":2705,"nl":2705},"Host Card Emulation",{"en":2707,"ru":2707,"es":2707,"nl":2707},"EMV Contactless",{"en":2709,"ru":2709,"es":2709,"nl":2709},"AOSP",[2711,2714,2717,2720],{"type":2712,"url":2713},"github","https:\u002F\u002Fgithub.com\u002Fthenixan",{"type":2715,"url":2716},"linkedin","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fthenixan\u002F",{"type":2718,"url":2719},"telegram","https:\u002F\u002Ft.me\u002Fthenixan",{"type":2534,"address":2721},"nixan@nerdy.pro",{},{"en":2724,"ru":2725,"es":2726,"nl":2727},"Founder and lead developer at Nerdy Production, former CTO of QIWI. Flutter architecture, payments infrastructure, and engineering teams that ship.","Основатель и ведущий разработчик Nerdy Production, экс-CTO QIWI. Архитектура на Flutter, платёжная инфраструктура и команды, которые доводят до релиза.","Fundador y lead developer en Nerdy Production, ex-CTO de QIWI. Arquitectura Flutter, infraestructura de pagos y equipos de ingeniería que entregan.","Oprichter en lead developer bij Nerdy Production, oud-CTO van QIWI. Flutter-architectuur, betaalinfrastructuur en teams die opleveren.",{"en":2729,"ru":2730,"es":2729,"nl":2729},"Ilya Nixan","Илья Никсан",{"en":2732,"ru":2733,"es":2734,"nl":2735},"Founder and lead developer at Nerdy Production, former CTO of QIWI — Flutter architecture, payments infrastructure, and teams that ship.","Основатель и ведущий разработчик Nerdy Production, экс-CTO QIWI: архитектура на Flutter, платёжная инфраструктура и команды, которые доводят до релиза.","Fundador y lead developer en Nerdy Production, ex-CTO de QIWI: arquitectura Flutter, infraestructura de pagos y equipos que entregan.","Oprichter en lead developer bij Nerdy Production, oud-CTO van QIWI — Flutter-architectuur, betaalinfrastructuur en teams die opleveren.","\u002Fteam\u002Fnixan.webp",{"en":2738,"ru":2739,"es":2740,"nl":2741},"Founder & Lead Developer","Основатель и ведущий разработчик","Fundador y lead developer","Oprichter & lead developer","founder_lead",[41,18,71,141,76,146,51,126,121,155,160,90,13,150,136,7,29,80],"nixan",{"en":2746,"ru":2747,"es":2748,"nl":2749},"Flutter architecture and leading delivery teams","Архитектура на Flutter и руководство командами поставки","Arquitectura Flutter y liderazgo de equipos de entrega","Flutter-architectuur en het leiden van opleverteams","BmeQZ7JyhRFIx8527WO2t1-WklP8AAvevAxiN9U4TEs",{"id":2752,"bio":2753,"expertise":2758,"extension":1161,"links":2770,"meta":2775,"metaDescription":2776,"name":2781,"ogDescription":2784,"photo":2789,"role":2790,"seniority":2653,"skills":2795,"slug":2796,"specialization":2797,"stem":2796,"__hash__":2802},"team_members\u002Froma.yaml",{"en":2754,"ru":2755,"es":2756,"nl":2757},"Roman works across native iOS and Android and [Flutter](\u002Ftechnologies\u002Fflutter), and pairs that mobile depth with [Python](\u002Ftechnologies\u002Fpython) and applied AI.\n\nMuch of his background is in ERP and CRM development — complex, data-heavy business systems where getting the domain model right matters more than the UI — which gives him a pragmatic eye for how an app fits the process behind it.\n","Роман работает с нативными iOS и Android и с [Flutter](\u002Ftechnologies\u002Fflutter), дополняя мобильную экспертизу [Python](\u002Ftechnologies\u002Fpython) и прикладным AI.\n\nЗначительная часть его опыта — разработка ERP и CRM, сложных систем с большим объёмом данных, где правильная доменная модель важнее интерфейса, — что даёт ему прагматичный взгляд на то, как приложение встраивается в процесс за ним.\n","Roman trabaja con iOS y Android nativos y con [Flutter](\u002Ftechnologies\u002Fflutter), y combina esa profundidad móvil con [Python](\u002Ftechnologies\u002Fpython) e IA aplicada.\n\nBuena parte de su trayectoria está en el desarrollo de ERP y CRM —sistemas de negocio complejos y con mucho dato, donde acertar con el modelo de dominio importa más que la interfaz—, lo que le da una mirada pragmática sobre cómo encaja una app en el proceso que hay detrás.\n","Roman werkt met native iOS en Android en met [Flutter](\u002Ftechnologies\u002Fflutter), en combineert die mobiele diepgang met [Python](\u002Ftechnologies\u002Fpython) en toegepaste AI.\n\nEen groot deel van zijn achtergrond ligt in ERP- en CRM-ontwikkeling — complexe, datazware bedrijfssystemen waarin het domeinmodel goed krijgen zwaarder weegt dan de interface — wat hem een pragmatische blik geeft op hoe een app in het proces erachter past.\n",[2759,2760,2765],{"en":2670,"ru":2671,"es":2672,"nl":2673},{"en":2761,"ru":2762,"es":2763,"nl":2764},"Applied AI","Прикладной AI","IA aplicada","Toegepaste AI",{"en":2766,"ru":2767,"es":2768,"nl":2769},"ERP and CRM systems","ERP- и CRM-системы","Sistemas ERP y CRM","ERP- en CRM-systemen",[2771,2773],{"type":2715,"url":2772},"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fromanbatler\u002F",{"type":2534,"address":2774},"roma@nerdy.pro",{},{"en":2777,"ru":2778,"es":2779,"nl":2780},"Senior mobile developer at Nerdy Production — native iOS and Android, Flutter, applied AI, and a background in data-heavy ERP and CRM systems.","Старший мобильный разработчик Nerdy Production: нативные iOS и Android, Flutter, прикладной AI и опыт в ERP- и CRM-системах.","Desarrollador móvil senior en Nerdy Production: iOS y Android nativos, Flutter, IA aplicada y experiencia en sistemas ERP y CRM con gran volumen de datos.","Senior mobiele developer bij Nerdy Production — native iOS en Android, Flutter, toegepaste AI, en een achtergrond in datazware ERP- en CRM-systemen.",{"en":2782,"ru":2783,"es":2782,"nl":2782},"Roman","Рома",{"en":2785,"ru":2786,"es":2787,"nl":2788},"Senior mobile developer at Nerdy Production — native iOS and Android, Flutter, applied AI, and years of data-heavy ERP and CRM behind him.","Старший мобильный разработчик Nerdy Production: нативные iOS и Android, Flutter, прикладной AI и годы работы с нагруженными ERP и CRM.","Desarrollador móvil senior en Nerdy Production: iOS y Android nativos, Flutter, IA aplicada y años de ERP y CRM con mucha carga de datos.","Senior mobiele developer bij Nerdy Production — native iOS en Android, Flutter, toegepaste AI, en jaren datazware ERP en CRM achter zich.","\u002Fteam\u002Froma.webp",{"en":2791,"ru":2792,"es":2793,"nl":2794},"Senior Mobile Developer","Старший мобильный разработчик","Desarrollador móvil senior","Senior mobiele developer",[7,41,18,141,146,106],"roma",{"en":2798,"ru":2799,"es":2800,"nl":2801},"Mobile, AI, and ERP\u002FCRM systems","Мобильная разработка, AI и системы ERP\u002FCRM","Móvil, IA y sistemas ERP\u002FCRM","Mobiel, AI en ERP\u002FCRM-systemen","caPSPO2BX35G9lEjNb6p3U4uF_V7yh8cexCe6l5hm8Y",{"id":2804,"bio":2805,"expertise":2810,"extension":1161,"links":2506,"meta":2821,"metaDescription":2822,"name":2827,"ogDescription":2830,"photo":2835,"role":2836,"seniority":2653,"skills":2841,"slug":2842,"specialization":2843,"stem":2842,"__hash__":2848},"team_members\u002Fxsox.yaml",{"en":2806,"ru":2807,"es":2808,"nl":2809},"Eugene builds the systems that apps depend on. His background is in backend engineering — designing and running the APIs, data models, and services behind a product, primarily in [Python](\u002Ftechnologies\u002Fpython) and [Django](\u002Ftechnologies\u002Fdjango).\n\nHe also works in [Flutter](\u002Ftechnologies\u002Fflutter), so he can reason about a feature from the database to the device and build the backend so the app that consumes it stays simple.\n","Женя строит системы, на которые опираются приложения. Его основной опыт — бэкенд-инженерия: проектирование и эксплуатация API, моделей данных и сервисов за продуктом, в первую очередь на [Python](\u002Ftechnologies\u002Fpython) и [Django](\u002Ftechnologies\u002Fdjango).\n\nОн также работает с [Flutter](\u002Ftechnologies\u002Fflutter), поэтому может рассуждать о фиче от базы данных до устройства и строить бэкенд так, чтобы потребляющее его приложение оставалось простым.\n","Eugene construye los sistemas de los que dependen las apps. Su trayectoria está en la ingeniería backend: diseñar y operar las APIs, los modelos de datos y los servicios que hay detrás de un producto, principalmente en [Python](\u002Ftechnologies\u002Fpython) y [Django](\u002Ftechnologies\u002Fdjango).\n\nTambién trabaja con [Flutter](\u002Ftechnologies\u002Fflutter), así que puede razonar sobre una funcionalidad desde la base de datos hasta el dispositivo y construir el backend de forma que la app que lo consume siga siendo simple.\n","Eugene bouwt de systemen waar apps op leunen. Zijn achtergrond ligt in backend-engineering: de API's, datamodellen en services achter een product ontwerpen en draaien, vooral in [Python](\u002Ftechnologies\u002Fpython) en [Django](\u002Ftechnologies\u002Fdjango).\n\nHij werkt daarnaast met [Flutter](\u002Ftechnologies\u002Fflutter), zodat hij over een functie kan nadenken van de database tot het toestel en de backend zo kan bouwen dat de app die hem verbruikt eenvoudig blijft.\n",[2811,2816],{"en":2812,"ru":2813,"es":2814,"nl":2815},"API design","Проектирование API","Diseño de APIs","API-ontwerp",{"en":2817,"ru":2818,"es":2819,"nl":2820},"Data modelling","Моделирование данных","Modelado de datos","Datamodellering",{},{"en":2823,"ru":2824,"es":2825,"nl":2826},"Senior backend developer at Nerdy Production building the APIs, data models and services behind our apps, primarily in Python and Django.","Старший бэкенд-разработчик Nerdy Production: API, модели данных и сервисы за нашими приложениями, в первую очередь на Python и Django.","Desarrollador backend senior en Nerdy Production: las APIs, modelos de datos y servicios detrás de nuestras apps, sobre todo en Python y Django.","Senior backenddeveloper bij Nerdy Production die de API's, datamodellen en services achter onze apps bouwt, vooral in Python en Django.",{"en":2828,"ru":2829,"es":2828,"nl":2828},"Eugene Xsox","Женя Xsox",{"en":2831,"ru":2832,"es":2833,"nl":2834},"Senior backend developer at Nerdy Production — the APIs, data models and services our apps run on, mostly in Python and Django.","Старший бэкенд-разработчик Nerdy Production: API, модели данных и сервисы, на которых работают наши приложения, в основном на Python и Django.","Desarrollador backend senior en Nerdy Production: las APIs, modelos de datos y servicios sobre los que corren nuestras apps, en Python y Django.","Senior backenddeveloper bij Nerdy Production — de API's, datamodellen en services waarop onze apps draaien, vooral in Python en Django.","\u002Fteam\u002Fxsox.webp",{"en":2837,"ru":2838,"es":2839,"nl":2840},"Senior Backend Developer","Старший бэкенд-разработчик","Desarrollador backend senior","Senior backenddeveloper",[106,24,41,18,155,160,90,150],"xsox",{"en":2844,"ru":2845,"es":2846,"nl":2847},"Python\u002FDjango backends, plus Flutter","Бэкенды на Python\u002FDjango и Flutter","Backends en Python\u002FDjango, además de Flutter","Backends in Python\u002FDjango, plus Flutter","w_QAKKq_Mh_dtMvAxFpAJBC10PW1I7zgVOxELwRHwO8",[2850,2859,2866,2871,2881],{"id":2851,"extension":1161,"meta":2852,"name":2853,"slug":2857,"stem":2857,"__hash__":2858},"blog_topics\u002Fcybersecurity.yaml",{},{"en":2854,"ru":2855,"es":2856,"nl":2854},"Cybersecurity","Кибербезопасность","Ciberseguridad","cybersecurity","kt1rdsMy5W4b9MvGHm6mZVCr0inHidEuqaosPYmmk4E",{"id":2860,"extension":1161,"meta":2861,"name":2862,"slug":2864,"stem":2864,"__hash__":2865},"blog_topics\u002Fdevops.yaml",{},{"en":2863,"ru":2863,"es":2863,"nl":2863},"DevOps","devops","D3mxlZuFHKCGeszKZuWCiQmHmP5EiCvzg1qEAIQhnyU",{"id":2867,"extension":1161,"meta":2868,"name":2869,"slug":41,"stem":41,"__hash__":2870},"blog_topics\u002Fflutter.yaml",{},{"en":39,"ru":39,"es":39,"nl":39},"OOeALAmwFuEqByuJvTGk4g15FIHENPfYMdpfZh_HTAE",{"id":2872,"extension":1161,"meta":2873,"name":2874,"slug":2879,"stem":2879,"__hash__":2880},"blog_topics\u002Ffor-founders.yaml",{},{"en":2875,"ru":2876,"es":2877,"nl":2878},"For Founders","Для основателей","Para fundadores","Voor oprichters","for-founders","lp2u6Ol2qF4DGxpB83HxbbGzWioT-e40mJhD4lr28-U",{"id":2882,"extension":1161,"meta":2883,"name":2884,"slug":2889,"stem":2889,"__hash__":2890},"blog_topics\u002Fsoftware-engineering.yaml",{},{"en":2885,"ru":2886,"es":2887,"nl":2888},"Software Engineering","Разработка ПО","Ingeniería de software","Software-engineering","software-engineering","aZMb3aUDptoFQk_cEE6l5H4wrYcgZuBYUubmKqOAaaU",{"data":2892,"body":2893},{},{"type":2894,"children":2895},"root",[2896,2928],{"type":2897,"tag":175,"props":2898,"children":2899},"element",{},[2900,2903,2908,2909,2914,2915,2920,2921,2926],{"type":2901,"value":2902},"text","Maxim es un ingeniero políglota que se mueve con soltura entre ",{"type":2897,"tag":2253,"props":2904,"children":2906},{"href":2905},"\u002Ftechnologies\u002Fgo",[2907],{"type":2901,"value":49},{"type":2901,"value":1916},{"type":2897,"tag":2253,"props":2910,"children":2912},{"href":2911},"\u002Ftechnologies\u002Fflutter",[2913],{"type":2901,"value":39},{"type":2901,"value":1916},{"type":2897,"tag":2253,"props":2916,"children":2918},{"href":2917},"\u002Ftechnologies\u002Fpython",[2919],{"type":2901,"value":104},{"type":2901,"value":1761},{"type":2897,"tag":2253,"props":2922,"children":2924},{"href":2923},"\u002Ftechnologies\u002Ftypescript",[2925],{"type":2901,"value":153},{"type":2901,"value":2927},", lo que le permite hacerse cargo de una funcionalidad desde el backend hasta la pantalla.",{"type":2897,"tag":175,"props":2929,"children":2930},{},[2931],{"type":2901,"value":2932},"Se formó en Ozon, uno de los mayores marketplaces de Rusia, donde la escala hace que la fiabilidad no sea negociable, y se le nota: el testing es para él una parte de primera clase de cómo construye, no un añadido al final.",[2934,2948,2965,2980,2993,3005,3019,3034,3047,3062,3075,3090,3102,3115,3128,3140,3157,3170,3183,3196,3210,3222,3232,3243,3257,3267,3280,3289,3298,3311,3323,3337,3344,3356,3367,3378,3390,3405,3418,3431,3443,3454,3468,3482,3494,3505,3517,3528,3541,3553,3564,3577,3586],{"slug":2935,"term":2709,"definition":2936,"category":2937,"aliases":2938,"links":2940,"related":2944,"readMore":-1,"target":2946,"hasArticle":2947},"aosp","El Android Open Source Project: Android sin la capa de Google encima, que cualquiera puede forkear. Los terminales de punto de venta, los kioscos y los sistemas de a bordo funcionan sobre forks suyos, y trabajar a ese nivel expone partes del SO que un desarrollador de apps nunca ve.","platform",[2939],"Android Open Source Project",[2941],{"kind":2942,"url":2943},"website","https:\u002F\u002Fsource.android.com\u002F",[2945],"host-card-emulation","\u002Fglossary#aosp",false,{"slug":2949,"term":2950,"definition":2951,"category":2952,"aliases":2953,"links":2958,"related":2962,"readMore":-1,"target":2964,"hasArticle":2947},"object-storage","Almacenamiento de objetos","Almacenamiento que guarda un archivo entero bajo una clave en vez de en un árbol de directorios: Amazon S3 y los muchos servicios que hablan su API. Barato, casi ilimitado y el hogar habitual de eventos en bruto, backups y multimedia: se escriben una vez, se leen poco y se guardan para siempre.","architecture",[2954,2955,2956,2957],"S3","almacenamiento compatible con S3","object storage","blob storage",[2959],{"kind":2960,"url":2961},"wikipedia","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FObject_storage",[2963],"bigquery","\u002Fglossary#object-storage",{"slug":2966,"term":2967,"definition":2968,"category":2969,"aliases":2970,"links":2975,"related":2976,"readMore":-1,"target":2979,"hasArticle":2947},"staff-augmentation","Ampliación de equipo","Un modelo de contratación en el que ingenieros de un socio externo se suman a tu equipo y trabajan bajo tu gestión —en tu repositorio, tus sprints, tu proceso— en vez de entregar un proyecto propio. Compras capacidad; el código y el contexto se quedan contigo.","business",[2971,2972,2973,2974],"staff augmentation","team augmentation","desarrolladores dedicados","outstaffing",[],[2977,2978],"total-cost-of-ownership","time-to-market","\u002Fglossary#staff-augmentation",{"slug":2981,"term":2982,"definition":2983,"category":2937,"aliases":2984,"links":2985,"related":2989,"readMore":-1,"target":2992,"hasArticle":2947},"app-clips","App Clips","Una función de Apple que ejecuta una porción pequeña de una app de iOS —menos de 15 MB— sin instalarla entera. Se invoca desde un código QR, una etiqueta NFC o un enlace, para cuando lo primero que hace un usuario no debería exigir pasar por la tienda.",[],[2986],{"kind":2987,"url":2988},"documentation","https:\u002F\u002Fdeveloper.apple.com\u002Fapp-clips\u002F",[2990,2991],"deep-linking","install-referrer","\u002Fglossary#app-clips",{"slug":2963,"term":2994,"definition":2995,"category":2937,"aliases":2996,"links":2997,"related":3002,"readMore":-1,"target":3004,"hasArticle":2947},"BigQuery","El almacén analítico de Google Cloud. Le lanzas SQL a miles de millones de filas y las recorre en segundos, sobre un almacenamiento separado de las máquinas que consultan, que es lo que mantiene los informes y la exploración fuera de la base de datos que sirve tráfico real.",[],[2998,3000],{"kind":2942,"url":2999},"https:\u002F\u002Fcloud.google.com\u002Fbigquery",{"kind":2960,"url":3001},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FBigQuery",[2949,3003],"elasticsearch","\u002Fglossary#bigquery",{"slug":3006,"term":3007,"definition":3008,"category":3009,"aliases":3010,"links":3014,"related":3017,"readMore":-1,"target":3018,"hasArticle":2947},"ci-cd","CI\u002FCD","Automatización que compila, testea y publica cada cambio sin que nadie ejecute comandos a mano. En móvil es lo que convierte una release en pulsar un botón en lugar de en una tarde esperando a que alguien esté disponible.","practice",[3011,3012,3013],"CI","integración continua","entrega continua",[3015],{"kind":2960,"url":3016},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCI\u002FCD",[],"\u002Fglossary#ci-cd",{"slug":3020,"term":3021,"definition":3022,"category":2952,"aliases":3023,"links":3027,"related":3030,"readMore":-1,"target":3033,"hasArticle":2947},"headless-cms","CMS headless","Un sistema de contenidos con editor y API pero sin frontend propio. Los editores publican en un solo sitio y el sitio web o la app renderizan ese contenido por su cuenta, así que la capa de presentación es tuya y no del proveedor del CMS.",[3024,3025,3026],"headless CMS","gestor de contenidos headless","API de contenidos",[3028],{"kind":2960,"url":3029},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHeadless_content_management_system",[3031,3032],"server-side-rendering","rest","\u002Fglossary#headless-cms",{"slug":3035,"term":3036,"definition":3037,"category":2952,"aliases":3038,"links":3041,"related":3044,"readMore":-1,"target":3046,"hasArticle":2947},"crud","CRUD","Create, read, update, delete: las cuatro operaciones detrás de casi todo formulario y pantalla de administración. Nombre corto para la mitad rutinaria de una app, la que gestiona datos, frente a la que lleva la lógica de dominio.",[3039,3040],"Create, Read, Update, Delete","crear, leer, actualizar, borrar",[3042],{"kind":2960,"url":3043},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCreate,_read,_update_and_delete",[3032,3045],"graphql","\u002Fglossary#crud",{"slug":3048,"term":3049,"definition":3050,"category":2952,"aliases":3051,"links":3055,"related":3058,"readMore":-1,"target":3061,"hasArticle":2947},"end-to-end-encryption","Cifrado de extremo a extremo","Cifrado aplicado en el dispositivo que envía y deshecho solo en el que recibe, de modo que el servicio que transporta el mensaje no puede leerlo: ni por orden judicial ni tras una brecha. Protege el contenido y nunca los metadatos.",[3052,3053,3054],"E2EE","cifrado extremo a extremo","end-to-end encryption",[3056],{"kind":2960,"url":3057},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FEnd-to-end_encryption",[3059,3060],"tls","jwt","\u002Fglossary#end-to-end-encryption",{"slug":3063,"term":3064,"definition":3065,"category":2952,"aliases":3066,"links":3070,"related":3073,"readMore":-1,"target":3074,"hasArticle":2947},"floating-point","Coma flotante","El formato binario IEEE 754 detrás de double y float. No puede representar 0,1 de forma exacta, así que 0,1 + 0,2 da 0,30000000000000004: invisible en gráficos y fatal en dinero, que va en unidades menores enteras o en un tipo decimal.",[3067,3068,3069],"IEEE 754","double","aritmética de coma flotante",[3071],{"kind":2960,"url":3072},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FIEEE_754",[],"\u002Fglossary#floating-point",{"slug":3076,"term":3077,"definition":3078,"category":2969,"aliases":3079,"links":3084,"related":3087,"readMore":-1,"target":3089,"hasArticle":2947},"in-app-purchase","Compra dentro de la app","Vender bienes digitales o una suscripción a través de la facturación de Apple o Google, obligatoria para contenido digital en ambas tiendas y sujeta a comisión. Lo difícil nunca es la compra: es restaurarla en un dispositivo nuevo y mantener honesto el estado de los derechos.",[3080,3081,3082,3083],"IAP","in-app purchase","compras integradas","suscripción in-app",[3085],{"kind":2987,"url":3086},"https:\u002F\u002Fdeveloper.apple.com\u002Fin-app-purchase\u002F",[3088],"product-market-fit","\u002Fglossary#in-app-purchase",{"slug":2977,"term":3091,"definition":3092,"category":2969,"aliases":3093,"links":3097,"related":3100,"readMore":-1,"target":3101,"hasArticle":2947},"Coste total de propiedad","Lo que cuesta un producto a lo largo de toda su vida, no solo construirlo: mantenimiento, actualizaciones, migraciones anuales de SO y de tiendas, y el segundo equipo que contratas para mantener dos bases de código alineadas. Suele superar al presupuesto de desarrollo, y casi nunca va dentro.",[3094,3095,3096],"TCO","total cost of ownership","coste de propiedad",[3098],{"kind":2960,"url":3099},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FTotal_cost_of_ownership",[2978],"\u002Fglossary#total-cost-of-ownership",{"slug":3103,"term":3104,"definition":3105,"category":3106,"aliases":3107,"links":3110,"related":3113,"readMore":-1,"target":3114,"hasArticle":2947},"post-quantum-cryptography","Criptografía poscuántica","Algoritmos de cifrado pensados para resistir a un futuro ordenador cuántico, ya estandarizados por el NIST. La migración urge antes que el hardware: el tráfico capturado hoy puede descifrarse cuando esa máquina exista.","protocol",[3108,3109],"PQC","cifrado poscuántico",[3111],{"kind":2960,"url":3112},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FPost-quantum_cryptography",[3059,3048],"\u002Fglossary#post-quantum-cryptography",{"slug":2990,"term":3116,"definition":3117,"category":2937,"aliases":3118,"links":3123,"related":3126,"readMore":3127,"target":3127,"hasArticle":360},"Deep linking","Un enlace que abre una pantalla concreta dentro de una app instalada en vez de su pantalla de inicio o una página web. El deep linking diferido sobrevive a la instalación, así que un toque que pasa por la tienda acaba igualmente en la pantalla correcta.",[3119,3120,3121,3122],"deep linking diferido","enlaces profundos","universal links","Android App Links",[3124],{"kind":2960,"url":3125},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FMobile_deep_linking",[2981,2991],"\u002Fglossary\u002Fdeep-linking",{"slug":3129,"term":3130,"definition":3131,"category":3009,"aliases":3132,"links":3136,"related":3137,"readMore":-1,"target":3139,"hasArticle":2947},"staged-rollout","Despliegue escalonado","Publicar una build primero a un porcentaje pequeño de usuarios y ampliarlo solo cuando la tasa sin fallos aguanta. Una build mala detectada al diez por ciento es una tarde mala; esa misma build al cien por cien es una semana mala.",[3133,3134,3135],"staged rollout","lanzamiento por fases","release canaria",[],[3138,3006],"feature-flags","\u002Fglossary#staged-rollout",{"slug":3141,"term":3142,"definition":3143,"category":3009,"aliases":3144,"links":3149,"related":3154,"readMore":-1,"target":3156,"hasArticle":2947},"dev-container","Dev Container","Un entorno de desarrollo descrito una sola vez en devcontainer.json —el SO, las herramientas y las versiones de runtime que un proyecto necesita— y abierto de forma idéntica en un contenedor por el editor de cada colaborador, en vez de una guía que cada quien interpreta a su manera.",[3145,3146,3147,3148],"devcontainer.json","Dev Containers","VS Code Dev Containers","contenedor de desarrollo",[3150,3152],{"kind":2942,"url":3151},"https:\u002F\u002Fcontainers.dev\u002F",{"kind":2987,"url":3153},"https:\u002F\u002Fcode.visualstudio.com\u002Fdocs\u002Fdevcontainers\u002Fcontainers",[3155],"container-registry","\u002Fglossary#dev-container",{"slug":3003,"term":3158,"definition":3159,"category":2937,"aliases":3160,"links":3163,"related":3168,"readMore":-1,"target":3169,"hasArticle":2947},"Elasticsearch","Un motor de búsqueda y analítica que indexa registros para poder filtrarlos y buscarlos de forma interactiva en vez de recorrerlos. Lo que usas cuando la pregunta es «enséñame estas sesiones concretas, filtradas de seis maneras» y no «suma esta columna».",[3161,3162],"Elastic","ELK",[3164,3166],{"kind":2942,"url":3165},"https:\u002F\u002Fwww.elastic.co\u002Felasticsearch",{"kind":2960,"url":3167},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FElasticsearch",[2963],"\u002Fglossary#elasticsearch",{"slug":3088,"term":3171,"definition":3172,"category":2969,"aliases":3173,"links":3177,"related":3180,"readMore":-1,"target":3182,"hasArticle":2947},"Encaje producto-mercado","El punto en el que un producto ha encontrado de forma demostrable a gente que lo quiere: lo usa, vuelve y paga. Antes de eso la ingeniería responde a una pregunta; después, responde a la demanda.",[3174,3175,3176],"PMF","product-market fit","product\u002Fmarket fit",[3178],{"kind":2960,"url":3179},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FProduct-market_fit",[3181,2978],"mvp","\u002Fglossary#product-market-fit",{"slug":3184,"term":3185,"definition":3186,"category":2952,"aliases":3187,"links":3190,"related":3191,"readMore":-1,"target":3195,"hasArticle":2947},"fan-out","Fan-out","Leer una fuente una sola vez y entregar cada actualización a todos los clientes suscritos. La versión ingenua escribe a los suscriptores en un bucle y se atasca en cuanto un socket va lento; una real usa un búfer por cliente y descarta a quien no siga el ritmo.",[3188,3189],"fanout","difusión",[],[3192,3193,3194],"websocket","pub-sub","server-sent-events","\u002Fglossary#fan-out",{"slug":3138,"term":3197,"definition":3198,"category":3009,"aliases":3199,"links":3203,"related":3206,"readMore":-1,"target":3209,"hasArticle":2947},"Feature flags","Interruptores que activan o desactivan funcionalidad desde la configuración y no desde una release. Permiten que un mismo binario se comporte distinto por marca, mercado o usuario, y apagar una función arriesgada sin pasar una build nueva por revisión.",[3200,3201,3202],"feature flag","feature toggle","banderas de funcionalidad",[3204],{"kind":2960,"url":3205},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FFeature_toggle",[3129,3207,3208],"white-label","multi-tenancy","\u002Fglossary#feature-flags",{"slug":3211,"term":2519,"definition":3212,"category":2952,"aliases":3213,"links":3217,"related":3220,"readMore":-1,"target":3221,"hasArticle":2947},"state-management","Cómo decide una app dónde vive un valor, quién puede cambiarlo y qué partes de la pantalla se redibujan cuando lo hace. En Flutter, elegir entre Riverpod, BLoC y Provider es de las primeras decisiones arquitectónicas y de las más difíciles de revisar.",[3214,3215,3216],"state management","BLoC","Riverpod",[3218],{"kind":2987,"url":3219},"https:\u002F\u002Fdocs.flutter.dev\u002Fdata-and-backend\u002Fstate-mgmt\u002Foptions",[195],"\u002Fglossary#state-management",{"slug":195,"term":3223,"definition":3224,"category":3009,"aliases":3225,"links":3228,"related":3230,"readMore":-1,"target":3231,"hasArticle":2947},"Golden test","Un test que renderiza un widget y compara el resultado píxel a píxel con una imagen de referencia guardada. En Flutter es la forma más barata de saber si un rediseño rompió el estado vacío a 320 pt, en modo oscuro y con el texto al 200 %.",[196,3226,3227],"test de captura","snapshot test",[3229],{"kind":2987,"url":2284},[3006],"\u002Fglossary#golden-test",{"slug":3045,"term":3233,"definition":3234,"category":2952,"aliases":3235,"links":3236,"related":3241,"readMore":-1,"target":3242,"hasArticle":2947},"GraphQL","Un lenguaje de consulta para APIs donde el cliente nombra exactamente los campos que quiere y recibe una única respuesta con esa forma. Elimina el exceso de datos al que derivan los endpoints REST y añade un fallo propio: una consulta sin límites que recorre todo el modelo.",[],[3237,3239],{"kind":2942,"url":3238},"https:\u002F\u002Fgraphql.org\u002F",{"kind":2960,"url":3240},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FGraphQL",[3032],"\u002Fglossary#graphql",{"slug":3244,"term":3245,"definition":3246,"category":3009,"aliases":3247,"links":3249,"related":3252,"readMore":-1,"target":3256,"hasArticle":2947},"hipaa","HIPAA","La ley estadounidense que regula la información sanitaria protegida: cómo puede almacenarse, transmitirse, registrarse y divulgarse. Como PCI-DSS, es una restricción arquitectónica que se elige al principio, no un documento de políticas que se añade antes del lanzamiento.",[3248],"Health Insurance Portability and Accountability Act",[3250],{"kind":2960,"url":3251},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHealth_Insurance_Portability_and_Accountability_Act",[3253,3254,3255],"pci-dss","gdpr","soc-2","\u002Fglossary#hipaa",{"slug":2945,"term":2705,"definition":3258,"category":2937,"aliases":3259,"links":3262,"related":3265,"readMore":-1,"target":3266,"hasArticle":2947},"Permitir que un móvil Android actúe por software como una tarjeta contactless sobre NFC, sin elemento seguro por hardware. Es así como paga una app de wallet en un terminal: el teléfono habla el mismo protocolo EMV contactless que habría hablado la tarjeta de plástico.",[3260,2707,3261],"HCE","pagos contactless",[3263],{"kind":2987,"url":3264},"https:\u002F\u002Fdeveloper.android.com\u002Fdevelop\u002Fconnectivity\u002Fnfc\u002Fhce",[3253,2935],"\u002Fglossary#host-card-emulation",{"slug":3268,"term":3269,"definition":3270,"category":2952,"aliases":3271,"links":3275,"related":3278,"readMore":-1,"target":3279,"hasArticle":2947},"multi-arch-image","Imagen multiarquitectura","Una única etiqueta de imagen que resuelve a binarios distintos según la arquitectura de CPU —linux\u002Famd64 y linux\u002Farm64 son el par habitual—, de modo que el mismo docker pull funciona sin cambios tanto en servidores Intel\u002FAMD como en portátiles con Apple Silicon.",[3272,3273,3274],"build multiarquitectura","multi-arch build","linux\u002Famd64 + linux\u002Farm64",[3276],{"kind":2987,"url":3277},"https:\u002F\u002Fdocs.docker.com\u002Fbuild\u002Fbuilding\u002Fmulti-platform\u002F",[3155],"\u002Fglossary#multi-arch-image",{"slug":2164,"term":2165,"definition":3281,"category":2937,"aliases":3282,"links":3283,"related":3286,"readMore":-1,"target":3288,"hasArticle":2947},"El motor de renderizado que usa Flutter hoy, por defecto en iOS desde 2023 y en Android desde 2024. Compila sus shaders por adelantado en lugar de durante la primera animación, lo que eliminó el jank de shaders, el problema más visible de Flutter en producción.",[],[3284],{"kind":2987,"url":3285},"https:\u002F\u002Fdocs.flutter.dev\u002Fperf\u002Fimpeller",[3287],"skia","\u002Fglossary#impeller",{"slug":2991,"term":3290,"definition":3291,"category":2937,"aliases":3292,"links":3293,"related":3296,"readMore":-1,"target":3297,"hasArticle":2947},"Install Referrer","Una API de Google Play que entrega a una app Android recién instalada los parámetros de campaña del enlace que llevó a la instalación. La mitad Android del deep linking diferido y la forma fiable de atribuir de dónde viene un usuario.",[],[3294],{"kind":2987,"url":3295},"https:\u002F\u002Fdeveloper.android.com\u002Fgoogle\u002Fplay\u002Finstallreferrer",[2990,2981],"\u002Fglossary#install-referrer",{"slug":3299,"term":3300,"definition":3301,"category":3009,"aliases":3302,"links":3305,"related":3308,"readMore":-1,"target":3310,"hasArticle":2947},"prompt-injection","Inyección de prompts","Un ataque en el que el texto que aporta un usuario lo lee un modelo de lenguaje como instrucciones y no como datos, desviándolo de sus propias reglas. El hermano de la inyección SQL en la era de los LLM, y aparece donde se concatena entrada de usuario en un prompt.",[3303,3304],"prompt injection","ataque de inyección",[3306],{"kind":2960,"url":3307},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FPrompt_injection",[3309],"rate-limiting","\u002Fglossary#prompt-injection",{"slug":3060,"term":3312,"definition":3313,"category":3106,"aliases":3314,"links":3316,"related":3321,"readMore":-1,"target":3322,"hasArticle":2947},"JWT","Un token firmado que lleva sus propios claims, de modo que un servidor puede saber a quién pertenece una petición sin consultar una sesión. Estándar para la autenticación móvil. La firma prueba que no se alteró; no oculta lo que hay dentro.",[3315],"JSON Web Token",[3317,3319],{"kind":2942,"url":3318},"https:\u002F\u002Fjwt.io\u002F",{"kind":2960,"url":3320},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FJSON_Web_Token",[3253],"\u002Fglossary#jwt",{"slug":3324,"term":3325,"definition":3326,"category":3009,"aliases":3327,"links":3332,"related":3335,"readMore":-1,"target":3336,"hasArticle":2947},"kyc","KYC","Know Your Customer: las verificaciones de identidad que hace un producto financiero regulado antes de dejar mover dinero —captura de documentos, prueba de vida, listas de sanciones y control antiblanqueo—. Condiciona el onboarding más que cualquier decisión de diseño.",[3328,3329,3330,3331],"Know Your Customer","AML","KYC\u002FAML","conocimiento del cliente",[3333],{"kind":2960,"url":3334},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FKnow_your_customer",[3253,3254],"\u002Fglossary#kyc",{"slug":76,"term":74,"definition":3338,"category":2937,"aliases":3339,"links":3341,"related":3342,"readMore":3343,"target":3343,"hasArticle":2947},"Compartir lógica de negocio escrita en Kotlin entre Android, iOS y el servidor mientras cada plataforma conserva su UI nativa. La alternativa a Flutter cuando la interfaz tiene que ser nativa pero las reglas que hay detrás no.",[3340],"KMP",[],[],"\u002Ftechnologies\u002Fkmp",{"slug":3309,"term":3345,"definition":3346,"category":3009,"aliases":3347,"links":3351,"related":3354,"readMore":-1,"target":3355,"hasArticle":2947},"Limitación de tasa","Un tope de cuántas peticiones puede hacer un mismo cliente en una ventana de tiempo. Es lo que impide que un usuario entusiasta, un scraper o un bot se gaste en una tarde un mes de presupuesto de API, y tiene que vivir en tu lado de la integración.",[3348,3349,3350],"rate limiting","rate limit","throttling",[3352],{"kind":2960,"url":3353},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FRate_limiting",[3299],"\u002Fglossary#rate-limiting",{"slug":3181,"term":3357,"definition":3358,"category":2969,"aliases":3359,"links":3362,"related":3365,"readMore":-1,"target":3366,"hasArticle":2947},"MVP","La versión más pequeña de un producto que puede ponerse delante de usuarios reales y aun así responder la pregunta por la que lo construiste. Una decisión sobre alcance, no sobre calidad: un MVP también tiene que funcionar.",[3360,3361],"producto mínimo viable","minimum viable product",[3363],{"kind":2960,"url":3364},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FMinimum_viable_product",[3207],"\u002Fglossary#mvp",{"slug":3207,"term":3368,"definition":3369,"category":2969,"aliases":3370,"links":3373,"related":3376,"readMore":3377,"target":3377,"hasArticle":360},"Marca blanca","Un mismo producto publicado bajo muchas marcas. Una plataforma móvil de marca blanca construye para cada cliente una app lista para la tienda con su nombre, su diseño y su contenido desde una única base de código compartida, en vez de forkear el proyecto por cliente.",[3207,3371,3372],"white label","app multiinquilino",[3374],{"kind":2960,"url":3375},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWhite-label_product",[3181],"\u002Fglossary\u002Fwhite-label",{"slug":3208,"term":3379,"definition":3380,"category":2952,"aliases":3381,"links":3385,"related":3388,"readMore":-1,"target":3389,"hasArticle":2947},"Multi-tenancy","Un solo despliegue que sirve a muchos clientes, cada uno viendo solo sus datos, su configuración y sus funciones activas porque el contexto de tenant se resuelve en cada petición. Es lo que hace que una flota de apps de marca sea un producto y no muchos forks.",[3382,3383,3384],"multi-tenant","multiinquilino","tenant",[3386],{"kind":2960,"url":3387},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FMultitenancy",[3207,3138],"\u002Fglossary#multi-tenancy",{"slug":3391,"term":3392,"definition":3393,"category":3106,"aliases":3394,"links":3398,"related":3403,"readMore":-1,"target":3404,"hasArticle":2947},"oauth","OAuth","El estándar detrás de Iniciar sesión con Apple, con Google y demás: el usuario autoriza tu app en un proveedor en el que ya confía y tu app recibe un token en lugar de su contraseña. Nadie inventa una credencial nueva y tú nunca almacenas ninguna.",[3395,3396,3397],"OAuth 2.0","login social","Iniciar sesión con Apple",[3399,3401],{"kind":2942,"url":3400},"https:\u002F\u002Foauth.net\u002F2\u002F",{"kind":2960,"url":3402},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FOAuth",[3060],"\u002Fglossary#oauth",{"slug":3253,"term":3406,"definition":3407,"category":3009,"aliases":3408,"links":3411,"related":3416,"readMore":-1,"target":3417,"hasArticle":2947},"PCI-DSS","El estándar de seguridad del sector de tarjetas que obliga a quien almacena, procesa o transmite datos de tarjeta. La mayoría de las apps se mantienen fuera de su alcance a propósito, delegando la introducción de la tarjeta en un proveedor de pagos certificado.",[3409,3410],"PCI DSS","Payment Card Industry Data Security Standard",[3412,3414],{"kind":2942,"url":3413},"https:\u002F\u002Fwww.pcisecuritystandards.org\u002F",{"kind":2960,"url":3415},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FPayment_Card_Industry_Data_Security_Standard",[3060],"\u002Fglossary#pci-dss",{"slug":3419,"term":3420,"definition":3421,"category":2937,"aliases":3422,"links":3426,"related":3429,"readMore":-1,"target":3430,"hasArticle":2947},"platform-channels","Platform channels","El puente que usa una app Flutter para llamar a código nativo de iOS y Android: Keychain y Keystore, biometría, hojas de pago, cualquier SDK sin paquete de Dart. Trabajo rutinario pero real, y lo primero donde encalla quien nunca salió de Dart.",[3423,3424,3425],"platform channel","method channel","canales de plataforma",[3427],{"kind":2987,"url":3428},"https:\u002F\u002Fdocs.flutter.dev\u002Fplatform-integration\u002Fplatform-channels",[76],"\u002Fglossary#platform-channels",{"slug":3193,"term":3432,"definition":3433,"category":2952,"aliases":3434,"links":3438,"related":3441,"readMore":-1,"target":3442,"hasArticle":2947},"Pub\u002FSub","Un patrón de mensajería donde un productor publica un evento y cualquier número de consumidores lo lee de forma independiente, con un broker en medio. El productor nunca los espera, y así la ruta de la petición sigue siendo rápida mientras el trabajo lento ocurre detrás.",[3435,3436,3437],"publicación\u002Fsuscripción","publish\u002Fsubscribe","Google Cloud Pub\u002FSub",[3439],{"kind":2960,"url":3440},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FPublish%E2%80%93subscribe_pattern",[],"\u002Fglossary#pub-sub",{"slug":3032,"term":3444,"definition":3445,"category":2952,"aliases":3446,"links":3449,"related":3452,"readMore":-1,"target":3453,"hasArticle":2947},"REST","El estilo convencional de las APIs HTTP: una URL nombra un recurso y el verbo HTTP dice qué hacer con él. La forma por defecto en que una app habla con un backend, y lo que esperan encontrar la mayoría de integraciones de terceros.",[3447,3448],"API REST","Representational State Transfer",[3450],{"kind":2960,"url":3451},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FREST",[3192,3194],"\u002Fglossary#rest",{"slug":3254,"term":3455,"definition":3456,"category":3009,"aliases":3457,"links":3461,"related":3466,"readMore":-1,"target":3467,"hasArticle":2947},"RGPD","El reglamento de la UE sobre los datos personales de las personas en la UE: una base legal para recogerlos, consentimiento real para el rastreo y derechos de acceso y supresión. Sigue a tus usuarios, no a tus servidores, así que se aplica esté donde esté la empresa.",[3458,3459,3460],"GDPR","Reglamento General de Protección de Datos","protección de datos",[3462,3464],{"kind":2942,"url":3463},"https:\u002F\u002Fgdpr.eu\u002F",{"kind":2960,"url":3465},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FGeneral_Data_Protection_Regulation",[3253,3255,3244],"\u002Fglossary#gdpr",{"slug":3155,"term":3469,"definition":3470,"category":2952,"aliases":3471,"links":3477,"related":3480,"readMore":-1,"target":3481,"hasArticle":2947},"Registro de contenedores","Almacenamiento alojado para imágenes de contenedores, direccionadas por nombre y etiqueta: Docker Hub, GitHub Container Registry o el propio de un proveedor cloud. Publicar una build ahí convierte 'funciona en mi máquina' en una imagen que cualquiera puede descargar y ejecutar sin cambios.",[3472,3473,3474,3475,3476],"registro de imágenes","Docker registry","GHCR","ghcr.io","container registry",[3478],{"kind":2987,"url":3479},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fpackages\u002Fworking-with-a-github-packages-registry\u002Fworking-with-the-container-registry",[3141,3268],"\u002Fglossary#container-registry",{"slug":3031,"term":3483,"definition":3484,"category":2952,"aliases":3485,"links":3489,"related":3492,"readMore":-1,"target":3493,"hasArticle":2947},"Renderizado en servidor","Construir una página como HTML terminado en el servidor, de modo que la primera respuesta ya lleva el contenido, los encabezados, las metaetiquetas y los datos estructurados. Los rastreadores, las vistas previas de enlaces y los dispositivos lentos lo leen sin ejecutar JavaScript.",[3486,3487,3488],"SSR","server-side rendering","renderizado del lado del servidor",[3490],{"kind":2960,"url":3491},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FServer-side_scripting",[3020],"\u002Fglossary#server-side-rendering",{"slug":3255,"term":3495,"definition":3496,"category":3009,"aliases":3497,"links":3500,"related":3503,"readMore":-1,"target":3504,"hasArticle":2947},"SOC 2","Un informe de auditoría externa sobre cómo trata una organización los datos de clientes —seguridad, disponibilidad, confidencialidad— y no un certificado que se compra. Los clientes empresariales lo piden, y condiciona la arquitectura mucho antes que la propia auditoría.",[3498,3499],"SOC2","System and Organization Controls",[3501],{"kind":2960,"url":3502},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSystem_and_Organization_Controls",[3253,3254,3244],"\u002Fglossary#soc-2",{"slug":3506,"term":3507,"definition":3508,"category":2969,"aliases":3509,"links":3512,"related":3515,"readMore":-1,"target":3516,"hasArticle":2947},"saas","SaaS","Software vendido como suscripción continua a un producto alojado, en lugar de como una licencia única que el cliente instala y opera. El proveedor mantiene los servidores, publica actualizaciones sin parar y cobra por usuario o por uso.",[3510,3511],"Software as a Service","software como servicio",[3513],{"kind":2960,"url":3514},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSoftware_as_a_service",[3208,3207],"\u002Fglossary#saas",{"slug":3194,"term":3518,"definition":3519,"category":3106,"aliases":3520,"links":3523,"related":3526,"readMore":-1,"target":3527,"hasArticle":2947},"Server-Sent Events","Un flujo unidireccional del servidor al cliente sobre una conexión HTTP corriente. Más simple que un WebSocket y suficiente allí donde solo el servidor tiene algo que decir: un feed de progreso, o una respuesta de IA que llega token a token.",[3521,3522],"SSE","eventos enviados por el servidor",[3524],{"kind":2960,"url":3525},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FServer-sent_events",[3192,3032],"\u002Fglossary#server-sent-events",{"slug":3287,"term":3529,"definition":3530,"category":2937,"aliases":3531,"links":3534,"related":3539,"readMore":-1,"target":3540,"hasArticle":2947},"Skia","La librería gráfica 2D de código abierto de Google que dibuja Chrome, Android y —hasta Impeller— cada fotograma de Flutter. Renderiza tanto a PDF como a pantalla, que es lo que hace la función de imprimir a PDF de Chrome.",[3532,3533],"Skia Graphics Engine","skia-safe",[3535,3537],{"kind":2942,"url":3536},"https:\u002F\u002Fskia.org\u002F",{"kind":2960,"url":3538},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSkia_Graphics_Engine",[2164],"\u002Fglossary#skia",{"slug":3059,"term":3542,"definition":3543,"category":3106,"aliases":3544,"links":3548,"related":3551,"readMore":-1,"target":3552,"hasArticle":2947},"TLS","La capa de cifrado bajo HTTPS. Demuestra que el servidor es quien dice su certificado, acuerda una clave nueva para la sesión y cifra todo lo demás, de modo que la red intermedia solo ve texto cifrado que no puede alterar sin que se note.",[3545,3546,3547],"SSL","HTTPS","Transport Layer Security",[3549],{"kind":2960,"url":3550},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FTransport_Layer_Security",[3048,3060],"\u002Fglossary#tls",{"slug":2978,"term":3554,"definition":3555,"category":2969,"aliases":3556,"links":3559,"related":3562,"readMore":-1,"target":3563,"hasArticle":2947},"Time to market","Cuánto se tarda en llevar un producto desde la decisión hasta usuarios reales. Casi todas las discusiones sobre stack y alcance son en realidad discusiones sobre este número, porque cada semana ahorrada es una semana de ingresos, feedback y posición competitiva.",[3557,3558],"TTM","tiempo de salida al mercado",[3560],{"kind":2960,"url":3561},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FTime_to_market",[3181,2977],"\u002Fglossary#time-to-market",{"slug":3565,"term":2526,"definition":3566,"category":3106,"aliases":3567,"links":3569,"related":3574,"readMore":-1,"target":3576,"hasArticle":2947},"webrtc","El estándar de navegador y móvil para enviar audio, vídeo y datos directamente entre dos dispositivos, con los servidores implicados solo en presentarlos entre sí. Es sobre lo que se construye una videollamada dentro de una app cuando no es un SDK alquilado.",[3568],"Web Real-Time Communication",[3570,3572],{"kind":2942,"url":3571},"https:\u002F\u002Fwebrtc.org\u002F",{"kind":2960,"url":3573},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWebRTC",[3575,3192],"xmpp","\u002Fglossary#webrtc",{"slug":3192,"term":3578,"definition":3579,"category":3106,"aliases":3580,"links":3581,"related":3584,"readMore":-1,"target":3585,"hasArticle":2947},"WebSocket","Un protocolo que mantiene una conexión abierta entre cliente y servidor para que cualquiera de los dos pueda enviar en cualquier momento, en vez de que el cliente pregunte una y otra vez. Sobre esto funcionan los precios en vivo, el chat y los indicadores de presencia.",[],[3582],{"kind":2960,"url":3583},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWebSocket",[3194,3032],"\u002Fglossary#websocket",{"slug":3575,"term":3587,"definition":3588,"category":3106,"aliases":3589,"links":3592,"related":3597,"readMore":-1,"target":3598,"hasArticle":2947},"XMPP","Un protocolo de mensajería abierto y federado, y la alternativa de siempre a escribir un backend de chat o alquilarlo. Se extiende a presencia, indicadores de escritura y transferencia de archivos, y es lo bastante antiguo como para que cada plataforma tenga una librería madura.",[3590,3591],"Jabber","Extensible Messaging and Presence Protocol",[3593,3595],{"kind":2942,"url":3594},"https:\u002F\u002Fxmpp.org\u002F",{"kind":2960,"url":3596},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FXMPP",[3565,3192],"\u002Fglossary#xmpp"]