mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-11-10 19:27:05 +01:00
billsplit was a standalone Flutter app in ~/Downloads with one commit and no git remote — that directory was the only copy of its history. Three things the move needed beyond copying the tree: - The root .gitignore silently swallowed 32 of its files (*.png, *.ico, *.manifest). Ignored files do not error on `git add`, they just vanish, so this would have committed an app that cannot build. Added a trailing block of ! overrides plus .binary-allowlist (which did not exist yet) — the documented in-repo exception for binaries a build genuinely needs. The ../testsAndMisc_binaries/ symlink scheme does not apply: those directories are gone, and a symlinked PNG breaks a fresh clone. - billsplit/.github/workflows/ci.yaml was inert once nested; GitHub only reads workflows at the repository root. Relocated with a working-directory and a billsplit/** path filter. - The app's coverage gate moved to python_pkg/billsplit_coverage/ so every Python file stays under python_pkg/, and gained a test suite at 100% branch coverage as that location requires. billsplit/ now holds only Dart. Also replaces the stock Flutter icon with a family icon from the shared generator. The glyph is a filled silhouette because the family's 72px stroke cannot carry a fine torn edge, and the split is an evenodd-cut perforation so the holes stay transparent on the foreground and monochrome layers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
185 lines
5.2 KiB
Dart
185 lines
5.2 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:billsplit/domain/models.dart';
|
|
import 'package:billsplit/state/app_state.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'helpers.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
Directory temp() {
|
|
final d = Directory.systemTemp.createTempSync('billsplit_state');
|
|
addTearDown(() {
|
|
if (d.existsSync()) d.deleteSync(recursive: true);
|
|
});
|
|
return d;
|
|
}
|
|
|
|
test('load on a fresh directory yields empty, usable state', () async {
|
|
final s = AppState(overrideDir: temp());
|
|
await s.load();
|
|
expect(s.loaded, isTrue);
|
|
expect(s.people, isEmpty);
|
|
s.dispose();
|
|
});
|
|
|
|
test('save/load roundtrip preserves everything', () async {
|
|
final dir = temp();
|
|
final s = AppState(overrideDir: dir);
|
|
await s.load();
|
|
final p = s.addPerson('Ala', drinks: false);
|
|
final g = s.addGroup('Meat')..memberIds.add(p.id);
|
|
s.addReceipt(
|
|
Receipt(
|
|
id: 'r1',
|
|
title: 'T',
|
|
items: [
|
|
Item(
|
|
id: 'i1',
|
|
name: 'x',
|
|
totalGr: 100,
|
|
assignment: Assignment(mode: AssignMode.group, groupId: g.id),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
await s.save();
|
|
s.dispose();
|
|
|
|
final s2 = AppState(overrideDir: dir);
|
|
await s2.load();
|
|
expect(s2.people.single.name, 'Ala');
|
|
expect(s2.people.single.drinks, isFalse);
|
|
expect(s2.groups.single.memberIds, {p.id});
|
|
expect(s2.receipts.single.participantIds, {p.id});
|
|
expect(s2.receipts.single.items.single.assignment.groupId, g.id);
|
|
s2.dispose();
|
|
});
|
|
|
|
test('load prunes dangling person references', () async {
|
|
final dir = temp();
|
|
File('${dir.path}/billsplit/state.json')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync(
|
|
jsonEncode({
|
|
'people': [
|
|
{'id': 'p1', 'name': 'Ala'},
|
|
],
|
|
'groups': [
|
|
{
|
|
'id': 'g1',
|
|
'name': 'G',
|
|
'memberIds': ['p1', 'ghost'],
|
|
},
|
|
],
|
|
'receipts': [
|
|
{
|
|
'id': 'r1',
|
|
'title': 'T',
|
|
'date': '2026-01-01T00:00:00.000',
|
|
'participantIds': ['p1', 'ghost'],
|
|
'items': [
|
|
{
|
|
'id': 'i1',
|
|
'name': 'x',
|
|
'totalGr': 1,
|
|
'assignment': {
|
|
'mode': 'custom',
|
|
'customIds': ['p1', 'ghost'],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
final s = AppState(overrideDir: dir);
|
|
await s.load();
|
|
expect(s.groups.single.memberIds, {'p1'});
|
|
expect(s.receipts.single.participantIds, {'p1'});
|
|
expect(s.receipts.single.items.single.assignment.customIds, {'p1'});
|
|
s.dispose();
|
|
});
|
|
|
|
test('corrupt JSON is survived', () async {
|
|
final dir = temp();
|
|
File('${dir.path}/billsplit/state.json')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('{oops');
|
|
final s = AppState(overrideDir: dir);
|
|
await s.load();
|
|
expect(s.loaded, isTrue);
|
|
expect(s.people, isEmpty);
|
|
s.dispose();
|
|
});
|
|
|
|
test('IO failure on load is survived (billsplit path is a file)', () async {
|
|
final dir = temp();
|
|
File('${dir.path}/billsplit').createSync(recursive: true);
|
|
final s = AppState(overrideDir: dir);
|
|
await s.load();
|
|
expect(s.loaded, isTrue);
|
|
s.dispose();
|
|
});
|
|
|
|
test('IO failure on save is survived (state.json is a directory)', () async {
|
|
final dir = temp();
|
|
Directory('${dir.path}/billsplit/state.json').createSync(recursive: true);
|
|
final s = AppState(overrideDir: dir);
|
|
await s.save(); // must not throw
|
|
s.dispose();
|
|
});
|
|
|
|
test('mutations debounce into a save', () async {
|
|
final dir = temp();
|
|
final s = AppState(overrideDir: dir);
|
|
await s.load();
|
|
s.addPerson('Ala');
|
|
expect(File('${dir.path}/billsplit/state.json').existsSync(), isFalse);
|
|
await Future<void>.delayed(const Duration(milliseconds: 600));
|
|
expect(File('${dir.path}/billsplit/state.json').existsSync(), isTrue);
|
|
s.dispose();
|
|
});
|
|
|
|
test('removePerson / removeGroup fallback / removeReceipt', () async {
|
|
final s = AppState(overrideDir: temp());
|
|
await s.load();
|
|
final p = s.addPerson('Ala');
|
|
final g = s.addGroup('G')..memberIds.add(p.id);
|
|
final item = Item(
|
|
id: 'i1',
|
|
name: 'x',
|
|
totalGr: 1,
|
|
assignment: Assignment(mode: AssignMode.group, groupId: g.id),
|
|
);
|
|
final r = Receipt(id: 'r1', title: 'T', items: [item]);
|
|
s
|
|
..addReceipt(r)
|
|
..removeGroup(g);
|
|
expect(item.assignment.mode, AssignMode.everyone);
|
|
expect(item.assignment.groupId, isNull);
|
|
|
|
s.removePerson(p);
|
|
expect(r.participantIds, isEmpty);
|
|
|
|
s.removeReceipt(r);
|
|
expect(s.receipts, isEmpty);
|
|
s.dispose();
|
|
});
|
|
|
|
test('default documents dir is used when no override is given', () async {
|
|
final dir = temp();
|
|
mockPathProvider(dir);
|
|
addTearDown(clearChannelMocks);
|
|
final s = AppState();
|
|
await s.load();
|
|
s.addPerson('Ala');
|
|
await s.save();
|
|
expect(File('${dir.path}/billsplit/state.json').existsSync(), isTrue);
|
|
s.dispose();
|
|
});
|
|
}
|