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
13 changes: 13 additions & 0 deletions lib/src/plugins/test/builders/test_builder_entity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ extension TestBuilderEntity on TestBuilder {
className = 'Update${entityName}UseCase';
returnTypeConstructor = 't$entityName';
break;
case 'toggle':
// #289: PR #287 added 'toggle' to the entity-methods default used by
// the di/test plugins (['get', 'update', 'toggle']) so canonical
// `zfa make <Entity> --preset=crud --with=vpc,state,di,test` routes to
// per-method generation. Every other generator (usecase, controller,
// presenter, view, repository, datasource, di) has a `toggle` case —
// the per-method test builder must too, otherwise the test plugin
// crashes with `Unknown method: toggle` before any file is written.
// Mirrors the usecase generator: `Toggle${entityName}UseCase` returns
// the toggled entity (Future<Entity>), not a stream and not void.
className = 'Toggle${entityName}UseCase';
returnTypeConstructor = 't$entityName';
break;
case 'delete':
className = 'Delete${entityName}UseCase';
returnTypeConstructor = 'null';
Expand Down
35 changes: 35 additions & 0 deletions lib/src/plugins/test/builders/test_builder_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,22 @@ extension TestBuilderHelpers on TestBuilder {
'UpdateParams<$idType, $dataType>',
).call([], {'id': idValue, 'data': dataValue}),
];
case 'toggle':
// #289: registerFallbackValue needs a concrete ToggleParams instance
// matching the usecase generator's signature (ToggleParams<I, F>
// with id/field/value). `${entityName}Fields` is the Field-class
// re-exported by the entity file; `config.queryField` (default 'id')
// resolves to a `Field<Entity, IdType>` constant the same way the
// `get` branch below does for its QueryParams filter.
return [
refer(
'ToggleParams<$idType, ${entityName}Fields>',
).call([], {
'id': idValue,
'field': refer('${entityName}Fields').property(config.queryField),

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness · 🟠 Major · 🔨 Medium

Emitted toggle test file does not typecheck — field: TodoFields.id is a Field, not a TodoFields

The toggle branch constructs ToggleParams<String, ${entityName}Fields>(id: ..., field: ${entityName}Fields.<queryField>, value: true) (here and again in _getFallbackValues at line 104). ToggleParams<I, F> declares field as type F, and the whole generated stack instantiates it as F = ${entityName}Fields. But ${entityName}Fields.id is a static const Field<Entity, IdType> property (see zorphy's fields_class_generator.dart and real generated entities), and abstract final class TodoFields has no instances — so no value of type TodoFields can ever be constructed.

dart analyze on a faithful minimal repro with the real ToggleParams and a real-shaped TodoFields fails:

error - The argument type 'Field<Todo, String>' can't be assigned to the parameter type 'TodoFields'.

Impact: in a real Flutter project, zfa make <Entity> --preset=crud --with=vpc,state,di,test with toggle in the methods list now succeeds (no crash — good) but emits toggle_<entity>_usecase_test.dart that fails dart analyze. The crash is replaced by silently-broken generated code.

Note this mirrors a latent inconsistency in the pre-existing toggle stack (the presenter also types field as ${entityName}Fields while the datasource consumes params.field as a Field via existing.copyWithField(params.field, ...)) — so the root fix is upstream: align ToggleParams's F parameter with the actual field-descriptor type (e.g. Field<Entity, bool>), or provide a constructible field value. Within this PR's scope, at minimum the emitted test should not reference ${entityName}Fields.<queryField> as a value.

Proposed fix

The cleanest fix is to stop treating the Fields class as a value type. Either:

  1. Change ToggleParams<I, F>'s F to the field descriptor type and generate ToggleParams<String, Field<Todo, bool>>(field: TodoFields.isCompleted, ...) across usecase/presenter/datasource/test — a broader, consistent fix; or
  2. In the test builder only, construct the params via a value that actually typechecks (e.g. a mocktail-safe placeholder of the correct type).

Because abstract final class TodoFields has no instances, option 2 is impossible without a core change — so this needs the upstream alignment (option 1). Verify with dart analyze on a generated workspace's test/domain/usecases/ after the change.

🤖 Prompt for AI Agents

In lib/src/plugins/test/builders/test_builder_helpers.dart (toggle branches in
_getFallbackValues and _generateFutureTests), the emitted
ToggleParams<String, TodoFields>(field: TodoFields.id, ...) does not typecheck:
TodoFields.id is a static const Field<Todo, String>, but the field parameter
has type F = TodoFields, an abstract final class with no instances. Align the
ToggleParams F type argument with the field-descriptor type actually consumed
(e.g. Field<Todo, bool>) across the usecase generator, presenter, datasource,
and test builders, then verify by generating a workspace with methods
[get, update, toggle] and running `dart analyze` on the emitted
test/domain/usecases/** files.

'value': literalBool(true),
}),
];
case 'delete':
return [
refer('DeleteParams<$idType>').constInstance([], {'id': idValue}),
Expand Down Expand Up @@ -182,6 +198,25 @@ extension TestBuilderHelpers on TestBuilder {
verifyCall = refer(
mockVarName,
).property('update').call([refer('any').call([])]);
} else if (method == 'toggle') {
// #289: Mirror the usecase generator's toggle shape — ToggleParams<I, F>
// with id, field (a Field<Entity, IdType> from ${entityName}Fields), and
// a bool value. The mock repository call is `toggle(any())`, identical to
// update/create — the per-method test builder only needs the params
// expression and the mock call shape to match.
paramsExpr = refer(
'ToggleParams<$idType, ${entityName}Fields>',
).call([], {
'id': idValue,
'field': refer('${entityName}Fields').property(config.queryField),
'value': literalBool(true),
});
arrangeCall = refer(
mockVarName,
).property('toggle').call([refer('any').call([])]);
verifyCall = refer(
mockVarName,
).property('toggle').call([refer('any').call([])]);
} else if (method == 'delete') {
paramsExpr = refer('DeleteParams<$idType>').call([], {'id': idValue});
arrangeCall = refer(
Expand Down
63 changes: 63 additions & 0 deletions test/integration/toggle_method_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,67 @@ void main() {
expect(controllerContent, contains('isToggling'));
expect(controllerContent, contains('_presenter.toggleTodo'));
});

// Regression test for #289: PR #287 added 'toggle' to the entity-methods
// default used by the di/test plugins (['get', 'update', 'toggle']) so a
// canonical `zfa make <Entity> --preset=crud --with=vpc,state,di,test`
// routes through the per-method generators. The test builder's method
// switch had no `toggle` case, so the test plugin crashed with
// `Unknown method: toggle` before emitting any file. This locks in the
// fix: with the test plugin on + toggle in the methods list, generation
// succeeds AND the per-method toggle usecase test file is emitted with the
// expected shape.
test(
'#289 — toggle test file is generated when test plugin is on (no Unknown method: toggle crash)',
() async {
final generator = CodeGenerator(
config: GeneratorConfig(
name: 'Todo',
methods: const ['get', 'update', 'toggle'],
generateData: true,
generateLocal: true,
generateUseCase: true,
generateVpcs: true,
generateState: true,
generateDi: true,
generateTest: true,
outputDir: outputDir,
),
outputDir: outputDir,
options: const GeneratorOptions(
dryRun: false,
force: true,
verbose: false,
),
);

final result = await generator.generate();
expect(
result.success,
isTrue,
reason: 'Generation crashed: ${result.errors.join('; ')}',
);

// The per-method test builder must emit a toggle-specific test file.
final toggleTestFile = File(
'$outputDir/../../test/domain/usecases/todo/toggle_todo_usecase_test.dart',
);
expect(
toggleTestFile.existsSync(),
isTrue,
reason: 'toggle_todo_usecase_test.dart should be generated',
);

final content = toggleTestFile.readAsStringSync();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 Tests · 🟡 Minor · ⚡ Quick win

Regression test never compiles the generated test file — it can't catch the type error above

The new test asserts result.success, file existence, and four contains(...) string checks against the emitted test file. It never runs dart analyze (or dart compile) on the generated workspace, so the emitted toggle_todo_usecase_test.dart could be full of type errors and this test would still pass — which is exactly what happens today (see the major finding). The test name promises "no Unknown method: toggle crash", and it verifies that, but the deliverable is a compilable test file, and that property is unverified.

The test utilities already expose runDartAnalyze(workspace, [...paths]) (regression_test_utils.dart) — wire it in so generation output is compile-verified, not just string-sniffed.

Proposed fix

After the expect(result.success, isTrue, ...) block, add:

final analyze = await runDartAnalyze(workspace, [
  'test/domain/usecases/todo/toggle_todo_usecase_test.dart',
]);
expect(analyze.exitCode, 0, reason: analyze.stdout);

(plus the equivalent dart analyze check on the lib/src output, which would also surface the pre-existing copyWithField gap in the generated local datasource).

🤖 Prompt for AI Agents

In test/integration/toggle_method_test.dart, the new #289 regression test
only asserts string containment on the emitted test file. Add a
runDartAnalyze(workspace, ['test/domain/usecases/todo/toggle_todo_usecase_test.dart'])
assertion (exit code 0) so generated test output is compile-verified; the
current suite passes even though the emitted toggle test file does not
typecheck. See regression_test_utils.dart for the helper.

// Class name follows the usecase generator's pattern.
expect(content, contains('ToggleTodoUseCase'));
// The mock repository must be exercised via its toggle method.
expect(content, contains('mockRepository.toggle('));
// The params constructor must match the usecase generator's signature.
expect(content, contains('ToggleParams<String, TodoFields>'));
// The Field constant must come from the entity's Fields class
// (config.queryField defaults to 'id').
expect(content, contains('TodoFields.id'));
},
);
}
Loading