Files
ImBenjiandClaude Opus 5.5 b269201919 The Garage SDKs, in the open
garage_auth, garage_entitlements, garage_iap and garage_ui, moved out of
Garage-Services and Metro-Map-Maker into one public repo. MIT, one readme,
docs under docs/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
2026-09-23 18:49:21 +01:00

164 lines
5.2 KiB
Dart

import "package:flutter/services.dart";
import "package:flutter_test/flutter_test.dart";
import "package:garage_ui/garage_ui.dart";
/// Type [text] one character at a time through [f], the way a keyboard would.
///
/// Feeding the whole string in one go would only ever test paste - and a mask
/// that handles paste can still put the caret in the wrong place on every
/// keystroke, which is the bug people actually hit.
TextEditingValue _type(MaskedTextInputFormatter f, String text) {
var v = TextEditingValue.empty;
for (final ch in text.split("")) {
final at = v.selection.baseOffset < 0
? v.text.length
: v.selection.baseOffset;
final next = TextEditingValue(
text: v.text.substring(0, at) + ch + v.text.substring(at),
selection: TextSelection.collapsed(offset: at + 1),
);
v = f.formatEditUpdate(v, next);
}
return v;
}
/// One backspace at the caret.
TextEditingValue _backspace(MaskedTextInputFormatter f, TextEditingValue v) {
final at = v.selection.baseOffset;
if (at <= 0) return v;
return f.formatEditUpdate(
v,
TextEditingValue(
text: v.text.substring(0, at - 1) + v.text.substring(at),
selection: TextSelection.collapsed(offset: at - 1),
),
);
}
TextEditingValue _paste(MaskedTextInputFormatter f, String text) =>
f.formatEditUpdate(
TextEditingValue.empty,
TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
),
);
void main() {
final phone = MaskedTextInputFormatter("### ###-####");
group("typing", () {
test("literals arrive on their own", () {
expect(_type(phone, "5").text, "5");
expect(_type(phone, "555").text, "555");
// the space appears with the 4th digit, not before it - a trailing
// literal you cant delete reads as the field being stuck
expect(_type(phone, "5551").text, "555 1");
expect(_type(phone, "555123").text, "555 123");
expect(_type(phone, "5551234").text, "555 123-4");
expect(_type(phone, "5551234567").text, "555 123-4567");
});
test("the caret stays after what you typed", () {
final v = _type(phone, "5551");
expect(v.text, "555 1");
expect(v.selection.baseOffset, 5);
});
test("it stops when the mask is full", () {
expect(_type(phone, "55512345678901").text, "555 123-4567");
});
test("characters the slot wont take are dropped, not the whole edit", () {
expect(_type(phone, "5a5b5c").text, "555");
});
});
group("deleting", () {
test("backspace over a literal still removes a character", () {
// the case a naive mask gets stuck on: the caret is just past the space,
// backspace kills the space, the digits are unchanged, and re-applying
// the mask puts the space straight back.
var v = _type(phone, "5551");
expect(v.text, "555 1");
v = _backspace(phone, v);
expect(v.text, "555");
v = _backspace(phone, v);
expect(v.text, "55");
});
test("backspacing the lot empties the field", () {
var v = _type(phone, "5551234567");
for (var i = 0; i < 20; i++) {
v = _backspace(phone, v);
}
expect(v.text, "");
});
test("backspace at the start is a no-op, not a crash", () {
final v = phone.formatEditUpdate(
TextEditingValue.empty,
TextEditingValue.empty,
);
expect(v.text, "");
expect(_backspace(phone, v).text, "");
});
});
group("pasting", () {
test("an already formatted number survives", () {
expect(_paste(phone, "555 123-4567").text, "555 123-4567");
});
test("someone elses formatting is reduced and re-laid", () {
expect(_paste(phone, "(555) 123-4567").text, "555 123-4567");
expect(_paste(phone, "555.123.4567").text, "555 123-4567");
expect(_paste(phone, "5551234567").text, "555 123-4567");
});
test("overlong paste is cut to the mask", () {
expect(_paste(phone, "555123456789").text, "555 123-4567");
});
});
group("slot kinds", () {
test("A takes letters, # takes digits", () {
final post = MaskedTextInputFormatter("AA# #AA");
expect(_type(post, "SW1A1AA").text, "SW1 1AA");
// a digit cant fill a letter slot
expect(_type(post, "12").text, "");
});
test("* takes either", () {
final any = MaskedTextInputFormatter("**-**");
expect(_type(any, "a1b2").text, "a1-b2");
});
});
group("unmask", () {
test("gives back only what was typed", () {
expect(phone.unmask("555 123-4567"), "5551234567");
expect(phone.unmask("555 1"), "5551");
expect(phone.unmask(""), "");
});
test("a literal that looks like a slot character isnt returned", () {
// the leading 1 is part of the mask, not something anybody typed
final trunk = MaskedTextInputFormatter("1-###-####");
final v = _type(trunk, "5551234");
expect(v.text, "1-555-1234");
expect(trunk.unmask(v.text), "5551234");
});
test("round trips through apply", () {
const raw = "5551234567";
expect(phone.unmask(phone.apply(raw)), raw);
});
});
test("slotCount counts slots, not mask length", () {
expect(phone.slotCount, 10);
expect(phone.mask.length, 12);
});
}