Skip to content

Commit

Permalink
feat(app): add complex any not handled guideline matching
Browse files Browse the repository at this point in the history
  • Loading branch information
tamslo committed Sep 20, 2024
1 parent 46a39f2 commit fc54d98
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 45 deletions.
62 changes: 36 additions & 26 deletions app/integration_test/drugs_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -77,26 +79,31 @@ void main() {
);
});

testWidgets(
'test drug content with any not handled fallback guidelines',
(tester) async {
Future<void> testPerGuideline(Drug drug) async {
for (
final guideline in drug.guidelines
) {
final anyNotHandledTestCases = <String, Drug>{
'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,
mockDrugsCubit,
drug: drug,
guideline: guideline,
);
}
}
await testPerGuideline(drugWithAnyNotHandledFallbackGuideline);
// await testPerGuideline(drugWithMultipleAnyNotHandledFallbackGuidelines);
},
);
},
);
}
}
});
}

Expand All @@ -111,7 +118,6 @@ Future<void> _expectDrugContent(
}) async {
when(() => mockDrugsCubit.state)
.thenReturn(isLoading ? DrugState.loading() : DrugState.loaded());
final relevantGuideline = guideline ?? drug.guidelines.first;
await tester.pumpWidget(
ChangeNotifierProvider(
create: (context) => ActiveDrugs(),
Expand Down Expand Up @@ -159,15 +165,10 @@ Future<void> _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,
Expand All @@ -181,6 +182,8 @@ Future<void> _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,
Expand All @@ -196,10 +199,17 @@ Future<void> _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,
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
),
),
],
);
16 changes: 9 additions & 7 deletions app/integration_test/fixtures/set_user_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}

Expand All @@ -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(
Expand All @@ -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,
Expand Down
62 changes: 52 additions & 10 deletions app/lib/common/models/drug/drug.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive/hive.dart';
import 'package:trotter/trotter.dart';

import '../../module.dart';

Expand Down Expand Up @@ -78,24 +79,65 @@ extension DrugExtension on Drug {
: namesMatch;
}

bool _lookupsMatchUserData(String gene, List<String> 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(
(gene, variants) => variants.any((variant) => variant == '*')
),
);
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 == '~')
Expand Down
8 changes: 8 additions & 0 deletions app/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions app/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit fc54d98

Please sign in to comment.