@kronor/dtv 0.2.9

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 (97) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/copilot-instructions.md +64 -0
  3. package/.github/workflows/ci.yml +51 -0
  4. package/.husky/pre-commit +8 -0
  5. package/README.md +63 -0
  6. package/docs/api/README.md +32 -0
  7. package/docs/api/cell-renderers.md +121 -0
  8. package/docs/api/no-rows-component.md +71 -0
  9. package/docs/api/runtime.md +78 -0
  10. package/e2e/app.spec.ts +6 -0
  11. package/e2e/cell-renderer-setfilterstate.spec.ts +63 -0
  12. package/e2e/filter-sharing.spec.ts +113 -0
  13. package/e2e/filter-url-persistence.spec.ts +36 -0
  14. package/e2e/graphqlMock.ts +144 -0
  15. package/e2e/multi-field-filters.spec.ts +95 -0
  16. package/e2e/pagination.spec.ts +38 -0
  17. package/e2e/payment-request-email-filter.spec.ts +67 -0
  18. package/e2e/save-filter-splitbutton.spec.ts +68 -0
  19. package/e2e/simple-view-email-filter.spec.ts +67 -0
  20. package/e2e/simple-view-transforms.spec.ts +171 -0
  21. package/e2e/simple-view.spec.ts +104 -0
  22. package/e2e/transform-regression.spec.ts +108 -0
  23. package/eslint.config.js +30 -0
  24. package/index.html +17 -0
  25. package/jest.config.js +10 -0
  26. package/package.json +45 -0
  27. package/playwright.config.ts +54 -0
  28. package/public/vite.svg +1 -0
  29. package/src/App.externalRuntime.test.ts +190 -0
  30. package/src/App.tsx +540 -0
  31. package/src/assets/react.svg +1 -0
  32. package/src/components/AIAssistantForm.tsx +241 -0
  33. package/src/components/FilterForm.test.ts +82 -0
  34. package/src/components/FilterForm.tsx +375 -0
  35. package/src/components/PhoneNumberFilter.tsx +102 -0
  36. package/src/components/SavedFilterList.tsx +181 -0
  37. package/src/components/SpeechInput.tsx +67 -0
  38. package/src/components/Table.tsx +119 -0
  39. package/src/components/TablePagination.tsx +40 -0
  40. package/src/components/aiAssistant.test.ts +270 -0
  41. package/src/components/aiAssistant.ts +291 -0
  42. package/src/framework/cell-renderer-components/CurrencyAmount.tsx +30 -0
  43. package/src/framework/cell-renderer-components/LayoutHelpers.tsx +74 -0
  44. package/src/framework/cell-renderer-components/Link.tsx +28 -0
  45. package/src/framework/cell-renderer-components/Mapping.tsx +11 -0
  46. package/src/framework/cell-renderer-components.test.ts +353 -0
  47. package/src/framework/column-definition.tsx +85 -0
  48. package/src/framework/currency.test.ts +46 -0
  49. package/src/framework/currency.ts +62 -0
  50. package/src/framework/data.staticConditions.test.ts +46 -0
  51. package/src/framework/data.test.ts +167 -0
  52. package/src/framework/data.ts +162 -0
  53. package/src/framework/filter-form-state.test.ts +189 -0
  54. package/src/framework/filter-form-state.ts +185 -0
  55. package/src/framework/filter-sharing.test.ts +135 -0
  56. package/src/framework/filter-sharing.ts +118 -0
  57. package/src/framework/filters.ts +194 -0
  58. package/src/framework/graphql.buildHasuraConditions.test.ts +473 -0
  59. package/src/framework/graphql.paginationKey.test.ts +29 -0
  60. package/src/framework/graphql.test.ts +286 -0
  61. package/src/framework/graphql.ts +462 -0
  62. package/src/framework/native-runtime/index.tsx +33 -0
  63. package/src/framework/native-runtime/nativeComponents.test.ts +108 -0
  64. package/src/framework/runtime-reference.test.ts +172 -0
  65. package/src/framework/runtime.ts +15 -0
  66. package/src/framework/saved-filters.test.ts +422 -0
  67. package/src/framework/saved-filters.ts +293 -0
  68. package/src/framework/state.test.ts +86 -0
  69. package/src/framework/state.ts +148 -0
  70. package/src/framework/transform.test.ts +51 -0
  71. package/src/framework/view-parser-initialvalues.test.ts +228 -0
  72. package/src/framework/view-parser.ts +714 -0
  73. package/src/framework/view.test.ts +1805 -0
  74. package/src/framework/view.ts +38 -0
  75. package/src/index.css +6 -0
  76. package/src/main.tsx +99 -0
  77. package/src/views/index.ts +12 -0
  78. package/src/views/payment-requests/components/NoRowsExtendDateRange.tsx +37 -0
  79. package/src/views/payment-requests/components/PaymentMethod.tsx +184 -0
  80. package/src/views/payment-requests/components/PaymentStatusTag.tsx +61 -0
  81. package/src/views/payment-requests/index.ts +1 -0
  82. package/src/views/payment-requests/runtime.tsx +145 -0
  83. package/src/views/payment-requests/view.json +692 -0
  84. package/src/views/payment-requests-initial-values.test.ts +73 -0
  85. package/src/views/request-log/index.ts +2 -0
  86. package/src/views/request-log/runtime.tsx +47 -0
  87. package/src/views/request-log/view.json +123 -0
  88. package/src/views/simple-test-view/index.ts +3 -0
  89. package/src/views/simple-test-view/runtime.tsx +85 -0
  90. package/src/views/simple-test-view/view.json +191 -0
  91. package/src/vite-env.d.ts +1 -0
  92. package/tailwind.config.js +7 -0
  93. package/tsconfig.app.json +26 -0
  94. package/tsconfig.jest.json +6 -0
  95. package/tsconfig.json +7 -0
  96. package/tsconfig.node.json +24 -0
  97. package/vite.config.ts +11 -0
