mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-09-19 09:40:14 +00:00
Implement scroll position management for navigation (#73)
* feat: implement scroll position management for main page navigation * fix: import saveMainPageScrollPosition in ServerCard and ServerCardInline components * feat: save scroll position when navigating to server from DashCommand * fix: prevent restoring stale scroll positions without main page origin
This commit is contained in:
@@ -83,8 +83,14 @@ describe("DashCommand", () => {
|
||||
expect(screen.getByText("edge-7")).toBeInTheDocument();
|
||||
expect(screen.getByText("ToggleDarkMode")).toBeInTheDocument();
|
||||
|
||||
Object.defineProperty(window, "scrollY", {
|
||||
configurable: true,
|
||||
value: 256,
|
||||
});
|
||||
await user.click(screen.getByText("edge-7"));
|
||||
expect(dashMocks.navigate).toHaveBeenCalledWith("/server/7");
|
||||
expect(sessionStorage.getItem("fromMainPage")).toBe("true");
|
||||
expect(sessionStorage.getItem("scrollPosition")).toBe("256");
|
||||
expect(dashMocks.closeCommand).toHaveBeenCalled();
|
||||
|
||||
await user.click(screen.getByText("ToggleDarkMode"));
|
||||
|
||||
@@ -179,6 +179,19 @@ describe("Header", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("ignores invalid custom links instead of crashing", async () => {
|
||||
Object.assign(window, {
|
||||
CustomLinks: "{bad-json",
|
||||
});
|
||||
|
||||
renderHeader();
|
||||
|
||||
expect(await screen.findByText("Nezha")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("link", { name: "Docs" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("stores and removes the active custom background", async () => {
|
||||
const user = userEvent.setup();
|
||||
headerMocks.backgroundImage = "/desktop.png";
|
||||
|
||||
@@ -104,6 +104,7 @@ describe("Tab and group switches", () => {
|
||||
it("scrolls overflowing group tabs and applies custom background styling", async () => {
|
||||
const user = userEvent.setup();
|
||||
const setCurrentTab = vi.fn();
|
||||
const scrollIntoView = vi.spyOn(HTMLElement.prototype, "scrollIntoView");
|
||||
vi.spyOn(HTMLElement.prototype, "scrollWidth", "get").mockReturnValue(320);
|
||||
vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(120);
|
||||
window.CustomBackgroundImage = "/background.jpg";
|
||||
@@ -125,6 +126,7 @@ describe("Tab and group switches", () => {
|
||||
|
||||
expect(scrollContainer.scrollLeft).toBe(42);
|
||||
expect(setCurrentTab).toHaveBeenCalledWith("Asia");
|
||||
expect(scrollIntoView).not.toHaveBeenCalled();
|
||||
expect(
|
||||
container.querySelector(".relative.flex.items-center")?.className,
|
||||
).toContain("bg-stone-100/70");
|
||||
|
||||
@@ -72,9 +72,14 @@ describe("ServerCard", () => {
|
||||
screen.getAllByText(/billingInfo.remaining: 16/).length,
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
Object.defineProperty(window, "scrollY", {
|
||||
configurable: true,
|
||||
value: 432,
|
||||
});
|
||||
fireEvent.click(screen.getByText("edge-online"));
|
||||
|
||||
expect(sessionStorage.getItem("fromMainPage")).toBe("true");
|
||||
expect(sessionStorage.getItem("scrollPosition")).toBe("432");
|
||||
expect(screen.getByText("/server/7")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ describe("ServerDetailOverview", () => {
|
||||
|
||||
it("renders server identity, hardware, traffic, and temperature details", async () => {
|
||||
const user = userEvent.setup();
|
||||
sessionStorage.setItem("fromMainPage", "true");
|
||||
seedWebSocketData({
|
||||
server: createServer({
|
||||
id: 7,
|
||||
@@ -113,8 +114,8 @@ describe("ServerDetailOverview", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/server/7"]}>
|
||||
const { unmount } = render(
|
||||
<MemoryRouter initialEntries={["/", "/server/7"]} initialIndex={1}>
|
||||
<ServerDetailOverview server_id="7" />
|
||||
<LocationProbe />
|
||||
</MemoryRouter>,
|
||||
@@ -139,5 +140,8 @@ describe("ServerDetailOverview", () => {
|
||||
|
||||
await user.click(screen.getByText("edge-detail"));
|
||||
expect(screen.getByText("/")).toBeInTheDocument();
|
||||
|
||||
unmount();
|
||||
expect(sessionStorage.getItem("fromMainPage")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,6 +93,31 @@ describe("ServiceTracker", () => {
|
||||
expect(screen.getByText("Monthly")).toBeInTheDocument();
|
||||
expect(screen.queryByText("hidden-server")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("falls back to zero uptime when a service has no checks", async () => {
|
||||
apiMocks.fetchService.mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
services: {
|
||||
http: {
|
||||
service_name: "HTTP Ping",
|
||||
current_up: 0,
|
||||
current_down: 0,
|
||||
total_up: 0,
|
||||
total_down: 0,
|
||||
delay: [],
|
||||
up: [0, 0],
|
||||
down: [0, 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderWithQuery(<ServiceTracker serverList={[createServer()]} />);
|
||||
|
||||
expect(await screen.findByText("HTTP Ping")).toBeInTheDocument();
|
||||
expect(screen.getByText("0.0% serviceTracker.uptime")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("CycleTransferStatsCard", () => {
|
||||
|
||||
@@ -81,6 +81,9 @@ describe("nezha api fetchers", () => {
|
||||
});
|
||||
|
||||
it("refreshes the token when a logged-in browser session has cookies", async () => {
|
||||
const consoleLog = vi
|
||||
.spyOn(console, "log")
|
||||
.mockImplementation(() => undefined);
|
||||
Object.defineProperty(document, "cookie", {
|
||||
configurable: true,
|
||||
value: "nezha_token=token; nz-csrf=test-csrf-token",
|
||||
@@ -108,5 +111,6 @@ describe("nezha api fetchers", () => {
|
||||
"X-CSRF-Token": "test-csrf-token",
|
||||
},
|
||||
});
|
||||
expect(consoleLog).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -275,6 +275,73 @@ describe("Servers page", () => {
|
||||
expect(screen.getAllByTestId("server-card")[0]).toHaveTextContent("alpha");
|
||||
});
|
||||
|
||||
it("sorts by system even when a platform value is missing", async () => {
|
||||
const missingPlatform = createServer({
|
||||
id: 1,
|
||||
name: "alpha",
|
||||
host: { platform: undefined },
|
||||
});
|
||||
const linux = createServer({
|
||||
id: 2,
|
||||
name: "beta",
|
||||
host: { platform: "linux" },
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderServerPage({
|
||||
connected: true,
|
||||
lastData: websocketPayload([missingPlatform, linux]),
|
||||
});
|
||||
|
||||
await user.selectOptions(screen.getByLabelText("Sort metric"), "system");
|
||||
|
||||
const cards = screen.getAllByTestId("server-card");
|
||||
expect(cards).toHaveLength(2);
|
||||
expect(cards[0]).toHaveTextContent("beta");
|
||||
expect(cards[1]).toHaveTextContent("alpha");
|
||||
});
|
||||
|
||||
it("restores the saved main page scroll position after data is ready", async () => {
|
||||
const scrollTo = vi.fn();
|
||||
vi.stubGlobal("scrollTo", scrollTo);
|
||||
vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
|
||||
callback(0);
|
||||
return 0;
|
||||
});
|
||||
sessionStorage.setItem("fromMainPage", "true");
|
||||
sessionStorage.setItem("scrollPosition", "345");
|
||||
|
||||
renderServerPage({
|
||||
connected: true,
|
||||
lastData: websocketPayload([createServer({ id: 1, name: "alpha" })]),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(scrollTo).toHaveBeenCalledWith({
|
||||
top: 345,
|
||||
left: 0,
|
||||
behavior: "auto",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("does not restore stale scroll positions without a main page origin", () => {
|
||||
const scrollTo = vi.fn();
|
||||
vi.stubGlobal("scrollTo", scrollTo);
|
||||
vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
|
||||
callback(0);
|
||||
return 0;
|
||||
});
|
||||
sessionStorage.setItem("scrollPosition", "345");
|
||||
|
||||
renderServerPage({
|
||||
connected: true,
|
||||
lastData: websocketPayload([createServer({ id: 1, name: "alpha" })]),
|
||||
});
|
||||
|
||||
expect(scrollTo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("toggles map and service tracker controls when service data exists", async () => {
|
||||
apiMocks.fetchService.mockResolvedValue({
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user