// 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:io' as io; 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/utils.dart'; import 'package:unified_analytics/unified_analytics.dart'; void main() { late MemoryFileSystem fs; late Directory home; late Directory dartToolDirectory; late File clientIdFile; late File sessionFile; late File configFile; late File logFile; late File dismissedSurveyFile; const homeDirName = 'home'; const initialTool = DashTool.flutterTool; const secondTool = DashTool.dartTool; const toolsMessageVersion = 1; const toolsMessage = 'toolsMessage'; const flutterChannel = 'flutterChannel'; const flutterVersion = 'flutterVersion'; const dartVersion = 'dartVersion'; const platform = DevicePlatform.macos; final testEvent = Event.hotReloadTime(timeMs: 50); setUp(() { // Setup the filesystem with the home directory final fsStyle = io.Platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix; fs = MemoryFileSystem.test(style: fsStyle); home = fs.directory(homeDirName); dartToolDirectory = home.childDirectory(kDartToolDirectoryName); // The 3 files that should have been generated clientIdFile = home .childDirectory(kDartToolDirectoryName) .childFile(kClientIdFileName); sessionFile = home .childDirectory(kDartToolDirectoryName) .childFile(kSessionFileName); configFile = home .childDirectory(kDartToolDirectoryName) .childFile(kConfigFileName); logFile = home .childDirectory(kDartToolDirectoryName) .childFile(kLogFileName); dismissedSurveyFile = home .childDirectory(kDartToolDirectoryName) .childFile(kDismissedSurveyFileName); }); test('Confirm workflow for first run', () { final firstAnalytics = Analytics.fake( tool: initialTool, homeDirectory: home, flutterChannel: flutterChannel, toolsMessageVersion: toolsMessageVersion, toolsMessage: toolsMessage, flutterVersion: flutterVersion, dartVersion: dartVersion, fs: fs, platform: platform, ); expect(firstAnalytics.shouldShowMessage, true); expect(firstAnalytics.okToSend, false); firstAnalytics.clientShowedMessage(); expect(firstAnalytics.shouldShowMessage, false); expect( firstAnalytics.okToSend, false, reason: 'On the first run, we should not be ok ' 'to send any events, even if the user accepts', ); }); test('Confirm workflow for updated tools message version + new tool', () { // Helper function to check the state of the instance void checkAnalyticsInstance(Analytics instance) { expect(instance.shouldShowMessage, true); expect(instance.okToSend, false); instance.clientShowedMessage(); expect(instance.shouldShowMessage, false); expect( instance.okToSend, false, reason: 'On the first run, we should not be ok ' 'to send any events, even if the user accepts', ); } final firstAnalytics = Analytics.fake( tool: initialTool, homeDirectory: home, flutterChannel: flutterChannel, toolsMessageVersion: toolsMessageVersion, toolsMessage: toolsMessage, flutterVersion: flutterVersion, dartVersion: dartVersion, fs: fs, platform: platform, ); checkAnalyticsInstance(firstAnalytics); // Instance where we increment the version of the message final secondAnalytics = Analytics.fake( tool: initialTool, homeDirectory: home, flutterChannel: flutterChannel, toolsMessageVersion: toolsMessageVersion + 1, // Incrementing version toolsMessage: toolsMessage, flutterVersion: flutterVersion, dartVersion: dartVersion, fs: fs, platform: platform, ); // Running the same checks for the second instance, it should // behave the same as if it was a first run checkAnalyticsInstance(secondAnalytics); // Instance for a different tool with the incremented version final thirdAnalytics = Analytics.fake( tool: secondTool, // Different tool homeDirectory: home, flutterChannel: flutterChannel, toolsMessageVersion: toolsMessageVersion + 1, // Incrementing version toolsMessage: toolsMessage, flutterVersion: flutterVersion, dartVersion: dartVersion, fs: fs, platform: platform, ); // The instance with a new tool getting onboarded should be // treated the same as the 2 previous instances checkAnalyticsInstance(thirdAnalytics); }); test('Confirm workflow for checking tools into the config file', () { final firstAnalytics = Analytics.fake( tool: initialTool, homeDirectory: home, flutterChannel: flutterChannel, toolsMessageVersion: toolsMessageVersion, toolsMessage: toolsMessage, flutterVersion: flutterVersion, dartVersion: dartVersion, fs: fs, platform: platform, ); // Host of assertions to ensure all required artifacts // are created expect( dartToolDirectory.existsSync(), true, reason: 'The directory should have been created', ); expect( clientIdFile.existsSync(), true, reason: 'The $kClientIdFileName file was not found', ); expect( sessionFile.existsSync(), true, reason: 'The $kSessionFileName file was not found', ); expect( configFile.existsSync(), true, reason: 'The $kConfigFileName was not found', ); expect( logFile.existsSync(), true, reason: 'The $kLogFileName file was not found', ); expect( dismissedSurveyFile.existsSync(), true, reason: 'The $dismissedSurveyFile file was not found', ); expect( dartToolDirectory.listSync().length, equals(5), reason: 'There should only be 5 files in the $kDartToolDirectoryName ' 'directory', ); expect(configFile.readAsStringSync(), kConfigString); expect(firstAnalytics.shouldShowMessage, true); // Attempting to send a message with this instance should be // blocked because it has not invoked `clientShowedMessage()` // and it is the first run // // Even after invoking the method, it should be prevented from // sending a message because it is the first time the tool was // run in this instance firstAnalytics.send(testEvent); expect(logFile.readAsLinesSync().length, 0); firstAnalytics.clientShowedMessage(); // Attempt to send two events, both should be blocked because it is // part of the first instance firstAnalytics.send(testEvent); firstAnalytics.send(testEvent); expect(logFile.readAsLinesSync().length, 0); // Creating a second analytics instance from the same tool now should // allow for events to be sent final secondAnalytics = Analytics.fake( tool: initialTool, homeDirectory: home, flutterChannel: flutterChannel, toolsMessageVersion: toolsMessageVersion, toolsMessage: toolsMessage, flutterVersion: flutterVersion, dartVersion: dartVersion, fs: fs, platform: platform, ); expect(secondAnalytics.shouldShowMessage, false); secondAnalytics.send(testEvent); expect( logFile.readAsLinesSync().length, 1, reason: 'Events will be blocked until invoking method ' 'ensuring client has seen message', ); secondAnalytics.send(testEvent); secondAnalytics.send(testEvent); expect(logFile.readAsLinesSync().length, 3); // Next, we will want to confirm that the message should be showing when // a new analytics instance has been created with a newer version for // message that should be shown // // In this case, it should be treated as a new tool being added for the // first time and all events should be blocked // Delete the log file to reset the counter of events sent logFile.deleteSync(); final thirdAnalytics = Analytics.fake( tool: initialTool, homeDirectory: home, flutterChannel: flutterChannel, toolsMessageVersion: toolsMessageVersion + 1, // Incrementing version toolsMessage: toolsMessage, flutterVersion: flutterVersion, dartVersion: dartVersion, fs: fs, platform: platform, ); expect( logFile.existsSync(), true, reason: 'The $kLogFileName file was not found', ); expect( thirdAnalytics.shouldShowMessage, true, reason: 'New version number should require showing message', ); thirdAnalytics.send(testEvent); expect(logFile.readAsLinesSync().length, 0); thirdAnalytics.clientShowedMessage(); // Attempt to send two events, both should be blocked because it is // part of the third instance which has a new version for the consent // message which will be treated as a new tool being onboarded thirdAnalytics.send(testEvent); thirdAnalytics.send(testEvent); expect(logFile.readAsLinesSync().length, 0); // The fourth instance of the analytics class with the consent message // version incremented should now be able to send messages final fourthAnalytics = Analytics.fake( tool: initialTool, homeDirectory: home, flutterChannel: flutterChannel, toolsMessageVersion: toolsMessageVersion + 1, // Incrementing version toolsMessage: toolsMessage, flutterVersion: flutterVersion, dartVersion: dartVersion, fs: fs, platform: platform, ); expect(fourthAnalytics.shouldShowMessage, false); fourthAnalytics.send(testEvent); expect( logFile.readAsLinesSync().length, 1, reason: 'Events will be blocked until invoking method ' 'ensuring client has seen message', ); fourthAnalytics.send(testEvent); fourthAnalytics.send(testEvent); expect(logFile.readAsLinesSync().length, 3); }); test('Disable second instance if first one did not show message', () { final firstAnalytics = Analytics.fake( tool: initialTool, homeDirectory: home, flutterChannel: flutterChannel, toolsMessageVersion: toolsMessageVersion, toolsMessage: toolsMessage, flutterVersion: flutterVersion, dartVersion: dartVersion, fs: fs, platform: platform, ); expect(firstAnalytics.shouldShowMessage, true); final secondAnalytics = Analytics.fake( tool: initialTool, homeDirectory: home, flutterChannel: flutterChannel, toolsMessageVersion: toolsMessageVersion, toolsMessage: toolsMessage, flutterVersion: flutterVersion, dartVersion: dartVersion, fs: fs, platform: platform, ); expect(secondAnalytics.shouldShowMessage, true); secondAnalytics.clientShowedMessage(); final thirdAnalytics = Analytics.fake( tool: initialTool, homeDirectory: home, flutterChannel: flutterChannel, toolsMessageVersion: toolsMessageVersion, toolsMessage: toolsMessage, flutterVersion: flutterVersion, dartVersion: dartVersion, fs: fs, platform: platform, ); expect(thirdAnalytics.shouldShowMessage, false); }); test('Passing large version number gets logged in config', () { final firstVersion = toolsMessageVersion + 3; final secondAnalytics = Analytics.fake( tool: secondTool, homeDirectory: home, flutterChannel: flutterChannel, toolsMessageVersion: firstVersion, toolsMessage: toolsMessage, flutterVersion: 'Flutter 3.6.0-7.0.pre.47', dartVersion: 'Dart 2.19.0', fs: fs, platform: platform, ); secondAnalytics.clientShowedMessage(); expect( configFile.readAsStringSync().endsWith( '${secondTool.label}=$dateStamp,$firstVersion\n', ), true, ); // Create a new instane of the secondTool with an even // bigger version final secondVersion = firstVersion + 3; final thirdAnalytics = Analytics.fake( tool: secondTool, homeDirectory: home, flutterChannel: flutterChannel, toolsMessageVersion: secondVersion, toolsMessage: toolsMessage, flutterVersion: 'Flutter 3.6.0-7.0.pre.47', dartVersion: 'Dart 2.19.0', fs: fs, platform: platform, ); expect( configFile.readAsStringSync().endsWith( '${secondTool.label}=$dateStamp,$firstVersion\n', ), true, ); // After invoking this method, it will get updated // in the config with the next version thirdAnalytics.clientShowedMessage(); expect( configFile.readAsStringSync().endsWith( '${secondTool.label}=$dateStamp,$secondVersion\n', ), true, ); }); }