@pantheon-systems/create-p1-starter-kit 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- package/template/__tests__/chatbot-flag-context.test.ts +55 -0
- package/template/__tests__/data-list-block-sub-components.test.tsx +246 -0
- package/template/__tests__/data-list-block-utils.test.ts +84 -0
- package/template/__tests__/data-list-block.test.ts +198 -0
- package/template/__tests__/published-page-routes.test.ts +86 -0
- package/template/app/[...puckPath]/page.tsx +10 -117
- package/template/app/p1/(editor)/[[...p1]]/editor-client.tsx +23 -73
- package/template/app/page.tsx +11 -69
- package/template/app/published-pages.tsx +23 -0
- package/template/app/welcome-block.tsx +25 -0
- package/template/components/ChatbotFlagProvider.tsx +3 -8
- package/template/components/p1-lockup.tsx +2 -1
- package/template/components/puck/image-block.tsx +5 -2
- package/template/components/puck/welcome-block-render.tsx +30 -37
- package/template/components/welcome-block.module.css +124 -0
- package/template/eslint.config.js +9 -1
- package/template/lib/chatbot-flag/flag-context.ts +32 -0
- package/template/package.json +3 -3
- package/template/vitest.config.ts +17 -20
- package/template/__tests__/published-page-404.test.ts +0 -65
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pantheon-systems/create-p1-starter-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Scaffold a new P1 starter project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,10 +37,11 @@
|
|
|
37
37
|
"eslint": "^9.27.0",
|
|
38
38
|
"isomorphic-dompurify": "^3.18.0",
|
|
39
39
|
"react": "^19.2.5",
|
|
40
|
+
"react-dom": "^19.2.5",
|
|
40
41
|
"vitest": "^4.1.5",
|
|
41
|
-
"@pantheon-systems/css-client": "0.
|
|
42
|
+
"@pantheon-systems/css-client": "0.14.0",
|
|
42
43
|
"@pantheon-systems/eslint-config": "0.1.0",
|
|
43
|
-
"@pantheon-systems/puck-css": "0.
|
|
44
|
+
"@pantheon-systems/puck-css": "0.14.0"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
47
|
"@clack/prompts": "^1.5.1",
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"clean": "rm -rf template node_modules/.vite",
|
|
54
55
|
"lint": "eslint index.js lib scripts",
|
|
55
56
|
"lint:fix": "eslint index.js lib scripts --fix",
|
|
57
|
+
"lint:template": "node scripts/lint-template.js",
|
|
56
58
|
"test": "vitest run",
|
|
57
59
|
"test:scaffold": "node scripts/validate-scaffold.js"
|
|
58
60
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { buildFlagContext } from "../lib/chatbot-flag/flag-context";
|
|
4
|
+
|
|
5
|
+
const user = (over: Partial<{ id: string; name: string; email: string }> = {}) => ({
|
|
6
|
+
id: "00000000-0000-4000-8000-000000000001",
|
|
7
|
+
name: "Test User",
|
|
8
|
+
...over,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("buildFlagContext", () => {
|
|
12
|
+
it("keys on the email so individual targets entered as emails match", () => {
|
|
13
|
+
expect(buildFlagContext(user({ email: "editor@example.com" })).key).toBe(
|
|
14
|
+
"editor@example.com",
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("lowercases the key, since target matching is case-sensitive", () => {
|
|
19
|
+
expect(buildFlagContext(user({ email: "Editor@Example.com" })).key).toBe(
|
|
20
|
+
"editor@example.com",
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("keeps the email as a targeting attribute so rules on it still work", () => {
|
|
25
|
+
expect(buildFlagContext(user({ email: "editor@example.com" })).email).toBe(
|
|
26
|
+
"editor@example.com",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("falls back to the user id when the user has no email", () => {
|
|
31
|
+
const context = buildFlagContext(user());
|
|
32
|
+
expect(context.key).toBe("00000000-0000-4000-8000-000000000001");
|
|
33
|
+
expect(context.anonymous).toBeUndefined();
|
|
34
|
+
expect(context.email).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// The id is opaque and matched verbatim, so normalizing it would move the target.
|
|
38
|
+
it("leaves the user id untouched when falling back to it", () => {
|
|
39
|
+
expect(buildFlagContext(user({ id: "00000000-0000-4000-8000-ABCDEF000002" })).key).toBe("00000000-0000-4000-8000-ABCDEF000002");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("treats a blank email as absent rather than keying on an empty string", () => {
|
|
43
|
+
expect(buildFlagContext(user({ email: " " })).key).toBe(
|
|
44
|
+
"00000000-0000-4000-8000-000000000001",
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("marks the pre-auth context anonymous", () => {
|
|
49
|
+
expect(buildFlagContext(null)).toEqual({
|
|
50
|
+
kind: "user",
|
|
51
|
+
key: "anonymous",
|
|
52
|
+
anonymous: true,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
4
|
+
import type { ResolvedItem, LayoutProps } from "@pantheon-systems/puck-css/fields";
|
|
5
|
+
|
|
6
|
+
const ITEMS: ResolvedItem[] = [
|
|
7
|
+
{
|
|
8
|
+
title: "Alpha",
|
|
9
|
+
subtitle: "First item",
|
|
10
|
+
teaser: "A brief preview of the first item",
|
|
11
|
+
image: "https://example.com/alpha.jpg",
|
|
12
|
+
icon: "star",
|
|
13
|
+
_raw: { name: "Alpha" },
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
title: "Beta",
|
|
17
|
+
subtitle: "Second item",
|
|
18
|
+
teaser: "A brief preview of the second item",
|
|
19
|
+
image: "https://example.com/beta.jpg",
|
|
20
|
+
icon: "circle",
|
|
21
|
+
_raw: { name: "Beta" },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
title: "Gamma",
|
|
25
|
+
subtitle: "Third item",
|
|
26
|
+
teaser: "",
|
|
27
|
+
image: "",
|
|
28
|
+
icon: "",
|
|
29
|
+
_raw: { name: "Gamma" },
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const BASE_PROPS: LayoutProps = {
|
|
34
|
+
items: ITEMS,
|
|
35
|
+
showTitle: true,
|
|
36
|
+
showSubtitle: true,
|
|
37
|
+
showTeaser: true,
|
|
38
|
+
showImage: true,
|
|
39
|
+
showIcon: true,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
describe("Cards sub-component", () => {
|
|
43
|
+
it("renders all item titles", async () => {
|
|
44
|
+
const { Cards } = await import("@pantheon-systems/puck-css/fields");
|
|
45
|
+
const html = renderToStaticMarkup(
|
|
46
|
+
<Cards {...BASE_PROPS} columns={3} imagePosition="top" />,
|
|
47
|
+
);
|
|
48
|
+
expect(html).toContain("Alpha");
|
|
49
|
+
expect(html).toContain("Beta");
|
|
50
|
+
expect(html).toContain("Gamma");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("hides titles when showTitle is false", async () => {
|
|
54
|
+
const { Cards } = await import("@pantheon-systems/puck-css/fields");
|
|
55
|
+
const html = renderToStaticMarkup(
|
|
56
|
+
<Cards
|
|
57
|
+
{...BASE_PROPS}
|
|
58
|
+
showTitle={false}
|
|
59
|
+
showImage={false}
|
|
60
|
+
columns={3}
|
|
61
|
+
imagePosition="top"
|
|
62
|
+
/>,
|
|
63
|
+
);
|
|
64
|
+
expect(html).not.toContain("Alpha");
|
|
65
|
+
expect(html).not.toContain("Beta");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("hides images when showImage is false", async () => {
|
|
69
|
+
const { Cards } = await import("@pantheon-systems/puck-css/fields");
|
|
70
|
+
const html = renderToStaticMarkup(
|
|
71
|
+
<Cards
|
|
72
|
+
{...BASE_PROPS}
|
|
73
|
+
showImage={false}
|
|
74
|
+
columns={3}
|
|
75
|
+
imagePosition="top"
|
|
76
|
+
/>,
|
|
77
|
+
);
|
|
78
|
+
expect(html).not.toContain("<img");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("renders images when showImage is true and image exists", async () => {
|
|
82
|
+
const { Cards } = await import("@pantheon-systems/puck-css/fields");
|
|
83
|
+
const html = renderToStaticMarkup(
|
|
84
|
+
<Cards {...BASE_PROPS} columns={3} imagePosition="top" />,
|
|
85
|
+
);
|
|
86
|
+
expect(html).toContain("alpha.jpg");
|
|
87
|
+
expect(html).toContain("beta.jpg");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("hides subtitles when showSubtitle is false", async () => {
|
|
91
|
+
const { Cards } = await import("@pantheon-systems/puck-css/fields");
|
|
92
|
+
const html = renderToStaticMarkup(
|
|
93
|
+
<Cards
|
|
94
|
+
{...BASE_PROPS}
|
|
95
|
+
showSubtitle={false}
|
|
96
|
+
columns={3}
|
|
97
|
+
imagePosition="top"
|
|
98
|
+
/>,
|
|
99
|
+
);
|
|
100
|
+
expect(html).not.toContain("First item");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("hides teasers when showTeaser is false", async () => {
|
|
104
|
+
const { Cards } = await import("@pantheon-systems/puck-css/fields");
|
|
105
|
+
const html = renderToStaticMarkup(
|
|
106
|
+
<Cards
|
|
107
|
+
{...BASE_PROPS}
|
|
108
|
+
showTeaser={false}
|
|
109
|
+
columns={3}
|
|
110
|
+
imagePosition="top"
|
|
111
|
+
/>,
|
|
112
|
+
);
|
|
113
|
+
expect(html).not.toContain("A brief preview of the first item");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("Rows sub-component", () => {
|
|
118
|
+
it("renders all item titles", async () => {
|
|
119
|
+
const { Rows } = await import("@pantheon-systems/puck-css/fields");
|
|
120
|
+
const html = renderToStaticMarkup(
|
|
121
|
+
<Rows {...BASE_PROPS} rowDensity="comfortable" imagePosition="left" />,
|
|
122
|
+
);
|
|
123
|
+
expect(html).toContain("Alpha");
|
|
124
|
+
expect(html).toContain("Beta");
|
|
125
|
+
expect(html).toContain("Gamma");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("hides titles when showTitle is false", async () => {
|
|
129
|
+
const { Rows } = await import("@pantheon-systems/puck-css/fields");
|
|
130
|
+
const html = renderToStaticMarkup(
|
|
131
|
+
<Rows
|
|
132
|
+
{...BASE_PROPS}
|
|
133
|
+
showTitle={false}
|
|
134
|
+
rowDensity="comfortable"
|
|
135
|
+
imagePosition="none"
|
|
136
|
+
/>,
|
|
137
|
+
);
|
|
138
|
+
expect(html).not.toContain("Alpha");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("hides images when imagePosition is none", async () => {
|
|
142
|
+
const { Rows } = await import("@pantheon-systems/puck-css/fields");
|
|
143
|
+
const html = renderToStaticMarkup(
|
|
144
|
+
<Rows
|
|
145
|
+
{...BASE_PROPS}
|
|
146
|
+
rowDensity="comfortable"
|
|
147
|
+
imagePosition="none"
|
|
148
|
+
/>,
|
|
149
|
+
);
|
|
150
|
+
expect(html).not.toContain("<img");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("Listing sub-component", () => {
|
|
155
|
+
it("renders all item titles", async () => {
|
|
156
|
+
const { Listing } = await import("@pantheon-systems/puck-css/fields");
|
|
157
|
+
const html = renderToStaticMarkup(
|
|
158
|
+
<Listing
|
|
159
|
+
{...BASE_PROPS}
|
|
160
|
+
imagePosition="left"
|
|
161
|
+
listingWidth="wide"
|
|
162
|
+
/>,
|
|
163
|
+
);
|
|
164
|
+
expect(html).toContain("Alpha");
|
|
165
|
+
expect(html).toContain("Beta");
|
|
166
|
+
expect(html).toContain("Gamma");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("hides titles when showTitle is false", async () => {
|
|
170
|
+
const { Listing } = await import("@pantheon-systems/puck-css/fields");
|
|
171
|
+
const html = renderToStaticMarkup(
|
|
172
|
+
<Listing
|
|
173
|
+
{...BASE_PROPS}
|
|
174
|
+
showTitle={false}
|
|
175
|
+
showImage={false}
|
|
176
|
+
imagePosition="left"
|
|
177
|
+
listingWidth="wide"
|
|
178
|
+
/>,
|
|
179
|
+
);
|
|
180
|
+
expect(html).not.toContain("Alpha");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("hides images when showImage is false", async () => {
|
|
184
|
+
const { Listing } = await import("@pantheon-systems/puck-css/fields");
|
|
185
|
+
const html = renderToStaticMarkup(
|
|
186
|
+
<Listing
|
|
187
|
+
{...BASE_PROPS}
|
|
188
|
+
showImage={false}
|
|
189
|
+
imagePosition="left"
|
|
190
|
+
listingWidth="wide"
|
|
191
|
+
/>,
|
|
192
|
+
);
|
|
193
|
+
expect(html).not.toContain("<img");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("renders teasers when showTeaser is true", async () => {
|
|
197
|
+
const { Listing } = await import("@pantheon-systems/puck-css/fields");
|
|
198
|
+
const html = renderToStaticMarkup(
|
|
199
|
+
<Listing
|
|
200
|
+
{...BASE_PROPS}
|
|
201
|
+
imagePosition="left"
|
|
202
|
+
listingWidth="wide"
|
|
203
|
+
/>,
|
|
204
|
+
);
|
|
205
|
+
expect(html).toContain("A brief preview of the first item");
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("image loading", () => {
|
|
210
|
+
it("lazy-loads item images in every layout by default", async () => {
|
|
211
|
+
const { Cards, Rows, Listing } = await import(
|
|
212
|
+
"@pantheon-systems/puck-css/fields"
|
|
213
|
+
);
|
|
214
|
+
const markups = [
|
|
215
|
+
renderToStaticMarkup(
|
|
216
|
+
<Cards {...BASE_PROPS} columns={3} imagePosition="top" />,
|
|
217
|
+
),
|
|
218
|
+
renderToStaticMarkup(
|
|
219
|
+
<Rows {...BASE_PROPS} rowDensity="comfortable" imagePosition="left" />,
|
|
220
|
+
),
|
|
221
|
+
renderToStaticMarkup(
|
|
222
|
+
<Listing {...BASE_PROPS} imagePosition="left" listingWidth="wide" />,
|
|
223
|
+
),
|
|
224
|
+
];
|
|
225
|
+
for (const html of markups) {
|
|
226
|
+
expect(html).toContain("<img");
|
|
227
|
+
expect(html).toContain('loading="lazy"');
|
|
228
|
+
expect(html).toContain('decoding="async"');
|
|
229
|
+
expect(html).not.toContain('loading="eager"');
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("honors an eager override", async () => {
|
|
234
|
+
const { Cards } = await import("@pantheon-systems/puck-css/fields");
|
|
235
|
+
const html = renderToStaticMarkup(
|
|
236
|
+
<Cards
|
|
237
|
+
{...BASE_PROPS}
|
|
238
|
+
imageLoading="eager"
|
|
239
|
+
columns={3}
|
|
240
|
+
imagePosition="top"
|
|
241
|
+
/>,
|
|
242
|
+
);
|
|
243
|
+
expect(html).toContain('loading="eager"');
|
|
244
|
+
expect(html).not.toContain('loading="lazy"');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
describe("data-list-block utils", () => {
|
|
4
|
+
describe("getByDotPath", () => {
|
|
5
|
+
it("traverses nested objects", async () => {
|
|
6
|
+
const { getByDotPath } = await import(
|
|
7
|
+
"@pantheon-systems/puck-css/fields"
|
|
8
|
+
);
|
|
9
|
+
expect(getByDotPath({ a: { b: { c: 42 } } }, "a.b.c")).toBe(42);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("returns undefined for missing paths", async () => {
|
|
13
|
+
const { getByDotPath } = await import(
|
|
14
|
+
"@pantheon-systems/puck-css/fields"
|
|
15
|
+
);
|
|
16
|
+
expect(getByDotPath({ a: 1 }, "b")).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("rejects unsafe keys", async () => {
|
|
20
|
+
const { getByDotPath } = await import(
|
|
21
|
+
"@pantheon-systems/puck-css/fields"
|
|
22
|
+
);
|
|
23
|
+
expect(getByDotPath({}, "__proto__")).toBeUndefined();
|
|
24
|
+
expect(getByDotPath({}, "constructor")).toBeUndefined();
|
|
25
|
+
expect(getByDotPath({}, "prototype")).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("viewExtractKey", () => {
|
|
30
|
+
it("extracts the key from a template token", async () => {
|
|
31
|
+
const { viewExtractKey } = await import(
|
|
32
|
+
"@pantheon-systems/puck-css/fields"
|
|
33
|
+
);
|
|
34
|
+
expect(viewExtractKey("{{ item.title }}")).toBe("title");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns empty string for non-template values", async () => {
|
|
38
|
+
const { viewExtractKey } = await import(
|
|
39
|
+
"@pantheon-systems/puck-css/fields"
|
|
40
|
+
);
|
|
41
|
+
expect(viewExtractKey("plain text")).toBe("");
|
|
42
|
+
expect(viewExtractKey("")).toBe("");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("resolveField", () => {
|
|
47
|
+
it("resolves a template value against an item", async () => {
|
|
48
|
+
const { resolveField } = await import(
|
|
49
|
+
"@pantheon-systems/puck-css/fields"
|
|
50
|
+
);
|
|
51
|
+
const item = { name: "Luke" };
|
|
52
|
+
expect(resolveField(item, "{{ item.name }}")).toBe("Luke");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns empty string for missing fields", async () => {
|
|
56
|
+
const { resolveField } = await import(
|
|
57
|
+
"@pantheon-systems/puck-css/fields"
|
|
58
|
+
);
|
|
59
|
+
expect(resolveField({}, "{{ item.missing }}")).toBe("");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("resolveItemFields", () => {
|
|
64
|
+
it("resolves all field mappings for an item", async () => {
|
|
65
|
+
const { resolveItemFields } = await import(
|
|
66
|
+
"@pantheon-systems/puck-css/fields"
|
|
67
|
+
);
|
|
68
|
+
const item = { name: "Luke", desc: "Jedi", img: "/luke.png" };
|
|
69
|
+
const result = resolveItemFields(item, {
|
|
70
|
+
titleField: "{{ item.name }}",
|
|
71
|
+
subtitleField: "",
|
|
72
|
+
teaserField: "{{ item.desc }}",
|
|
73
|
+
imageField: "{{ item.img }}",
|
|
74
|
+
iconField: "",
|
|
75
|
+
});
|
|
76
|
+
expect(result.title).toBe("Luke");
|
|
77
|
+
expect(result.subtitle).toBe("");
|
|
78
|
+
expect(result.teaser).toBe("Jedi");
|
|
79
|
+
expect(result.image).toBe("/luke.png");
|
|
80
|
+
expect(result.icon).toBe("");
|
|
81
|
+
expect(result._raw).toBe(item);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
const fieldsOf = (block: { fields: Record<string, unknown> }) =>
|
|
4
|
+
block.fields as Record<string, { type?: string }>;
|
|
5
|
+
|
|
6
|
+
describe("dataListBlock", () => {
|
|
7
|
+
it("has label 'List'", async () => {
|
|
8
|
+
const { dataListBlock } = await import(
|
|
9
|
+
"../components/puck/data-list-block"
|
|
10
|
+
);
|
|
11
|
+
expect(dataListBlock.label).toBe("List");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("has a custom datasourceId field", async () => {
|
|
15
|
+
const { dataListBlock } = await import(
|
|
16
|
+
"../components/puck/data-list-block"
|
|
17
|
+
);
|
|
18
|
+
expect(fieldsOf(dataListBlock).datasourceId.type).toBe("custom");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("has a custom field for viewMode", async () => {
|
|
22
|
+
const { dataListBlock } = await import(
|
|
23
|
+
"../components/puck/data-list-block"
|
|
24
|
+
);
|
|
25
|
+
expect(fieldsOf(dataListBlock).viewMode.type).toBe("custom");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("has custom schema-select fields for item field mappings", async () => {
|
|
29
|
+
const { dataListBlock } = await import(
|
|
30
|
+
"../components/puck/data-list-block"
|
|
31
|
+
);
|
|
32
|
+
expect(fieldsOf(dataListBlock).titleField.type).toBe("custom");
|
|
33
|
+
expect(fieldsOf(dataListBlock).subtitleField.type).toBe("custom");
|
|
34
|
+
expect(fieldsOf(dataListBlock).teaserField.type).toBe("custom");
|
|
35
|
+
expect(fieldsOf(dataListBlock).imageField.type).toBe("custom");
|
|
36
|
+
expect(fieldsOf(dataListBlock).iconField.type).toBe("custom");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("does not expose separate radio fields for visibility toggles (inline in schema-select)", async () => {
|
|
40
|
+
const { dataListBlock } = await import(
|
|
41
|
+
"../components/puck/data-list-block"
|
|
42
|
+
);
|
|
43
|
+
const fields = dataListBlock.fields as Record<string, unknown>;
|
|
44
|
+
expect(fields.showTitle).toBeUndefined();
|
|
45
|
+
expect(fields.showSubtitle).toBeUndefined();
|
|
46
|
+
expect(fields.showTeaser).toBeUndefined();
|
|
47
|
+
expect(fields.showImage).toBeUndefined();
|
|
48
|
+
expect(fields.showIcon).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("has a unified imagePosition custom field", async () => {
|
|
52
|
+
const { dataListBlock } = await import(
|
|
53
|
+
"../components/puck/data-list-block"
|
|
54
|
+
);
|
|
55
|
+
expect(fieldsOf(dataListBlock).imagePosition.type).toBe("custom");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("does not expose per-view image position fields", async () => {
|
|
59
|
+
const { dataListBlock } = await import(
|
|
60
|
+
"../components/puck/data-list-block"
|
|
61
|
+
);
|
|
62
|
+
const fields = dataListBlock.fields as Record<string, unknown>;
|
|
63
|
+
expect(fields.cardImagePosition).toBeUndefined();
|
|
64
|
+
expect(fields.rowShowImage).toBeUndefined();
|
|
65
|
+
expect(fields.listingImagePosition).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("has layout option fields for cards view", async () => {
|
|
69
|
+
const { dataListBlock } = await import(
|
|
70
|
+
"../components/puck/data-list-block"
|
|
71
|
+
);
|
|
72
|
+
expect(fieldsOf(dataListBlock).columns.type).toBe("custom");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("has layout option fields for rows view", async () => {
|
|
76
|
+
const { dataListBlock } = await import(
|
|
77
|
+
"../components/puck/data-list-block"
|
|
78
|
+
);
|
|
79
|
+
expect(fieldsOf(dataListBlock).rowDensity.type).toBe("custom");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("has layout option fields for listing view", async () => {
|
|
83
|
+
const { dataListBlock } = await import(
|
|
84
|
+
"../components/puck/data-list-block"
|
|
85
|
+
);
|
|
86
|
+
expect(fieldsOf(dataListBlock).listingWidth.type).toBe("custom");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("has list-level fields", async () => {
|
|
90
|
+
const { dataListBlock } = await import(
|
|
91
|
+
"../components/puck/data-list-block"
|
|
92
|
+
);
|
|
93
|
+
expect(fieldsOf(dataListBlock).heading.type).toBe("custom");
|
|
94
|
+
expect(fieldsOf(dataListBlock).groupBy.type).toBe("custom");
|
|
95
|
+
expect(fieldsOf(dataListBlock).sortBy.type).toBe("custom");
|
|
96
|
+
expect(fieldsOf(dataListBlock).sortDir.type).toBe("custom");
|
|
97
|
+
expect(fieldsOf(dataListBlock).maxItems.type).toBe("custom");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("does not have dataSourceType or cmsTemplateId fields", async () => {
|
|
101
|
+
const { dataListBlock } = await import(
|
|
102
|
+
"../components/puck/data-list-block"
|
|
103
|
+
);
|
|
104
|
+
expect(dataListBlock.fields).not.toHaveProperty("dataSourceType");
|
|
105
|
+
expect(dataListBlock.fields).not.toHaveProperty("cmsTemplateId");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("has sensible defaultProps", async () => {
|
|
109
|
+
const { dataListBlock } = await import(
|
|
110
|
+
"../components/puck/data-list-block"
|
|
111
|
+
);
|
|
112
|
+
const d = dataListBlock.defaultProps;
|
|
113
|
+
expect(d.datasourceId).toBe("");
|
|
114
|
+
expect(d.viewMode).toBe("grid");
|
|
115
|
+
expect(d.showTitle).toBe(true);
|
|
116
|
+
expect(d.showSubtitle).toBe(true);
|
|
117
|
+
expect(d.showTeaser).toBe(false);
|
|
118
|
+
expect(d.showImage).toBe(true);
|
|
119
|
+
expect(d.showIcon).toBe(false);
|
|
120
|
+
expect(d.columns).toBe(3);
|
|
121
|
+
expect(d.imagePosition).toBe("top");
|
|
122
|
+
expect(d.rowDensity).toBe("comfortable");
|
|
123
|
+
expect(d.listingWidth).toBe("wide");
|
|
124
|
+
expect(d.heading).toBe("");
|
|
125
|
+
expect(d.groupBy).toBe("");
|
|
126
|
+
expect(d.sortBy).toBe("");
|
|
127
|
+
expect(d.sortDir).toBe("asc");
|
|
128
|
+
expect(d.maxItems).toBe(0);
|
|
129
|
+
expect(d.items).toBe("");
|
|
130
|
+
expect(d).not.toHaveProperty("dataSourceType");
|
|
131
|
+
expect(d).not.toHaveProperty("cmsTemplateId");
|
|
132
|
+
expect(d).not.toHaveProperty("titleField");
|
|
133
|
+
expect(d).not.toHaveProperty("imageField");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("resolveData sets items token when datasource is selected", async () => {
|
|
137
|
+
const { dataListBlock } = await import(
|
|
138
|
+
"../components/puck/data-list-block"
|
|
139
|
+
);
|
|
140
|
+
const result = await dataListBlock.resolveData(
|
|
141
|
+
{
|
|
142
|
+
props: {
|
|
143
|
+
...dataListBlock.defaultProps,
|
|
144
|
+
datasourceId: "swapi_list",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{ changed: { datasourceId: true } },
|
|
148
|
+
);
|
|
149
|
+
expect(result.props.items).toBe("{{ swapi_list.items }}");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("resolveData sets empty items when no datasource selected", async () => {
|
|
153
|
+
const { dataListBlock } = await import(
|
|
154
|
+
"../components/puck/data-list-block"
|
|
155
|
+
);
|
|
156
|
+
const result = await dataListBlock.resolveData(
|
|
157
|
+
{
|
|
158
|
+
props: {
|
|
159
|
+
...dataListBlock.defaultProps,
|
|
160
|
+
datasourceId: "",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{ changed: { datasourceId: true } },
|
|
164
|
+
);
|
|
165
|
+
expect(result.props.items).toBe("");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("resolveData skips when datasourceId has not changed", async () => {
|
|
169
|
+
const { dataListBlock } = await import(
|
|
170
|
+
"../components/puck/data-list-block"
|
|
171
|
+
);
|
|
172
|
+
const result = await dataListBlock.resolveData(
|
|
173
|
+
{
|
|
174
|
+
props: {
|
|
175
|
+
...dataListBlock.defaultProps,
|
|
176
|
+
datasourceId: "swapi_list",
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{ changed: { viewMode: true } },
|
|
180
|
+
);
|
|
181
|
+
expect(result.props).toEqual({});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("exposes an imageLoading field defaulting to lazy", async () => {
|
|
185
|
+
const { dataListBlock } = await import(
|
|
186
|
+
"../components/puck/data-list-block"
|
|
187
|
+
);
|
|
188
|
+
expect(fieldsOf(dataListBlock).imageLoading.type).toBe("custom");
|
|
189
|
+
expect(dataListBlock.defaultProps.imageLoading).toBe("lazy");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("provides a render function", async () => {
|
|
193
|
+
const { dataListBlock } = await import(
|
|
194
|
+
"../components/puck/data-list-block"
|
|
195
|
+
);
|
|
196
|
+
expect(typeof dataListBlock.render).toBe("function");
|
|
197
|
+
});
|
|
198
|
+
});
|