Skip to content

Commit 3cea27c

Browse files
test: Add missing unit tests
1 parent b8149c1 commit 3cea27c

8 files changed

Lines changed: 487 additions & 0 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { beforeEach, describe, expect, it, vi } from "@test/unit";
2+
import { mount } from "@vue/test-utils";
3+
import ModelListField from "./ModelListField.vue";
4+
5+
const events = { emit: vi.fn(), off: vi.fn(), on: vi.fn() };
6+
const api = { get: vi.fn() };
7+
const panel = { error: vi.fn() };
8+
9+
const initial = {
10+
columns: {},
11+
models: [],
12+
pagination: { page: 1, total: 0 }
13+
};
14+
15+
function factory() {
16+
return mount(ModelListField, {
17+
props: {
18+
endpoints: { field: "pages/test/fields/drafts" },
19+
initial,
20+
name: "drafts"
21+
},
22+
shallow: true,
23+
global: {
24+
mocks: {
25+
$api: api,
26+
$events: events,
27+
$panel: panel
28+
}
29+
}
30+
});
31+
}
32+
33+
/**
34+
* The arguments of the last emitted event. The instance
35+
* is compared by identity, as enumerating its keys warns.
36+
*/
37+
function lastEmit() {
38+
return events.emit.mock.calls.at(-1) ?? [];
39+
}
40+
41+
describe("ModelListField.vue", () => {
42+
beforeEach(() => {
43+
api.get.mockReset();
44+
events.emit.mockClear();
45+
panel.error.mockClear();
46+
});
47+
48+
it("announces itself once it is mounted", () => {
49+
const wrapper = factory();
50+
51+
expect(events.emit).toHaveBeenCalledTimes(1);
52+
expect(lastEmit()[0]).toBe("field.loaded");
53+
expect(lastEmit()[1]).toBe(wrapper.vm);
54+
});
55+
56+
it("listens to model updates while mounted", () => {
57+
const wrapper = factory();
58+
59+
expect(events.on).toHaveBeenCalledWith("model.update", expect.anything());
60+
61+
wrapper.unmount();
62+
63+
expect(events.off).toHaveBeenCalledWith("model.update", expect.anything());
64+
});
65+
66+
it("announces itself again after a reload", async () => {
67+
const state = { ...initial, pagination: { page: 2, total: 25 } };
68+
api.get.mockResolvedValue(state);
69+
70+
const wrapper = factory();
71+
await wrapper.vm.reload({ page: 2 });
72+
73+
expect(api.get).toHaveBeenCalledWith("pages/test/fields/drafts", {
74+
page: 2,
75+
searchterm: null
76+
});
77+
78+
// the fresh state replaces the initial one
79+
expect(wrapper.vm.state).toStrictEqual(state);
80+
81+
// once on mount, once after the reload
82+
expect(events.emit).toHaveBeenCalledTimes(2);
83+
expect(lastEmit()[0]).toBe("field.loaded");
84+
expect(lastEmit()[1]).toBe(wrapper.vm);
85+
});
86+
87+
it("announces itself even when the reload fails", async () => {
88+
const error = new Error("Nope");
89+
api.get.mockRejectedValue(error);
90+
91+
const wrapper = factory();
92+
await wrapper.vm.reload();
93+
94+
expect(panel.error).toHaveBeenCalledWith(error);
95+
expect(wrapper.vm.isProcessing).toBe(false);
96+
expect(events.emit).toHaveBeenCalledTimes(2);
97+
expect(lastEmit()[0]).toBe("field.loaded");
98+
expect(lastEmit()[1]).toBe(wrapper.vm);
99+
});
100+
});

panel/src/components/Forms/ModelForm.test.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, expect, it } from "vitest";
2+
import { mount } from "@vue/test-utils";
23
import ModelForm from "./ModelForm.vue";
34

45
const { disabled, isEmpty, resolvedColumns } = ModelForm.computed;
@@ -20,6 +21,59 @@ function context(props = {}) {
2021
return ctx;
2122
}
2223

