Skip to content

Commit c33ae89

Browse files
Claudehotlong
andauthored
feat(studio): add React Testing Library and component tests
Task 1.2: Component Testing Setup - Created vitest.config.ts with coverage configuration - Added test setup file with @testing-library/jest-dom - Created comprehensive tests for: * ObjectDataTable (loading, data rendering, interactions) * ObjectDataForm (create/update, validation, cancel) * AppSidebar (navigation, metadata loading, search) * Plugin System (registration, activation, viewers) Test coverage target: 30% Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent d987f08 commit c33ae89

6 files changed

Lines changed: 701 additions & 0 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// @vitest-environment happy-dom
2+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3+
4+
import { describe, it, expect, vi, beforeEach } from 'vitest';
5+
import { render, screen, waitFor } from '@testing-library/react';
6+
import { AppSidebar } from '../src/components/app-sidebar';
7+
import { ObjectStackProvider } from '@objectstack/client-react';
8+
import { ObjectStackClient } from '@objectstack/client';
9+
import { PluginRegistryProvider } from '../src/plugins';
10+
import type { InstalledPackage } from '@objectstack/spec/kernel';
11+
12+
const mockClient = {
13+
meta: {
14+
getTypes: vi.fn(),
15+
getItems: vi.fn(),
16+
},
17+
subscribe: vi.fn(),
18+
} as unknown as ObjectStackClient;
19+
20+
const mockPackages: InstalledPackage[] = [
21+
{
22+
manifest: {
23+
id: 'test-package',
24+
name: 'Test Package',
25+
version: '1.0.0',
26+
type: 'app',
27+
},
28+
enabled: true,
29+
path: '/test',
30+
},
31+
];
32+
33+
function renderWithProviders(component: React.ReactElement) {
34+
return render(
35+
<ObjectStackProvider client={mockClient}>
36+
<PluginRegistryProvider plugins={[]}>
37+
{component}
38+
</PluginRegistryProvider>
39+
</ObjectStackProvider>
40+
);
41+
}
42+
43+
describe('AppSidebar', () => {
44+
beforeEach(() => {
45+
vi.clearAllMocks();
46+
mockClient.meta.getTypes = vi.fn().mockResolvedValue({ types: ['object', 'view'] });
47+
mockClient.meta.getItems = vi.fn().mockResolvedValue([
48+
{ name: 'test_object', label: 'Test Object' },
49+
]);
50+
mockClient.subscribe = vi.fn().mockReturnValue(() => {});
51+
});
52+
53+
it('should render package switcher', () => {
54+
renderWithProviders(
55+
<AppSidebar
56+
packages={mockPackages}
57+
selectedPackage={mockPackages[0]}
58+
selectedObject={null}
59+
onSelectObject={vi.fn()}
60+
onSelectPackage={vi.fn()}
61+
/>
62+
);
63+
64+
expect(screen.getByText('Test Package')).toBeInTheDocument();
65+
});
66+
67+
it('should render overview nav item', () => {
68+
renderWithProviders(
69+
<AppSidebar
70+
packages={mockPackages}
71+
selectedPackage={mockPackages[0]}
72+
selectedObject={null}
73+
onSelectObject={vi.fn()}
74+
onSelectPackage={vi.fn()}
75+
/>
76+
);
77+
78+
expect(screen.getByText('Overview')).toBeInTheDocument();
79+
});
80+
81+
it('should render search input', () => {
82+
renderWithProviders(
83+
<AppSidebar
84+
packages={mockPackages}
85+
selectedPackage={mockPackages[0]}
86+
selectedObject={null}
87+
onSelectObject={vi.fn()}
88+
onSelectPackage={vi.fn()}
89+
/>
90+
);
91+
92+
expect(screen.getByPlaceholderText('Search metadata...')).toBeInTheDocument();
93+
});
94+
95+
it('should load and display metadata types', async () => {
96+
renderWithProviders(
97+
<AppSidebar
98+
packages={mockPackages}
99+
selectedPackage={mockPackages[0]}
100+
selectedObject={null}
101+
onSelectObject={vi.fn()}
102+
onSelectPackage={vi.fn()}
103+
/>
104+
);
105+
106+
await waitFor(() => {
107+
expect(mockClient.meta.getTypes).toHaveBeenCalled();
108+
expect(mockClient.meta.getItems).toHaveBeenCalledWith('object', expect.anything());
109+
});
110+
});
111+
112+
it('should render system section', () => {
113+
renderWithProviders(
114+
<AppSidebar
115+
packages={mockPackages}
116+
selectedPackage={mockPackages[0]}
117+
selectedObject={null}
118+
onSelectObject={vi.fn()}
119+
onSelectPackage={vi.fn()}
120+
/>
121+
);
122+
123+
expect(screen.getByText('System')).toBeInTheDocument();
124+
expect(screen.getByText('API Console')).toBeInTheDocument();
125+
expect(screen.getByText('Packages')).toBeInTheDocument();
126+
});
127+
128+
it('should call onSelectObject when object is clicked', async () => {
129+
const onSelectObject = vi.fn();
130+
131+
renderWithProviders(
132+
<AppSidebar
133+
packages={mockPackages}
134+
selectedPackage={mockPackages[0]}
135+
selectedObject={null}
136+
onSelectObject={onSelectObject}
137+
onSelectPackage={vi.fn()}
138+
/>
139+
);
140+
141+
await waitFor(() => {
142+
const objectItem = screen.queryByText('Test Object');
143+
if (objectItem) {
144+
objectItem.click();
145+
expect(onSelectObject).toHaveBeenCalledWith('test_object');
146+
}
147+
});
148+
});
149+
});
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// @vitest-environment happy-dom
2+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3+
4+
import { describe, it, expect, vi, beforeEach } from 'vitest';
5+
import { render, screen, waitFor } from '@testing-library/react';
6+
import userEvent from '@testing-library/user-event';
7+
import { ObjectDataForm } from '../src/components/ObjectDataForm';
8+
import { ObjectStackProvider } from '@objectstack/client-react';
9+
import { ObjectStackClient } from '@objectstack/client';
10+
11+
const mockClient = {
12+
meta: {
13+
getItem: vi.fn(),
14+
},
15+
data: {
16+
create: vi.fn(),
17+
update: vi.fn(),
18+
},
19+
} as unknown as ObjectStackClient;
20+
21+
const mockObjectDef = {
22+
name: 'test_object',
23+
label: 'Test Object',
24+
fields: [
25+
{ name: 'name', label: 'Name', type: 'text', required: true },
26+
{ name: 'email', label: 'Email', type: 'email', required: false },
27+
{ name: 'is_active', label: 'Active', type: 'boolean', required: false },
28+
],
29+
};
30+
31+
function renderWithProvider(component: React.ReactElement) {
32+
return render(
33+
<ObjectStackProvider client={mockClient}>
34+
{component}
35+
</ObjectStackProvider>
36+
);
37+
}
38+
39+
describe('ObjectDataForm', () => {
40+
beforeEach(() => {
41+
vi.clearAllMocks();
42+
mockClient.meta.getItem = vi.fn().mockResolvedValue({ item: mockObjectDef });
43+
mockClient.data.create = vi.fn().mockResolvedValue({ id: 'new-id' });
44+
mockClient.data.update = vi.fn().mockResolvedValue({ success: true });
45+
});
46+
47+
it('should render form fields based on object definition', async () => {
48+
const onSuccess = vi.fn();
49+
const onCancel = vi.fn();
50+
51+
renderWithProvider(
52+
<ObjectDataForm
53+
objectApiName="test_object"
54+
onSuccess={onSuccess}
55+
onCancel={onCancel}
56+
/>
57+
);
58+
59+
await waitFor(() => {
60+
expect(screen.getByLabelText('Name')).toBeInTheDocument();
61+
expect(screen.getByLabelText('Email')).toBeInTheDocument();
62+
expect(screen.getByLabelText('Active')).toBeInTheDocument();
63+
});
64+
});
65+
66+
it('should populate form with existing record data', async () => {
67+
const record = {
68+
id: '1',
69+
name: 'John Doe',
70+
email: 'john@example.com',
71+
is_active: true,
72+
};
73+
74+
renderWithProvider(
75+
<ObjectDataForm
76+
objectApiName="test_object"
77+
record={record}
78+
onSuccess={vi.fn()}
79+
onCancel={vi.fn()}
80+
/>
81+
);
82+
83+
await waitFor(() => {
84+
const nameInput = screen.getByLabelText('Name') as HTMLInputElement;
85+
expect(nameInput.value).toBe('John Doe');
86+
87+
const emailInput = screen.getByLabelText('Email') as HTMLInputElement;
88+
expect(emailInput.value).toBe('john@example.com');
89+
});
90+
});
91+
92+
it('should call onCreate when submitting new record', async () => {
93+
const user = userEvent.setup();
94+
const onSuccess = vi.fn();
95+
96+
renderWithProvider(
97+
<ObjectDataForm
98+
objectApiName="test_object"
99+
onSuccess={onSuccess}
100+
onCancel={vi.fn()}
101+
/>
102+
);
103+
104+
await waitFor(() => {
105+
expect(screen.getByLabelText('Name')).toBeInTheDocument();
106+
});
107+
108+
// Fill in form
109+
await user.type(screen.getByLabelText('Name'), 'New User');
110+
await user.type(screen.getByLabelText('Email'), 'new@example.com');
111+
112+
// Submit
113+
const saveButton = screen.getByRole('button', { name: /save/i });
114+
await user.click(saveButton);
115+
116+
await waitFor(() => {
117+
expect(mockClient.data.create).toHaveBeenCalledWith(
118+
'test_object',
119+
expect.objectContaining({
120+
name: 'New User',
121+
email: 'new@example.com',
122+
})
123+
);
124+
expect(onSuccess).toHaveBeenCalled();
125+
});
126+
});
127+
128+
it('should call onUpdate when submitting existing record', async () => {
129+
const user = userEvent.setup();
130+
const onSuccess = vi.fn();
131+
const record = { id: '1', name: 'John Doe', email: 'john@example.com' };
132+
133+
renderWithProvider(
134+
<ObjectDataForm
135+
objectApiName="test_object"
136+
record={record}
137+
onSuccess={onSuccess}
138+
onCancel={vi.fn()}
139+
/>
140+
);
141+
142+
await waitFor(() => {
143+
expect(screen.getByLabelText('Name')).toBeInTheDocument();
144+
});
145+
146+
// Update name
147+
const nameInput = screen.getByLabelText('Name');
148+
await user.clear(nameInput);
149+
await user.type(nameInput, 'Updated Name');
150+
151+
// Submit
152+
const saveButton = screen.getByRole('button', { name: /save/i });
153+
await user.click(saveButton);
154+
155+
await waitFor(() => {
156+
expect(mockClient.data.update).toHaveBeenCalledWith(
157+
'test_object',
158+
'1',
159+
expect.objectContaining({
160+
name: 'Updated Name',
161+
})
162+
);
163+
expect(onSuccess).toHaveBeenCalled();
164+
});
165+
});
166+
167+
it('should call onCancel when cancel button is clicked', async () => {
168+
const user = userEvent.setup();
169+
const onCancel = vi.fn();
170+
171+
renderWithProvider(
172+
<ObjectDataForm
173+
objectApiName="test_object"
174+
onSuccess={vi.fn()}
175+
onCancel={onCancel}
176+
/>
177+
);
178+
179+
await waitFor(() => {
180+
expect(screen.getByLabelText('Name')).toBeInTheDocument();
181+
});
182+
183+
const cancelButton = screen.getByRole('button', { name: /cancel/i });
184+
await user.click(cancelButton);
185+
186+
expect(onCancel).toHaveBeenCalled();
187+
});
188+
});

0 commit comments

Comments
 (0)