Skip to content

Commit ed24233

Browse files
CopilotCopilot
andcommitted
test: add L2 feature tests for swimlanes, shared view, comments, and embeddable form
- KanbanSwimlanes: test swimlane rendering, collapse/expand, card counts - SharedViewLinkPassword: test password input, expiration dropdown, badges, onShare callback - CommentThreadSortReactions: test sort dropdown, reaction buttons, existing reactions display - EmbeddableFormPrefill: test URL param prefill, explicit prefill override, thank-you page Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f6473eb commit ed24233

4 files changed

Lines changed: 760 additions & 0 deletions

File tree

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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, waitFor } from '@testing-library/react';
11+
import React from 'react';
12+
import { CommentThread } from '../CommentThread';
13+
import type { Comment } from '../CommentThread';
14+
15+
const mockComments: Comment[] = [
16+
{
17+
id: '1',
18+
author: { id: 'u1', name: 'Alice' },
19+
content: 'First',
20+
mentions: [],
21+
createdAt: '2025-01-01T10:00:00Z',
22+
},
23+
{
24+
id: '2',
25+
author: { id: 'u2', name: 'Bob' },
26+
content: 'Second',
27+
mentions: [],
28+
createdAt: '2025-01-02T10:00:00Z',
29+
},
30+
{
31+
id: '3',
32+
author: { id: 'u1', name: 'Alice' },
33+
content: 'Third',
34+
mentions: [],
35+
createdAt: '2025-01-03T10:00:00Z',
36+
reactions: { '👍': ['u2'] },
37+
},
38+
];
39+
40+
const currentUser = { id: 'u1', name: 'Alice' };
41+
42+
describe('CommentThread - Sorting & Reactions', () => {
43+
describe('Sort dropdown', () => {
44+
it('renders a sort dropdown with "Oldest" and "Newest" options', () => {
45+
render(
46+
<CommentThread
47+
threadId="t1"
48+
comments={mockComments}
49+
currentUser={currentUser}
50+
/>
51+
);
52+
53+
const sortSelect = screen.getByRole('combobox', { name: 'Sort comments' });
54+
expect(sortSelect).toBeInTheDocument();
55+
56+
const options = sortSelect.querySelectorAll('option');
57+
const optionTexts = Array.from(options).map(o => o.textContent);
58+
expect(optionTexts).toContain('Oldest');
59+
expect(optionTexts).toContain('Newest');
60+
});
61+
62+
it('defaults to "Oldest" sort order showing comments in chronological order', () => {
63+
const { container } = render(
64+
<CommentThread
65+
threadId="t1"
66+
comments={mockComments}
67+
currentUser={currentUser}
68+
/>
69+
);
70+
71+
const commentElements = container.querySelectorAll('[data-comment-id]');
72+
const ids = Array.from(commentElements).map(el => el.getAttribute('data-comment-id'));
73+
expect(ids).toEqual(['1', '2', '3']);
74+
});
75+
76+
it('changes to "Newest" sort order and reverses comment display', () => {
77+
const { container } = render(
78+
<CommentThread
79+
threadId="t1"
80+
comments={mockComments}
81+
currentUser={currentUser}
82+
/>
83+
);
84+
85+
// Change sort to newest
86+
const sortSelect = screen.getByRole('combobox', { name: 'Sort comments' });
87+
fireEvent.change(sortSelect, { target: { value: 'newest' } });
88+
89+
// After re-sort, comments should be in reverse chronological order
90+
const commentElements = container.querySelectorAll('[data-comment-id]');
91+
const ids = Array.from(commentElements).map(el => el.getAttribute('data-comment-id'));
92+
expect(ids).toEqual(['3', '2', '1']);
93+
});
94+
});
95+
96+
describe('Reaction buttons', () => {
97+
it('renders 👍 and ❤️ reaction buttons in comment actions when onReaction is provided', () => {
98+
render(
99+
<CommentThread
100+
threadId="t1"
101+
comments={mockComments}
102+
currentUser={currentUser}
103+
onReaction={vi.fn()}
104+
/>
105+
);
106+
107+
// Each comment should have reaction action buttons
108+
const thumbsUpButtons = screen.getAllByRole('button', { name: /👍/ });
109+
expect(thumbsUpButtons.length).toBeGreaterThan(0);
110+
111+
const heartButtons = screen.getAllByRole('button', { name: // });
112+
expect(heartButtons.length).toBeGreaterThan(0);
113+
});
114+
115+
it('calls onReaction when clicking a 👍 action button', () => {
116+
const onReaction = vi.fn();
117+
render(
118+
<CommentThread
119+
threadId="t1"
120+
comments={mockComments}
121+
currentUser={currentUser}
122+
onReaction={onReaction}
123+
/>
124+
);
125+
126+
// Find the 👍 action buttons (in the actions div, not the reaction bar)
127+
const allThumbsButtons = screen.getAllByText('👍');
128+
// Click the first one
129+
fireEvent.click(allThumbsButtons[0]);
130+
131+
expect(onReaction).toHaveBeenCalled();
132+
// Should be called with a comment id and the emoji
133+
const [commentId, emoji] = onReaction.mock.calls[0];
134+
expect(emoji).toBe('👍');
135+
});
136+
137+
it('calls onReaction when clicking a ❤️ action button', () => {
138+
const onReaction = vi.fn();
139+
render(
140+
<CommentThread
141+
threadId="t1"
142+
comments={mockComments}
143+
currentUser={currentUser}
144+
onReaction={onReaction}
145+
/>
146+
);
147+
148+
const heartButtons = screen.getAllByText('❤️');
149+
fireEvent.click(heartButtons[0]);
150+
151+
expect(onReaction).toHaveBeenCalled();
152+
const [, emoji] = onReaction.mock.calls[0];
153+
expect(emoji).toBe('❤️');
154+
});
155+
});
156+
157+
describe('Existing reactions display', () => {
158+
it('displays existing reactions with count', () => {
159+
render(
160+
<CommentThread
161+
threadId="t1"
162+
comments={mockComments}
163+
currentUser={currentUser}
164+
onReaction={vi.fn()}
165+
/>
166+
);
167+
168+
// Comment 3 has a 👍 reaction from u2 with count 1
169+
// The reaction bar shows "👍 1"
170+
expect(screen.getByText('👍 1')).toBeInTheDocument();
171+
});
172+
173+
it('calls onReaction when clicking an existing reaction badge', () => {
174+
const onReaction = vi.fn();
175+
render(
176+
<CommentThread
177+
threadId="t1"
178+
comments={mockComments}
179+
currentUser={currentUser}
180+
onReaction={onReaction}
181+
/>
182+
);
183+
184+
// Click the existing "👍 1" reaction badge on comment 3
185+
const reactionBadge = screen.getByText('👍 1');
186+
fireEvent.click(reactionBadge);
187+
188+
expect(onReaction).toHaveBeenCalledWith('3', '👍');
189+
});
190+
191+
it('does not render reaction action buttons when onReaction is not provided', () => {
192+
render(
193+
<CommentThread
194+
threadId="t1"
195+
comments={[mockComments[0]]}
196+
currentUser={currentUser}
197+
/>
198+
);
199+
200+
// Without onReaction, the action buttons for 👍 and ❤️ should not appear
201+
// Only "Reply" should be present (and possibly Edit/Delete for own comments)
202+
const buttons = screen.getAllByRole('button');
203+
const buttonTexts = buttons.map(b => b.textContent);
204+
expect(buttonTexts).not.toContain('👍');
205+
expect(buttonTexts).not.toContain('❤️');
206+
});
207+
});
208+
});
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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, afterEach } from 'vitest';
10+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
11+
import React from 'react';
12+
13+
// Capture the initialData passed to ObjectForm
14+
let capturedSchema: any = null;
15+
let capturedOnSuccess: ((data: any) => void) | null = null;
16+
17+
vi.mock('../ObjectForm', () => ({
18+
ObjectForm: ({ schema, dataSource }: any) => {
19+
capturedSchema = schema;
20+
capturedOnSuccess = schema?.onSuccess ?? null;
21+
return (
22+
<div>
23+
<div data-testid="object-form">
24+
{schema?.initialData && (
25+
<div data-testid="initial-data">
26+
{JSON.stringify(schema.initialData)}
27+
</div>
28+
)}
29+
</div>
30+
<button
31+
data-testid="mock-submit"
32+
onClick={() => schema?.onSuccess?.({ name: 'submitted' })}
33+
>
34+
Submit
35+
</button>
36+
</div>
37+
);
38+
},
39+
}));
40+
41+
vi.mock('@object-ui/types', () => ({
42+
// DataSource type is only used for typing, provide empty default
43+
}));
44+
45+
import { EmbeddableForm } from '../EmbeddableForm';
46+
import type { EmbeddableFormConfig } from '../EmbeddableForm';
47+
48+
const baseConfig: EmbeddableFormConfig = {
49+
formId: 'test-form',
50+
objectName: 'contacts',
51+
title: 'Test Form',
52+
};
53+
54+
describe('EmbeddableForm - URL Prefill', () => {
55+
const originalLocation = window.location;
56+
57+
beforeEach(() => {
58+
capturedSchema = null;
59+
capturedOnSuccess = null;
60+
});
61+
62+
afterEach(() => {
63+
// Restore window.location
64+
Object.defineProperty(window, 'location', {
65+
writable: true,
66+
value: originalLocation,
67+
});
68+
});
69+
70+
it('reads URL search params and passes them as initialData', () => {
71+
Object.defineProperty(window, 'location', {
72+
writable: true,
73+
value: { ...originalLocation, search: '?name=John&email=john@test.com' },
74+
});
75+
76+
render(<EmbeddableForm config={baseConfig} />);
77+
78+
expect(capturedSchema).not.toBeNull();
79+
expect(capturedSchema.initialData).toEqual({
80+
name: 'John',
81+
email: 'john@test.com',
82+
});
83+
});
84+
85+
it('passes explicit prefillParams as initialData', () => {
86+
Object.defineProperty(window, 'location', {
87+
writable: true,
88+
value: { ...originalLocation, search: '' },
89+
});
90+
91+
render(
92+
<EmbeddableForm
93+
config={baseConfig}
94+
prefillParams={{ company: 'Acme', role: 'Admin' }}
95+
/>
96+
);
97+
98+
expect(capturedSchema.initialData).toEqual({
99+
company: 'Acme',
100+
role: 'Admin',
101+
});
102+
});
103+
104+
it('explicit prefillParams override URL params for the same field', () => {
105+
Object.defineProperty(window, 'location', {
106+
writable: true,
107+
value: { ...originalLocation, search: '?name=URLName&email=url@test.com' },
108+
});
109+
110+
render(
111+
<EmbeddableForm
112+
config={baseConfig}
113+
prefillParams={{ name: 'ExplicitName' }}
114+
/>
115+
);
116+
117+
// name should come from prefillParams, email should come from URL
118+
expect(capturedSchema.initialData).toEqual({
119+
name: 'ExplicitName',
120+
email: 'url@test.com',
121+
});
122+
});
123+
124+
it('passes undefined initialData when no params are provided', () => {
125+
Object.defineProperty(window, 'location', {
126+
writable: true,
127+
value: { ...originalLocation, search: '' },
128+
});
129+
130+
render(<EmbeddableForm config={baseConfig} />);
131+
132+
expect(capturedSchema.initialData).toBeUndefined();
133+
});
134+
135+
it('shows the thank-you page after successful submission', async () => {
136+
Object.defineProperty(window, 'location', {
137+
writable: true,
138+
value: { ...originalLocation, search: '' },
139+
});
140+
141+
render(
142+
<EmbeddableForm
143+
config={{
144+
...baseConfig,
145+
thankYouPage: {
146+
title: 'All Done!',
147+
message: 'We got your response.',
148+
},
149+
}}
150+
/>
151+
);
152+
153+
// Form should be visible initially
154+
expect(screen.getByText('Test Form')).toBeInTheDocument();
155+
156+
// Click mock submit to trigger onSuccess
157+
const submitBtn = screen.getByTestId('mock-submit');
158+
fireEvent.click(submitBtn);
159+
160+
// Thank-you page should appear
161+
await waitFor(() => {
162+
expect(screen.getByText('All Done!')).toBeInTheDocument();
163+
expect(screen.getByText('We got your response.')).toBeInTheDocument();
164+
});
165+
166+
// Form title should no longer be visible
167+
expect(screen.queryByText('Test Form')).not.toBeInTheDocument();
168+
});
169+
170+
it('shows default thank-you message when no custom thankYouPage is configured', async () => {
171+
Object.defineProperty(window, 'location', {
172+
writable: true,
173+
value: { ...originalLocation, search: '' },
174+
});
175+
176+
render(<EmbeddableForm config={baseConfig} />);
177+
178+
const submitBtn = screen.getByTestId('mock-submit');
179+
fireEvent.click(submitBtn);
180+
181+
await waitFor(() => {
182+
expect(screen.getByText('Thank You!')).toBeInTheDocument();
183+
expect(screen.getByText('Your submission has been received successfully.')).toBeInTheDocument();
184+
});
185+
});
186+
});

0 commit comments

Comments
 (0)