1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 decodeThirdPartyLicenseManifest,
5 filterThirdPartyLicenseEntries,
6 findThirdPartyLicenseEntry,
7 formatLicenseBundles,
8 thirdPartyLicenseEntryKey,
9 type ThirdPartyLicenseEntry,
10} from "./thirdPartyLicenses.js";
11
12const ENTRIES: ReadonlyArray<ThirdPartyLicenseEntry> = [
13 {
14 bundles: ["web"],
15 kind: "package",
16 license: "MIT",
17 name: "react",
18 noticeText: "React license",
19 sourceUrl: "https://react.dev",
20 version: "19.2.6",
21 },
22 {
23 bundles: ["assets", "mobile"],
24 kind: "custom",
25 license: "CC-BY-4.0",
26 name: "sample-icons",
27 noticeText: "Icon notice",
28 sourceUrl: null,
29 version: null,
30 },
31];
32
33describe("third-party license manifests", () => {
34 it("decodes the generated manifest shape", () => {
35 expect(decodeThirdPartyLicenseManifest({ schemaVersion: 1, entries: ENTRIES })).toEqual({
36 schemaVersion: 1,
37 entries: ENTRIES,
38 });
39 });
40
41 it("rejects unsupported manifest versions", () => {
42 expect(() => decodeThirdPartyLicenseManifest({ schemaVersion: 2, entries: [] })).toThrow(
43 "unsupported format",
44 );
45 });
46
47 it("rejects unsafe source links", () => {
48 expect(() =>
49 decodeThirdPartyLicenseManifest({
50 schemaVersion: 1,
51 entries: [{ ...ENTRIES[0]!, sourceUrl: "javascript:alert(1)" }],
52 }),
53 ).toThrow("invalid shape");
54 });
55
56 it("rejects duplicate navigation keys", () => {
57 expect(() =>
58 decodeThirdPartyLicenseManifest({ schemaVersion: 1, entries: [ENTRIES[0]!, ENTRIES[0]!] }),
59 ).toThrow("duplicate entry");
60 });
61
62 it("filters by package, license, version, and bundle", () => {
63 expect(filterThirdPartyLicenseEntries(ENTRIES, "react 19.2")).toEqual([ENTRIES[0]]);
64 expect(filterThirdPartyLicenseEntries(ENTRIES, "cc-by mobile")).toEqual([ENTRIES[1]]);
65 expect(filterThirdPartyLicenseEntries(ENTRIES, "apache")).toEqual([]);
66 });
67
68 it("formats platform bundle names for display", () => {
69 expect(
70 formatLicenseBundles(["android", "assets", "ios", "mobile", "plugin", "constructor"]),
71 ).toBe("Android, Assets, iOS, Mobile, plugin, constructor");
72 });
73
74 it("finds an entry by its stable navigation key", () => {
75 const entry = ENTRIES[0]!;
76 expect(findThirdPartyLicenseEntry(ENTRIES, thirdPartyLicenseEntryKey(entry))).toBe(entry);
77 });
78
79 it("uses path-safe navigation keys for scoped packages", () => {
80 expect(thirdPartyLicenseEntryKey({ ...ENTRIES[0]!, name: "@scope/package" })).not.toContain(
81 "/",
82 );
83 });
84
85 it("uses distinct navigation keys when names and versions contain delimiters", () => {
86 const first = thirdPartyLicenseEntryKey({ ...ENTRIES[0]!, name: "a:1", version: null });
87 const second = thirdPartyLicenseEntryKey({ ...ENTRIES[0]!, name: "a", version: "1:custom" });
88
89 expect(first).not.toBe(second);
90 });
91});