import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:sm_mobile/services/editor/languages.dart'; import 'package:sm_mobile/services/editor/text_codec.dart'; import 'package:sm_mobile/services/editor/validation.dart'; Uint8List b(List v) => Uint8List.fromList(v); /// Decodes, then encodes the same text with the detected settings: the /// bytes must come back unchanged. void roundTrip(Uint8List bytes, TextEncoding expected, {LineBreak? lineBreak}) { final d = decodeDocument(bytes); expect(d.encoding, expected); expect(d.exact, true); if (lineBreak != null) expect(d.lineBreaks.dominant, lineBreak); final again = encodeText( withLineBreaks(d.text, d.lineBreaks.dominant), d.encoding, ); expect(again, bytes); } void main() { group('encodings are preserved byte for byte', () { const text = 'Příliš žluťoučký kůň úpěl ďábelské ódy – €\n'; test('UTF-8 with and without BOM', () { roundTrip(b(utf8.encode(text)), TextEncoding.utf8); roundTrip( b([0xef, 0xbb, 0xbf, ...utf8.encode(text)]), TextEncoding.utf8Bom, ); }); test('UTF-16 LE/BE with BOM and LE without BOM', () { for (final e in [TextEncoding.utf16le, TextEncoding.utf16be]) { roundTrip(encodeText('A😀$text', e), e); } final noBom = encodeText( 'plain ascii text\n', TextEncoding.utf16le, ).sublist(2); expect(detectEncoding(noBom), TextEncoding.utf16le); }); test('legacy 8-bit text is never guessed silently', () { final cp1250 = encodeText(text, TextEncoding.windows1250); expect(detectEncoding(cp1250), isNull); expect(() => decodeDocument(cp1250), throwsA(isA())); final d = decodeDocument(cp1250, encoding: TextEncoding.windows1250); expect(d.text, text); expect(encodeText(d.text, TextEncoding.windows1250), cp1250); final iso = encodeText('Žluťoučký\n', TextEncoding.iso88592); expect( decodeDocument(iso, encoding: TextEncoding.iso88592).text, 'Žluťoučký\n', ); expect(decodeText(b([0xe9]), TextEncoding.latin1), 'é'); }); test('undefined bytes and unrepresentable characters are reported', () { expect( () => decodeText(b([0x81]), TextEncoding.windows1252), throwsFormatException, ); expect( () => encodeText('日本', TextEncoding.windows1250), throwsA(isA().having((u) => u.count, 'count', 2)), ); expect( () => decodeText(b([0xc3]), TextEncoding.utf8), throwsFormatException, ); }); }); group('line endings and final newline', () { test('LF, CRLF and CR are kept; final newline presence is kept', () { roundTrip( b(utf8.encode('a\nb\n')), TextEncoding.utf8, lineBreak: LineBreak.lf, ); roundTrip( b(utf8.encode('a\r\nb\r\n')), TextEncoding.utf8, lineBreak: LineBreak.crlf, ); roundTrip( b(utf8.encode('a\rb\r')), TextEncoding.utf8, lineBreak: LineBreak.cr, ); roundTrip(b(utf8.encode('a\nb')), TextEncoding.utf8); expect(decodeDocument(b(utf8.encode('a\nb'))).finalNewline, false); expect(decodeDocument(b(utf8.encode('a\nb\n'))).finalNewline, true); }); test('mixed endings are detected and not reported as exact', () { final d = decodeDocument(b(utf8.encode('a\r\nb\nc\r\n'))); expect(d.lineBreaks.mixed, true); expect(d.lineBreaks.dominant, LineBreak.crlf); expect(d.exact, false); }); }); test('binary data is recognised, UTF-16 text is not binary', () { expect(looksBinary(b([0x89, 0x50, 0x4e, 0x47, 0, 0, 0, 0x0d])), true); expect(looksBinary(b(utf8.encode('server {\n listen 80;\n}\n'))), false); expect(looksBinary(encodeText('hello world', TextEncoding.utf16le)), false); }); group('syntax detection', () { final cases = { 'index.html': 'HTML', 'page.xhtml': 'HTML', 'index.php': 'PHP', 'style.scss': 'SCSS', 'style.sass': 'Sass (indented)', 'app.mjs': 'JavaScript', 'App.tsx': 'TypeScript', 'data.jsonl': 'JSON Lines', 'schema.xsd': 'XML', 'compose.yml': 'YAML', 'README.md': 'Markdown', 'main.py': 'Python', 'main.dart': 'Dart', 'Main.kt': 'Kotlin', 'lib.rs': 'Rust', 'main.go': 'Go', 'x.hpp': 'C++', 'P.cs': 'C#', 'a.rb': 'Ruby', 'a.pm': 'Perl', 'a.lua': 'Lua', 'a.psm1': 'PowerShell', 'a.zsh': 'Shell (Bash/sh/zsh)', 'a.fish': 'fish', 'q.sql': 'SQL', 's.gql': 'GraphQL', 'app.toml': 'TOML', 'nginx.service': 'systemd unit', 'app.desktop': 'systemd unit', 'Info.plist': 'Property list (XML)', '.htaccess': 'Apache config', '.gitignore': 'Unix config', '.editorconfig': 'INI / config', '.env': '.env', '.env.production': '.env', 'Dockerfile': 'Dockerfile', 'Dockerfile.dev': 'Dockerfile', 'Makefile': 'Makefile', 'CMakeLists.txt': 'CMake', 'nginx.conf': 'NGINX', '/etc/nginx/sites-available/site.conf': 'NGINX', 'httpd.conf': 'Apache config', 'sshd_config': 'SSH config', '/home/u/.ssh/config': 'SSH config', 'hosts': 'Unix config', 'fstab': 'Unix config', 'crontab': 'Unix config', '.bashrc': 'Shell (Bash/sh/zsh)', 'x.patch': 'Diff / patch', 'main.tf': 'Terraform (HCL)', 'build.gradle': 'Gradle', 'a.proto': 'Protocol Buffers', 'run.bat': 'Batch (cmd)', 'server.pem': 'PEM / key / certificate', 'notes.txt': 'Text', 'app.log': 'Log', 'table.tsv': 'CSV / TSV', }; test('by name, extension and path', () { cases.forEach((name, label) { expect(detectSyntax(name).label, label, reason: name); }); }); test('files without extension: shebang and content', () { expect( detectSyntax('deploy', content: '#!/usr/bin/env bash\n').id, 'bash', ); expect( detectSyntax('tool', content: '#!/usr/bin/python3\n').id, 'python', ); expect( detectSyntax('unit', content: '[Unit]\nDescription=x\n').id, 'systemd', ); expect(detectSyntax('page', content: '').id, 'html'); expect(detectSyntax('README').id, 'text'); }); test('every highlighted syntax has a bundled grammar', () { for (final s in syntaxes) { if (s.grammar != null) { expect(grammars[s.grammar], isNotNull, reason: s.id); } } }); test('viewer kinds', () { expect(fileKind('a.HEIC'), FileKind.image); expect(fileKind('a.svg'), FileKind.svg); expect(fileKind('a.docx'), FileKind.office); expect(fileKind('a.tar.gz'), FileKind.archive); expect(fileKind('a.ipynb'), FileKind.notebook); expect(fileKind('a.crt'), FileKind.certificate); expect(fileKind('a.mp4'), FileKind.video); expect(fileKind('blob', head: b([0x25, 0x50, 0x44, 0x46])), FileKind.pdf); expect(fileKind('nginx.conf'), FileKind.text); }); }); group('validation never changes the text', () { test('JSON, JSON Lines, YAML and XML report line and column', () { expect(validate('json', '{"a": [1, 2]}'), isNull); final j = validate('json', '{\n "a": 1,\n "b": }')!; expect(j.line, 3); expect(validate('jsonl', '{"a":1}\n{"b":\n')!.line, 2); expect(validate('yaml', 'a: 1\nb: [1, 2\n'), isNotNull); expect(validate('yaml', 'a: 1\nlist:\n - x\n'), isNull); final x = validate('xml', '\n ')!; expect(x.line, isNotNull); expect(validate('xml', ''), isNull); expect(validate('python', 'whatever'), isNull); }); test('formatting is explicit and only for valid input', () { expect( formatText('json', '{"a":[1,2]}'), '{\n "a": [\n 1,\n 2\n ]\n}', ); expect(formatText('json', '{"a":'), isNull); expect(formatText('xml', '', indent: 2), '\n \n'); }); }); }