agentleFS
jahfaliabdulrahman-devjahfaliabdulrahman-dev/hermes-skills/skills/flutter-isar-testing/SKILL.md

flutter-isar-testing

Skill1 starsChanged 41 days agoApache-2.0View on GitHub

Use when testing Isar in Flutter (اختبار Isar). In-memory Isar testing setup for Flutter — unit tests with real Isar DB on macOS

The file

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 0 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    If tearDown calls isar.close(), don't close manually in the test:

    line 131
  2. Very specific:

    If the widget shows an infinite loading spinner by design, never pumpAndSettle while it is visible — use await tester.pump(const …

    line 155
  3. Very specific:

    Route/sheet exit animations need fake-clock frames: After Navigator.pop (sheet close, dialog close, route fallback), assert the destination …

    line 160
  4. Very specific:

    Run: flutter test integration_test/ (requires connected device)

    line 183

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Flutter Isar Testing — In-Memory Setup

Problem

flutter test runs headless and can't find libisar.dylib on macOS. Isar.open() requires a real directory path (not null). Widget tests with the app's root widget crash (SIGTERM) because the app initializes Isar + all providers.

Solution: 3-Part Setup

1. Symlink libisar.dylib to project root

Isar looks for the native library in the project root. isar_flutter_libs has it but flutter test doesn't link it automatically.

ln -sf "$HOME/.pub-cache/hosted/pub.dev/isar_flutter_libs-*/macos/libisar.dylib" \
  "$PWD/libisar.dylib"

2. Test helper — openTestIsar()

Create test/helpers/test_helpers.dart:

import 'dart:io';
import 'package:isar/isar.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// Import ALL schemas + the isarProvider
import 'package:my_app/data/models/vehicle.dart';
import 'package:my_app/data/models/maintenance_record.dart';
import 'package:my_app/data/models/service_task.dart';
import 'package:my_app/data/models/part_price.dart';
import 'package:my_app/data/models/invoice_image.dart';
import 'package:my_app/data/datasources/local/isar_provider.dart';

Future<Isar> openTestIsar() async {
  final name = 'test_${DateTime.now().microsecondsSinceEpoch}';
  final dir = await Directory.systemTemp.createTemp('isar_test_');

  return Isar.open(
    [VehicleSchema, MaintenanceRecordSchema, ServiceTaskSchema,
     PartPriceSchema, InvoiceImageSchema],
    directory: dir.path,  // REQUIRED — Isar 3.1.0+1 doesn't accept null
    name: name,
  );
}

Future<List<Override>> createTestOverrides() async {
  final isar = await openTestIsar();
  return [isarProvider.overrideWithValue(isar)];
}

3. Test pattern

import 'package:flutter_test/flutter_test.dart';
import 'package:isar/isar.dart';
import 'helpers/test_helpers.dart';

void main() {
  late Isar isar;

  setUp(() async {
    isar = await openTestIsar();
  });

  tearDown(() async {
    await isar.close(deleteFromDisk: true);
  });

  group('My Test', () {
    test('CRUD works', () async {
      final vehicle = createTestVehicle(name: 'Test Car');
      await isar.writeTxn(() async {
        await isar.vehicles.put(vehicle);
      });

      final stored = await isar.vehicles.get(vehicle.id);
      expect(stored!.name, equals('Test Car'));
    });
  });
}

Pitfalls & Fixes

isActiveEqualTo not available for non-indexed bool

Isar only generates query methods for @Index() fields. For non-indexed booleans:

// ❌ Fails
final active = await isar.vehicles.where().isActiveEqualTo(true).findAll();

// ✅ Works
final all = await isar.vehicles.where().findAll();
final active = all.where((v) => v.isActive).toList();

SettingsNotifier can't be used directly

Notifier requires a Riverpod container to build. Don't instantiate it directly:

// ❌ Fails — LateInitializationError
final notifier = SettingsNotifier();
notifier.build();
notifier.toggleLocale();

// ✅ Test SettingsState directly
const state = SettingsState(locale: AppLocale.en);
expect(state.t('app_title'), equals('My App'));

Cost predictor outlier test needs enough normal data

With too few data points, the std dev is inflated and outliers aren't filtered:

// ❌ 4 normal + 1 outlier — std dev too high, outlier not filtered
final records = [100, 105, 110, 1000]; // mean=328, std=377, z(1000)=1.78 < 2.0

// ✅ 10 normal + 1 outlier — std dev stable, outlier filtered
final records = [100, 102, 104, 106, 108, 100, 102, 104, 106, 108, 10000];

Don't double-close Isar

If tearDown calls isar.close(), don't close manually in the test:

// ❌ IsarError: instance already closed
test('test', () async {
  // ... test ...
  await isar.close(deleteFromDisk: false); // conflicts with tearDown
});

// ✅ Let tearDown handle cleanup
test('test', () async {
  // ... test ... (no manual close)
});

Widget tests with the app's root widget crash in headless runner

The app's root widget initState() calls initIsarDatabase() which needs real native libs. Options:

  1. Use createTestOverrides() + ProviderScope to inject test Isar
  2. Move heavy widget tests to integration_test/ for device-based execution

Widget tests hang: pumpAndSettle timed out with Isar under FakeAsync

pumpAndSettle waits until no frames are scheduled, but an infinite spinner (loading state) or real async Isar work never settles under the widget test's FakeAsync clock → the test times out.

  • If the widget shows an infinite loading spinner by design, never pumpAndSettle while it is visible — use await tester.pump(const Duration(...)) with explicit steps.
  • Isar is REAL async; its futures do not resolve as the fake clock advances. For widgets that touch Isar: override the repository/use-case (inject fakes/Riverpod overrides) OR test with real-async patterns.
  • ⚠️ STOP-28 correction (2026-08-11, LL-053): the previous wording "test with real-async patterns (tester.runAsync) plus pump() with fixed durations" was AMBIGUOUS and was read as "call pump inside runAsync" — that nesting hung Linux CI for 10 minutes (fake-clock primitives inside real-clock runAsync starve waiting for a frame the real clock never schedules). It propagated to 13 files (44 sites). The correct rule: runAsync wraps the WAITING only — NEVER pumpWidget/pump/tap inside it. The safe shape is settleReal from test/helpers/pump.dart (runAsync wraps only the waiting, never a pump), and test/structure/test_harness_policy_test.dart fails on any NEW nesting. When a test passes locally but hangs CI on Linux — suspect this pattern first, not the scheduler.
  • until: must be DATA-DEPENDENT, never chrome (2026-08-11, paid-debt round). The AppBar title of the destination screen renders before its providers' Isar reads land. Waiting on the title returns early → the assertions fail AND teardown's isar.close() can hang on in-flight queries. Correct conditions, in order of strength: (a) single-provider screen → the first data text inside the body ('وش هي بالضبط؟' on TaskRecord); (b) multi-provider screen → find.byType(CircularProgressIndicator).evaluate().isEmpty && <screen title>; (c) full-app splash redirect → no spinner AND the seeded hero data ('Toyota Camry'), never the dashboard AppBar title.
  • Teardown hang class: trailing real async blocks isar.close(). After a test that triggers a real write chain (TX + storage + provider invalidations), the app's tail may leave the Isar isolate busy; addTearDown(() => isar.close(...)) then hangs forever (not caught by --timeout, which only covers the test body). Fix: before the test ends, await tester.settleReal(cycles: 40, step: const Duration(milliseconds: 10)); — real event-loop turns deliver the pending response, the fake-frame pump flushes its continuation. A drain query alone (runAsync(findFirst)) does NOT fix it; unmounting the tree does NOT fix it; the settleReal alternation does.
  • Route/sheet exit animations need fake-clock frames. After Navigator.pop (sheet close, dialog close, route fallback), assert the destination by waiting settleReal(until: <the thing gone or the next screen's data>) — a single bare pump() advances the fake clock by zero and the old screen is still in the tree.
  • Diagnose before patching: probe whether the async layer resolves under FakeAsync at all (a quick tester.runAsync experiment) — throwing a different pump strategy blindly just moves the timeout.

Integration Tests (Device-Based)

For tests that need real Isar + full app lifecycle:

integration_test/
  driver.dart
  tc01_test.dart
  app_full_flow_test.dart
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  // ... tests run on device with real Isar
}

Run: flutter test integration_test/ (requires connected device)

Project File Structure

project/
  libisar.dylib              ← symlink to isar_flutter_libs
  test/
    helpers/
      test_helpers.dart      ← openTestIsar() + factory helpers
    TC001_test.dart          ← pure logic (no app root widget)
    TC002_test.dart          ← Isar CRUD
    ...
  integration_test/
    driver.dart              ← integration test driver
    app_full_flow_test.dart  ← full app on device

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.