validate method
Implementation
EnsembleTestValidationResult validate() {
final issues = <EnsembleTestValidationIssue>[];
void add(
ValidationSeverity severity,
String code,
String message, {
String? path,
String? testId,
}) {
issues.add(EnsembleTestValidationIssue(
severity: severity,
code: code,
message: message,
path: path,
testId: testId,
));
}
late final EnsembleAppInspection inspection;
try {
inspection = EnsembleAppInspector(appDir).inspect();
} on StateError catch (error) {
add(ValidationSeverity.error, 'config', error.message);
return EnsembleTestValidationResult(issues);
}
final testsDirRelative = p.posix.join(inspection.appPath, 'tests');
final testsDir = Directory(p.join(appDir, testsDirRelative));
if (!testsDir.existsSync()) {
add(
ValidationSeverity.error,
'missingTests',
'No declarative tests found. Add *.test.yaml files under $testsDirRelative/',
);
return EnsembleTestValidationResult(issues);
}
final testFiles = testsDir
.listSync(recursive: true)
.whereType<File>()
.where((file) => file.path.endsWith('.test.yaml'))
.toList()
..sort((a, b) => a.path.compareTo(b.path));
if (testFiles.isEmpty) {
add(
ValidationSeverity.error,
'missingTests',
'No declarative tests found. Add *.test.yaml files under $testsDirRelative/',
);
return EnsembleTestValidationResult(issues);
}
final screensByName = {
for (final screen in inspection.screens) screen.name: screen
};
final knownScreens = screensByName.keys.toSet();
final knownWidgetIds = inspection.screens.expand((s) => s.testIds).toSet();
final knownApis = inspection.screens.expand((s) => s.apis).toSet();
final ids = <String, String>{};
final prerequisites = <String, String>{};
for (final file in testFiles) {
final relativePath =
p.relative(file.path, from: appDir).replaceAll('\\', '/');
final dynamic doc = loadYaml(file.readAsStringSync());
if (doc is! YamlMap) {
add(ValidationSeverity.error, 'parse', 'Test root must be a map',
path: relativePath);
continue;
}
final id = doc['id']?.toString();
if (id == null || id.isEmpty) {
add(ValidationSeverity.error, 'missingId', 'Each test must have an id',
path: relativePath);
continue;
}
final existing = ids[id];
if (existing != null) {
add(
ValidationSeverity.error,
'duplicateId',
'Duplicate test id "$id" also found in $existing',
path: relativePath,
testId: id,
);
} else {
ids[id] = relativePath;
}
final startScreen = doc['startScreen']?.toString();
final prerequisite = doc['prerequisite']?.toString();
if (prerequisite != null && prerequisite.isNotEmpty) {
prerequisites[id] = prerequisite;
}
if (startScreen != null &&
startScreen.isNotEmpty &&
!knownScreens.contains(startScreen)) {
add(
ValidationSeverity.error,
'unknownScreen',
'Unknown startScreen "$startScreen"',
path: relativePath,
testId: id,
);
}
final steps = doc['steps'];
if (steps is! YamlList || steps.isEmpty) {
add(
ValidationSeverity.error,
'missingSteps',
'Test must define a non-empty steps list',
path: relativePath,
testId: id,
);
continue;
}
final stepInfo = _collectStepInfo(steps);
for (final widgetId in stepInfo.widgetIds) {
if (!knownWidgetIds.contains(widgetId)) {
add(
ValidationSeverity.warning,
'unknownWidgetId',
'No obvious widget id/testId definition found for "$widgetId"',
path: relativePath,
testId: id,
);
}
}
for (final api in stepInfo.apiNames) {
if (!knownApis.contains(api)) {
add(
ValidationSeverity.warning,
'unknownApi',
'No obvious API definition found for "$api"',
path: relativePath,
testId: id,
);
}
}
for (final fixture in stepInfo.fixtures) {
final fixtureFile = File(
fixture.startsWith('fixtures/')
? p.join(testsDir.path, fixture)
: p.join(testsDir.path, 'fixtures', fixture),
);
if (!fixtureFile.existsSync()) {
add(
ValidationSeverity.error,
'missingFixture',
'Fixture not found: tests/fixtures/$fixture',
path: relativePath,
testId: id,
);
}
}
final rootMocks = _rootMockApis(doc);
final onLoadApis =
startScreen != null && screensByName[startScreen] != null
? screensByName[startScreen]!.apis
: const <String>[];
for (final api in onLoadApis) {
if (stepInfo.mockApis.contains(api) && !rootMocks.contains(api)) {
add(
ValidationSeverity.warning,
'mockPlacement',
'API "$api" may run during onLoad. Prefer root mocks.apis for startup APIs.',
path: relativePath,
testId: id,
);
}
}
}
for (final entry in prerequisites.entries) {
if (!ids.containsKey(entry.value)) {
add(
ValidationSeverity.error,
'unknownPrerequisite',
'Unknown prerequisite "${entry.value}"',
path: ids[entry.key],
testId: entry.key,
);
}
}
return EnsembleTestValidationResult(issues);
}