mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-08 07:49:12 +00:00
chore: type improvements (#598)
* chore: type improvements * ci: typechecking in pipeline
This commit is contained in:
@@ -28,19 +28,19 @@ jobs:
|
|||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# Run code quality per package
|
# Run code quality per package
|
||||||
- name: React - Run linter
|
- name: React - Run linter + TypeScript checks
|
||||||
if: always()
|
if: always()
|
||||||
run: pnpm lint
|
run: pnpm lint && tsc --noEmit
|
||||||
working-directory: ./apps/client
|
working-directory: ./apps/client
|
||||||
|
|
||||||
- name: Server - Run linter
|
- name: Server - Run linter + TypeScript checks
|
||||||
if: always()
|
if: always()
|
||||||
run: pnpm lint
|
run: pnpm lint && tsc --noEmit
|
||||||
working-directory: ./apps/server
|
working-directory: ./apps/server
|
||||||
|
|
||||||
- name: Utils - Run linter
|
- name: Utils - Run linter + TypeScript checks
|
||||||
if: always()
|
if: always()
|
||||||
run: pnpm lint
|
run: pnpm lint && tsc --noEmit
|
||||||
working-directory: ./packages/utils
|
working-directory: ./packages/utils
|
||||||
|
|
||||||
- name: Types - Run linter
|
- name: Types - Run linter
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ interface TextInputProps extends BaseProps {
|
|||||||
isTextArea?: false;
|
isTextArea?: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResizeOptions = 'horizontal' | 'vertical' | 'none';
|
||||||
|
|
||||||
interface TextAreaProps extends BaseProps {
|
interface TextAreaProps extends BaseProps {
|
||||||
isTextArea: true;
|
isTextArea: true;
|
||||||
resize?: 'horizontal' | 'vertical' | 'none';
|
resize?: ResizeOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
type InputProps = TextInputProps | TextAreaProps;
|
type InputProps = TextInputProps | TextAreaProps;
|
||||||
@@ -35,7 +37,7 @@ export default function TextInput(props: InputProps) {
|
|||||||
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
|
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
|
||||||
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
|
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
|
||||||
|
|
||||||
let resize = 'none';
|
let resize: ResizeOptions = 'none';
|
||||||
if (isTextArea) {
|
if (isTextArea) {
|
||||||
resize = (props as TextAreaProps)?.resize ?? 'none';
|
resize = (props as TextAreaProps)?.resize ?? 'none';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-nocheck -- working on it
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { OSCSettings } from 'ontime-types';
|
import { OSCSettings } from 'ontime-types';
|
||||||
|
|
||||||
|
|||||||
@@ -9,21 +9,21 @@ const cachedRundownPlaceholder = { rundown: [], revision: -1 };
|
|||||||
|
|
||||||
// TODO: can we leverage structural sharing to see if data has changed?
|
// TODO: can we leverage structural sharing to see if data has changed?
|
||||||
export default function useRundown() {
|
export default function useRundown() {
|
||||||
return useQuery<GetRundownCached>({
|
const { data, status, isError, refetch, isFetching } = useQuery<GetRundownCached>({
|
||||||
queryKey: RUNDOWN,
|
queryKey: RUNDOWN,
|
||||||
queryFn: fetchCachedRundown,
|
queryFn: fetchCachedRundown,
|
||||||
placeholderData: cachedRundownPlaceholder,
|
placeholderData: cachedRundownPlaceholder,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
select: (data) => data.rundown,
|
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchInterval,
|
refetchInterval: queryRefetchInterval,
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
// structuralSharing: (oldData: GetRundownCached | undefined, newData: GetRundownCached) => {
|
// structuralSharing: (oldData: GetRundownCached | undefined, newData: GetRundownCached) => {
|
||||||
// if (oldData === undefined) {
|
// if (oldData === undefined) {
|
||||||
// cachedRundownPlaceholder;
|
// return cachedRundownPlaceholder;
|
||||||
// }
|
// }
|
||||||
// const hasDataChanged = oldData?.revision === newData.revision;
|
// const hasDataChanged = oldData?.revision === newData.revision;
|
||||||
// return hasDataChanged ? oldData : newData;
|
// return hasDataChanged ? oldData : newData;
|
||||||
// },
|
// },
|
||||||
});
|
});
|
||||||
|
return { data: data?.rundown ?? [], status, isError, refetch, isFetching };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-nocheck -- working on it
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
interface WebkitDocument extends Document {
|
interface WebkitDocument extends Document {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo } from 'react';
|
import { memo, ReactNode } from 'react';
|
||||||
import { Button, Checkbox, Switch } from '@chakra-ui/react';
|
import { Button, Checkbox, Switch } from '@chakra-ui/react';
|
||||||
import { Column } from '@tanstack/react-table';
|
import { Column } from '@tanstack/react-table';
|
||||||
import { OntimeRundownEntry } from 'ontime-types';
|
import { OntimeRundownEntry } from 'ontime-types';
|
||||||
@@ -44,7 +44,7 @@ function CuesheetTableSettings(props: CuesheetTableSettingsProps) {
|
|||||||
defaultChecked={visible}
|
defaultChecked={visible}
|
||||||
onChange={column.getToggleVisibilityHandler()}
|
onChange={column.getToggleVisibilityHandler()}
|
||||||
/>
|
/>
|
||||||
{columnHeader}
|
{columnHeader as ReactNode}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { millisToString } from 'ontime-utils';
|
|||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const parseField = (field: keyof OntimeRundown, data: unknown): string => {
|
export const parseField = <T extends OntimeEntryCommonKeys>(field: T, data: unknown): string => {
|
||||||
let val;
|
let val;
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case 'timeStart':
|
case 'timeStart':
|
||||||
@@ -96,6 +96,7 @@ export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, userF
|
|||||||
|
|
||||||
rundown.forEach((entry) => {
|
rundown.forEach((entry) => {
|
||||||
const row: string[] = [];
|
const row: string[] = [];
|
||||||
|
// @ts-expect-error -- not sure how to type this
|
||||||
fieldOrder.forEach((field) => row.push(parseField(field, entry[field])));
|
fieldOrder.forEach((field) => row.push(parseField(field, entry[field])));
|
||||||
data.push(row);
|
data.push(row);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-nocheck -- working on it
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { FormControl, Input, Switch } from '@chakra-ui/react';
|
import { FormControl, Input, Switch } from '@chakra-ui/react';
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function AliasesForm() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
reset(data);
|
reset({ aliases: data });
|
||||||
}
|
}
|
||||||
}, [data, reset]);
|
}, [data, reset]);
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default function AppSettingsModal() {
|
|||||||
reset(data);
|
reset(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const disableInputs = status === 'loading';
|
const disableInputs = status === 'pending';
|
||||||
|
|
||||||
if (isFetching) {
|
if (isFetching) {
|
||||||
return <ModalLoader />;
|
return <ModalLoader />;
|
||||||
@@ -87,7 +87,7 @@ export default function AppSettingsModal() {
|
|||||||
description='Protect the editor with a pin code'
|
description='Protect the editor with a pin code'
|
||||||
error={errors.editorKey?.message}
|
error={errors.editorKey?.message}
|
||||||
>
|
>
|
||||||
<ModalPinInput register={register} formName='editorKey' isDisabled={disableInputs} />
|
<ModalPinInput register={register as any} formName='editorKey' isDisabled={disableInputs} />
|
||||||
</ModalSplitInput>
|
</ModalSplitInput>
|
||||||
<ModalSplitInput
|
<ModalSplitInput
|
||||||
field='operatorKey'
|
field='operatorKey'
|
||||||
@@ -95,7 +95,7 @@ export default function AppSettingsModal() {
|
|||||||
description='Protect the cuesheet with a pin code'
|
description='Protect the cuesheet with a pin code'
|
||||||
error={errors.operatorKey?.message}
|
error={errors.operatorKey?.message}
|
||||||
>
|
>
|
||||||
<ModalPinInput register={register} formName='operatorKey' isDisabled={disableInputs} />
|
<ModalPinInput register={register as any} formName='operatorKey' isDisabled={disableInputs} />
|
||||||
</ModalSplitInput>
|
</ModalSplitInput>
|
||||||
<div style={{ height: '16px' }} />
|
<div style={{ height: '16px' }} />
|
||||||
<ModalSplitInput
|
<ModalSplitInput
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function ProjectDataForm() {
|
|||||||
reset(data);
|
reset(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const disableInputs = status === 'loading';
|
const disableInputs = status === 'pending';
|
||||||
|
|
||||||
if (isFetching) {
|
if (isFetching) {
|
||||||
return <ModalLoader />;
|
return <ModalLoader />;
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export default function ViewSettingsForm() {
|
|||||||
<AlertTitle>CSS Override</AlertTitle>
|
<AlertTitle>CSS Override</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Ontime will use the CSS file at its install location. <br />
|
Ontime will use the CSS file at its install location. <br />
|
||||||
<span className={style.url}>{info.cssOverride}</span>
|
<span className={style.url}>{info?.cssOverride}</span>
|
||||||
<ModalLink href={cssOverrideDocsUrl}>For more information, see the docs</ModalLink>
|
<ModalLink href={cssOverrideDocsUrl}>For more information, see the docs</ModalLink>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { EventItemActions } from '../../RundownEntry';
|
|||||||
interface BlockActionMenuProps {
|
interface BlockActionMenuProps {
|
||||||
enableDelete?: boolean;
|
enableDelete?: boolean;
|
||||||
showClone?: boolean;
|
showClone?: boolean;
|
||||||
actionHandler: (action: EventItemActions, payload?: unknown) => void;
|
actionHandler: (action: EventItemActions, payload?: any) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable react/display-name */
|
|
||||||
import { ComponentType, useMemo } from 'react';
|
import { ComponentType, useMemo } from 'react';
|
||||||
import { SupportedEvent } from 'ontime-types';
|
import { TimeManagerType } from 'common/models/TimeManager.type';
|
||||||
|
import { Message, OntimeEvent, ProjectData, SupportedEvent, TimerMessage, ViewSettings } from 'ontime-types';
|
||||||
import { useStore } from 'zustand';
|
import { useStore } from 'zustand';
|
||||||
|
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
@@ -9,8 +9,32 @@ import useViewSettings from '../../common/hooks-query/useViewSettings';
|
|||||||
import { runtime } from '../../common/stores/runtime';
|
import { runtime } from '../../common/stores/runtime';
|
||||||
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
||||||
|
|
||||||
const withData = <P extends object>(Component: ComponentType<P>) => {
|
type WithDataProps = {
|
||||||
return (props: Partial<P>) => {
|
isMirrored: boolean;
|
||||||
|
pres: TimerMessage;
|
||||||
|
publ: Message;
|
||||||
|
lower: Message;
|
||||||
|
eventNow: OntimeEvent | null;
|
||||||
|
publicEventNow: OntimeEvent | null;
|
||||||
|
eventNext: OntimeEvent | null;
|
||||||
|
publicEventNext: OntimeEvent | null;
|
||||||
|
time: TimeManagerType;
|
||||||
|
events: OntimeEvent[];
|
||||||
|
backstageEvents: OntimeEvent[];
|
||||||
|
selectedId: string | null;
|
||||||
|
publicSelectedId: string | null;
|
||||||
|
nextId: string | null;
|
||||||
|
general: ProjectData;
|
||||||
|
viewSettings: ViewSettings;
|
||||||
|
onAir: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDisplayName(Component: React.ComponentType<any>): string {
|
||||||
|
return Component.displayName || Component.name || 'Component';
|
||||||
|
}
|
||||||
|
|
||||||
|
const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
|
||||||
|
const WithDataComponent = (props: P) => {
|
||||||
// persisted app state
|
// persisted app state
|
||||||
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
||||||
|
|
||||||
@@ -84,6 +108,9 @@ const withData = <P extends object>(Component: ComponentType<P>) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
WithDataComponent.displayName = `WithData(${getDisplayName(Component)})`;
|
||||||
|
return WithDataComponent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withData;
|
export default withData;
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ export default function Countdown(props: CountdownProps) {
|
|||||||
<div className='time'>{clock}</div>
|
<div className='time'>{clock}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='status'>{getLocalizedString(`countdown.${runningMessage}`)}</div>
|
{runningMessage !== TimerMessage.unhandled && (
|
||||||
|
<div className='status'>{getLocalizedString(`countdown.${runningMessage}`)}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<span className={`timer ${standby ? 'timer--paused' : ''} ${isRunningFinished ? 'timer--finished' : ''}`}>
|
<span className={`timer ${standby ? 'timer--paused' : ''} ${isRunningFinished ? 'timer--finished' : ''}`}>
|
||||||
{formattedTimer}
|
{formattedTimer}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Alias, DatabaseModel, OntimeRundown, Settings } from 'ontime-types';
|
||||||
import { safeMerge } from '../DataProvider.utils.js';
|
import { safeMerge } from '../DataProvider.utils.js';
|
||||||
|
|
||||||
describe('safeMerge', () => {
|
describe('safeMerge', () => {
|
||||||
@@ -5,8 +6,10 @@ describe('safeMerge', () => {
|
|||||||
rundown: [],
|
rundown: [],
|
||||||
project: {
|
project: {
|
||||||
title: 'existing title',
|
title: 'existing title',
|
||||||
|
description: 'existing description',
|
||||||
publicUrl: 'existing public URL',
|
publicUrl: 'existing public URL',
|
||||||
backstageUrl: 'existing backstageUrl',
|
backstageUrl: 'existing backstageUrl',
|
||||||
|
publicInfo: 'existing backstageInfo',
|
||||||
backstageInfo: 'existing backstageInfo',
|
backstageInfo: 'existing backstageInfo',
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
@@ -42,7 +45,7 @@ describe('safeMerge', () => {
|
|||||||
onFinish: [],
|
onFinish: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
} as DatabaseModel;
|
||||||
|
|
||||||
it('returns existing data if new data is not provided', () => {
|
it('returns existing data if new data is not provided', () => {
|
||||||
const mergedData = safeMerge(existing, undefined);
|
const mergedData = safeMerge(existing, undefined);
|
||||||
@@ -51,7 +54,7 @@ describe('safeMerge', () => {
|
|||||||
|
|
||||||
it('merges the rundown key', () => {
|
it('merges the rundown key', () => {
|
||||||
const newData = {
|
const newData = {
|
||||||
rundown: [{ name: 'item 1' }, { name: 'item 2' }],
|
rundown: [{ title: 'item 1' }, { title: 'item 2' }] as OntimeRundown,
|
||||||
};
|
};
|
||||||
const mergedData = safeMerge(existing, newData);
|
const mergedData = safeMerge(existing, newData);
|
||||||
expect(mergedData.rundown).toEqual(newData.rundown);
|
expect(mergedData.rundown).toEqual(newData.rundown);
|
||||||
@@ -64,9 +67,11 @@ describe('safeMerge', () => {
|
|||||||
publicInfo: 'new public info',
|
publicInfo: 'new public info',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
// @ts-expect-error -- just testing
|
||||||
const mergedData = safeMerge(existing, newData);
|
const mergedData = safeMerge(existing, newData);
|
||||||
expect(mergedData.project).toEqual({
|
expect(mergedData.project).toEqual({
|
||||||
title: 'new title',
|
title: 'new title',
|
||||||
|
description: 'existing description',
|
||||||
publicUrl: 'existing public URL',
|
publicUrl: 'existing public URL',
|
||||||
publicInfo: 'new public info',
|
publicInfo: 'new public info',
|
||||||
backstageUrl: 'existing backstageUrl',
|
backstageUrl: 'existing backstageUrl',
|
||||||
@@ -79,7 +84,7 @@ describe('safeMerge', () => {
|
|||||||
settings: {
|
settings: {
|
||||||
serverPort: 3000,
|
serverPort: 3000,
|
||||||
language: 'pt',
|
language: 'pt',
|
||||||
},
|
} as Settings,
|
||||||
};
|
};
|
||||||
const mergedData = safeMerge(existing, newData);
|
const mergedData = safeMerge(existing, newData);
|
||||||
expect(mergedData.settings).toEqual({
|
expect(mergedData.settings).toEqual({
|
||||||
@@ -108,6 +113,7 @@ describe('safeMerge', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
//@ts-expect-error -- testing partial merge
|
||||||
const mergedData = safeMerge(existing, newData);
|
const mergedData = safeMerge(existing, newData);
|
||||||
expect(mergedData.osc).toEqual({
|
expect(mergedData.osc).toEqual({
|
||||||
portIn: 7777,
|
portIn: 7777,
|
||||||
@@ -135,7 +141,7 @@ describe('safeMerge', () => {
|
|||||||
it('should merge the aliases key when present', () => {
|
it('should merge the aliases key when present', () => {
|
||||||
const existingData = {
|
const existingData = {
|
||||||
rundown: [],
|
rundown: [],
|
||||||
event: {
|
project: {
|
||||||
title: '',
|
title: '',
|
||||||
publicUrl: '',
|
publicUrl: '',
|
||||||
publicInfo: '',
|
publicInfo: '',
|
||||||
@@ -183,10 +189,13 @@ describe('safeMerge', () => {
|
|||||||
onFinish: [],
|
onFinish: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
} as DatabaseModel;
|
||||||
|
|
||||||
const newData = {
|
const newData = {
|
||||||
aliases: ['alias1', 'alias2'],
|
aliases: [
|
||||||
|
{ enabled: true, alias: 'alias1', pathAndParams: '' },
|
||||||
|
{ enabled: true, alias: 'alias2', pathAndParams: '' },
|
||||||
|
] as Alias[],
|
||||||
};
|
};
|
||||||
|
|
||||||
const mergedData = safeMerge(existingData, newData);
|
const mergedData = safeMerge(existingData, newData);
|
||||||
@@ -217,6 +226,7 @@ describe('safeMerge', () => {
|
|||||||
user3: 'David',
|
user3: 'David',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//@ts-expect-error -- testing partial merge
|
||||||
const result = safeMerge(existing, newData);
|
const result = safeMerge(existing, newData);
|
||||||
expect(result.userFields).toEqual(expected);
|
expect(result.userFields).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ describe('mergeObject()', () => {
|
|||||||
third: 'yes',
|
third: 'yes',
|
||||||
};
|
};
|
||||||
const b = {
|
const b = {
|
||||||
first: 0,
|
first: 'no',
|
||||||
second: null,
|
second: null,
|
||||||
third: '',
|
third: '',
|
||||||
};
|
};
|
||||||
const merged = mergeObject(a, b);
|
const merged = mergeObject(a, b);
|
||||||
expect(merged).toStrictEqual({
|
expect(merged).toStrictEqual({
|
||||||
first: 0,
|
first: 'no',
|
||||||
second: null,
|
second: null,
|
||||||
third: '',
|
third: '',
|
||||||
});
|
});
|
||||||
@@ -57,6 +57,7 @@ describe('mergeObject()', () => {
|
|||||||
third: '',
|
third: '',
|
||||||
forth: 'not-this',
|
forth: 'not-this',
|
||||||
};
|
};
|
||||||
|
// @ts-expect-error -- testing changing type
|
||||||
const merged = mergeObject(a, b);
|
const merged = mergeObject(a, b);
|
||||||
expect(merged).toStrictEqual({
|
expect(merged).toStrictEqual({
|
||||||
first: 0,
|
first: 0,
|
||||||
@@ -83,6 +84,7 @@ describe('mergeObject()', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error -- testing missing property
|
||||||
const merged = mergeObject(a, b);
|
const merged = mergeObject(a, b);
|
||||||
|
|
||||||
expect(merged.name).toBe('Doe');
|
expect(merged.name).toBe('Doe');
|
||||||
|
|||||||
Reference in New Issue
Block a user