24+
describe("ModelForm.vue", () => {
25+
const columns = {
26+
0: { width: "1/2", fields: { headline: { type: "text" } } },
27+
1: { width: "1/2", sticky: true, fields: { text: { type: "textarea" } } }
28+
};
29+
30+
it("renders a column for each column of the tab", () => {
31+
const wrapper = mount(ModelForm, {
32+
props: { api: "pages/test", columns, content: { headline: "Test" } }
33+
});
34+
35+
const rendered = wrapper.findAll("k-column");
36+
37+
expect(wrapper.find("form.k-model-form").exists()).toBe(true);
38+
expect(rendered.length).toBe(2);
39+
expect(rendered[0].attributes("width")).toBe("1/2");
40+
expect(rendered[1].attributes("sticky")).toBe("true");
41+
expect(wrapper.findAll("k-fieldset").length).toBe(2);
42+
});
43+
44+
it("renders the empty state instead of the form", () => {
45+
const wrapper = mount(ModelForm, {
46+
props: { columns: {}, empty: "No blueprint" }
47+
});
48+
49+
expect(wrapper.find("k-box").exists()).toBe(true);
50+
expect(wrapper.find("form").exists()).toBe(false);
51+
});
52+
53+
it("passes on the input of a fieldset", async () => {
54+
const wrapper = mount(ModelForm, {
55+
props: { api: "pages/test", columns }
56+
});
57+
58+
await wrapper.find("k-fieldset").trigger("input");
59+
60+
expect(wrapper.emitted("input")).toHaveLength(1);
61+
});
62+
63+
it("submits the form and the fieldsets", async () => {
64+
const wrapper = mount(ModelForm, {
65+
props: { api: "pages/test", columns }
66+
});
67+
68+
await wrapper.find("form").trigger("submit");
69+
expect(wrapper.emitted("submit")).toHaveLength(1);
70+
71+
// the submit of a fieldset bubbles up to the form as well
72+
await wrapper.find("k-fieldset").trigger("submit");
73+
expect(wrapper.emitted("submit")).toHaveLength(3);
74+
});
75+
});
76+
2377
describe("ModelForm.fieldsWithAdditionalData()", () => {
2478
it("points regular fields at the field endpoint", () => {
2579
const ctx = context();
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, expect, it } from "@test/unit";
2+
import { mount } from "@vue/test-utils";
3+
import ModelTabs from "./ModelTabs.vue";
4+
5+
const tabs = [
6+
{
7+
name: "main",
8+
columns: [
9+
{ fields: { headline: { type: "text" } } },
10+
{ fields: { Text: { type: "textarea" } } }
11+
]
12+
},
13+
{
14+
name: "meta",
15+
columns: [{ fields: { seo: { type: "text" } } }]
16+
}
17+
];
18+
19+
type Badge = { text: number } | undefined;
20+
21+
function badges(diff = {}): Badge[] {
22+
const wrapper = mount(ModelTabs, { props: { tab: "main", tabs, diff } });
23+
const withBadges = wrapper.vm.withBadges as { badge: Badge }[];
24+
return withBadges.map((tab) => tab.badge);
25+
}
26+
27+
describe("ModelTabs.vue", () => {
28+
describe("element", () => {
29+
it.rendersAs(() => mount(ModelTabs).find("k-tabs"), "K-TABS");
30+
});
31+
32+
describe("withBadges", () => {
33+
it("counts the changed fields of all columns of a tab", () => {
34+
expect(badges({ headline: "a", seo: "b" })).toStrictEqual([
35+
{ text: 1 },
36+
{ text: 1 }
37+
]);
38+
});
39+
40+
it("compares the field names case-insensitively", () => {
41+
expect(badges({ text: "a" })).toStrictEqual([{ text: 1 }, undefined]);
42+
});
43+
44+
it("has no badge without changes", () => {
45+
expect(badges()).toStrictEqual([undefined, undefined]);
46+
});
47+
});
48+
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { beforeEach, describe, expect, it, vi } from "@test/unit";
2+
import { mount } from "@vue/test-utils";
3+
import PreviewForm from "./PreviewForm.vue";
4+
5+
const events = { on: vi.fn(), off: vi.fn() };
6+
7+
function factory() {
8+
return mount(PreviewForm, {
9+
props: {
10+
api: "pages/test",
11+
blueprint: "default",
12+
content: {},
13+
diff: {},
14+
tab: { name: "main", columns: {} },
15+
tabs: [{ name: "main" }, { name: "meta" }]
16+
},
17+
global: {
18+
mocks: {
19+
$events: events,
20+
$panel: {
21+
config: { debug: false },
22+
view: { path: "/pages/test" }
23+
}
24+
}
25+
}
26+
});
27+
}
28+
29+
type Link = HTMLElement & {
30+
__vue__: { to: string; onClick?: (event: unknown) => void };
31+
};
32+
33+
/**
34+
* Creates a fake field with a single link in its element
35+
*/
36+
function field(to: string) {
37+
const el = document.createElement("div");
38+
el.innerHTML = `<p class="k-item-title"><a class="k-link"></a></p>`;
39+
40+
const link = el.querySelector(".k-link") as Link;
41+
link.__vue__ = { to };
42+
43+
return { field: { $el: el }, link };
44+
}
45+
46+
describe("PreviewForm.vue", () => {
47+
beforeEach(() => {
48+
events.on.mockClear();
49+
events.off.mockClear();
50+
});
51+
52+
describe("events", () => {
53+
it("listens to loaded fields and sections while mounted", () => {
54+
const wrapper = factory();
55+
56+
expect(events.on).toHaveBeenCalledWith(
57+
"field.loaded",
58+
wrapper.vm.fixLinks
59+
);
60+
expect(events.on).toHaveBeenCalledWith(
61+
"section.loaded",
62+
wrapper.vm.fixLinks
63+
);
64+
65+
const fixLinks = wrapper.vm.fixLinks;
66+
wrapper.unmount();
67+
68+
expect(events.off).toHaveBeenCalledWith("field.loaded", fixLinks);
69+
expect(events.off).toHaveBeenCalledWith("section.loaded", fixLinks);
70+
});
71+
});
72+
73+
describe("form", () => {
74+
it("passes on the events of the form and its controls", async () => {
75+
const wrapper = factory();
76+
77+
await wrapper.find("k-model-form").trigger("input");
78+
await wrapper.find("k-model-form").trigger("submit");
79+
await wrapper.find("k-form-controls").trigger("discard");
80+
81+
expect(wrapper.emitted("input")).toHaveLength(1);
82+
expect(wrapper.emitted("submit")).toHaveLength(1);
83+
expect(wrapper.emitted("discard")).toHaveLength(1);
84+
});
85+
});
86+
87+
describe("fixLinks", () => {
88+
it("opens page links in the preview view", () => {
89+
const wrapper = factory();
90+
const { field: loaded, link } = field("/pages/test");
91+
92+
wrapper.vm.fixLinks(loaded);
93+
94+
const event = { preventDefault: vi.fn() };
95+
link.__vue__.onClick?.(event);
96+
97+
expect(event.preventDefault).toHaveBeenCalled();
98+
expect(wrapper.emitted("navigate")?.[0]).toStrictEqual([
99+
"/pages/test/preview/form"
100+
]);
101+
});
102+
103+
it("keeps all other links untouched", () => {
104+
const wrapper = factory();
105+
const { field: loaded, link } = field("/pages/test/files/test.jpg");
106+
107+
wrapper.vm.fixLinks(loaded);
108+
109+
expect(link.__vue__.onClick).toBeUndefined();
110+
});
111+
});
112+
});

tests/Blueprint/AcceptRulesTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,38 @@ public function testFileTemplatesFromFields(): void
107107
$this->assertSame(['a', 'b', 'c', 'd', 'e'], $rules->fileTemplates());
108108
}
109109

110+
public function testFileTemplatesFromFieldsWithUnknownType(): void
111+
{
112+
$blueprint = new Blueprint([
113+
'model' => $this->model,
114+
'name' => 'default',
115+
'fields' => [
116+
'blocks' => [
117+
'type' => 'blocks',
118+
'fieldsets' => [
119+
'text' => [
120+
'fields' => [
121+
// fieldsets are not normalized, so the field
122+
// type can be anything at this point
123+
'text' => [
124+
'type' => 'does-not-exist',
125+
'uploads' => [
126+
'template' => 'a'
127+
]
128+
]
129+
]
130+
]
131+
]
132+
]
133+
]
134+
]);
135+
136+
$rules = new AcceptRules($blueprint);
137+
138+
// a field type that cannot be resolved does not support uploads
139+
$this->assertSame([], $rules->fileTemplates());
140+
}
141+
110142
public function testFileTemplatesFromFieldsWithDifferentParent(): void
111143
{
112144
$this->app = $this->app->clone([

0 commit comments

Comments
 (0)