mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 23:25:24 +00:00
feat: finish the homepage plugin, move service links and canvas out of core (B19)
This commit is contained in:
1 parent
21937a6b02
commit
d4ec444558
256 files changed
+27365
-9077
No files matched your search
@@ -0,0 +1,184 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
||||
|
||||
vi.mock("@termix/plugin-sdk/frontend", async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
useTranslation: () => ({ t: (key: string) => key, language: "en" }),
|
||||
}));
|
||||
|
||||
import { ClockWidget } from "../../src/frontend/widgets/ClockWidget.js";
|
||||
import { ClockEditForm } from "../../src/frontend/dialogs/ClockEditForm.js";
|
||||
import {
|
||||
normalizeTimezone,
|
||||
validateClockTimezone,
|
||||
} from "../../src/frontend/clock-timezone.js";
|
||||
import type { CanvasWidget, ClockConfig } from "../../src/frontend/types.js";
|
||||
|
||||
const widget: CanvasWidget = {
|
||||
id: 1,
|
||||
typeId: "clock",
|
||||
title: "Clock",
|
||||
config: {},
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 8,
|
||||
h: 5,
|
||||
zOrder: 0,
|
||||
};
|
||||
|
||||
function renderClock(config: Partial<ClockConfig> = {}) {
|
||||
return render(
|
||||
<ClockWidget
|
||||
widget={widget}
|
||||
config={{ showSeconds: false, format: "24h", ...config }}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
function renderForm(config: Partial<ClockConfig> = {}) {
|
||||
const onChange = vi.fn();
|
||||
render(
|
||||
<ClockEditForm
|
||||
config={{ showSeconds: false, format: "24h", ...config }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
return {
|
||||
onChange,
|
||||
input: screen.getByPlaceholderText(/America\/New_York/) as HTMLInputElement,
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe("ClockWidget timezone handling", () => {
|
||||
it("renders instead of throwing when a saved config holds an invalid timezone", () => {
|
||||
// Regression: "America/New York" (space, not underscore) made
|
||||
// toLocaleTimeString throw a RangeError and took down the whole canvas.
|
||||
expect(() => renderClock({ timezone: "America/New York" })).not.toThrow();
|
||||
expect(screen.queryByText("America/New York")).toBeNull();
|
||||
});
|
||||
|
||||
it("falls back to local time formatting for an invalid timezone", () => {
|
||||
renderClock({ timezone: "Not/AZone" });
|
||||
const local = new Date().toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
expect(screen.getByText(local)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps and displays a valid timezone", () => {
|
||||
renderClock({ timezone: "America/New_York" });
|
||||
expect(screen.getByText("America/New_York")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows no timezone label when none is configured", () => {
|
||||
const { container } = renderClock();
|
||||
expect(
|
||||
container.querySelector('[class*="text-muted-foreground/60"]'),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("formats in the configured timezone, not the host timezone", () => {
|
||||
renderClock({ timezone: "Asia/Tokyo" });
|
||||
const tokyo = new Date().toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
timeZone: "Asia/Tokyo",
|
||||
});
|
||||
expect(screen.getByText(tokyo)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateClockTimezone", () => {
|
||||
it("turns spaces into underscores", () => {
|
||||
expect(validateClockTimezone("America/New York")).toEqual({
|
||||
timezone: "America/New_York",
|
||||
valid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("trims surrounding whitespace", () => {
|
||||
expect(validateClockTimezone(" Europe/Paris ")).toEqual({
|
||||
timezone: "Europe/Paris",
|
||||
valid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("reports an unusable zone as invalid but still returns it normalized", () => {
|
||||
expect(validateClockTimezone("Not/AZone")).toEqual({
|
||||
timezone: "Not/AZone",
|
||||
valid: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("treats blank input as clearing the timezone", () => {
|
||||
expect(validateClockTimezone(" ")).toEqual({
|
||||
timezone: undefined,
|
||||
valid: true,
|
||||
});
|
||||
expect(validateClockTimezone(undefined)).toEqual({
|
||||
timezone: undefined,
|
||||
valid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes every run of whitespace", () => {
|
||||
expect(normalizeTimezone("America/Argentina/Buenos Aires")).toBe(
|
||||
"America/Argentina/Buenos_Aires",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ClockEditForm", () => {
|
||||
it("stores what the user typed without rewriting it mid-edit", () => {
|
||||
const { onChange, input } = renderForm();
|
||||
fireEvent.change(input, { target: { value: "America/Los Angeles" } });
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ timezone: "America/Los Angeles" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("flags an unusable zone the same way FolderMetadataDialog flags a duplicate name", () => {
|
||||
const { input } = renderForm({ timezone: "Not/AZone" });
|
||||
expect(input.getAttribute("aria-invalid")).toBe("true");
|
||||
expect(input.className).toContain("border-destructive");
|
||||
expect(screen.getByText("homepage.invalidTimezone")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("accepts a zone typed with a space, since it is normalized before checking", () => {
|
||||
const { input } = renderForm({ timezone: "America/New York" });
|
||||
expect(input.getAttribute("aria-invalid")).toBe("false");
|
||||
expect(screen.queryByText("homepage.invalidTimezone")).toBeNull();
|
||||
});
|
||||
|
||||
it("says nothing about an empty field", () => {
|
||||
const { input } = renderForm();
|
||||
expect(input.getAttribute("aria-invalid")).toBe("false");
|
||||
expect(screen.queryByText("homepage.invalidTimezone")).toBeNull();
|
||||
});
|
||||
|
||||
it("clears the timezone when the field is emptied", () => {
|
||||
const { onChange, input } = renderForm({ timezone: "Europe/Paris" });
|
||||
fireEvent.change(input, { target: { value: "" } });
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ timezone: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves the other clock settings when the timezone changes", () => {
|
||||
const { onChange, input } = renderForm({
|
||||
showSeconds: true,
|
||||
format: "12h",
|
||||
});
|
||||
fireEvent.change(input, { target: { value: "UTC" } });
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
timezone: "UTC",
|
||||
showSeconds: true,
|
||||
format: "12h",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user