1import { describe, expect, it } from "vite-plus/test";
2
3import { normalizeDesktopUpdateReleaseNotes } from "./releaseNotes.ts";
4
5describe("normalizeDesktopUpdateReleaseNotes", () => {
6 it("shows the newest changes and counts all real changes", () => {
7 const result = normalizeDesktopUpdateReleaseNotes(
8 [
9 "- feat: first change",
10 "- fix: second change",
11 "- fix: third change",
12 "- fix: fourth change",
13 "- fix: fifth change",
14 "- fix: sixth change",
15 "- fix: seventh change",
16 "- fix: eighth change",
17 "- fix(web): keep long task drawers usable on small screens by @human in #8313",
18 "- fix(opencode): handle child approvals, stops, and model catalogs by @human in #8480",
19 "## New Contributors",
20 "- @human made their first contribution in #8435",
21 "**Full Changelog**: https://github.com/pingdotgg/t3code/compare/old...new",
22 ].join("\n"),
23 "0.0.36-nightly.20260828.1213",
24 "nightly",
25 );
26
27 expect(result).toEqual({
28 releaseNotes: [
29 {
30 version: "0.0.36-nightly.20260828.1213",
31 items: [
32 "fix(opencode): handle child approvals, stops, and model catalogs by @human in #8480",
33 "fix(web): keep long task drawers usable on small screens by @human in #8313",
34 "fix: eighth change",
35 "fix: seventh change",
36 "fix: sixth change",
37 "fix: fifth change",
38 "fix: fourth change",
39 "fix: third change",
40 ],
41 totalItems: 10,
42 },
43 ],
44 omittedReleaseCount: 0,
45 });
46 });
47
48 it("excludes a GitHub HTML contributor section", () => {
49 const result = normalizeDesktopUpdateReleaseNotes(
50 "<h2>What's Changed</h2><ul><li>Older fix</li><li>Newer fix</li></ul>" +
51 "<h2>New Contributors</h2><ul><li>@human made their first contribution</li></ul>" +
52 "<h2>Full Changelog</h2>",
53 "1.2.3",
54 "latest",
55 );
56
57 expect(result).toEqual({
58 releaseNotes: [{ version: "1.2.3", items: ["Newer fix", "Older fix"], totalItems: 2 }],
59 omittedReleaseCount: 0,
60 });
61 });
62
63 it("does not count Markdown or HTML section headings as changes", () => {
64 const changes = Array.from({ length: 8 }, (_, index) => `Change ${index + 1}`);
65 const result = normalizeDesktopUpdateReleaseNotes(
66 [
67 { version: "1.2.4", note: ["### Features", ...changes].join("\n- ") },
68 {
69 version: "1.2.3",
70 note: `<h3>Fixes</h3><ul>${changes.map((change) => `<li>${change}</li>`).join("")}</ul>`,
71 },
72 ],
73 "1.2.4",
74 "latest",
75 );
76
77 expect(result.releaseNotes).toEqual([
78 { version: "1.2.4", items: changes.toReversed(), totalItems: 8 },
79 { version: "1.2.3", items: changes.toReversed(), totalItems: 8 },
80 ]);
81 });
82
83 it("keeps per-version order and drops empty groups", () => {
84 const result = normalizeDesktopUpdateReleaseNotes(
85 [
86 { version: "1.2.3", note: "- Newer release" },
87 { version: "1.2.2", note: "Full changelog: https://example.com/compare/x...y" },
88 { version: "1.2.1", note: "- Older release" },
89 ],
90 "1.2.3",
91 "latest",
92 );
93
94 expect(result).toEqual({
95 releaseNotes: [
96 { version: "1.2.3", items: ["Newer release"], totalItems: 1 },
97 { version: "1.2.1", items: ["Older release"], totalItems: 1 },
98 ],
99 omittedReleaseCount: 0,
100 });
101 });
102
103 it("drops releases from other trains before grouping on nightly", () => {
104 // electron-updater's full changelog is "every version above the running
105 // one", and preview sorts above nightly, so the preview cuts come first.
106 const releaseNotes = [
107 { version: "0.0.41-preview.20260914.1683", note: "- Maintainer test build" },
108 { version: "0.0.41-preview.20260913.1669", note: "- Maintainer test build" },
109 { version: "0.0.41-nightly.20260914.1707", note: "- Nightly change 2" },
110 { version: "0.0.41-nightly.20260914.1700", note: "- Nightly change 1" },
111 ];
112
113 const result = normalizeDesktopUpdateReleaseNotes(
114 releaseNotes,
115 "0.0.41-nightly.20260914.1707",
116 "nightly",
117 );
118
119 expect(result.releaseNotes.map(({ version }) => version)).toEqual([
120 "0.0.41-nightly.20260914.1707",
121 "0.0.41-nightly.20260914.1700",
122 ]);
123 expect(result.omittedReleaseCount).toBe(0);
124 });
125
126 it("keeps only stable releases on the latest channel", () => {
127 const result = normalizeDesktopUpdateReleaseNotes(
128 [
129 { version: "0.0.42", note: "- Stable change" },
130 { version: "0.0.42-nightly.20260915.1710", note: "- Nightly change" },
131 ],
132 "0.0.42",
133 "latest",
134 );
135
136 expect(result.releaseNotes.map(({ version }) => version)).toEqual(["0.0.42"]);
137 });
138
139 it("counts valid groups before applying the six-release limit", () => {
140 const releaseNotes = [
141 { version: "1.3.9", note: "- Change 9" },
142 { version: "1.3.8", note: "Full changelog: https://example.com/compare/x...y" },
143 { version: "1.3.7", note: "- Change 7" },
144 { version: "1.3.6", note: "- Change 6" },
145 { version: "1.3.5", note: "- Change 5" },
146 { version: "1.3.4", note: "- Change 4" },
147 { version: "1.3.3", note: "- Change 3" },
148 { version: "1.3.2", note: "- Change 2" },
149 ];
150
151 const result = normalizeDesktopUpdateReleaseNotes(releaseNotes, "1.3.9", "latest");
152
153 expect(result.releaseNotes.map(({ version }) => version)).toEqual([
154 "1.3.9",
155 "1.3.7",
156 "1.3.6",
157 "1.3.5",
158 "1.3.4",
159 "1.3.3",
160 ]);
161 expect(result.omittedReleaseCount).toBe(1);
162 });
163
164 it("decodes valid HTML entities", () => {
165 const result = normalizeDesktopUpdateReleaseNotes(
166 "- Fix &amp; polish &#128512;",
167 "1.0.0",
168 "latest",
169 );
170 expect(result).toEqual({
171 releaseNotes: [{ version: "1.0.0", items: ["Fix & polish 😀"], totalItems: 1 }],
172 omittedReleaseCount: 0,
173 });
174 });
175
176 it("ignores malformed and empty entries instead of throwing", () => {
177 const result = normalizeDesktopUpdateReleaseNotes(
178 [
179 { version: "1.2.3", note: "- Valid change" },
180 { version: "1.2.2", note: "" },
181 { version: 42, note: "- Bad version type" },
182 { version: "1.2.1", note: { html: "<p>object note</p>" } },
183 "not an object",
184 null,
185 ],
186 "1.2.3",
187 "latest",
188 );
189
190 expect(result).toEqual({
191 releaseNotes: [{ version: "1.2.3", items: ["Valid change"], totalItems: 1 }],
192 omittedReleaseCount: 0,
193 });
194 });
195
196 it("returns an empty result for an invalid payload", () => {
197 expect(normalizeDesktopUpdateReleaseNotes({ note: "- Invalid" }, "1.0.0", "latest")).toEqual({
198 releaseNotes: [],
199 omittedReleaseCount: 0,
200 });
201 });
202
203 it("does not throw on out-of-range numeric entities and keeps the literal", () => {
204 const result = normalizeDesktopUpdateReleaseNotes(
205 "- Broken entity &#9999999999;",
206 "1.0.0",
207 "latest",
208 );
209 expect(result).toEqual({
210 releaseNotes: [{ version: "1.0.0", items: ["Broken entity &#9999999999;"], totalItems: 1 }],
211 omittedReleaseCount: 0,
212 });
213 });
214});