feat(updates): auto-apply and self-restart on new dashboard versions
Adds POST /api/updates/apply (pull, rebuild, restart) plus an "Update now" button and hourly auto-check in the UI, for checkouts that are safely fast-forwardable. Reintroduces self-restart (previously removed in edc25ca for cross-environment reliability concerns) per explicit user request.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @file UpdateNotifier.test.tsx
|
||||
* @description Pins the "Update now" self-apply flow added on top of the
|
||||
* existing manual-command modal: the button only renders when the checkout
|
||||
* situation is fast-forwardable, clicking it calls `api.updates.apply()`,
|
||||
* and a successful `applied: true` response flips the modal into a
|
||||
* "restarting" state that polls `api.updates.status()` until it resolves.
|
||||
*
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "i18next";
|
||||
import { UpdateNotifier } from "../UpdateNotifier";
|
||||
import type { UpdateStatusPayload } from "../../lib/types";
|
||||
|
||||
const statusMock = vi.fn();
|
||||
const checkMock = vi.fn();
|
||||
const applyMock = vi.fn();
|
||||
|
||||
vi.mock("../../lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, unknown>>();
|
||||
return {
|
||||
...actual,
|
||||
api: {
|
||||
updates: {
|
||||
status: (...args: unknown[]) => statusMock(...args),
|
||||
check: (...args: unknown[]) => checkMock(...args),
|
||||
apply: (...args: unknown[]) => applyMock(...args),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const BASE_STATUS: UpdateStatusPayload = {
|
||||
git_repo: true,
|
||||
update_available: true,
|
||||
repo_root: "/repo",
|
||||
remote_ref: "origin/main",
|
||||
canonical_remote: "origin",
|
||||
current_branch: "main",
|
||||
tracking_upstream: "origin/main",
|
||||
tracks_canonical: true,
|
||||
situation: "tracking_canonical",
|
||||
situation_note: null,
|
||||
local_sha: "aaa",
|
||||
remote_sha: "bbb",
|
||||
commits_behind: 2,
|
||||
manual_command: 'cd "/repo" && git pull --ff-only && npm run setup',
|
||||
message: "2 commit(s) on origin/main not in your checkout.",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
statusMock.mockReset().mockResolvedValue(BASE_STATUS);
|
||||
checkMock.mockReset().mockResolvedValue(BASE_STATUS);
|
||||
applyMock.mockReset();
|
||||
try {
|
||||
localStorage.clear();
|
||||
} catch {
|
||||
// Some CI environments stub a non-functional localStorage; the component
|
||||
// already tolerates that (see UpdateNotifier's own try/catch), so tests do too.
|
||||
}
|
||||
});
|
||||
|
||||
describe("UpdateNotifier — Update now", () => {
|
||||
it("shows the Update now button for a fast-forwardable checkout and applies on click", async () => {
|
||||
applyMock.mockResolvedValue({ ...BASE_STATUS, applied: true, update_available: false });
|
||||
render(<UpdateNotifier />);
|
||||
|
||||
const updateBtn = await screen.findByText(i18n.t("updates:updateNow"));
|
||||
await userEvent.click(updateBtn);
|
||||
|
||||
await waitFor(() => expect(applyMock).toHaveBeenCalled());
|
||||
// Appears both in the status banner and the button label while restarting.
|
||||
const restartingNodes = await screen.findAllByText(i18n.t("updates:restarting"));
|
||||
expect(restartingNodes.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("hides the Update now button for a non-fast-forwardable branch", async () => {
|
||||
statusMock.mockResolvedValue({
|
||||
...BASE_STATUS,
|
||||
situation: "feature_branch",
|
||||
tracks_canonical: false,
|
||||
});
|
||||
render(<UpdateNotifier />);
|
||||
|
||||
await screen.findByText(i18n.t("updates:title"));
|
||||
expect(screen.queryByText(i18n.t("updates:updateNow"))).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("surfaces the decline reason instead of restarting on a 409", async () => {
|
||||
applyMock.mockResolvedValue({
|
||||
...BASE_STATUS,
|
||||
applied: false,
|
||||
reason: "not_fast_forwardable",
|
||||
});
|
||||
render(<UpdateNotifier />);
|
||||
|
||||
const updateBtn = await screen.findByText(i18n.t("updates:updateNow"));
|
||||
await userEvent.click(updateBtn);
|
||||
|
||||
expect(await screen.findByText(i18n.t("updates:reasonNotFastForwardable"))).toBeInTheDocument();
|
||||
expect(screen.queryByText(i18n.t("updates:restarting"))).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user