Skip to content

Commit 47a3b9d

Browse files
authored
Merge pull request #521 from fractal-analytics-platform/sandbox3
Added Task Manifest Sandbox page, validated text in numeric inputs
2 parents 8b88177 + d4c6db4 commit 47a3b9d

26 files changed

+967
-205
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
> Starting from this release the Sandbox pages are not included in fractal-web anymore, instead they are static pages published together with the documentation.
66
7+
* Removed "Clear" button from tuples (\#521);
8+
* Added "Reset" button on top-level properties having a default value (\#521);
9+
* Added task manifest sandbox (\#521);
10+
* Validated invalid text in numeric inputs (\#521);
711
* Moved JSON Schema components to a separated component (\#518);
812
* Moved Sandbox pages to a separated component (\#518);
913

docs/sandbox-pages.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The following sandbox pages are available:
66

77
* [JSON Schema Sandbox page](../sandbox\#jschema)
88
* [Task Version Update Sandbox page](../sandbox\#version-update)
9+
* [Task Manifest Sandbox page](../sandbox\#task-manifest)
910

1011
Both these pages provide a textarea for the JSON schemas. Notice that you can't paste the whole task manifest: you need to pick one of the `args_schema` values.
1112

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { FormManager } from '../src/lib/components/form_manager';
3+
4+
describe('badInput', () => {
5+
it('Detect bad input in nested object', () => {
6+
const manager = new FormManager(
7+
{
8+
title: 'test',
9+
type: 'object',
10+
properties: {
11+
foo: {
12+
type: 'object',
13+
properties: {
14+
bar: {
15+
type: 'number'
16+
}
17+
}
18+
}
19+
}
20+
},
21+
vi.fn()
22+
);
23+
expect(manager.getFormData()).deep.eq({ foo: { bar: null } });
24+
manager.root.children[0].children[0].badInput = true;
25+
expect(manager.getFormData()).deep.eq({ foo: { bar: 'invalid' } });
26+
});
27+
28+
it('Detect bad input in nested array', () => {
29+
const manager = new FormManager(
30+
{
31+
title: 'test',
32+
type: 'object',
33+
properties: {
34+
foo: {
35+
default: [0],
36+
type: 'array',
37+
items: {
38+
type: 'number'
39+
}
40+
}
41+
}
42+
},
43+
vi.fn()
44+
);
45+
expect(manager.getFormData()).deep.eq({ foo: [0] });
46+
manager.root.children[0].children[0].badInput = true;
47+
expect(manager.getFormData()).deep.eq({ foo: ['invalid'] });
48+
});
49+
50+
it('Detect bad input in nested tuple', () => {
51+
const manager = new FormManager(
52+
{
53+
title: 'test',
54+
type: 'object',
55+
properties: {
56+
foo: {
57+
default: [0],
58+
type: 'array',
59+
minItems: 1,
60+
maxItems: 1,
61+
items: [
62+
{
63+
type: 'integer'
64+
}
65+
]
66+
}
67+
}
68+
},
69+
vi.fn()
70+
);
71+
expect(manager.getFormData()).deep.eq({ foo: [0] });
72+
manager.root.children[0].children[0].badInput = true;
73+
expect(manager.getFormData()).deep.eq({ foo: ['invalid'] });
74+
});
75+
});

jschema/__tests__/jschema_initial_data.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,4 +466,22 @@ describe('jschema_intial_data', () => {
466466
);
467467
expect(data).deep.eq({ b1: false, b2: null });
468468
});
469+
470+
it('Handle tuple size/items mismatch (minItems = 2; items.length = 1)', () => {
471+
const data = getJsonSchemaData({
472+
title: 'test',
473+
type: 'object',
474+
properties: {
475+
foo: {
476+
default: [1, 2],
477+
type: 'array',
478+
minItems: 2,
479+
maxItems: 2,
480+
items: [{ type: 'integer' }]
481+
}
482+
},
483+
required: ['foo']
484+
});
485+
expect(data).deep.eq({ foo: [1, null] });
486+
});
469487
});

jschema/__tests__/properties/array.test.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,20 @@ describe('Array properties', () => {
3535
checkBold(screen.getByText('ArrayProperty'), true);
3636
let inputs = screen.getAllByRole('textbox');
3737
expect(inputs.length).eq(3);
38-
const clearBtns = screen.getAllByRole('button', { name: 'Clear' });
39-
expect(clearBtns.length).eq(3);
38+
expect(screen.queryAllByRole('button', { name: 'Remove' })).toHaveLength(0);
4039
expect(component.getArguments()).deep.eq({ testProp: [null, null, null] });
4140
await fireEvent.input(inputs[0], { target: { value: 'foo' } });
4241
expect(onChange).toHaveBeenCalledWith({ testProp: ['foo', null, null] });
43-
await fireEvent.click(clearBtns[0]);
44-
expect(inputs[0]).toHaveValue('');
45-
expect(onChange).toHaveBeenCalledWith({ testProp: [null, null, null] });
4642
const addBtn = screen.getByRole('button', { name: 'Add argument to list' });
4743
expect(addBtn.disabled).toBe(false);
4844
await fireEvent.click(addBtn);
49-
expect(onChange).toHaveBeenCalledWith({ testProp: [null, null, null, null] });
45+
expect(onChange).toHaveBeenCalledWith({ testProp: ['foo', null, null, null] });
5046
expect(screen.getAllByRole('button', { name: 'Remove' }).length).eq(4);
5147
await fireEvent.click(addBtn);
5248
inputs = screen.getAllByRole('textbox');
5349
expect(inputs.length).eq(5);
5450
expect(onChange).toHaveBeenCalledWith({
55-
testProp: [null, null, null, null, null]
51+
testProp: ['foo', null, null, null, null]
5652
});
5753
expect(addBtn.disabled).toBe(true);
5854
});
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { fireEvent, screen } from '@testing-library/svelte';
3+
import { renderSchemaWithSingleProperty } from './property_test_utils';
4+
5+
describe('Reset properties to their default values', async () => {
6+
it('Reset object with default value on top-level property', async () => {
7+
const { component, onChange } = renderSchemaWithSingleProperty(
8+
{
9+
type: 'object',
10+
default: { key1: { key2: 'foo' } },
11+
properties: { key1: { type: 'object', properties: { key2: { type: 'string' } } } },
12+
required: ['key1']
13+
},
14+
true
15+
);
16+
expect(component.getArguments()).deep.eq({ testProp: { key1: { key2: 'foo' } } });
17+
await fireEvent.input(screen.getByRole('textbox'), { target: { value: 'bar' } });
18+
expect(onChange).toHaveBeenCalledWith({ testProp: { key1: { key2: 'bar' } } });
19+
await fireEvent.click(screen.getByRole('button', { name: 'Reset' }));
20+
expect(onChange).toHaveBeenCalledWith({ testProp: { key1: { key2: 'foo' } } });
21+
});
22+
23+
it('Object with default on nested object does not have the Reset button', async () => {
24+
const { component, onChange } = renderSchemaWithSingleProperty(
25+
{
26+
type: 'object',
27+
properties: {
28+
key1: {
29+
type: 'object',
30+
default: { key2: 'foo' },
31+
properties: { key2: { type: 'string' } }
32+
}
33+
},
34+
required: ['key1']
35+
},
36+
true
37+
);
38+
expect(component.getArguments()).deep.eq({ testProp: { key1: { key2: 'foo' } } });
39+
await fireEvent.input(screen.getByRole('textbox'), { target: { value: 'bar' } });
40+
expect(onChange).toHaveBeenCalledWith({ testProp: { key1: { key2: 'bar' } } });
41+
expect(screen.queryAllByRole('button', { name: 'Reset' })).toHaveLength(0);
42+
});
43+
44+
it('Reset tuple with default value on top-level property', async () => {
45+
const { component, onChange } = renderSchemaWithSingleProperty(
46+
{
47+
type: 'array',
48+
minItems: 2,
49+
maxItems: 2,
50+
items: [{ type: 'integer' }, { type: 'integer' }],
51+
default: [1, 2]
52+
},
53+
true
54+
);
55+
expect(component.getArguments()).deep.eq({ testProp: [1, 2] });
56+
const [input1, input2] = screen.getAllByRole('spinbutton');
57+
expect(input1).toHaveValue(1);
58+
expect(input2).toHaveValue(2);
59+
await fireEvent.input(input1, { target: { value: '3' } });
60+
expect(onChange).toHaveBeenCalledWith({ testProp: [3, 2] });
61+
await fireEvent.click(screen.getByRole('button', { name: 'Reset' }));
62+
expect(onChange).toHaveBeenCalledWith({ testProp: [1, 2] });
63+
});
64+
65+
it('Tuple with default on nested items does not have the Reset button', async () => {
66+
const { component, onChange } = renderSchemaWithSingleProperty(
67+
{
68+
type: 'array',
69+
minItems: 2,
70+
maxItems: 2,
71+
items: [
72+
{ type: 'integer', default: 1 },
73+
{ type: 'integer', default: 2 }
74+
]
75+
},
76+
true
77+
);
78+
expect(component.getArguments()).deep.eq({ testProp: [1, 2] });
79+
const [input1, input2] = screen.getAllByRole('spinbutton');
80+
expect(input1).toHaveValue(1);
81+
expect(input2).toHaveValue(2);
82+
await fireEvent.input(input1, { target: { value: '3' } });
83+
expect(onChange).toHaveBeenCalledWith({ testProp: [3, 2] });
84+
expect(screen.queryAllByRole('button', { name: 'Reset' })).toHaveLength(0);
85+
});
86+
87+
it('Reset array with default value on top-level property', async () => {
88+
const { component, onChange } = renderSchemaWithSingleProperty(
89+
{
90+
type: 'array',
91+
items: { type: 'string' },
92+
default: ['a', 'b']
93+
},
94+
true
95+
);
96+
expect(component.getArguments()).deep.eq({ testProp: ['a', 'b'] });
97+
const [input1, input2] = screen.getAllByRole('textbox');
98+
expect(input1).toHaveValue('a');
99+
expect(input2).toHaveValue('b');
100+
await fireEvent.input(input1, { target: { value: 'x' } });
101+
expect(onChange).toHaveBeenCalledWith({ testProp: ['x', 'b'] });
102+
await fireEvent.click(screen.getByRole('button', { name: 'Reset' }));
103+
expect(onChange).toHaveBeenCalledWith({ testProp: ['a', 'b'] });
104+
});
105+
106+
it('Array with default on nested items does not have the Reset button', async () => {
107+
const { component, onChange } = renderSchemaWithSingleProperty(
108+
{
109+
type: 'array',
110+
items: { type: 'string', default: 'a' },
111+
minItems: 2
112+
},
113+
true
114+
);
115+
expect(component.getArguments()).deep.eq({ testProp: ['a', 'a'] });
116+
const [input1, input2] = screen.getAllByRole('textbox');
117+
expect(input1).toHaveValue('a');
118+
expect(input2).toHaveValue('a');
119+
await fireEvent.input(input1, { target: { value: 'x' } });
120+
expect(onChange).toHaveBeenCalledWith({ testProp: ['x', 'a'] });
121+
expect(screen.queryAllByRole('button', { name: 'Reset' })).toHaveLength(0);
122+
});
123+
});

jschema/__tests__/properties/tuple.test.js

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,6 @@ describe('Tuple properties', () => {
5151

5252
// verify that value has been reset to default
5353
expect(onChange).toHaveBeenCalledWith({ patch_size: [1300, 'foo', 1] });
54-
55-
expect(screen.getAllByRole('button', { name: 'Clear' }).length).eq(3);
56-
inputs = screen.getAllByRole('spinbutton');
57-
expect(inputs.length).eq(2);
58-
expect(inputs[0]).toHaveValue(1300);
59-
expect(screen.getByRole('textbox').value).eq('foo');
60-
expect(inputs[1]).toHaveValue(1);
61-
await fireEvent.click(screen.getAllByRole('button', { name: 'Clear' })[0]);
62-
expect(onChange).toHaveBeenCalledWith({ patch_size: [null, 'foo', 1] });
63-
expect(inputs[0]).toHaveValue(null);
6454
});
6555

6656
it('optional tuple without default values', async function () {
@@ -114,13 +104,6 @@ describe('Tuple properties', () => {
114104
expect(inputs[1].value).eq('');
115105

116106
expect(onChange).toHaveBeenCalledWith({ patch_size: [null, null] });
117-
118-
await fireEvent.input(inputs[1], { target: { value: 'bar' } });
119-
expect(inputs[1].value).eq('bar');
120-
expect(onChange).toHaveBeenCalledWith({ patch_size: [null, 'bar'] });
121-
await fireEvent.click(screen.getAllByRole('button', { name: 'Clear' })[1]);
122-
expect(inputs[1].value).eq('');
123-
expect(onChange).toHaveBeenCalledWith({ patch_size: [null, null] });
124107
});
125108

126109
it('required tuple with default values', async function () {
@@ -158,10 +141,10 @@ describe('Tuple properties', () => {
158141
expect(inputs[1].value).eq('1500');
159142
expect(inputs[2].value).eq('1');
160143
expect(screen.queryAllByRole('button', { name: 'Remove tuple' }).length).eq(0);
161-
await fireEvent.click(screen.getAllByRole('button', { name: 'Clear' })[0]);
162-
expect(inputs[0].value).eq('');
163-
expect(screen.getAllByRole('spinbutton').length).eq(3);
164-
expect(onChange).toHaveBeenCalledWith({ patch_size: [null, 1500, 1] });
144+
await fireEvent.input(screen.getAllByRole('spinbutton')[0], { target: { value: 10 } });
145+
expect(onChange).toHaveBeenCalledWith({ patch_size: [10, 1500, 1] });
146+
await fireEvent.click(screen.getByRole('button', { name: 'Reset' }));
147+
expect(onChange).toHaveBeenCalledWith({ patch_size: [1300, 1500, 1] });
165148
});
166149

167150
it('required tuple without default values', async function () {
@@ -197,9 +180,7 @@ describe('Tuple properties', () => {
197180
await fireEvent.input(inputs[0], { target: { value: 'foo' } });
198181
expect(inputs[0].value).eq('foo');
199182
expect(onChange).toHaveBeenCalledWith({ patch_size: ['foo', null] });
200-
await fireEvent.click(screen.getAllByRole('button', { name: 'Clear' })[0]);
201-
expect(inputs[0].value).eq('');
202-
expect(onChange).toHaveBeenCalledWith({ patch_size: [null, null] });
183+
expect(screen.queryAllByRole('button', { name: 'Reset' })).toHaveLength(0);
203184
});
204185

205186
it('nested tuple', async function () {
@@ -247,8 +228,6 @@ describe('Tuple properties', () => {
247228
expect(screen.getAllByRole('button', { name: 'Remove tuple' }).length).toEqual(2);
248229
await fireEvent.input(screen.getByRole('textbox'), { target: { value: 'foo' } });
249230
expect(onChange).toHaveBeenCalledWith({ arg_A: [[['foo']]] });
250-
await fireEvent.click(screen.getByRole('button', { name: 'Clear' }));
251-
expect(onChange).toHaveBeenCalledWith({ arg_A: [[[null]]] });
252231
});
253232

254233
it('referenced tuple', async function () {

jschema/src/lib/components/JSchema.svelte

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@
4343
4444
function initFormManager() {
4545
if (schema) {
46-
formManager = new FormManager(schema, dispatch, propertiesToIgnore, schemaData);
46+
try {
47+
formManager = new FormManager(schema, dispatch, propertiesToIgnore, schemaData);
48+
} catch (err) {
49+
console.error(err);
50+
formManager = undefined;
51+
}
4752
} else {
4853
formManager = undefined;
4954
}
@@ -53,7 +58,7 @@
5358
{#if formManager}
5459
{#key formManager}
5560
<div id={componentId}>
56-
<ObjectProperty formElement={formManager.root} />
61+
<ObjectProperty formElement={formManager.root} isRoot={true} />
5762
</div>
5863
{/key}
5964
{/if}

0 commit comments

Comments
 (0)