mirror of
https://github.com/we-promise/sure.git
synced 2026-08-05 16:42:18 +00:00
* feat(mobile): add SureFontWeights design tokens Introduce named font-weight tokens so widgets reference a DS tier instead of a raw FontWeight, mirroring the web design system's Tailwind weight utilities (font-medium / font-semibold). - design/tokens/sure.tokens.json: add font.weight.medium (500) and .semibold (600) as the canonical source. - generate_sure_tokens.mjs: emit SureTokens.weightMedium / weightSemibold (with a font-weight resolver + validation), regenerate sure_tokens.dart. - sure_tokens_test.dart: assert the generated weights match canonical. - Migrate the dashboard-area money/hierarchy sites onto the tokens (net_worth_card, account_card, dashboard, recent_transactions, transactions_list): FontWeight.w500 -> SureTokens.weightMedium, FontWeight.w600 -> SureTokens.weightSemibold. Weight-only; other screens adopt the tokens in their own polish PRs. 166 tests pass; flutter analyze: no new issues; token generator --check is clean. * build(tokens): regenerate web _generated.css for new font weights Adding font.weight.medium/semibold to sure.tokens.json also flows through the web token generator (bin/tokens.mjs), which emits --font-weight-medium and --font-weight-semibold. Commit the regenerated _generated.css so the tokens:check gate stays green (node bin/tokens.mjs + git diff --quiet). * test(system): de-flake account edit via account-menu AccountsTest#assert_account_created opened the account menu and clicked Edit immediately after visiting the account page. That page issues a Turbo morph refresh shortly after load (turbo_refreshes_with :morph reacting to a family-stream broadcast), which can detach the menu node mid-click — "Selenium::WebDriver::Error: Node with given id does not belong to the document" — or wipe the just-opened edit form. Capybara does not auto-retry that inspector error, so the test flaked intermittently in CI. Extract open_account_edit_dialog, which retries the menu interaction until the edit form is present, rescuing the transient detach/stale errors. Mirrors the existing PropertyTest#open_account_edit_dialog precedent for the same morph race, but also tolerates the click itself raising while the refresh is in flight.
174 lines
5.1 KiB
Dart
174 lines
5.1 KiB
Dart
// ignore_for_file: deprecated_member_use
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:sure_mobile/theme/sure_tokens.dart';
|
|
|
|
void main() {
|
|
late Map<String, dynamic> tokens;
|
|
|
|
setUpAll(() {
|
|
tokens =
|
|
jsonDecode(_tokensFile().readAsStringSync()) as Map<String, dynamic>;
|
|
});
|
|
|
|
test('generated tokens keep the canonical token version', () {
|
|
expect(SureTokens.version, tokens[r'$version']);
|
|
});
|
|
|
|
test(
|
|
'generated light and dark colors match representative canonical tokens',
|
|
() {
|
|
expect(
|
|
SureTokens.light.surface.value,
|
|
_resolveColor(tokens, 'color.surface', dark: false),
|
|
);
|
|
expect(
|
|
SureTokens.dark.surface.value,
|
|
_resolveColor(tokens, 'color.surface', dark: true),
|
|
);
|
|
expect(
|
|
SureTokens.light.textPrimary.value,
|
|
_resolveColor(tokens, 'utility.text-primary', dark: false),
|
|
);
|
|
expect(
|
|
SureTokens.dark.textPrimary.value,
|
|
_resolveColor(tokens, 'utility.text-primary', dark: true),
|
|
);
|
|
expect(
|
|
SureTokens.light.destructive.value,
|
|
_resolveColor(tokens, 'color.destructive', dark: false),
|
|
);
|
|
expect(
|
|
SureTokens.dark.destructive.value,
|
|
_resolveColor(tokens, 'color.destructive', dark: true),
|
|
);
|
|
},
|
|
);
|
|
|
|
test('generated radii match canonical dimensions', () {
|
|
expect(SureTokens.radiusMd, _resolveDimension(tokens, 'border.radius.md'));
|
|
expect(SureTokens.radiusLg, _resolveDimension(tokens, 'border.radius.lg'));
|
|
});
|
|
|
|
test('generated font weights match canonical tiers', () {
|
|
expect(
|
|
SureTokens.weightMedium.value,
|
|
_resolveWeight(tokens, 'font.weight.medium'),
|
|
);
|
|
expect(
|
|
SureTokens.weightSemibold.value,
|
|
_resolveWeight(tokens, 'font.weight.semibold'),
|
|
);
|
|
});
|
|
|
|
test('generated focus-ring and bg-inverse match canonical tokens', () {
|
|
expect(
|
|
SureTokens.light.focusRing.value,
|
|
_resolveColor(tokens, 'color.focus-ring', dark: false),
|
|
);
|
|
expect(
|
|
SureTokens.dark.focusRing.value,
|
|
_resolveColor(tokens, 'color.focus-ring', dark: true),
|
|
);
|
|
expect(
|
|
SureTokens.light.bgInverse.value,
|
|
_resolveColor(tokens, 'utility.bg-inverse', dark: false),
|
|
);
|
|
expect(
|
|
SureTokens.dark.bgInverse.value,
|
|
_resolveColor(tokens, 'utility.bg-inverse', dark: true),
|
|
);
|
|
});
|
|
|
|
test('generated shadow scale carries mode-aware color and geometry', () {
|
|
final lightSm = SureTokens.light.shadowSm.single;
|
|
expect(lightSm.offset, const Offset(0, 1));
|
|
expect(lightSm.blurRadius, 6.0);
|
|
expect(lightSm.spreadRadius, 0.0);
|
|
expect(
|
|
lightSm.color.value,
|
|
_resolveColorValue(tokens, '{color.black|6%}', dark: false),
|
|
);
|
|
expect(
|
|
SureTokens.dark.shadowSm.single.color.value,
|
|
_resolveColorValue(tokens, '{color.white|8%}', dark: true),
|
|
);
|
|
// The xl step keeps the canonical negative spread (0px 20px 24px -4px).
|
|
expect(SureTokens.light.shadowXl.single.spreadRadius, -4.0);
|
|
});
|
|
}
|
|
|
|
int _resolveColor(
|
|
Map<String, dynamic> tokens,
|
|
String path, {
|
|
required bool dark,
|
|
}) {
|
|
final node = _nodeAt(tokens, path);
|
|
final extensions = node[r'$extensions'] as Map<String, dynamic>?;
|
|
final value = dark && extensions != null && extensions['sure.dark'] != null
|
|
? extensions['sure.dark'] as String
|
|
: node[r'$value'] as String;
|
|
|
|
return _resolveColorValue(tokens, value, dark: dark);
|
|
}
|
|
|
|
int _resolveColorValue(
|
|
Map<String, dynamic> tokens,
|
|
String value, {
|
|
required bool dark,
|
|
}) {
|
|
final hex = RegExp(r'^#([0-9a-fA-F]{6})$').firstMatch(value);
|
|
if (hex != null) {
|
|
return int.parse('FF${hex.group(1)!}', radix: 16);
|
|
}
|
|
|
|
final ref = RegExp(r'^\{([^|}]+)(?:\|([0-9]+)%?)?\}$').firstMatch(value);
|
|
if (ref != null) {
|
|
final resolved = _resolveColor(tokens, ref.group(1)!, dark: dark);
|
|
final alphaPercent = ref.group(2);
|
|
if (alphaPercent == null) return resolved;
|
|
|
|
final alpha = ((int.parse(alphaPercent) / 100) * 255).round();
|
|
return (alpha << 24) | (resolved & 0x00FFFFFF);
|
|
}
|
|
|
|
throw StateError('Unsupported color value: $value');
|
|
}
|
|
|
|
int _resolveWeight(Map<String, dynamic> tokens, String path) {
|
|
return _nodeAt(tokens, path)[r'$value'] as int;
|
|
}
|
|
|
|
double _resolveDimension(Map<String, dynamic> tokens, String path) {
|
|
final value = _nodeAt(tokens, path)[r'$value'] as String;
|
|
final match = RegExp(r'^([0-9]+(?:\.[0-9]+)?)px$').firstMatch(value);
|
|
if (match == null) throw StateError('Unsupported dimension value: $value');
|
|
|
|
return double.parse(match.group(1)!);
|
|
}
|
|
|
|
Map<String, dynamic> _nodeAt(Map<String, dynamic> tokens, String path) {
|
|
dynamic current = tokens;
|
|
for (final part in path.split('.')) {
|
|
current = current[part];
|
|
}
|
|
|
|
return current as Map<String, dynamic>;
|
|
}
|
|
|
|
File _tokensFile() {
|
|
var directory = Directory.current;
|
|
|
|
for (var depth = 0; depth < 4; depth += 1) {
|
|
final file = File('${directory.path}/design/tokens/sure.tokens.json');
|
|
if (file.existsSync()) return file;
|
|
|
|
directory = directory.parent;
|
|
}
|
|
|
|
throw StateError('Could not locate design/tokens/sure.tokens.json');
|
|
}
|