123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746 |
- import * as GroupActionCreators from 'sentry/actionCreators/group';
- import GroupingStore from 'sentry/stores/groupingStore';
- describe('Grouping Store', function () {
- let trigger!: jest.SpyInstance;
- beforeAll(function () {
- MockApiClient.asyncDelay = 1;
- });
- afterAll(function () {
- MockApiClient.asyncDelay = undefined;
- });
- beforeEach(function () {
- GroupingStore.init();
- trigger = jest.spyOn(GroupingStore, 'trigger');
- MockApiClient.addMockResponse({
- url: '/issues/groupId/hashes/',
- body: [
- {
- latestEvent: {
- eventID: 'event-1',
- },
- state: 'locked',
- id: '1',
- },
- {
- latestEvent: {
- eventID: 'event-2',
- },
- state: 'unlocked',
- id: '2',
- },
- {
- latestEvent: {
- eventID: 'event-3',
- },
- state: 'unlocked',
- id: '3',
- },
- {
- latestEvent: {
- eventID: 'event-4',
- },
- state: 'unlocked',
- id: '4',
- },
- {
- latestEvent: {
- eventID: 'event-5',
- },
- state: 'locked',
- id: '5',
- },
- ],
- });
- MockApiClient.addMockResponse({
- url: '/issues/groupId/similar/',
- body: [
- [
- {
- id: '274',
- },
- {
- 'exception:stacktrace:pairs': 0.375,
- 'exception:stacktrace:application-chunks': 0.175,
- 'message:message:character-shingles': 0.775,
- },
- ],
- [
- {
- id: '275',
- },
- {'exception:stacktrace:pairs': 1.0},
- ],
- [
- {
- id: '216',
- },
- {
- 'exception:stacktrace:application-chunks': 0.000235,
- 'exception:stacktrace:pairs': 0.001488,
- },
- ],
- [
- {
- id: '217',
- },
- {
- 'exception:message:character-shingles': null,
- 'exception:stacktrace:application-chunks': 0.25,
- 'exception:stacktrace:pairs': 0.25,
- 'message:message:character-shingles': 0.7,
- },
- ],
- ],
- });
- });
- afterEach(function () {
- MockApiClient.clearMockResponses();
- jest.resetAllMocks();
- jest.restoreAllMocks();
- });
- describe('onFetch()', function () {
- beforeEach(() => GroupingStore.init());
- it('initially gets called with correct state values', function () {
- GroupingStore.onFetch([]);
- expect(trigger).toHaveBeenCalled();
- expect(trigger).toHaveBeenCalledWith(
- expect.objectContaining({
- error: false,
- filteredSimilarItems: [],
- loading: true,
- mergeState: new Map(),
- mergedItems: [],
- mergedLinks: '',
- similarItems: [],
- similarLinks: '',
- unmergeState: new Map(),
- })
- );
- });
- it('fetches list of similar items', async function () {
- await GroupingStore.onFetch([
- {dataKey: 'similar', endpoint: '/issues/groupId/similar/'},
- ]);
- expect(trigger).toHaveBeenCalled();
- const calls = trigger.mock.calls;
- const arg: any = calls[calls.length - 1][0];
- expect(arg.filteredSimilarItems).toHaveLength(1);
- expect(arg.similarItems).toHaveLength(3);
- expect(arg).toMatchObject({
- loading: false,
- error: false,
- mergeState: new Map(),
- mergedItems: [],
- similarItems: [
- {
- isBelowThreshold: false,
- issue: {
- id: '274',
- },
- },
- {
- isBelowThreshold: false,
- issue: {
- id: '275',
- },
- },
- {
- isBelowThreshold: false,
- issue: {
- id: '217',
- },
- },
- ],
- filteredSimilarItems: [
- {
- isBelowThreshold: true,
- issue: {
- id: '216',
- },
- },
- ],
- unmergeState: new Map(),
- });
- });
- it('unsuccessfully fetches list of similar items', function () {
- MockApiClient.clearMockResponses();
- MockApiClient.addMockResponse({
- url: '/issues/groupId/similar/',
- statusCode: 500,
- body: {message: 'failed'},
- });
- const promise = GroupingStore.onFetch([
- {dataKey: 'similar', endpoint: '/issues/groupId/similar/'},
- ]);
- expect(trigger).toHaveBeenCalled();
- const calls = trigger.mock.calls;
- return promise.then(() => {
- const arg = calls[calls.length - 1][0];
- expect(arg).toMatchObject({
- loading: false,
- error: true,
- mergeState: new Map(),
- mergedItems: [],
- unmergeState: new Map(),
- });
- });
- });
- it('ignores null scores in aggregate', async function () {
- await GroupingStore.onFetch([
- {dataKey: 'similar', endpoint: '/issues/groupId/similar/'},
- ]);
- expect(trigger).toHaveBeenCalled();
- const calls = trigger.mock.calls;
- const arg: any = calls[calls.length - 1][0];
- const item = arg.similarItems.find(({issue}) => issue.id === '217');
- expect(item.aggregate.exception).toBe(0.25);
- expect(item.aggregate.message).toBe(0.7);
- });
- it('fetches list of hashes', function () {
- const promise = GroupingStore.onFetch([
- {dataKey: 'merged', endpoint: '/issues/groupId/hashes/'},
- ]);
- expect(trigger).toHaveBeenCalled();
- const calls = trigger.mock.calls;
- return promise.then(() => {
- const arg = calls[calls.length - 1][0];
- expect(arg.mergedItems).toHaveLength(5);
- expect(arg).toMatchObject({
- loading: false,
- error: false,
- similarItems: [],
- filteredSimilarItems: [],
- mergeState: new Map(),
- unmergeState: new Map([
- ['1', {busy: true}],
- ['2', {busy: false}],
- ['3', {busy: false}],
- ['4', {busy: false}],
- ['5', {busy: true}],
- ]),
- });
- });
- });
- it('unsuccessfully fetches list of hashes items', function () {
- MockApiClient.clearMockResponses();
- MockApiClient.addMockResponse({
- url: '/issues/groupId/hashes/',
- statusCode: 500,
- body: {message: 'failed'},
- });
- const promise = GroupingStore.onFetch([
- {dataKey: 'merged', endpoint: '/issues/groupId/hashes/'},
- ]);
- expect(trigger).toHaveBeenCalled();
- const calls = trigger.mock.calls;
- return promise.then(() => {
- const arg = calls[calls.length - 1][0];
- expect(arg).toMatchObject({
- loading: false,
- error: true,
- mergeState: new Map(),
- mergedItems: [],
- unmergeState: new Map(),
- });
- });
- });
- });
- describe('Similar Issues list (to be merged)', function () {
- let mergeList: (typeof GroupingStore)['state']['mergeList'];
- let mergeState: (typeof GroupingStore)['state']['mergeState'];
- beforeEach(function () {
- GroupingStore.init();
- mergeList = [];
- mergeState = new Map();
- GroupingStore.onFetch([{dataKey: 'similar', endpoint: '/issues/groupId/similar/'}]);
- });
- describe('onToggleMerge (checkbox state)', function () {
- // Attempt to check first item but its "locked" so should not be able to do anything
- it('can check and uncheck item', function () {
- GroupingStore.onToggleMerge('1');
- mergeList = ['1'];
- mergeState.set('1', {checked: true});
- expect(GroupingStore.getState().mergeList).toEqual(mergeList);
- expect(GroupingStore.getState().mergeState).toEqual(mergeState);
- // Uncheck
- GroupingStore.onToggleMerge('1');
- mergeList = mergeList.filter(item => item !== '1');
- mergeState.set('1', {checked: false});
- // Check all
- GroupingStore.onToggleMerge('1');
- GroupingStore.onToggleMerge('2');
- GroupingStore.onToggleMerge('3');
- mergeList = ['1', '2', '3'];
- mergeState.set('1', {checked: true});
- mergeState.set('2', {checked: true});
- mergeState.set('3', {checked: true});
- expect(GroupingStore.getState().mergeList).toEqual(mergeList);
- expect(GroupingStore.getState().mergeState).toEqual(mergeState);
- expect(trigger).toHaveBeenLastCalledWith({
- mergeDisabled: false,
- mergeList,
- mergeState,
- });
- });
- });
- describe('onMerge', function () {
- beforeEach(function () {
- MockApiClient.clearMockResponses();
- MockApiClient.addMockResponse({
- method: 'PUT',
- url: '/projects/orgId/projectId/issues/',
- });
- GroupingStore.init();
- });
- it('disables rows to be merged', async function () {
- const mergeMock = jest.spyOn(GroupActionCreators, 'mergeGroups');
- trigger.mockReset();
- GroupingStore.onToggleMerge('1');
- mergeList = ['1'];
- mergeState.set('1', {checked: true});
- expect(trigger).toHaveBeenLastCalledWith(
- expect.objectContaining({
- mergeDisabled: false,
- mergeList,
- mergeState,
- })
- );
- trigger.mockReset();
- // Everything is sync so trigger will have been called multiple times
- const promise = GroupingStore.onMerge({
- params: {
- orgId: 'orgId',
- groupId: 'groupId',
- },
- projectId: 'projectId',
- });
- mergeState.set('1', {checked: true, busy: true});
- expect(trigger).toHaveBeenCalledWith(
- expect.objectContaining({
- mergeDisabled: true,
- mergeList,
- mergeState,
- })
- );
- await promise;
- expect(mergeMock).toHaveBeenCalledWith(
- expect.anything(),
- {
- orgId: 'orgId',
- projectId: 'projectId',
- itemIds: ['1', 'groupId'],
- query: undefined,
- },
- {
- error: expect.any(Function),
- success: expect.any(Function),
- complete: expect.any(Function),
- }
- );
- // Should be removed from mergeList after merged
- mergeList = mergeList.filter(item => item !== '1');
- mergeState.set('1', {checked: false, busy: true});
- expect(trigger).toHaveBeenLastCalledWith(
- expect.objectContaining({
- mergeDisabled: false,
- mergeList,
- mergeState,
- })
- );
- });
- it('keeps rows in "busy" state and unchecks after successfully adding to merge queue', async function () {
- GroupingStore.onToggleMerge('1');
- mergeList = ['1'];
- mergeState.set('1', {checked: true});
- // Expect checked
- expect(trigger).toHaveBeenCalledWith(
- expect.objectContaining({
- mergeDisabled: false,
- mergeList,
- mergeState,
- })
- );
- trigger.mockReset();
- // Start unmerge
- const promise = GroupingStore.onMerge({
- params: {
- orgId: 'orgId',
- groupId: 'groupId',
- },
- projectId: 'projectId',
- });
- mergeState.set('1', {checked: true, busy: true});
- // Expect checked to remain the same, but is now busy
- expect(trigger).toHaveBeenCalledWith(
- expect.objectContaining({
- mergeDisabled: true,
- mergeList,
- mergeState,
- })
- );
- await promise;
- mergeState.set('1', {checked: false, busy: true});
- // After promise, reset checked to false, but keep busy
- expect(trigger).toHaveBeenLastCalledWith(
- expect.objectContaining({
- mergeDisabled: false,
- mergeList: [],
- mergeState,
- })
- );
- });
- it('resets busy state and has same items checked after error when trying to merge', async function () {
- MockApiClient.clearMockResponses();
- MockApiClient.addMockResponse({
- method: 'PUT',
- url: '/projects/orgId/projectId/issues/',
- statusCode: 500,
- body: {},
- });
- GroupingStore.onToggleMerge('1');
- mergeList = ['1'];
- mergeState.set('1', {checked: true});
- const promise = GroupingStore.onMerge({
- params: {
- orgId: 'orgId',
- groupId: 'groupId',
- },
- projectId: 'projectId',
- });
- mergeState.set('1', {checked: true, busy: true});
- expect(trigger).toHaveBeenCalledWith(
- expect.objectContaining({
- mergeDisabled: true,
- mergeList,
- mergeState,
- })
- );
- await promise;
- // Error state
- mergeState.set('1', {checked: true, busy: false});
- expect(trigger).toHaveBeenLastCalledWith(
- expect.objectContaining({
- mergeDisabled: false,
- mergeList,
- mergeState,
- })
- );
- });
- });
- });
- describe('Hashes list (to be unmerged)', function () {
- let unmergeList: (typeof GroupingStore)['state']['unmergeList'];
- let unmergeState: (typeof GroupingStore)['state']['unmergeState'];
- beforeEach(async function () {
- GroupingStore.init();
- unmergeList = new Map();
- unmergeState = new Map();
- await GroupingStore.onFetch([
- {dataKey: 'merged', endpoint: '/issues/groupId/hashes/'},
- ]);
- trigger.mockClear();
- unmergeState = new Map([...GroupingStore.getState().unmergeState]);
- });
- // WARNING: all the tests in this describe block are not running in isolated state.
- // There is a good chance that moving them around will break them. To simulate an isolated state,
- // add a beforeEach(() => GroupingStore.init())
- describe('onToggleUnmerge (checkbox state for hashes)', function () {
- // Attempt to check first item but its "locked" so should not be able to do anything
- it('can not check locked item', function () {
- GroupingStore.onToggleUnmerge('1');
- expect(GroupingStore.getState().unmergeList).toEqual(unmergeList);
- expect(GroupingStore.getState().unmergeState).toEqual(unmergeState);
- expect(trigger).not.toHaveBeenCalled();
- });
- it('can check and uncheck unlocked items', function () {
- // Check
- GroupingStore.onToggleUnmerge(['2', 'event-2']);
- unmergeList.set('2', 'event-2');
- unmergeState.set('2', {busy: false, checked: true});
- expect(GroupingStore.getState().unmergeList).toEqual(unmergeList);
- expect(GroupingStore.getState().unmergeState).toEqual(unmergeState);
- // Uncheck
- GroupingStore.onToggleUnmerge(['2', 'event-2']);
- unmergeList.delete('2');
- unmergeState.set('2', {busy: false, checked: false});
- expect(GroupingStore.getState().unmergeList).toEqual(unmergeList);
- expect(GroupingStore.getState().unmergeState).toEqual(unmergeState);
- // Check
- GroupingStore.onToggleUnmerge(['2', 'event-2']);
- unmergeList.set('2', 'event-2');
- unmergeState.set('2', {busy: false, checked: true});
- expect(GroupingStore.getState().unmergeList).toEqual(unmergeList);
- expect(GroupingStore.getState().unmergeState).toEqual(unmergeState);
- expect(trigger).toHaveBeenLastCalledWith(
- expect.objectContaining({
- enableFingerprintCompare: false,
- unmergeLastCollapsed: false,
- unmergeDisabled: false,
- unmergeList,
- unmergeState,
- })
- );
- });
- it('should have Compare button enabled only when two fingerprints are checked', function () {
- expect(GroupingStore.getState().enableFingerprintCompare).toBe(false);
- GroupingStore.onToggleUnmerge(['2', 'event-2']);
- GroupingStore.onToggleUnmerge(['3', 'event-3']);
- expect(GroupingStore.getState().enableFingerprintCompare).toBe(true);
- GroupingStore.onToggleUnmerge(['2', 'event-2']);
- expect(GroupingStore.getState().enableFingerprintCompare).toBe(false);
- });
- it('selecting all available checkboxes should disable the unmerge button and re-enable when unchecking', function () {
- GroupingStore.onToggleUnmerge(['2', 'event-2']);
- GroupingStore.onToggleUnmerge(['3', 'event-3']);
- GroupingStore.onToggleUnmerge(['4', 'event-4']);
- unmergeList.set('2', 'event-2');
- unmergeList.set('3', 'event-3');
- unmergeList.set('4', 'event-4');
- unmergeState.set('2', {busy: false, checked: true});
- unmergeState.set('3', {busy: false, checked: true});
- unmergeState.set('4', {busy: false, checked: true});
- expect(GroupingStore.getState().unmergeList).toEqual(unmergeList);
- expect(GroupingStore.getState().unmergeState).toEqual(unmergeState);
- expect(GroupingStore.getState().unmergeDisabled).toBe(true);
- // Unchecking
- GroupingStore.onToggleUnmerge(['4', 'event-4']);
- unmergeList.delete('4');
- unmergeState.set('4', {busy: false, checked: false});
- expect(GroupingStore.getState().unmergeList).toEqual(unmergeList);
- expect(GroupingStore.getState().unmergeState).toEqual(unmergeState);
- expect(GroupingStore.getState().unmergeDisabled).toBe(false);
- expect(trigger).toHaveBeenLastCalledWith({
- enableFingerprintCompare: true,
- unmergeLastCollapsed: false,
- unmergeDisabled: false,
- unmergeList,
- unmergeState,
- });
- });
- });
- // WARNING: all the tests in this describe block are not running in isolated state.
- // There is a good chance that moving them around will break them. To simulate an isolated state,
- // add a beforeEach(() => GroupingStore.init())
- describe('onUnmerge', function () {
- beforeEach(function () {
- MockApiClient.clearMockResponses();
- MockApiClient.addMockResponse({
- method: 'PUT',
- url: '/organizations/org-slug/issues/groupId/hashes/',
- });
- });
- it('can not toggle unmerge for a locked item', function () {
- // Event 1 is locked
- GroupingStore.onToggleUnmerge(['1', 'event-1']);
- unmergeState.set('1', {busy: true});
- // trigger does NOT get called because an item returned via API is in a "locked" state
- expect(trigger).not.toHaveBeenCalled();
- GroupingStore.onUnmerge({
- orgSlug: 'org-slug',
- groupId: 'groupId',
- });
- expect(trigger).toHaveBeenCalledWith({
- enableFingerprintCompare: false,
- unmergeLastCollapsed: false,
- unmergeDisabled: true,
- unmergeList,
- unmergeState,
- });
- });
- it('disables rows to be merged', async function () {
- GroupingStore.onToggleUnmerge(['2', 'event-2']);
- unmergeList.set('2', 'event-2');
- unmergeState.set('2', {checked: true, busy: false});
- // trigger does NOT get called because an item returned via API is in a "locked" state
- expect(trigger).toHaveBeenCalledWith({
- enableFingerprintCompare: false,
- unmergeLastCollapsed: false,
- unmergeDisabled: false,
- unmergeList,
- unmergeState,
- });
- const promise = GroupingStore.onUnmerge({
- orgSlug: 'org-slug',
- groupId: 'groupId',
- });
- unmergeState.set('2', {checked: false, busy: true});
- expect(trigger).toHaveBeenCalledWith({
- enableFingerprintCompare: false,
- unmergeLastCollapsed: false,
- unmergeDisabled: true,
- unmergeList,
- unmergeState,
- });
- await promise;
- // Success
- unmergeState.set('2', {checked: false, busy: true});
- unmergeList.delete('2');
- expect(trigger).toHaveBeenLastCalledWith({
- enableFingerprintCompare: false,
- unmergeLastCollapsed: false,
- unmergeDisabled: false,
- unmergeList,
- unmergeState,
- });
- });
- it('keeps rows in "busy" state and unchecks after successfully adding to unmerge queue', async function () {
- GroupingStore.onToggleUnmerge(['2', 'event-2']);
- unmergeList.set('2', 'event-2');
- unmergeState.set('2', {checked: true, busy: false});
- const promise = GroupingStore.onUnmerge({
- groupId: 'groupId',
- orgSlug: 'org-slug',
- });
- unmergeState.set('2', {checked: false, busy: true});
- expect(trigger).toHaveBeenCalledWith({
- enableFingerprintCompare: false,
- unmergeLastCollapsed: false,
- unmergeDisabled: true,
- unmergeList,
- unmergeState,
- });
- await promise;
- expect(trigger).toHaveBeenLastCalledWith({
- enableFingerprintCompare: false,
- unmergeLastCollapsed: false,
- unmergeDisabled: false,
- unmergeList: new Map(),
- unmergeState,
- });
- });
- it('resets busy state and has same items checked after error when trying to merge', async function () {
- MockApiClient.clearMockResponses();
- MockApiClient.addMockResponse({
- method: 'PUT',
- url: '/organizations/org-slug/issues/groupId/hashes/',
- statusCode: 500,
- body: {},
- });
- GroupingStore.onToggleUnmerge(['2', 'event-2']);
- unmergeList.set('2', 'event-2');
- const promise = GroupingStore.onUnmerge({
- groupId: 'groupId',
- orgSlug: 'org-slug',
- });
- unmergeState.set('2', {checked: false, busy: true});
- expect(trigger).toHaveBeenCalledWith(
- expect.objectContaining({
- enableFingerprintCompare: false,
- unmergeLastCollapsed: false,
- unmergeDisabled: true,
- unmergeList,
- unmergeState,
- })
- );
- await promise;
- unmergeState.set('2', {checked: true, busy: false});
- expect(trigger).toHaveBeenLastCalledWith({
- enableFingerprintCompare: false,
- unmergeLastCollapsed: false,
- unmergeDisabled: false,
- unmergeList,
- unmergeState,
- });
- });
- });
- });
- });
|