@oruga-ui/oruga-next
Version:
UI components for Vue.js and CSS framework agnostic
361 lines (332 loc) • 12.9 kB
text/typescript
/* eslint-disable vue/one-component-per-file */
import { describe, expect, test } from "vitest";
import { userEvent, type Locator, type LocatorSelectors } from "vitest/browser";
import { render } from "vitest-browser-vue";
import {
defineComponent,
h,
ref,
useTemplateRef,
type PropType,
type VNode,
} from "vue";
import type { ComponentExposed } from "vue-component-type-helpers";
import OButton from "@/components/button/Button.vue";
import OInput from "@/components/input/Input.vue";
import OField from "@/components/field/Field.vue";
type CustomValidityCallback = (
currentValue: string | number | null | undefined,
state: ValidityState,
) => string;
const NativeForm = defineComponent({
name: "NativeForm",
props: {
customValidity: {
type: [String, Function] as PropType<
string | CustomValidityCallback
>,
default: undefined,
},
disabled: {
type: Boolean,
default: false,
},
required: {
type: Boolean,
default: true,
},
},
setup: (props) => {
const value = ref("");
const onInput = (e: string): void => {
value.value = e;
};
return (): VNode =>
h("form", { onSubmit: () => false }, [
h(OField, { "data-testid": "field" }, () => [
h(OInput, {
customValidity: props.customValidity,
disabled: props.disabled,
modelValue: value.value,
required: props.required,
onInput,
}),
]),
h(OButton, { type: "submit" }, () => "Submit"),
]);
},
});
const findMessage = (screen: LocatorSelectors): Locator => {
return screen.getByTestId("field").getByRole("paragraph");
};
const inside = (
containerStart: number,
containerEnd: number,
elementStart: number,
elementEnd: number,
): boolean => {
return (
(elementStart >= containerStart && elementStart < containerEnd) ||
(elementEnd > containerStart && elementEnd <= containerEnd) ||
(elementStart <= containerStart && elementEnd >= containerEnd)
);
};
const visibleWithin = (parent: Element, child: Element): boolean => {
const parentBB = parent.getBoundingClientRect();
const childBB = child.getBoundingClientRect();
return (
inside(parentBB.left, parentBB.right, childBB.left, childBB.right) &&
inside(parentBB.top, parentBB.bottom, childBB.top, childBB.bottom)
);
};
describe("useInputHandler", () => {
test("shows validation message on blur", async () => {
const screen = render(NativeForm);
const input = screen.getByRole("textbox");
const message = findMessage(screen);
await expect.element(message).not.toBeInTheDocument();
await input.click();
await userEvent.keyboard("{Tab}");
await expect.element(message).toBeInTheDocument();
});
test("shows validation message on submit", async () => {
const Spacer = (): VNode => h("div", { style: "height: 40em" });
const ScrollingForm = (): VNode =>
h(
"form",
{
"data-testid": "form",
style: "max-height: 4em; overflow-y: scroll;",
onSubmit: () => false,
},
[
h(OField, { "data-testid": "disabled-field" }, () => [
h(OInput, { disabled: true, required: true }),
]),
h(Spacer),
h(OField, { "data-testid": "field" }, () => [
h(OInput, { required: true }),
]),
h(Spacer),
h(OButton, { type: "submit" }, () => "Submit"),
],
);
const screen = render(ScrollingForm);
const message = findMessage(screen);
await expect.element(message).not.toBeInTheDocument();
const submit = screen.getByRole("button");
await submit.click();
await expect.element(message).toBeInTheDocument();
// Test that we focused and scrolled to the right element.
const form = screen.getByTestId("form").element();
const mainInput = screen.getByTestId("field").getByRole("textbox");
await expect.element(mainInput).toHaveFocus();
await expect
.poll(() => visibleWithin(form, message.element()))
.toBeTruthy();
const disabledInput = screen
.getByTestId("disabled-field")
.getByRole("textbox");
await expect
.poll(() => visibleWithin(form, disabledInput.element()))
.toBeFalsy();
});
test("scrolls to invalid inputs without fields", async () => {
const Spacer = (): VNode => h("div", { style: "height: 40em" });
const ScrollingForm = (): VNode =>
h(
"form",
{
"data-testid": "form",
style: "max-height: 4em; overflow-y: scroll;",
onSubmit: () => false,
},
[
h(OInput, { required: true }),
h(Spacer),
h(OButton, { type: "submit" }, () => "Submit"),
],
);
const screen = render(ScrollingForm);
const input = screen.getByRole("textbox");
const submit = screen.getByRole("button");
submit.element().scrollIntoView();
const form = screen.getByTestId("form").element();
await expect
.poll(() => visibleWithin(form, input.element()))
.toBeFalsy();
await submit.click();
await expect.element(input).toHaveFocus();
await expect
.poll(() => visibleWithin(form, input.element()))
.toBeTruthy();
});
test("shows validation message when explicitly triggered", async () => {
const TriggerField = defineComponent({
name: "TriggerField",
setup: () => {
const input =
useTemplateRef<ComponentExposed<typeof OInput>>("my-input");
return (): VNode[] => [
h(OField, { "data-testid": "field" }, () =>
h(OInput, { ref: "my-input", required: true }),
),
h(
OButton,
{
type: "button",
onClick: () => input.value?.checkHtml5Validity(),
},
() => "Trigger Validation",
),
];
},
});
const screen = render(TriggerField);
const button = screen.getByRole("button");
const message = findMessage(screen);
await expect.element(message).not.toBeInTheDocument();
await button.click();
await expect.element(message).toBeInTheDocument();
});
test("hides validation message on input change", async () => {
const screen = render(NativeForm);
const input = screen.getByRole("textbox");
await input.click();
await userEvent.keyboard("{Tab}");
const message = findMessage(screen);
await expect.element(message).toBeInTheDocument();
await input.click();
await userEvent.keyboard("test");
await expect.element(message).not.toBeInTheDocument();
});
test("hides validation message when input is disabled", async () => {
const screen = render(NativeForm);
const input = screen.getByRole("textbox");
await input.click();
await userEvent.keyboard("{Tab}");
const message = findMessage(screen);
await expect.element(message).toBeInTheDocument();
screen.rerender({ disabled: true });
await expect.element(message).not.toBeInTheDocument();
});
test("hides validation message when parent fieldset is disabled", async () => {
const FieldsetForm = defineComponent({
name: "FieldsetForm",
props: {
disabled: {
type: Boolean,
default: false,
},
},
setup: (props) => {
return (): VNode =>
h("form", { onSubmit: () => false }, [
h(
"fieldset",
{ disabled: props.disabled },
h(OField, { "data-testid": "field" }, () =>
h(OInput, { required: true }),
),
),
h(OButton, { "native-type": "submit" }, () => "Submit"),
]);
},
});
const screen = render(FieldsetForm);
const input = screen.getByRole("textbox");
await input.click();
await userEvent.keyboard("{Tab}");
const message = findMessage(screen);
await expect.element(message).toBeInTheDocument();
screen.rerender({ disabled: true });
await expect.element(message).not.toBeInTheDocument();
});
test("hides validation message when validation attribute is removed", async () => {
const screen = render(NativeForm);
const input = screen.getByRole("textbox");
await input.click();
await userEvent.keyboard("{Tab}");
const message = findMessage(screen);
await expect.element(message).toBeInTheDocument();
screen.rerender({ required: false });
await expect.element(message).not.toBeInTheDocument();
});
test("hides validation message when validation attribute is changed", async () => {
const NumberForm = defineComponent({
name: "NumberForm",
props: {
max: {
type: Number,
default: 1,
},
},
setup: (props) => {
return (): VNode =>
h(OField, { "data-testid": "field" }, () =>
h(OInput, {
type: "number",
max: props.max,
modelValue: 2,
}),
);
},
});
const screen = render(NumberForm);
const input = screen.getByRole("spinbutton");
await input.click();
await userEvent.keyboard("{Tab}");
const message = findMessage(screen);
await expect.element(message).toBeInTheDocument();
screen.rerender({ max: 2 });
await expect.element(message).not.toBeInTheDocument();
});
test("does not overwrite parent component's message", async () => {
const FormWithParentMessage = defineComponent({
name: "FormWithParentMessage",
render: () =>
h(
OField,
{
"data-testid": "field",
message: "Override message",
},
() => [h(OInput, { required: true })],
),
});
const screen = render(FormWithParentMessage);
const input = screen.getByRole("textbox");
await input.click();
await userEvent.keyboard("{Tab}");
const message = findMessage(screen);
await expect.element(message).toHaveTextContent("Override message");
});
test("sets custom message", async () => {
const screen = render(() =>
h(NativeForm, {
customValidity: "Override message",
}),
);
const input = screen.getByRole("textbox");
await input.click();
await userEvent.keyboard("{Tab}");
const message = findMessage(screen);
await expect.element(message).toHaveTextContent("Override message");
});
test("overrides native validation message", async () => {
const screen = render(() =>
h(NativeForm, {
customValidity: (_, validity) =>
validity.valueMissing ? "Override message" : "",
}),
);
const input = screen.getByRole("textbox");
await input.click();
await userEvent.keyboard("{Tab}");
const message = findMessage(screen);
await expect.element(message).toHaveTextContent("Override message");
await input.click();
await userEvent.keyboard("test");
await expect.element(message).not.toBeInTheDocument();
});
});