diff --git a/specifyweb/frontend/js_src/lib/components/BatchEdit/__tests__/index.test.ts b/specifyweb/frontend/js_src/lib/components/BatchEdit/__tests__/index.test.ts new file mode 100644 index 00000000000..21f43c77a05 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/BatchEdit/__tests__/index.test.ts @@ -0,0 +1,73 @@ +import { requireContext } from '../../../tests/helpers'; +import type { RA } from '../../../utils/types'; +import { tables } from '../../DataModel/tables'; +import type { QueryField } from '../../QueryBuilder/helpers'; +import type { MappingPath } from '../../WbPlanView/Mapper'; +import { buildBatchEditFromQueryBody } from '../index'; + +requireContext(); + +const queryField = ( + mappingPath: MappingPath, + isDisplay: boolean = true +): QueryField => ({ + id: 0, + mappingPath, + sortType: undefined, + isDisplay, + filters: [], +}); + +/* A query over Collection Object that reaches through a to-one relationship + * (accession), a to-many one (accession agents), and an unchecked field. + */ +const fields: RA = [ + queryField(['catalogNumber']), + queryField(['guid'], false), // Unchecked in the query builder + queryField(['accession', 'accessionNumber']), + queryField(['accession', 'accessionAgents', '#1', 'role']), +]; + +const buildBody = ( + hasRelationships: boolean, + extraFields: RA = fields +) => + buildBatchEditFromQueryBody({ + query: new tables.SpQuery.Resource({ name: 'Test Query' }), + limit: 5000, + fields: extraFields, + baseTableName: 'CollectionObject', + dataSetName: 'Test Query - Fri Sep 11 2026', + recordSetId: 42, + treeDefsFilter: {}, + hasRelationships, + }); + +describe('buildBatchEditFromQueryBody', () => { + test('captions describe each displayed field with relationships', () => { + expect(buildBody(true).captions).toEqual([ + 'Cat #', + 'Accession #', + 'Accession Agents - Role', + ]); + }); + + test('fields hidden in the query builder get no caption', () => { + const captions = buildBody(true).captions; + expect(captions).toHaveLength(3); + expect(captions).not.toContain('Collection Object - GUID'); + }); + + // Covers front end half of verifying relationships are not editable. + test('omitRelationships is the inverse of the preference', () => { + expect(buildBody(true).omitrelationships).toBe(false); + expect(buildBody(false).omitrelationships).toBe(true); + }); + + test('carries the data set name, limit and record set through', () => { + const body = buildBody(true); + expect(body.name).toBe('Test Query - Fri Sep 11 2026'); + expect(body.limit).toBe(5000); + expect(body.recordsetid).toBe(42); + }); +}); diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/__tests__/datasetVariants.test.ts b/specifyweb/frontend/js_src/lib/components/WbUtils/__tests__/datasetVariants.test.ts new file mode 100644 index 00000000000..8a52e434f7f --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/__tests__/datasetVariants.test.ts @@ -0,0 +1,109 @@ +import { hasPermission } from '../../Permissions/helpers'; +import { userPreferences } from '../../Preferences/userPreferences'; +import { datasetVariants } from '../datasetVariants'; + +jest.mock('../../Permissions/helpers', () => ({ + hasPermission: jest.fn(), +})); + +const mockedHasPermission = hasPermission as jest.Mock; + +const setBatchEditPreferences = ( + enableRelationships: boolean, + showRollback: boolean +): void => { + userPreferences.set( + 'batchEdit', + 'editor', + 'enableRelationships', + enableRelationships + ); + userPreferences.set('batchEdit', 'editor', 'showRollback', showRollback); +}; + +// Drop every explicitly set preference so lookups fall back to the defaults +const resetPreferences = (): void => userPreferences.setRaw({}); + +beforeAll(() => { + jest.useFakeTimers(); +}); + +afterAll(() => { + jest.useRealTimers(); +}); + +describe('batch edit rollback availability', () => { + test.each([ + [false, true, true, true], + [true, true, true, false], + [false, false, true, false], + [false, true, false, false], + [true, false, true, false], + [true, true, false, false], + [false, false, false, false], + [true, false, false, false], + ])( + 'enableRelationships=%s showRollback=%s permission=%s -> canUndo=%s', + (enableRelationships, showRollback, permission, expected) => { + setBatchEditPreferences(enableRelationships, showRollback); + mockedHasPermission.mockReturnValue(permission); + + expect(datasetVariants.batchEdit.canUndo()).toBe(expected); + } + ); + + test('checks the batch edit rollback permission', () => { + setBatchEditPreferences(false, true); + mockedHasPermission.mockReturnValue(true); + + datasetVariants.batchEdit.canUndo(); + + expect(mockedHasPermission).toHaveBeenCalledWith( + '/batch_edit/dataset', + 'rollback' + ); + }); + + test('does not consult permissions when relationships are enabled', () => { + setBatchEditPreferences(true, true); + mockedHasPermission.mockReturnValue(true); + + expect(datasetVariants.batchEdit.canUndo()).toBe(false); + expect(mockedHasPermission).not.toHaveBeenCalled(); + }); + + test('rollback is hidden based off default preferences', () => { + resetPreferences(); + + expect( + userPreferences.definition('batchEdit', 'editor', 'enableRelationships') + .defaultValue + ).toBe(true); + expect( + userPreferences.definition('batchEdit', 'editor', 'showRollback') + .defaultValue + ).toBe(true); + + // Unset preferences resolve to the declared defaults + expect( + userPreferences.get('batchEdit', 'editor', 'enableRelationships') + ).toBe(true); + expect(userPreferences.get('batchEdit', 'editor', 'showRollback')).toBe( + true + ); + + expect(datasetVariants.batchEdit.canUndo()).toBe(false); + expect(mockedHasPermission).not.toHaveBeenCalled(); + }); + + test('workbench rollback is not affected by batch edit preferences', () => { + setBatchEditPreferences(true, true); + mockedHasPermission.mockReturnValue(true); + + expect(datasetVariants.workbench.canUndo()).toBe(true); + expect(mockedHasPermission).toHaveBeenCalledWith( + '/workbench/dataset', + 'unupload' + ); + }); +}); \ No newline at end of file