Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/embedded/hooks/useEmbeddedView/useEmbeddedView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Iterable } from '../../../core/classes/Iterable';
import { IterableEmbeddedViewType } from '../../enums';
import type { IterableEmbeddedComponentProps } from '../../types/IterableEmbeddedComponentProps';
import type { IterableEmbeddedMessageElementsButton } from '../../types/IterableEmbeddedMessageElementsButton';
import { normalizeEmbeddedViewConfig } from '../../utils/normalizeEmbeddedViewConfig';
import { getMedia } from './getMedia';
import { getStyles } from './getStyles';

Expand Down Expand Up @@ -44,9 +45,14 @@ export const useEmbeddedView = (
onMessageClick = noop,
}: IterableEmbeddedComponentProps
) => {
const normalizedConfig = useMemo(
() => normalizeEmbeddedViewConfig(config),
[config]
);

const parsedStyles = useMemo(() => {
return getStyles(viewType, config);
}, [viewType, config]);
return getStyles(viewType, normalizedConfig);
}, [viewType, normalizedConfig]);
const media = useMemo(() => {
return getMedia(viewType, message);
}, [viewType, message]);
Expand Down
106 changes: 106 additions & 0 deletions src/embedded/utils/normalizeEmbeddedViewConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { normalizeEmbeddedViewConfig } from './normalizeEmbeddedViewConfig';

describe('normalizeEmbeddedViewConfig', () => {
let warnSpy: jest.SpyInstance;

beforeEach(() => {
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
});

afterEach(() => {
warnSpy.mockRestore();
});

it('returns null or undefined unchanged', () => {
expect(normalizeEmbeddedViewConfig(null)).toBeNull();
expect(normalizeEmbeddedViewConfig(undefined)).toBeUndefined();
});

it('parses numeric strings for borderWidth and borderCornerRadius', () => {
const input = {
borderWidth: '45',
borderCornerRadius: '12.5',
backgroundColor: '#fff',
};

// Runtime JSON / native payloads may use strings for numeric fields.
const result = normalizeEmbeddedViewConfig(input as never);

expect(result).toEqual({
borderWidth: 45,
borderCornerRadius: 12.5,
backgroundColor: '#fff',
});
expect(warnSpy).not.toHaveBeenCalled();
});

it('trims whitespace before parsing numeric strings', () => {
const result = normalizeEmbeddedViewConfig({
borderWidth: ' 8 ',
} as never);

expect(result?.borderWidth).toBe(8);
expect(warnSpy).not.toHaveBeenCalled();
});

it('leaves valid numbers unchanged', () => {
const result = normalizeEmbeddedViewConfig({
borderWidth: 3,
borderCornerRadius: 0,
});

expect(result?.borderWidth).toBe(3);
expect(result?.borderCornerRadius).toBe(0);
expect(warnSpy).not.toHaveBeenCalled();
});

it('drops non-parsable strings and warns', () => {
const result = normalizeEmbeddedViewConfig({
borderWidth: 'nope',
borderCornerRadius: '10',
} as never);

expect(result?.borderWidth).toBeUndefined();
expect(result?.borderCornerRadius).toBe(10);
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy.mock.calls[0][0]).toContain('borderWidth');
});

it('drops empty strings and warns', () => {
const result = normalizeEmbeddedViewConfig({
borderWidth: ' ',
} as never);

expect(result?.borderWidth).toBeUndefined();
expect(warnSpy).toHaveBeenCalled();
});

it('drops NaN and Infinity numbers and warns', () => {
const result = normalizeEmbeddedViewConfig({
borderWidth: Number.NaN,
borderCornerRadius: Number.POSITIVE_INFINITY,
} as never);

expect(result?.borderWidth).toBeUndefined();
expect(result?.borderCornerRadius).toBeUndefined();
expect(warnSpy).toHaveBeenCalledTimes(2);
});

it('drops invalid types and warns', () => {
const result = normalizeEmbeddedViewConfig({
borderWidth: true,
} as never);

expect(result?.borderWidth).toBeUndefined();
expect(warnSpy).toHaveBeenCalled();
});

it('does not mutate the original config object', () => {
const original = { borderWidth: '7' as const };
const snapshot = { ...original };

normalizeEmbeddedViewConfig(original as never);

expect(original).toEqual(snapshot);
});
});
75 changes: 75 additions & 0 deletions src/embedded/utils/normalizeEmbeddedViewConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { IterableEmbeddedViewConfig } from '../types/IterableEmbeddedViewConfig';

const NUMERIC_KEYS: (keyof Pick<
IterableEmbeddedViewConfig,
'borderWidth' | 'borderCornerRadius'
>)[] = ['borderWidth', 'borderCornerRadius'];

function coerceNumericField(
key: 'borderWidth' | 'borderCornerRadius',
value: unknown
): number | undefined {
if (value === undefined || value === null) {
return undefined;
}
if (typeof value === 'number') {
if (Number.isFinite(value)) {
return value;
}
console.warn(
`[IterableEmbeddedView] Ignoring ${String(key)}: expected a finite number, got ${String(value)}`
);
return undefined;
}
if (typeof value === 'string') {
const trimmed = value.trim();
if (trimmed === '') {
console.warn(
`[IterableEmbeddedView] Ignoring ${String(key)}: empty string is not a valid number`
);
return undefined;
}
const n = parseFloat(trimmed);
if (Number.isFinite(n)) {
return n;
}
console.warn(
`[IterableEmbeddedView] Ignoring ${String(key)}: could not parse string as a number: ${JSON.stringify(value)}`
);
return undefined;
}
console.warn(
`[IterableEmbeddedView] Ignoring ${String(key)}: expected number or numeric string, got ${typeof value}`
);
return undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 2 issues:

1. Function with many returns (count = 7): coerceNumericField [qlty:return-statements]


2. Function with high complexity (count = 10): coerceNumericField [qlty:function-complexity]

}

/**
* Returns a shallow copy of config with numeric fields coerced from strings when possible.
* Values that cannot be coerced are omitted so style resolution can fall back to defaults.
*/
export function normalizeEmbeddedViewConfig(
config: IterableEmbeddedViewConfig | null | undefined
): IterableEmbeddedViewConfig | null | undefined {
if (config == null) {
return config;
}
const next: IterableEmbeddedViewConfig = { ...config };
const loose = config as Record<string, unknown>;
for (const key of NUMERIC_KEYS) {
const raw = loose[key as string];
if (raw === undefined) {
continue;
}
if (typeof raw === 'number' && Number.isFinite(raw)) {
continue;
}
const coerced = coerceNumericField(key, raw);
if (coerced === undefined) {
delete next[key];
} else {
next[key] = coerced;
}
}
return next;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with high complexity (count = 12): normalizeEmbeddedViewConfig [qlty:function-complexity]

}
Loading