From 62c27033e34584da0a7fb488e1aff23f3b034e6c Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Fri, 20 Mar 2026 14:03:34 -0700 Subject: [PATCH 1/6] arrayEquals: fix edge cases --- .../src/internal/util/utils.test.ts | 27 +++++++++++++++++++ .../components/src/internal/util/utils.ts | 19 ++++++++++--- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/packages/components/src/internal/util/utils.test.ts b/packages/components/src/internal/util/utils.test.ts index ec4d3963e8..39ebcdc487 100644 --- a/packages/components/src/internal/util/utils.test.ts +++ b/packages/components/src/internal/util/utils.test.ts @@ -1588,6 +1588,7 @@ describe('arrayEquals', () => { test('ignore order, case sensitive', () => { expect(arrayEquals(undefined, undefined)).toBeTruthy(); expect(arrayEquals(undefined, null)).toBeTruthy(); + expect(arrayEquals(null, undefined)).toBeTruthy(); expect(arrayEquals([], [])).toBeTruthy(); expect(arrayEquals(null, [])).toBeFalsy(); expect(arrayEquals(['a'], null)).toBeFalsy(); @@ -1621,6 +1622,32 @@ describe('arrayEquals', () => { expect(arrayEquals(['a', 'b'], ['A', 'b'], false)).toBeFalsy(); expect(arrayEquals(['a', 'b'], ['B', 'A'], false)).toBeFalsy(); }); + + test('does not mutate original arrays', () => { + const arrA = ['b', 'a']; + const arrB = ['a', 'b']; + arrayEquals(arrA, arrB, true); + expect(arrA[0]).toBe('b'); + expect(arrB[0]).toBe('a'); + }); + + test('handles delimiter collision (Accuracy Check)', () => { + const arrA = ['a;b', 'c']; + const arrB = ['a', 'b;c']; + expect(arrayEquals(arrA, arrB)).toBeFalsy(); + }); + + test('handles numeric-string collisions', () => { + const arrA = ['1', '23']; + const arrB = ['12', '3']; + expect(arrayEquals(arrA, arrB)).toBeFalsy(); + }); + + test('handles duplicate elements correctly with ignoreOrder', () => { + const arrA = ['a', 'a', 'b']; + const arrB = ['a', 'b', 'b']; + expect(arrayEquals(arrA, arrB, true)).toBeFalsy(); + }); }); describe('getValueFromRow', () => { diff --git a/packages/components/src/internal/util/utils.ts b/packages/components/src/internal/util/utils.ts index f1efe6d3fd..22033004a1 100644 --- a/packages/components/src/internal/util/utils.ts +++ b/packages/components/src/internal/util/utils.ts @@ -748,16 +748,27 @@ export function isQuotedWithDelimiters(value: any, delimiter: string): boolean { return strVal.startsWith('"') && strVal.endsWith('"'); } -export function arrayEquals(a: string[], b: string[], ignoreOrder = true, caseInsensitive?: boolean): boolean { +export function arrayEquals( + a: null | string[] | undefined, + b: null | string[] | undefined, + ignoreOrder = true, + caseInsensitive = false +): boolean { if (a === b) return true; if (a == null && b == null) return true; if (a == null || b == null) return false; if (a.length !== b.length) return false; - const aStr = ignoreOrder ? a.sort().join(';') : a.join(';'); - const bStr = ignoreOrder ? b.sort().join(';') : b.join(';'); + const normalize = (s: string) => (caseInsensitive ? s.toLowerCase() : s); - return caseInsensitive ? aStr.toLowerCase() === bStr.toLowerCase() : aStr === bStr; + if (ignoreOrder) { + // Use a copy to avoid mutating the original arrays + const aSorted = [...a].map(normalize).sort(); + const bSorted = [...b].map(normalize).sort(); + return aSorted.every((val, index) => val === bSorted[index]); + } + + return a.every((val, index) => normalize(val) === normalize(b[index])); } export function getValueFromRow(row: Record, col: string): number | string { From ed975ca667ef0d99ec72fce13fa59eb4aff41ada Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Fri, 20 Mar 2026 14:04:08 -0700 Subject: [PATCH 2/6] extractChanges: support column.jsonType "array" --- .../components/forms/detail/utils.test.ts | 53 ++++++++++++++----- .../internal/components/forms/detail/utils.ts | 9 ++++ 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/packages/components/src/internal/components/forms/detail/utils.test.ts b/packages/components/src/internal/components/forms/detail/utils.test.ts index 582c16ca92..3f5f5b82d5 100644 --- a/packages/components/src/internal/components/forms/detail/utils.test.ts +++ b/packages/components/src/internal/components/forms/detail/utils.test.ts @@ -1,4 +1,4 @@ -import { fromJS } from 'immutable'; +import { fromJS, List } from 'immutable'; import { QueryColumn } from '../../../../public/QueryColumn'; import { QueryInfo } from '../../../../public/QueryInfo'; @@ -26,13 +26,21 @@ const COLUMN_FILE_INPUT = new QueryColumn({ inputType: 'file', jsonType: 'string', }); +const COLUMN_ARRAY_INPUT = new QueryColumn({ + fieldKey: 'arrInput', + name: 'arrInput', + fieldKeyArray: ['arrInput'], + inputType: 'text', + jsonType: 'array', +}); const QUERY_INFO = QueryInfo.fromJsonForTests({ name: 'test', schemaName: 'schema', columns: { - strInput: COLUMN_STRING_INPUT, + arrInput: COLUMN_ARRAY_INPUT, dtInput: COLUMN_DATE_INPUT, fileInput: COLUMN_FILE_INPUT, + strInput: COLUMN_STRING_INPUT, }, }); @@ -40,10 +48,10 @@ describe('extractChanges', () => { test('file input', () => { const FILE = new File([], 'file'); const currentData = fromJS({ fileInput: { value: FILE } }); - expect(extractChanges(QUERY_INFO, currentData, {}).fileInput).toBe(undefined); + expect(extractChanges(QUERY_INFO, currentData, {}).fileInput).toBeUndefined(); expect(extractChanges(QUERY_INFO, currentData, { fileInput: undefined }).fileInput).toBeUndefined(); expect(extractChanges(QUERY_INFO, currentData, { fileInput: FILE }).fileInput).toBeUndefined(); - expect(extractChanges(QUERY_INFO, currentData, { fileInput: null }).fileInput).toBe(null); + expect(extractChanges(QUERY_INFO, currentData, { fileInput: null }).fileInput).toBeNull(); expect( extractChanges(QUERY_INFO, currentData, { fileInput: new File([], 'fileEdit') }).fileInput ).toBeDefined(); @@ -51,22 +59,22 @@ describe('extractChanges', () => { test('string input', () => { const currentData = fromJS({ strInput: { value: 'abc' } }); - expect(extractChanges(QUERY_INFO, currentData, {}).strInput).toBe(undefined); - expect(extractChanges(QUERY_INFO, currentData, { strInput: undefined }).strInput).toBe(null); - expect(extractChanges(QUERY_INFO, currentData, { strInput: null }).strInput).toBe(null); + expect(extractChanges(QUERY_INFO, currentData, {}).strInput).toBeUndefined(); + expect(extractChanges(QUERY_INFO, currentData, { strInput: undefined }).strInput).toBeNull(); + expect(extractChanges(QUERY_INFO, currentData, { strInput: null }).strInput).toBeNull(); expect(extractChanges(QUERY_INFO, currentData, { strInput: '' }).strInput).toBe(''); expect(extractChanges(QUERY_INFO, currentData, { strInput: [] }).strInput).toStrictEqual([]); - expect(extractChanges(QUERY_INFO, currentData, { strInput: 'abc' }).strInput).toBe(undefined); - expect(extractChanges(QUERY_INFO, currentData, { strInput: ' abc ' }).strInput).toBe(undefined); + expect(extractChanges(QUERY_INFO, currentData, { strInput: 'abc' }).strInput).toBeUndefined(); + expect(extractChanges(QUERY_INFO, currentData, { strInput: ' abc ' }).strInput).toBeUndefined(); expect(extractChanges(QUERY_INFO, currentData, { strInput: ' abcd ' }).strInput).toBe('abcd'); }); test('date input', () => { let currentData = fromJS({ dtInput: { value: '2022-08-30 01:02:03' } }); - expect(extractChanges(QUERY_INFO, currentData, {}).dtInput).toBe(undefined); - expect(extractChanges(QUERY_INFO, currentData, { dtInput: undefined }).dtInput).toBe(null); - expect(extractChanges(QUERY_INFO, currentData, { dtInput: null }).dtInput).toBe(null); - expect(extractChanges(QUERY_INFO, currentData, { dtInput: '2022-08-30 01:02:03' }).dtInput).toBe(undefined); + expect(extractChanges(QUERY_INFO, currentData, {}).dtInput).toBeUndefined(); + expect(extractChanges(QUERY_INFO, currentData, { dtInput: undefined }).dtInput).toBeNull(); + expect(extractChanges(QUERY_INFO, currentData, { dtInput: null }).dtInput).toBeNull(); + expect(extractChanges(QUERY_INFO, currentData, { dtInput: '2022-08-30 01:02:03' }).dtInput).toBeUndefined(); expect(extractChanges(QUERY_INFO, currentData, { dtInput: '2022-08-30 01:02:04' }).dtInput).toBe( '2022-08-30 01:02:04' ); // Issue 40139, 52536: date comparison only down to minute precision @@ -81,10 +89,27 @@ describe('extractChanges', () => { ); currentData = fromJS({ dtInput: { value: '2022-08-30' } }); - expect(extractChanges(QUERY_INFO, currentData, { dtInput: '2022-08-30' }).dtInput).toBe(undefined); + expect(extractChanges(QUERY_INFO, currentData, { dtInput: '2022-08-30' }).dtInput).toBeUndefined(); expect(extractChanges(QUERY_INFO, currentData, { dtInput: '2022-08-31' }).dtInput).toBe('2022-08-31'); expect(extractChanges(QUERY_INFO, currentData, { dtInput: '2022-08-30 01:02:03' }).dtInput).toBe( '2022-08-30 01:02:03' ); }); + + test('array input', () => { + // The existing value is an Immutable List + const currentDataList = fromJS({ arrInput: { value: List([1, 2, 3]) } }); + expect(extractChanges(QUERY_INFO, currentDataList, { arrInput: [1, 2, 3] }).arrInput).toBeUndefined(); + expect(extractChanges(QUERY_INFO, currentDataList, { arrInput: [1, 2, 4] }).arrInput).toEqual([1, 2, 4]); + expect(extractChanges(QUERY_INFO, currentDataList, { arrInput: [1, 2] }).arrInput).toEqual([1, 2]); + + // Existing value is a raw JavaScript array + const currentDataRaw = fromJS({ arrInput: { value: [10, 20] } }); + expect(extractChanges(QUERY_INFO, currentDataRaw, { arrInput: [10, 20] }).arrInput).toBeUndefined(); + expect(extractChanges(QUERY_INFO, currentDataRaw, { arrInput: [10, 20, 30] }).arrInput).toEqual([10, 20, 30]); + + // Nulls and Undefined + expect(extractChanges(QUERY_INFO, currentDataList, { arrInput: null }).arrInput).toBeNull(); + expect(extractChanges(QUERY_INFO, currentDataList, { arrInput: undefined }).arrInput).toBeNull(); + }); }); diff --git a/packages/components/src/internal/components/forms/detail/utils.ts b/packages/components/src/internal/components/forms/detail/utils.ts index 05d81e5275..4885f3f6a5 100644 --- a/packages/components/src/internal/components/forms/detail/utils.ts +++ b/packages/components/src/internal/components/forms/detail/utils.ts @@ -2,6 +2,7 @@ import { List, Map } from 'immutable'; import { Utils } from '@labkey/api'; import { QueryInfo } from '../../../../public/QueryInfo'; +import { isSetEqual } from '../../../util/utils'; function arrayListIsEqual(valueArr: Array, nestedModelList: List>): boolean { let matched = 0; @@ -92,6 +93,14 @@ export function extractChanges( if (existingValue === newValue) { return false; } + } else if (column?.jsonType === 'array') { + if (Array.isArray(newValue)) { + const existingArray = List.isList(existingValue) ? existingValue.toJS() : existingValue; + + if (Array.isArray(existingArray) && isSetEqual(newValue, existingArray)) { + return false; + } + } } changedValues[col.name] = newValue === undefined ? null : newValue; From 6cc5f3bcde7ea3f763d533ab78cbcf131ac179db Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Fri, 20 Mar 2026 14:04:41 -0700 Subject: [PATCH 3/6] EditableDetailPanel: refactor to compare against model with update columns --- .../public/QueryModel/EditableDetailPanel.tsx | 248 ++++++++++-------- 1 file changed, 144 insertions(+), 104 deletions(-) diff --git a/packages/components/src/public/QueryModel/EditableDetailPanel.tsx b/packages/components/src/public/QueryModel/EditableDetailPanel.tsx index 6bbe16abfb..81ad74d57a 100644 --- a/packages/components/src/public/QueryModel/EditableDetailPanel.tsx +++ b/packages/components/src/public/QueryModel/EditableDetailPanel.tsx @@ -1,4 +1,4 @@ -import React, { FC, ReactNode, useCallback, useState } from 'react'; +import React, { FC, ReactNode, useCallback, useMemo, useState } from 'react'; import { fromJS } from 'immutable'; import { Query } from '@labkey/api'; @@ -20,7 +20,8 @@ import { useAppContext } from '../../internal/AppContext'; import { QueryModel } from './QueryModel'; -import { DetailPanel, DetailPanelWithModel } from './DetailPanel'; +import { DetailPanel } from './DetailPanel'; +import { InjectedQueryModels, withQueryModels } from './withQueryModels'; import { EDIT_METHOD } from '../../internal/constants'; import { useRouteLeave } from '../../internal/util/RouteLeave'; @@ -47,38 +48,42 @@ export interface EditableDetailPanelProps { title?: string; } -export const EditableDetailPanel: FC = props => { +interface EditingFormProps extends Omit { + onCancel: () => void; +} + +const EditingFormImpl: FC = props => { const { + asSubPanel, + canUpdate, + containerFilter, containerPath, - model, + detailEditRenderer, + detailRenderer, + disabled, + editColumns, + internalSpacesWarningFieldKeys, + onAdditionalFormDataChange, onBeforeUpdate, + onCancel, onCommentChange, onEditToggle, onUpdate, - appEditable, - containerFilter, - disabled, - detailEditRenderer, - detailHeader, - detailRenderer, - internalSpacesWarningFieldKeys, - asSubPanel, - canUpdate, - editColumns, - queryColumns, + queryModels, submitText = 'Save', title, - onAdditionalFormDataChange, } = props; + const editModel = queryModels.model; const { api } = useAppContext(); const [_, setIsDirty] = useRouteLeave(); const [canSubmit, setCanSubmit] = useState(false); - const [editing, setEditing] = useState(false); const [error, setError] = useState(undefined); const [warning, setWarning] = useState(undefined); const [comment, setComment] = useState(); const { requiresUserComment } = useDataChangeCommentsRequired(); + const hasValidUserComment = comment?.trim()?.length > 0; + const _onCommentChange = useCallback( _comment => { setComment(_comment); @@ -87,17 +92,6 @@ export const EditableDetailPanel: FC = props => { [onCommentChange] ); - const hasValidUserComment = comment?.trim()?.length > 0; - - const toggleEditing = useCallback((): void => { - const updated = !editing; - setEditing(updated); - setIsDirty(false); - setWarning(undefined); - setError(undefined); - onEditToggle?.(updated); - }, [editing, onEditToggle, setIsDirty]); - const disableSubmitButton = useCallback((): void => { setCanSubmit(false); }, []); @@ -120,9 +114,9 @@ export const EditableDetailPanel: FC = props => { const handleSubmit = useCallback( async (values: Record): Promise => { - const { queryInfo } = model; - const row = model.getRow(); - const updatedValues = extractChanges(queryInfo, fromJS(model.getRow()), values); + const { queryInfo } = editModel; + const row = editModel.getRow(); + const updatedValues = extractChanges(queryInfo, fromJS(editModel.getRow()), values); if (Object.keys(updatedValues).length === 0) { setCanSubmit(false); @@ -155,7 +149,7 @@ export const EditableDetailPanel: FC = props => { }); setIsDirty(false); - setEditing(false); + onCancel(); onUpdate?.(); onEditToggle?.(false); } catch (e) { @@ -163,41 +157,24 @@ export const EditableDetailPanel: FC = props => { setWarning(undefined); } }, - [model, onBeforeUpdate, api.query, containerPath, comment, onUpdate, onEditToggle, setIsDirty] + [api.query, comment, containerPath, editModel, onBeforeUpdate, onCancel, onEditToggle, onUpdate, setIsDirty] ); - const isEditable = !model.isLoading && model.hasRows && (model.queryInfo?.isAppEditable() || appEditable); + return ( + +
+ - const panel = ( -
- - -
-
- {error && {error}} +
+
+ {error && {error}} - {!editing && (detailHeader ?? null)} - - {!editing && ( - )} - - {/* When editing load a model that includes the update columns and editing mode rendering */} - {editing && ( - = props => { editingMode fileInputRenderer={fileInputRenderer} internalSpacesWarningFieldKeys={internalSpacesWarningFieldKeys} + model={editModel} onAdditionalFormDataChange={onAdditionalFormDataChange} - queryConfig={{ - ...model.queryConfig, - // Issue 46478: Include update columns in request columns to ensure values are available - requiredColumns: model.requiredColumns.concat( - model.updateColumns.map(col => col.fieldKey) - ), - }} /> - )} +
-
+ + + + + + + + {asSubPanel &&
} + + ); +}; + +const EditingFormWithModels = withQueryModels(EditingFormImpl); + +// Lazy wrapper: only mounted when editing, builds the edit-mode queryConfig and key +const EditingForm: FC = props => { + const { model } = props; + const queryConfig = useMemo( + () => ({ + ...model.queryConfig, + // Issue 46478: Include update columns in request columns to ensure values are available + requiredColumns: model.requiredColumns.concat(model.updateColumns.map(col => col.fieldKey)), + }), + [model] ); + const queryConfigs = useMemo(() => ({ model: queryConfig }), [queryConfig]); + const { keyValue, schemaQuery } = queryConfig; + const { schemaName, queryName } = schemaQuery; + // Key ensures we re-mount when the queryConfig identity changes + const key = `${schemaName}.${queryName}.${keyValue}`; + + return ; +}; + +export const EditableDetailPanel: FC = props => { + const { + appEditable, + canUpdate, + containerFilter, + containerPath, + detailHeader, + detailRenderer, + model, + onEditToggle, + queryColumns, + title, + } = props; + + const [_, setIsDirty] = useRouteLeave(); + const [editing, setEditing] = useState(false); + + const toggleEditing = useCallback((): void => { + setEditing(true); + setIsDirty(false); + onEditToggle?.(true); + }, [onEditToggle, setIsDirty]); + + const handleCancel = useCallback((): void => { + setEditing(false); + setIsDirty(false); + onEditToggle?.(false); + }, [onEditToggle, setIsDirty]); + + const isEditable = !model.isLoading && model.hasRows && (model.queryInfo?.isAppEditable() || appEditable); if (editing) { - return ( - - {panel} - - - - - - - - {asSubPanel &&
} - - ); + return ; } - return panel; + return ( +
+ + +
+
+ {detailHeader ?? null} + + +
+
+
+ ); }; EditableDetailPanel.displayName = 'EditableDetailPanel'; From 314fe4d1e2f94fc039bf52c79b60ca7af7539f92 Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Fri, 20 Mar 2026 14:06:09 -0700 Subject: [PATCH 4/6] 7.23.4-fb-mv-edit-960.0 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 4451bc391f..d9edb086c0 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.23.3", + "version": "7.23.4-fb-mv-edit-960.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.23.3", + "version": "7.23.4-fb-mv-edit-960.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index eedec7b0d9..0dff40a617 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.23.3", + "version": "7.23.4-fb-mv-edit-960.0", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From a80da99a8734e9599ad13151c25048d953916d4d Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Fri, 20 Mar 2026 14:27:10 -0700 Subject: [PATCH 5/6] Remove comments --- .../components/src/public/QueryModel/EditableDetailPanel.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/components/src/public/QueryModel/EditableDetailPanel.tsx b/packages/components/src/public/QueryModel/EditableDetailPanel.tsx index 81ad74d57a..80e3dbc32c 100644 --- a/packages/components/src/public/QueryModel/EditableDetailPanel.tsx +++ b/packages/components/src/public/QueryModel/EditableDetailPanel.tsx @@ -217,7 +217,6 @@ const EditingFormImpl: FC = props => { const EditingFormWithModels = withQueryModels(EditingFormImpl); -// Lazy wrapper: only mounted when editing, builds the edit-mode queryConfig and key const EditingForm: FC = props => { const { model } = props; const queryConfig = useMemo( @@ -231,7 +230,6 @@ const EditingForm: FC = props => { const queryConfigs = useMemo(() => ({ model: queryConfig }), [queryConfig]); const { keyValue, schemaQuery } = queryConfig; const { schemaName, queryName } = schemaQuery; - // Key ensures we re-mount when the queryConfig identity changes const key = `${schemaName}.${queryName}.${keyValue}`; return ; From 5f851b39c26d8c1cc34187c9409d71dd5d8ac5aa Mon Sep 17 00:00:00 2001 From: labkey-nicka Date: Fri, 20 Mar 2026 14:34:01 -0700 Subject: [PATCH 6/6] test updates --- .../src/internal/util/utils.test.ts | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/components/src/internal/util/utils.test.ts b/packages/components/src/internal/util/utils.test.ts index 39ebcdc487..4f6336b8b3 100644 --- a/packages/components/src/internal/util/utils.test.ts +++ b/packages/components/src/internal/util/utils.test.ts @@ -1631,55 +1631,49 @@ describe('arrayEquals', () => { expect(arrB[0]).toBe('a'); }); - test('handles delimiter collision (Accuracy Check)', () => { - const arrA = ['a;b', 'c']; - const arrB = ['a', 'b;c']; - expect(arrayEquals(arrA, arrB)).toBeFalsy(); + test('handles delimiter collision', () => { + expect(arrayEquals(['a;b', 'c'], ['a', 'b;c'])).toBeFalsy(); }); test('handles numeric-string collisions', () => { - const arrA = ['1', '23']; - const arrB = ['12', '3']; - expect(arrayEquals(arrA, arrB)).toBeFalsy(); + expect(arrayEquals(['1', '23'], ['12', '3'])).toBeFalsy(); }); test('handles duplicate elements correctly with ignoreOrder', () => { - const arrA = ['a', 'a', 'b']; - const arrB = ['a', 'b', 'b']; - expect(arrayEquals(arrA, arrB, true)).toBeFalsy(); + expect(arrayEquals(['a', 'a', 'b'], ['a', 'b', 'b'], true)).toBeFalsy(); }); }); describe('getValueFromRow', () => { test('no row', () => { - expect(getValueFromRow(undefined, 'Name')).toEqual(undefined); - expect(getValueFromRow({}, 'Name')).toEqual(undefined); + expect(getValueFromRow(undefined, 'Name')).toBeUndefined(); + expect(getValueFromRow({}, 'Name')).toBeUndefined(); }); test('returns value', () => { const row = { Name: 'test' }; expect(getValueFromRow(row, 'Name')).toEqual('test'); expect(getValueFromRow(row, 'name')).toEqual('test'); - expect(getValueFromRow(row, 'bogus')).toEqual(undefined); + expect(getValueFromRow(row, 'bogus')).toBeUndefined(); }); test('returns value from object', () => { const row = { Name: { value: 'test' } }; expect(getValueFromRow(row, 'Name')).toEqual('test'); expect(getValueFromRow(row, 'name')).toEqual('test'); - expect(getValueFromRow(row, 'bogus')).toEqual(undefined); + expect(getValueFromRow(row, 'bogus')).toBeUndefined(); }); test('returns value from array', () => { const flatRow = { Name: ['test1', 'test2'] }; - expect(getValueFromRow(flatRow, 'Name')).toEqual(undefined); - expect(getValueFromRow(flatRow, 'name')).toEqual(undefined); - expect(getValueFromRow(flatRow, 'bogus')).toEqual(undefined); + expect(getValueFromRow(flatRow, 'Name')).toBeUndefined(); + expect(getValueFromRow(flatRow, 'name')).toBeUndefined(); + expect(getValueFromRow(flatRow, 'bogus')).toBeUndefined(); const nestedRow = { Name: [{ value: 'test1' }, { value: 'test2' }] }; expect(getValueFromRow(nestedRow, 'Name')).toEqual('test1'); expect(getValueFromRow(nestedRow, 'name')).toEqual('test1'); - expect(getValueFromRow(nestedRow, 'bogus')).toEqual(undefined); + expect(getValueFromRow(nestedRow, 'bogus')).toBeUndefined(); }); });