Skip to content

Commit 4a5e2fa

Browse files
Copilothotlong
andcommitted
test: add comprehensive tests for all new App Creation & Editing Flow components (85 new tests)
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 182ea6e commit 4a5e2fa

7 files changed

Lines changed: 1112 additions & 0 deletions

File tree

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { describe, it, expect, vi, beforeEach } from 'vitest';
10+
import { render, screen, fireEvent } from '@testing-library/react';
11+
import { AppCreationWizard } from '../AppCreationWizard';
12+
import type { ObjectSelection } from '@object-ui/types';
13+
14+
const MOCK_OBJECTS: ObjectSelection[] = [
15+
{ name: 'contacts', label: 'Contacts', icon: 'Users', selected: false },
16+
{ name: 'orders', label: 'Orders', icon: 'ShoppingCart', selected: false },
17+
{ name: 'products', label: 'Products', icon: 'Package', selected: false },
18+
];
19+
20+
const MOCK_TEMPLATES = [
21+
{ id: 'crm', label: 'CRM', description: 'Customer relationship management' },
22+
{ id: 'erp', label: 'ERP', description: 'Enterprise resource planning' },
23+
];
24+
25+
describe('AppCreationWizard', () => {
26+
beforeEach(() => {
27+
vi.clearAllMocks();
28+
});
29+
30+
// ============================
31+
// Rendering
32+
// ============================
33+
describe('Rendering', () => {
34+
it('should render with default props', () => {
35+
render(<AppCreationWizard />);
36+
expect(screen.getByTestId('wizard-step-basic')).toBeDefined();
37+
expect(screen.getByTestId('wizard-step-objects')).toBeDefined();
38+
expect(screen.getByTestId('wizard-step-navigation')).toBeDefined();
39+
expect(screen.getByTestId('wizard-step-branding')).toBeDefined();
40+
});
41+
42+
it('should render step 1 content by default', () => {
43+
render(<AppCreationWizard />);
44+
expect(screen.getByTestId('wizard-step-basic-content')).toBeDefined();
45+
expect(screen.getByTestId('app-name-input')).toBeDefined();
46+
expect(screen.getByTestId('app-title-input')).toBeDefined();
47+
});
48+
49+
it('should render cancel button', () => {
50+
render(<AppCreationWizard />);
51+
expect(screen.getByTestId('wizard-cancel')).toBeDefined();
52+
});
53+
54+
it('should render next button on step 1', () => {
55+
render(<AppCreationWizard />);
56+
expect(screen.getByTestId('wizard-next')).toBeDefined();
57+
});
58+
});
59+
60+
// ============================
61+
// Step 1: Basic Info
62+
// ============================
63+
describe('Step 1: Basic Info', () => {
64+
it('should accept valid snake_case app name', () => {
65+
render(<AppCreationWizard />);
66+
const nameInput = screen.getByTestId('app-name-input');
67+
fireEvent.change(nameInput, { target: { value: 'my_app' } });
68+
expect((nameInput as HTMLInputElement).value).toBe('my_app');
69+
});
70+
71+
it('should show error for invalid app name', () => {
72+
render(<AppCreationWizard />);
73+
const nameInput = screen.getByTestId('app-name-input');
74+
fireEvent.change(nameInput, { target: { value: 'My App' } });
75+
expect(screen.getByText(/snake_case/)).toBeDefined();
76+
});
77+
78+
it('should accept title input', () => {
79+
render(<AppCreationWizard />);
80+
const titleInput = screen.getByTestId('app-title-input');
81+
fireEvent.change(titleInput, { target: { value: 'My Application' } });
82+
expect((titleInput as HTMLInputElement).value).toBe('My Application');
83+
});
84+
85+
it('should render layout options', () => {
86+
render(<AppCreationWizard />);
87+
expect(screen.getByTestId('app-layout-sidebar')).toBeDefined();
88+
expect(screen.getByTestId('app-layout-header')).toBeDefined();
89+
expect(screen.getByTestId('app-layout-empty')).toBeDefined();
90+
});
91+
92+
it('should disable next when name is empty', () => {
93+
render(<AppCreationWizard />);
94+
const nextBtn = screen.getByTestId('wizard-next');
95+
expect(nextBtn.hasAttribute('disabled')).toBe(true);
96+
});
97+
98+
it('should disable next when name is invalid', () => {
99+
render(<AppCreationWizard />);
100+
fireEvent.change(screen.getByTestId('app-name-input'), { target: { value: 'Bad Name' } });
101+
fireEvent.change(screen.getByTestId('app-title-input'), { target: { value: 'Title' } });
102+
const nextBtn = screen.getByTestId('wizard-next');
103+
expect(nextBtn.hasAttribute('disabled')).toBe(true);
104+
});
105+
106+
it('should enable next when name and title are valid', () => {
107+
render(<AppCreationWizard />);
108+
fireEvent.change(screen.getByTestId('app-name-input'), { target: { value: 'my_app' } });
109+
fireEvent.change(screen.getByTestId('app-title-input'), { target: { value: 'My App' } });
110+
const nextBtn = screen.getByTestId('wizard-next');
111+
expect(nextBtn.hasAttribute('disabled')).toBe(false);
112+
});
113+
114+
it('should render templates when provided', () => {
115+
render(<AppCreationWizard templates={MOCK_TEMPLATES} />);
116+
expect(screen.getByText('CRM')).toBeDefined();
117+
});
118+
});
119+
120+
// ============================
121+
// Step Navigation
122+
// ============================
123+
describe('Step Navigation', () => {
124+
function goToStep2() {
125+
fireEvent.change(screen.getByTestId('app-name-input'), { target: { value: 'test_app' } });
126+
fireEvent.change(screen.getByTestId('app-title-input'), { target: { value: 'Test' } });
127+
fireEvent.click(screen.getByTestId('wizard-next'));
128+
}
129+
130+
it('should navigate to step 2 on next', () => {
131+
render(<AppCreationWizard availableObjects={MOCK_OBJECTS} />);
132+
goToStep2();
133+
expect(screen.getByTestId('wizard-step-objects-content')).toBeDefined();
134+
});
135+
136+
it('should show back button on step 2', () => {
137+
render(<AppCreationWizard availableObjects={MOCK_OBJECTS} />);
138+
goToStep2();
139+
expect(screen.getByTestId('wizard-back')).toBeDefined();
140+
});
141+
142+
it('should navigate back to step 1', () => {
143+
render(<AppCreationWizard availableObjects={MOCK_OBJECTS} />);
144+
goToStep2();
145+
fireEvent.click(screen.getByTestId('wizard-back'));
146+
expect(screen.getByTestId('wizard-step-basic-content')).toBeDefined();
147+
});
148+
149+
it('should navigate to step 3 from step 2', () => {
150+
render(<AppCreationWizard availableObjects={MOCK_OBJECTS} />);
151+
goToStep2();
152+
fireEvent.click(screen.getByTestId('wizard-next'));
153+
expect(screen.getByTestId('wizard-step-navigation-content')).toBeDefined();
154+
});
155+
156+
it('should show complete button on last step', () => {
157+
render(<AppCreationWizard availableObjects={MOCK_OBJECTS} />);
158+
goToStep2();
159+
fireEvent.click(screen.getByTestId('wizard-next')); // step 3
160+
fireEvent.click(screen.getByTestId('wizard-next')); // step 4
161+
expect(screen.getByTestId('wizard-complete')).toBeDefined();
162+
});
163+
});
164+
165+
// ============================
166+
// Step 2: Object Selection
167+
// ============================
168+
describe('Step 2: Object Selection', () => {
169+
function renderStep2() {
170+
render(<AppCreationWizard availableObjects={MOCK_OBJECTS} />);
171+
fireEvent.change(screen.getByTestId('app-name-input'), { target: { value: 'test_app' } });
172+
fireEvent.change(screen.getByTestId('app-title-input'), { target: { value: 'Test' } });
173+
fireEvent.click(screen.getByTestId('wizard-next'));
174+
}
175+
176+
it('should render object cards', () => {
177+
renderStep2();
178+
expect(screen.getByTestId('object-card-contacts')).toBeDefined();
179+
expect(screen.getByTestId('object-card-orders')).toBeDefined();
180+
expect(screen.getByTestId('object-card-products')).toBeDefined();
181+
});
182+
183+
it('should toggle object selection', () => {
184+
renderStep2();
185+
fireEvent.click(screen.getByTestId('object-card-contacts'));
186+
// Card should be selected (visual feedback)
187+
expect(screen.getByTestId('object-card-contacts')).toBeDefined();
188+
});
189+
190+
it('should render search input', () => {
191+
renderStep2();
192+
expect(screen.getByTestId('object-search')).toBeDefined();
193+
});
194+
195+
it('should filter objects by search', () => {
196+
renderStep2();
197+
fireEvent.change(screen.getByTestId('object-search'), { target: { value: 'contact' } });
198+
expect(screen.getByTestId('object-card-contacts')).toBeDefined();
199+
expect(screen.queryByTestId('object-card-orders')).toBeNull();
200+
});
201+
});
202+
203+
// ============================
204+
// Step 3: Navigation Builder
205+
// ============================
206+
describe('Step 3: Navigation Builder', () => {
207+
function renderStep3WithObjects() {
208+
render(<AppCreationWizard availableObjects={MOCK_OBJECTS} />);
209+
fireEvent.change(screen.getByTestId('app-name-input'), { target: { value: 'test_app' } });
210+
fireEvent.change(screen.getByTestId('app-title-input'), { target: { value: 'Test' } });
211+
fireEvent.click(screen.getByTestId('wizard-next')); // step 2
212+
// Select objects
213+
fireEvent.click(screen.getByTestId('object-card-contacts'));
214+
fireEvent.click(screen.getByTestId('object-card-orders'));
215+
fireEvent.click(screen.getByTestId('wizard-next')); // step 3
216+
}
217+
218+
it('should auto-generate nav items from selected objects', () => {
219+
renderStep3WithObjects();
220+
expect(screen.getByTestId('nav-item-contacts')).toBeDefined();
221+
expect(screen.getByTestId('nav-item-orders')).toBeDefined();
222+
});
223+
224+
it('should render add buttons', () => {
225+
renderStep3WithObjects();
226+
expect(screen.getByTestId('add-group-btn')).toBeDefined();
227+
expect(screen.getByTestId('add-url-btn')).toBeDefined();
228+
expect(screen.getByTestId('add-separator-btn')).toBeDefined();
229+
});
230+
231+
it('should add a group nav item', () => {
232+
renderStep3WithObjects();
233+
fireEvent.click(screen.getByTestId('add-group-btn'));
234+
expect(screen.getByText('New Group')).toBeDefined();
235+
});
236+
237+
it('should add a URL nav item', () => {
238+
renderStep3WithObjects();
239+
fireEvent.click(screen.getByTestId('add-url-btn'));
240+
expect(screen.getByText('New Link')).toBeDefined();
241+
});
242+
});
243+
244+
// ============================
245+
// Step 4: Branding
246+
// ============================
247+
describe('Step 4: Branding', () => {
248+
function renderStep4() {
249+
render(<AppCreationWizard />);
250+
fireEvent.change(screen.getByTestId('app-name-input'), { target: { value: 'test_app' } });
251+
fireEvent.change(screen.getByTestId('app-title-input'), { target: { value: 'Test' } });
252+
fireEvent.click(screen.getByTestId('wizard-next')); // step 2
253+
fireEvent.click(screen.getByTestId('wizard-next')); // step 3
254+
fireEvent.click(screen.getByTestId('wizard-next')); // step 4
255+
}
256+
257+
it('should render branding inputs', () => {
258+
renderStep4();
259+
expect(screen.getByTestId('branding-logo-input')).toBeDefined();
260+
expect(screen.getByTestId('branding-color-input')).toBeDefined();
261+
expect(screen.getByTestId('branding-favicon-input')).toBeDefined();
262+
});
263+
264+
it('should accept logo URL', () => {
265+
renderStep4();
266+
const input = screen.getByTestId('branding-logo-input');
267+
fireEvent.change(input, { target: { value: 'https://example.com/logo.svg' } });
268+
expect((input as HTMLInputElement).value).toBe('https://example.com/logo.svg');
269+
});
270+
271+
it('should accept primary color', () => {
272+
renderStep4();
273+
const input = screen.getByTestId('branding-color-input');
274+
fireEvent.change(input, { target: { value: '#ff5733' } });
275+
expect((input as HTMLInputElement).value).toBe('#ff5733');
276+
});
277+
});
278+
279+
// ============================
280+
// Callbacks
281+
// ============================
282+
describe('Callbacks', () => {
283+
it('should call onCancel when cancel is clicked', () => {
284+
const onCancel = vi.fn();
285+
render(<AppCreationWizard onCancel={onCancel} />);
286+
fireEvent.click(screen.getByTestId('wizard-cancel'));
287+
expect(onCancel).toHaveBeenCalledOnce();
288+
});
289+
290+
it('should call onComplete with draft on complete', () => {
291+
const onComplete = vi.fn();
292+
render(<AppCreationWizard onComplete={onComplete} />);
293+
// Fill step 1
294+
fireEvent.change(screen.getByTestId('app-name-input'), { target: { value: 'test_app' } });
295+
fireEvent.change(screen.getByTestId('app-title-input'), { target: { value: 'Test App' } });
296+
// Navigate to step 4
297+
fireEvent.click(screen.getByTestId('wizard-next')); // step 2
298+
fireEvent.click(screen.getByTestId('wizard-next')); // step 3
299+
fireEvent.click(screen.getByTestId('wizard-next')); // step 4
300+
// Complete
301+
fireEvent.click(screen.getByTestId('wizard-complete'));
302+
expect(onComplete).toHaveBeenCalledOnce();
303+
expect(onComplete.mock.calls[0][0]).toMatchObject({
304+
name: 'test_app',
305+
title: 'Test App',
306+
});
307+
});
308+
});
309+
310+
// ============================
311+
// Read-only Mode
312+
// ============================
313+
describe('Read-only Mode', () => {
314+
it('should disable inputs in read-only mode', () => {
315+
render(<AppCreationWizard readOnly />);
316+
const nameInput = screen.getByTestId('app-name-input') as HTMLInputElement;
317+
expect(nameInput.disabled).toBe(true);
318+
});
319+
320+
it('should disable next button in read-only mode', () => {
321+
render(<AppCreationWizard readOnly />);
322+
const nextBtn = screen.getByTestId('wizard-next');
323+
expect(nextBtn.hasAttribute('disabled')).toBe(true);
324+
});
325+
});
326+
});

0 commit comments

Comments
 (0)