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); }); }