feat: add semantic motion system (#1320)

* feat: add semantic motion system

* feat: animate session workspace transitions

* feat: refine motion accessibility and transfer feedback
This commit is contained in:
ZacharyZcR
2026-08-24 19:36:10 +08:00
committed by GitHub
parent fafd428072
commit 69002e6416
15 changed files with 569 additions and 90 deletions
@@ -19,6 +19,9 @@ describe("ConnectionScreen", () => {
).not.toThrow();
expect(screen.getByText("common.loading")).toBeTruthy();
const status = screen.getByRole("status");
expect(status.getAttribute("data-status")).toBe("connecting");
expect(status.querySelector('[data-active="true"]')).toBeTruthy();
});
it("renders the disconnected state without a provider", () => {
@@ -32,6 +35,9 @@ describe("ConnectionScreen", () => {
).not.toThrow();
expect(screen.getByText("guacamole.hostNotFound")).toBeTruthy();
expect(screen.getByRole("status").getAttribute("data-status")).toBe(
"disconnected",
);
});
it("still shows the connection log when a provider is present", () => {
@@ -0,0 +1,61 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { TransferProgressBar } from "../../../features/file-manager/components/TransferProgressBar";
afterEach(cleanup);
describe("TransferProgressBar", () => {
it("exposes determinate progress and drives the fill with a transform", () => {
const { container } = render(
<TransferProgressBar value={42} label="Uploading archive" />,
);
const progress = screen.getByRole("progressbar", {
name: "Uploading archive",
});
expect(progress.getAttribute("aria-valuenow")).toBe("42");
expect(progress.getAttribute("aria-busy")).toBe("false");
expect(
container
.querySelector<HTMLElement>(".transfer-progress-fill")
?.style.getPropertyValue("--transfer-progress"),
).toBe("0.42");
});
it("clamps invalid percentages before exposing them", () => {
const { rerender } = render(
<TransferProgressBar value={140} label="Uploading archive" />,
);
expect(screen.getByRole("progressbar").getAttribute("aria-valuenow")).toBe(
"100",
);
rerender(<TransferProgressBar value={-20} label="Uploading archive" />);
expect(screen.getByRole("progressbar").getAttribute("aria-valuenow")).toBe(
"0",
);
});
it("marks unknown and stalled progress without inventing a percentage", () => {
const { container } = render(
<TransferProgressBar label="Reconnecting" stalled />,
);
const progress = screen.getByRole("progressbar");
expect(progress.hasAttribute("aria-valuenow")).toBe(false);
expect(progress.getAttribute("aria-busy")).toBe("true");
expect(progress.getAttribute("data-stalled")).toBe("true");
expect(
container
.querySelector(".transfer-progress-fill")
?.classList.contains("transfer-progress-indeterminate"),
).toBe(true);
expect(
container
.querySelector(".transfer-progress-fill")
?.classList.contains("transfer-progress-stalled"),
).toBe(true);
});
});
+83
View File
@@ -0,0 +1,83 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render } from "@testing-library/react";
import type { Tab } from "@/types/ui-types";
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
vi.mock("@/lib/electron", () => ({ isElectron: () => true }));
import { TabBar } from "@/shell/TabBar";
afterEach(cleanup);
const tabs = [
{ id: "dashboard", type: "dashboard", label: "Dashboard" },
{ id: "terminal-1", type: "terminal", label: "web-01" },
] as Tab[];
function renderTabBar(activeTabId: string, onSetActiveTab = vi.fn()) {
return render(
<TabBar
tabs={tabs}
activeTabId={activeTabId}
splitMode="none"
paneTabIds={[]}
focusedPaneIndex={null}
onSetActiveTab={onSetActiveTab}
onCloseTab={() => {}}
onRefreshTab={() => {}}
onReorderTabs={() => {}}
onSplitTab={() => {}}
onAddToSplit={() => {}}
onRemoveFromSplit={() => {}}
isAppFullscreen={false}
onToggleAppFullscreen={() => {}}
/>,
);
}
describe("TabBar workspace continuity", () => {
it("keeps one shared indicator on the active workspace", () => {
const { container, rerender } = renderTabBar("dashboard");
expect(
container.querySelector('[data-workspace-indicator="dashboard"]'),
).toBeTruthy();
rerender(
<TabBar
tabs={tabs}
activeTabId="terminal-1"
splitMode="none"
paneTabIds={[]}
focusedPaneIndex={null}
onSetActiveTab={() => {}}
onCloseTab={() => {}}
onRefreshTab={() => {}}
onReorderTabs={() => {}}
onSplitTab={() => {}}
onAddToSplit={() => {}}
onRemoveFromSplit={() => {}}
isAppFullscreen={false}
onToggleAppFullscreen={() => {}}
/>,
);
expect(
container.querySelectorAll("[data-workspace-indicator]"),
).toHaveLength(1);
expect(
container.querySelector('[data-workspace-indicator="terminal-1"]'),
).toBeTruthy();
});
it("activates a workspace immediately when its tab is clicked", () => {
const onSetActiveTab = vi.fn();
const { getByText } = renderTabBar("dashboard", onSetActiveTab);
fireEvent.click(getByText("web-01"));
expect(onSetActiveTab).toHaveBeenCalledWith("terminal-1");
});
});