diff --git a/lib/reducers/report-store-reducer.test.js b/lib/reducers/report-store-reducer.test.js new file mode 100644 index 000000000..0f91078f6 --- /dev/null +++ b/lib/reducers/report-store-reducer.test.js @@ -0,0 +1,287 @@ +// @flow + +import reduceReportStore from './report-store-reducer.js'; +import type { LogInResult } from '../types/account-types.js'; +import type { LoadingInfo } from '../types/loading-types.js'; +import type { AppState, BaseAction } from '../types/redux-types.js'; +import { + type ReportStore, + reportTypes, + type MediaMissionReportCreationRequest, + type ErrorReportCreationRequest, + type EnabledReports, + type ClientThreadInconsistencyReportCreationRequest, +} from '../types/report-types.js'; + +const loadingInfo: LoadingInfo = { + fetchIndex: 0, + trackMultipleRequests: false, + customKeyName: undefined, +}; + +// this is only for types compatibility and `any` will not have any influence +// on tests correctness +const defaultState: AppState = ({}: any); +const defaultBaseAction: BaseAction = ({ + payload: ({}: any), + loadingInfo, +}: any); +const defaultAction = { + payload: ({}: any), + loadingInfo, +}; + +const mockErrorReport: ErrorReportCreationRequest = { + type: reportTypes.ERROR, + platformDetails: { platform: 'web' }, + errors: [], + preloadedState: defaultState, + currentState: defaultState, + actions: [], + id: '1-1', +}; + +const mockInconsistencyReport: ClientThreadInconsistencyReportCreationRequest = + { + type: reportTypes.THREAD_INCONSISTENCY, + platformDetails: { platform: 'web' }, + beforeAction: {}, + action: defaultBaseAction, + pushResult: {}, + lastActions: [], + time: 0, + id: '1-2', + }; + +const mockMediaReport: MediaMissionReportCreationRequest = { + type: reportTypes.MEDIA_MISSION, + platformDetails: { platform: 'web' }, + time: Date.now(), + mediaMission: { + steps: [], + result: { success: true }, + totalTime: 0, + userTime: 0, + }, + id: '1-3', +}; + +const defaultEnabledReports: EnabledReports = { + crashReports: true, + inconsistencyReports: true, + mediaReports: true, +}; + +const defaultEmptyReportStore: ReportStore = { + queuedReports: [], + enabledReports: defaultEnabledReports, +}; + +const defaultReportStore: ReportStore = { + queuedReports: [mockErrorReport, mockInconsistencyReport, mockMediaReport], + enabledReports: defaultEnabledReports, +}; + +describe('session change test', () => { + const mockLogInResult: LogInResult = ({ + currentUserInfo: { id: '-1', username: 'test' }, + }: any); + + test('should handle log out', () => { + const action = { ...defaultAction, type: 'LOG_OUT_SUCCESS' }; + const result = reduceReportStore(defaultReportStore, action, []); + expect(result.queuedReports).toHaveLength(0); + }); + + test('should handle log out with new inconsistencies', () => { + const action = { ...defaultAction, type: 'LOG_OUT_SUCCESS' }; + const result = reduceReportStore(defaultReportStore, action, [ + mockErrorReport, + ]); + expect(result.queuedReports).toHaveLength(0); + }); + + test('should handle log in', () => { + const action = { + type: 'LOG_IN_SUCCESS', + payload: mockLogInResult, + loadingInfo, + }; + const result = reduceReportStore(defaultReportStore, action, []); + expect(result.queuedReports).toHaveLength(0); + }); + + test('should handle log in with new inconsistencies', () => { + const action = { + type: 'LOG_IN_SUCCESS', + payload: mockLogInResult, + loadingInfo, + }; + const result = reduceReportStore(defaultReportStore, action, [ + mockErrorReport, + ]); + expect(result.queuedReports).toHaveLength(0); + }); +}); + +describe('updateReportsEnabledActionType test', () => { + test('should handle the same enabled reports', () => { + const action = { + type: 'UPDATE_REPORTS_ENABLED', + payload: defaultEnabledReports, + }; + const result = reduceReportStore(defaultReportStore, action, []); + expect(result.queuedReports).toStrictEqual( + defaultReportStore.queuedReports, + ); + }); + + test('should handle changing enabled reports', () => { + const action = { + type: 'UPDATE_REPORTS_ENABLED', + payload: { + crashReports: true, + inconsistencyReports: false, + mediaReports: false, + }, + }; + const result = reduceReportStore(defaultReportStore, action, []); + expect(result.queuedReports).toHaveLength(1); + const enabledReportsExist = result.queuedReports.some( + report => report.type === reportTypes.ERROR, + ); + const notEnabledReportsExist = result.queuedReports.some( + report => report.type !== reportTypes.ERROR, + ); + expect(enabledReportsExist).toBeTruthy(); + expect(notEnabledReportsExist).toBeFalsy(); + }); + + test('should handle changing enabled reports with new inconsistencies', () => { + const action = { + type: 'UPDATE_REPORTS_ENABLED', + payload: { + crashReports: true, + inconsistencyReports: false, + mediaReports: false, + }, + }; + const result = reduceReportStore(defaultReportStore, action, [ + { ...mockErrorReport, id: 'new-id-error' }, + { ...mockMediaReport, id: 'new-id-media' }, + ]); + expect(result.queuedReports).toHaveLength(2); + const enabledReports = result.queuedReports.filter( + report => report.type === reportTypes.ERROR, + ); + const notEnabledReportsExist = result.queuedReports.some( + report => report.type !== reportTypes.ERROR, + ); + expect(enabledReports).toHaveLength(2); + expect(notEnabledReportsExist).toBeFalsy(); + }); +}); + +describe('queueReportsActionType test', () => { + test('should handle adding enabled report', () => { + const action = { + type: 'QUEUE_REPORTS', + payload: { + reports: [ + { ...mockErrorReport, id: 'new-id-error' }, + { ...mockMediaReport, id: 'new-id-media' }, + ], + }, + }; + const reportStore = { + queuedReports: [mockErrorReport], + enabledReports: { + crashReports: true, + inconsistencyReports: false, + mediaReports: false, + }, + }; + const result = reduceReportStore(reportStore, action, [ + { ...mockErrorReport, id: 'new-id-error-inc' }, + { ...mockMediaReport, id: 'new-id-media-inc' }, + ]); + expect(result.queuedReports).toHaveLength(3); + const enabledReports = result.queuedReports.filter( + report => report.type === reportTypes.ERROR, + ); + const notEnabledReportsExist = result.queuedReports.some( + report => report.type !== reportTypes.ERROR, + ); + expect(enabledReports).toHaveLength(3); + expect(notEnabledReportsExist).toBeFalsy(); + }); +}); + +describe('sending report test', () => { + test('should remove sent report', () => { + const reportStore = reduceReportStore( + defaultEmptyReportStore, + defaultBaseAction, + [mockErrorReport, mockMediaReport], + ); + expect(reportStore.queuedReports).toHaveLength(2); + const [sentReport, notSentReport] = reportStore.queuedReports; + + const action = { + type: 'SEND_REPORT_SUCCESS', + payload: { + reports: [sentReport], + }, + loadingInfo, + }; + const result = reduceReportStore(reportStore, action, []); + expect(result.queuedReports).toHaveLength(1); + expect(result.queuedReports).toContain(notSentReport); + expect(result.queuedReports).not.toContain(sentReport); + }); + + test('should remove sent report and handle new inconsistencies', () => { + const reportStore = reduceReportStore( + defaultEmptyReportStore, + defaultBaseAction, + [mockErrorReport, mockMediaReport], + ); + expect(reportStore.queuedReports).toHaveLength(2); + const [sentReport, notSentReport] = reportStore.queuedReports; + + const action = { + type: 'SEND_REPORT_SUCCESS', + payload: { + reports: [sentReport], + }, + loadingInfo, + }; + const result = reduceReportStore(reportStore, action, [ + mockInconsistencyReport, + ]); + expect(result.queuedReports).toHaveLength(2); + expect(result.queuedReports).toContain(notSentReport); + expect(result.queuedReports).not.toContain(sentReport); + }); +}); + +describe('new inconsistencies test', () => { + test('should handle new inconsistencies without any action', () => { + const reportStore = reduceReportStore( + { + queuedReports: [mockErrorReport], + enabledReports: { + crashReports: true, + inconsistencyReports: false, + mediaReports: false, + }, + }, + defaultBaseAction, + [ + { ...mockErrorReport, id: 'new-id-error' }, + { ...mockMediaReport, id: 'new-id-media' }, + ], + ); + expect(reportStore.queuedReports).toHaveLength(2); + }); +}); diff --git a/lib/types/report-types.js b/lib/types/report-types.js index e2889f5ef..34ed3f9af 100644 --- a/lib/types/report-types.js +++ b/lib/types/report-types.js @@ -1,217 +1,217 @@ // @flow import invariant from 'invariant'; import t, { type TInterface } from 'tcomb'; import { type PlatformDetails } from './device-types.js'; import { type RawEntryInfo, type CalendarQuery } from './entry-types.js'; import { type MediaMission } from './media-types.js'; import type { AppState, BaseAction } from './redux-types.js'; import { type RawThreadInfo } from './thread-types.js'; import type { UserInfo, UserInfos } from './user-types.js'; import { tPlatformDetails, tShape } from '../utils/validation-utils.js'; export type EnabledReports = { +crashReports: boolean, +inconsistencyReports: boolean, +mediaReports: boolean, }; export type SupportedReports = $Keys; export const defaultEnabledReports: EnabledReports = { crashReports: false, inconsistencyReports: false, mediaReports: false, }; export const defaultDevEnabledReports: EnabledReports = { crashReports: true, inconsistencyReports: true, mediaReports: true, }; export type ReportStore = { +enabledReports: EnabledReports, +queuedReports: $ReadOnlyArray, }; export const reportTypes = Object.freeze({ ERROR: 0, THREAD_INCONSISTENCY: 1, ENTRY_INCONSISTENCY: 2, MEDIA_MISSION: 3, USER_INCONSISTENCY: 4, }); type ReportType = $Values; export function assertReportType(reportType: number): ReportType { invariant( reportType === 0 || reportType === 1 || reportType === 2 || reportType === 3 || reportType === 4, 'number is not ReportType enum', ); return reportType; } export type ErrorInfo = { componentStack: string, ... }; export type ErrorData = { error: Error, info?: ErrorInfo }; export type FlatErrorData = { errorMessage: string, stack?: string, componentStack?: ?string, }; export type ActionSummary = { +type: $PropertyType, +time: number, +summary: string, }; export type ThreadInconsistencyReportShape = { +platformDetails: PlatformDetails, +beforeAction: { +[id: string]: RawThreadInfo }, +action: BaseAction, +pollResult?: { +[id: string]: RawThreadInfo }, +pushResult: { +[id: string]: RawThreadInfo }, +lastActionTypes?: $ReadOnlyArray<$PropertyType>, +lastActions?: $ReadOnlyArray, +time?: number, }; export type EntryInconsistencyReportShape = { +platformDetails: PlatformDetails, +beforeAction: { +[id: string]: RawEntryInfo }, +action: BaseAction, +calendarQuery: CalendarQuery, +pollResult?: { +[id: string]: RawEntryInfo }, +pushResult: { +[id: string]: RawEntryInfo }, +lastActionTypes?: $ReadOnlyArray<$PropertyType>, +lastActions?: $ReadOnlyArray, +time: number, }; export type UserInconsistencyReportShape = { +platformDetails: PlatformDetails, +action: BaseAction, +beforeStateCheck: UserInfos, +afterStateCheck: UserInfos, +lastActions: $ReadOnlyArray, +time: number, }; -type ErrorReportCreationRequest = { +export type ErrorReportCreationRequest = { +type: 0, +platformDetails: PlatformDetails, +errors: $ReadOnlyArray, +preloadedState: AppState, +currentState: AppState, +actions: $ReadOnlyArray, +id: string, }; export type ThreadInconsistencyReportCreationRequest = { ...ThreadInconsistencyReportShape, +type: 1, }; export type EntryInconsistencyReportCreationRequest = { ...EntryInconsistencyReportShape, +type: 2, }; export type MediaMissionReportCreationRequest = { +type: 3, +platformDetails: PlatformDetails, +time: number, // ms +mediaMission: MediaMission, +uploadServerID?: ?string, +uploadLocalID?: ?string, +mediaLocalID?: ?string, // deprecated +messageServerID?: ?string, +messageLocalID?: ?string, +id: string, }; export type UserInconsistencyReportCreationRequest = { ...UserInconsistencyReportShape, +type: 4, +id: string, }; export type ReportCreationRequest = | ErrorReportCreationRequest | ThreadInconsistencyReportCreationRequest | EntryInconsistencyReportCreationRequest | MediaMissionReportCreationRequest | UserInconsistencyReportCreationRequest; export type ClientThreadInconsistencyReportShape = { +platformDetails: PlatformDetails, +beforeAction: { +[id: string]: RawThreadInfo }, +action: BaseAction, +pushResult: { +[id: string]: RawThreadInfo }, +lastActions: $ReadOnlyArray, +time: number, }; export type ClientEntryInconsistencyReportShape = { +platformDetails: PlatformDetails, +beforeAction: { +[id: string]: RawEntryInfo }, +action: BaseAction, +calendarQuery: CalendarQuery, +pushResult: { +[id: string]: RawEntryInfo }, +lastActions: $ReadOnlyArray, +time: number, }; export type ClientThreadInconsistencyReportCreationRequest = { ...ClientThreadInconsistencyReportShape, +type: 1, +id: string, }; export type ClientEntryInconsistencyReportCreationRequest = { ...ClientEntryInconsistencyReportShape, +type: 2, +id: string, }; export type ClientReportCreationRequest = | ErrorReportCreationRequest | ClientThreadInconsistencyReportCreationRequest | ClientEntryInconsistencyReportCreationRequest | MediaMissionReportCreationRequest | UserInconsistencyReportCreationRequest; export type QueueReportsPayload = { +reports: $ReadOnlyArray, }; export type ClearDeliveredReportsPayload = { +reports: $ReadOnlyArray, }; export type ReportCreationResponse = { +id: string, }; type ReportInfo = { +id: string, +viewerID: string, +platformDetails: PlatformDetails, +creationTime: number, }; export const reportInfoValidator: TInterface = tShape({ id: t.String, viewerID: t.String, platformDetails: tPlatformDetails, creationTime: t.Number, }); export type FetchErrorReportInfosRequest = { +cursor: ?string, }; export type FetchErrorReportInfosResponse = { +reports: $ReadOnlyArray, +userInfos: $ReadOnlyArray, }; export type ReduxToolsImport = { +preloadedState: AppState, +payload: $ReadOnlyArray, };