Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ First stable release. wind is utility-first, Tailwind-syntax styling for Flutter
- New regression coverage for the arbitrary-hex background (`background_parser_test.dart`), `decoration`-stays-null contract (`wind_style_test.dart`), malformed-JSON graceful degradation (`w_dynamic_renderer_test.dart`), `WindThemeData` value equality (`wind_theme_data_test.dart`), and icon-only button Semantics label (`w_button_test.dart`).
- `tool/coverage.sh` portable threshold-aware lcov wrapper; GitHub Actions gate fails any PR dropping below 90%.
- Surgical `// coverage:ignore-line` pragmas only on lines structurally unreachable from `flutter test` (`kDebugMode` branches, `dart:io` `Platform.is*` branches not matching the CI host). Each pragma carries a one-line WHY comment.
- Final 1.0.0 QA gate: added permanent regression suites under `test/pixel/` (px-exact geometry + color via `getRect`/`getSize`/`renderObject`, no golden files), `test/interaction/` (tap/hover/focus/disabled/responsive/animation/overlay in-process), and `test/performance/` (parser cache hit/miss speedup ratio gate, ~26x, plus report-only large-tree pump timing). Validated live via a fresh consumer app driven by `fluttersdk_dusk` (all routes navigated, `wind:` enricher 7-field block confirmed, screenshots manually compared). No behavioral regressions found.

