-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathfield-value.test.ts
More file actions
194 lines (176 loc) · 7.91 KB
/
Copy pathfield-value.test.ts
File metadata and controls
194 lines (176 loc) · 7.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
/**
* Field runtime value-shape contract (ADR-0104 D1).
*
* The write-vectors here deliberately mirror the field-zoo round-trip MATRIX
* (packages/qa/dogfood/test/field-zoo-roundtrip.dogfood.test.ts) — the
* executable oracle of what the platform actually stores. If a case here and
* a MATRIX case disagree, the MATRIX (deployed reality) wins.
*/
import { describe, it, expect } from 'vitest';
import { FieldType } from './field.zod';
import {
STRING_VALUE_TYPES,
NUMERIC_VALUE_TYPES,
BOOLEAN_VALUE_TYPES,
CALENDAR_DATE_TYPES,
INSTANT_TYPES,
CLOCK_TIME_TYPES,
SINGLE_OPTION_TYPES,
MULTI_OPTION_TYPES,
REFERENCE_VALUE_TYPES,
FILE_REFERENCE_TYPES,
STRUCTURED_JSON_TYPES,
COMPUTED_VALUE_TYPES,
MULTI_CAPABLE_TYPES,
isMultiValueField,
valueSchemaFor,
} from './field-value.zod';
const ok = (def: Parameters<typeof valueSchemaFor>[0], v: unknown, form?: 'stored' | 'expanded') =>
expect(valueSchemaFor(def, form).safeParse(v).success).toBe(true);
const bad = (def: Parameters<typeof valueSchemaFor>[0], v: unknown, form?: 'stored' | 'expanded') =>
expect(valueSchemaFor(def, form).safeParse(v).success).toBe(false);
describe('semantic type classes', () => {
it('every class member is a declared FieldType', () => {
const all = new Set<string>(FieldType.options);
for (const cls of [
STRING_VALUE_TYPES, NUMERIC_VALUE_TYPES, BOOLEAN_VALUE_TYPES,
CALENDAR_DATE_TYPES, INSTANT_TYPES, CLOCK_TIME_TYPES,
SINGLE_OPTION_TYPES, MULTI_OPTION_TYPES, REFERENCE_VALUE_TYPES,
FILE_REFERENCE_TYPES, STRUCTURED_JSON_TYPES, COMPUTED_VALUE_TYPES,
MULTI_CAPABLE_TYPES,
]) {
for (const t of cls) expect(all).toContain(t);
}
});
it('every FieldType lands in at least one value class (no unclassified types)', () => {
const classified = new Set<string>([
...STRING_VALUE_TYPES, ...NUMERIC_VALUE_TYPES, ...BOOLEAN_VALUE_TYPES,
...CALENDAR_DATE_TYPES, ...INSTANT_TYPES, ...CLOCK_TIME_TYPES,
...SINGLE_OPTION_TYPES, ...MULTI_OPTION_TYPES, ...REFERENCE_VALUE_TYPES,
...FILE_REFERENCE_TYPES, ...STRUCTURED_JSON_TYPES, ...COMPUTED_VALUE_TYPES,
]);
const unclassified = FieldType.options.filter((t) => !classified.has(t));
expect(unclassified).toEqual([]);
});
it('the shape classes are mutually disjoint (COMPUTED is the orthogonal who-writes axis: `summary` is numeric AND computed)', () => {
const classes = [
STRING_VALUE_TYPES, NUMERIC_VALUE_TYPES, BOOLEAN_VALUE_TYPES,
CALENDAR_DATE_TYPES, INSTANT_TYPES, CLOCK_TIME_TYPES,
SINGLE_OPTION_TYPES, MULTI_OPTION_TYPES, REFERENCE_VALUE_TYPES,
FILE_REFERENCE_TYPES, STRUCTURED_JSON_TYPES,
];
const seen = new Map<string, number>();
classes.forEach((cls, i) => {
for (const t of cls) {
expect(seen.has(t), `type "${t}" appears in class #${seen.get(t)} and #${i}`).toBe(false);
seen.set(t, i);
}
});
});
});
describe('isMultiValueField', () => {
it('inherently-multi option types are always arrays', () => {
for (const type of ['multiselect', 'checkboxes', 'tags']) {
expect(isMultiValueField({ type })).toBe(true);
expect(isMultiValueField({ type, multiple: false })).toBe(true);
}
});
it('multi-capable types require the multiple flag', () => {
for (const type of ['select', 'radio', 'lookup', 'user', 'file', 'image']) {
expect(isMultiValueField({ type })).toBe(false);
expect(isMultiValueField({ type, multiple: true })).toBe(true);
}
});
it('a stray multiple on a non-multi-capable type is ignored (matches the engine)', () => {
for (const type of ['master_detail', 'tree', 'text', 'number']) {
expect(isMultiValueField({ type, multiple: true })).toBe(false);
}
});
});
describe('valueSchemaFor — stored form (field-zoo reality)', () => {
it('strings', () => {
ok({ type: 'text' }, 'hello');
ok({ type: 'signature' }, 'data:image/png;base64,AAAA');
ok({ type: 'color' }, '#FF8800');
bad({ type: 'text' }, 42);
});
it('numerics — currency is a BARE number (the retired CurrencyValueSchema object is rejected)', () => {
ok({ type: 'currency' }, 1234.56);
bad({ type: 'currency' }, { value: 1234.56, currency: 'USD' });
ok({ type: 'progress' }, 60);
bad({ type: 'number' }, 'NaN-ish');
bad({ type: 'number' }, Infinity);
});
it('booleans are real booleans', () => {
ok({ type: 'boolean' }, true);
bad({ type: 'boolean' }, 1);
});
it('date is a calendar day, datetime a zoned instant, time a wall clock (#2004 / ADR-0053)', () => {
ok({ type: 'date' }, '2024-03-15');
bad({ type: 'date' }, '2024-03-15T14:30:00.000Z');
ok({ type: 'datetime' }, '2024-03-15T14:30:00.000Z');
ok({ type: 'datetime' }, '2024-03-15T14:30:00+08:00');
bad({ type: 'datetime' }, '2024-03-15 14:30:00');
bad({ type: 'datetime' }, '2024-03-15');
ok({ type: 'time' }, '14:30:00');
ok({ type: 'time' }, '14:30');
bad({ type: 'time' }, '14:60');
bad({ type: 'time' }, 'not-a-time');
});
it('option types enforce declared option codes; free-form without options', () => {
const options = [{ value: 'high' }, { value: 'low' }];
ok({ type: 'select', options }, 'high');
bad({ type: 'select', options }, 'HIGH');
ok({ type: 'select' }, 'anything');
ok({ type: 'multiselect', options }, ['high', 'low']);
bad({ type: 'multiselect', options }, ['high', 'nope']);
bad({ type: 'multiselect', options }, 'high'); // scalar at a multi field
ok({ type: 'tags' }, ['alpha', 'beta']);
});
it('references store id strings; multiple stores arrays of ids', () => {
ok({ type: 'lookup' }, 'acc_synthetic_0001');
bad({ type: 'lookup' }, { id: 'acc_1', name: 'Acme' }); // expanded form ≠ stored form
bad({ type: 'lookup' }, '');
ok({ type: 'user', multiple: true }, ['usr_1', 'usr_2']);
bad({ type: 'user', multiple: true }, 'usr_1');
});
it('expanded form admits the in-place $expand record object for references', () => {
ok({ type: 'lookup' }, { id: 'acc_1', name: 'Acme' }, 'expanded');
ok({ type: 'lookup' }, 'acc_1', 'expanded'); // unresolvable ids stay ids
});
it('file-likes admit the transitional inline object OR an opaque id/url string (pre-D3-wave-2)', () => {
ok({ type: 'file' }, { url: 'https://cdn/f.pdf', name: 'f.pdf', size: 1024 });
ok({ type: 'image' }, { url: 'https://cdn/i.png', alt: 'i' });
ok({ type: 'file' }, { url: 'https://cdn/f.pdf', mimeType: 'application/pdf' });
ok({ type: 'file' }, 'file_01HXYZ');
ok({ type: 'image', multiple: true }, ['a.png', 'b.png']);
bad({ type: 'file' }, 42);
bad({ type: 'file' }, {});
});
it('D3 wave 1: the media object form requires a url — a url-less fragment is no longer waved through', () => {
// The whole FILE_REFERENCE_TYPES class shares the one contract.
for (const type of ['file', 'image', 'avatar', 'video', 'audio']) {
ok({ type }, { url: 'https://cdn/x' });
bad({ type }, { name: 'x' }); // object without url — the tightening
bad({ type }, { size: 10 }); // ditto
}
ok({ type: 'video' }, { url: 'https://cdn/v.mp4', duration: 12 });
});
it('structured JSON types', () => {
ok({ type: 'location' }, { lat: 37.77, lng: -122.42 });
bad({ type: 'location' }, { latitude: 37.77, longitude: -122.42 }); // the retired spec-only shape
ok({ type: 'address' }, { street: '1 Main', city: 'SF', country: 'US' });
ok({ type: 'vector' }, [0.1, 0.2, 0.3]);
bad({ type: 'vector' }, [0.1, 'x']);
ok({ type: 'repeater' }, [{ a: 1 }, { a: 2 }]);
bad({ type: 'repeater' }, { a: 1 });
ok({ type: 'record' }, { home: '+1', work: '+2' });
ok({ type: 'composite' }, { label: 'x', n: 1 });
});
it('json/code and computed types are explicitly open', () => {
ok({ type: 'json' }, { a: 1, b: [2, 3] });
ok({ type: 'formula' }, 31.5);
ok({ type: 'autonumber' }, 'INV-0001');
});
});