mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
feat: changelog.md release notes, v tags, plugin release notes and videos
This commit is contained in:
1 parent
b9dd3488a5
commit
9bc2be721b
45 files changed
+1462
-816
No files matched your search
@@ -0,0 +1,107 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import i18next from "i18next";
|
||||
import { I18nextProvider, initReactI18next } from "react-i18next";
|
||||
import type { PluginEntry } from "@/plugins/plugin-model";
|
||||
|
||||
const getPluginChangelog = vi.hoisted(() => vi.fn());
|
||||
vi.mock("@/api/plugins-api", () => ({ getPluginChangelog }));
|
||||
|
||||
const { PluginReleaseNotes, PluginVideo } =
|
||||
await import("@/plugins/PluginReleaseNotes");
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
getPluginChangelog.mockReset();
|
||||
});
|
||||
|
||||
async function i18n() {
|
||||
const instance = i18next.createInstance();
|
||||
await instance.use(initReactI18next).init({
|
||||
lng: "en",
|
||||
resources: {
|
||||
en: {
|
||||
translation: {
|
||||
plugins: {
|
||||
manager: {
|
||||
releaseNotes: "Release notes",
|
||||
installedBadge: "Installed",
|
||||
changeTypes: { added: "Added", fixed: "Fixed" },
|
||||
videoTitle: "{{name}} video",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
function plugin(overrides: Partial<PluginEntry> = {}): PluginEntry {
|
||||
return {
|
||||
id: "docker",
|
||||
name: "Docker",
|
||||
installed: true,
|
||||
version: "1.0.0",
|
||||
versions: [],
|
||||
...overrides,
|
||||
} as PluginEntry;
|
||||
}
|
||||
|
||||
describe("PluginReleaseNotes", () => {
|
||||
it("shows the installed copy's changelog grouped by type", async () => {
|
||||
getPluginChangelog.mockResolvedValue([
|
||||
{
|
||||
version: "1.0.0",
|
||||
changes: [
|
||||
{ type: "added", text: "Runs `docker ps`" },
|
||||
{ type: "fixed", text: "A crash" },
|
||||
],
|
||||
},
|
||||
]);
|
||||
render(
|
||||
<I18nextProvider i18n={await i18n()}>
|
||||
<PluginReleaseNotes plugin={plugin()} />
|
||||
</I18nextProvider>,
|
||||
);
|
||||
await waitFor(() => expect(screen.getByText("A crash")).toBeTruthy());
|
||||
expect(getPluginChangelog).toHaveBeenCalledWith("docker");
|
||||
expect(screen.getByText("Added")).toBeTruthy();
|
||||
expect(screen.getByText("docker ps").tagName).toBe("CODE");
|
||||
expect(screen.getByText("Installed")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders nothing for a plugin with no versions", async () => {
|
||||
const { container } = render(
|
||||
<I18nextProvider i18n={await i18n()}>
|
||||
<PluginReleaseNotes plugin={plugin({ installed: false })} />
|
||||
</I18nextProvider>,
|
||||
);
|
||||
expect(container.innerHTML).toBe("");
|
||||
expect(getPluginChangelog).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("PluginVideo", () => {
|
||||
it("embeds a YouTube id", async () => {
|
||||
render(
|
||||
<I18nextProvider i18n={await i18n()}>
|
||||
<PluginVideo videoId="dQw4w9WgXcQ" name="Docker" />
|
||||
</I18nextProvider>,
|
||||
);
|
||||
const frame = screen.getByTitle("Docker video") as HTMLIFrameElement;
|
||||
expect(frame.src).toBe(
|
||||
"https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?rel=0",
|
||||
);
|
||||
});
|
||||
|
||||
it("loads nothing without a valid id", async () => {
|
||||
const { container } = render(
|
||||
<I18nextProvider i18n={await i18n()}>
|
||||
<PluginVideo videoId="https://evil.example" name="Docker" />
|
||||
<PluginVideo name="Docker" />
|
||||
</I18nextProvider>,
|
||||
);
|
||||
expect(container.querySelector("iframe")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
describeContributions,
|
||||
formatBytes,
|
||||
formatCount,
|
||||
groupChanges,
|
||||
matchesQuery,
|
||||
mergeReleaseNotes,
|
||||
mergePlugins,
|
||||
orderByRisk,
|
||||
pluginSource,
|
||||
@@ -13,6 +15,7 @@ import {
|
||||
reportIssueUrl,
|
||||
sortPlugins,
|
||||
uploadEntry,
|
||||
youtubeEmbedUrl,
|
||||
} from "@/plugins/plugin-model";
|
||||
|
||||
function summary(overrides: Partial<PluginSummary> = {}): PluginSummary {
|
||||
@@ -283,3 +286,88 @@ describe("reportIssueUrl", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("mergeReleaseNotes", () => {
|
||||
const registryVersion = (version: string, extra = {}) => ({
|
||||
version,
|
||||
compatible: true,
|
||||
capabilities: [],
|
||||
size: 1,
|
||||
publishedAt: "2026-10-01T00:00:00Z",
|
||||
...extra,
|
||||
});
|
||||
|
||||
it("joins registry and local notes newest first, local winning", () => {
|
||||
const rows = mergeReleaseNotes(
|
||||
[
|
||||
registryVersion("1.2.0", {
|
||||
compatible: false,
|
||||
notes: { changes: [{ type: "added", text: "Registry" }] },
|
||||
}),
|
||||
registryVersion("1.0.0", {
|
||||
releaseNotesUrl: "https://github.com/x/y/releases/tag/v1.0.0",
|
||||
notes: { changes: [{ type: "added", text: "Old" }] },
|
||||
}),
|
||||
],
|
||||
[
|
||||
{
|
||||
version: "1.0.0",
|
||||
date: "2026-09-01",
|
||||
summary: "First.",
|
||||
changes: [{ type: "added", text: "Local" }],
|
||||
},
|
||||
{ version: "0.9.0", changes: [{ type: "fixed", text: "Beta" }] },
|
||||
],
|
||||
);
|
||||
expect(rows.map((r) => r.version)).toEqual(["1.2.0", "1.0.0", "0.9.0"]);
|
||||
expect(rows[0]).toMatchObject({ compatible: false });
|
||||
expect(rows[1]).toEqual({
|
||||
version: "1.0.0",
|
||||
date: "2026-09-01",
|
||||
summary: "First.",
|
||||
changes: [{ type: "added", text: "Local" }],
|
||||
compatible: true,
|
||||
releaseNotesUrl: "https://github.com/x/y/releases/tag/v1.0.0",
|
||||
});
|
||||
expect(rows[2]).toMatchObject({ date: null, compatible: true });
|
||||
});
|
||||
|
||||
it("is empty with nothing to show", () => {
|
||||
expect(mergeReleaseNotes([], [])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("groupChanges", () => {
|
||||
it("groups by type in a fixed order", () => {
|
||||
expect(
|
||||
groupChanges([
|
||||
{ type: "fixed", text: "b" },
|
||||
{ type: "added", text: "a" },
|
||||
{ type: "fixed", text: "c" },
|
||||
]),
|
||||
).toEqual([
|
||||
{ type: "added", items: ["a"] },
|
||||
{ type: "fixed", items: ["b", "c"] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("youtubeEmbedUrl", () => {
|
||||
it("builds a privacy embed from a video id only", () => {
|
||||
expect(youtubeEmbedUrl("dQw4w9WgXcQ")).toBe(
|
||||
"https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?rel=0",
|
||||
);
|
||||
});
|
||||
|
||||
it("refuses anything that is not an id", () => {
|
||||
for (const value of [
|
||||
undefined,
|
||||
"",
|
||||
"https://evil.example/x",
|
||||
"dQw4w9WgXcQ/../../x",
|
||||
"javascript:alert(1)",
|
||||
]) {
|
||||
expect(youtubeEmbedUrl(value)).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user