@@ -0,0 +1,167 @@
1
+ import { flattenFields } from './data';
2
+ import { ColumnDefinition, fieldAlias, field, queryConfigs } from './column-definition';
3
+
4
+ describe('flattenFields', () => {
5
+ it('extracts simple fields from rows', () => {
6
+ const rows = [
7
+ { id: 1, name: 'Alice', age: 30 },
8
+ { id: 2, name: 'Bob', age: 25 }
9
+ ];
10
+ const columns: ColumnDefinition[] = [
11
+ { data: [{ type: 'field', path: 'id' }] } as ColumnDefinition,
12
+ { data: [{ type: 'field', path: 'name' }] } as ColumnDefinition
13
+ ];
14
+ const result = flattenFields(rows, columns);
15
+ expect(result).toEqual([
16
+ [{ id: 1 }, { name: 'Alice' }],
17
+ [{ id: 2 }, { name: 'Bob' }]
18
+ ]);
19
+ });
20
+
21
+ it('handles nested field paths', () => {
22
+ const rows = [
23
+ { id: 1, user: { profile: { email: 'alice@example.com' } } },
24
+ { id: 2, user: { profile: { email: 'bob@example.com' } } }
25
+ ];
26
+ const columns: ColumnDefinition[] = [
27
+ { data: [{ type: 'field', path: 'user.profile.email' }] } as ColumnDefinition
28
+ ];
29
+ const result = flattenFields(rows, columns);
30
+ expect(result).toEqual([
31
+ [{ 'user.profile.email': 'alice@example.com' }],
32
+ [{ 'user.profile.email': 'bob@example.com' }]
33
+ ]);
34
+ });
35
+
36
+ it('handles multiple nested field paths with array parent', () => {
37
+ const rows = [
38
+ {
39
+ id: 1,
40
+ users: [
41
+ { profile: { email: 'alice@example.com', age: 30 } },
42
+ { profile: { email: 'bob@example.com', age: 25 } }
43
+ ]
44
+ },
45
+ {
46
+ id: 2,
47
+ users: [
48
+ { profile: { email: 'carol@example.com', age: 28 } }
49
+ ]
50
+ }
51
+ ];
52
+ const columns: ColumnDefinition[] = [
53
+ {
54
+ data: [
55
+ { type: 'field', path: 'users.profile.email' },
56
+ { type: 'field', path: 'users.profile.age' }
57
+ ]
58
+ } as ColumnDefinition
59
+ ];
60
+ const result = flattenFields(rows, columns);
61
+ expect(result).toEqual([
62
+ [
63
+ {
64
+ 'users.profile.email': ['alice@example.com', 'bob@example.com'],
65
+ 'users.profile.age': [30, 25]
66
+ }
67
+ ],
68
+ [
69
+ {
70
+ 'users.profile.email': ['carol@example.com'],
71
+ 'users.profile.age': [28]
72
+ }
73
+ ]
74
+ ]);
75
+ });
76
+
77
+ it('handles field aliases correctly', () => {
78
+ // Simulate the response that would come from GraphQL with aliases
79
+ const rows = [
80
+ {
81
+ id: 1,
82
+ user: { name: 'Alice' },
83
+ userName: 'Alice' // This is what GraphQL would return for the alias
84
+ },
85
+ {
86
+ id: 2,
87
+ user: { name: 'Bob' },
88
+ userName: 'Bob' // This is what GraphQL would return for the alias
89
+ }
90
+ ];
91
+ const columns: ColumnDefinition[] = [
92
+ {
93
+ data: [fieldAlias("userName", field("user.name"))]
94
+ } as ColumnDefinition
95
+ ];
96
+ const result = flattenFields(rows, columns);
97
+
98
+ // The flattened result should use the alias name "userName", not the original path "user.name"
99
+ expect(result).toEqual([
100
+ [{ userName: 'Alice' }],
101
+ [{ userName: 'Bob' }]
102
+ ]);
103
+ });
104
+
105
+ it('handles field aliases with queryConfigs correctly', () => {
106
+ // Simulate the response that would come from GraphQL with aliases for nested queryConfigs
107
+ const rows = [
108
+ {
109
+ id: 1,
110
+ posts: [
111
+ { title: 'Post 1', created_at: '2023-01-01' },
112
+ { title: 'Post 2', created_at: '2023-01-02' }
113
+ ],
114
+ recentPostTitles: [ // This is what GraphQL would return for the alias
115
+ 'Post 2',
116
+ 'Post 1'
117
+ ]
118
+ }
119
+ ];
120
+ const columns: ColumnDefinition[] = [
121
+ {
122
+ data: [
123
+ fieldAlias("recentPostTitles", queryConfigs([
124
+ { field: "posts" },
125
+ { field: "title" }
126
+ ]))
127
+ ]
128
+ } as ColumnDefinition
129
+ ];
130
+ const result = flattenFields(rows, columns);
131
+
132
+ // The flattened result should use the alias name "recentPostTitles", not the generated path "posts.title"
133
+ expect(result).toEqual([
134
+ [{
135
+ recentPostTitles: [
136
+ 'Post 2',
137
+ 'Post 1'
138
+ ]
139
+ }]
140
+ ]);
141
+ });
142
+
143
+ it('handles nested field aliases correctly', () => {
144
+ // Test aliasing a field alias (though this is a rare case, it should be supported)
145
+ const rows = [
146
+ {
147
+ id: 1,
148
+ user: { name: 'Alice' },
149
+ userName: 'Alice', // This would be the first alias from GraphQL
150
+ displayName: 'Alice' // This would be the second alias from GraphQL
151
+ }
152
+ ];
153
+ const columns: ColumnDefinition[] = [
154
+ {
155
+ data: [
156
+ fieldAlias("displayName", fieldAlias("userName", field("user.name")))
157
+ ]
158
+ } as ColumnDefinition
159
+ ];
160
+ const result = flattenFields(rows, columns);
161
+
162
+ // The flattened result should use the outermost alias name "displayName"
163
+ expect(result).toEqual([
164
+ [{ displayName: 'Alice' }]
165
+ ]);
166
+ });
167
+ });
@@ -0,0 +1,162 @@
1
+ import { GraphQLClient } from 'graphql-request';
2
+ import { buildHasuraConditions } from '../framework/graphql';
3
+ import { View } from '../framework/view';
4
+ import { ColumnDefinition, FieldQuery, QueryConfig } from '../framework/column-definition';
5
+ import { FilterState } from './state';
6
+
7
+ export interface FetchDataResult {
8
+ rows: Record<string, unknown>[]; // Fetched rows from the query
9
+ flattenedRows: Record<string, unknown>[][]; // Rows flattened according to column definitions
10
+ }
11
+
12
+ function hasKey<K extends string | number | symbol, T extends { [key in K]: unknown[] }>(obj: unknown, key: K): obj is T {
13
+ return typeof obj === 'object' && obj !== null && key in obj && Array.isArray((obj as T)[key]);
14
+ }
15
+
16
+ // Request counter to be able to cancel handling of previous requests
17
+ let requestCounter = 0;
18
+
19
+ export const fetchData = async ({
20
+ client,
21
+ view,
22
+ query,
23
+ filterState,
24
+ rows,
25
+ cursor
26
+ }: {
27
+ client: GraphQLClient;
28
+ view: View;
29
+ query: string;
30
+ filterState: FilterState;
31
+ rows: number;
32
+ cursor: string | number | null;
33
+ }): Promise<FetchDataResult> => {
34
+ // Assign a unique ID to this request for ordering
35
+ const currentRequestId = ++requestCounter;
36
+
37
+ try {
38
+ let conditions = buildHasuraConditions(filterState, view.filterSchema);
39
+
40
+ // Merge staticConditions (always-on) if present
41
+ if (view.staticConditions && view.staticConditions.length > 0) {
42
+ // If existing conditions object is empty (no user filters), we still wrap both sides in _and for consistency
43
+ conditions = { _and: [conditions, ...view.staticConditions] } as any;
44
+ }
45
+ if (cursor !== null) {
46
+ const pagKey = view.paginationKey;
47
+ const pagCond = { [pagKey]: { _lt: cursor } };
48
+ // Always wrap in _and for pagination
49
+ // If static conditions already produced an _and wrapper, append to its array to avoid nesting
50
+ if (conditions && '_and' in conditions && Array.isArray((conditions as any)._and)) {
51
+ (conditions as any)._and.push(pagCond);
52
+ } else {
53
+ conditions = { _and: [conditions, pagCond] } as any;
54
+ }
55
+ }
56
+ const variables = {
57
+ conditions,
58
+ limit: rows,
59
+ orderBy: [{ [view.paginationKey]: 'DESC' }],
60
+ };
61
+
62
+ const response = await client.request(query, variables);
63
+
64
+ // Check if this is still the most recent request
65
+ if (currentRequestId !== requestCounter) {
66
+ // A newer request has been started, discard this response
67
+ throw new DOMException('Request superseded by newer request', 'AbortError');
68
+ }
69
+
70
+ if (!hasKey(response, view.collectionName)) {
71
+ console.error('Error fetching data, unexpected response format:', response);
72
+ return { rows: [], flattenedRows: [] };
73
+ }
74
+
75
+ const rowsFetched = response[view.collectionName];
76
+
77
+ // Flatten the data before returning
78
+ return {
79
+ rows: rowsFetched as Record<string, any>[],
80
+ flattenedRows: flattenFields(rowsFetched as Record<string, any>[], view.columnDefinitions)
81
+ }
82
+ } catch (error) {
83
+ // Don't log AbortError as it's expected when cancelling requests
84
+ if (error instanceof DOMException && error.name === 'AbortError') {
85
+ throw error; // Re-throw abort errors so fetchDataWrapper can cancel response handling
86
+ }
87
+ console.error('Error fetching data:', error);
88
+ return { rows: [], flattenedRows: [] };
89
+ }
90
+ };
91
+
92
+ // Applies flattenColumnFields to all rows for all columns
93
+ export const flattenFields = (
94
+ rows: Record<string, any>[],
95
+ columns: ColumnDefinition[]
96
+ ): Record<string, any>[][] => {
97
+ return rows.map(row =>
98
+ columns.map(column => flattenColumnFields(row, column))
99
+ );
100
+ };
101
+
102
+ // Helper to extract field values for a column from a row
103
+ export const flattenColumnFields = (row: Record<string, any>, column: ColumnDefinition) => {
104
+ const values: Record<string, any> = {};
105
+
106
+ const extractField = (fieldQuery: FieldQuery) => {
107
+ if (fieldQuery.type === 'field') {
108
+ const path = fieldQuery.path.split('.');
109
+ let value: any = row;
110
+ for (const p of path) {
111
+ if (Array.isArray(value)) {
112
+ // If value is an array, map extraction for each item
113
+ value = value.map(item => {
114
+ let v = item;
115
+ for (let i = path.indexOf(p); i < path.length; i++) {
116
+ v = v?.[path[i]];
117
+ }
118
+ return v;
119
+ });
120
+ break;
121
+ } else {
122
+ value = value?.[p];
123
+ }
124
+ }
125
+ values[fieldQuery.path] = value;
126
+ } else if (fieldQuery.type === 'queryConfigs') {
127
+ const pathKey = fieldQuery.configs.map(c => c.field).join('.');
128
+
129
+ const extract = (currentValue: any, configs: QueryConfig[]): any => {
130
+ if (configs.length === 0) {
131
+ return currentValue;
132
+ }
133
+
134
+ if (currentValue === undefined || currentValue === null) {
135
+ return undefined;
136
+ }
137
+
138
+ const [currentConfig, ...remainingConfigs] = configs;
139
+
140
+ if (Array.isArray(currentValue)) {
141
+ return currentValue.map(item => extract(item, configs));
142
+ }
143
+
144
+ if (typeof currentValue === 'object' && currentConfig.field in currentValue) {
145
+ const nextValue = currentValue[currentConfig.field];
146
+ return extract(nextValue, remainingConfigs);
147
+ }
148
+
149
+ return undefined;
150
+ };
151
+
152
+ values[pathKey] = extract(row, fieldQuery.configs);
153
+ } else if (fieldQuery.type === 'fieldAlias') {
154
+ // For field aliases, we look up the value using the alias name instead of the original field path
155
+ // The GraphQL response should contain the aliased field name
156
+ values[fieldQuery.alias] = row[fieldQuery.alias];
157
+ }
158
+ };
159
+
160
+ column.data.forEach(extractField);
161
+ return values;
162
+ };
@@ -0,0 +1,189 @@
1
+ import {
2
+ serializeFilterFormStateMap,
3
+ parseFilterFormState
4
+ } from './filter-form-state';
5
+ import { FilterSchemasAndGroups } from './filters';
6
+ import { FilterState } from './state';
7
+
8
+ describe('filter-form-state', () => {
9
+ const mockFilterSchema: FilterSchemasAndGroups = {
10
+ groups: [
11
+ { name: 'Basic', label: 'Basic' }
12
+ ],
13
+ filters: [
14
+ {
15
+ id: 'email-filter',
16
+ label: 'Email',
17
+ expression: {
18
+ type: 'equals',
19
+ field: 'email',
20
+ value: { type: 'text', label: 'Email' }
21
+ },
22
+ group: 'Basic',
23
+ aiGenerated: false
24
+ },
25
+ {
26
+ id: 'date-filter',
27
+ label: 'Date',
28
+ expression: {
29
+ type: 'equals',
30
+ field: 'created_at',
31
+ value: { type: 'date', label: 'Created Date' }
32
+ },
33
+ group: 'Basic',
34
+ aiGenerated: false
35
+ }
36
+ ]
37
+ };
38
+
39
+ const mockFilterState: FilterState = new Map([
40
+ ['email-filter', {
41
+ type: 'leaf',
42
+ field: 'email',
43
+ value: 'test@example.com',
44
+ control: { type: 'text', label: 'Email' },
45
+ filterType: 'equals'
46
+ }],
47
+ ['date-filter', {
48
+ type: 'leaf',
49
+ field: 'created_at',
50
+ value: new Date('2023-01-01T00:00:00.000Z'),
51
+ control: { type: 'date', label: 'Created Date' },
52
+ filterType: 'equals'
53
+ }]
54
+ ]);
55
+
56
+ describe('serializeFilterFormStateMap', () => {
57
+ it('should serialize filter state Map to JSON-compatible format', () => {
58
+ const serialized = serializeFilterFormStateMap(mockFilterState);
59
+
60
+ expect(typeof serialized).toBe('object');
61
+ expect(serialized).not.toBeNull();
62
+ expect(Array.isArray(serialized)).toBe(false);
63
+
64
+ // Check that both filters are present as object properties
65
+ expect(serialized['email-filter']).toEqual({
66
+ type: 'leaf',
67
+ field: 'email',
68
+ value: 'test@example.com',
69
+ control: { type: 'text', label: 'Email' },
70
+ filterType: 'equals'
71
+ });
72
+
73
+ expect(serialized['date-filter']).toEqual({
74
+ type: 'leaf',
75
+ field: 'created_at',
76
+ value: '2023-01-01T00:00:00.000Z', // Date should be serialized as ISO string
77
+ control: { type: 'date', label: 'Created Date' },
78
+ filterType: 'equals'
79
+ });
80
+ });
81
+
82
+ it('should handle complex nested structures', () => {
83
+ const complexState: FilterState = new Map([
84
+ ['complex-filter', {
85
+ type: 'and',
86
+ filterType: 'and',
87
+ children: [
88
+ {
89
+ type: 'leaf',
90
+ field: 'email',
91
+ value: 'test@example.com',
92
+ control: { type: 'text', label: 'Email' },
93
+ filterType: 'equals'
94
+ },
95
+ {
96
+ type: 'not',
97
+ filterType: 'not',
98
+ child: {
99
+ type: 'leaf',
100
+ field: 'status',
101
+ value: 'deleted',
102
+ control: { type: 'text', label: 'Status' },
103
+ filterType: 'equals'
104
+ }
105
+ }
106
+ ]
107
+ }]
108
+ ]);
109
+
110
+ const serialized = serializeFilterFormStateMap(complexState);
111
+ expect(typeof serialized).toBe('object');
112
+ expect(Array.isArray(serialized)).toBe(false);
113
+
114
+ const complexFilter = serialized['complex-filter'];
115
+ expect(complexFilter).toBeDefined();
116
+ expect(complexFilter.type).toBe('and');
117
+ expect(complexFilter.children).toHaveLength(2);
118
+ expect(complexFilter.children[1].type).toBe('not');
119
+ });
120
+ });
121
+
122
+ describe('parseFilterFormState', () => {
123
+ it('should parse serialized state back to FilterState with date handling', () => {
124
+ const serialized = {
125
+ 'email-filter': {
126
+ type: 'leaf',
127
+ field: 'email',
128
+ value: 'test@example.com',
129
+ control: { type: 'text', label: 'Email' },
130
+ filterType: 'equals'
131
+ },
132
+ 'date-filter': {
133
+ type: 'leaf',
134
+ field: 'created_at',
135
+ value: '2023-01-01T00:00:00.000Z',
136
+ control: { type: 'date', label: 'Created Date' },
137
+ filterType: 'equals'
138
+ }
139
+ };
140
+
141
+ const parsed = parseFilterFormState(serialized, mockFilterSchema);
142
+
143
+ expect(parsed).toBeInstanceOf(Map);
144
+ expect(parsed.size).toBe(2);
145
+
146
+ const emailFilter = parsed.get('email-filter');
147
+ expect(emailFilter).toEqual({ type: 'leaf', value: 'test@example.com' });
148
+
149
+ const dateFilter = parsed.get('date-filter');
150
+ expect(dateFilter?.type).toBe('leaf');
151
+ expect((dateFilter as any).value).toBeInstanceOf(Date);
152
+ expect(((dateFilter as any).value as Date).toISOString()).toBe('2023-01-01T00:00:00.000Z');
153
+ });
154
+
155
+ it('should treat invalid date strings as plain strings', () => {
156
+ const serialized = {
157
+ 'date-filter': {
158
+ type: 'leaf',
159
+ field: 'created_at',
160
+ value: 'invalid-date',
161
+ control: { type: 'date', label: 'Created Date' },
162
+ filterType: 'equals'
163
+ }
164
+ };
165
+ const parsed = parseFilterFormState(serialized, mockFilterSchema);
166
+ const dateFilter = parsed.get('date-filter');
167
+ expect(dateFilter).toEqual({ type: 'leaf', value: 'invalid-date' });
168
+ });
169
+ });
170
+
171
+ describe('round-trip serialization/parsing', () => {
172
+ it('should preserve data through serialize/parse cycle', () => {
173
+ // Use the new serializeFilterFormStateMap function for a true round-trip test
174
+ const serialized = serializeFilterFormStateMap(mockFilterState);
175
+ const parsed = parseFilterFormState(serialized, mockFilterSchema);
176
+
177
+ expect(parsed).toBeInstanceOf(Map);
178
+ expect(parsed.size).toBe(2);
179
+
180
+ const emailFilter = parsed.get('email-filter');
181
+ const dateFilter = parsed.get('date-filter');
182
+
183
+ expect(emailFilter).toEqual({ type: 'leaf', value: 'test@example.com' });
184
+ expect(dateFilter?.type).toBe('leaf');
185
+ expect((dateFilter as any).value).toBeInstanceOf(Date);
186
+ expect(((dateFilter as any).value as Date).toISOString()).toBe('2023-01-01T00:00:00.000Z');
187
+ });
188
+ });
189
+ });