Skip to content

Commit 1c51a9e

Browse files
committed
chore: closing the test coverage gap
1 parent 411ce6b commit 1c51a9e

File tree

24 files changed

+2394
-68
lines changed

24 files changed

+2394
-68
lines changed

packages/core/src/localizer.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,24 @@ function autoTemplate(template) {
3030
template
3131
);
3232
}
33-
return template;
33+
34+
// Create a routing function that uses the specialized methods
35+
function route(value) {
36+
if (value instanceof Date) {
37+
return template.getLocalizedDate(value);
38+
} else if (typeof value === 'number') {
39+
return template.getLocalizedNumber(value);
40+
} else {
41+
return template.getLocalizedString(value);
42+
}
43+
}
44+
45+
// Copy all the specialized methods to the route function
46+
route.getLocalizedString = template.getLocalizedString;
47+
route.getLocalizedDate = template.getLocalizedDate;
48+
route.getLocalizedNumber = template.getLocalizedNumber;
49+
50+
return route;
3451
} else {
3552
Object.assign(
3653
route,

packages/core/src/types.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,18 @@ export const FormType = t.oneOfType([
99
]);
1010
export const FormsType = t.arrayOf(
1111
function (propValue, key, componentName, location, propFullName) {
12-
const isString = typeof propValue[key] === 'string';
13-
const isObject =
14-
typeof propValue[key] === 'object' && propValue[key] !== null;
12+
const item = propValue[key];
13+
const isString = typeof item === 'string';
14+
const isObject = typeof item === 'object' && item !== null;
1515

1616
if (!isString && !isObject) {
1717
return new Error(
18-
`Invalid prop \`${propFullName}\` of type \`${typeof propValue[key]}\` supplied to \`${componentName}\`, expected \`string\` or \`object\`.`
18+
`Invalid prop \`${propFullName}\` of type \`${typeof item}\` supplied to \`${componentName}\`, expected \`string\` or \`object\`.`
1919
);
2020
}
21+
22+
// Return null for valid cases (string or object)
23+
return null;
2124
}
2225
);
2326
FormType.items = FormsType;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { describe, it } from 'mocha';
2+
import * as chai from 'chai';
3+
import React from 'react';
4+
import { render, renderHook } from '@testing-library/react';
5+
import { RenderingContext, ModelContext } from '@forml/context';
6+
import { useModelStore } from '@forml/hooks';
7+
import Dynamic from '../../../src/components/mapper/dynamic.jsx';
8+
9+
const { expect } = chai;
10+
11+
describe('Dynamic Component', function () {
12+
const mockRenderingContext = {
13+
prefix: '',
14+
mapper: {
15+
text: () => <input type="text" data-testid="text-field" />,
16+
},
17+
decorator: {},
18+
localizer: (value) => value,
19+
};
20+
21+
function makeWrapper({ modelStore }) {
22+
return ({ children }) => (
23+
<RenderingContext.Provider value={mockRenderingContext}>
24+
<ModelContext.Provider value={modelStore}>
25+
{children}
26+
</ModelContext.Provider>
27+
</RenderingContext.Provider>
28+
);
29+
}
30+
31+
it('renders with dynamic form generation', function () {
32+
const schema = {
33+
type: 'object',
34+
properties: {
35+
dynamicField: { type: 'string' },
36+
},
37+
};
38+
const model = {};
39+
const modelStore = renderHook(() => useModelStore(schema, model)).result
40+
.current;
41+
42+
const mockForm = {
43+
key: 'dynamicField',
44+
generate: [
45+
{
46+
type: 'text',
47+
title: 'Dynamic Field',
48+
},
49+
],
50+
};
51+
52+
const mockOnChange = () => {};
53+
const wrapper = makeWrapper({ modelStore });
54+
55+
const { getByTestId } = render(
56+
<Dynamic form={mockForm} onChange={mockOnChange} />,
57+
{ wrapper }
58+
);
59+
60+
// The dynamic component should render the generated form
61+
expect(getByTestId('text-field')).to.exist;
62+
});
63+
64+
it('creates proper nested context with prefix', function () {
65+
const schema = {
66+
type: 'object',
67+
properties: {
68+
nested: {
69+
type: 'object',
70+
properties: {
71+
field: { type: 'string' },
72+
},
73+
},
74+
},
75+
};
76+
const model = {};
77+
const modelStore = renderHook(() => useModelStore(schema, model)).result
78+
.current;
79+
80+
const mockForm = {
81+
key: 'nested.field',
82+
generate: [
83+
{
84+
type: 'text',
85+
title: 'Nested Dynamic Field',
86+
},
87+
],
88+
};
89+
90+
const mockOnChange = () => {};
91+
const wrapper = makeWrapper({ modelStore });
92+
93+
// Just ensure it renders without error
94+
const { container } = render(
95+
<Dynamic form={mockForm} onChange={mockOnChange} />,
96+
{ wrapper }
97+
);
98+
99+
expect(container.firstChild).to.exist;
100+
});
101+
});
102+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, it } from 'mocha';
2+
import * as chai from 'chai';
3+
import React from 'react';
4+
import { render } from '@testing-library/react';
5+
import Null from '../../../src/components/mapper/null.jsx';
6+
7+
const { expect } = chai;
8+
9+
describe('Null Component', function () {
10+
it('renders and returns null', function () {
11+
const mockForm = {
12+
type: 'null',
13+
title: 'Null Field'
14+
};
15+
16+
const { container } = render(<Null form={mockForm} />);
17+
18+
// The null component should render nothing
19+
expect(container.firstChild).to.be.null;
20+
});
21+
22+
it('has the correct displayName', function () {
23+
expect(Null.name).to.equal('Null');
24+
});
25+
});
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { describe, it } from 'mocha';
2+
import * as chai from 'chai';
3+
import React from 'react';
4+
import { render, fireEvent } from '@testing-library/react';
5+
import { RenderingContext, ModelContext } from '@forml/context';
6+
import Tabs from '../../../src/components/mapper/tabs.jsx';
7+
8+
const { expect } = chai;
9+
10+
describe('Tabs Component', function () {
11+
// Mock tab decorator
12+
function MockTab(props) {
13+
const { form, activate, index } = props;
14+
return (
15+
<button data-testid={`tab-${index}`} onClick={activate}>
16+
{form.title || `Tab ${index}`}
17+
</button>
18+
);
19+
}
20+
function MockPanel(props) {
21+
const { form, index, children } = props;
22+
return (
23+
<div data-testid={`panel-${index}`}>
24+
<h3>{form.title}</h3>
25+
{children}
26+
</div>
27+
);
28+
}
29+
function MockTabsContainer(props) {
30+
const { tabs, panels, value = 0, activateTab } = props;
31+
return (
32+
<div data-testid="tabs-container">
33+
<div data-testid="tab-list">{tabs}</div>
34+
<div data-testid="panel-content">{panels[value]}</div>
35+
</div>
36+
);
37+
}
38+
MockTabsContainer.Panel = MockPanel;
39+
MockTabsContainer.Tab = MockTab;
40+
41+
const mockTabsDecorator = {
42+
tabs: MockTabsContainer,
43+
};
44+
45+
const mockRenderingContext = {
46+
prefix: '',
47+
mapper: {
48+
text: (props) => (
49+
<input type="text" data-testid="text-field" {...props} />
50+
),
51+
},
52+
decorator: mockTabsDecorator,
53+
localizer: (value) => value,
54+
};
55+
56+
const mockModelContext = {
57+
model: {},
58+
keyMaps: {},
59+
schema: {},
60+
};
61+
62+
function wrapper({ children }) {
63+
return (
64+
<RenderingContext.Provider value={mockRenderingContext}>
65+
<ModelContext.Provider value={mockModelContext}>
66+
{children}
67+
</ModelContext.Provider>
68+
</RenderingContext.Provider>
69+
);
70+
}
71+
72+
it('renders tabs with multiple tab items', function () {
73+
const mockForm = {
74+
type: 'tabs',
75+
tabs: [
76+
{
77+
title: 'Tab One',
78+
type: 'text',
79+
schema: { type: 'string' },
80+
},
81+
{
82+
title: 'Tab Two',
83+
type: 'text',
84+
schema: { type: 'string' },
85+
},
86+
{
87+
title: 'Tab Three',
88+
type: 'text',
89+
schema: { type: 'string' },
90+
},
91+
],
92+
};
93+
94+
const mockOnChange = () => {};
95+
96+
const { getByTestId } = render(
97+
<Tabs form={mockForm} onChange={mockOnChange} />,
98+
{ wrapper }
99+
);
100+
101+
// Should render all tabs
102+
expect(getByTestId('tab-0')).to.exist;
103+
expect(getByTestId('tab-1')).to.exist;
104+
expect(getByTestId('tab-2')).to.exist;
105+
106+
// Should render the tabs container
107+
expect(getByTestId('tabs-container')).to.exist;
108+
expect(getByTestId('panel-content')).to.exist;
109+
});
110+
111+
it('handles tab activation', function () {
112+
const mockForm = {
113+
type: 'tabs',
114+
tabs: [
115+
{
116+
title: 'First Tab',
117+
type: 'text',
118+
schema: { type: 'string' },
119+
},
120+
{
121+
title: 'Second Tab',
122+
type: 'text',
123+
schema: { type: 'string' },
124+
},
125+
],
126+
};
127+
128+
const mockOnChange = () => {};
129+
130+
const { getByTestId } = render(
131+
<Tabs form={mockForm} onChange={mockOnChange} />,
132+
{ wrapper }
133+
);
134+
135+
// Click on the second tab to activate it
136+
const secondTab = getByTestId('tab-1');
137+
fireEvent.click(secondTab);
138+
139+
// Should successfully handle the tab click
140+
expect(secondTab).to.exist;
141+
});
142+
143+
it('renders with htmlClass when provided', function () {
144+
const mockForm = {
145+
type: 'tabs',
146+
htmlClass: 'custom-tabs',
147+
tabs: [
148+
{
149+
title: 'Tab',
150+
type: 'text',
151+
schema: { type: 'string' },
152+
},
153+
],
154+
};
155+
156+
const mockOnChange = () => {};
157+
158+
const { getByTestId } = render(
159+
<Tabs form={mockForm} onChange={mockOnChange} />,
160+
{ wrapper }
161+
);
162+
163+
expect(getByTestId('tabs-container')).to.exist;
164+
});
165+
166+
it('handles empty tabs array', function () {
167+
const mockForm = {
168+
type: 'tabs',
169+
tabs: [],
170+
};
171+
172+
const mockOnChange = () => {};
173+
174+
const { getByTestId } = render(
175+
<Tabs form={mockForm} onChange={mockOnChange} />,
176+
{ wrapper }
177+
);
178+
179+
expect(getByTestId('tabs-container')).to.exist;
180+
});
181+
});
182+

0 commit comments

Comments
 (0)