fix(assets): declare the content subdirectories so onboarding ships

Audit of the same failure family as the shrunk drawables: references by
NAME that nothing validates at compile time.

The whole onboarding and release-notes feature had never shipped. Reading
the installed APK: ZERO entries under assets/content/, while
assets/icons/alarmas/* was present. pubspec declared `assets/content/`, and
Flutter does not recurse -- naming a directory includes the files sitting
directly in it, never its subdirectories. Every content file lives in one
(onboarding/, updates/<locale>/), so none of them were packaged.

On the device that surfaced on every single launch:

  Unable to load asset: "assets/content/onboarding/en.md"

with the file plainly present on disk. That is why it never looked like a
packaging problem. The tell was already in the pubspec: assets/icons/alarmas/
is listed explicitly, so the rule was known once and not applied here.

All 14 content directories are now declared: onboarding/ plus updates/ for
each of the 13 locales.

The guard is a test that loads every file under assets/content/ through
rootBundle, because that is the only thing that proves an asset is declared
and will ship. A test asserting File.existsSync would have stayed green
through all of this -- the files were never missing, only unpackaged. Run
against the unfixed pubspec it fails 26 of 27; with the fix it passes.

Tests: 1165 -> 1192.
This commit is contained in:
2026-08-07 13:05:59 +02:00
parent 28f47d6340
commit 0ef6ce35b4
2 changed files with 77 additions and 0 deletions
+22
View File
@@ -75,4 +75,26 @@ flutter:
- assets/audio/
- assets/mockups/
- assets/generated/
# Flutter NO recurse: declarar 'assets/content/' incluye solo los
# ficheros sueltos de esa carpeta, nunca los de sus subcarpetas. Todo
# el contenido vive en subcarpetas, asi que NADA de esto viajaba en el
# APK -- verificado abriendo el binario instalado: cero entradas de
# assets/content. El onboarding reventaba en cada arranque con
# 'Unable to load asset: assets/content/onboarding/en.md' aunque el
# fichero existe en disco. Mismo fallo de familia que los drawables
# resueltos por nombre: referencia sin validacion en compilacion.
- assets/content/
- assets/content/onboarding/
- assets/content/updates/ar/
- assets/content/updates/bn/
- assets/content/updates/de/
- assets/content/updates/en/
- assets/content/updates/es/
- assets/content/updates/fr/
- assets/content/updates/hi/
- assets/content/updates/id/
- assets/content/updates/it/
- assets/content/updates/ja/
- assets/content/updates/pt/
- assets/content/updates/ru/
- assets/content/updates/zh/
@@ -0,0 +1,55 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
/// Every file under `assets/content/` must be loadable through `rootBundle`,
/// which is the only thing that proves it is DECLARED in pubspec.yaml and
/// therefore actually ships.
///
/// Found by reading the installed APK: it contained ZERO entries under
/// `assets/content/`, while `assets/icons/alarmas/*` was present. pubspec
/// declared `assets/content/` — and Flutter does NOT recurse: naming a
/// directory includes the files sitting directly in it, never its
/// subdirectories. All of this content lives in subdirectories
/// (`onboarding/`, `updates/<locale>/`), so the entire onboarding and
/// release-notes feature had never shipped in any build. On the device it
/// surfaced on every launch as:
///
/// Unable to load asset: "assets/content/onboarding/en.md"
///
/// with the file plainly present on disk.
///
/// Same family as the drawables the resource shrinker deleted: a reference by
/// NAME that nothing validates at compile time, so it fails only on a device.
/// A test that merely checked `File(...).existsSync()` would have stayed green
/// throughout — the files were never missing. Loading through `rootBundle` is
/// what makes it a real guard, because that is the path the app itself takes.
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final directorio = Directory('assets/content');
final ficheros =
directorio
.listSync(recursive: true)
.whereType<File>()
.map((f) => f.path.replaceAll(r'\', '/'))
.toList()
..sort();
test('hay contenido que comprobar (si no, este test sería vacuo)', () {
expect(ficheros, isNotEmpty);
});
for (final ruta in ficheros) {
test('$ruta está declarado y se puede cargar', () async {
await expectLater(
rootBundle.loadString(ruta),
completes,
reason:
'existe en disco pero rootBundle no lo encuentra: falta declarar '
'su directorio en pubspec.yaml, y no viajará en el APK',
);
});
}
}