mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 15:15:24 +00:00
* feat: let the login page send the token back to the mobile app * feat: add host-scripts entry to the plugin sdk * feat: let plugins say how they show in the mobile app * feat: frame host for running one plugin view outside the shell * fix: build the SDK host modules in production mode * chore: note the SDK host build fix in the changelog * feat: list plugin engine ranges for the mobile app * feat: host color and icon columns that sync * chore: update translations and plugin pins * fix: test * fix: show source address when rejecting proxy auth * fix: deleting your own account removes all of its data * fix(deps): update body-parser to 2.4.0 (#1736) * fix: body-parser 보안 패치 반영 * chore: 자동 검사를 막던 기존 JSON 서식 정리 --------- Co-authored-by: renovys <180682734+renovys@users.noreply.github.com> * fix(sdk): decode command output as GBK when it is not valid UTF-8 (#1735) * fix: honor shared credentials on shared hosts (#1730) * fix: honor shared credentials on shared hosts and in personal SSH auth * fix: snapshot shared SSH auth from a credential shared with the host owner * fix: size DataView list columns by the table's own width (#1713) * fix: size DataView list columns by the table's own width The list grid was built from every column, so columns hidden below md/lg left empty tracks and the visible cells landed under the wrong headers. Each width now gets a template of only its visible columns (CSS variables + container queries on the table, so a narrow panel counts, not the window), the header uses the same gap as the rows, and rows use a minimum height so wrapped cells no longer overlap. * test: wait for the title bar offset effect before asserting The bar shows once getTitleBarVisible resolves, but --termix-titlebar-height is set in a later passive effect, so reading it right after findByText raced (3-5 of 40 runs under load, on main too). * chore: rerun CI against dev-26.10.6 * fix: unlock passphrase keys once per connect burst (#1737) * feat: give ssh transports the host origin and sync id (#1717) * fix: wake other clients after a sync push (Plugin-Sync#1) * feat: look up local row ids by sync id for the sync plugin (Plugin-Sync#4) * feat: rail items can show a status dot (Plugin-Sync#5) * fix: add the desktop shared copy banner text (#1709) * feat: sidebar preference for alternating rows (#1712) * fix: striped rows text * feat: shell can open a saved host's editor for plugins (Plugin-SSH-Terminal#36) * fix: show the logo in the macos menu bar (#1728) * fix: number shortcuts skip the home tab (#1724) * fix: show plugins that could not be downloaded as failed (#1714) * fix: pass the host id as text when opening its editor * fix: retry a plugin bundle that was not ready yet (#1708) * fix: let commonjs plugins fall back to core packages on the desktop * fix: let non docker installs switch nginx to https (#1711) * feat: remove a folder share from its hosts too (#1719) * chore: changelog for merged pull requests * fix: accept the release .sig when installing a plugin from a file * feat: folder logins for shared hosts, read only plugin sync, full desktop builds * fix: test * fix: stop dev runner restart loops and narrow plugin watchers * chore: update translations and plugin pins * chore: lint, format, and bump version to 26.10.6 --------- Co-authored-by: renovys <renovy@gmail.com> Co-authored-by: renovys <180682734+renovys@users.noreply.github.com> Co-authored-by: Sebastian Cao <cycsmail@gmail.com> Co-authored-by: Jonathan Rehuel Lewerissa <jonathan.rehuel16@gmail.com> Co-authored-by: Rhiz3K <33246262+Rhiz3K@users.noreply.github.com>
176 lines
5.5 KiB
TypeScript
176 lines
5.5 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { validate } from "../packages/plugin-sdk/cli/commands/validate.mjs";
|
|
|
|
const roots: string[] = [];
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
for (const root of roots.splice(0))
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
function plugin(dialects: string[]) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "termix-validate-"));
|
|
roots.push(root);
|
|
fs.writeFileSync(
|
|
path.join(root, "manifest.json"),
|
|
JSON.stringify({ id: "fixture", version: "1.0.0" }),
|
|
);
|
|
for (const dialect of dialects) {
|
|
const dir = path.join(root, "migrations", dialect);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(dir, "0001_init.sql"),
|
|
"CREATE TABLE p_fixture_items (id INTEGER PRIMARY KEY);\n",
|
|
);
|
|
}
|
|
return root;
|
|
}
|
|
|
|
async function problems(cwd: string) {
|
|
const lines: string[] = [];
|
|
vi.spyOn(console, "error").mockImplementation((line) => lines.push(line));
|
|
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
await expect(validate({ cwd })).rejects.toThrow();
|
|
return lines.join("\n");
|
|
}
|
|
|
|
describe("termix-plugin validate migrations", () => {
|
|
it("flags an engine with no migrations folder", async () => {
|
|
const output = await problems(plugin(["sqlite", "postgres"]));
|
|
expect(output).toContain("migrations/mysql is missing");
|
|
expect(output).not.toContain("migrations/postgres is missing");
|
|
});
|
|
|
|
it("accepts all three engines", async () => {
|
|
const output = await problems(plugin(["sqlite", "postgres", "mysql"]));
|
|
expect(output).not.toMatch(/migrations\/\w+ is missing/);
|
|
});
|
|
});
|
|
|
|
describe("termix-plugin validate themes", () => {
|
|
function themed(css: string | null) {
|
|
const root = plugin([]);
|
|
fs.writeFileSync(
|
|
path.join(root, "manifest.json"),
|
|
JSON.stringify({
|
|
id: "fixture",
|
|
version: "1.0.0",
|
|
capabilities: ["ui:theme"],
|
|
contributes: {
|
|
themes: [
|
|
{
|
|
id: "paper",
|
|
label: "Paper",
|
|
mode: "light",
|
|
css: "themes/paper.css",
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
);
|
|
if (css !== null) {
|
|
fs.mkdirSync(path.join(root, "themes"));
|
|
fs.writeFileSync(path.join(root, "themes", "paper.css"), css);
|
|
}
|
|
return root;
|
|
}
|
|
|
|
it("reports a missing theme file", async () => {
|
|
expect(await problems(themed(null))).toContain(
|
|
"theme paper: themes/paper.css does not exist",
|
|
);
|
|
});
|
|
|
|
it("reports theme CSS that reaches outside or leaves its class", async () => {
|
|
const output = await problems(
|
|
themed('@import "x.css"; body { color: red }'),
|
|
);
|
|
expect(output).toContain("theme paper: @import is not allowed in a theme");
|
|
expect(output).toContain(
|
|
'theme paper: Selector "body" must start with .paper',
|
|
);
|
|
});
|
|
|
|
it("says nothing about CSS that is fine", async () => {
|
|
const output = await problems(themed(".paper { --background: #fff; }"));
|
|
expect(output).not.toContain("theme paper");
|
|
});
|
|
});
|
|
|
|
describe("termix-plugin validate layouts and capabilities", () => {
|
|
function withManifest(extra: Record<string, unknown>) {
|
|
const root = plugin([]);
|
|
fs.writeFileSync(
|
|
path.join(root, "manifest.json"),
|
|
JSON.stringify({ id: "fixture", version: "1.0.0", ...extra }),
|
|
);
|
|
return root;
|
|
}
|
|
|
|
it("flags ui:theme with no themes and ui:layout with no layouts", async () => {
|
|
const output = await problems(
|
|
withManifest({ capabilities: ["ui:theme", "ui:layout"] }),
|
|
);
|
|
expect(output).toContain(
|
|
'"ui:theme" is declared but contributes.themes is empty',
|
|
);
|
|
expect(output).toContain(
|
|
'"ui:layout" is declared but contributes.layouts is empty',
|
|
);
|
|
});
|
|
|
|
it("flags a layout plugin with no frontend source", async () => {
|
|
const output = await problems(
|
|
withManifest({
|
|
capabilities: ["ui:layout"],
|
|
contributes: { layouts: [{ id: "wide", label: "Wide" }] },
|
|
}),
|
|
);
|
|
expect(output).toContain("contributes.layouts needs a frontend");
|
|
});
|
|
|
|
it("accepts a layout plugin that has a frontend entry", async () => {
|
|
const root = withManifest({
|
|
capabilities: ["ui:layout"],
|
|
contributes: { layouts: [{ id: "wide", label: "Wide" }] },
|
|
});
|
|
fs.mkdirSync(path.join(root, "src", "frontend"), { recursive: true });
|
|
fs.writeFileSync(path.join(root, "src", "frontend", "index.tsx"), "");
|
|
const output = await problems(root);
|
|
expect(output).not.toContain("contributes.layouts needs a frontend");
|
|
expect(output).not.toContain("is declared but");
|
|
});
|
|
});
|
|
|
|
describe("termix-plugin validate mobile entry", () => {
|
|
function mobile(built: boolean) {
|
|
const root = plugin([]);
|
|
fs.writeFileSync(
|
|
path.join(root, "manifest.json"),
|
|
JSON.stringify({
|
|
id: "fixture",
|
|
version: "1.0.0",
|
|
contributes: { mobile: { mode: "webview", entry: "dist/mobile.js" } },
|
|
}),
|
|
);
|
|
if (built) {
|
|
fs.mkdirSync(path.join(root, "dist"), { recursive: true });
|
|
fs.writeFileSync(path.join(root, "dist", "mobile.js"), "");
|
|
}
|
|
return root;
|
|
}
|
|
|
|
it("reports a mobile entry that was not built", async () => {
|
|
expect(await problems(mobile(false))).toContain(
|
|
"manifest references dist/mobile.js, which does not exist",
|
|
);
|
|
});
|
|
|
|
it("finds a built mobile entry", async () => {
|
|
expect(await problems(mobile(true))).not.toContain("dist/mobile.js");
|
|
});
|
|
});
|