-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathFieldEditWidget.test.ts
More file actions
133 lines (121 loc) · 6.04 KB
/
Copy pathFieldEditWidget.test.ts
File metadata and controls
133 lines (121 loc) · 6.04 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
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { describe, it, expect } from 'vitest';
import { FieldType } from '@objectstack/spec/data';
import {
FORM_FIELD_TYPES,
INLINE_EXCLUDED_FIELD_TYPES,
hasFieldEditWidget,
isInlineExcludedFieldType,
mapFieldTypeToFormType,
} from './index';
/**
* A field type the form can render — either a direct widget-map key, or a spec
* spelling that resolves onto one through the alias table (`secret` →
* `field:password`, `autonumber` → `field:auto_number`, …).
*
* `FORM_FIELD_TYPES` alone is `Object.keys(fieldWidgetMap)`, which does NOT
* include the alias-only spellings — see the note at index.tsx's
* `resolveFormWidgetType`. Using it as the definition of "a real type" made the
* staleness check below reject `secret`, a genuine spec field type the form
* renders, purely because it arrives via an alias.
*/
function isRenderableFormType(t: string): boolean {
// `text` is the alias table's fallback, so a non-`field:text` result means an
// explicit entry exists. `text` itself is a direct widget key, so it is
// already covered by the first clause and no real type is missed here.
return FORM_FIELD_TYPES.includes(t) || mapFieldTypeToFormType(t) !== 'field:text';
}
/**
* Drift-guard: the inline cell editor reuses the form's field widgets, but the
* two lists were hand-maintained separately and drifted — `lookup` (and other
* relational types) had a perfectly good form widget yet fell back to a plain
* text box inline, because nobody wired it up. This pins the contract: every
* type the FORM can render must have an explicit inline decision — an editor or
* a documented exclusion — so a new form widget can never again silently become
* an editable text box (or a missing one) in the grid.
*/
describe('inline editor ↔ form widget parity', () => {
it('every form field type either has an inline editor or is explicitly excluded', () => {
const undecided = FORM_FIELD_TYPES.filter(
(t) => !hasFieldEditWidget(t) && !INLINE_EXCLUDED_FIELD_TYPES.has(t),
);
// If this fails: a form widget type has no inline decision. Either add it to
// EDIT_WIDGETS (inline-editable) or to INLINE_EXCLUDED_FIELD_TYPES (with a
// reason) in FieldEditWidget.tsx.
expect(undecided).toEqual([]);
});
it('the exclusion set lists only real form types (no stale entries)', () => {
const stale = [...INLINE_EXCLUDED_FIELD_TYPES].filter((t) => !isRenderableFormType(t));
expect(stale).toEqual([]);
});
it('credential types are never inline-editable', () => {
// The grid's fallback for a type with no inline editor is a PLAIN TEXT input.
// Both of these are masked on read, so that input would show the mask as the
// value and write it straight back; `secret` also round-trips through an
// encrypted store (ADR-0100), so the cell holds an opaque ref, not the value.
for (const t of ['password', 'secret']) {
expect(hasFieldEditWidget(t), `${t} must not have an inline editor`).toBe(false);
expect(INLINE_EXCLUDED_FIELD_TYPES.has(t), `${t} must be explicitly excluded`).toBe(true);
}
});
it('relational fields use the standard picker inline (regression: lookup was a text box)', () => {
for (const t of ['lookup', 'master_detail', 'user', 'owner']) {
expect(hasFieldEditWidget(t)).toBe(true);
}
});
it('computed / binary form types are NOT inline-editable (excluded)', () => {
for (const t of ['formula', 'summary', 'auto_number', 'file', 'image']) {
expect(hasFieldEditWidget(t)).toBe(false);
expect(INLINE_EXCLUDED_FIELD_TYPES.has(t)).toBe(true);
}
});
it('structured-value types edit inline with their form widget (color/address/location/code/…)', () => {
for (const t of ['color', 'address', 'location', 'geolocation', 'code', 'qrcode']) {
expect(hasFieldEditWidget(t)).toBe(true);
expect(INLINE_EXCLUDED_FIELD_TYPES.has(t)).toBe(false);
}
});
});
/**
* #2942 — the guard above iterates FORM_FIELD_TYPES (the form widget-map
* keys), so a SPEC field type that reaches the form only through the alias
* table could sit in neither set and silently fall back to the grid's plain
* text input: `json`, `composite`, `record`, `repeater`, `tree`, `video`,
* `audio`, `autonumber` all did. This pins the contract at the spec boundary:
* every `FieldType` member must resolve (alias-aware) to an inline editor or
* a documented exclusion.
*/
describe('inline editor ↔ SPEC FieldType parity (#2942)', () => {
const specTypes: string[] = Array.isArray((FieldType as { options?: readonly string[] }).options)
? [...(FieldType as { options: readonly string[] }).options]
: [];
it('reads a non-empty enum from the spec', () => {
expect(specTypes, 'could not read FieldType.options from the spec').not.toEqual([]);
});
it('every spec field type resolves to an inline decision (editor or exclusion)', () => {
const undecided = specTypes.filter(
(t) => !hasFieldEditWidget(t) && !isInlineExcludedFieldType(t),
);
// If this fails: a spec field type would fall back to a plain text input
// inline. Give it a widget (EDIT_WIDGETS / the alias table) or a
// documented exclusion (INLINE_EXCLUDED_FIELD_TYPES).
expect(undecided).toEqual([]);
});
it('the structured/computed spec spellings are closed corruption paths', () => {
// Excluded (alias-aware): editing these through a text box corrupts the value.
for (const t of ['composite', 'record', 'repeater', 'video', 'audio', 'autonumber']) {
expect(isInlineExcludedFieldType(t), `${t} must be excluded from inline editing`).toBe(true);
expect(hasFieldEditWidget(t), `${t} must not resolve to an editor`).toBe(false);
}
// Editable through their form widgets: json → code editor, tree → lookup picker.
for (const t of ['json', 'tree']) {
expect(hasFieldEditWidget(t), `${t} must resolve to its form widget`).toBe(true);
}
});
});