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
85 changes: 85 additions & 0 deletions components/MatrixInput.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { MatrixInput } from './MatrixInput';
import React from 'react';

// Mock framer-motion to avoid animation issues in tests
vi.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
h3: ({ children, ...props }: any) => <h3 {...props}>{children}</h3>,
path: ({ children, ...props }: any) => <path {...props}>{children}</path>,
},
AnimatePresence: ({ children }: any) => <>{children}</>,
LayoutGroup: ({ children }: any) => <>{children}</>,
}));

Comment on lines +7 to +16

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

The framer-motion mock spreads all motion props onto real DOM/SVG elements. This can generate noisy React warnings (e.g., non-standard attributes like layout, initial, animate, transition getting attached to <div>/<path>) and may break under stricter React DOM prop validation. Prefer a mock component that only forwards safe props (e.g., children, className, style, on*) and drops motion-specific props.

Suggested change
vi.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
h3: ({ children, ...props }: any) => <h3 {...props}>{children}</h3>,
path: ({ children, ...props }: any) => <path {...props}>{children}</path>,
},
AnimatePresence: ({ children }: any) => <>{children}</>,
LayoutGroup: ({ children }: any) => <>{children}</>,
}));
vi.mock('framer-motion', () => {
const filterProps = (props: any) => {
const safeProps: any = {};
Object.keys(props).forEach((key) => {
if (
key === 'className' ||
key === 'style' ||
key === 'id' ||
key === 'title' ||
key === 'role' ||
key === 'tabIndex' ||
key.startsWith('data-') ||
key.startsWith('aria-') ||
/^on[A-Z]/.test(key)
) {
safeProps[key] = props[key];
}
});
return safeProps;
};
return {
motion: {
div: ({ children, ...props }: any) => (
<div {...filterProps(props)}>{children}</div>
),
h3: ({ children, ...props }: any) => (
<h3 {...filterProps(props)}>{children}</h3>
),
path: ({ children, ...props }: any) => (
// SVG path elements typically don't have children, but we preserve the signature
<path {...filterProps(props)}>{children}</path>
),
},
AnimatePresence: ({ children }: any) => <>{children}</>,
LayoutGroup: ({ children }: any) => <>{children}</>,
};
});

Copilot uses AI. Check for mistakes.
describe('MatrixInput Performance Benchmark', () => {
it('calls onChange only once after typing completes (Optimized)', () => {
const handleChange = vi.fn();
const data = [[0]];

render(
<MatrixInput
data={data}
onChange={handleChange}
editable={true}
/>
);

const input = screen.getByRole('textbox');

// Simulate typing "12.5"
fireEvent.focus(input);
fireEvent.change(input, { target: { value: '1' } });
fireEvent.change(input, { target: { value: '12' } });
fireEvent.change(input, { target: { value: '12.' } });
fireEvent.change(input, { target: { value: '12.5' } });

// Expect NO onChange calls yet (optimization)
expect(handleChange).toHaveBeenCalledTimes(0);

// Trigger blur to commit the change
fireEvent.blur(input);

// Expect onChange to be called exactly once
expect(handleChange).toHaveBeenCalledTimes(1);

// Verify the called value
// data is [[0]], changed 0,0 to 12.5.
// The new data passed to onChange should be [[12.5]]
expect(handleChange).toHaveBeenCalledWith([[12.5]]);
});

it('commits change on Enter key', () => {
const handleChange = vi.fn();
const data = [[0]];

render(
<MatrixInput
data={data}
onChange={handleChange}
editable={true}
/>
);

const input = screen.getByRole('textbox');

fireEvent.focus(input);
fireEvent.change(input, { target: { value: '99' } });

expect(handleChange).toHaveBeenCalledTimes(0);

// Press Enter
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 });

// In JSDOM/Testing Library, e.currentTarget.blur() might not synchronously trigger the React onBlur event
// so we might need to simulate the blur event explicitly if the component relies on the browser's behavior.
// However, let's try to just fire blur manually to simulate the browser behavior that we know happens.
// This confirms that IF blur happens (which we trigger on Enter), THEN logic runs.
fireEvent.blur(input);

Comment on lines +73 to +81

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

The “commits change on Enter key” test doesn’t actually validate the Enter behavior: the explicit fireEvent.blur(input) will commit even if the onKeyDown handler is removed or broken. To make this test meaningful, assert that pressing Enter triggers input.blur() (spy on blur) and/or that focus changes after Enter, then separately fire blur only if needed to simulate the browser event dispatch.

Copilot uses AI. Check for mistakes.
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith([[99]]);
});
});
44 changes: 32 additions & 12 deletions components/MatrixInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,45 @@ export const MatrixInput: React.FC<MatrixProps> = ({
setEditValue({ key: `${r}-${c}`, val: data[r][c].toString() });
};

const handleBlur = () => {
setEditValue(null);
};
const commitEdit = () => {
if (!editValue || !onChange) {
setEditValue(null);
return;
}

const handleChange = (r: number, c: number, val: string) => {
setEditValue({ key: `${r}-${c}`, val });
const [r, c] = editValue.key.split('-').map(Number);
const val = editValue.val;

if (!onChange) return;

// Allow typing "-", ".", etc. without reverting immediately
let newValue = data[r][c];
Comment on lines +24 to +33

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

commitEdit reconstructs the edited coordinates by parsing editValue.key (split + Number). This adds avoidable string parsing and makes the commit path more error-prone (e.g., malformed key => NaN indices => runtime error when indexing data[r][c]). Consider storing { r: number; c: number; val: string } in state instead of { key: string; val: string }, so you can commit without parsing and keep the indices type-safe.

Copilot uses AI. Check for mistakes.

const num = parseFloat(val);
if (!isNaN(num)) {
const newData = data.map(row => [...row]);
newData[r][c] = num;
onChange(newData);
newValue = num;
} else if (val === '') {
newValue = 0;
}

if (newValue !== data[r][c]) {
const newData = data.map(row => [...row]);
newData[r][c] = 0;
newData[r][c] = newValue;
onChange(newData);
}

setEditValue(null);
};

const handleBlur = () => {
commitEdit();
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.currentTarget.blur();
}
};

const handleChange = (r: number, c: number, val: string) => {
setEditValue({ key: `${r}-${c}`, val });
};

const themes = {
Expand Down Expand Up @@ -120,6 +139,7 @@ export const MatrixInput: React.FC<MatrixProps> = ({
readOnly={!editable}
onFocus={() => editable && handleFocus(rIndex, cIndex)}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
onChange={(e) => handleChange(rIndex, cIndex, e.target.value)}
className={`
w-10 h-10 sm:w-12 sm:h-12 md:w-14 md:h-14
Expand Down
Loading
Loading