Skip to content

Commit 4504a50

Browse files
committed
more progress, needs more reviewing
1 parent 5a17d5b commit 4504a50

File tree

3 files changed

+88
-13
lines changed

3 files changed

+88
-13
lines changed

packages/leva/src/plugins/Select/select-plugin.test.ts

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -245,33 +245,88 @@ describe('Select Plugin - normalize function', () => {
245245
expect(result.settings.keys).toEqual(['', 'a', 'b'])
246246
expect(result.settings.values).toEqual(['', 'a', 'b'])
247247
})
248+
249+
it('should handle invalid mixed-type arrays (fallback scenario)', () => {
250+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
251+
252+
// This tests the fallback case where options contain invalid types like nested arrays
253+
const input = { options: ['x', 'y', ['x', 'y']] as any }
254+
const result = normalize(input)
255+
256+
// Falls through to empty fallback
257+
expect(result.value).toBe(undefined)
258+
expect(result.settings.keys).toEqual([])
259+
expect(result.settings.values).toEqual([])
260+
expect(consoleWarnSpy).toHaveBeenCalledWith(
261+
expect.stringContaining('[Leva] Invalid Select options format'),
262+
input.options
263+
)
264+
265+
consoleWarnSpy.mockRestore()
266+
})
267+
268+
it('should handle completely invalid options (not array or object)', () => {
269+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
270+
271+
const input = { options: null as any }
272+
const result = normalize(input)
273+
274+
expect(result.value).toBe(undefined)
275+
expect(result.settings.keys).toEqual([])
276+
expect(result.settings.values).toEqual([])
277+
expect(consoleWarnSpy).toHaveBeenCalled()
278+
279+
consoleWarnSpy.mockRestore()
280+
})
248281
})
249282
})
250283

251284
describe('Select Plugin - schema validation', () => {
252285
it('should accept array of primitives', () => {
253-
const result = schema(null, ['x', 'y', 'z'])
254-
expect(result.success).toBe(true)
286+
const result = schema(null, { options: ['x', 'y', 'z'] })
287+
expect(result).toBe(true)
255288
})
256289

257290
it('should accept object with primitive values', () => {
258-
const result = schema(null, { foo: 'bar', baz: 1 })
259-
expect(result.success).toBe(true)
291+
const result = schema(null, { options: { foo: 'bar', baz: 1 } })
292+
expect(result).toBe(true)
260293
})
261294

262295
it('should accept array of value/label objects', () => {
263-
const result = schema(null, [{ value: 'x', label: 'X' }, { value: 'y' }])
264-
expect(result.success).toBe(true)
296+
const result = schema(null, { options: [{ value: 'x', label: 'X' }, { value: 'y' }] })
297+
expect(result).toBe(true)
265298
})
266299

267-
it('should reject invalid input', () => {
300+
it('should reject invalid input (missing options)', () => {
301+
const result = schema(null, {})
302+
expect(result).toBe(false)
303+
})
304+
305+
it('should reject null', () => {
268306
const result = schema(null, null)
269-
expect(result.success).toBe(false)
307+
expect(result).toBe(false)
308+
})
309+
310+
it('should accept array with non-primitive values (validation happens in normalize)', () => {
311+
// Schema only checks if 'options' exists and is an array/object
312+
// Detailed validation of options content happens in normalize() with better warnings
313+
const result = schema(null, { options: [{ nested: 'object' }, 'string'] })
314+
expect(result).toBe(true)
315+
})
316+
317+
it('should reject boolean primitives', () => {
318+
const result = schema(null, true)
319+
expect(result).toBe(false)
320+
})
321+
322+
it('should reject number primitives', () => {
323+
const result = schema(null, 10)
324+
expect(result).toBe(false)
270325
})
271326

272-
it('should reject array with non-primitive values', () => {
273-
const result = schema(null, [{ nested: 'object' }, 'string'])
274-
expect(result.success).toBe(false)
327+
it('should reject settings without options key', () => {
328+
const result = schema(null, { value: 'x' })
329+
expect(result).toBe(false)
275330
})
276331
})
277332

packages/leva/src/plugins/Select/select-plugin.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,20 @@ const arrayOfValueLabelObjectsSchema = z.array(valueLabelObjectSchema)
3737

3838
const allUsecases = z.union([arrayOfPrimitivesSchema, keyAsLabelObjectSchema, arrayOfValueLabelObjectsSchema])
3939

40+
/**
41+
* Schema for the settings object - checks if it has an 'options' key
42+
* We accept any array or any object (excluding arrays) for the options value
43+
* Detailed validation of options content happens in normalize() with better error messages
44+
*/
45+
const selectInputSchema = z.object({
46+
options: z.union([
47+
z.array(z.any()), // Any array
48+
z.object({}).passthrough(), // Any object with any keys
49+
]),
50+
})
51+
4052
// the options attribute is either a key value object, an array, or an array of {value, label} objects
41-
export const schema = (_o: any, s: any) => allUsecases.safeParse(s)
53+
export const schema = (_o: any, s: any) => selectInputSchema.safeParse(s).success
4254

4355
export const sanitize = (value: any, { values }: InternalSelectSettings) => {
4456
if (values.indexOf(value) < 0) throw Error(`Selected value doesn't match Select options`)
@@ -77,6 +89,14 @@ export const normalize = (input: SelectInput) => {
7789
gatheredKeys = Object.keys(isKeyAsLabelObject.data)
7890
} else {
7991
// Fallback (shouldn't happen if schema validation is correct)
92+
console.warn(
93+
'[Leva] Invalid Select options format. Expected one of:\n' +
94+
' - Array of primitives: ["x", "y", 1, true]\n' +
95+
' - Object with key-value pairs: { x: 1, foo: "bar" }\n' +
96+
' - Array of {value, label} objects: [{ value: "x", label: "X" }]\n' +
97+
'Received:',
98+
options
99+
)
80100
gatheredValues = []
81101
gatheredKeys = []
82102
}

packages/leva/stories/advanced/Busy.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ function BusyControls() {
5555
string: { value: 'something', optional: true, order: -2 },
5656
range: { value: 0, min: -10, max: 10, order: -3 },
5757
image: { image: undefined },
58-
select: { options: ['x', 'y', ['x', 'y']] },
58+
select: { options: ['x', 'y', 'x,y'] },
5959
interval: { min: -100, max: 100, value: [-10, 10] },
6060
color: '#ffffff',
6161
refMonitor: monitor(noise ? frame : () => 0, { graph: true, interval: 30 }),

0 commit comments

Comments
 (0)