@caelo-cms/shared 0.10.57 → 0.10.59
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/dist/ai-tools.d.ts +30 -0
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +26 -9
- package/dist/ai-tools.js.map +1 -1
- package/dist/bug-intake.d.ts +176 -0
- package/dist/bug-intake.d.ts.map +1 -0
- package/dist/bug-intake.js +200 -0
- package/dist/bug-intake.js.map +1 -0
- package/dist/content.js +7 -7
- package/dist/edit-markers.d.ts +49 -0
- package/dist/edit-markers.d.ts.map +1 -0
- package/dist/edit-markers.js +225 -0
- package/dist/edit-markers.js.map +1 -0
- package/dist/google-oauth.d.ts +22 -0
- package/dist/google-oauth.d.ts.map +1 -0
- package/dist/google-oauth.js +25 -0
- package/dist/google-oauth.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/link-canonicalise.d.ts +109 -0
- package/dist/link-canonicalise.d.ts.map +1 -0
- package/dist/link-canonicalise.js +471 -0
- package/dist/link-canonicalise.js.map +1 -0
- package/dist/link-refs.d.ts +63 -8
- package/dist/link-refs.d.ts.map +1 -1
- package/dist/link-refs.js +80 -14
- package/dist/link-refs.js.map +1 -1
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +15 -0
- package/dist/media.js.map +1 -1
- package/dist/nested-module-render.d.ts +17 -0
- package/dist/nested-module-render.d.ts.map +1 -1
- package/dist/nested-module-render.js +45 -6
- package/dist/nested-module-render.js.map +1 -1
- package/dist/page-log.d.ts +1 -1
- package/dist/preview-compose.d.ts.map +1 -1
- package/dist/preview-compose.js +12 -2
- package/dist/preview-compose.js.map +1 -1
- package/dist/theme-normalize.d.ts.map +1 -1
- package/dist/theme-normalize.js +17 -2
- package/dist/theme-normalize.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/ai-tools.ts +29 -11
- package/src/bug-intake.test.ts +154 -0
- package/src/bug-intake.ts +256 -0
- package/src/content.ts +7 -7
- package/src/edit-markers.test.ts +111 -0
- package/src/edit-markers.ts +244 -0
- package/src/google-oauth.ts +27 -0
- package/src/index.ts +4 -0
- package/src/link-canonicalise.test.ts +387 -0
- package/src/link-canonicalise.ts +598 -0
- package/src/link-refs.test.ts +206 -0
- package/src/link-refs.ts +119 -21
- package/src/media.ts +16 -0
- package/src/nested-module-render.ts +59 -4
- package/src/preview-compose.ts +11 -2
- package/src/theme-normalize-typography-subfields.test.ts +54 -0
- package/src/theme-normalize.ts +19 -2
- package/src/version.ts +1 -1
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tier-1 unit tests for write-time link canonicalisation (epic #724,
|
|
5
|
+
* rollout C): which positions are links, what becomes a reference, what
|
|
6
|
+
* stays literal, and what is an issue.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "bun:test";
|
|
10
|
+
import {
|
|
11
|
+
canonicaliseContentValues,
|
|
12
|
+
canonicaliseFieldDefaults,
|
|
13
|
+
canonicaliseHtmlLinks,
|
|
14
|
+
canonicaliseJsonLinks,
|
|
15
|
+
canonicaliseUrl,
|
|
16
|
+
collectLinkLiterals,
|
|
17
|
+
describeLinkIssues,
|
|
18
|
+
type LinkTargetLookup,
|
|
19
|
+
linkCanonicaliseRun,
|
|
20
|
+
} from "./link-canonicalise.js";
|
|
21
|
+
|
|
22
|
+
const ABOUT = "11111111-1111-4111-8111-111111111111";
|
|
23
|
+
const HOME = "22222222-2222-4222-8222-222222222222";
|
|
24
|
+
const GONE = "33333333-3333-4333-8333-333333333333";
|
|
25
|
+
const HERO = "44444444-4444-4444-8444-444444444444";
|
|
26
|
+
|
|
27
|
+
const lookup: LinkTargetLookup = {
|
|
28
|
+
pageIdForPath: (p) =>
|
|
29
|
+
(
|
|
30
|
+
({
|
|
31
|
+
"/": HOME,
|
|
32
|
+
"/company/about": ABOUT,
|
|
33
|
+
"/old-about": ABOUT,
|
|
34
|
+
// Old URLs an import redirected (301) to the page.
|
|
35
|
+
"/about.html": ABOUT,
|
|
36
|
+
"/about/index.html": ABOUT,
|
|
37
|
+
}) as Record<string, string>
|
|
38
|
+
)[p] ?? null,
|
|
39
|
+
mediaIdForSlug: (s) => (s === "hero" ? HERO : null),
|
|
40
|
+
pageExists: (id) => id === ABOUT || id === HOME,
|
|
41
|
+
mediaExists: (id) => id === HERO,
|
|
42
|
+
isOwnHost: (h) => h === "example.com",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const run = (grandfathered: string[] = []) =>
|
|
46
|
+
linkCanonicaliseRun(lookup, { grandfathered: new Set(grandfathered) });
|
|
47
|
+
|
|
48
|
+
describe("canonicaliseUrl", () => {
|
|
49
|
+
it("turns internal paths, own-domain URLs and media URLs into references", () => {
|
|
50
|
+
const r = run();
|
|
51
|
+
expect(canonicaliseUrl("/company/about", r, "x")).toBe(`caelo:page/${ABOUT}`);
|
|
52
|
+
expect(canonicaliseUrl("/company/about/#team", r, "x")).toBe(`caelo:page/${ABOUT}#team`);
|
|
53
|
+
expect(canonicaliseUrl("/company/about?ref=nav", r, "x")).toBe(`caelo:page/${ABOUT}?ref=nav`);
|
|
54
|
+
expect(canonicaliseUrl("/", r, "x")).toBe(`caelo:page/${HOME}`);
|
|
55
|
+
expect(canonicaliseUrl("https://example.com/company/about", r, "x")).toBe(
|
|
56
|
+
`caelo:page/${ABOUT}`,
|
|
57
|
+
);
|
|
58
|
+
expect(canonicaliseUrl("/old-about", r, "x")).toBe(`caelo:page/${ABOUT}`);
|
|
59
|
+
expect(canonicaliseUrl("/_caelo/media/hero", r, "x")).toBe(`caelo:media/${HERO}`);
|
|
60
|
+
expect(canonicaliseUrl("/_caelo/media/hero/webp-800", r, "x")).toBe(
|
|
61
|
+
`caelo:media/${HERO}/webp-800`,
|
|
62
|
+
);
|
|
63
|
+
expect(canonicaliseUrl(`/_caelo/media/${HERO}/webp-400`, r, "x")).toBe(
|
|
64
|
+
`caelo:media/${HERO}/webp-400`,
|
|
65
|
+
);
|
|
66
|
+
expect(r.issues).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("leaves external, special and non-page URLs literal", () => {
|
|
70
|
+
const r = run();
|
|
71
|
+
for (const v of [
|
|
72
|
+
"https://other.org/about",
|
|
73
|
+
"//cdn.example.com/x.js",
|
|
74
|
+
"mailto:hi@example.com",
|
|
75
|
+
"tel:+491234",
|
|
76
|
+
"#pricing",
|
|
77
|
+
"{{cta_href}}",
|
|
78
|
+
"/robots.txt",
|
|
79
|
+
"/sitemap.xml",
|
|
80
|
+
"/_assets/app.css",
|
|
81
|
+
"/api/forms/contact",
|
|
82
|
+
"relative/path",
|
|
83
|
+
"",
|
|
84
|
+
]) {
|
|
85
|
+
expect(canonicaliseUrl(v, r, "x")).toBe(v);
|
|
86
|
+
}
|
|
87
|
+
expect(r.issues).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("reports unknown paths, unknown media and dangling references", () => {
|
|
91
|
+
const r = run();
|
|
92
|
+
expect(canonicaliseUrl("/nope", r, "values.cta_href")).toBe("/nope");
|
|
93
|
+
expect(canonicaliseUrl("/_caelo/media/missing", r, "html")).toBe("/_caelo/media/missing");
|
|
94
|
+
expect(canonicaliseUrl(`caelo:page/${GONE}`, r, "items[0].href")).toBe(`caelo:page/${GONE}`);
|
|
95
|
+
expect(canonicaliseUrl(`caelo:page/${ABOUT}`, r, "ok")).toBe(`caelo:page/${ABOUT}`);
|
|
96
|
+
expect(r.issues.map((i) => [i.kind, i.value, i.where])).toEqual([
|
|
97
|
+
["unknown-page-path", "/nope", "values.cta_href"],
|
|
98
|
+
["unknown-media", "/_caelo/media/missing", "html"],
|
|
99
|
+
["dangling-page-ref", `caelo:page/${GONE}`, "items[0].href"],
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("resolves file-shaped old URLs a page or redirect answers before treating them as files", () => {
|
|
104
|
+
const r = run();
|
|
105
|
+
expect(canonicaliseUrl("/about.html", r, "x")).toBe(`caelo:page/${ABOUT}`);
|
|
106
|
+
expect(canonicaliseUrl("/about/index.html#team", r, "x")).toBe(`caelo:page/${ABOUT}#team`);
|
|
107
|
+
expect(r.issues).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("reports a file path nothing serves (strict) and keeps it literal (report)", () => {
|
|
111
|
+
const strict = run();
|
|
112
|
+
expect(canonicaliseUrl("/files/guide.pdf", strict, "html")).toBe("/files/guide.pdf");
|
|
113
|
+
expect(canonicaliseUrl("/index.php", strict, "html")).toBe("/index.php");
|
|
114
|
+
expect(strict.issues.map((i) => [i.kind, i.value])).toEqual([
|
|
115
|
+
["unknown-file", "/files/guide.pdf"],
|
|
116
|
+
["unknown-file", "/index.php"],
|
|
117
|
+
]);
|
|
118
|
+
const report = linkCanonicaliseRun(lookup, { mode: "report" });
|
|
119
|
+
expect(canonicaliseUrl("/files/guide.pdf", report, "html")).toBe("/files/guide.pdf");
|
|
120
|
+
expect(report.issues.map((i) => i.kind)).toEqual(["unknown-file"]);
|
|
121
|
+
expect(describeLinkIssues(strict.issues)).toContain("media library");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("refuses a media link with a query or anchor instead of dropping it", () => {
|
|
125
|
+
const r = run();
|
|
126
|
+
expect(canonicaliseUrl("/_caelo/media/hero#page=3", r, "x")).toBe("/_caelo/media/hero#page=3");
|
|
127
|
+
expect(canonicaliseUrl("/_caelo/media/hero?download=1", r, "y")).toBe(
|
|
128
|
+
"/_caelo/media/hero?download=1",
|
|
129
|
+
);
|
|
130
|
+
expect(r.issues.map((i) => [i.kind, i.where])).toEqual([
|
|
131
|
+
["unsupported-link-form", "x"],
|
|
132
|
+
["unsupported-link-form", "y"],
|
|
133
|
+
]);
|
|
134
|
+
const report = linkCanonicaliseRun(lookup, { mode: "report" });
|
|
135
|
+
expect(canonicaliseUrl("/_caelo/media/hero#page=3", report, "x")).toBe(
|
|
136
|
+
"/_caelo/media/hero#page=3",
|
|
137
|
+
);
|
|
138
|
+
expect(report.issues.map((i) => i.kind)).toEqual(["unsupported-link-form"]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("stores the orig variant as the plain media reference", () => {
|
|
142
|
+
const r = run();
|
|
143
|
+
expect(canonicaliseUrl("/_caelo/media/hero/orig", r, "x")).toBe(`caelo:media/${HERO}`);
|
|
144
|
+
expect(canonicaliseUrl(`/_caelo/media/${HERO}/orig`, r, "x")).toBe(`caelo:media/${HERO}`);
|
|
145
|
+
expect(r.issues).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("grandfathers links already stored", () => {
|
|
149
|
+
const r = run(["/nope", `caelo:page/${GONE}`]);
|
|
150
|
+
canonicaliseUrl("/nope", r, "x");
|
|
151
|
+
canonicaliseUrl(`caelo:page/${GONE}`, r, "x");
|
|
152
|
+
expect(r.issues).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("restricts media positions to media", () => {
|
|
156
|
+
const r = run();
|
|
157
|
+
expect(canonicaliseUrl("/company/about", r, "x", "media")).toBe("/company/about");
|
|
158
|
+
expect(r.issues).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("keeps a link to a planned page literal and pending, never an issue", () => {
|
|
162
|
+
const planned = new Set(["/later", "/old/page.html"]);
|
|
163
|
+
const r = linkCanonicaliseRun({ ...lookup, isPendingPagePath: (p) => planned.has(p) });
|
|
164
|
+
expect(canonicaliseUrl("/later/#x", r, "values.cta_href")).toBe("/later/#x");
|
|
165
|
+
expect(canonicaliseUrl("https://example.com/old/page.html", r, "html")).toBe(
|
|
166
|
+
"https://example.com/old/page.html",
|
|
167
|
+
);
|
|
168
|
+
// An existing page still resolves; an unplanned path is still an issue.
|
|
169
|
+
expect(canonicaliseUrl("/company/about", r, "x")).toBe(`caelo:page/${ABOUT}`);
|
|
170
|
+
expect(canonicaliseUrl("/typo", r, "y")).toBe("/typo");
|
|
171
|
+
expect(r.pending).toEqual([
|
|
172
|
+
{ value: "/later/#x", path: "/later", where: "values.cta_href" },
|
|
173
|
+
{ value: "https://example.com/old/page.html", path: "/old/page.html", where: "html" },
|
|
174
|
+
]);
|
|
175
|
+
expect(r.issues.map((i) => [i.kind, i.value])).toEqual([["unknown-page-path", "/typo"]]);
|
|
176
|
+
// Without an import run nothing is pending.
|
|
177
|
+
const strict = run();
|
|
178
|
+
canonicaliseUrl("/later", strict, "x");
|
|
179
|
+
expect(strict.pending).toEqual([]);
|
|
180
|
+
expect(strict.issues.map((i) => i.kind)).toEqual(["unknown-page-path"]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("reports a malformed reference instead of passing it through", () => {
|
|
184
|
+
const r = run();
|
|
185
|
+
const bad = [
|
|
186
|
+
"caelo:page/1111", // truncated id
|
|
187
|
+
`caelo:page/${ABOUT}/team`, // stray path after the id
|
|
188
|
+
"caelo:page/ABCDEF01-1111-4111-8111-111111111111", // ids are lowercase
|
|
189
|
+
`caelo:media/${HERO}/WEBP`, // variant outside the grammar
|
|
190
|
+
];
|
|
191
|
+
for (const v of bad) expect(canonicaliseUrl(v, r, "values.cta_href")).toBe(v);
|
|
192
|
+
expect(r.issues.map((i) => [i.kind, i.value])).toEqual(
|
|
193
|
+
bad.map((v) => ["malformed-link-ref", v]),
|
|
194
|
+
);
|
|
195
|
+
expect(describeLinkIssues(r.issues)).toContain("write the page's path");
|
|
196
|
+
// Grandfathered like any other stored literal.
|
|
197
|
+
const g = run(["caelo:page/1111"]);
|
|
198
|
+
canonicaliseUrl("caelo:page/1111", g, "x");
|
|
199
|
+
expect(g.issues).toEqual([]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("turns a query or anchor outside the grammar into an issue, never a throw", () => {
|
|
203
|
+
// `formatLinkRef` throws on a suffix it cannot parse back; the
|
|
204
|
+
// canonicaliser must validate with the grammar's own patterns first.
|
|
205
|
+
const r = run();
|
|
206
|
+
for (const v of [
|
|
207
|
+
"/company/about#a b",
|
|
208
|
+
"/company/about#x#y",
|
|
209
|
+
"/company/about?a=1?b=2",
|
|
210
|
+
'/company/about?q="x"',
|
|
211
|
+
`/company/about#${"a".repeat(257)}`,
|
|
212
|
+
`/company/about?${"q".repeat(513)}`,
|
|
213
|
+
"/company/about#über",
|
|
214
|
+
]) {
|
|
215
|
+
expect(() => canonicaliseUrl(v, r, "x")).not.toThrow();
|
|
216
|
+
expect(canonicaliseUrl(v, run(), "x")).toBe(v);
|
|
217
|
+
}
|
|
218
|
+
expect(r.issues.every((i) => i.kind === "unsupported-link-form")).toBe(true);
|
|
219
|
+
expect(r.issues).toHaveLength(7);
|
|
220
|
+
// The longest suffixes the grammar allows still become references.
|
|
221
|
+
const ok = run();
|
|
222
|
+
const anchor = "a".repeat(256);
|
|
223
|
+
expect(canonicaliseUrl(`/company/about#${anchor}`, ok, "x")).toBe(
|
|
224
|
+
`caelo:page/${ABOUT}#${anchor}`,
|
|
225
|
+
);
|
|
226
|
+
expect(ok.issues).toEqual([]);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("HTML and JSON positions", () => {
|
|
231
|
+
it("canonicalises href, src, srcset and poster in HTML", () => {
|
|
232
|
+
const r = run();
|
|
233
|
+
const html =
|
|
234
|
+
`<a class="b" href="/company/about#x">A</a><a href='https://other.org/'>O</a>` +
|
|
235
|
+
`<img src="/_caelo/media/hero" srcset="/_caelo/media/hero/webp-400 400w, /_caelo/media/hero/webp-800 800w">` +
|
|
236
|
+
`<img data-src="/company/about" src="/company/about"><a href="{{href}}">t</a>`;
|
|
237
|
+
expect(canonicaliseHtmlLinks(html, r, "html")).toBe(
|
|
238
|
+
`<a class="b" href="caelo:page/${ABOUT}#x">A</a><a href='https://other.org/'>O</a>` +
|
|
239
|
+
`<img src="caelo:media/${HERO}" srcset="caelo:media/${HERO}/webp-400 400w, caelo:media/${HERO}/webp-800 800w">` +
|
|
240
|
+
`<img data-src="/company/about" src="/company/about"><a href="{{href}}">t</a>`,
|
|
241
|
+
);
|
|
242
|
+
expect(r.issues).toEqual([]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("reads attributes only inside start tags, not in escaped code samples", () => {
|
|
246
|
+
const r = run();
|
|
247
|
+
const html =
|
|
248
|
+
'<pre><code><a href="/nope">Docs</a></code></pre>' +
|
|
249
|
+
`<a title="a > b" href="/company/about">x</a><img alt='it"s' src="/_caelo/media/hero">`;
|
|
250
|
+
expect(canonicaliseHtmlLinks(html, r, "html")).toBe(
|
|
251
|
+
'<pre><code><a href="/nope">Docs</a></code></pre>' +
|
|
252
|
+
`<a title="a > b" href="caelo:page/${ABOUT}">x</a><img alt='it"s' src="caelo:media/${HERO}">`,
|
|
253
|
+
);
|
|
254
|
+
expect(r.issues).toEqual([]);
|
|
255
|
+
// An unterminated quote does not hide the tags before it.
|
|
256
|
+
expect(canonicaliseHtmlLinks('<a href="/company/about">x</a><b title="oops>', run(), "h")).toBe(
|
|
257
|
+
`<a href="caelo:page/${ABOUT}">x</a><b title="oops>`,
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("reports malformed references in any HTML link position, once each", () => {
|
|
262
|
+
const r = run();
|
|
263
|
+
const html =
|
|
264
|
+
`<a href="caelo:page/${ABOUT}/team">A</a>` +
|
|
265
|
+
`<div data-href="caelo:page/1234">B</div>` +
|
|
266
|
+
`<img srcset="caelo:media/${HERO} 1x, caelo:media/abc 2x">` +
|
|
267
|
+
`<a href="caelo:page/${ABOUT}#ok">C</a>` +
|
|
268
|
+
"<p>Write caelo:page/<id> in prose: not a link.</p>";
|
|
269
|
+
expect(canonicaliseHtmlLinks(html, r, "html")).toBe(html);
|
|
270
|
+
expect(r.issues.map((i) => [i.kind, i.value])).toEqual([
|
|
271
|
+
["malformed-link-ref", `caelo:page/${ABOUT}/team`],
|
|
272
|
+
["malformed-link-ref", "caelo:media/abc"],
|
|
273
|
+
["malformed-link-ref", "caelo:page/1234"],
|
|
274
|
+
]);
|
|
275
|
+
// HTML without any attribute is still checked (a value that starts
|
|
276
|
+
// with the broken reference); element text is not a link position.
|
|
277
|
+
const t = run();
|
|
278
|
+
canonicaliseHtmlLinks("caelo:page/1234 <b>caelo:page/5678</b>", t, "html");
|
|
279
|
+
expect(t.issues.map((i) => i.value)).toEqual(["caelo:page/1234"]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("reports a malformed reference in a plain text value", () => {
|
|
283
|
+
const r = run();
|
|
284
|
+
expect(canonicaliseJsonLinks({ label: "caelo:page/1234" }, r, "values")).toEqual({
|
|
285
|
+
label: "caelo:page/1234",
|
|
286
|
+
});
|
|
287
|
+
expect(r.issues.map((i) => [i.kind, i.where])).toEqual([
|
|
288
|
+
["malformed-link-ref", "values.label"],
|
|
289
|
+
]);
|
|
290
|
+
// A stored malformed value is grandfathered by the next write.
|
|
291
|
+
const g = linkCanonicaliseRun(lookup, {
|
|
292
|
+
grandfathered: collectLinkLiterals({ label: "caelo:page/1234" }),
|
|
293
|
+
});
|
|
294
|
+
canonicaliseJsonLinks({ label: "caelo:page/1234" }, g, "values");
|
|
295
|
+
expect(g.issues).toEqual([]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("treats link-named keys as URLs and plain text as text", () => {
|
|
299
|
+
const r = run();
|
|
300
|
+
const items = [
|
|
301
|
+
{ label: "About", href: "/company/about", children: [{ label: "Home", href: "/" }] },
|
|
302
|
+
{ label: "/month", note: "/month" },
|
|
303
|
+
];
|
|
304
|
+
expect(canonicaliseJsonLinks(items, r, "items")).toEqual([
|
|
305
|
+
{
|
|
306
|
+
label: "About",
|
|
307
|
+
href: `caelo:page/${ABOUT}`,
|
|
308
|
+
children: [{ label: "Home", href: `caelo:page/${HOME}` }],
|
|
309
|
+
},
|
|
310
|
+
{ label: "/month", note: "/month" },
|
|
311
|
+
]);
|
|
312
|
+
expect(r.issues).toEqual([]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("canonicalises content values by field kind and name", () => {
|
|
316
|
+
const r = run();
|
|
317
|
+
const out = canonicaliseContentValues(
|
|
318
|
+
[
|
|
319
|
+
{ name: "cta", kind: "url" },
|
|
320
|
+
{ name: "hero", kind: "image" },
|
|
321
|
+
{ name: "price_suffix", kind: "text" },
|
|
322
|
+
{ name: "nav", kind: "link-list" },
|
|
323
|
+
{ name: "body", kind: "richtext" },
|
|
324
|
+
{ name: "card", kind: "module" },
|
|
325
|
+
],
|
|
326
|
+
{
|
|
327
|
+
cta: "/company/about",
|
|
328
|
+
hero: "/_caelo/media/hero",
|
|
329
|
+
price_suffix: "/month",
|
|
330
|
+
nav: [{ label: "Home", href: "/" }],
|
|
331
|
+
body: '<p><a href="/company/about">us</a></p>',
|
|
332
|
+
card: { moduleId: ABOUT, contentInstanceId: HOME },
|
|
333
|
+
legacy_href: "/company/about",
|
|
334
|
+
},
|
|
335
|
+
r,
|
|
336
|
+
);
|
|
337
|
+
expect(out).toEqual({
|
|
338
|
+
cta: `caelo:page/${ABOUT}`,
|
|
339
|
+
hero: `caelo:media/${HERO}`,
|
|
340
|
+
price_suffix: "/month",
|
|
341
|
+
nav: [{ label: "Home", href: `caelo:page/${HOME}` }],
|
|
342
|
+
body: `<p><a href="caelo:page/${ABOUT}">us</a></p>`,
|
|
343
|
+
card: { moduleId: ABOUT, contentInstanceId: HOME },
|
|
344
|
+
legacy_href: `caelo:page/${ABOUT}`,
|
|
345
|
+
});
|
|
346
|
+
expect(r.issues).toEqual([]);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("canonicalises field defaults (layout chrome)", () => {
|
|
350
|
+
const r = run();
|
|
351
|
+
const fields = canonicaliseFieldDefaults(
|
|
352
|
+
[
|
|
353
|
+
{
|
|
354
|
+
name: "links",
|
|
355
|
+
kind: "link-list",
|
|
356
|
+
label: "L",
|
|
357
|
+
default: [{ label: "A", href: "/company/about" }],
|
|
358
|
+
},
|
|
359
|
+
{ name: "title", kind: "text", label: "T", default: "/company/about" },
|
|
360
|
+
],
|
|
361
|
+
r,
|
|
362
|
+
);
|
|
363
|
+
expect(fields[0]?.default).toEqual([{ label: "A", href: `caelo:page/${ABOUT}` }]);
|
|
364
|
+
expect(fields[1]?.default).toBe("/company/about");
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe("grandfathering + messages", () => {
|
|
369
|
+
it("collects every link literal from a stored state", () => {
|
|
370
|
+
const literals = collectLinkLiterals({
|
|
371
|
+
html: '<a href="/nope">x</a>',
|
|
372
|
+
values: { cta: "/gone", text: "hello" },
|
|
373
|
+
});
|
|
374
|
+
expect(literals.has("/nope")).toBe(true);
|
|
375
|
+
expect(literals.has("/gone")).toBe(true);
|
|
376
|
+
expect(literals.has("hello")).toBe(false);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("names the value, the place and the next step", () => {
|
|
380
|
+
const msg = describeLinkIssues([
|
|
381
|
+
{ kind: "unknown-page-path", value: "/pricing", where: "values.cta_href" },
|
|
382
|
+
]);
|
|
383
|
+
expect(msg).toContain('values.cta_href: "/pricing" is not the path of any page on this site');
|
|
384
|
+
expect(msg).toContain("list_pages");
|
|
385
|
+
expect(msg).toContain("create it first");
|
|
386
|
+
});
|
|
387
|
+
});
|