@pantheon-systems/create-p1-starter-kit 0.12.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.
Files changed (42) hide show
  1. package/README.md +3 -3
  2. package/lib/cli.js +122 -53
  3. package/lib/cli.test.js +55 -0
  4. package/lib/copy-template.js +37 -0
  5. package/lib/copy-template.test.js +42 -1
  6. package/lib/messages.js +1 -1
  7. package/package.json +7 -4
  8. package/template/README.md +82 -0
  9. package/template/__tests__/auth-route.test.ts +1 -1
  10. package/template/__tests__/chatbot-flag-context.test.ts +55 -0
  11. package/template/__tests__/data-list-block-sub-components.test.tsx +246 -0
  12. package/template/__tests__/data-list-block-utils.test.ts +84 -0
  13. package/template/__tests__/data-list-block.test.ts +198 -0
  14. package/template/__tests__/published-page-routes.test.ts +86 -0
  15. package/template/__tests__/seo-metadata.test.ts +1 -1
  16. package/template/__tests__/styles-canvas-scope.test.ts +1 -1
  17. package/template/__tests__/widget-logout.test.ts +114 -0
  18. package/template/app/[...puckPath]/client.tsx +38 -10
  19. package/template/app/[...puckPath]/page.tsx +10 -117
  20. package/template/app/[...puckPath]/widget-logout.ts +36 -0
  21. package/template/app/p1/(editor)/[[...p1]]/editor-client.tsx +33 -140
  22. package/template/app/page.tsx +11 -69
  23. package/template/app/published-pages.tsx +23 -0
  24. package/template/app/styles.css +1 -1
  25. package/template/app/welcome-block.tsx +25 -0
  26. package/template/ci-examples/github-actions-sync-puck-registry.yml +15 -7
  27. package/template/components/ChatbotFlagProvider.tsx +3 -8
  28. package/template/components/p1-lockup.tsx +2 -1
  29. package/template/components/puck/image-block.tsx +5 -2
  30. package/template/components/puck/welcome-block-render.tsx +30 -37
  31. package/template/components/welcome-block.module.css +124 -0
  32. package/template/eslint.config.js +72 -1
  33. package/template/gitignore +43 -0
  34. package/template/lib/chatbot-flag/ai-generate.ts +1 -1
  35. package/template/lib/chatbot-flag/flag-context.ts +32 -0
  36. package/template/lib/page-seo.ts +1 -1
  37. package/template/package.json +5 -5
  38. package/template/scripts/__tests__/sync-puck-registry.test.ts +3 -3
  39. package/template/scripts/sync-puck-registry.ts +18 -18
  40. package/template/vitest.config.ts +17 -20
  41. package/template/CHANGELOG.md +0 -76
  42. package/template/__tests__/published-page-404.test.ts +0 -65
@@ -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
+ });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The render pipeline itself is tested in the SDK (create-published-page.test.tsx).
3
+ * What matters here is the wiring: that both routes are shims over the factory,
4
+ * and that the segment config Next.js statically analyzes survives in the files.
5
+ */
6
+
7
+ import { readFileSync } from "fs";
8
+ import { resolve, dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { describe, expect, it, vi } from "vitest";
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const starterDir = resolve(__dirname, "..");
14
+
15
+ const factory = vi.hoisted(() => ({
16
+ Page: () => null,
17
+ HomePage: () => null,
18
+ generateMetadata: vi.fn(),
19
+ generateHomeMetadata: vi.fn(),
20
+ generateStaticParams: vi.fn(),
21
+ }));
22
+
23
+ vi.mock("@pantheon-systems/p1-next-sdk/server", () => ({
24
+ createPublishedPage: vi.fn(() => factory),
25
+ loadRouteTemplateKeys: vi.fn(),
26
+ }));
27
+
28
+ // The routes reach these through app/published-pages.tsx. Mocked so this stays
29
+ // a wiring test: importing the real modules drags in the whole datasource graph,
30
+ // which is not installed when the built template's copy of this test runs
31
+ // inside the create-p1-starter-kit package.
32
+ vi.mock("../lib/remote-datasource-fetchers", () => ({
33
+ REMOTE_DATASOURCE_FETCHERS: [],
34
+ }));
35
+ vi.mock("../lib/page-seo", () => ({ resolvePageMetadata: vi.fn() }));
36
+
37
+ vi.mock("../app/[...puckPath]/client", () => ({ Client: () => null }));
38
+ vi.mock("../components/puck/welcome-block-render", () => ({
39
+ WelcomeBlockRender: () => null,
40
+ }));
41
+
42
+ const routes = {
43
+ catchAll: "app/[...puckPath]/page.tsx",
44
+ home: "app/page.tsx",
45
+ } as const;
46
+
47
+ describe("published page routes are shims over the SDK factory", () => {
48
+ it("wires the catch-all route to the factory", async () => {
49
+ const route = await import("../app/[...puckPath]/page");
50
+ expect(route.default).toBe(factory.Page);
51
+ expect(route.generateMetadata).toBe(factory.generateMetadata);
52
+ expect(route.generateStaticParams).toBe(factory.generateStaticParams);
53
+ });
54
+
55
+ it("wires the home route to the factory", async () => {
56
+ const route = await import("../app/page");
57
+ expect(route.default).toBe(factory.HomePage);
58
+ expect(route.generateMetadata).toBe(factory.generateHomeMetadata);
59
+ });
60
+
61
+ // Next.js statically analyzes segment-config exports, so this value cannot be
62
+ // re-exported through the factory — a computed or forwarded `revalidate` goes
63
+ // undetected and the route silently loses its revalidation window.
64
+ it.each(Object.values(routes))(
65
+ "keeps revalidate a literal export in %s",
66
+ (file) => {
67
+ const source = readFileSync(resolve(starterDir, file), "utf-8");
68
+ expect(source).toMatch(/^export const revalidate = \d+;$/m);
69
+ },
70
+ );
71
+
72
+ // The pipeline is the SDK's now; a copy growing back here is the regression
73
+ // this guards against.
74
+ it.each(Object.values(routes))("leaves no pipeline logic in %s", (file) => {
75
+ const source = readFileSync(resolve(starterDir, file), "utf-8");
76
+ for (const symbol of [
77
+ "loadPublishedPage",
78
+ "loadRemoteDatasourceContext",
79
+ "resolveDataTemplates",
80
+ "isInternalPath",
81
+ "notFound",
82
+ ]) {
83
+ expect(source).not.toContain(symbol);
84
+ }
85
+ });
86
+ });
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
2
2
  import { buildPageMetadata } from "../lib/seo-metadata";
3
3
 
4
4
  /**
5
- * PCC-3407: buildPageMetadata maps the head metadata inputs (client-derived
5
+ * buildPageMetadata maps the head metadata inputs (client-derived
6
6
  * title/description/canonical plus the backend-supplied siteName) onto the
7
7
  * Next.js Metadata object that renders the per-page <head> tags. Next replaces
8
8
  * (not deep-merges) a page's openGraph over the layout's, so og:type and the
@@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest";
6
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
7
  const appDir = resolve(__dirname, "..");
8
8
 
9
- // PCC-3499 / PCC-3513: Puck's collectStyles() copies every parent
9
+ // Puck's collectStyles() copies every parent
10
10
  // <style>/<link> element into the canvas-preview iframe verbatim (there is
11
11
  // no exclusion API), and separately its CopyHostStyles helper syncs this
12
12
  // document's <body> attributes (including `class`) onto the iframe's own