Production deps: `flutter` (SDK), `flutter_svg ^2.0.0`, `fluttersdk_wind_diagnostics_contracts ^1.0.0`. Dev deps: `flutter_test` (SDK), `flutter_lints ^5.0.0`. Full v1 documentation at [fluttersdk.com/wind](https://fluttersdk.com/wind); LLM-facing skill at `skills/wind-ui/` distributed via [fluttersdk/ai](https://github.com/fluttersdk/ai) (`npx skills add fluttersdk/ai --skill wind-ui`).

Expand Down
2 changes: 2 additions & 0 deletions skills/wind-ui/references/tailwind-divergence.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,5 @@ These canonical Tailwind classes are silently ignored by wind (unknown tokens ar
| `text-7xl` / `8xl` / `9xl` | silently capped (max is `text-6xl`) | `text-6xl` or arbitrary `text-[96px]` |

Reminder: a wind page needs a Material ancestor (a `Scaffold`) for `WText` to inherit a default text style; without one, Flutter renders the yellow-underline fallback. Real apps always have a `Scaffold`, so this only bites bare `WDiv > WText` route bodies.

State-shadowing edge: a disabled outer `WAnchor` does NOT suppress a `hover:` class that a nested `WDiv` carries on itself. `WDiv` auto-wraps in its own non-disabled `WAnchor` whose state provider shadows the ancestor's `isDisabled`, so the inner `hover:` still fires. This is an edge case (a disabled control wrapping a separately-hover-styled child); style the disabled state on the same element that owns the `hover:` class, or drive `disabled:` on the inner `WDiv` directly.
222 changes: 222 additions & 0 deletions test/interaction/animation_overlay_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fluttersdk_wind/fluttersdk_wind.dart';

/// Wraps [child] in a MaterialApp + WindTheme + Scaffold so className-styled
/// widgets resolve their styling context.
Widget wrapWithTheme(Widget child) {
return MaterialApp(
home: WindTheme(
data: WindThemeData(),
child: Scaffold(body: child),
),
);
}

void main() {
// Parser cache persists between tests; clearing avoids false-positive passes.
setUp(WindParser.clearCache);

group('ANIMATION characterization', () {
// NEVER pumpAndSettle here: animate-* loops forever and would hang.

testWidgets('animate-spin renders a RotationTransition', (tester) async {
const key = ValueKey('spin');
await tester.pumpWidget(
wrapWithTheme(
const WDiv(
key: key,
className: 'animate-spin w-8 h-8 bg-blue-500',
child: Text('S'),
),
),
);
await tester.pump(const Duration(milliseconds: 16));

expect(
find.descendant(
of: find.byKey(key),
matching: find.byType(RotationTransition),
),
findsOneWidget,
);
});

testWidgets('animate-pulse renders a FadeTransition', (tester) async {
const key = ValueKey('pulse');
await tester.pumpWidget(
wrapWithTheme(
const WDiv(
key: key,
className: 'animate-pulse w-8 h-8 bg-blue-500',
child: Text('P'),
),
),
);
await tester.pump(const Duration(milliseconds: 16));

expect(
find.descendant(
of: find.byKey(key),
matching: find.byType(FadeTransition),
),
findsOneWidget,
);
});

testWidgets('animate-ping renders an AnimatedBuilder (Transform path)',
(tester) async {
const key = ValueKey('ping');
await tester.pumpWidget(
wrapWithTheme(
const WDiv(
key: key,
className: 'animate-ping w-8 h-8 bg-blue-500',
child: Text('I'),
),
),
);
await tester.pump(const Duration(milliseconds: 16));

// ping animates scale + opacity via an AnimatedBuilder that emits a
// Transform every frame.
expect(
find.descendant(
of: find.byKey(key),
matching: find.byType(AnimatedBuilder),
),
findsOneWidget,
);
expect(
find.descendant(
of: find.byKey(key),
matching: find.byType(Transform),
),
findsWidgets,
);
});

testWidgets('animate-bounce renders an AnimatedBuilder (Transform path)',
(tester) async {
const key = ValueKey('bounce');
await tester.pumpWidget(
wrapWithTheme(
const WDiv(
key: key,
className: 'animate-bounce w-8 h-8 bg-blue-500',
child: Text('B'),
),
),
);
await tester.pump(const Duration(milliseconds: 16));

expect(
find.descendant(
of: find.byKey(key),
matching: find.byType(AnimatedBuilder),
),
findsOneWidget,
);
expect(
find.descendant(
of: find.byKey(key),
matching: find.byType(Transform),
),
findsWidgets,
);
});
});

group('OVERLAY characterization', () {
testWidgets('WPopover opens content and closes on the close callback',
(tester) async {
await tester.pumpWidget(
wrapWithTheme(
WPopover(
className: 'w-48 bg-white rounded-lg',
triggerBuilder: (context, isOpen, isHovering) =>
const Text('Open menu'),
contentBuilder: (context, close) => WButton(
onTap: close,
child: const Text('Close menu'),
),
),
),
);

// Closed: content is not present.
expect(find.text('Close menu'), findsNothing);

// Open via the trigger.
await tester.tap(find.text('Open menu'));
await tester.pumpAndSettle();
expect(find.text('Close menu'), findsOneWidget);

// Close via the content button's close callback.
await tester.tap(find.text('Close menu'));
await tester.pumpAndSettle();
expect(find.text('Close menu'), findsNothing);
});

testWidgets('WPopover closes on outside tap', (tester) async {
await tester.pumpWidget(
wrapWithTheme(
Column(
children: [
WPopover(
className: 'w-48 bg-white rounded-lg',
triggerBuilder: (context, isOpen, isHovering) =>
const Text('Open popover'),
contentBuilder: (context, close) => const Text('Panel body'),
),
const SizedBox(height: 200, child: Text('Outside area')),
],
),
),
);

await tester.tap(find.text('Open popover'));
await tester.pumpAndSettle();
expect(find.text('Panel body'), findsOneWidget);

// Tap outside the overlay; TapRegion.onTapOutside closes it.
await tester.tapAt(tester.getCenter(find.text('Outside area')));
await tester.pumpAndSettle();
expect(find.text('Panel body'), findsNothing);
});

testWidgets('WSelect opens the option list and closes after selecting',
(tester) async {
String? selected;

await tester.pumpWidget(
wrapWithTheme(
WSelect<String>(
value: null,
placeholder: 'Choose',
options: const [
SelectOption(value: 'x', label: 'Xenon'),
SelectOption(value: 'y', label: 'Yttrium'),
],
onChange: (v) => selected = v,
),
),
);

// Closed: options not shown.
expect(find.text('Yttrium'), findsNothing);

// Open.
await tester.tap(find.text('Choose'));
await tester.pump();
expect(find.text('Yttrium'), findsOneWidget);

// Select an option -> single-select closes the menu.
await tester.tap(find.text('Yttrium'));
await tester.pump();

expect(selected, 'y');
expect(find.text('Yttrium'), findsNothing);
});
});
}
Loading
Loading