@object-ui/plugin-detail 3.3.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +21 -1
  3. package/dist/AddressField-LgHnO2Lk.js +98 -0
  4. package/dist/AutoNumberField-xZCrU0eW.js +14 -0
  5. package/dist/{AvatarField-Xuieq0ZI.js → AvatarField-Dy2XGlPz.js} +16 -15
  6. package/dist/{BooleanField-DwfMKknK.js → BooleanField-C0Clfka5.js} +11 -10
  7. package/dist/CodeField-CHUa07B6.js +23 -0
  8. package/dist/ColorField-vxHqEhcS.js +38 -0
  9. package/dist/CurrencyField-DiWjYWDo.js +49 -0
  10. package/dist/DateField-DGaRPM4P.js +22 -0
  11. package/dist/DateTimeField-8QnpsI_h.js +30 -0
  12. package/dist/EmailField-CkVgMbpI.js +26 -0
  13. package/dist/FileField-5UPV7uek.js +149 -0
  14. package/dist/FormulaField-BUgt6-Pi.js +17 -0
  15. package/dist/GeolocationField-D9T_jgG6.js +118 -0
  16. package/dist/GridField-DE_HwiIN.js +49 -0
  17. package/dist/ImageField-Dswnqtzf.js +73 -0
  18. package/dist/LocationField-gjqbE6na.js +36 -0
  19. package/dist/LookupField-BcS3LRKc.js +901 -0
  20. package/dist/{MasterDetailField-B0HTmmD7.js → MasterDetailField-BF6_-X3A.js} +20 -19
  21. package/dist/NumberField-Dj2rYmrS.js +27 -0
  22. package/dist/ObjectField-BymIojwd.js +50 -0
  23. package/dist/{PasswordField-DVTimsc3.js → PasswordField-ED_Xgqz-.js} +8 -7
  24. package/dist/PercentField-D-JKOxKC.js +61 -0
  25. package/dist/PhoneField-DSCaGYq7.js +26 -0
  26. package/dist/QRCodeField-CtcOUapi.js +73 -0
  27. package/dist/{RatingField-rRi_P0N0.js → RatingField-BDnyQFWy.js} +10 -9
  28. package/dist/RichTextField-CH6LVZQA.js +33 -0
  29. package/dist/SelectField-DE4dpkMV.js +36 -0
  30. package/dist/{SignatureField-2CnhcWI0.js → SignatureField-B1wh3f5A.js} +18 -17
  31. package/dist/{SliderField-DEpMVXko.js → SliderField-zoTCKh9n.js} +2 -1
  32. package/dist/SummaryField-BeBVT6VN.js +22 -0
  33. package/dist/TextAreaField-rfUGrRxh.js +37 -0
  34. package/dist/TextField-C_yM7ATQ.js +30 -0
  35. package/dist/TimeField-BcQmBZi9.js +22 -0
  36. package/dist/UrlField-BakaF6NI.js +31 -0
  37. package/dist/UserField-zS7y3eKb.js +76 -0
  38. package/dist/VectorField-CTZ4myDM.js +34 -0
  39. package/dist/index.js +1912 -1728
  40. package/dist/index.umd.cjs +38 -47
  41. package/dist/packages/plugin-detail/src/DetailSection.d.ts.map +1 -1
  42. package/dist/packages/plugin-detail/src/DetailView.d.ts +24 -0
  43. package/dist/packages/plugin-detail/src/DetailView.d.ts.map +1 -1
  44. package/dist/packages/plugin-detail/src/RelatedList.d.ts +8 -0
  45. package/dist/packages/plugin-detail/src/RelatedList.d.ts.map +1 -1
  46. package/dist/packages/plugin-detail/src/useDetailTranslation.d.ts.map +1 -1
  47. package/dist/plugin-detail.css +1 -2
  48. package/dist/rolldown-runtime-DnwLefa7.js +23 -0
  49. package/dist/{src-C56Ly5uG.js → src-DyUKLvMN.js} +18271 -26636
  50. package/dist/{useFieldTranslation-CkxqyB82.js → useFieldTranslation-BRgjC1oq.js} +1 -1
  51. package/package.json +33 -11
  52. package/.turbo/turbo-build.log +0 -64
  53. package/dist/AddressField-CDLSeyNx.js +0 -93
  54. package/dist/AutoNumberField-CtE7suf5.js +0 -14
  55. package/dist/CodeField-CfwgRxx2.js +0 -22
  56. package/dist/ColorField-YKHA7dBD.js +0 -37
  57. package/dist/CurrencyField-tvS3fPAF.js +0 -51
  58. package/dist/DateField-BKqXpkOh.js +0 -21
  59. package/dist/DateTimeField-CR-nJCE7.js +0 -32
  60. package/dist/EmailField-CgvW1Qal.js +0 -28
  61. package/dist/FileField-BVAme2ML.js +0 -151
  62. package/dist/FormulaField-DamJ2VaG.js +0 -14
  63. package/dist/GeolocationField-C99z7ZBM.js +0 -113
  64. package/dist/GridField-C9JbpTx_.js +0 -51
  65. package/dist/ImageField-CDANtgVV.js +0 -75
  66. package/dist/LocationField-ZSyZ0O-h.js +0 -35
  67. package/dist/LookupField-B3hQJt95.js +0 -903
  68. package/dist/LookupField-D00z6gn_.js +0 -2
  69. package/dist/NumberField-DL2QAL7X.js +0 -26
  70. package/dist/ObjectField-JYvUnuRO.js +0 -52
  71. package/dist/PercentField-DjR6BSpw.js +0 -63
  72. package/dist/PhoneField-CX1JL-jp.js +0 -28
  73. package/dist/QRCodeField-CH_1pU6R.js +0 -72
  74. package/dist/RichTextField-CJqLWlrb.js +0 -32
  75. package/dist/SelectField-DGoDoRM_.js +0 -30
  76. package/dist/SelectField-XBVI50AD.js +0 -2
  77. package/dist/SummaryField-7ch9aqAu.js +0 -19
  78. package/dist/TextAreaField-Cmw1oXcw.js +0 -36
  79. package/dist/TextField-OTLa3p51.js +0 -29
  80. package/dist/TimeField-DKPoNWoR.js +0 -21
  81. package/dist/UrlField-CxbmzP9f.js +0 -33
  82. package/dist/UserField-ChvwUkMK.js +0 -78
  83. package/dist/VectorField-BVClL8Vw.js +0 -36
  84. package/src/ActivityTimeline.tsx +0 -184
  85. package/src/CommentAttachment.tsx +0 -194
  86. package/src/CommentInput.tsx +0 -81
  87. package/src/DetailSection.tsx +0 -340
  88. package/src/DetailTabs.tsx +0 -73
  89. package/src/DetailView.stories.tsx +0 -334
  90. package/src/DetailView.tsx +0 -823
  91. package/src/DiffView.tsx +0 -233
  92. package/src/FieldChangeItem.tsx +0 -46
  93. package/src/HeaderHighlight.tsx +0 -88
  94. package/src/InlineCreateRelated.tsx +0 -291
  95. package/src/MentionAutocomplete.tsx +0 -123
  96. package/src/PointInTimeRestore.tsx +0 -261
  97. package/src/ReactionPicker.tsx +0 -106
  98. package/src/RecordActivityTimeline.tsx +0 -433
  99. package/src/RecordChatterPanel.tsx +0 -209
  100. package/src/RecordComments.tsx +0 -217
  101. package/src/RecordNavigationEnhanced.tsx +0 -213
  102. package/src/RelatedList.tsx +0 -413
  103. package/src/RelationshipGraph.tsx +0 -286
  104. package/src/RichTextCommentInput.tsx +0 -350
  105. package/src/SectionGroup.tsx +0 -101
  106. package/src/SubscriptionToggle.tsx +0 -62
  107. package/src/ThreadedReplies.tsx +0 -163
  108. package/src/__tests__/ActivityTimeline.test.tsx +0 -119
  109. package/src/__tests__/ActivityTimelineFiltering.test.tsx +0 -143
  110. package/src/__tests__/CommentInput.test.tsx +0 -57
  111. package/src/__tests__/DetailSection.test.tsx +0 -490
  112. package/src/__tests__/DetailView.test.tsx +0 -694
  113. package/src/__tests__/FieldChangeItem.test.tsx +0 -119
  114. package/src/__tests__/HeaderHighlight.test.tsx +0 -213
  115. package/src/__tests__/MentionAutocomplete.test.tsx +0 -97
  116. package/src/__tests__/ReactionPicker.test.tsx +0 -113
  117. package/src/__tests__/RecordActivityTimeline.test.tsx +0 -395
  118. package/src/__tests__/RecordChatterPanel.test.tsx +0 -265
  119. package/src/__tests__/RecordComments.test.tsx +0 -96
  120. package/src/__tests__/RecordCommentsPinSearch.test.tsx +0 -133
  121. package/src/__tests__/RelatedList.test.tsx +0 -160
  122. package/src/__tests__/SectionGroup.test.tsx +0 -101
  123. package/src/__tests__/SubscriptionToggle.test.tsx +0 -84
  124. package/src/__tests__/ThreadedReplies.test.tsx +0 -212
  125. package/src/__tests__/autoLayout.test.ts +0 -228
  126. package/src/__tests__/phase12-features.test.tsx +0 -583
  127. package/src/__tests__/roadmap-features.test.tsx +0 -478
  128. package/src/autoLayout.ts +0 -128
  129. package/src/index.tsx +0 -149
  130. package/src/useDetailTranslation.ts +0 -183
  131. package/tsconfig.json +0 -18
  132. package/vite.config.ts +0 -57
  133. package/vitest.config.ts +0 -13
  134. package/vitest.setup.ts +0 -1
@@ -1,119 +0,0 @@
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 } from 'vitest';
10
- import { render, screen } from '@testing-library/react';
11
- import '@testing-library/jest-dom';
12
- import { FieldChangeItem } from '../FieldChangeItem';
13
- import type { FieldChangeEntry } from '@object-ui/types';
14
-
15
- describe('FieldChangeItem', () => {
16
- it('should render field label with old and new display values', () => {
17
- const change: FieldChangeEntry = {
18
- field: 'status',
19
- fieldLabel: 'Status',
20
- oldDisplayValue: 'Open',
21
- newDisplayValue: 'Closed',
22
- };
23
- render(<FieldChangeItem change={change} />);
24
- expect(screen.getByText('Status')).toBeInTheDocument();
25
- expect(screen.getByText('Open')).toBeInTheDocument();
26
- expect(screen.getByText('Closed')).toBeInTheDocument();
27
- });
28
-
29
- it('should derive field label from field name when fieldLabel is not set', () => {
30
- const change: FieldChangeEntry = {
31
- field: 'first_name',
32
- oldValue: 'John',
33
- newValue: 'Jane',
34
- };
35
- render(<FieldChangeItem change={change} />);
36
- expect(screen.getByText('First name')).toBeInTheDocument();
37
- });
38
-
39
- it('should use raw values when display values are not set', () => {
40
- const change: FieldChangeEntry = {
41
- field: 'priority',
42
- fieldLabel: 'Priority',
43
- oldValue: 'low',
44
- newValue: 'high',
45
- };
46
- render(<FieldChangeItem change={change} />);
47
- expect(screen.getByText('low')).toBeInTheDocument();
48
- expect(screen.getByText('high')).toBeInTheDocument();
49
- });
50
-
51
- it('should show (empty) when value is null/undefined', () => {
52
- const change: FieldChangeEntry = {
53
- field: 'notes',
54
- fieldLabel: 'Notes',
55
- newValue: 'Some text',
56
- };
57
- render(<FieldChangeItem change={change} />);
58
- expect(screen.getByText('(empty)')).toBeInTheDocument();
59
- expect(screen.getByText('Some text')).toBeInTheDocument();
60
- });
61
-
62
- it('should apply custom className', () => {
63
- const change: FieldChangeEntry = {
64
- field: 'name',
65
- fieldLabel: 'Name',
66
- oldValue: 'A',
67
- newValue: 'B',
68
- };
69
- const { container } = render(<FieldChangeItem change={change} className="custom-class" />);
70
- expect(container.firstChild).toHaveClass('custom-class');
71
- });
72
-
73
- it('should render arrow icon between old and new values', () => {
74
- const change: FieldChangeEntry = {
75
- field: 'status',
76
- fieldLabel: 'Status',
77
- oldValue: 'Open',
78
- newValue: 'Closed',
79
- };
80
- const { container } = render(<FieldChangeItem change={change} />);
81
- // ArrowRight renders as an SVG with lucide classes
82
- const svg = container.querySelector('svg');
83
- expect(svg).toBeInTheDocument();
84
- });
85
-
86
- it('should render old value with line-through style', () => {
87
- const change: FieldChangeEntry = {
88
- field: 'status',
89
- fieldLabel: 'Status',
90
- oldDisplayValue: 'Open',
91
- newDisplayValue: 'Closed',
92
- };
93
- render(<FieldChangeItem change={change} />);
94
- const oldEl = screen.getByText('Open');
95
- expect(oldEl).toHaveClass('line-through');
96
- });
97
-
98
- it('should use fieldLabel priority over auto-generated label', () => {
99
- const change: FieldChangeEntry = {
100
- field: 'first_name',
101
- fieldLabel: 'Custom Label',
102
- oldValue: 'A',
103
- newValue: 'B',
104
- };
105
- render(<FieldChangeItem change={change} />);
106
- expect(screen.getByText('Custom Label')).toBeInTheDocument();
107
- expect(screen.queryByText('First name')).not.toBeInTheDocument();
108
- });
109
-
110
- it('should show (empty) for both null old and new values', () => {
111
- const change: FieldChangeEntry = {
112
- field: 'notes',
113
- fieldLabel: 'Notes',
114
- };
115
- render(<FieldChangeItem change={change} />);
116
- const emptyTexts = screen.getAllByText('(empty)');
117
- expect(emptyTexts).toHaveLength(2);
118
- });
119
- });
@@ -1,213 +0,0 @@
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 } from 'vitest';
10
- import { render, screen } from '@testing-library/react';
11
- import { HeaderHighlight } from '../HeaderHighlight';
12
- import type { HighlightField } from '@object-ui/types';
13
-
14
- describe('HeaderHighlight', () => {
15
- const fields: HighlightField[] = [
16
- { name: 'revenue', label: 'Annual Revenue' },
17
- { name: 'employees', label: 'Employees' },
18
- { name: 'industry', label: 'Industry' },
19
- ];
20
-
21
- const data = {
22
- revenue: '$5M',
23
- employees: 150,
24
- industry: 'Technology',
25
- };
26
-
27
- it('should render highlight fields with labels and values', () => {
28
- render(<HeaderHighlight fields={fields} data={data} />);
29
- expect(screen.getByText('Annual Revenue')).toBeInTheDocument();
30
- expect(screen.getByText('$5M')).toBeInTheDocument();
31
- expect(screen.getByText('Employees')).toBeInTheDocument();
32
- expect(screen.getByText('150')).toBeInTheDocument();
33
- expect(screen.getByText('Industry')).toBeInTheDocument();
34
- expect(screen.getByText('Technology')).toBeInTheDocument();
35
- });
36
-
37
- it('should not render when no data is provided', () => {
38
- const { container } = render(<HeaderHighlight fields={fields} />);
39
- expect(container.innerHTML).toBe('');
40
- });
41
-
42
- it('should not render when fields array is empty', () => {
43
- const { container } = render(<HeaderHighlight fields={[]} data={data} />);
44
- expect(container.innerHTML).toBe('');
45
- });
46
-
47
- it('should hide fields with null or empty values', () => {
48
- const sparseData = { revenue: '$5M', employees: null, industry: '' };
49
- render(<HeaderHighlight fields={fields} data={sparseData} />);
50
- expect(screen.getByText('$5M')).toBeInTheDocument();
51
- expect(screen.queryByText('Employees')).not.toBeInTheDocument();
52
- expect(screen.queryByText('Industry')).not.toBeInTheDocument();
53
- });
54
-
55
- it('should not render when all field values are empty', () => {
56
- const emptyData = { revenue: null, employees: undefined, industry: '' };
57
- const { container } = render(<HeaderHighlight fields={fields} data={emptyData} />);
58
- expect(container.innerHTML).toBe('');
59
- });
60
-
61
- it('should render icon when provided', () => {
62
- const fieldsWithIcon: HighlightField[] = [
63
- { name: 'revenue', label: 'Revenue', icon: '💰' },
64
- ];
65
- render(<HeaderHighlight fields={fieldsWithIcon} data={{ revenue: '$5M' }} />);
66
- expect(screen.getByText('💰')).toBeInTheDocument();
67
- });
68
-
69
- it('should render currency fields with formatted value via CellRenderer', () => {
70
- const currencyFields: HighlightField[] = [
71
- { name: 'amount', label: 'Amount', type: 'currency' },
72
- ];
73
- render(<HeaderHighlight fields={currencyFields} data={{ amount: 250000 }} />);
74
- // CurrencyCellRenderer should format — should NOT show raw "250000"
75
- expect(screen.queryByText('250000')).not.toBeInTheDocument();
76
- expect(screen.getByText(/250,000/)).toBeInTheDocument();
77
- });
78
-
79
- it('should render select fields as badge via CellRenderer', () => {
80
- const selectFields: HighlightField[] = [
81
- { name: 'stage', label: 'Stage', type: 'select' },
82
- ];
83
- render(
84
- <HeaderHighlight
85
- fields={selectFields}
86
- data={{ stage: 'prospecting' }}
87
- objectSchema={{
88
- fields: {
89
- stage: {
90
- type: 'select',
91
- options: [
92
- { value: 'prospecting', label: 'Prospecting', color: 'blue' },
93
- ],
94
- },
95
- },
96
- }}
97
- />
98
- );
99
- expect(screen.getByText('Prospecting')).toBeInTheDocument();
100
- });
101
-
102
- it('should enrich field type from objectSchema when field.type is not set', () => {
103
- const fieldsNoType: HighlightField[] = [
104
- { name: 'amount', label: 'Amount' },
105
- ];
106
- render(
107
- <HeaderHighlight
108
- fields={fieldsNoType}
109
- data={{ amount: 5000 }}
110
- objectSchema={{
111
- fields: {
112
- amount: { type: 'currency', currency: 'USD' },
113
- },
114
- }}
115
- />
116
- );
117
- // CurrencyCellRenderer should format, not raw String()
118
- expect(screen.queryByText('5000')).not.toBeInTheDocument();
119
- expect(screen.getByText(/5,000/)).toBeInTheDocument();
120
- });
121
-
122
- it('should fall back to text when no type info is available', () => {
123
- const fieldsNoType: HighlightField[] = [
124
- { name: 'custom', label: 'Custom' },
125
- ];
126
- render(<HeaderHighlight fields={fieldsNoType} data={{ custom: 'raw-value' }} />);
127
- expect(screen.getByText('raw-value')).toBeInTheDocument();
128
- });
129
-
130
- it('should safely render object values without crashing (React error #310 guard)', () => {
131
- // Simulates MongoDB Decimal128 or expanded reference objects
132
- const fieldsWithNumber: HighlightField[] = [
133
- { name: 'amount', label: 'Amount' },
134
- { name: 'account', label: 'Account' },
135
- ];
136
- const objectData = {
137
- amount: { $numberDecimal: '250000' },
138
- account: { id: 'abc', name: 'Acme Corp' },
139
- };
140
- const objectSchema = {
141
- fields: {
142
- amount: { type: 'number' },
143
- account: { type: 'text' },
144
- },
145
- };
146
- // Should NOT crash — cell renderers coerce values via coerceToSafeValue
147
- const { container } = render(
148
- <HeaderHighlight
149
- fields={fieldsWithNumber}
150
- data={objectData}
151
- objectSchema={objectSchema}
152
- />
153
- );
154
- expect(container.innerHTML).not.toBe('');
155
- // NumberCellRenderer coerces $numberDecimal to number
156
- expect(screen.getByText('250,000')).toBeInTheDocument();
157
- // TextCellRenderer extracts name from object
158
- expect(screen.getByText('Acme Corp')).toBeInTheDocument();
159
- });
160
-
161
- it('should safely render lookup object values via LookupCellRenderer', () => {
162
- const lookupFields: HighlightField[] = [
163
- { name: 'owner', label: 'Owner' },
164
- ];
165
- const lookupData = {
166
- owner: { id: 'u1', name: 'Jane Doe' },
167
- };
168
- const objectSchema = {
169
- fields: {
170
- owner: { type: 'lookup' },
171
- },
172
- };
173
- // LookupCellRenderer handles object values natively
174
- render(
175
- <HeaderHighlight
176
- fields={lookupFields}
177
- data={lookupData}
178
- objectSchema={objectSchema}
179
- />
180
- );
181
- expect(screen.getByText('Jane Doe')).toBeInTheDocument();
182
- });
183
-
184
- it('should safely render array values via TextCellRenderer', () => {
185
- const arrayFields: HighlightField[] = [
186
- { name: 'tags', label: 'Tags' },
187
- ];
188
- const arrayData = {
189
- tags: ['urgent', 'follow-up'],
190
- };
191
- const { container } = render(
192
- <HeaderHighlight fields={arrayFields} data={arrayData} />
193
- );
194
- expect(container.innerHTML).not.toBe('');
195
- // TextCellRenderer coerces arrays to comma-separated string
196
- expect(screen.getByText('urgent, follow-up')).toBeInTheDocument();
197
- });
198
-
199
- it('should safely render array of objects via TextCellRenderer', () => {
200
- const arrayFields: HighlightField[] = [
201
- { name: 'contacts', label: 'Contacts' },
202
- ];
203
- const arrayData = {
204
- contacts: [{ name: 'Alice' }, { name: 'Bob' }],
205
- };
206
- const { container } = render(
207
- <HeaderHighlight fields={arrayFields} data={arrayData} />
208
- );
209
- expect(container.innerHTML).not.toBe('');
210
- // TextCellRenderer coerces array of objects to "Alice, Bob"
211
- expect(screen.getByText('Alice, Bob')).toBeInTheDocument();
212
- });
213
- });
@@ -1,97 +0,0 @@
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 } from 'vitest';
10
- import { render, screen, fireEvent } from '@testing-library/react';
11
- import '@testing-library/jest-dom';
12
- import { MentionAutocomplete, createMentionFromSuggestion } from '../MentionAutocomplete';
13
- import type { MentionSuggestionItem } from '../MentionAutocomplete';
14
-
15
- const mockSuggestions: MentionSuggestionItem[] = [
16
- { id: 'u1', name: 'Alice Smith', type: 'user' },
17
- { id: 'u2', name: 'Bob Johnson', type: 'user', avatarUrl: 'https://example.com/bob.jpg' },
18
- { id: 't1', name: 'Engineering', type: 'team' },
19
- ];
20
-
21
- describe('MentionAutocomplete', () => {
22
- it('should render suggestions when visible', () => {
23
- const onSelect = vi.fn();
24
- render(
25
- <MentionAutocomplete query="" suggestions={mockSuggestions} onSelect={onSelect} visible />,
26
- );
27
- expect(screen.getByText('Alice Smith')).toBeInTheDocument();
28
- expect(screen.getByText('Bob Johnson')).toBeInTheDocument();
29
- expect(screen.getByText('Engineering')).toBeInTheDocument();
30
- });
31
-
32
- it('should not render when not visible', () => {
33
- const onSelect = vi.fn();
34
- const { container } = render(
35
- <MentionAutocomplete query="" suggestions={mockSuggestions} onSelect={onSelect} visible={false} />,
36
- );
37
- expect(container.firstChild).toBeNull();
38
- });
39
-
40
- it('should filter suggestions by query', () => {
41
- const onSelect = vi.fn();
42
- render(
43
- <MentionAutocomplete query="Ali" suggestions={mockSuggestions} onSelect={onSelect} visible />,
44
- );
45
- expect(screen.getByText('Alice Smith')).toBeInTheDocument();
46
- expect(screen.queryByText('Bob Johnson')).not.toBeInTheDocument();
47
- });
48
-
49
- it('should show type label for non-user types', () => {
50
- const onSelect = vi.fn();
51
- render(
52
- <MentionAutocomplete query="Eng" suggestions={mockSuggestions} onSelect={onSelect} visible />,
53
- );
54
- expect(screen.getByText('(team)')).toBeInTheDocument();
55
- });
56
-
57
- it('should call onSelect when a suggestion is clicked', () => {
58
- const onSelect = vi.fn();
59
- render(
60
- <MentionAutocomplete query="" suggestions={mockSuggestions} onSelect={onSelect} visible />,
61
- );
62
- fireEvent.mouseDown(screen.getByText('Alice Smith'));
63
- expect(onSelect).toHaveBeenCalledWith(mockSuggestions[0]);
64
- });
65
-
66
- it('should render avatar image when avatarUrl is present', () => {
67
- const onSelect = vi.fn();
68
- render(
69
- <MentionAutocomplete query="" suggestions={mockSuggestions} onSelect={onSelect} visible />,
70
- );
71
- const img = screen.getByAltText('Bob Johnson');
72
- expect(img).toBeInTheDocument();
73
- expect(img).toHaveAttribute('src', 'https://example.com/bob.jpg');
74
- });
75
-
76
- it('should not render when no matching suggestions', () => {
77
- const onSelect = vi.fn();
78
- const { container } = render(
79
- <MentionAutocomplete query="xyz" suggestions={mockSuggestions} onSelect={onSelect} visible />,
80
- );
81
- expect(container.firstChild).toBeNull();
82
- });
83
- });
84
-
85
- describe('createMentionFromSuggestion', () => {
86
- it('should create a Mention object from a suggestion item', () => {
87
- const item: MentionSuggestionItem = { id: 'u1', name: 'Alice', type: 'user' };
88
- const mention = createMentionFromSuggestion(item, 5, 6);
89
- expect(mention).toEqual({
90
- type: 'user',
91
- id: 'u1',
92
- name: 'Alice',
93
- offset: 5,
94
- length: 6,
95
- });
96
- });
97
- });
@@ -1,113 +0,0 @@
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 } from 'vitest';
10
- import { render, screen, fireEvent } from '@testing-library/react';
11
- import '@testing-library/jest-dom';
12
- import { ReactionPicker } from '../ReactionPicker';
13
- import type { Reaction } from '@object-ui/types';
14
-
15
- const mockReactions: Reaction[] = [
16
- { emoji: '👍', count: 3, reacted: true },
17
- { emoji: '❤️', count: 1, reacted: false },
18
- ];
19
-
20
- describe('ReactionPicker', () => {
21
- it('should render existing reactions with counts', () => {
22
- render(<ReactionPicker reactions={mockReactions} />);
23
- expect(screen.getByText('👍')).toBeInTheDocument();
24
- expect(screen.getByText('3')).toBeInTheDocument();
25
- expect(screen.getByText('❤️')).toBeInTheDocument();
26
- expect(screen.getByText('1')).toBeInTheDocument();
27
- });
28
-
29
- it('should highlight reacted emoji', () => {
30
- render(<ReactionPicker reactions={mockReactions} />);
31
- const thumbs = screen.getByLabelText(/👍 3/);
32
- expect(thumbs).toHaveClass('bg-primary/10');
33
- });
34
-
35
- it('should show add reaction button when onToggleReaction provided', () => {
36
- const onToggle = vi.fn();
37
- render(<ReactionPicker reactions={[]} onToggleReaction={onToggle} />);
38
- expect(screen.getByLabelText('Add reaction')).toBeInTheDocument();
39
- });
40
-
41
- it('should not show add button when no onToggleReaction', () => {
42
- render(<ReactionPicker reactions={[]} />);
43
- expect(screen.queryByLabelText('Add reaction')).not.toBeInTheDocument();
44
- });
45
-
46
- it('should call onToggleReaction when clicking existing reaction', () => {
47
- const onToggle = vi.fn();
48
- render(<ReactionPicker reactions={mockReactions} onToggleReaction={onToggle} />);
49
- fireEvent.click(screen.getByLabelText(/👍 3/));
50
- expect(onToggle).toHaveBeenCalledWith('👍');
51
- });
52
-
53
- it('should show emoji picker when add button is clicked', () => {
54
- const onToggle = vi.fn();
55
- render(<ReactionPicker reactions={[]} onToggleReaction={onToggle} />);
56
- fireEvent.click(screen.getByLabelText('Add reaction'));
57
- expect(screen.getByRole('listbox', { name: 'Emoji picker' })).toBeInTheDocument();
58
- });
59
-
60
- it('should call onToggleReaction when emoji is selected from picker', () => {
61
- const onToggle = vi.fn();
62
- render(<ReactionPicker reactions={[]} onToggleReaction={onToggle} />);
63
- fireEvent.click(screen.getByLabelText('Add reaction'));
64
- // Select first emoji option (👍)
65
- const options = screen.getAllByRole('option');
66
- fireEvent.click(options[0]);
67
- expect(onToggle).toHaveBeenCalledWith('👍');
68
- });
69
-
70
- it('should disable reaction buttons when no onToggleReaction', () => {
71
- render(<ReactionPicker reactions={mockReactions} />);
72
- const thumbsBtn = screen.getByLabelText(/👍 3/);
73
- expect(thumbsBtn).toBeDisabled();
74
- const heartBtn = screen.getByLabelText(/❤️ 1/);
75
- expect(heartBtn).toBeDisabled();
76
- });
77
-
78
- it('should render custom emojiOptions', () => {
79
- const onToggle = vi.fn();
80
- const customEmoji = ['🚀', '🔥', '✅'];
81
- render(
82
- <ReactionPicker reactions={[]} onToggleReaction={onToggle} emojiOptions={customEmoji} />,
83
- );
84
- fireEvent.click(screen.getByLabelText('Add reaction'));
85
- const options = screen.getAllByRole('option');
86
- expect(options).toHaveLength(3);
87
- expect(options[0]).toHaveTextContent('🚀');
88
- expect(options[1]).toHaveTextContent('🔥');
89
- expect(options[2]).toHaveTextContent('✅');
90
- });
91
-
92
- it('should include emoji and count in aria-label', () => {
93
- render(<ReactionPicker reactions={mockReactions} />);
94
- expect(screen.getByLabelText('👍 3 reactions')).toBeInTheDocument();
95
- expect(screen.getByLabelText('❤️ 1 reaction')).toBeInTheDocument();
96
- });
97
-
98
- it('should show non-reacted emoji with bg-muted style', () => {
99
- render(<ReactionPicker reactions={mockReactions} />);
100
- const heart = screen.getByLabelText(/❤️ 1/);
101
- expect(heart).toHaveClass('bg-muted');
102
- });
103
-
104
- it('should close picker after selecting emoji', () => {
105
- const onToggle = vi.fn();
106
- render(<ReactionPicker reactions={[]} onToggleReaction={onToggle} />);
107
- fireEvent.click(screen.getByLabelText('Add reaction'));
108
- expect(screen.getByRole('listbox', { name: 'Emoji picker' })).toBeInTheDocument();
109
- const options = screen.getAllByRole('option');
110
- fireEvent.click(options[0]);
111
- expect(screen.queryByRole('listbox', { name: 'Emoji picker' })).not.toBeInTheDocument();
112
- });
113
- });