From fc54d98a7f9dcaf43102efb108aa26f8d2df539d Mon Sep 17 00:00:00 2001 From: Tamara Slosarek Date: Fri, 20 Sep 2024 22:22:39 +0200 Subject: [PATCH] feat(app): add complex any not handled guideline matching --- app/integration_test/drugs_test.dart | 62 ++++++++------ ...e_any_not_handled_fallback_guidelines.dart | 82 ++++++++++++++++++- .../fixtures/set_user_data.dart | 16 ++-- app/lib/common/models/drug/drug.dart | 62 +++++++++++--- app/pubspec.lock | 8 ++ app/pubspec.yaml | 1 + 6 files changed, 186 insertions(+), 45 deletions(-) diff --git a/app/integration_test/drugs_test.dart b/app/integration_test/drugs_test.dart index 5d08b887..a097bb7b 100644 --- a/app/integration_test/drugs_test.dart +++ b/app/integration_test/drugs_test.dart @@ -9,7 +9,7 @@ import 'package:provider/provider.dart'; import 'fixtures/drugs/with_any_fallback_guideline.dart'; import 'fixtures/drugs/with_any_not_handled_guideline.dart'; -// import 'fixtures/drugs/with_multiple_any_not_handled_fallback_guidelines.dart'; +import 'fixtures/drugs/with_multiple_any_not_handled_fallback_guidelines.dart'; import 'fixtures/drugs/with_proper_guideline.dart'; import 'fixtures/drugs/without_guidelines.dart'; import 'fixtures/set_user_data.dart'; @@ -42,7 +42,7 @@ void main() { }, ); - testWidgets('test drug content with proper guideline', (tester) async { + testWidgets('test drug content with guideline', (tester) async { await _expectDrugContent( tester, mockDrugsCubit, @@ -68,7 +68,9 @@ void main() { expectNoGuidelines: true, ); }); + }); + group('integration test for special guidelines', () { testWidgets('test drug content with any fallback guideline', (tester) async { await _expectDrugContent( tester, @@ -77,13 +79,20 @@ void main() { ); }); - testWidgets( - 'test drug content with any not handled fallback guidelines', - (tester) async { - Future testPerGuideline(Drug drug) async { - for ( - final guideline in drug.guidelines - ) { + final anyNotHandledTestCases = { + 'any not handled fallback guideline': + drugWithAnyNotHandledFallbackGuideline, + 'multiple any not handled fallback guidelines': + drugWithMultipleAnyNotHandledFallbackGuidelines, + }; + for (final (anyNotHandledTestCase) in anyNotHandledTestCases.entries) { + final description = 'test drug content with ${anyNotHandledTestCase.key}'; + final drug = anyNotHandledTestCase.value; + for (final (index, guideline) in drug.guidelines.indexed) { + // Run per case to ensure clean setup + testWidgets( + '$description (${index + 1}/${drug.guidelines.length})', + (tester) async { setUserDataForGuideline(guideline); await _expectDrugContent( tester, @@ -91,12 +100,10 @@ void main() { drug: drug, guideline: guideline, ); - } - } - await testPerGuideline(drugWithAnyNotHandledFallbackGuideline); - // await testPerGuideline(drugWithMultipleAnyNotHandledFallbackGuidelines); - }, - ); + }, + ); + } + } }); } @@ -111,7 +118,6 @@ Future _expectDrugContent( }) async { when(() => mockDrugsCubit.state) .thenReturn(isLoading ? DrugState.loading() : DrugState.loaded()); - final relevantGuideline = guideline ?? drug.guidelines.first; await tester.pumpWidget( ChangeNotifierProvider( create: (context) => ActiveDrugs(), @@ -159,15 +165,10 @@ Future _expectDrugContent( ValueKey('annotationCard'), ), ) as RoundedCard; - expect( - card.color, - expectNoGuidelines - ? WarningLevel.green.color - : relevantGuideline.annotations.warningLevel.color, - ); expect(find.byType(Disclaimer), findsOneWidget); final context = tester.element(find.byType(Scaffold).first); if (expectNoGuidelines) { + expect(card.color, WarningLevel.green.color); expect( find.byTooltip(context.l10n.drugs_page_tooltip_guideline_missing), findsOneWidget, @@ -181,6 +182,8 @@ Future _expectDrugContent( findsOneWidget, ); } else { + final relevantGuideline = guideline ?? drug.guidelines.first; + expect(card.color, relevantGuideline.annotations.warningLevel.color); expect( find.byTooltip(context.l10n.drugs_page_tooltip_guideline_present( relevantGuideline.externalData.first.source, @@ -196,10 +199,17 @@ Future _expectDrugContent( findsOneWidget, ); for (final genotypeKey in drug.guidelineGenotypes) { - expect( - find.text(genotypeKey), - findsOneWidget, - ); + if (genotypeKey.contains(' ')) { + expect( + find.textContaining(genotypeKey.split(' ').first), + findsOneWidget, + ); + } else { + expect( + find.text(genotypeKey), + findsOneWidget, + ); + } } } } diff --git a/app/integration_test/fixtures/drugs/with_multiple_any_not_handled_fallback_guidelines.dart b/app/integration_test/fixtures/drugs/with_multiple_any_not_handled_fallback_guidelines.dart index f82c79e1..190d207e 100644 --- a/app/integration_test/fixtures/drugs/with_multiple_any_not_handled_fallback_guidelines.dart +++ b/app/integration_test/fixtures/drugs/with_multiple_any_not_handled_fallback_guidelines.dart @@ -13,6 +13,32 @@ final drugWithMultipleAnyNotHandledFallbackGuidelines = Drug( brandNames: ['Votrient'], ), guidelines: [ + Guideline( + id: '6686a865826414ec5b05c436', + version: 1, + lookupkey: { + 'HLA-B': ['*57:01 positive'], + 'UGT1A1': ['Poor Metabolizer'], + }, + externalData: [ + GuidelineExtData( + source: 'FDA', + guidelineName: 'Table of Pharmacogenetic Associations (Section 2)', + guidelineUrl: 'https://www.fda.gov/medical-devices/precision-medicine/table-pharmacogenetic-associations#section2', + implications: { + 'HLA-B': 'May result in higher adverse reaction risk (liver enzyme elevations). Monitor liver function tests regardless of genotype.', + 'UGT1A1': 'Results in higher adverse reaction risk (hyperbilirubinemia).', + }, + recommendation: 'Might be included in implication text (imported from FDA, source only states one text per guideline)', + comments: null, + ), + ], + annotations: GuidelineAnnotations( + implication: 'You have an increased risk for side effects. (Test case 1)', + recommendation: 'You can still use pazopanib at standard dose. Consult your pharmacist or doctor for more information. (Test case 1)', + warningLevel: WarningLevel.yellow, + ), + ), Guideline( id: '66b50b2433cbe5c07ee31651', version: 1, @@ -34,10 +60,62 @@ final drugWithMultipleAnyNotHandledFallbackGuidelines = Drug( ), ], annotations: GuidelineAnnotations( - implication: 'You have an increased risk for side effects.', - recommendation: 'You can still use pazopanib at standard dose. Consult your pharmacist or doctor for more information.', + implication: 'You have an increased risk for side effects. (Test case 2)', + recommendation: 'You can still use pazopanib at standard dose. Consult your pharmacist or doctor for more information. (Test case 2)', warningLevel: WarningLevel.yellow, ), ), + Guideline( + id: '66b50b2433cbe5c07ee31657', + version: 1, + lookupkey: { + 'HLA-B': ['*57:01 positive'], + 'UGT1A1': ['~'], + }, + externalData: [ + GuidelineExtData( + source: 'FDA', + guidelineName: 'Table of Pharmacogenetic Associations (Section 2)', + guidelineUrl: 'https://www.fda.gov/medical-devices/precision-medicine/table-pharmacogenetic-associations#section2', + implications: { + 'HLA-B': 'May result in higher adverse reaction risk (liver enzyme elevations). Monitor liver function tests regardless of genotype.', + 'UGT1A1': 'Standard procedure', + }, + recommendation: 'Might be included in implication text (imported from FDA, source only states one text per guideline)', + comments: null, + ), + ], + annotations: GuidelineAnnotations( + implication: 'You may have an increased risk for side effects. (Test case 3)', + recommendation: 'You can still use pazopanib at standard dose. Consult your pharmacist or doctor for more information. (Test case 3)', + warningLevel: WarningLevel.yellow, + ), + ), + Guideline( + id: '66b50b2433cbe5c07ee3165d', + version: 1, + lookupkey: { + 'HLA-B': ['~'], + 'UGT1A1': ['~'], + }, + externalData: [ + GuidelineExtData( + source: 'FDA', + guidelineName: 'Table of Pharmacogenetic Associations (Section 2)', + guidelineUrl: 'https://www.fda.gov/medical-devices/precision-medicine/table-pharmacogenetic-associations#section2', + implications: { + 'HLA-B': 'Standard procedure', + 'UGT1A1': 'Standard procedure', + }, + recommendation: 'Might be included in implication text (imported from FDA, source only states one text per guideline)', + comments: null, + ), + ], + annotations: GuidelineAnnotations( + implication: 'Your phenotype does not have a clinically significant influence on pazopanib. (Test case 4)', + recommendation: 'You can use pazopanib at standard dose. Consult your pharmacist or doctor for more information. (Test case 4)', + warningLevel: WarningLevel.green, + ), + ), ], ); \ No newline at end of file diff --git a/app/integration_test/fixtures/set_user_data.dart b/app/integration_test/fixtures/set_user_data.dart index a2bd5799..49e295a2 100644 --- a/app/integration_test/fixtures/set_user_data.dart +++ b/app/integration_test/fixtures/set_user_data.dart @@ -7,8 +7,8 @@ class _UserDataConfig { }); final String gene; final String lookupkey; - final String phenotype = 'phenotype does not matter for test'; - final String variant = 'variant does not matter for test'; + String get phenotype => lookupkey; + String get variant => lookupkey; final String allelesTested = 'allelesTested does not matter for test'; } @@ -32,11 +32,11 @@ void setUserDataForGuideline(Guideline guideline) { lookupkey: lookupkey, ); // Need to be careful with non-unique genes here; e.g., is we want to use - // multiple HLA-B variants in the tests, we will need to check for the - // genotype key (which is in the current setup not possible without the - // variant) + // multiple HLA-B variants in the tests or overwrite a specific HLA-B + // variant, we will need to check for the genotype key (which is in the + // current setup not possible without the proper variant) UserData.instance.labData = UserData.instance.labData!.filter( - (labResult) => labResult.gene != gene + (labResult) => labResult.gene != gene ).toList(); UserData.instance.labData!.add( LabResult( @@ -46,7 +46,9 @@ void setUserDataForGuideline(Guideline guideline) { allelesTested: userDataConfig.allelesTested, ), ); - UserData.instance.genotypeResults![userDataConfig.gene] = GenotypeResult( + UserData.instance.genotypeResults![ + GenotypeKey(userDataConfig.gene, userDataConfig.variant).value + ] = GenotypeResult( gene: userDataConfig.gene, phenotype: userDataConfig.phenotype, variant: userDataConfig.variant, diff --git a/app/lib/common/models/drug/drug.dart b/app/lib/common/models/drug/drug.dart index 58928a34..34f33583 100644 --- a/app/lib/common/models/drug/drug.dart +++ b/app/lib/common/models/drug/drug.dart @@ -1,5 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hive/hive.dart'; +import 'package:trotter/trotter.dart'; import '../../module.dart'; @@ -78,6 +79,54 @@ extension DrugExtension on Drug { : namesMatch; } + bool _lookupsMatchUserData(String gene, List variants) => + variants.any((variant) => variants.contains( + UserData.lookupFor( + GenotypeKey(gene, variant).value, + drug: name, + ), + )); + + Guideline? _getExactGuideline() { + final exactGuidelines = guidelines.filter( + (guideline) => guideline.lookupkey.none( + (gene, variants) => variants.contains('~') + ) + ); + return exactGuidelines.firstOrNullWhere( + (guideline) => guideline.lookupkey.all(_lookupsMatchUserData) + ); + } + + Guideline? _getPartiallyHandledGuideline() { + if (guidelines.isEmpty) return null; + final partialGuidelines = guidelines.filter( + (guideline) => guideline.lookupkey.values.any( + (values) => values.contains('~'), + ), + ); + if (partialGuidelines.isEmpty) return null; + final guidelineGenes = guidelines.first.lookupkey.keys.toList(); + Guideline? partiallyHandledGuideline; + var currentMatchingNumber = guidelineGenes.length - 1; + while (currentMatchingNumber > 0 && partiallyHandledGuideline == null) { + final currentGeneCombinations = + Combinations(currentMatchingNumber, guidelineGenes)().toList(); + for (final geneCombination in currentGeneCombinations) { + if (partiallyHandledGuideline != null) break; + partiallyHandledGuideline = partialGuidelines.firstOrNullWhere( + (guideline) => guideline.lookupkey.all( + (gene, variants) => geneCombination.contains(gene) + ? _lookupsMatchUserData(gene, variants) + : variants.any((variant) => variant == '~'), + ), + ); + } + currentMatchingNumber--; + } + return partiallyHandledGuideline; + } + Guideline? get userGuideline { final anyFallbackGuideline = guidelines.firstOrNullWhere( (guideline) => guideline.lookupkey.all( @@ -85,17 +134,10 @@ extension DrugExtension on Drug { ), ); if (anyFallbackGuideline != null) return anyFallbackGuideline; - final exactGuideline = guidelines.firstOrNullWhere( - (guideline) => guideline.lookupkey.all( - (gene, variants) => variants.any((variant) => - variants.contains(UserData.lookupFor( - GenotypeKey(gene, variant).value, - drug: name, - ) - )), - ), - ); + final exactGuideline = _getExactGuideline(); if (exactGuideline != null) return exactGuideline; + final partiallyHandledGuideline = _getPartiallyHandledGuideline(); + if (partiallyHandledGuideline != null) return partiallyHandledGuideline; return guidelines.firstOrNullWhere( (guideline) => guideline.lookupkey.all( (gene, variants) => variants.any((variant) => variant == '~') diff --git a/app/pubspec.lock b/app/pubspec.lock index efc3f3b0..78b3f23f 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -1246,6 +1246,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + trotter: + dependency: "direct main" + description: + name: trotter + sha256: "59c8d5e98904355e887d80aae52b02828a811fe96474645aaf8fa7761ea64851" + url: "https://pub.dev" + source: hosted + version: "2.2.0" typed_data: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index a0600f9c..e7b17127 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: printing: ^5.9.3 provider: ^6.1.1 shared_preferences: ^2.0.15 + trotter: ^2.2.0 url_launcher: ^6.1.4 dev_dependencies: