@object-ui/plugin-timeline 3.0.3 → 3.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/plugin-timeline",
3
- "version": "3.0.3",
3
+ "version": "3.1.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Timeline component plugin for Object UI",
@@ -24,26 +24,27 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@objectstack/spec": "^3.0.4",
27
+ "@objectstack/spec": "^3.2.1",
28
+ "class-variance-authority": "^0.7.1",
28
29
  "zod": "^4.3.6",
29
- "@object-ui/core": "3.0.3",
30
- "@object-ui/components": "3.0.3",
31
- "@object-ui/mobile": "3.0.3",
32
- "@object-ui/react": "3.0.3",
33
- "@object-ui/types": "3.0.3"
30
+ "@object-ui/components": "3.1.1",
31
+ "@object-ui/core": "3.1.1",
32
+ "@object-ui/mobile": "3.1.1",
33
+ "@object-ui/types": "3.1.1",
34
+ "@object-ui/react": "3.1.1"
34
35
  },
35
36
  "peerDependencies": {
36
37
  "react": "^18.0.0 || ^19.0.0",
37
38
  "react-dom": "^18.0.0 || ^19.0.0"
38
39
  },
39
40
  "devDependencies": {
40
- "@types/react": "19.2.13",
41
+ "@types/react": "19.2.14",
41
42
  "@types/react-dom": "19.2.3",
42
43
  "@vitejs/plugin-react": "^5.1.4",
43
44
  "typescript": "^5.9.3",
44
45
  "vite": "^7.3.1",
45
46
  "vite-plugin-dts": "^4.5.4",
46
- "@object-ui/data-objectstack": "3.0.3"
47
+ "@object-ui/data-objectstack": "3.1.1"
47
48
  },
48
49
  "scripts": {
49
50
  "build": "vite build",
@@ -15,6 +15,7 @@ vi.mock('@object-ui/react', () => ({
15
15
  close: vi.fn(),
16
16
  setIsOpen: vi.fn(),
17
17
  mode: 'overlay',
18
+ view: undefined,
18
19
  }),
19
20
  }));
20
21
 
@@ -66,7 +67,7 @@ describe('ObjectTimeline', () => {
66
67
  type: 'timeline',
67
68
  objectName: 'events',
68
69
  titleField: 'name',
69
- dateField: 'date' // Mapping needs to be correct in logic
70
+ dateField: 'date' // Backward-compat: legacy dateField still works
70
71
  };
71
72
 
72
73
  render(<ObjectTimeline schema={schema} dataSource={mockDataSource} />);
@@ -79,4 +80,59 @@ describe('ObjectTimeline', () => {
79
80
  expect(screen.getByText('Event 1')).toBeDefined();
80
81
  });
81
82
  });
83
+
84
+ it('uses spec-compliant startDateField property', async () => {
85
+ const dataWithDates = [
86
+ { id: '1', name: 'Sprint 1', start_date: '2024-01-01', end_date: '2024-01-14', team: 'Alpha' },
87
+ ];
88
+ (mockDataSource.find as any).mockResolvedValue(dataWithDates);
89
+
90
+ const schema: any = {
91
+ type: 'timeline',
92
+ objectName: 'sprints',
93
+ titleField: 'name',
94
+ startDateField: 'start_date',
95
+ endDateField: 'end_date',
96
+ };
97
+
98
+ render(<ObjectTimeline schema={schema} dataSource={mockDataSource} />);
99
+
100
+ await waitFor(() => {
101
+ expect(screen.getByText('Sprint 1')).toBeDefined();
102
+ });
103
+ });
104
+
105
+ it('supports groupByField and colorField properties', async () => {
106
+ const dataWithGroups = [
107
+ { id: '1', name: 'Task A', start_date: '2024-01-01', team: 'Alpha', priority: 'high' },
108
+ ];
109
+ (mockDataSource.find as any).mockResolvedValue(dataWithGroups);
110
+
111
+ const schema: any = {
112
+ type: 'timeline',
113
+ objectName: 'tasks',
114
+ titleField: 'name',
115
+ startDateField: 'start_date',
116
+ groupByField: 'team',
117
+ colorField: 'priority',
118
+ };
119
+
120
+ render(<ObjectTimeline schema={schema} dataSource={mockDataSource} />);
121
+
122
+ await waitFor(() => {
123
+ expect(screen.getByText('Task A')).toBeDefined();
124
+ });
125
+ });
126
+
127
+ it('supports scale property', () => {
128
+ const schema: any = {
129
+ type: 'timeline',
130
+ items: [
131
+ { title: 'Weekly Event', date: '2024-01-01' }
132
+ ],
133
+ scale: 'week',
134
+ };
135
+ render(<ObjectTimeline schema={schema} />);
136
+ expect(screen.getByText('Weekly Event')).toBeDefined();
137
+ });
82
138
  });
@@ -7,9 +7,10 @@
7
7
  */
8
8
 
9
9
  import React, { useEffect, useState, useCallback } from 'react';
10
- import type { DataSource, TimelineSchema } from '@object-ui/types';
10
+ import type { DataSource, TimelineSchema, TimelineConfig } from '@object-ui/types';
11
11
  import { useDataScope, useNavigationOverlay } from '@object-ui/react';
12
12
  import { NavigationOverlay } from '@object-ui/components';
13
+ import { extractRecords, buildExpandFields } from '@object-ui/core';
13
14
  import { usePullToRefresh } from '@object-ui/mobile';
14
15
  import { z } from 'zod';
15
16
  import { TimelineRenderer } from './renderer';
@@ -25,16 +26,36 @@ const TimelineExtensionSchema = z.object({
25
26
  mapping: TimelineMappingSchema.optional(),
26
27
  objectName: z.string().optional(),
27
28
  titleField: z.string().optional(),
29
+ /** @deprecated Use startDateField instead */
28
30
  dateField: z.string().optional(),
31
+ startDateField: z.string().optional(),
32
+ endDateField: z.string().optional(),
29
33
  descriptionField: z.string().optional(),
34
+ groupByField: z.string().optional(),
35
+ colorField: z.string().optional(),
36
+ scale: z.enum(['hour', 'day', 'week', 'month', 'quarter', 'year']).optional(),
30
37
  });
31
38
 
32
39
  export interface ObjectTimelineProps {
33
40
  schema: TimelineSchema & {
34
41
  objectName?: string;
42
+ /** Spec-compliant nested timeline config */
43
+ timeline?: TimelineConfig;
44
+ /** @deprecated Use timeline.titleField instead */
35
45
  titleField?: string;
46
+ /** @deprecated Use timeline.startDateField instead */
36
47
  dateField?: string;
48
+ /** @deprecated Use timeline.startDateField instead */
49
+ startDateField?: string;
50
+ /** @deprecated Use timeline.endDateField instead */
51
+ endDateField?: string;
37
52
  descriptionField?: string;
53
+ /** @deprecated Use timeline.groupByField instead */
54
+ groupByField?: string;
55
+ /** @deprecated Use timeline.colorField instead */
56
+ colorField?: string;
57
+ /** @deprecated Use timeline.scale instead */
58
+ scale?: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
38
59
  // Map data fields to timeline item properties
39
60
  mapping?: {
40
61
  title?: string;
@@ -61,6 +82,10 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
61
82
  const [loading, setLoading] = useState(false);
62
83
  const [error, setError] = useState<Error | null>(null);
63
84
  const [refreshKey, setRefreshKey] = useState(0);
85
+ const [objectDef, setObjectDef] = useState<any>(null);
86
+
87
+ // Resolve nested TimelineConfig (spec-compliant)
88
+ const timelineConfig = schema.timeline;
64
89
 
65
90
  useEffect(() => {
66
91
  const result = TimelineExtensionSchema.safeParse(schema);
@@ -71,24 +96,35 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
71
96
 
72
97
  const boundData = useDataScope(schema.bind);
73
98
 
99
+ // Fetch object definition for metadata
100
+ useEffect(() => {
101
+ let isMounted = true;
102
+ const fetchMeta = async () => {
103
+ if (!dataSource || typeof dataSource.getObjectSchema !== 'function' || !schema.objectName) return;
104
+ try {
105
+ const def = await dataSource.getObjectSchema(schema.objectName);
106
+ if (isMounted) setObjectDef(def);
107
+ } catch (e) {
108
+ console.warn('Failed to fetch object def for ObjectTimeline', e);
109
+ }
110
+ };
111
+ fetchMeta();
112
+ return () => { isMounted = false; };
113
+ }, [schema.objectName, dataSource]);
114
+
74
115
  useEffect(() => {
75
116
  const fetchData = async () => {
76
- if (!dataSource || !schema.objectName) return;
117
+ if (!dataSource || typeof dataSource.find !== 'function' || !schema.objectName) return;
77
118
  setLoading(true);
78
119
  try {
120
+ // Auto-inject $expand for lookup/master_detail fields
121
+ const expand = buildExpandFields(objectDef?.fields);
79
122
  const results = await dataSource.find(schema.objectName, {
80
- options: { $top: 100 }
123
+ options: { $top: 100 },
124
+ ...(expand.length > 0 ? { $expand: expand } : {}),
81
125
  });
82
- let data = results;
83
- if ((results as any).records) {
84
- data = (results as any).records;
85
- } else if ((results as any).data) {
86
- data = (results as any).data;
87
- }
88
-
89
- if (Array.isArray(data)) {
90
- setFetchedData(data);
91
- }
126
+ const data = extractRecords(results);
127
+ setFetchedData(data);
92
128
  } catch (e) {
93
129
  console.error(e);
94
130
  setError(e as Error);
@@ -100,7 +136,7 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
100
136
  if (schema.objectName && !boundData && !schema.items && !(props as any).data) {
101
137
  fetchData();
102
138
  }
103
- }, [schema.objectName, dataSource, boundData, schema.items, (props as any).data, refreshKey]);
139
+ }, [schema.objectName, dataSource, boundData, schema.items, (props as any).data, refreshKey, objectDef]);
104
140
 
105
141
  const rawData = (props as any).data || boundData || fetchedData;
106
142
 
@@ -108,19 +144,27 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
108
144
  let effectiveItems = schema.items;
109
145
 
110
146
  if (!effectiveItems && rawData && Array.isArray(rawData)) {
111
- const titleField = schema.mapping?.title || schema.titleField || 'name';
112
- const dateField = schema.mapping?.date || schema.dateField || 'date';
113
- const descField = schema.mapping?.description || schema.descriptionField || 'description';
114
- const variantField = schema.mapping?.variant || 'variant';
147
+ // Resolve TimelineConfig with backwards-compatible fallbacks
148
+ const titleField = timelineConfig?.titleField ?? schema.mapping?.title ?? schema.titleField ?? 'name';
149
+ // Spec-compliant: prefer timeline.startDateField, fallback to flat props
150
+ const startDateField = timelineConfig?.startDateField ?? schema.mapping?.date ?? schema.startDateField ?? schema.dateField ?? 'date';
151
+ const endDateField = timelineConfig?.endDateField ?? schema.endDateField ?? startDateField;
152
+ const descField = schema.mapping?.description ?? schema.descriptionField ?? 'description';
153
+ const variantField = schema.mapping?.variant ?? 'variant';
154
+ const groupByField = timelineConfig?.groupByField ?? schema.groupByField;
155
+ const colorField = timelineConfig?.colorField ?? schema.colorField;
115
156
 
116
157
  effectiveItems = rawData.map(item => ({
117
158
  title: item[titleField],
118
159
  // Support both 'time' (vertical) and 'startDate' (gantt)
119
- time: item[dateField],
120
- startDate: item[dateField],
121
- endDate: item[dateField], // Default single point
160
+ time: item[startDateField],
161
+ startDate: item[startDateField],
162
+ endDate: item[endDateField],
122
163
  description: item[descField],
123
164
  variant: item[variantField] || 'default',
165
+ // Spec-compliant: group and color support
166
+ ...(groupByField ? { group: item[groupByField] } : {}),
167
+ ...(colorField ? { color: item[colorField] } : {}),
124
168
  // Pass original item for click handlers
125
169
  _data: item
126
170
  }));
@@ -141,10 +185,15 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
141
185
  onRowClick: onRowClick ?? onItemClick,
142
186
  });
143
187
 
188
+ // Resolve scale: spec timeline.scale takes priority over flat schema.scale
189
+ const resolvedScale = timelineConfig?.scale ?? schema.scale;
190
+
144
191
  const effectiveSchema = {
145
192
  ...schema,
146
193
  items: effectiveItems || [],
147
194
  className: className || schema.className,
195
+ // Map spec 'scale' to renderer 'timeScale' (used by gantt variant)
196
+ ...(resolvedScale ? { timeScale: resolvedScale } : {}),
148
197
  onItemClick: (item: any) => {
149
198
  const record = item._data || item;
150
199
  navigation.handleClick(record);
@@ -161,7 +210,7 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
161
210
  }
162
211
 
163
212
  return (
164
- <div ref={pullRef} className="relative overflow-auto h-full">
213
+ <div ref={pullRef} className="relative overflow-auto h-full min-w-0">
165
214
  {pullDistance > 0 && (
166
215
  <div
167
216
  className="flex items-center justify-center text-xs text-muted-foreground"
@@ -0,0 +1,232 @@
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 } from '@testing-library/react';
11
+ import '@testing-library/jest-dom';
12
+ import { ObjectTimeline } from '../ObjectTimeline';
13
+
14
+ // Mock dependencies
15
+ vi.mock('@object-ui/react', () => ({
16
+ useDataScope: () => undefined,
17
+ useNavigationOverlay: () => ({
18
+ isOverlay: false,
19
+ handleClick: vi.fn(),
20
+ selectedRecord: null,
21
+ isOpen: false,
22
+ close: vi.fn(),
23
+ setIsOpen: vi.fn(),
24
+ mode: 'page' as const,
25
+ width: undefined,
26
+ view: undefined,
27
+ open: vi.fn(),
28
+ }),
29
+ }));
30
+
31
+ vi.mock('@object-ui/components', () => ({
32
+ NavigationOverlay: () => null,
33
+ }));
34
+
35
+ vi.mock('@object-ui/mobile', () => ({
36
+ usePullToRefresh: () => ({
37
+ ref: { current: null },
38
+ isRefreshing: false,
39
+ pullDistance: 0,
40
+ }),
41
+ }));
42
+
43
+ // Mock the TimelineRenderer to inspect the schema it receives
44
+ const mockTimelineRenderer = vi.fn(() => <div data-testid="timeline-renderer" />);
45
+ vi.mock('../renderer', () => ({
46
+ TimelineRenderer: (props: any) => mockTimelineRenderer(props),
47
+ }));
48
+
49
+ describe('ObjectTimeline Spec Config', () => {
50
+ beforeEach(() => {
51
+ mockTimelineRenderer.mockClear();
52
+ });
53
+
54
+ const mockData = [
55
+ { id: '1', name: 'Event A', start_date: '2024-01-01', end_date: '2024-01-15', category: 'work', priority_color: 'red' },
56
+ { id: '2', name: 'Event B', start_date: '2024-02-01', end_date: '2024-02-28', category: 'personal', priority_color: 'blue' },
57
+ ];
58
+
59
+ describe('nested timeline config', () => {
60
+ it('uses timeline.startDateField to resolve start dates', () => {
61
+ const schema: any = {
62
+ type: 'timeline' as const,
63
+ objectName: 'events',
64
+ timeline: { startDateField: 'start_date', titleField: 'name' },
65
+ };
66
+ render(<ObjectTimeline schema={schema} data={mockData} />);
67
+ expect(mockTimelineRenderer).toHaveBeenCalled();
68
+ const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
69
+ expect(renderedSchema.items[0].time).toBe('2024-01-01');
70
+ expect(renderedSchema.items[0].startDate).toBe('2024-01-01');
71
+ });
72
+
73
+ it('uses timeline.titleField to resolve titles', () => {
74
+ const schema: any = {
75
+ type: 'timeline' as const,
76
+ objectName: 'events',
77
+ timeline: { startDateField: 'start_date', titleField: 'name' },
78
+ };
79
+ render(<ObjectTimeline schema={schema} data={mockData} />);
80
+ const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
81
+ expect(renderedSchema.items[0].title).toBe('Event A');
82
+ expect(renderedSchema.items[1].title).toBe('Event B');
83
+ });
84
+
85
+ it('uses timeline.endDateField to resolve end dates', () => {
86
+ const schema: any = {
87
+ type: 'timeline' as const,
88
+ objectName: 'events',
89
+ timeline: { startDateField: 'start_date', endDateField: 'end_date', titleField: 'name' },
90
+ };
91
+ render(<ObjectTimeline schema={schema} data={mockData} />);
92
+ const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
93
+ expect(renderedSchema.items[0].endDate).toBe('2024-01-15');
94
+ });
95
+
96
+ it('uses timeline.groupByField to add group property', () => {
97
+ const schema: any = {
98
+ type: 'timeline' as const,
99
+ objectName: 'events',
100
+ timeline: { startDateField: 'start_date', titleField: 'name', groupByField: 'category' },
101
+ };
102
+ render(<ObjectTimeline schema={schema} data={mockData} />);
103
+ const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
104
+ expect(renderedSchema.items[0].group).toBe('work');
105
+ expect(renderedSchema.items[1].group).toBe('personal');
106
+ });
107
+
108
+ it('uses timeline.colorField to add color property', () => {
109
+ const schema: any = {
110
+ type: 'timeline' as const,
111
+ objectName: 'events',
112
+ timeline: { startDateField: 'start_date', titleField: 'name', colorField: 'priority_color' },
113
+ };
114
+ render(<ObjectTimeline schema={schema} data={mockData} />);
115
+ const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
116
+ expect(renderedSchema.items[0].color).toBe('red');
117
+ expect(renderedSchema.items[1].color).toBe('blue');
118
+ });
119
+
120
+ it('uses timeline.scale to set timeScale on rendered schema', () => {
121
+ const schema: any = {
122
+ type: 'timeline' as const,
123
+ objectName: 'events',
124
+ timeline: { startDateField: 'start_date', titleField: 'name', scale: 'month' },
125
+ };
126
+ render(<ObjectTimeline schema={schema} data={mockData} />);
127
+ const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
128
+ expect(renderedSchema.timeScale).toBe('month');
129
+ });
130
+
131
+ it('timeline.scale supports all valid values', () => {
132
+ const scales = ['hour', 'day', 'week', 'month', 'quarter', 'year'] as const;
133
+ scales.forEach((scale) => {
134
+ mockTimelineRenderer.mockClear();
135
+ const schema: any = {
136
+ type: 'timeline' as const,
137
+ objectName: 'events',
138
+ timeline: { startDateField: 'start_date', titleField: 'name', scale },
139
+ };
140
+ render(<ObjectTimeline schema={schema} data={mockData} />);
141
+ const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
142
+ expect(renderedSchema.timeScale).toBe(scale);
143
+ });
144
+ });
145
+ });
146
+
147
+ describe('backward compatibility', () => {
148
+ it('falls back to flat startDateField when timeline config is not set', () => {
149
+ const schema: any = {
150
+ type: 'timeline' as const,
151
+ objectName: 'events',
152
+ startDateField: 'start_date',
153
+ titleField: 'name',
154
+ };
155
+ render(<ObjectTimeline schema={schema} data={mockData} />);
156
+ const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
157
+ expect(renderedSchema.items[0].time).toBe('2024-01-01');
158
+ });
159
+
160
+ it('falls back to legacy dateField when startDateField is not set', () => {
161
+ const data = [{ id: '1', name: 'Event', legacy_date: '2024-03-15' }];
162
+ const schema: any = {
163
+ type: 'timeline' as const,
164
+ objectName: 'events',
165
+ dateField: 'legacy_date',
166
+ titleField: 'name',
167
+ };
168
+ render(<ObjectTimeline schema={schema} data={data} />);
169
+ const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
170
+ expect(renderedSchema.items[0].time).toBe('2024-03-15');
171
+ });
172
+
173
+ it('falls back to flat groupByField when timeline.groupByField is not set', () => {
174
+ const schema: any = {
175
+ type: 'timeline' as const,
176
+ objectName: 'events',
177
+ startDateField: 'start_date',
178
+ titleField: 'name',
179
+ groupByField: 'category',
180
+ };
181
+ render(<ObjectTimeline schema={schema} data={mockData} />);
182
+ const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
183
+ expect(renderedSchema.items[0].group).toBe('work');
184
+ });
185
+
186
+ it('falls back to flat colorField when timeline.colorField is not set', () => {
187
+ const schema: any = {
188
+ type: 'timeline' as const,
189
+ objectName: 'events',
190
+ startDateField: 'start_date',
191
+ titleField: 'name',
192
+ colorField: 'priority_color',
193
+ };
194
+ render(<ObjectTimeline schema={schema} data={mockData} />);
195
+ const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
196
+ expect(renderedSchema.items[0].color).toBe('red');
197
+ });
198
+
199
+ it('falls back to flat scale when timeline.scale is not set', () => {
200
+ const schema: any = {
201
+ type: 'timeline' as const,
202
+ objectName: 'events',
203
+ startDateField: 'start_date',
204
+ titleField: 'name',
205
+ scale: 'week',
206
+ };
207
+ render(<ObjectTimeline schema={schema} data={mockData} />);
208
+ const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
209
+ expect(renderedSchema.timeScale).toBe('week');
210
+ });
211
+
212
+ it('spec timeline config takes priority over flat props', () => {
213
+ const schema: any = {
214
+ type: 'timeline' as const,
215
+ objectName: 'events',
216
+ startDateField: 'end_date', // flat — should be overridden
217
+ titleField: 'id', // flat — should be overridden
218
+ groupByField: 'priority_color', // flat — should be overridden
219
+ timeline: {
220
+ startDateField: 'start_date', // spec — takes priority
221
+ titleField: 'name', // spec — takes priority
222
+ groupByField: 'category', // spec — takes priority
223
+ },
224
+ };
225
+ render(<ObjectTimeline schema={schema} data={mockData} />);
226
+ const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema;
227
+ expect(renderedSchema.items[0].time).toBe('2024-01-01');
228
+ expect(renderedSchema.items[0].title).toBe('Event A');
229
+ expect(renderedSchema.items[0].group).toBe('work');
230
+ });
231
+ });
232
+ });
@@ -0,0 +1,150 @@
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 React from 'react';
10
+ import { describe, it, expect } from 'vitest';
11
+ import { render } from '@testing-library/react';
12
+ import '@testing-library/jest-dom';
13
+ import {
14
+ Timeline,
15
+ TimelineItem,
16
+ TimelineMarker,
17
+ timelineItemVariants,
18
+ timelineMarkerVariants,
19
+ } from '../index';
20
+
21
+ describe('TimelineItem density variants', () => {
22
+ it('renders with default density (mb-10)', () => {
23
+ const { container } = render(
24
+ <Timeline>
25
+ <TimelineItem data-testid="item">Content</TimelineItem>
26
+ </Timeline>
27
+ );
28
+ const item = container.querySelector('li');
29
+ expect(item?.className).toContain('mb-10');
30
+ expect(item?.className).toContain('ml-6');
31
+ });
32
+
33
+ it('renders with compact density (mb-3)', () => {
34
+ const { container } = render(
35
+ <Timeline>
36
+ <TimelineItem density="compact">Content</TimelineItem>
37
+ </Timeline>
38
+ );
39
+ const item = container.querySelector('li');
40
+ expect(item?.className).toContain('mb-3');
41
+ expect(item?.className).toContain('ml-6');
42
+ });
43
+
44
+ it('renders with comfortable density (mb-6)', () => {
45
+ const { container } = render(
46
+ <Timeline>
47
+ <TimelineItem density="comfortable">Content</TimelineItem>
48
+ </Timeline>
49
+ );
50
+ const item = container.querySelector('li');
51
+ expect(item?.className).toContain('mb-6');
52
+ expect(item?.className).toContain('ml-6');
53
+ });
54
+
55
+ it('allows className override', () => {
56
+ const { container } = render(
57
+ <Timeline>
58
+ <TimelineItem density="compact" className="custom-class">Content</TimelineItem>
59
+ </Timeline>
60
+ );
61
+ const item = container.querySelector('li');
62
+ expect(item?.className).toContain('custom-class');
63
+ expect(item?.className).toContain('mb-3');
64
+ });
65
+
66
+ it('cva function produces expected classes', () => {
67
+ expect(timelineItemVariants({ density: 'default' })).toContain('mb-10');
68
+ expect(timelineItemVariants({ density: 'compact' })).toContain('mb-3');
69
+ expect(timelineItemVariants({ density: 'comfortable' })).toContain('mb-6');
70
+ });
71
+ });
72
+
73
+ describe('TimelineMarker semantic color variants', () => {
74
+ it('renders with default variant', () => {
75
+ const { container } = render(<TimelineMarker />);
76
+ const marker = container.firstChild as HTMLElement;
77
+ expect(marker.className).toContain('bg-blue-200');
78
+ expect(marker.className).toContain('border-blue-400');
79
+ });
80
+
81
+ it('renders with success variant', () => {
82
+ const { container } = render(<TimelineMarker variant="success" />);
83
+ const marker = container.firstChild as HTMLElement;
84
+ expect(marker.className).toContain('bg-emerald-200');
85
+ expect(marker.className).toContain('border-emerald-500');
86
+ });
87
+
88
+ it('renders with warning variant', () => {
89
+ const { container } = render(<TimelineMarker variant="warning" />);
90
+ const marker = container.firstChild as HTMLElement;
91
+ expect(marker.className).toContain('bg-amber-200');
92
+ expect(marker.className).toContain('border-amber-500');
93
+ });
94
+
95
+ it('renders with danger variant', () => {
96
+ const { container } = render(<TimelineMarker variant="danger" />);
97
+ const marker = container.firstChild as HTMLElement;
98
+ expect(marker.className).toContain('bg-red-200');
99
+ expect(marker.className).toContain('border-red-500');
100
+ });
101
+
102
+ it('renders with info variant', () => {
103
+ const { container } = render(<TimelineMarker variant="info" />);
104
+ const marker = container.firstChild as HTMLElement;
105
+ expect(marker.className).toContain('bg-purple-200');
106
+ expect(marker.className).toContain('border-purple-500');
107
+ });
108
+
109
+ it('renders with todo status variant', () => {
110
+ const { container } = render(<TimelineMarker variant="todo" />);
111
+ const marker = container.firstChild as HTMLElement;
112
+ expect(marker.className).toContain('bg-slate-200');
113
+ expect(marker.className).toContain('border-slate-400');
114
+ });
115
+
116
+ it('renders with in-progress status variant', () => {
117
+ const { container } = render(<TimelineMarker variant="in-progress" />);
118
+ const marker = container.firstChild as HTMLElement;
119
+ expect(marker.className).toContain('bg-blue-200');
120
+ expect(marker.className).toContain('border-blue-500');
121
+ });
122
+
123
+ it('renders with done status variant', () => {
124
+ const { container } = render(<TimelineMarker variant="done" />);
125
+ const marker = container.firstChild as HTMLElement;
126
+ expect(marker.className).toContain('bg-emerald-200');
127
+ expect(marker.className).toContain('border-emerald-500');
128
+ });
129
+
130
+ it('cva function produces expected classes for status variants', () => {
131
+ expect(timelineMarkerVariants({ variant: 'todo' })).toContain('bg-slate-200');
132
+ expect(timelineMarkerVariants({ variant: 'in-progress' })).toContain('bg-blue-200');
133
+ expect(timelineMarkerVariants({ variant: 'done' })).toContain('bg-emerald-200');
134
+ });
135
+
136
+ it('applies common base classes', () => {
137
+ const { container } = render(<TimelineMarker variant="default" />);
138
+ const marker = container.firstChild as HTMLElement;
139
+ expect(marker.className).toContain('rounded-full');
140
+ expect(marker.className).toContain('border-2');
141
+ expect(marker.className).toContain('absolute');
142
+ });
143
+
144
+ it('allows className override', () => {
145
+ const { container } = render(<TimelineMarker variant="success" className="extra-class" />);
146
+ const marker = container.firstChild as HTMLElement;
147
+ expect(marker.className).toContain('extra-class');
148
+ expect(marker.className).toContain('bg-emerald-200');
149
+ });
150
+ });