@shopware-ag/meteor-component-library
Version:
The meteor component library is a Vue component library developed by Shopware. It is based on the [Meteor Design System](https://shopware.design/).
440 lines (367 loc) • 14.4 kB
text/typescript
import MtModal from "./mt-modal.vue";
import MtModalRoot from "./sub-components/mt-modal-root.vue";
import MtModalTrigger from "./sub-components/mt-modal-trigger.vue";
import MtModalAction from "./sub-components/mt-modal-action.vue";
import { render, screen, fireEvent, waitFor } from "@testing-library/vue";
import { ref } from "vue";
describe("mt-modal", () => {
it("the modal is hidden by default", () => {
// GIVEN
render({
components: { MtModal, MtModalRoot, MtModalTrigger },
template: `
<mt-modal-root>
<mt-modal title='title'>mt-modal works!</mt-modal>
</mt-modal-root>`,
});
// WHEN
const modal = screen.queryByRole("dialog");
// THEN
expect(modal).not.toBeInTheDocument();
});
it("the modal is visible by default", () => {
// GIVEN
render({
components: { MtModal, MtModalRoot },
template: `
<mt-modal-root isOpen>
<mt-modal title='title'>mt-modal works!</mt-modal>
</mt-modal-root>`,
});
// WHEN
const modal = screen.queryByRole("dialog");
// THEN
expect(modal).toBeInTheDocument();
});
it("opens the modal when clicking the trigger", async () => {
// GIVEN
const onChange = vi.fn();
render({
components: { MtModal, MtModalRoot, MtModalTrigger },
setup() {
return { onChange };
},
template: `
<mt-modal-root ="onChange">
<mt-modal-trigger as='button'>Open modal</mt-modal-trigger>
<mt-modal title='title'>mt-modal works!</mt-modal>
</mt-modal-root>`,
});
// WHEN
await fireEvent.click(screen.getByRole("button"));
// THEN
const modal = screen.queryByRole("dialog");
expect(modal).toBeInTheDocument();
expect(onChange).toHaveBeenNthCalledWith(1, true);
});
it("opens the modal when clicking the trigger when 'isOpen' gets changed", async () => {
// GIVEN
const onChange = vi.fn();
render({
components: { MtModal, MtModalRoot, MtModalTrigger },
setup() {
const isOpen = ref(false);
return { onChange, isOpen };
},
template: `
<button ="isOpen = !isOpen">Toggle modal</button>
<mt-modal-root ="onChange" :isOpen="isOpen">
<mt-modal title='title'>mt-modal works!</mt-modal>
</mt-modal-root>`,
});
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
// WHEN
await fireEvent.click(screen.getByRole("button"));
// THEN
const modal = screen.queryByRole("dialog");
expect(modal).toBeInTheDocument();
expect(onChange).toHaveBeenNthCalledWith(1, true);
});
it("closes the modal", async () => {
const onChange = vi.fn();
// GIVEN
render({
components: { MtModal, MtModalRoot, MtModalTrigger },
setup() {
return { onChange };
},
template: `
<mt-modal-root ='onChange'>
<mt-modal-trigger as='button'>Open modal</mt-modal-trigger>
<mt-modal title='title'>mt-modal works!</mt-modal>
</mt-modal-root>`,
});
await fireEvent.click(screen.getByRole("button", { name: "Open modal" }));
// WHEN
await fireEvent.click(screen.getByRole("button", { name: "Close" }));
// THEN
const modal = screen.queryByRole("dialog");
expect(modal).not.toBeInTheDocument();
expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange).toHaveBeenLastCalledWith(false);
});
it("closes the modal through an action button", async () => {
// GIVEN
render({
components: { MtModal, MtModalRoot, MtModalTrigger, MtModalAction },
template: `
<mt-modal-root>
<mt-modal-trigger as="button">Open modal</mt-modal-trigger>
<mt-modal title='title'>
<template #default>mt-modal works!</template>
<template #footer>
<mt-modal-action as="button" ="done => done()">Confirm</mt-modal-action>
</template>
</mt-modal>
</mt-modal-root>`,
});
await fireEvent.click(screen.getByRole("button", { name: "Open modal" }));
// WHEN
await fireEvent.click(screen.getByRole("button", { name: "Confirm" }));
// THEN
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
it("closes the modal when pressing escape", async () => {
// GIVEN
render({
components: { MtModal, MtModalRoot, MtModalTrigger },
template: `
<mt-modal-root>
<mt-modal-trigger as='button'>Open modal</mt-modal-trigger>
<mt-modal title='title'>mt-modal works!</mt-modal>
</mt-modal-root>`,
});
await fireEvent.click(screen.getByRole("button", { name: "Open modal" }));
// WHEN
await fireEvent.keyDown(document, { key: "Escape" });
// THEN
const modal = screen.queryByRole("dialog");
expect(modal).not.toBeInTheDocument();
});
it("closes the modal when clicking on the backdrop", async () => {
// GIVEN
render({
components: { MtModal, MtModalRoot, MtModalTrigger },
template: `
<mt-modal-root>
<mt-modal-trigger as='button'>Open modal</mt-modal-trigger>
<mt-modal title='title'>mt-modal works!</mt-modal>
</mt-modal-root>`,
});
await fireEvent.click(screen.getByRole("button", { name: "Open modal" }));
// WHEN
await fireEvent.click(screen.getByTestId("modal-backdrop"));
// THEN
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
describe("modal stacking", () => {
const STACKING_WARNING =
"[MtModal] It is not recommended to stack multiple modals on top of each other.";
/**
* Returns the order of backdrops and modals in the body as 'backdrop' | 'modal'.
* Expected visual hierarchy: each backdrop is followed by its corresponding modal.
*/
function getBodyStackOrder(): ("backdrop" | "modal")[] {
const elements = document.body.querySelectorAll(".mt-modal-root__backdrop, .mt-modal");
return Array.from(elements).map((el) =>
el.classList.contains("mt-modal") ? "modal" : "backdrop",
);
}
it("does not show stacking warning when only one modal is open", async () => {
// GIVEN a single modal root
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
render({
components: { MtModal, MtModalRoot, MtModalTrigger },
template: `
<mt-modal-root>
<mt-modal-trigger as="button">Open</mt-modal-trigger>
<mt-modal title="Single">Content</mt-modal>
</mt-modal-root>
`,
});
// WHEN opening the modal
await fireEvent.click(screen.getByRole("button", { name: "Open" }));
await waitFor(() => {
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
// THEN no stacking warning is shown
expect(warnSpy).not.toHaveBeenCalledWith(STACKING_WARNING);
warnSpy.mockRestore();
});
it("shows stacking warning when opening a second modal", async () => {
// GIVEN two modal roots
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
render({
components: { MtModal, MtModalRoot, MtModalTrigger },
setup() {
const open1 = ref(false);
const open2 = ref(false);
return { open1, open2 };
},
template: `
<mt-modal-root :isOpen="open1" ="(v) => (open1 = v)">
<mt-modal-trigger as="button">Open 1</mt-modal-trigger>
<mt-modal title="Modal 1">Content 1</mt-modal>
</mt-modal-root>
<mt-modal-root :isOpen="open2" ="(v) => (open2 = v)">
<mt-modal-trigger as="button">Open 2</mt-modal-trigger>
<mt-modal title="Modal 2">Content 2</mt-modal>
</mt-modal-root>
`,
});
// WHEN opening the first modal
await fireEvent.click(screen.getByRole("button", { name: "Open 1" }));
await waitFor(() => {
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
expect(warnSpy).not.toHaveBeenCalledWith(STACKING_WARNING);
// WHEN opening the second modal
await fireEvent.click(screen.getByRole("button", { name: "Open 2" }));
await waitFor(() => {
expect(screen.getAllByRole("dialog")).toHaveLength(2);
});
// THEN stacking warning is shown
expect(warnSpy).toHaveBeenCalledWith(STACKING_WARNING);
warnSpy.mockRestore();
});
it("orders modal-root before mt-modal for each pair when opening two modals", async () => {
// GIVEN two modal roots with modals
render({
components: { MtModal, MtModalRoot, MtModalTrigger },
setup() {
const open1 = ref(false);
const open2 = ref(false);
return { open1, open2 };
},
template: `
<mt-modal-root :isOpen="open1" ="(v) => (open1 = v)">
<mt-modal-trigger as="button">Open 1</mt-modal-trigger>
<mt-modal title="Modal 1">Content 1</mt-modal>
</mt-modal-root>
<mt-modal-root :isOpen="open2" ="(v) => (open2 = v)">
<mt-modal-trigger as="button">Open 2</mt-modal-trigger>
<mt-modal title="Modal 2">Content 2</mt-modal>
</mt-modal-root>
`,
});
// WHEN opening first modal, then second
await fireEvent.click(screen.getByRole("button", { name: "Open 1" }));
await fireEvent.click(screen.getByRole("button", { name: "Open 2" }));
await waitFor(() => {
expect(screen.getAllByRole("dialog")).toHaveLength(2);
});
// THEN order in body is backdrop > modal for each pair (visual hierarchy)
expect(getBodyStackOrder()).toEqual(["backdrop", "modal", "backdrop", "modal"]);
});
it("orders modal-root before mt-modal for each pair when opening three modals", async () => {
// GIVEN three modal roots with modals
render({
components: { MtModal, MtModalRoot, MtModalTrigger },
setup() {
const open1 = ref(false);
const open2 = ref(false);
const open3 = ref(false);
return { open1, open2, open3 };
},
template: `
<mt-modal-root :isOpen="open1" ="(v) => (open1 = v)">
<mt-modal-trigger as="button">Open 1</mt-modal-trigger>
<mt-modal title="Modal 1">Content 1</mt-modal>
</mt-modal-root>
<mt-modal-root :isOpen="open2" ="(v) => (open2 = v)">
<mt-modal-trigger as="button">Open 2</mt-modal-trigger>
<mt-modal title="Modal 2">Content 2</mt-modal>
</mt-modal-root>
<mt-modal-root :isOpen="open3" ="(v) => (open3 = v)">
<mt-modal-trigger as="button">Open 3</mt-modal-trigger>
<mt-modal title="Modal 3">Content 3</mt-modal>
</mt-modal-root>
`,
});
// WHEN opening all three in sequence
await fireEvent.click(screen.getByRole("button", { name: "Open 1" }));
await fireEvent.click(screen.getByRole("button", { name: "Open 2" }));
await fireEvent.click(screen.getByRole("button", { name: "Open 3" }));
await waitFor(() => {
expect(screen.getAllByRole("dialog")).toHaveLength(3);
});
// THEN order in body is backdrop > modal for each pair
expect(getBodyStackOrder()).toEqual([
"backdrop",
"modal",
"backdrop",
"modal",
"backdrop",
"modal",
]);
});
it("keeps root-before-modal order after closing a modal and opening it again", async () => {
// GIVEN two modal roots with modals
render({
components: { MtModal, MtModalRoot, MtModalTrigger },
setup() {
const open1 = ref(false);
const open2 = ref(false);
return { open1, open2 };
},
template: `
<mt-modal-root :isOpen="open1" ="(v) => (open1 = v)">
<mt-modal-trigger as="button">Open 1</mt-modal-trigger>
<mt-modal title="Modal 1">Content 1</mt-modal>
</mt-modal-root>
<mt-modal-root :isOpen="open2" ="(v) => (open2 = v)">
<mt-modal-trigger as="button">Open 2</mt-modal-trigger>
<mt-modal title="Modal 2">Content 2</mt-modal>
</mt-modal-root>
`,
});
// WHEN open first, then second, then close first, then open first again
await fireEvent.click(screen.getByRole("button", { name: "Open 1" }));
await fireEvent.click(screen.getByRole("button", { name: "Open 2" }));
await waitFor(() => {
expect(screen.getAllByRole("dialog")).toHaveLength(2);
});
expect(getBodyStackOrder()).toEqual(["backdrop", "modal", "backdrop", "modal"]);
const closeButtons = screen.getAllByRole("button", { name: "Close" });
await fireEvent.click(closeButtons[0]);
await waitFor(() => {
expect(screen.getAllByRole("dialog")).toHaveLength(1);
});
await fireEvent.click(screen.getByRole("button", { name: "Open 1" }));
await waitFor(() => {
expect(screen.getAllByRole("dialog")).toHaveLength(2);
});
// THEN order in body is still backdrop > modal for each pair
expect(getBodyStackOrder()).toEqual(["backdrop", "modal", "backdrop", "modal"]);
});
it("orders modal-root before mt-modal for nested modals", async () => {
// GIVEN a modal with a nested modal inside
render({
components: { MtModal, MtModalRoot, MtModalTrigger },
template: `
<mt-modal-root>
<mt-modal-trigger as="button">Open Outer</mt-modal-trigger>
<mt-modal title="Outer Modal">
<mt-modal-root>
<mt-modal-trigger as="button">Open Inner</mt-modal-trigger>
<mt-modal title="Inner Modal">Inner Content</mt-modal>
</mt-modal-root>
</mt-modal>
</mt-modal-root>
`,
});
// WHEN opening the outer modal
await fireEvent.click(screen.getByRole("button", { name: "Open Outer" }));
await waitFor(() => {
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
// WHEN opening the inner modal
await fireEvent.click(screen.getByRole("button", { name: "Open Inner" }));
await waitFor(() => {
expect(screen.getAllByRole("dialog")).toHaveLength(2);
});
// THEN order in body is backdrop > modal for each pair
expect(getBodyStackOrder()).toEqual(["backdrop", "modal", "backdrop", "modal"]);
});
});
});