-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathparamToField.test.ts
More file actions
155 lines (141 loc) · 5.79 KB
/
Copy pathparamToField.test.ts
File metadata and controls
155 lines (141 loc) · 5.79 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
/**
* 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.
*/
/**
* paramToField — the param → field adapter behind ActionParamDialog's shared
* field-widget rendering (ADR-0059), plus the drift guard pinning param
* support ⊇ form support. The dialog used to hand-roll a per-type ternary
* chain, so every form type without its own branch (`file`, `image`,
* `richtext`, `color`, …) silently collapsed to a text box; routing through
* `FORM_FIELD_TYPES` + this drift test makes that class of bug impossible to
* reintroduce silently.
*/
import { describe, it, expect, vi } from 'vitest';
import { FORM_FIELD_TYPES } from '@object-ui/fields';
import type { ActionParamDef } from '@object-ui/core';
import { paramToField, resolveParamWidgetType } from './paramToField';
const p = (over: Partial<ActionParamDef>): ActionParamDef => ({
name: 'x',
label: 'X',
type: 'text',
...over,
});
describe('param widget support ⊇ form widget support (drift guard)', () => {
it('every form field type resolves to its own widget — never the text fallback', () => {
// If this fails: a type was added to `fieldWidgetMap` that the param
// dialog would degrade to another widget. The adapter resolves widget-map
// keys by identity, so this can only regress if that resolution changes —
// do not special-case types out without an alias entry here.
const degraded = FORM_FIELD_TYPES.filter((t) => resolveParamWidgetType(t) !== t);
expect(degraded).toEqual([]);
});
it('legacy param-only spellings fold onto canonical widgets', () => {
expect(resolveParamWidgetType('checkbox')).toBe('boolean');
expect(resolveParamWidgetType('reference')).toBe('lookup');
expect(resolveParamWidgetType('datetime-local')).toBe('datetime');
expect(resolveParamWidgetType('autonumber')).toBe('auto_number');
});
it('spec FieldType aliases resolve through the form mapping, unknown types fall back to text', () => {
expect(resolveParamWidgetType('toggle')).toBe('boolean');
expect(resolveParamWidgetType('json')).toBe('code');
expect(resolveParamWidgetType('secret')).toBe('password');
expect(resolveParamWidgetType('tree')).toBe('lookup');
expect(resolveParamWidgetType('no-such-type')).toBe('text');
});
});
describe('paramToField', () => {
it('maps the widget-relevant config for a plain param', () => {
const field = paramToField(p({
name: 'reason',
label: 'Reason',
type: 'textarea',
required: true,
placeholder: 'Why?',
}));
expect(field).toMatchObject({
name: 'reason',
label: 'Reason',
type: 'textarea',
required: true,
placeholder: 'Why?',
});
});
it('carries options for select params', () => {
const options = [{ label: 'A', value: 'a' }];
expect(paramToField(p({ type: 'select', options }))).toMatchObject({ type: 'select', options });
});
it('carries upload config (multiple/accept/maxSize) for file params', () => {
const field = paramToField(p({
type: 'file',
multiple: true,
accept: ['application/pdf'],
maxSize: 5 * 1024 * 1024,
}));
expect(field).toMatchObject({
type: 'file',
multiple: true,
accept: ['application/pdf'],
maxSize: 5 * 1024 * 1024,
});
});
it('renders boolean params as a checkbox (dialog inline-row UX), not the form switch', () => {
expect(paramToField(p({ type: 'boolean' }))).toMatchObject({ type: 'boolean', widget: 'checkbox' });
expect(paramToField(p({ type: 'checkbox' }))).toMatchObject({ type: 'boolean', widget: 'checkbox' });
});
it('maps the full lookup picker config to snake_case field metadata', () => {
const field = paramToField(p({
type: 'lookup',
referenceTo: 'space_users',
displayField: 'name',
idField: 'id',
descriptionField: 'email',
multiple: true,
titleFormat: '{first_name} {last_name}',
lookupColumns: [{ field: 'name' }],
lookupFilters: [{ field: 'active', operator: '=', value: true }],
lookupPageSize: 25,
dependsOn: ['org'],
}));
expect(field).toMatchObject({
type: 'lookup',
reference_to: 'space_users',
display_field: 'name',
id_field: 'id',
description_field: 'email',
multiple: true,
title_format: '{first_name} {last_name}',
lookup_columns: [{ field: 'name' }],
lookup_filters: [{ field: 'active', operator: '=', value: true }],
lookup_page_size: 25,
depends_on: ['org'],
});
});
it('lookup param without a referenceTo target falls back to a text input (param-only fallback)', () => {
expect(paramToField(p({ type: 'lookup' }))).toMatchObject({ type: 'text' });
expect(paramToField(p({ type: 'reference' }))).toMatchObject({ type: 'text' });
});
// #3405 — the fallback is now a broken-metadata signal, not a normal path,
// so it must be audible in dev instead of silently handing the user a box
// that wants a raw UUID.
it('warns in dev when a picker param degrades for want of a target', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
try {
paramToField(p({ name: 'inspector', type: 'lookup' }));
expect(warn).toHaveBeenCalledTimes(1);
expect(warn.mock.calls[0][0]).toContain('inspector');
expect(warn.mock.calls[0][0]).toContain('reference');
warn.mockClear();
paramToField(p({ name: 'inspector', type: 'lookup', referenceTo: 'sys_user' }));
expect(warn).not.toHaveBeenCalled();
} finally {
warn.mockRestore();
}
});
it('user params keep their picker without needing referenceTo (implicit sys_user)', () => {
expect(paramToField(p({ type: 'user' }))).toMatchObject({ type: 'user' });
});
});