Full time-domain audit (three shipped date bugs prompted it) found one
root cause and two latent travel defects, all now fixed:
Day-stepping used add(Duration(days: 1)), which shifts the absolute
instant by exactly 86400s — documented Dart behavior (sdk#47666), so
crossing a DST transition drifted the wall hour by +-1h permanently
for the rest of the candidate scan (verified: 2026-03-28 07:30
Europe/Madrid + "1 day" = 08:30). The native Calendar engine preserves
wall time, and the single-authority fix made the drifted Dart verdict
win. Candidates now advance by calendar reconstruction (_siguienteDia:
DateTime(y, m, d+1, hora, minuto)), the same wall-clock-preserving
semantics as Calendar.add(DAY_OF_YEAR, 1) plus AOSP DeskClock's
defensive hour/minute re-assertion, keeping both engines in agreement
through any transition.
Instant-valued fields (snoozeHasta/snoozeOrigen/proximaEjecucion/
ultimaEjecucionGestionada/creadaEn/actualizadaEn) serialized as
offset-less local ISO, so re-parsing after a device timezone change
reinterpreted the same wall fields as a different instant. They now
serialize as UTC ("Z"); reads normalize to local, and legacy
offset-less payloads parse identically — no migration. fechaUnica
stays local on purpose: it is a wall-clock date.
One-shot alarms sent fechaUnica's midnight epoch to the native side,
whose boot/travel re-arm derives the calendar day back from it in the
CURRENT zone — a westward shift rolled the date to the previous day.
The channel now anchors the date at local noon, keeping it stable
across real-world zone shifts.
Property tests lock the no-drift guarantee (400 daily / 200 weekday
iterations must all land exactly at hora:minuto — on DST-observing
dev machines this crosses real transitions), plus UTC round-trip,
legacy-payload compatibility, and wall-date preservation tests.
71 lines
2.4 KiB
Dart
71 lines
2.4 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pluriwave/modelos/alarma_musical.dart';
|
|
|
|
void main() {
|
|
AlarmaMusical base({DateTime? snoozeHasta, DateTime? fechaUnica}) =>
|
|
AlarmaMusical(
|
|
id: 'f1',
|
|
nombre: 'Fechas',
|
|
hora: 7,
|
|
minuto: 30,
|
|
tipoProgramacion:
|
|
fechaUnica != null
|
|
? TipoProgramacionAlarma.unica
|
|
: TipoProgramacionAlarma.diaria,
|
|
diasSemana: const [],
|
|
fechaUnica: fechaUnica,
|
|
snoozeHasta: snoozeHasta,
|
|
snoozeOrigen: snoozeHasta?.subtract(const Duration(minutes: 5)),
|
|
ultimaEjecucionGestionada: snoozeHasta?.subtract(
|
|
const Duration(minutes: 5),
|
|
),
|
|
);
|
|
|
|
test('los instantes se serializan como UTC con offset explicito (Z)', () {
|
|
// A local toIso8601String() carries no offset, so re-parsing it after a
|
|
// device timezone change reinterprets the same wall fields as a
|
|
// DIFFERENT instant (a snooze set in Madrid would shift hours after
|
|
// landing in New York). Instants must round-trip offset-carrying.
|
|
final snooze = DateTime(2026, 7, 13, 7, 10);
|
|
final json = base(snoozeHasta: snooze).toJson();
|
|
|
|
expect((json['snoozeHasta'] as String).endsWith('Z'), isTrue);
|
|
expect((json['snoozeOrigen'] as String).endsWith('Z'), isTrue);
|
|
expect(
|
|
(json['ultimaEjecucionGestionada'] as String).endsWith('Z'),
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
test('el round-trip de instantes preserva el instante exacto', () {
|
|
final snooze = DateTime(2026, 7, 13, 7, 10);
|
|
final recuperada = AlarmaMusical.fromJson(base(snoozeHasta: snooze).toJson());
|
|
|
|
expect(recuperada.snoozeHasta, snooze);
|
|
expect(
|
|
recuperada.snoozeOrigen,
|
|
snooze.subtract(const Duration(minutes: 5)),
|
|
);
|
|
});
|
|
|
|
test('los payloads legados sin offset siguen leyendose como hora local '
|
|
'(sin migracion)', () {
|
|
final legado = base().toJson()
|
|
..['snoozeHasta'] = '2026-07-13T07:10:00.000';
|
|
final recuperada = AlarmaMusical.fromJson(legado);
|
|
|
|
expect(recuperada.snoozeHasta, DateTime(2026, 7, 13, 7, 10));
|
|
});
|
|
|
|
test('fechaUnica sigue siendo fecha de pared: conserva y/m/d en local', () {
|
|
final unica = DateTime(2026, 7, 13);
|
|
final recuperada = AlarmaMusical.fromJson(
|
|
base(fechaUnica: unica).toJson(),
|
|
);
|
|
|
|
expect(recuperada.fechaUnica!.year, 2026);
|
|
expect(recuperada.fechaUnica!.month, 7);
|
|
expect(recuperada.fechaUnica!.day, 13);
|
|
});
|
|
}
|