// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'dart:convert'; import 'package:clock/clock.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:test/test.dart'; import 'package:unified_analytics/src/constants.dart'; import 'package:unified_analytics/src/enums.dart'; import 'package:unified_analytics/src/survey_handler.dart'; import 'package:unified_analytics/src/utils.dart'; import 'package:unified_analytics/unified_analytics.dart'; void main() { final testEvent = Event.hotReloadTime(timeMs: 10); group('Unit testing function sampleRate:', () { // Set a string that can be used in place of a survey's unique ID final iterations = 1000; final uuid = Uuid(123); final uniqueSurveyId = uuid.generateV4(); // Set how much the actual sampled rate can be (allowing 5% of variability) final marginOfError = 0.05; test('Unit testing the sampleRate method', () { // These strings had a predetermined output from the utility function final string1 = 'string1'; final string2 = 'string2'; expect(sampleRate(string1, string2), 0.40); }); test('Simulating with various sample rates', () { final sampleRateToTestList = [0.10, 0.25, 0.50, 0.75, 0.80, 0.95]; for (final sampleRateToTest in sampleRateToTestList) { var count = 0; for (var i = 0; i < iterations; i++) { // Regenerate the client id to simulate a unique user final generatedClientId = uuid.generateV4(); if (sampleRate(uniqueSurveyId, generatedClientId) <= sampleRateToTest) { count += 1; } } final actualSampledRate = count / iterations; final actualMarginOfError = (sampleRateToTest - actualSampledRate) .abs(); expect( actualMarginOfError < marginOfError, true, reason: 'Failed on sample rate = $sampleRateToTest with' ' actual rate $actualMarginOfError ' 'and a margin of error = $marginOfError', ); } }); }); group('Unit testing function checkSurveyDate:', () { final date = DateTime(2023, 5, 1); // Two surveys created, one that is within the survey date // range, and one that is not final validSurvey = Survey( uniqueId: 'uniqueId', startDate: DateTime(2023, 1, 1), endDate: DateTime(2023, 12, 31), description: 'description', snoozeForMinutes: 10, samplingRate: 1.0, excludeDashToolList: [], conditionList: [], buttonList: [], ); final invalidSurvey = Survey( uniqueId: 'uniqueId', startDate: DateTime(2022, 1, 1), endDate: DateTime(2022, 12, 31), description: 'description', snoozeForMinutes: 10, samplingRate: 1.0, excludeDashToolList: [], conditionList: [], buttonList: [], ); test('expired survey', () { final clock = Clock.fixed(date); withClock(clock, () { expect(SurveyHandler.checkSurveyDate(invalidSurvey), false); }); }); test('valid survey', () { final clock = Clock.fixed(date); withClock(clock, () { expect(SurveyHandler.checkSurveyDate(validSurvey), true); }); }); }); group('Unit testing function parseSurveysFromJson', () { final validContents = ''' [ { "uniqueId": "xxxxx", "startDate": "2023-06-01T09:00:00-07:00", "endDate": "2023-06-30T09:00:00-07:00", "description": "xxxxxxx", "snoozeForMinutes": "10", "samplingRate": "1.0", "excludeDashTools": [], "conditions": [ { "field": "logFileStats.recordCount", "operator": ">=", "value": 1000 }, { "field": "logFileStats.toolCount.flutter-tool", "operator": "<", "value": 3 } ], "buttons": [ { "buttonText": "Take Survey", "action": "accept", "url": "https://google.qualtrics.com/jfe/form/SV_5gsB2EuG324y2", "promptRemainsVisible": false } ] } ] '''; // The value for the condition is not a valid integer final invalidConditionValueContents = ''' [ { "uniqueId": "xxxxx", "startDate": "2023-06-01T09:00:00-07:00", "endDate": "2023-06-30T09:00:00-07:00", "description": "xxxxxxx", "snoozeForMinutes": "10", "samplingRate": "1.0", "excludeDashTools": [], "conditions": [ { "field": "logFileStats.recordCount", "operator": ">=", "value": "1000xxxx" } ], "buttons": [ { "buttonText": "Take Survey", "action": "accept", "url": "https://google.qualtrics.com/jfe/form/SV_5gsB2EuG324y2", "promptRemainsVisible": false } ] } ] '''; // Using a dash tool in the excludeDashTools array that is not a valid // DashTool label final invalidDashToolContents = ''' [ { "uniqueId": "xxxxx", "startDate": "2023-06-01T09:00:00-07:00", "endDate": "2023-06-30T09:00:00-07:00", "description": "xxxxxxx", "snoozeForMinutes": "10", "samplingRate": "1.0", "excludeDashTools": [ "not-a-valid-dash-tool" ], "conditions": [ { "field": "logFileStats.recordCount", "operator": ">=", "value": 1000 }, { "field": "logFileStats.toolCount.flutter-tool", "operator": "<", "value": 3 } ], "buttons": [ { "buttonText": "Take Survey", "action": "accept", "url": "https://google.qualtrics.com/jfe/form/SV_5gsB2EuG324y2", "promptRemainsVisible": false } ] } ] '''; test('valid json', () { withClock(Clock.fixed(DateTime(2023, 6, 15)), () { final parsedSurveys = SurveyHandler.parseSurveysFromJson( jsonDecode(validContents) as List, ); expect(parsedSurveys.length, 1); expect(parsedSurveys.first.conditionList.length, 2); final firstCondition = parsedSurveys.first.conditionList.first; final secondCondition = parsedSurveys.first.conditionList[1]; expect(firstCondition.field, 'logFileStats.recordCount'); expect(firstCondition.operatorString, '>='); expect(firstCondition.value, 1000); expect(secondCondition.field, 'logFileStats.toolCount.flutter-tool'); expect(secondCondition.operatorString, '<'); expect(secondCondition.value, 3); expect(parsedSurveys.first.buttonList.length, 1); expect( parsedSurveys.first.buttonList.first.promptRemainsVisible, false, ); }); }); test('invalid condition json', () { withClock(Clock.fixed(DateTime(2023, 6, 15)), () { final parsedSurveys = SurveyHandler.parseSurveysFromJson( jsonDecode(invalidConditionValueContents) as List, ); expect( parsedSurveys.length, 0, reason: 'The condition value is not a ' 'proper integer so it should error returning no surveys', ); }); }); test('invalid dash tool json', () { withClock(Clock.fixed(DateTime(2023, 6, 15)), () { final parsedSurveys = SurveyHandler.parseSurveysFromJson( jsonDecode(invalidDashToolContents) as List, ); expect( parsedSurveys.length, 0, reason: 'The dash tool in the exclude array is not valid ' 'so it should error returning no surveys', ); }); }); }); group('Testing with FakeSurveyHandler', () { late Analytics analytics; late Directory homeDirectory; late MemoryFileSystem fs; late File clientIdFile; late File dismissedSurveyFile; setUp(() { fs = MemoryFileSystem.test(style: FileSystemStyle.posix); homeDirectory = fs.directory('home'); // Write the client ID file out so that we don't get // a randomly assigned id for this test generated within // the analytics constructor clientIdFile = homeDirectory .childDirectory(kDartToolDirectoryName) .childFile(kClientIdFileName); clientIdFile.createSync(recursive: true); clientIdFile.writeAsStringSync('string1'); // Assign the json file that will hold the persisted surveys dismissedSurveyFile = homeDirectory .childDirectory(kDartToolDirectoryName) .childFile(kDismissedSurveyFileName); // Setup two tools to be onboarded with this package so // that we can simulate two different tools interacting with // surveys // // This is especially useful when testing the "excludeDashTools" array // to prevent certain tools from getting a survey from this package final initialAnalyticsFlutter = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, ); final initialAnalyticsDart = Analytics.fake( tool: DashTool.dartTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, ); initialAnalyticsFlutter.clientShowedMessage(); initialAnalyticsDart.clientShowedMessage(); }); test('returns valid survey', () async { await withClock(Clock.fixed(DateTime(2023, 3, 3)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', startDate: DateTime(2023, 1, 1), endDate: DateTime(2023, 12, 31), description: 'description', snoozeForMinutes: 10, samplingRate: 1.0, excludeDashToolList: [], conditionList: [ Condition('logFileStats.recordCount', '>=', 50), Condition('logFileStats.toolCount.flutter-tool', '>', 0), ], buttonList: [ SurveyButton( buttonText: 'buttonText', action: 'accept', url: 'http://example.com', promptRemainsVisible: false, ), ], ), ], ), ); // Simulate 60 events to send so that the first condition is satisified for (var i = 0; i < 60; i++) { analytics.send(testEvent); } final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 1); final survey = fetchedSurveys.first; expect(survey.conditionList.length, 2); expect(survey.buttonList.length, 1); }); }); test('does not return expired survey', () async { await withClock(Clock.fixed(DateTime(2023, 3, 3)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', startDate: DateTime(2022, 1, 1), endDate: DateTime(2022, 12, 31), description: 'description', snoozeForMinutes: 10, samplingRate: 1.0, excludeDashToolList: [], conditionList: [ Condition('logFileStats.recordCount', '>=', 50), Condition('logFileStats.toolCount.flutter-tool', '>', 0), ], buttonList: [], ), ], ), ); // Simulate 60 events to send so that the first condition is satisified for (var i = 0; i < 60; i++) { analytics.send(testEvent); } final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 0); }); }); test('does not return survey if opted out of telemetry', () async { await withClock(Clock.fixed(DateTime(2023, 3, 3)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', startDate: DateTime(2023, 1, 1), endDate: DateTime(2023, 12, 31), description: 'description', snoozeForMinutes: 10, samplingRate: 1.0, excludeDashToolList: [], conditionList: [ Condition('logFileStats.recordCount', '>=', 50), Condition('logFileStats.toolCount.flutter-tool', '>', 0), ], buttonList: [], ), ], ), ); // Simulate 60 events to send so that the first condition is satisified for (var i = 0; i < 60; i++) { analytics.send(testEvent); } await analytics.setTelemetry(false); expect(analytics.okToSend, false); final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 0); }); }); test('returns valid survey from json', () async { await withClock(Clock.fixed(DateTime(2023, 3, 3)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromString( dismissedSurveyFile: dismissedSurveyFile, content: ''' [ { "uniqueId": "uniqueId123", "startDate": "2023-01-01T09:00:00-07:00", "endDate": "2023-12-31T09:00:00-07:00", "description": "description123", "snoozeForMinutes": "10", "samplingRate": "1.0", "excludeDashTools": [], "conditions": [ { "field": "logFileStats.recordCount", "operator": ">=", "value": 50 } ], "buttons": [ { "buttonText": "Take Survey", "action": "accept", "url": "https://google.qualtrics.com/jfe/form/SV_5gsB2EuG324y2", "promptRemainsVisible": false }, { "buttonText": "Dismiss", "action": "dismiss", "url": null, "promptRemainsVisible": false }, { "buttonText": "More Info", "action": "snooze", "url": "https://docs.flutter.dev/reference/crash-reporting", "promptRemainsVisible": true } ] } ] ''', ), ); // Simulate 60 events to send so that the first condition is satisified for (var i = 0; i < 60; i++) { analytics.send(testEvent); } final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 1); final survey = fetchedSurveys.first; expect(survey.uniqueId, 'uniqueId123'); expect(survey.startDate.year, 2023); expect(survey.startDate.month, 1); expect(survey.startDate.day, 1); expect(survey.endDate.year, 2023); expect(survey.endDate.month, 12); expect(survey.endDate.day, 31); expect(survey.description, 'description123'); expect(survey.snoozeForMinutes, 10); expect(survey.samplingRate, 1.0); expect(survey.conditionList.length, 1); final condition = survey.conditionList.first; expect(condition.field, 'logFileStats.recordCount'); expect(condition.operatorString, '>='); expect(condition.value, 50); final buttonList = survey.buttonList; expect(buttonList.length, 3); expect(buttonList.first.buttonText, 'Take Survey'); expect(buttonList.first.action, 'accept'); expect( buttonList.first.url, 'https://google.qualtrics.com/jfe/form/SV_5gsB2EuG324y2', ); expect(buttonList.first.promptRemainsVisible, false); expect(buttonList.elementAt(1).buttonText, 'Dismiss'); expect(buttonList.elementAt(1).action, 'dismiss'); expect(buttonList.elementAt(1).url, isNull); expect(buttonList.elementAt(1).promptRemainsVisible, false); expect(buttonList.last.buttonText, 'More Info'); expect(buttonList.last.action, 'snooze'); expect( buttonList.last.url, 'https://docs.flutter.dev/reference/crash-reporting', ); expect(buttonList.last.promptRemainsVisible, true); }); }); test('no survey returned from malformed json', () async { // The date is not valid for the start date await withClock(Clock.fixed(DateTime(2023, 3, 3)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromString( dismissedSurveyFile: dismissedSurveyFile, content: ''' [ { "uniqueId": "uniqueId123", "startDate": "NOT A REAL DATE", "endDate": "2023-07-30T09:00:00-07:00", "description": "Help improve Flutter's release builds with this 3-question survey!", "snoozeForMinutes": "7200", "samplingRate": "0.1", "excludeDashTools": [], "conditions": [ { "field": "logFileStats.recordCount", "operator": ">=", "value": 50 } ], "buttons": [ { "buttonText": "Take Survey", "action": "accept", "url": "https://google.qualtrics.com/jfe/form/SV_5gsB2EuG5Et5Yy2", "promptRemainsVisible": false }, { "buttonText": "Dismiss", "action": "dismiss", "url": null, "promptRemainsVisible": false }, { "buttonText": "More Info", "action": "snooze", "url": "https://docs.flutter.dev/reference/crash-reporting", "promptRemainsVisible": false } ] } ] ''', ), ); // Simulate 60 events to send so that the first condition is satisified for (var i = 0; i < 60; i++) { analytics.send(testEvent); } final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 0); }); }); test('returns two valid survey from json', () async { await withClock(Clock.fixed(DateTime(2023, 3, 3)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromString( dismissedSurveyFile: dismissedSurveyFile, content: ''' [ { "uniqueId": "12345", "startDate": "2023-01-01T09:00:00-07:00", "endDate": "2023-12-31T09:00:00-07:00", "description": "xxxxxxx", "snoozeForMinutes": "10", "samplingRate": "1.0", "excludeDashTools": [], "conditions": [ { "field": "logFileStats.recordCount", "operator": ">=", "value": 50 } ], "buttons": [] }, { "uniqueId": "67890", "startDate": "2023-01-01T09:00:00-07:00", "endDate": "2023-12-31T09:00:00-07:00", "description": "xxxxxxx", "snoozeForMinutes": "10", "samplingRate": "1.0", "excludeDashTools": [], "conditions": [ { "field": "logFileStats.recordCount", "operator": ">=", "value": 50 } ], "buttons": [ { "buttonText": "More Info", "action": "snooze", "url": "https://docs.flutter.dev/reference/crash-reporting", "promptRemainsVisible": true } ] } ] ''', ), ); // Simulate 60 events to send so that the first condition is satisified for (var i = 0; i < 60; i++) { analytics.send(testEvent); } final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 2); final firstSurvey = fetchedSurveys.first; final secondSurvey = fetchedSurveys.last; expect(firstSurvey.uniqueId, '12345'); expect(secondSurvey.uniqueId, '67890'); final secondSurveyButtons = secondSurvey.buttonList; expect(secondSurveyButtons.length, 1); expect(secondSurveyButtons.first.buttonText, 'More Info'); expect(secondSurveyButtons.first.action, 'snooze'); expect( secondSurveyButtons.first.url, 'https://docs.flutter.dev/reference/crash-reporting', ); expect(secondSurveyButtons.first.promptRemainsVisible, true); }); }); test('valid survey not returned if opted out', () async { await withClock(Clock.fixed(DateTime(2023, 3, 3)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', startDate: DateTime(2023, 1, 1), endDate: DateTime(2023, 12, 31), description: 'description', snoozeForMinutes: 10, samplingRate: 1.0, excludeDashToolList: [], conditionList: [ Condition('logFileStats.recordCount', '>=', 50), Condition('logFileStats.toolCount.flutter-tool', '>', 0), ], buttonList: [], ), ], ), ); // Simulate 60 events to send so that the first condition is satisified for (var i = 0; i < 60; i++) { analytics.send(testEvent); } // Setting to false will prevent anything from getting returned await analytics.setTelemetry(false); var fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 0); // Setting telemetry back to true should enable the surveys to get // returned again; we will also need to send the fake events again // because on opt out, the log file will get cleared and one of // the conditions for the fake survey loaded is that we need // at least 50 records for one of the conditions await analytics.setTelemetry(true); for (var i = 0; i < 60; i++) { analytics.send(testEvent); } fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 1); }); }); test('Sampling rate correctly returns a valid survey', () async { // This test will use a predefined client ID string of `string1` // which has been set in the setup along with a predefined // string for the survey ID of `string2` to get a sample rate value // // The combination of `string1` and `string2` will return 0.40 // from the sampleRate utility function so we have set the threshold // to be 0.6 which should return surveys await withClock(Clock.fixed(DateTime(2023, 3, 3)), () async { final survey = Survey( uniqueId: 'string2', startDate: DateTime(2023, 1, 1), endDate: DateTime(2023, 12, 31), description: 'description', snoozeForMinutes: 10, samplingRate: 0.6, excludeDashToolList: [], conditionList: [ Condition('logFileStats.recordCount', '>=', 50), Condition('logFileStats.toolCount.flutter-tool', '>', 0), ], buttonList: [], ); analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [survey], ), ); // Simulate 60 events to send so that the first condition is satisified for (var i = 0; i < 60; i++) { analytics.send(testEvent); } final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(survey.samplingRate, 0.6); expect(fetchedSurveys.length, 1); }); }); test('Sampling rate filters out a survey', () async { // We will reduce the survey's sampling rate to be 0.3 which is // less than value returned from the predefined client ID and // survey sample await withClock(Clock.fixed(DateTime(2023, 3, 3)), () async { final survey = Survey( uniqueId: 'string2', startDate: DateTime(2023, 1, 1), endDate: DateTime(2023, 12, 31), description: 'description', snoozeForMinutes: 10, samplingRate: 0.15, excludeDashToolList: [], conditionList: [ Condition('logFileStats.recordCount', '>=', 50), Condition('logFileStats.toolCount.flutter-tool', '>', 0), ], buttonList: [], ); analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [survey], ), ); // Simulate 60 events to send so that the first condition is satisified for (var i = 0; i < 60; i++) { analytics.send(testEvent); } final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(survey.samplingRate, 0.15); expect(fetchedSurveys.length, 0); }); }); test( 'Snoozing survey is successful with snooze timeout from survey', () async { expect( dismissedSurveyFile.readAsStringSync(), '{}', reason: 'Should be an empty object', ); // Initialize the survey class that we will use for this test final minutesToSnooze = 30; final surveyToLoad = Survey( uniqueId: 'uniqueId', startDate: DateTime(2023, 1, 1), endDate: DateTime(2023, 12, 31), description: 'description', snoozeForMinutes: minutesToSnooze, // Initialized survey with `minutesToSnooze` samplingRate: 1.0, excludeDashToolList: [], conditionList: [], buttonList: [], ); await withClock(Clock.fixed(DateTime(2023, 3, 3, 12, 0)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 1); final survey = fetchedSurveys.first; expect(survey.snoozeForMinutes, minutesToSnooze); // We will snooze the survey now and it should not show up // if we fetch surveys again before the minutes to snooze time // has finished analytics.surveyShown(survey); }); // This analytics instance will be simulated to be shortly after the // first snooze, but before the snooze period has elapsed await withClock(Clock.fixed(DateTime(2023, 3, 3, 12, 15)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect( fetchedSurveys.length, 0, reason: 'The snooze period has not elapsed yet', ); }); // This analytics instance will be simulated to be after the snooze // period await withClock(Clock.fixed(DateTime(2023, 3, 3, 12, 35)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect( fetchedSurveys.length, 1, reason: 'The snooze period has elapsed', ); }); }, ); test('Dimissing permanently is successful', () async { final minutesToSnooze = 10; final surveyToLoad = Survey( uniqueId: 'uniqueId', startDate: DateTime(2023, 1, 1), endDate: DateTime(2023, 12, 31), description: 'description', snoozeForMinutes: minutesToSnooze, samplingRate: 1.0, excludeDashToolList: [], conditionList: [], buttonList: [ SurveyButton( buttonText: 'buttonText', action: 'accept', promptRemainsVisible: false, ), SurveyButton( buttonText: 'buttonText', action: 'dismiss', promptRemainsVisible: false, ), ], ); await withClock(Clock.fixed(DateTime(2023, 3, 3, 12, 0)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 1); // Dismissing permanently will ensure that this survey is not // shown again final survey = fetchedSurveys.first; analytics.surveyInteracted( survey: survey, surveyButton: survey.buttonList.first, ); }); // Moving out a week await withClock(Clock.fixed(DateTime(2023, 3, 10, 12, 0)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 0); }); }); test('malformed persisted json file for surveys', () async { // When the survey handler encounters an error when parsing the // persisted json file, it will reset it using the static method // under the [Initializer] class and reset it to be an empty json object final minutesToSnooze = 10; final surveyToLoad = Survey( uniqueId: 'uniqueId', startDate: DateTime(2023, 1, 1), endDate: DateTime(2023, 12, 31), description: 'description', snoozeForMinutes: minutesToSnooze, samplingRate: 1.0, excludeDashToolList: [], conditionList: [], buttonList: [ SurveyButton( buttonText: 'buttonText', action: 'accept', promptRemainsVisible: false, ), SurveyButton( buttonText: 'buttonText', action: 'dismiss', promptRemainsVisible: false, ), ], ); await withClock(Clock.fixed(DateTime(2023, 3, 3, 12, 0)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 1); // Dismissing permanently will ensure that this survey is not // shown again final survey = fetchedSurveys.first; expect(survey.buttonList.length, 2); analytics.surveyInteracted( survey: survey, surveyButton: survey.buttonList.first, ); }); // Purposefully write invalid json into the persisted file dismissedSurveyFile.writeAsStringSync('{'); // Moving out a week await withClock(Clock.fixed(DateTime(2023, 3, 10, 12, 0)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 1); }); }); test('persisted json file goes missing handled', () async { // If the persisted json file with the dismissed surveys is missing // there should be error handling to recreate the file again with // an empty json object final minutesToSnooze = 10; final surveyToLoad = Survey( uniqueId: 'uniqueId', startDate: DateTime(2023, 1, 1), endDate: DateTime(2023, 12, 31), description: 'description', snoozeForMinutes: minutesToSnooze, samplingRate: 1.0, excludeDashToolList: [], conditionList: [], buttonList: [ SurveyButton( buttonText: 'buttonText', action: 'accept', promptRemainsVisible: false, ), SurveyButton( buttonText: 'buttonText', action: 'dismiss', promptRemainsVisible: false, ), ], ); await withClock(Clock.fixed(DateTime(2023, 3, 3, 12, 0)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 1); // Dismissing permanently will ensure that this survey is not // shown again final survey = fetchedSurveys.first; expect(survey.buttonList.length, 2); analytics.surveyInteracted( survey: survey, surveyButton: survey.buttonList.first, ); }); // Moving out a week await withClock(Clock.fixed(DateTime(2023, 3, 10, 12, 0)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); // Purposefully delete the file dismissedSurveyFile.deleteSync(); final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 1); }); }); test('Filtering out with excludeDashTool array', () async { await withClock(Clock.fixed(DateTime(2023, 3, 3)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', startDate: DateTime(2023, 1, 1), endDate: DateTime(2023, 12, 31), description: 'description', snoozeForMinutes: 10, samplingRate: 1.0, // This should be the same as the tool in the // Analytics constructor above excludeDashToolList: [DashTool.flutterTool], conditionList: [], buttonList: [ SurveyButton( buttonText: 'buttonText', action: 'accept', url: 'http://example.com', promptRemainsVisible: false, ), ], ), ], ), ); final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 0); }); }); test('Filter from excludeDashTool array does not ' 'apply for different tool', () async { await withClock(Clock.fixed(DateTime(2023, 3, 3)), () async { analytics = Analytics.fake( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', startDate: DateTime(2023, 1, 1), endDate: DateTime(2023, 12, 31), description: 'description', snoozeForMinutes: 10, samplingRate: 1.0, // This should be different from the tool in the // Analytics constructor above excludeDashToolList: [DashTool.devtools], conditionList: [], buttonList: [ SurveyButton( buttonText: 'buttonText', action: 'accept', url: 'http://example.com', promptRemainsVisible: false, ), ], ), ], ), ); final fetchedSurveys = await analytics.fetchAvailableSurveys(); expect(fetchedSurveys.length, 1); final survey = fetchedSurveys.first; expect(survey.excludeDashToolList.length, 1); expect(survey.excludeDashToolList.contains(DashTool.devtools), true); }); }); }); }