Files
react-jsonschema-form/packages/core/test/ArrayField_test.js
Jason Ritchie ef8b7fce19 Monorepo with Lerna (#1501)
Use Lerna to maintain core and themes as a monorepo. This should streamline creation of new themes and maintenance of existing theme(s).
2019-11-10 08:26:01 -08:00

1674 lines
51 KiB
JavaScript

import React from "react";
import { expect } from "chai";
import { Simulate } from "react-dom/test-utils";
import { createFormComponent, createSandbox } from "./test_utils";
const ArrayKeyDataAttr = "data-rjsf-itemkey";
const ExposedArrayKeyTemplate = function(props) {
return (
<div className="array">
{props.items &&
props.items.map(element => (
<div
key={element.key}
className="array-item"
data-rjsf-itemkey={element.key}>
<div>{element.children}</div>
{(element.hasMoveUp || element.hasMoveDown) && (
<button
className="array-item-move-down"
onClick={element.onReorderClick(
element.index,
element.index + 1
)}>
Down
</button>
)}
{(element.hasMoveUp || element.hasMoveDown) && (
<button
className="array-item-move-up"
onClick={element.onReorderClick(
element.index,
element.index - 1
)}>
Up
</button>
)}
{element.hasRemove && (
<button
className="array-item-remove"
onClick={element.onDropIndexClick(element.index)}>
Remove
</button>
)}
<button onClick={element.onDropIndexClick(element.index)}>
Delete
</button>
<hr />
</div>
))}
{props.canAdd && (
<div className="array-item-add">
<button onClick={props.onAddClick} type="button">
Add New
</button>
</div>
)}
</div>
);
};
describe("ArrayField", () => {
let sandbox;
const CustomComponent = props => {
return <div id="custom">{props.rawErrors}</div>;
};
beforeEach(() => {
sandbox = createSandbox();
});
afterEach(() => {
sandbox.restore();
});
describe("Unsupported array schema", () => {
it("should warn on missing items descriptor", () => {
const { node } = createFormComponent({ schema: { type: "array" } });
expect(
node.querySelector(".field-array > .unsupported-field").textContent
).to.contain("Missing items definition");
});
});
describe("List of inputs", () => {
const schema = {
type: "array",
title: "my list",
description: "my description",
items: { type: "string" },
};
it("should render a fieldset", () => {
const { node } = createFormComponent({ schema });
const fieldset = node.querySelectorAll("fieldset");
expect(fieldset).to.have.length.of(1);
expect(fieldset[0].id).eql("root");
});
it("should render a fieldset legend", () => {
const { node } = createFormComponent({ schema });
const legend = node.querySelector("fieldset > legend");
expect(legend.textContent).eql("my list");
expect(legend.id).eql("root__title");
});
it("should render a description", () => {
const { node } = createFormComponent({ schema });
const description = node.querySelector("fieldset > .field-description");
expect(description.textContent).eql("my description");
expect(description.id).eql("root__description");
});
it("should render a hidden list", () => {
const { node } = createFormComponent({
schema,
uiSchema: {
"ui:widget": "hidden",
},
});
expect(node.querySelector("div.hidden > fieldset")).to.exist;
});
it("should render a customized title", () => {
const CustomTitleField = ({ title }) => <div id="custom">{title}</div>;
const { node } = createFormComponent({
schema,
fields: { TitleField: CustomTitleField },
});
expect(node.querySelector("fieldset > #custom").textContent).to.eql(
"my list"
);
});
it("should render a customized description", () => {
const CustomDescriptionField = ({ description }) => (
<div id="custom">{description}</div>
);
const { node } = createFormComponent({
schema,
fields: {
DescriptionField: CustomDescriptionField,
},
});
expect(node.querySelector("fieldset > #custom").textContent).to.eql(
"my description"
);
});
it("should render a customized file widget", () => {
const { node } = createFormComponent({
schema,
uiSchema: {
"ui:widget": "files",
},
widgets: { FileWidget: CustomComponent },
});
expect(node.querySelector("#custom")).to.exist;
});
it("should pass rawErrors down to custom array field templates", () => {
const schema = {
type: "array",
title: "my list",
description: "my description",
items: { type: "string" },
minItems: 2,
};
const { node } = createFormComponent({
schema,
ArrayFieldTemplate: CustomComponent,
formData: [1],
liveValidate: true,
});
const matches = node.querySelectorAll("#custom");
expect(matches).to.have.length.of(1);
expect(matches[0].textContent).to.eql(
"should NOT have fewer than 2 items"
);
});
it("should contain no field in the list by default", () => {
const { node } = createFormComponent({ schema });
expect(node.querySelectorAll(".field-string")).to.have.length.of(0);
});
it("should have an add button", () => {
const { node } = createFormComponent({ schema });
expect(node.querySelector(".array-item-add button")).not.eql(null);
});
it("should not have an add button if addable is false", () => {
const { node } = createFormComponent({
schema,
uiSchema: { "ui:options": { addable: false } },
});
expect(node.querySelector(".array-item-add button")).to.be.null;
});
it("should add a new field when clicking the add button", () => {
const { node } = createFormComponent({ schema });
Simulate.click(node.querySelector(".array-item-add button"));
expect(node.querySelectorAll(".field-string")).to.have.length.of(1);
});
it("should assign new keys/ids when clicking the add button", () => {
const { node } = createFormComponent({
schema,
ArrayFieldTemplate: ExposedArrayKeyTemplate,
});
Simulate.click(node.querySelector(".array-item-add button"));
expect(node.querySelector(".array-item").hasAttribute(ArrayKeyDataAttr))
.to.be.true;
});
it("should not provide an add button if length equals maxItems", () => {
const { node } = createFormComponent({
schema: { maxItems: 2, ...schema },
formData: ["foo", "bar"],
});
expect(node.querySelector(".array-item-add button")).to.be.null;
});
it("should provide an add button if length is lesser than maxItems", () => {
const { node } = createFormComponent({
schema: { maxItems: 2, ...schema },
formData: ["foo"],
});
expect(node.querySelector(".array-item-add button")).not.eql(null);
});
it("should retain existing row keys/ids when adding new row", () => {
const { node } = createFormComponent({
schema: { maxItems: 2, ...schema },
formData: ["foo"],
ArrayFieldTemplate: ExposedArrayKeyTemplate,
});
const startRows = node.querySelectorAll(".array-item");
const startRow1_key = startRows[0].getAttribute(ArrayKeyDataAttr);
const startRow2_key = startRows[1]
? startRows[1].getAttribute(ArrayKeyDataAttr)
: undefined;
Simulate.click(node.querySelector(".array-item-add button"));
const endRows = node.querySelectorAll(".array-item");
const endRow1_key = endRows[0].getAttribute(ArrayKeyDataAttr);
const endRow2_key = endRows[1].getAttribute(ArrayKeyDataAttr);
expect(startRow1_key).to.equal(endRow1_key);
expect(startRow2_key).to.not.equal(endRow2_key);
expect(startRow2_key).to.be.undefined;
expect(endRows[0].hasAttribute(ArrayKeyDataAttr)).to.be.true;
expect(endRows[1].hasAttribute(ArrayKeyDataAttr)).to.be.true;
});
it("should allow inserting anywhere in list", () => {
function addItemAboveOrBelow(item) {
const beforeIndex = item.index;
const addBeforeButton = (
<button
key={`array-item-add-before-${item.key}`}
className={
"array-item-move-before array-item-move-before-to-" + beforeIndex
}
onClick={item.onAddIndexClick(beforeIndex)}>
{"Add Item Above"}
</button>
);
const afterIndex = item.index + 1;
const addAfterButton = (
<button
key={`array-item-add-after-${item.key}`}
className={
"array-item-move-after array-item-move-after-to-" + afterIndex
}
onClick={item.onAddIndexClick(afterIndex)}>
{"Add Item Below"}
</button>
);
return (
<div
key={item.key}
data-rjsf-itemkey={item.key}
className={`array-item item-${item.index}`}>
<div>{addBeforeButton}</div>
{item.children}
<div>{addAfterButton}</div>
<hr />
</div>
);
}
function addAboveOrBelowArrayFieldTemplate(props) {
return (
<div className="array">{props.items.map(addItemAboveOrBelow)}</div>
);
}
const { node } = createFormComponent({
schema,
formData: ["foo", "bar", "baz"],
ArrayFieldTemplate: addAboveOrBelowArrayFieldTemplate,
});
const addBeforeButtons = node.querySelectorAll(".array-item-move-before");
const addAfterButtons = node.querySelectorAll(".array-item-move-after");
const startRows = node.querySelectorAll(".array-item");
const startRow1_key = startRows[0].getAttribute(ArrayKeyDataAttr);
const startRow2_key = startRows[1].getAttribute(ArrayKeyDataAttr);
const startRow3_key = startRows[2].getAttribute(ArrayKeyDataAttr);
Simulate.click(addBeforeButtons[0]);
Simulate.click(addAfterButtons[0]);
const endRows = node.querySelectorAll(".array-item");
const endRow2_key = endRows[1].getAttribute(ArrayKeyDataAttr);
const endRow4_key = endRows[3].getAttribute(ArrayKeyDataAttr);
const endRow5_key = endRows[4].getAttribute(ArrayKeyDataAttr);
expect(startRow1_key).to.equal(endRow2_key);
expect(startRow2_key).to.equal(endRow4_key);
expect(startRow3_key).to.equal(endRow5_key);
expect(endRows[0].hasAttribute(ArrayKeyDataAttr)).to.be.true;
expect(endRows[1].hasAttribute(ArrayKeyDataAttr)).to.be.true;
expect(endRows[2].hasAttribute(ArrayKeyDataAttr)).to.be.true;
expect(endRows[3].hasAttribute(ArrayKeyDataAttr)).to.be.true;
expect(endRows[4].hasAttribute(ArrayKeyDataAttr)).to.be.true;
});
it("should not provide an add button if addable is expliclty false regardless maxItems value", () => {
const { node } = createFormComponent({
schema: { maxItems: 2, ...schema },
formData: ["foo"],
uiSchema: {
"ui:options": {
addable: false,
},
},
});
expect(node.querySelector(".array-item-add button")).to.be.null;
});
it("should ignore addable value if maxItems constraint is not satisfied", () => {
const { node } = createFormComponent({
schema: { maxItems: 2, ...schema },
formData: ["foo", "bar"],
uiSchema: {
"ui:options": {
addable: true,
},
},
});
expect(node.querySelector(".array-item-add button")).to.be.null;
});
it("should mark a non-null array item widget as required", () => {
const { node } = createFormComponent({ schema });
Simulate.click(node.querySelector(".array-item-add button"));
expect(node.querySelector(".field-string input[type=text]").required).eql(
true
);
});
it("should fill an array field with data", () => {
const { node } = createFormComponent({
schema,
formData: ["foo", "bar"],
});
const inputs = node.querySelectorAll(".field-string input[type=text]");
expect(inputs).to.have.length.of(2);
expect(inputs[0].value).eql("foo");
expect(inputs[1].value).eql("bar");
});
it("shouldn't have reorder buttons when list length <= 1", () => {
const { node } = createFormComponent({ schema, formData: ["foo"] });
expect(node.querySelector(".array-item-move-up")).eql(null);
expect(node.querySelector(".array-item-move-down")).eql(null);
});
it("should have reorder buttons when list length >= 2", () => {
const { node } = createFormComponent({
schema,
formData: ["foo", "bar"],
});
expect(node.querySelector(".array-item-move-up")).not.eql(null);
expect(node.querySelector(".array-item-move-down")).not.eql(null);
});
it("should move down a field from the list", () => {
const { node } = createFormComponent({
schema,
formData: ["foo", "bar", "baz"],
});
const moveDownBtns = node.querySelectorAll(".array-item-move-down");
Simulate.click(moveDownBtns[0]);
const inputs = node.querySelectorAll(".field-string input[type=text]");
expect(inputs).to.have.length.of(3);
expect(inputs[0].value).eql("bar");
expect(inputs[1].value).eql("foo");
expect(inputs[2].value).eql("baz");
});
it("should move up a field from the list", () => {
const { node } = createFormComponent({
schema,
formData: ["foo", "bar", "baz"],
});
const moveUpBtns = node.querySelectorAll(".array-item-move-up");
Simulate.click(moveUpBtns[2]);
const inputs = node.querySelectorAll(".field-string input[type=text]");
expect(inputs).to.have.length.of(3);
expect(inputs[0].value).eql("foo");
expect(inputs[1].value).eql("baz");
expect(inputs[2].value).eql("bar");
});
it("should retain row keys/ids when moving down", () => {
const { node } = createFormComponent({
schema,
formData: ["foo", "bar", "baz"],
ArrayFieldTemplate: ExposedArrayKeyTemplate,
});
const moveDownBtns = node.querySelectorAll(".array-item-move-down");
const startRows = node.querySelectorAll(".array-item");
const startRow1_key = startRows[0].getAttribute(ArrayKeyDataAttr);
const startRow2_key = startRows[1].getAttribute(ArrayKeyDataAttr);
const startRow3_key = startRows[2].getAttribute(ArrayKeyDataAttr);
Simulate.click(moveDownBtns[0]);
const endRows = node.querySelectorAll(".array-item");
const endRow1_key = endRows[0].getAttribute(ArrayKeyDataAttr);
const endRow2_key = endRows[1].getAttribute(ArrayKeyDataAttr);
const endRow3_key = endRows[2].getAttribute(ArrayKeyDataAttr);
expect(startRow1_key).to.equal(endRow2_key);
expect(startRow2_key).to.equal(endRow1_key);
expect(startRow3_key).to.equal(endRow3_key);
expect(endRows[0].hasAttribute(ArrayKeyDataAttr)).to.be.true;
expect(endRows[1].hasAttribute(ArrayKeyDataAttr)).to.be.true;
expect(endRows[2].hasAttribute(ArrayKeyDataAttr)).to.be.true;
});
it("should retain row keys/ids when moving up", () => {
const { node } = createFormComponent({
schema,
formData: ["foo", "bar", "baz"],
ArrayFieldTemplate: ExposedArrayKeyTemplate,
});
const moveUpBtns = node.querySelectorAll(".array-item-move-up");
const startRows = node.querySelectorAll(".array-item");
const startRow1_key = startRows[0].getAttribute(ArrayKeyDataAttr);
const startRow2_key = startRows[1].getAttribute(ArrayKeyDataAttr);
const startRow3_key = startRows[2].getAttribute(ArrayKeyDataAttr);
Simulate.click(moveUpBtns[2]);
const endRows = node.querySelectorAll(".array-item");
const endRow1_key = endRows[0].getAttribute(ArrayKeyDataAttr);
const endRow2_key = endRows[1].getAttribute(ArrayKeyDataAttr);
const endRow3_key = endRows[2].getAttribute(ArrayKeyDataAttr);
expect(startRow1_key).to.equal(endRow1_key);
expect(startRow2_key).to.equal(endRow3_key);
expect(startRow3_key).to.equal(endRow2_key);
expect(endRows[0].hasAttribute(ArrayKeyDataAttr)).to.be.true;
expect(endRows[1].hasAttribute(ArrayKeyDataAttr)).to.be.true;
expect(endRows[2].hasAttribute(ArrayKeyDataAttr)).to.be.true;
});
it("should move from first to last in the list", () => {
function moveAnywhereArrayItemTemplate(props) {
const buttons = [];
for (let i = 0; i < 3; i++) {
buttons.push(
<button
key={i}
className={"array-item-move-to-" + i}
onClick={props.onReorderClick(props.index, i)}>
{"Move item to index " + i}
</button>
);
}
return (
<div
key={props.key}
data-rjsf-itemkey={props.key}
className={`array-item item-${props.index}`}>
{props.children}
{buttons}
</div>
);
}
function moveAnywhereArrayFieldTemplate(props) {
return (
<div className="array">
{props.items.map(moveAnywhereArrayItemTemplate)}
</div>
);
}
const { node } = createFormComponent({
schema,
formData: ["foo", "bar", "baz"],
ArrayFieldTemplate: moveAnywhereArrayFieldTemplate,
});
const startRows = node.querySelectorAll(".array-item");
const startRow1_key = startRows[0].getAttribute(ArrayKeyDataAttr);
const startRow2_key = startRows[1].getAttribute(ArrayKeyDataAttr);
const startRow3_key = startRows[2].getAttribute(ArrayKeyDataAttr);
const button = node.querySelector(".item-0 .array-item-move-to-2");
Simulate.click(button);
const inputs = node.querySelectorAll(".field-string input[type=text]");
expect(inputs[0].value).eql("bar");
expect(inputs[1].value).eql("baz");
expect(inputs[2].value).eql("foo");
const endRows = node.querySelectorAll(".array-item");
const endRow1_key = endRows[0].getAttribute(ArrayKeyDataAttr);
const endRow2_key = endRows[1].getAttribute(ArrayKeyDataAttr);
const endRow3_key = endRows[2].getAttribute(ArrayKeyDataAttr);
expect(startRow1_key).to.equal(endRow3_key);
expect(startRow2_key).to.equal(endRow1_key);
expect(startRow3_key).to.equal(endRow2_key);
expect(endRows[0].hasAttribute(ArrayKeyDataAttr)).to.be.true;
expect(endRows[1].hasAttribute(ArrayKeyDataAttr)).to.be.true;
expect(endRows[2].hasAttribute(ArrayKeyDataAttr)).to.be.true;
});
it("should disable move buttons on the ends of the list", () => {
const { node } = createFormComponent({
schema,
formData: ["foo", "bar"],
});
const moveUpBtns = node.querySelectorAll(".array-item-move-up");
const moveDownBtns = node.querySelectorAll(".array-item-move-down");
expect(moveUpBtns[0].disabled).eql(true);
expect(moveDownBtns[0].disabled).eql(false);
expect(moveUpBtns[1].disabled).eql(false);
expect(moveDownBtns[1].disabled).eql(true);
});
it("should not show move up/down buttons if orderable is false", () => {
const { node } = createFormComponent({
schema,
formData: ["foo", "bar"],
uiSchema: { "ui:options": { orderable: false } },
});
const moveUpBtns = node.querySelector(".array-item-move-up");
const moveDownBtns = node.querySelector(".array-item-move-down");
expect(moveUpBtns).to.be.null;
expect(moveDownBtns).to.be.null;
});
it("should remove a field from the list", () => {
const { node } = createFormComponent({
schema,
formData: ["foo", "bar"],
});
const dropBtns = node.querySelectorAll(".array-item-remove");
Simulate.click(dropBtns[0]);
const inputs = node.querySelectorAll(".field-string input[type=text]");
expect(inputs).to.have.length.of(1);
expect(inputs[0].value).eql("bar");
});
it("should delete item from list and correct indices", () => {
const { node } = createFormComponent({
schema,
formData: ["foo", "bar", "baz"],
});
const deleteBtns = node.querySelectorAll(".array-item-remove");
Simulate.click(deleteBtns[0]);
const inputs = node.querySelectorAll(".field-string input[type=text]");
Simulate.change(inputs[0], { target: { value: "fuzz" } });
expect(inputs).to.have.length.of(2);
expect(inputs[0].value).eql("fuzz");
expect(inputs[1].value).eql("baz");
});
it("should retain row keys/ids of remaining rows when a row is removed", () => {
const { node } = createFormComponent({
schema,
formData: ["foo", "bar"],
ArrayFieldTemplate: ExposedArrayKeyTemplate,
});
const startRows = node.querySelectorAll(".array-item");
const startRow2_key = startRows[1].getAttribute(ArrayKeyDataAttr);
const dropBtns = node.querySelectorAll(".array-item-remove");
Simulate.click(dropBtns[0]);
const endRows = node.querySelectorAll(".array-item");
const endRow1_key = endRows[0].getAttribute(ArrayKeyDataAttr);
expect(startRow2_key).to.equal(endRow1_key);
expect(endRows[0].hasAttribute(ArrayKeyDataAttr)).to.be.true;
});
it("should not show remove button if removable is false", () => {
const { node } = createFormComponent({
schema,
formData: ["foo", "bar"],
uiSchema: { "ui:options": { removable: false } },
});
const dropBtn = node.querySelector(".array-item-remove");
expect(dropBtn).to.be.null;
});
it("should force revalidation when a field is removed", () => {
// refs #195
const { node } = createFormComponent({
schema: {
...schema,
items: { ...schema.items, minLength: 4 },
},
formData: ["foo", "bar!"],
});
try {
Simulate.submit(node);
} catch (e) {
// Silencing error thrown as failure is expected here
}
expect(
node.querySelectorAll(".has-error .error-detail")
).to.have.length.of(1);
const dropBtns = node.querySelectorAll(".array-item-remove");
Simulate.click(dropBtns[0]);
expect(
node.querySelectorAll(".has-error .error-detail")
).to.have.length.of(0);
});
it("should handle cleared field values in the array", () => {
const schema = {
type: "array",
items: { type: "integer" },
};
const formData = [1, 2, 3];
const { comp, node } = createFormComponent({
liveValidate: true,
schema,
formData,
});
Simulate.change(node.querySelector("#root_1"), {
target: { value: "" },
});
expect(comp.state.formData).eql([1, null, 3]);
expect(comp.state.errors).to.have.length.of(1);
});
it("should render the input widgets with the expected ids", () => {
const { node } = createFormComponent({
schema,
formData: ["foo", "bar"],
});
const inputs = node.querySelectorAll("input[type=text]");
expect(inputs[0].id).eql("root_0");
expect(inputs[1].id).eql("root_1");
});
it("should render nested input widgets with the expected ids", () => {
const complexSchema = {
type: "object",
properties: {
foo: {
type: "array",
items: {
type: "object",
properties: {
bar: { type: "string" },
baz: { type: "string" },
},
},
},
},
};
const { node } = createFormComponent({
schema: complexSchema,
formData: {
foo: [{ bar: "bar1", baz: "baz1" }, { bar: "bar2", baz: "baz2" }],
},
});
const inputs = node.querySelectorAll("input[type=text]");
expect(inputs[0].id).eql("root_foo_0_bar");
expect(inputs[1].id).eql("root_foo_0_baz");
expect(inputs[2].id).eql("root_foo_1_bar");
expect(inputs[3].id).eql("root_foo_1_baz");
});
it("should render enough inputs with proper defaults to match minItems in schema when no formData is set", () => {
const complexSchema = {
type: "object",
definitions: {
Thing: {
type: "object",
properties: {
name: {
type: "string",
default: "Default name",
},
},
},
},
properties: {
foo: {
type: "array",
minItems: 2,
items: {
$ref: "#/definitions/Thing",
},
},
},
};
let form = createFormComponent({
schema: complexSchema,
formData: {},
});
let inputs = form.node.querySelectorAll("input[type=text]");
expect(inputs[0].value).eql("Default name");
expect(inputs[1].value).eql("Default name");
});
it("should render an input for each default value, even when this is greater than minItems", () => {
const schema = {
type: "object",
properties: {
turtles: {
type: "array",
minItems: 2,
default: ["Raphael", "Michaelangelo", "Donatello", "Leonardo"],
items: {
type: "string",
},
},
},
};
const { node } = createFormComponent({ schema: schema });
const inputs = node.querySelectorAll("input[type=text]");
expect(inputs.length).to.eql(4);
expect(inputs[0].value).to.eql("Raphael");
expect(inputs[1].value).to.eql("Michaelangelo");
expect(inputs[2].value).to.eql("Donatello");
expect(inputs[3].value).to.eql("Leonardo");
});
it("should render enough input to match minItems, populating the first with default values, and the rest empty", () => {
const schema = {
type: "object",
properties: {
turtles: {
type: "array",
minItems: 4,
default: ["Raphael", "Michaelangelo"],
items: {
type: "string",
},
},
},
};
const { node } = createFormComponent({ schema });
const inputs = node.querySelectorAll("input[type=text]");
expect(inputs.length).to.eql(4);
expect(inputs[0].value).to.eql("Raphael");
expect(inputs[1].value).to.eql("Michaelangelo");
expect(inputs[2].value).to.eql("");
expect(inputs[3].value).to.eql("");
});
it("should render enough input to match minItems, populating the first with default values, and the rest with the item default", () => {
const schema = {
type: "object",
properties: {
turtles: {
type: "array",
minItems: 4,
default: ["Raphael", "Michaelangelo"],
items: {
type: "string",
default: "Unknown",
},
},
},
};
const { node } = createFormComponent({ schema });
const inputs = node.querySelectorAll("input[type=text]");
expect(inputs.length).to.eql(4);
expect(inputs[0].value).to.eql("Raphael");
expect(inputs[1].value).to.eql("Michaelangelo");
expect(inputs[2].value).to.eql("Unknown");
expect(inputs[3].value).to.eql("Unknown");
});
it("should not add minItems extra formData entries when schema item is a multiselect", () => {
const schema = {
type: "object",
properties: {
multipleChoicesList: {
type: "array",
minItems: 3,
uniqueItems: true,
items: {
type: "string",
enum: ["Aramis", "Athos", "Porthos", "d'Artagnan"],
},
},
},
};
const uiSchema = {
multipleChoicesList: {
"ui:widget": "checkboxes",
},
};
const form = createFormComponent({
schema: schema,
uiSchema: uiSchema,
formData: {},
liveValidate: true,
}).comp;
expect(form.state.formData).to.have.property("multipleChoicesList");
expect(form.state.formData.multipleChoicesList).to.be.empty;
expect(form.state.errors.length).to.equal(1);
expect(form.state.errors[0].name).to.equal("minItems");
expect(form.state.errors[0].params.limit).to.equal(3);
});
it("should honor given formData, even when it does not meet ths minItems-requirement", () => {
const complexSchema = {
type: "object",
definitions: {
Thing: {
type: "object",
properties: {
name: {
type: "string",
default: "Default name",
},
},
},
},
properties: {
foo: {
type: "array",
minItems: 2,
items: {
$ref: "#/definitions/Thing",
},
},
},
};
const form = createFormComponent({
schema: complexSchema,
formData: { foo: [] },
});
const inputs = form.node.querySelectorAll("input[type=text]");
expect(inputs.length).eql(0);
});
});
describe("Multiple choices list", () => {
const schema = {
type: "array",
title: "My field",
items: {
enum: ["foo", "bar", "fuzz"],
type: "string",
},
uniqueItems: true,
};
describe("Select multiple widget", () => {
it("should render a select widget", () => {
const { node } = createFormComponent({ schema });
expect(node.querySelectorAll("select")).to.have.length.of(1);
});
it("should render a select widget with a label", () => {
const { node } = createFormComponent({ schema });
expect(node.querySelector(".field label").textContent).eql("My field");
});
it("should render a select widget with multiple attribute", () => {
const { node } = createFormComponent({ schema });
expect(node.querySelector(".field select").getAttribute("multiple")).not
.to.be.null;
});
it("should render options", () => {
const { node } = createFormComponent({ schema });
expect(node.querySelectorAll("select option")).to.have.length.of(3);
});
it("should handle a change event", () => {
const { comp, node } = createFormComponent({ schema });
Simulate.change(node.querySelector(".field select"), {
target: {
options: [
{ selected: true, value: "foo" },
{ selected: true, value: "bar" },
{ selected: false, value: "fuzz" },
],
},
});
expect(comp.state.formData).eql(["foo", "bar"]);
});
it("should handle a blur event", () => {
const onBlur = sandbox.spy();
const { node } = createFormComponent({ schema, onBlur });
const select = node.querySelector(".field select");
Simulate.blur(select, {
target: {
options: [
{ selected: true, value: "foo" },
{ selected: true, value: "bar" },
{ selected: false, value: "fuzz" },
],
},
});
expect(onBlur.calledWith(select.id, ["foo", "bar"])).to.be.true;
});
it("should handle a focus event", () => {
const onFocus = sandbox.spy();
const { node } = createFormComponent({ schema, onFocus });
const select = node.querySelector(".field select");
Simulate.focus(select, {
target: {
options: [
{ selected: true, value: "foo" },
{ selected: true, value: "bar" },
{ selected: false, value: "fuzz" },
],
},
});
expect(onFocus.calledWith(select.id, ["foo", "bar"])).to.be.true;
});
it("should fill field with data", () => {
const { node } = createFormComponent({
schema,
formData: ["foo", "bar"],
});
const options = node.querySelectorAll(".field select option");
expect(options).to.have.length.of(3);
expect(options[0].selected).eql(true); // foo
expect(options[1].selected).eql(true); // bar
expect(options[2].selected).eql(false); // fuzz
});
it("should render the select widget with the expected id", () => {
const { node } = createFormComponent({ schema });
expect(node.querySelector("select").id).eql("root");
});
it("should pass rawErrors down to custom widgets", () => {
const { node } = createFormComponent({
schema,
widgets: {
SelectWidget: CustomComponent,
},
formData: ["foo", "foo"],
liveValidate: true,
});
const matches = node.querySelectorAll("#custom");
expect(matches).to.have.length.of(1);
expect(matches[0].textContent).to.eql(
"should NOT have duplicate items (items ## 1 and 0 are identical)"
);
});
});
describe("CheckboxesWidget", () => {
const uiSchema = {
"ui:widget": "checkboxes",
};
it("should render the expected number of checkboxes", () => {
const { node } = createFormComponent({ schema, uiSchema });
expect(node.querySelectorAll("[type=checkbox]")).to.have.length.of(3);
});
it("should render the expected labels", () => {
const { node } = createFormComponent({ schema, uiSchema });
const labels = [].map.call(
node.querySelectorAll(".checkbox label"),
node => node.textContent
);
expect(labels).eql(["foo", "bar", "fuzz"]);
});
it("should handle a change event", () => {
const { comp, node } = createFormComponent({
schema,
uiSchema,
});
Simulate.change(node.querySelectorAll("[type=checkbox]")[0], {
target: { checked: true },
});
Simulate.change(node.querySelectorAll("[type=checkbox]")[2], {
target: { checked: true },
});
expect(comp.state.formData).eql(["foo", "fuzz"]);
});
it("should fill field with data", () => {
const { node } = createFormComponent({
schema,
uiSchema,
formData: ["foo", "fuzz"],
});
const labels = [].map.call(
node.querySelectorAll("[type=checkbox]"),
node => node.checked
);
expect(labels).eql([true, false, true]);
});
it("should render the widget with the expected id", () => {
const { node } = createFormComponent({ schema, uiSchema });
expect(node.querySelector(".checkboxes").id).eql("root");
});
it("should support inline checkboxes", () => {
const { node } = createFormComponent({
schema,
uiSchema: {
"ui:widget": "checkboxes",
"ui:options": {
inline: true,
},
},
});
expect(node.querySelectorAll(".checkbox-inline")).to.have.length.of(3);
});
it("should pass rawErrors down to custom widgets", () => {
const schema = {
type: "array",
title: "My field",
items: {
enum: ["foo", "bar", "fuzz"],
type: "string",
},
minItems: 3,
uniqueItems: true,
};
const { node } = createFormComponent({
schema,
widgets: {
CheckboxesWidget: CustomComponent,
},
uiSchema,
formData: [],
liveValidate: true,
});
const matches = node.querySelectorAll("#custom");
expect(matches).to.have.length.of(1);
expect(matches[0].textContent).to.eql(
"should NOT have fewer than 3 items"
);
});
});
});
describe("Multiple files field", () => {
const schema = {
type: "array",
title: "My field",
items: {
type: "string",
format: "data-url",
},
};
it("should render an input[type=file] widget", () => {
const { node } = createFormComponent({ schema });
expect(node.querySelectorAll("input[type=file]")).to.have.length.of(1);
});
it("should render a select widget with a label", () => {
const { node } = createFormComponent({ schema });
expect(node.querySelector(".field label").textContent).eql("My field");
});
it("should render a file widget with multiple attribute", () => {
const { node } = createFormComponent({ schema });
expect(node.querySelector(".field [type=file]").getAttribute("multiple"))
.not.to.be.null;
});
it("should handle a change event", () => {
sandbox.stub(window, "FileReader").returns({
set onload(fn) {
fn({ target: { result: "data:text/plain;base64,x=" } });
},
readAsDataUrl() {},
});
const { comp, node } = createFormComponent({ schema });
Simulate.change(node.querySelector(".field input[type=file]"), {
target: {
files: [
{ name: "file1.txt", size: 1, type: "type" },
{ name: "file2.txt", size: 2, type: "type" },
],
},
});
return new Promise(setImmediate).then(() =>
expect(comp.state.formData).eql([
"data:text/plain;name=file1.txt;base64,x=",
"data:text/plain;name=file2.txt;base64,x=",
])
);
});
it("should fill field with data", () => {
const { node } = createFormComponent({
schema,
formData: [
"data:text/plain;name=file1.txt;base64,dGVzdDE=",
"data:image/png;name=file2.png;base64,ZmFrZXBuZw==",
],
});
const li = node.querySelectorAll(".file-info li");
expect(li).to.have.length.of(2);
expect(li[0].textContent).eql("file1.txt (text/plain, 5 bytes)");
expect(li[1].textContent).eql("file2.png (image/png, 7 bytes)");
});
it("should render the file widget with the expected id", () => {
const { node } = createFormComponent({ schema });
expect(node.querySelector("input[type=file]").id).eql("root");
});
it("should pass rawErrors down to custom widgets", () => {
const schema = {
type: "array",
title: "My field",
items: {
type: "string",
format: "data-url",
},
minItems: 5,
};
const { node } = createFormComponent({
schema,
widgets: {
FileWidget: CustomComponent,
},
formData: [],
liveValidate: true,
});
const matches = node.querySelectorAll("#custom");
expect(matches).to.have.length.of(1);
expect(matches[0].textContent).to.eql(
"should NOT have fewer than 5 items"
);
});
});
describe("Nested lists", () => {
const schema = {
type: "array",
title: "A list of arrays",
items: {
type: "array",
title: "A list of numbers",
items: {
type: "number",
},
},
};
it("should render two lists of inputs inside of a list", () => {
const { node } = createFormComponent({
schema,
formData: [[1, 2], [3, 4]],
});
expect(node.querySelectorAll("fieldset fieldset")).to.have.length.of(2);
});
it("should add an inner list when clicking the add button", () => {
const { node } = createFormComponent({ schema });
expect(node.querySelectorAll("fieldset fieldset")).to.be.empty;
Simulate.click(node.querySelector(".array-item-add button"));
expect(node.querySelectorAll("fieldset fieldset")).to.have.length.of(1);
});
it("should pass rawErrors down to every level of custom widgets", () => {
const CustomItem = props => <div id="custom-item">{props.children}</div>;
const CustomTemplate = props => {
return (
<div id="custom">
{props.items &&
props.items.map((p, i) => <CustomItem key={i} {...p} />)}
<div id="custom-error">
{props.rawErrors && props.rawErrors.join(", ")}
</div>
</div>
);
};
const schema = {
type: "array",
title: "A list of arrays",
items: {
type: "array",
title: "A list of numbers",
items: {
type: "number",
},
minItems: 3,
},
minItems: 2,
};
const { node } = createFormComponent({
schema,
ArrayFieldTemplate: CustomTemplate,
formData: [[]],
liveValidate: true,
});
const matches = node.querySelectorAll("#custom-error");
expect(matches).to.have.length.of(2);
expect(matches[0].textContent).to.eql(
"should NOT have fewer than 3 items"
);
expect(matches[1].textContent).to.eql(
"should NOT have fewer than 2 items"
);
});
});
describe("Fixed items lists", () => {
const schema = {
type: "array",
title: "List of fixed items",
items: [
{
type: "string",
title: "Some text",
},
{
type: "number",
title: "A number",
},
],
};
const schemaAdditional = {
type: "array",
title: "List of fixed items",
items: [
{
type: "number",
title: "A number",
},
{
type: "number",
title: "Another number",
},
],
additionalItems: {
type: "string",
title: "Additional item",
},
};
it("should render a fieldset", () => {
const { node } = createFormComponent({ schema });
expect(node.querySelectorAll("fieldset")).to.have.length.of(1);
});
it("should render a fieldset legend", () => {
const { node } = createFormComponent({ schema });
const legend = node.querySelector("fieldset > legend");
expect(legend.textContent).eql("List of fixed items");
expect(legend.id).eql("root__title");
});
it("should render field widgets", () => {
const { node } = createFormComponent({ schema });
const strInput = node.querySelector(
"fieldset .field-string input[type=text]"
);
const numInput = node.querySelector(
"fieldset .field-number input[type=number]"
);
expect(strInput.id).eql("root_0");
expect(numInput.id).eql("root_1");
});
it("should mark non-null item widgets as required", () => {
const { node } = createFormComponent({ schema });
const strInput = node.querySelector(
"fieldset .field-string input[type=text]"
);
const numInput = node.querySelector(
"fieldset .field-number input[type=number]"
);
expect(strInput.required).eql(true);
expect(numInput.required).eql(true);
});
it("should fill fields with data", () => {
const { node } = createFormComponent({
schema,
formData: ["foo", 42],
});
const strInput = node.querySelector(
"fieldset .field-string input[type=text]"
);
const numInput = node.querySelector(
"fieldset .field-number input[type=number]"
);
expect(strInput.value).eql("foo");
expect(numInput.value).eql("42");
});
it("should handle change events", () => {
const { comp, node } = createFormComponent({ schema });
const strInput = node.querySelector(
"fieldset .field-string input[type=text]"
);
const numInput = node.querySelector(
"fieldset .field-number input[type=number]"
);
Simulate.change(strInput, { target: { value: "bar" } });
Simulate.change(numInput, { target: { value: "101" } });
expect(comp.state.formData).eql(["bar", 101]);
});
it("should generate additional fields and fill data", () => {
const { node } = createFormComponent({
schema: schemaAdditional,
formData: [1, 2, "bar"],
});
const addInput = node.querySelector(
"fieldset .field-string input[type=text]"
);
expect(addInput.id).eql("root_2");
expect(addInput.value).eql("bar");
});
it("should apply uiSchema to additionalItems", () => {
const { node } = createFormComponent({
schema: schemaAdditional,
uiSchema: {
additionalItems: {
"ui:title": "Custom title",
},
},
formData: [1, 2, "bar"],
});
const label = node.querySelector(
"fieldset .field-string label.control-label"
);
expect(label.textContent).eql("Custom title*");
});
it("should have an add button if additionalItems is an object", () => {
const { node } = createFormComponent({ schema: schemaAdditional });
expect(node.querySelector(".array-item-add button")).not.to.be.null;
});
it("should not have an add button if additionalItems is not set", () => {
const { node } = createFormComponent({ schema });
expect(node.querySelector(".array-item-add button")).to.be.null;
});
it("should not have an add button if addable is false", () => {
const { node } = createFormComponent({
schema,
uiSchema: { "ui:options": { addable: false } },
});
expect(node.querySelector(".array-item-add button")).to.be.null;
});
it("[fixed-noadditional] should not provide an add button regardless maxItems", () => {
const { node } = createFormComponent({
schema: { maxItems: 3, ...schema },
});
expect(node.querySelector(".array-item-add button")).to.be.null;
});
it("[fixed] should not provide an add button if length equals maxItems", () => {
const { node } = createFormComponent({
schema: { maxItems: 2, ...schemaAdditional },
});
expect(node.querySelector(".array-item-add button")).to.be.null;
});
it("[fixed] should provide an add button if length is lesser than maxItems", () => {
const { node } = createFormComponent({
schema: { maxItems: 3, ...schemaAdditional },
});
expect(node.querySelector(".array-item-add button")).not.to.be.null;
});
it("[fixed] should not provide an add button if addable is expliclty false regardless maxItems value", () => {
const { node } = createFormComponent({
schema: { maxItems: 2, ...schema },
uiSchema: {
"ui:options": {
addable: false,
},
},
});
expect(node.querySelector(".array-item-add button")).to.be.null;
});
it("[fixed] should ignore addable value if maxItems constraint is not satisfied", () => {
const { node } = createFormComponent({
schema: { maxItems: 2, ...schema },
uiSchema: {
"ui:options": {
addable: true,
},
},
});
expect(node.querySelector(".array-item-add button")).to.be.null;
});
describe("operations for additional items", () => {
const { comp, node } = createFormComponent({
schema: schemaAdditional,
formData: [1, 2, "foo"],
ArrayFieldTemplate: ExposedArrayKeyTemplate,
});
const startRows = node.querySelectorAll(".array-item");
const startRow1_key = startRows[0].getAttribute(ArrayKeyDataAttr);
const startRow2_key = startRows[1].getAttribute(ArrayKeyDataAttr);
const startRow3_key = startRows[2].getAttribute(ArrayKeyDataAttr);
const startRow4_key = startRows[3]
? startRows[3].getAttribute(ArrayKeyDataAttr)
: undefined;
it("should add a field when clicking add button", () => {
const addBtn = node.querySelector(".array-item-add button");
Simulate.click(addBtn);
expect(node.querySelectorAll(".field-string")).to.have.length.of(2);
expect(comp.state.formData).eql([1, 2, "foo", undefined]);
});
it("should retain existing row keys/ids when adding additional items", () => {
const endRows = node.querySelectorAll(".array-item");
const endRow1_key = endRows[0].getAttribute(ArrayKeyDataAttr);
const endRow2_key = endRows[1].getAttribute(ArrayKeyDataAttr);
const endRow3_key = endRows[2].getAttribute(ArrayKeyDataAttr);
const endRow4_key = endRows[3].getAttribute(ArrayKeyDataAttr);
expect(startRow1_key).to.equal(endRow1_key);
expect(startRow2_key).to.equal(endRow2_key);
expect(startRow3_key).to.equal(endRow3_key);
expect(startRow4_key).to.not.equal(endRow4_key);
expect(startRow4_key).to.be.undefined;
expect(endRows[0].hasAttribute(ArrayKeyDataAttr)).to.be.true;
expect(endRows[1].hasAttribute(ArrayKeyDataAttr)).to.be.true;
expect(endRows[2].hasAttribute(ArrayKeyDataAttr)).to.be.true;
expect(endRows[3].hasAttribute(ArrayKeyDataAttr)).to.be.true;
});
it("should change the state when changing input value", () => {
const inputs = node.querySelectorAll(".field-string input[type=text]");
Simulate.change(inputs[0], { target: { value: "bar" } });
Simulate.change(inputs[1], { target: { value: "baz" } });
expect(comp.state.formData).eql([1, 2, "bar", "baz"]);
});
it("should remove array items when clicking remove buttons", () => {
let dropBtns = node.querySelectorAll(".array-item-remove");
Simulate.click(dropBtns[0]);
expect(node.querySelectorAll(".field-string")).to.have.length.of(1);
expect(comp.state.formData).eql([1, 2, "baz"]);
dropBtns = node.querySelectorAll(".array-item-remove");
Simulate.click(dropBtns[0]);
expect(node.querySelectorAll(".field-string")).to.be.empty;
expect(comp.state.formData).eql([1, 2]);
});
});
});
describe("Multiple number choices list", () => {
const schema = {
type: "array",
title: "My field",
items: {
enum: [1, 2, 3],
type: "integer",
},
uniqueItems: true,
};
it("should convert array of strings to numbers if type of items is 'number'", () => {
const { comp, node } = createFormComponent({ schema });
Simulate.change(node.querySelector(".field select"), {
target: {
options: [
{ selected: true, value: "1" },
{ selected: true, value: "2" },
{ selected: false, value: "3" },
],
},
});
expect(comp.state.formData).eql([1, 2]);
});
});
describe("Title", () => {
const TitleField = props => <div id={`title-${props.title}`} />;
const fields = { TitleField };
it("should pass field name to TitleField if there is no title", () => {
const schema = {
type: "object",
properties: {
array: {
type: "array",
items: {},
},
},
};
const { node } = createFormComponent({ schema, fields });
expect(node.querySelector("#title-array")).to.not.be.null;
});
it("should pass schema title to TitleField", () => {
const schema = {
type: "array",
title: "test",
items: {},
};
const { node } = createFormComponent({ schema, fields });
expect(node.querySelector("#title-test")).to.not.be.null;
});
it("should pass empty schema title to TitleField", () => {
const schema = {
type: "array",
title: "",
items: {},
};
const { node } = createFormComponent({ schema, fields });
expect(node.querySelector("#title-")).to.be.null;
});
});
});