@warpgogol/werkstatt-shared 0.13.0 → 0.14.1
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/README.md +26 -16
- package/README.uk.md +24 -14
- package/package.json +27 -69
- package/src/checks/result-helpers.ts +21 -4
- package/src/checks/tests/remediation-auto-populate.test.ts +83 -0
- package/src/integration/funnel.ts +78 -1
- package/src/integration/index.ts +19 -2
- package/src/integration/lagebild-ingress.ts +169 -0
- package/src/integration/orchestration.ts +7 -13
- package/src/integration/port-barrel.ts +9 -28
- package/src/integration/tests/funnel-bridge.test.ts +7 -7
- package/src/integration/tests/lagebild-ingress.test.ts +118 -0
- package/src/integration/vitest.config.ts +7 -0
- package/src/observability/typed-refs.ts +1 -1
- package/src/ontology/analytics/matomo-fleet.registry.yaml +1 -1
- package/src/ontology/archetypes/index.json +12 -9
- package/src/ontology/archetypes/index.yaml +26 -10
- package/src/ontology/blueprints/website-local.yaml +11 -11
- package/src/ontology/blueprints/website-service.yaml +20 -20
- package/src/ontology/capabilities/lead.submit.yaml +2 -2
- package/src/ontology/enums.ts +2 -2
- package/src/ontology/external-surfaces/jsonld-types.yaml +14 -14
- package/src/ontology/external-surfaces/url-schema.yaml +1 -1
- package/src/ontology/index.ts +2 -0
- package/src/ontology/manifest.ts +19 -0
- package/src/ontology/schemas/index.ts +7 -2
- package/src/ontology/schemas/system/integrations.ts +1 -1
- package/src/ontology/schemas/system/manifest.ts +38 -1
- package/src/ontology/schemas/system/tests/growth.test.ts +22 -0
- package/src/ontology/schemas/system/tests/integrations.test.ts +12 -0
- package/src/ontology/schemas/system/tests/manifest.test.ts +22 -0
- package/src/ontology/schemas/system/tests/page-output.test.ts +13 -0
- package/src/ontology/schemas/system/tests/release.test.ts +22 -0
- package/src/ontology/schemas/system/tests/text.test.ts +18 -0
- package/src/ontology/schemas/system/tests/verification.test.ts +12 -0
- package/src/ontology/schemas/system.ts +7 -2
- package/src/ontology/schemas/tests/biome.test.ts +19 -0
- package/src/ontology/schemas/tests/capability.test.ts +22 -0
- package/src/ontology/schemas/tests/constellation.test.ts +22 -0
- package/src/ontology/schemas/tests/dns-records.test.ts +13 -0
- package/src/ontology/schemas/tests/manifest-resolver.test.ts +13 -0
- package/src/ontology/schemas/tests/page-entry.test.ts +22 -0
- package/src/ontology/schemas/tests/section-archetype.test.ts +12 -0
- package/src/ontology/schemas/tests/site-family.test.ts +18 -0
- package/src/ontology/schemas/tests/system.test.ts +20 -0
- package/src/ontology/shared-section-props/common.ts +11 -0
- package/src/ontology/shared-section-props/visual-header.ts +1 -1
- package/src/share/agent/api-catalog.ts +5 -0
- package/src/share/agent/ard-catalog.ts +14 -0
- package/src/share/agent/index.ts +2 -0
- package/src/share/agent/manifest.ts +25 -2
- package/src/share/agent/mcp-card.ts +4 -0
- package/src/share/agent/openapi.ts +78 -0
- package/src/share/agent/search.test.ts +188 -0
- package/src/share/agent/search.ts +164 -0
- package/src/share/agent/tests/api-catalog.test.ts +8 -0
- package/src/share/agent/tests/ard-catalog.test.ts +8 -0
- package/src/share/agent/tests/capability.test.ts +14 -0
- package/src/share/agent/tests/fleet-catalog.test.ts +15 -0
- package/src/share/agent/tests/knowledge.test.ts +20 -0
- package/src/share/agent/tests/manifest.test.ts +9 -0
- package/src/share/agent/tests/mcp-card.test.ts +8 -0
- package/src/share/agent/tests/omit-empty.test.ts +33 -0
- package/src/share/agent/tests/openapi.test.ts +8 -0
- package/src/share/agent/tests/proof.test.ts +15 -0
- package/src/share/env.d.ts +0 -4
- package/src/share/knowledge/tests/claim.test.ts +11 -0
- package/src/share/knowledge/tests/derivation.test.ts +30 -0
- package/src/share/knowledge/tests/freshness.test.ts +18 -0
- package/src/share/knowledge/tests/ledger.test.ts +35 -0
- package/src/share/knowledge/tests/plan.test.ts +19 -0
- package/src/share/knowledge/tests/source.test.ts +19 -0
- package/src/share/remediation/remediation-catalog.ts +196 -0
- package/src/share/schemas/effects.ts +16 -0
- package/src/share/schemas/media.ts +4 -0
- package/src/share/schemas/section-shell.ts +2 -1
- package/src/share/schemas/tests/claim-records.test.ts +52 -0
- package/src/share/schemas/tests/claims.test.ts +23 -0
- package/src/share/schemas/tests/effects.test.ts +116 -0
- package/src/share/schemas/tests/features.test.ts +61 -0
- package/src/share/schemas/tests/horizontal-align.test.ts +16 -0
- package/src/share/schemas/tests/icon-color.test.ts +16 -0
- package/src/share/schemas/tests/live-photo.test.ts +40 -0
- package/src/share/schemas/tests/material-credit.test.ts +51 -0
- package/src/share/schemas/tests/media.test.ts +26 -0
- package/src/share/schemas/tests/navigation.test.ts +58 -0
- package/src/share/schemas/tests/page-base.test.ts +19 -0
- package/src/share/schemas/tests/participant.test.ts +26 -0
- package/src/share/schemas/tests/person.test.ts +12 -0
- package/src/share/schemas/tests/print.test.ts +30 -0
- package/src/share/schemas/tests/section-background.test.ts +31 -0
- package/src/share/schemas/tests/section-body.test.ts +16 -0
- package/src/share/schemas/tests/section-cards.test.ts +17 -0
- package/src/share/schemas/tests/section-cta.test.ts +41 -0
- package/src/share/schemas/tests/section-header.test.ts +31 -0
- package/src/share/schemas/tests/section-image.test.ts +24 -0
- package/src/share/schemas/tests/section-motion.test.ts +29 -0
- package/src/share/schemas/tests/section-shell.test.ts +42 -0
- package/src/share/schemas/tests/section-stats.test.ts +17 -0
- package/src/share/schemas/tests/site-background.test.ts +33 -0
- package/src/share/schemas/tests/standard-list-item.test.ts +16 -0
- package/src/share/scripts/external-link-qr.ts +9 -1
- package/src/share/scripts/gsap-counter.ts +2 -0
- package/src/share/scripts/inline-number-animation.ts +2 -0
- package/src/share/scripts/live-photos.ts +2 -0
- package/src/share/scripts/tests/external-link-qr.test.ts +8 -0
- package/src/share/scripts/tests/external-links.test.ts +25 -0
- package/src/share/scripts/tests/gsap-counter.test.ts +8 -0
- package/src/share/scripts/tests/gsap-mountain-journey.test.ts +8 -0
- package/src/share/scripts/tests/gsap-parallax.test.ts +8 -0
- package/src/share/scripts/tests/gsap-reveal.test.ts +8 -0
- package/src/share/scripts/tests/gsap-stagger.test.ts +8 -0
- package/src/share/scripts/tests/inline-number-animation.test.ts +8 -0
- package/src/share/scripts/tests/lenis.test.ts +8 -0
- package/src/share/scripts/tests/live-photos.test.ts +8 -0
- package/src/share/scripts/tests/lordicon.test.ts +8 -0
- package/src/share/scripts/tests/orchestrator.test.ts +8 -0
- package/src/share/scripts/tests/scheduler.test.ts +23 -0
- package/src/share/scripts/video-player.test.ts +210 -0
- package/src/share/scripts/video-player.ts +73 -13
- package/src/share/semantic/extract.ts +25 -1
- package/src/share/semantic/jsonld/tests/article.test.ts +89 -0
- package/src/share/semantic/jsonld/tests/breadcrumb.test.ts +62 -0
- package/src/share/semantic/jsonld/tests/collection-list.test.ts +39 -0
- package/src/share/semantic/jsonld/tests/faq.test.ts +60 -0
- package/src/share/semantic/jsonld/tests/initiative.test.ts +56 -0
- package/src/share/semantic/jsonld/tests/organization.test.ts +123 -0
- package/src/share/semantic/jsonld/tests/person.test.ts +89 -0
- package/src/share/semantic/jsonld/tests/service.test.ts +65 -0
- package/src/share/semantic/jsonld/tests/shared.test.ts +41 -0
- package/src/share/semantic/jsonld/tests/video.test.ts +84 -0
- package/src/share/semantic/jsonld/tests/webpage.test.ts +93 -0
- package/src/share/semantic/jsonld/tests/website.test.ts +27 -0
- package/src/share/semantic/jsonld/types.ts +1 -0
- package/src/share/semantic/models.ts +1 -0
- package/src/share/semantic/tests/ai.test.ts +55 -0
- package/src/share/semantic/tests/block-extraction.test.ts +34 -0
- package/src/share/semantic/tests/breadcrumbs.test.ts +127 -0
- package/src/share/semantic/tests/build-page.test.ts +122 -0
- package/src/share/semantic/tests/business-projection.test.ts +188 -0
- package/src/share/semantic/tests/derived-prices-loader.test.ts +46 -0
- package/src/share/semantic/tests/extract.test.ts +126 -0
- package/src/share/semantic/tests/feed.test.ts +113 -0
- package/src/share/semantic/tests/helpers.ts +32 -0
- package/src/share/semantic/tests/ids.test.ts +118 -0
- package/src/share/semantic/tests/image-sitemap.test.ts +109 -0
- package/src/share/semantic/tests/jsonld.test.ts +61 -0
- package/src/share/semantic/tests/llms-policy.test.ts +60 -0
- package/src/share/semantic/tests/llms.test.ts +118 -0
- package/src/share/semantic/tests/markdown-hygiene.test.ts +72 -0
- package/src/share/semantic/tests/markdown-page.test.ts +97 -0
- package/src/share/semantic/tests/markdown-twin-provenance.test.ts +152 -0
- package/src/share/semantic/tests/organization-profile.test.ts +107 -0
- package/src/share/semantic/tests/output-projection.test.ts +77 -0
- package/src/share/semantic/tests/page-markdown.test.ts +98 -0
- package/src/share/semantic/tests/page-utils.test.ts +88 -0
- package/src/share/semantic/tests/price-marker-resolver.test.ts +88 -0
- package/src/share/semantic/tests/robots.test.ts +70 -0
- package/src/share/semantic/tests/split-sentences.test.ts +57 -0
- package/src/share/semantic/tests/update-stamp.test.ts +102 -0
- package/src/share/tests/manifest.test.ts +51 -0
- package/src/share/tests/openapi.test.ts +30 -0
- package/src/share/types/qrcode-browser.d.ts +18 -0
- package/src/share/utility-registry.yaml +16 -2
- package/src/integration/crm-buffer.ts +0 -491
- package/src/integration-adapter-supabase-crm/adapter.ts +0 -437
- package/src/integration-adapter-supabase-crm/client.ts +0 -605
- package/src/integration-adapter-supabase-crm/index.ts +0 -28
- package/src/integration-adapter-supabase-crm/pipedrive-sync-target.ts +0 -482
- package/src/integration-adapter-supabase-crm/supabase-rest.ts +0 -89
- package/src/integration-adapter-supabase-crm/tenant-registry.ts +0 -334
- package/src/integration-adapter-supabase-crm/tests/change-balance.pbt.test.ts +0 -83
- package/src/integration-adapter-supabase-crm/tests/funnel-persistence.test.ts +0 -409
- package/src/integration-adapter-supabase-crm/tests/lifecycle-sync.test.ts +0 -288
- package/src/integration-adapter-supabase-crm/tests/stage-map.test.ts +0 -80
- package/src/integration-adapter-supabase-crm/vitest.config.ts +0 -8
- package/src/integration-adapter-supabase-crm/worker.ts +0 -223
- package/src/ontology/archetypes/components/currency-selector.yaml +0 -19
|
@@ -68,8 +68,32 @@ const SENTENCE_ABBREVIATIONS: Record<string, string[]> = {
|
|
|
68
68
|
"u.",
|
|
69
69
|
"vgl.",
|
|
70
70
|
"bspw.",
|
|
71
|
+
"zzgl.",
|
|
72
|
+
"inkl.",
|
|
73
|
+
"exkl.",
|
|
74
|
+
"bzw.",
|
|
75
|
+
"ggf.",
|
|
76
|
+
"ggfls.",
|
|
77
|
+
"sog.",
|
|
78
|
+
"o.Ä.",
|
|
79
|
+
],
|
|
80
|
+
uk: [
|
|
81
|
+
"т.д.",
|
|
82
|
+
"т.п.",
|
|
83
|
+
"п.",
|
|
84
|
+
"ст.",
|
|
85
|
+
"див.",
|
|
86
|
+
"пор.",
|
|
87
|
+
"напр.",
|
|
88
|
+
"ім.",
|
|
89
|
+
"о.",
|
|
90
|
+
"вкл.",
|
|
91
|
+
"тис.",
|
|
92
|
+
"млн.",
|
|
93
|
+
"млрд.",
|
|
94
|
+
"грн.",
|
|
95
|
+
"№",
|
|
71
96
|
],
|
|
72
|
-
uk: ["т.д.", "т.п.", "п.", "ст.", "див.", "пор.", "напр.", "ім.", "о."],
|
|
73
97
|
en: ["e.g.", "i.e.", "etc.", "vs.", "Mr.", "Mrs.", "Dr.", "Inc.", "Ltd."],
|
|
74
98
|
};
|
|
75
99
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildArticleNode } from "../article.ts";
|
|
3
|
+
import { createJsonLdContext } from "../context.ts";
|
|
4
|
+
import { makePage } from "../../tests/helpers.ts";
|
|
5
|
+
|
|
6
|
+
describe("buildArticleNode", () => {
|
|
7
|
+
it("returns null when page has no datePublished", () => {
|
|
8
|
+
const page = makePage();
|
|
9
|
+
const ctx = createJsonLdContext(page);
|
|
10
|
+
expect(buildArticleNode(ctx)).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("builds Article/BlogPosting node with datePublished", () => {
|
|
14
|
+
const page = makePage({ datePublished: "2024-01-15" });
|
|
15
|
+
const ctx = createJsonLdContext(page);
|
|
16
|
+
const node = buildArticleNode(ctx);
|
|
17
|
+
expect(node).not.toBeNull();
|
|
18
|
+
expect(node!["@type"]).toEqual(["Article", "BlogPosting"]);
|
|
19
|
+
expect(node!.datePublished).toBe("2024-01-15");
|
|
20
|
+
expect(node!.dateModified).toBe("2024-01-15");
|
|
21
|
+
expect(node!.headline).toBe("Test Heading");
|
|
22
|
+
expect(node!.inLanguage).toBe("de");
|
|
23
|
+
expect(node!.publisher).toEqual({ "@id": ctx.ids.organization });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("uses dateModified when provided", () => {
|
|
27
|
+
const page = makePage({ datePublished: "2024-01-15", dateModified: "2024-06-01" });
|
|
28
|
+
const ctx = createJsonLdContext(page);
|
|
29
|
+
const node = buildArticleNode(ctx);
|
|
30
|
+
expect(node!.dateModified).toBe("2024-06-01");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("emits author as Person when author string is present", () => {
|
|
34
|
+
const page = makePage({ datePublished: "2024-01-15", author: "Jane Doe" });
|
|
35
|
+
const ctx = createJsonLdContext(page);
|
|
36
|
+
const node = buildArticleNode(ctx);
|
|
37
|
+
expect(node!.author).toEqual({ "@type": "Person", name: "Jane Doe" });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("emits structured author from authorRecord", () => {
|
|
41
|
+
const page = makePage({
|
|
42
|
+
datePublished: "2024-01-15",
|
|
43
|
+
authorRecord: { name: "Jane Doe", contactUrl: "https://example.com/contact" },
|
|
44
|
+
});
|
|
45
|
+
const ctx = createJsonLdContext(page);
|
|
46
|
+
const node = buildArticleNode(ctx);
|
|
47
|
+
expect(node!.author).toEqual({
|
|
48
|
+
"@type": "Person",
|
|
49
|
+
name: "Jane Doe",
|
|
50
|
+
url: "https://example.com/contact",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("emits keywords as comma-separated string", () => {
|
|
55
|
+
const page = makePage({ datePublished: "2024-01-15", keywords: ["foo", "bar"] });
|
|
56
|
+
const ctx = createJsonLdContext(page);
|
|
57
|
+
const node = buildArticleNode(ctx);
|
|
58
|
+
expect(node!.keywords).toBe("foo, bar");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("emits image from primaryImage", () => {
|
|
62
|
+
const page = makePage({
|
|
63
|
+
datePublished: "2024-01-15",
|
|
64
|
+
primaryImage: { url: "https://example.com/img.jpg" },
|
|
65
|
+
});
|
|
66
|
+
const ctx = createJsonLdContext(page);
|
|
67
|
+
const node = buildArticleNode(ctx);
|
|
68
|
+
expect(node!.image).toBe("https://example.com/img.jpg");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("uses page URL as mainEntityOfPage for ratgeber depth-1", () => {
|
|
72
|
+
const page = makePage({
|
|
73
|
+
datePublished: "2024-01-15",
|
|
74
|
+
surfaceId: "ratgeber",
|
|
75
|
+
depth: 1,
|
|
76
|
+
url: "https://example.com/de/ratgeber/article/",
|
|
77
|
+
});
|
|
78
|
+
const ctx = createJsonLdContext(page);
|
|
79
|
+
const node = buildArticleNode(ctx);
|
|
80
|
+
expect(node!.mainEntityOfPage).toBe("https://example.com/de/ratgeber/article/");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("uses webpage @id as mainEntityOfPage for non-ratgeber pages", () => {
|
|
84
|
+
const page = makePage({ datePublished: "2024-01-15" });
|
|
85
|
+
const ctx = createJsonLdContext(page);
|
|
86
|
+
const node = buildArticleNode(ctx);
|
|
87
|
+
expect(node!.mainEntityOfPage).toEqual({ "@id": ctx.webpageId });
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildBreadcrumbNode } from "../breadcrumb.ts";
|
|
3
|
+
import { createJsonLdContext } from "../context.ts";
|
|
4
|
+
import { makePage } from "../../tests/helpers.ts";
|
|
5
|
+
|
|
6
|
+
describe("buildBreadcrumbNode", () => {
|
|
7
|
+
it("returns null when only one breadcrumb", () => {
|
|
8
|
+
const page = makePage({
|
|
9
|
+
breadcrumbs: [{ name: "Home", url: "https://example.com/de/" }],
|
|
10
|
+
});
|
|
11
|
+
const ctx = createJsonLdContext(page);
|
|
12
|
+
expect(buildBreadcrumbNode(ctx)).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("builds BreadcrumbList with multiple crumbs", () => {
|
|
16
|
+
const page = makePage({
|
|
17
|
+
breadcrumbs: [
|
|
18
|
+
{ name: "Home", url: "https://example.com/de/" },
|
|
19
|
+
{ name: "About", url: "https://example.com/de/about/" },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
const ctx = createJsonLdContext(page);
|
|
23
|
+
const node = buildBreadcrumbNode(ctx);
|
|
24
|
+
expect(node).not.toBeNull();
|
|
25
|
+
expect(node!["@type"]).toBe("BreadcrumbList");
|
|
26
|
+
expect(node!.itemListElement).toHaveLength(2);
|
|
27
|
+
expect(node as Record<string, unknown>).toMatchObject({
|
|
28
|
+
itemListElement: [
|
|
29
|
+
{ position: 1, name: "Home" },
|
|
30
|
+
{ position: 2, name: "About" },
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("resolves relative crumb URLs against page URL", () => {
|
|
36
|
+
const page = makePage({
|
|
37
|
+
breadcrumbs: [
|
|
38
|
+
{ name: "Home", url: "/de/" },
|
|
39
|
+
{ name: "About", url: "/de/about/" },
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
const ctx = createJsonLdContext(page);
|
|
43
|
+
const node = buildBreadcrumbNode(ctx);
|
|
44
|
+
const items = (node as Record<string, unknown[]>).itemListElement as Record<string, unknown>[];
|
|
45
|
+
expect(items[0].item).toBe("https://example.com/de/");
|
|
46
|
+
expect(items[1].item).toBe("https://example.com/de/about/");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("assigns position-scoped @id to each crumb", () => {
|
|
50
|
+
const page = makePage({
|
|
51
|
+
breadcrumbs: [
|
|
52
|
+
{ name: "Home", url: "https://example.com/de/" },
|
|
53
|
+
{ name: "About", url: "https://example.com/de/about/" },
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
const ctx = createJsonLdContext(page);
|
|
57
|
+
const node = buildBreadcrumbNode(ctx);
|
|
58
|
+
const items = (node as Record<string, unknown[]>).itemListElement as Record<string, unknown>[];
|
|
59
|
+
expect(items[0]["@id"]).toBe(`${ctx.breadcrumbId}/1`);
|
|
60
|
+
expect(items[1]["@id"]).toBe(`${ctx.breadcrumbId}/2`);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildCollectionListNode } from "../collection-list.ts";
|
|
3
|
+
import { createJsonLdContext } from "../context.ts";
|
|
4
|
+
import { makePage } from "../../tests/helpers.ts";
|
|
5
|
+
|
|
6
|
+
describe("buildCollectionListNode", () => {
|
|
7
|
+
it("returns null when page is not collection type", () => {
|
|
8
|
+
const page = makePage({ type: "content" });
|
|
9
|
+
const ctx = createJsonLdContext(page);
|
|
10
|
+
expect(buildCollectionListNode(ctx)).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns null when collection has no items", () => {
|
|
14
|
+
const page = makePage({ type: "collection", collectionItems: [] });
|
|
15
|
+
const ctx = createJsonLdContext(page);
|
|
16
|
+
expect(buildCollectionListNode(ctx)).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("builds ItemList for collection page with items", () => {
|
|
20
|
+
const page = makePage({
|
|
21
|
+
type: "collection",
|
|
22
|
+
collectionItems: [
|
|
23
|
+
{ url: "https://example.com/de/industry-a/", name: "Industry A" },
|
|
24
|
+
{ url: "https://example.com/de/industry-b/", name: "Industry B" },
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
const ctx = createJsonLdContext(page);
|
|
28
|
+
const node = buildCollectionListNode(ctx);
|
|
29
|
+
expect(node).not.toBeNull();
|
|
30
|
+
expect(node!["@type"]).toBe("ItemList");
|
|
31
|
+
expect(node!.itemListElement).toHaveLength(2);
|
|
32
|
+
const items = (node as Record<string, unknown[]>).itemListElement as Record<string, unknown>[];
|
|
33
|
+
expect(items[0]).toMatchObject({
|
|
34
|
+
position: 1,
|
|
35
|
+
url: "https://example.com/de/industry-a/",
|
|
36
|
+
name: "Industry A",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildFaqNodes, buildFaqPageNode } from "../faq.ts";
|
|
3
|
+
import { createJsonLdContext } from "../context.ts";
|
|
4
|
+
import { makePage } from "../../tests/helpers.ts";
|
|
5
|
+
|
|
6
|
+
describe("buildFaqPageNode", () => {
|
|
7
|
+
it("returns null when no faqEntries", () => {
|
|
8
|
+
const page = makePage();
|
|
9
|
+
const ctx = createJsonLdContext(page);
|
|
10
|
+
expect(buildFaqPageNode(ctx)).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns null when faqEntries is empty", () => {
|
|
14
|
+
const page = makePage({ faqEntries: [] });
|
|
15
|
+
const ctx = createJsonLdContext(page);
|
|
16
|
+
expect(buildFaqPageNode(ctx)).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("builds FAQPage with Question nodes", () => {
|
|
20
|
+
const page = makePage({
|
|
21
|
+
faqEntries: [
|
|
22
|
+
{ id: "q1", question: "What is this?", answer: "A test." },
|
|
23
|
+
{ id: "q2", question: "How much?", answer: "Free." },
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
const ctx = createJsonLdContext(page);
|
|
27
|
+
const node = buildFaqPageNode(ctx);
|
|
28
|
+
expect(node).not.toBeNull();
|
|
29
|
+
expect(node!["@type"]).toBe("FAQPage");
|
|
30
|
+
expect(node!.mainEntity).toHaveLength(2);
|
|
31
|
+
const entities = (node as Record<string, unknown[]>).mainEntity as Record<string, unknown>[];
|
|
32
|
+
expect(entities[0]).toEqual({
|
|
33
|
+
"@type": "Question",
|
|
34
|
+
name: "What is this?",
|
|
35
|
+
acceptedAnswer: { "@type": "Answer", text: "A test." },
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("buildFaqNodes", () => {
|
|
41
|
+
it("suppresses FAQPage on ratgeber depth-1 article pages", () => {
|
|
42
|
+
const page = makePage({
|
|
43
|
+
surfaceId: "ratgeber",
|
|
44
|
+
depth: 1,
|
|
45
|
+
faqEntries: [{ id: "q1", question: "Q?", answer: "A." }],
|
|
46
|
+
});
|
|
47
|
+
const ctx = createJsonLdContext(page);
|
|
48
|
+
expect(buildFaqNodes(ctx)).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("emits FAQPage for non-ratgeber pages with FAQ entries", () => {
|
|
52
|
+
const page = makePage({
|
|
53
|
+
faqEntries: [{ id: "q1", question: "Q?", answer: "A." }],
|
|
54
|
+
});
|
|
55
|
+
const ctx = createJsonLdContext(page);
|
|
56
|
+
const nodes = buildFaqNodes(ctx);
|
|
57
|
+
expect(nodes).toHaveLength(1);
|
|
58
|
+
expect(nodes[0]["@type"]).toBe("FAQPage");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildInitiativeNodes, buildInitiativesListNode } from "../initiative.ts";
|
|
3
|
+
import { createJsonLdContext } from "../context.ts";
|
|
4
|
+
import { makePage } from "../../tests/helpers.ts";
|
|
5
|
+
|
|
6
|
+
describe("buildInitiativeNodes", () => {
|
|
7
|
+
it("returns empty array when no initiatives", () => {
|
|
8
|
+
const page = makePage();
|
|
9
|
+
const ctx = createJsonLdContext(page);
|
|
10
|
+
expect(buildInitiativeNodes(ctx)).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("builds Thing/Project nodes for each initiative", () => {
|
|
14
|
+
const page = makePage({
|
|
15
|
+
initiatives: [
|
|
16
|
+
{ id: "i1", name: "Project Alpha", summary: "First project" },
|
|
17
|
+
{ id: "i2", name: "Project Beta", summary: "Second project" },
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
const ctx = createJsonLdContext(page);
|
|
21
|
+
const nodes = buildInitiativeNodes(ctx);
|
|
22
|
+
expect(nodes).toHaveLength(2);
|
|
23
|
+
expect(nodes[0]["@type"]).toEqual(["Thing", "Project"]);
|
|
24
|
+
expect(nodes[0].name).toBe("Project Alpha");
|
|
25
|
+
expect(nodes[0].description).toBe("First project");
|
|
26
|
+
expect(nodes[1].name).toBe("Project Beta");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("buildInitiativesListNode", () => {
|
|
31
|
+
it("returns null when no initiatives", () => {
|
|
32
|
+
const page = makePage();
|
|
33
|
+
const ctx = createJsonLdContext(page);
|
|
34
|
+
expect(buildInitiativesListNode(ctx)).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("builds ItemList with positioned initiatives", () => {
|
|
38
|
+
const page = makePage({
|
|
39
|
+
initiatives: [
|
|
40
|
+
{ id: "i1", name: "Project Alpha", summary: "First" },
|
|
41
|
+
{ id: "i2", name: "Project Beta", summary: "Second" },
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
const ctx = createJsonLdContext(page);
|
|
45
|
+
const node = buildInitiativesListNode(ctx);
|
|
46
|
+
expect(node).not.toBeNull();
|
|
47
|
+
expect(node!["@type"]).toBe("ItemList");
|
|
48
|
+
expect(node!.itemListElement).toHaveLength(2);
|
|
49
|
+
expect(node as Record<string, unknown>).toMatchObject({
|
|
50
|
+
itemListElement: [
|
|
51
|
+
{ position: 1, name: "Project Alpha" },
|
|
52
|
+
{ position: 2, name: "Project Beta" },
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildOrganizationNode } from "../organization.ts";
|
|
3
|
+
import { createJsonLdContext } from "../context.ts";
|
|
4
|
+
import { makePage } from "../../tests/helpers.ts";
|
|
5
|
+
|
|
6
|
+
describe("buildOrganizationNode", () => {
|
|
7
|
+
it("builds Organization node with default schemaType", () => {
|
|
8
|
+
const page = makePage();
|
|
9
|
+
const ctx = createJsonLdContext(page);
|
|
10
|
+
const node = buildOrganizationNode(ctx);
|
|
11
|
+
expect(node["@type"]).toEqual(["Organization", "NGO"]);
|
|
12
|
+
expect(node["@id"]).toBe(ctx.ids.organization);
|
|
13
|
+
expect(node.name).toBe("Warpgogol");
|
|
14
|
+
expect(node.description).toBe("Digital infrastructure for nonprofits");
|
|
15
|
+
expect(node.url).toBe("https://example.com");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("uses custom schemaType when provided", () => {
|
|
19
|
+
const page = makePage({
|
|
20
|
+
organization: makePage().organization,
|
|
21
|
+
});
|
|
22
|
+
page.organization.schemaType = ["Organization", "ProfessionalService"];
|
|
23
|
+
const ctx = createJsonLdContext(page);
|
|
24
|
+
const node = buildOrganizationNode(ctx);
|
|
25
|
+
expect(node["@type"]).toEqual(["Organization", "ProfessionalService"]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("emits legalName, email, foundingYear, registration when present", () => {
|
|
29
|
+
const page = makePage({
|
|
30
|
+
organization: makePage().organization,
|
|
31
|
+
});
|
|
32
|
+
page.organization.legalName = "Warpgogol GmbH";
|
|
33
|
+
page.organization.email = "info@example.com";
|
|
34
|
+
page.organization.foundingYear = "2020";
|
|
35
|
+
page.organization.registration = "HRB 12345";
|
|
36
|
+
const ctx = createJsonLdContext(page);
|
|
37
|
+
const node = buildOrganizationNode(ctx);
|
|
38
|
+
expect(node.legalName).toBe("Warpgogol GmbH");
|
|
39
|
+
expect(node.email).toBe("info@example.com");
|
|
40
|
+
expect(node.foundingDate).toBe("2020");
|
|
41
|
+
expect(node.identifier).toBe("HRB 12345");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("emits address when present", () => {
|
|
45
|
+
const page = makePage({
|
|
46
|
+
organization: makePage().organization,
|
|
47
|
+
});
|
|
48
|
+
page.organization.address = {
|
|
49
|
+
streetAddress: "Test Str. 1",
|
|
50
|
+
postalCode: "12345",
|
|
51
|
+
addressLocality: "Berlin",
|
|
52
|
+
addressCountry: "DE",
|
|
53
|
+
};
|
|
54
|
+
const ctx = createJsonLdContext(page);
|
|
55
|
+
const node = buildOrganizationNode(ctx);
|
|
56
|
+
expect(node.address).toEqual({
|
|
57
|
+
"@type": "PostalAddress",
|
|
58
|
+
streetAddress: "Test Str. 1",
|
|
59
|
+
postalCode: "12345",
|
|
60
|
+
addressLocality: "Berlin",
|
|
61
|
+
addressCountry: "DE",
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("emits contactPoints when present", () => {
|
|
66
|
+
const page = makePage({
|
|
67
|
+
organization: makePage().organization,
|
|
68
|
+
});
|
|
69
|
+
page.organization.contactPoints = [
|
|
70
|
+
{ contactType: "customer service", email: "support@example.com" },
|
|
71
|
+
];
|
|
72
|
+
const ctx = createJsonLdContext(page);
|
|
73
|
+
const node = buildOrganizationNode(ctx);
|
|
74
|
+
expect(node.contactPoint).toHaveLength(1);
|
|
75
|
+
expect((node.contactPoint as unknown[])[0]).toMatchObject({
|
|
76
|
+
"@type": "ContactPoint",
|
|
77
|
+
contactType: "customer service",
|
|
78
|
+
email: "support@example.com",
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("emits sameAs links", () => {
|
|
83
|
+
const page = makePage({
|
|
84
|
+
organization: makePage().organization,
|
|
85
|
+
});
|
|
86
|
+
page.organization.sameAs = ["https://twitter.com/example", "https://github.com/example"];
|
|
87
|
+
const ctx = createJsonLdContext(page);
|
|
88
|
+
const node = buildOrganizationNode(ctx);
|
|
89
|
+
expect(node.sameAs).toEqual(["https://twitter.com/example", "https://github.com/example"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("emits logo and image", () => {
|
|
93
|
+
const page = makePage({
|
|
94
|
+
organization: makePage().organization,
|
|
95
|
+
});
|
|
96
|
+
page.organization.logo = "https://example.com/logo.png";
|
|
97
|
+
page.organization.image = "https://example.com/image.png";
|
|
98
|
+
const ctx = createJsonLdContext(page);
|
|
99
|
+
const node = buildOrganizationNode(ctx);
|
|
100
|
+
expect(node.logo).toEqual({ "@type": "ImageObject", url: "https://example.com/logo.png" });
|
|
101
|
+
expect(node.image).toBe("https://example.com/image.png");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("emits founders as Person references", () => {
|
|
105
|
+
const page = makePage({
|
|
106
|
+
organization: makePage().organization,
|
|
107
|
+
});
|
|
108
|
+
page.organization.founders = [{ name: "Jane Doe" }];
|
|
109
|
+
const ctx = createJsonLdContext(page);
|
|
110
|
+
const node = buildOrganizationNode(ctx);
|
|
111
|
+
expect(node.founder).toEqual([{ "@id": ctx.ids.person("Jane Doe") }]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("emits areaServed when present", () => {
|
|
115
|
+
const page = makePage({
|
|
116
|
+
organization: makePage().organization,
|
|
117
|
+
});
|
|
118
|
+
page.organization.areaServed = ["Berlin", "Brandenburg"];
|
|
119
|
+
const ctx = createJsonLdContext(page);
|
|
120
|
+
const node = buildOrganizationNode(ctx);
|
|
121
|
+
expect(node.areaServed).toEqual(["Berlin", "Brandenburg"]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildPersonNodes } from "../person.ts";
|
|
3
|
+
import { createJsonLdContext } from "../context.ts";
|
|
4
|
+
import { makePage } from "../../tests/helpers.ts";
|
|
5
|
+
|
|
6
|
+
describe("buildPersonNodes", () => {
|
|
7
|
+
it("returns empty array when no people, founders, or boardMembers", () => {
|
|
8
|
+
const page = makePage();
|
|
9
|
+
const ctx = createJsonLdContext(page);
|
|
10
|
+
expect(buildPersonNodes(ctx)).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("builds Person nodes from page.people", () => {
|
|
14
|
+
const page = makePage({
|
|
15
|
+
people: [{ name: "Jane Doe", role: "CEO", description: "A leader" }],
|
|
16
|
+
});
|
|
17
|
+
const ctx = createJsonLdContext(page);
|
|
18
|
+
const nodes = buildPersonNodes(ctx);
|
|
19
|
+
expect(nodes).toHaveLength(1);
|
|
20
|
+
expect(nodes[0]["@type"]).toBe("Person");
|
|
21
|
+
expect(nodes[0].name).toBe("Jane Doe");
|
|
22
|
+
expect(nodes[0].jobTitle).toBe("CEO");
|
|
23
|
+
expect(nodes[0].description).toBe("A leader");
|
|
24
|
+
expect(nodes[0].worksFor).toEqual({ "@id": ctx.ids.organization });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("merges founders, boardMembers, and people with deduplication by name", () => {
|
|
28
|
+
const page = makePage({
|
|
29
|
+
organization: makePage().organization,
|
|
30
|
+
people: [{ name: "Jane Doe", role: "CEO" }],
|
|
31
|
+
});
|
|
32
|
+
page.organization.founders = [{ name: "Jane Doe", role: "Founder" }];
|
|
33
|
+
page.organization.boardMembers = [{ name: "John Smith", role: "Board Member" }];
|
|
34
|
+
const ctx = createJsonLdContext(page);
|
|
35
|
+
const nodes = buildPersonNodes(ctx);
|
|
36
|
+
expect(nodes).toHaveLength(2);
|
|
37
|
+
const names = nodes.map((n) => n.name);
|
|
38
|
+
expect(names).toContain("Jane Doe");
|
|
39
|
+
expect(names).toContain("John Smith");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("emits extended fields (address, knowsAbout, affiliation)", () => {
|
|
43
|
+
const page = makePage({
|
|
44
|
+
people: [
|
|
45
|
+
{
|
|
46
|
+
name: "Jane Doe",
|
|
47
|
+
address: { addressLocality: "Berlin", addressCountry: "DE" },
|
|
48
|
+
knowsAbout: ["AI", "Cloud"],
|
|
49
|
+
affiliation: { name: "Warpgogol", url: "https://example.com" },
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
const ctx = createJsonLdContext(page);
|
|
54
|
+
const nodes = buildPersonNodes(ctx);
|
|
55
|
+
expect(nodes[0].address).toEqual({
|
|
56
|
+
"@type": "PostalAddress",
|
|
57
|
+
addressLocality: "Berlin",
|
|
58
|
+
addressCountry: "DE",
|
|
59
|
+
});
|
|
60
|
+
expect(nodes[0].knowsAbout).toEqual(["AI", "Cloud"]);
|
|
61
|
+
expect(nodes[0].affiliation).toEqual({
|
|
62
|
+
"@type": "Organization",
|
|
63
|
+
name: "Warpgogol",
|
|
64
|
+
url: "https://example.com",
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("emits birthDate, deathDate, image, sameAs, profileUrl", () => {
|
|
69
|
+
const page = makePage({
|
|
70
|
+
people: [
|
|
71
|
+
{
|
|
72
|
+
name: "Jane Doe",
|
|
73
|
+
birthDate: "1980",
|
|
74
|
+
deathDate: "2024",
|
|
75
|
+
image: "https://example.com/jane.jpg",
|
|
76
|
+
sameAs: ["https://linkedin.com/in/jane"],
|
|
77
|
+
profileUrl: "https://example.com/de/team/jane/",
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
const ctx = createJsonLdContext(page);
|
|
82
|
+
const nodes = buildPersonNodes(ctx);
|
|
83
|
+
expect(nodes[0].birthDate).toBe("1980");
|
|
84
|
+
expect(nodes[0].deathDate).toBe("2024");
|
|
85
|
+
expect(nodes[0].image).toBe("https://example.com/jane.jpg");
|
|
86
|
+
expect(nodes[0].sameAs).toEqual(["https://linkedin.com/in/jane"]);
|
|
87
|
+
expect(nodes[0].url).toBe("https://example.com/de/team/jane/");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildServiceNodes } from "../service.ts";
|
|
3
|
+
import { createJsonLdContext } from "../context.ts";
|
|
4
|
+
import { makePage } from "../../tests/helpers.ts";
|
|
5
|
+
|
|
6
|
+
describe("buildServiceNodes", () => {
|
|
7
|
+
it("returns org-level Service nodes for non-surface pages", () => {
|
|
8
|
+
const page = makePage({
|
|
9
|
+
organization: makePage().organization,
|
|
10
|
+
});
|
|
11
|
+
page.organization.services = [
|
|
12
|
+
{ id: "s1", name: "Hosting", description: "Managed hosting" },
|
|
13
|
+
{ id: "s2", name: "Security" },
|
|
14
|
+
];
|
|
15
|
+
const ctx = createJsonLdContext(page);
|
|
16
|
+
const nodes = buildServiceNodes(ctx);
|
|
17
|
+
expect(nodes).toHaveLength(2);
|
|
18
|
+
expect(nodes[0]["@type"]).toBe("Service");
|
|
19
|
+
expect(nodes[0].name).toBe("Hosting");
|
|
20
|
+
expect(nodes[0].description).toBe("Managed hosting");
|
|
21
|
+
expect(nodes[0].provider).toEqual({ "@id": ctx.ids.organization });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("emits industry-specific Service node when industryService is present", () => {
|
|
25
|
+
const page = makePage({
|
|
26
|
+
surfaceId: "website-local",
|
|
27
|
+
depth: 1,
|
|
28
|
+
industryService: {
|
|
29
|
+
serviceType: "Web Development",
|
|
30
|
+
audience: "Nonprofits",
|
|
31
|
+
description: "Custom web development",
|
|
32
|
+
areaServed: "Berlin",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const ctx = createJsonLdContext(page);
|
|
36
|
+
const nodes = buildServiceNodes(ctx);
|
|
37
|
+
expect(nodes).toHaveLength(1);
|
|
38
|
+
expect(nodes[0]["@type"]).toBe("Service");
|
|
39
|
+
expect(nodes[0].serviceType).toBe("Web Development");
|
|
40
|
+
expect(nodes[0].audience).toEqual({ "@type": "Audience", name: "Nonprofits" });
|
|
41
|
+
expect(nodes[0].areaServed).toEqual({ "@type": "City", name: "Berlin" });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("suppresses Service nodes for surface pages without industryService at prohibited depths", () => {
|
|
45
|
+
const page = makePage({
|
|
46
|
+
surfaceId: "website-local",
|
|
47
|
+
depth: 0,
|
|
48
|
+
});
|
|
49
|
+
const ctx = createJsonLdContext(page);
|
|
50
|
+
expect(buildServiceNodes(ctx)).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("emits org-level Service nodes for surface depth-1 without industryService", () => {
|
|
54
|
+
const page = makePage({
|
|
55
|
+
surfaceId: "website-local",
|
|
56
|
+
depth: 1,
|
|
57
|
+
organization: makePage().organization,
|
|
58
|
+
});
|
|
59
|
+
page.organization.services = [{ id: "s1", name: "Consulting" }];
|
|
60
|
+
const ctx = createJsonLdContext(page);
|
|
61
|
+
const nodes = buildServiceNodes(ctx);
|
|
62
|
+
expect(nodes).toHaveLength(1);
|
|
63
|
+
expect(nodes[0].name).toBe("Consulting");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { dedupeGraph } from "../shared.ts";
|
|
3
|
+
import type { JsonLdNode } from "../types.ts";
|
|
4
|
+
|
|
5
|
+
describe("dedupeGraph", () => {
|
|
6
|
+
it("returns empty array for empty input", () => {
|
|
7
|
+
expect(dedupeGraph([])).toEqual([]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("removes duplicate nodes by @id", () => {
|
|
11
|
+
const nodes: JsonLdNode[] = [
|
|
12
|
+
{ "@id": "org-1", "@type": "Organization", name: "Org" },
|
|
13
|
+
{ "@id": "org-1", "@type": "Organization", name: "Duplicate" },
|
|
14
|
+
{ "@id": "org-2", "@type": "Organization", name: "Other" },
|
|
15
|
+
];
|
|
16
|
+
const result = dedupeGraph(nodes);
|
|
17
|
+
expect(result).toHaveLength(2);
|
|
18
|
+
expect(result[0]["@id"]).toBe("org-1");
|
|
19
|
+
expect(result[0].name).toBe("Org");
|
|
20
|
+
expect(result[1]["@id"]).toBe("org-2");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("preserves first occurrence of duplicates", () => {
|
|
24
|
+
const nodes: JsonLdNode[] = [
|
|
25
|
+
{ "@id": "a", value: "first" },
|
|
26
|
+
{ "@id": "a", value: "second" },
|
|
27
|
+
];
|
|
28
|
+
const result = dedupeGraph(nodes);
|
|
29
|
+
expect(result).toHaveLength(1);
|
|
30
|
+
expect(result[0].value).toBe("first");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("keeps nodes without @id", () => {
|
|
34
|
+
const nodes: JsonLdNode[] = [
|
|
35
|
+
{ "@type": "Thing", name: "No ID" },
|
|
36
|
+
{ "@type": "Thing", name: "Also No ID" },
|
|
37
|
+
];
|
|
38
|
+
const result = dedupeGraph(nodes);
|
|
39
|
+
expect(result).toHaveLength(2);
|
|
40
|
+
});
|
|
41
|
+
});
|