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);

Comment on lines +26 to +41
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The tests repeatedly cast inputs to never to bypass type checking. That makes the intent harder to follow and can hide mistakes in the test setup; prefer a more explicit cast like unknown as IterableEmbeddedViewConfig (or a small helper type) for invalid-shape inputs.

Copilot uses AI. Check for mistakes.
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;
}
Comment on lines +32 to +35
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

parseFloat() is permissive (e.g., it will accept values like "12px" or "10abc" and return a finite number), which means malformed numeric strings won’t trigger the warning and will be applied as styles. Consider using a stricter conversion (e.g., Number(trimmed) with a full-string numeric check) so only truly numeric strings are coerced and everything else falls back to defaults.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I thought the same as the warning above so that parseFloat("3abc") === 3 but I guess we can live with assuming people will always type 3px instead.

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