mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(studio): hold navigation during market publication
This commit is contained in:
1 parent
f1bae78d39
commit
215dedc5b4
3 files changed
+262
-3
No files matched your search
@@ -404,7 +404,7 @@ export function MarketGroup({ port, onInstalled, onViewInstalled, account, onSig
|
||||
if (!handle) return;
|
||||
setDraft({ port, handle, pkg }); setView("publish");
|
||||
}} />}
|
||||
{at === "publish" && handle && <PublishForm port={port} handle={handle} initial={draft?.pkg} onMine={() => { setDraft(null); setView("mine"); }} />}
|
||||
{at === "publish" && handle && <PublishForm port={port} handle={handle} initial={draft?.pkg} onApplying={applyingChanged} onMine={() => { setDraft(null); setView("mine"); }} />}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ interface Draft {
|
||||
|
||||
const EMPTY: Draft = { kind: "skill", name: "", source: "", summary: "", description: "", repoUrl: "", version: "", tags: [], tagInput: "", private: false };
|
||||
|
||||
interface PublishProps { port: AgentPort; handle: string; onMine: () => void; initial?: MarketPackage }
|
||||
interface PublishProps { port: AgentPort; handle: string; onMine: () => void; onApplying?: (applying: boolean) => void; initial?: MarketPackage }
|
||||
|
||||
export function PublishForm(props: PublishProps) {
|
||||
const [owner, setOwner] = useState({ port: props.port, handle: props.handle, generation: 0 });
|
||||
@@ -53,7 +53,7 @@ export function PublishForm(props: PublishProps) {
|
||||
|
||||
// The form only collects; which sources are publishable and what the registry
|
||||
// accepts are the kernel's and the registry's answers, shown as they come back.
|
||||
function PublishDraft({ port, handle, onMine, initial }: PublishProps) {
|
||||
function PublishDraft({ port, handle, onMine, onApplying, initial }: PublishProps) {
|
||||
const [d, setD] = useState<Draft>(() => initial ? {
|
||||
...EMPTY, kind: initial.kind, name: initial.name, summary: initial.summary,
|
||||
description: initial.description, repoUrl: initial.repoUrl, tags: [...initial.tags],
|
||||
@@ -64,6 +64,11 @@ function PublishDraft({ port, handle, onMine, initial }: PublishProps) {
|
||||
const [done, setDone] = useState<MarketPublished | null>(null);
|
||||
const set = (k: keyof Draft) => (e: { target: { value: string } }) => setD({ ...d, [k]: e.target.value });
|
||||
|
||||
useEffect(() => {
|
||||
onApplying?.(busy);
|
||||
return () => onApplying?.(false);
|
||||
}, [busy, onApplying]);
|
||||
|
||||
const submit = async () => {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { StrictMode } from "react";
|
||||
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import "./testkit";
|
||||
import { Settings } from "./Settings";
|
||||
import { MarketGroup } from "./Market";
|
||||
import { MockPort } from "../port/mock";
|
||||
import { MockHub } from "../port/mock_hub";
|
||||
import { boot, STORAGE, t } from "../i18n";
|
||||
import type { AgentPort, MarketPlan, MarketPublished, SessionStatus } from "../port/port";
|
||||
|
||||
afterEach(() => { cleanup(); vi.restoreAllMocks(); localStorage.setItem(STORAGE, "zh"); boot(); });
|
||||
|
||||
const account = (handle = "demo") => ({ signedIn: true, user: { handle, email: `${handle}@example.com`, label: handle } });
|
||||
const field = (label: string) => screen.getByLabelText<HTMLInputElement>(new RegExp(`^${t(label)}`));
|
||||
const published = () => document.querySelector<HTMLElement>('.mkt-pub[data-stage="done"] [role="status"]');
|
||||
const fill = (name = "notes-kit") => {
|
||||
fireEvent.change(field("名称"), { target: { value: name } });
|
||||
fireEvent.change(field("来源地址"), { target: { value: `https://example.com/${name}/SKILL.md` } });
|
||||
};
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (error: Error) => void;
|
||||
const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no; });
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
const receipt = async (port: AgentPort, privateDraft = false, name = "notes-kit") => port.publishMarket({
|
||||
kind: "skill", name, source: `https://example.com/${name}/SKILL.md`, visibility: privateDraft ? "private" : "public",
|
||||
});
|
||||
const draw = (port: AgentPort, onClose = vi.fn()) => <Settings
|
||||
hub={new MockHub() as never} port={port}
|
||||
status={{ preset: "balanced", toolApprovalMode: "ask" } as SessionStatus}
|
||||
theme="light" onTheme={() => {}} contrast="" onContrast={() => {}} weight="" onWeight={() => {}}
|
||||
look={{} as never} onLook={() => {}} reloadThemes={() => {}}
|
||||
onClose={onClose} onChanged={() => {}} onError={() => {}} at="ext:market"
|
||||
account={account()} accountUnread="" reloadAccount={() => {}}
|
||||
/>;
|
||||
|
||||
it("keeps a pending publish receipt visible after clicking the browse view", async () => {
|
||||
const port = new MockPort() as unknown as AgentPort;
|
||||
const out = await receipt(port);
|
||||
const pending = deferred<MarketPublished>();
|
||||
vi.spyOn(port, "publishMarket").mockImplementation(() => pending.promise);
|
||||
render(draw(port));
|
||||
await userEvent.click(screen.getByRole("radio", { name: "发布" }));
|
||||
fill();
|
||||
await userEvent.click(screen.getByRole("button", { name: "提交审核" }));
|
||||
await userEvent.click(screen.getByRole("radio", { name: "浏览" }));
|
||||
await act(async () => pending.resolve(out));
|
||||
expect(published()).not.toBeNull();
|
||||
expect(published()?.textContent).toContain("已提交 demo/notes-kit 0.1.0");
|
||||
});
|
||||
|
||||
it.each(["zh", "en"].flatMap((lang) => [false, true].flatMap((privateDraft) =>
|
||||
["success", "failure"].map((outcome) => ({ lang, privateDraft, outcome })),
|
||||
)))("holds the $lang author form and Settings navigation through private=$privateDraft publish $outcome", async ({ lang, privateDraft, outcome }) => {
|
||||
localStorage.setItem(STORAGE, lang); boot();
|
||||
const port = new MockPort() as unknown as AgentPort;
|
||||
const out = await receipt(port, privateDraft);
|
||||
const pending = deferred<MarketPublished>();
|
||||
const publish = vi.spyOn(port, "publishMarket").mockImplementationOnce(() => pending.promise).mockResolvedValue(out);
|
||||
const onClose = vi.fn();
|
||||
render(draw(port, onClose));
|
||||
await userEvent.click(screen.getByRole("radio", { name: t("发布") }));
|
||||
fill();
|
||||
if (privateDraft) await userEvent.click(screen.getByRole("checkbox"));
|
||||
await userEvent.click(screen.getByRole("button", { name: t(privateDraft ? "保存为私有" : "提交审核") }));
|
||||
const form = document.querySelector<HTMLFormElement>("form.mkt-pub")!;
|
||||
const controls = [
|
||||
document.querySelector<HTMLButtonElement>('[data-action="settings.close"]')!,
|
||||
document.querySelector<HTMLButtonElement>('[data-action="settings.section"][data-value="session"]')!,
|
||||
screen.getByRole<HTMLButtonElement>("tab", { name: t("已安装") }),
|
||||
screen.getByRole<HTMLButtonElement>("radio", { name: t("浏览") }),
|
||||
screen.getByRole<HTMLButtonElement>("radio", { name: t("我的发布") }),
|
||||
];
|
||||
for (const button of controls) {
|
||||
expect(button.disabled).toBe(true);
|
||||
await userEvent.click(button);
|
||||
}
|
||||
await userEvent.type(screen.getByRole("textbox", { name: t("搜索设置") }), "模型");
|
||||
const found = screen.getAllByRole<HTMLButtonElement>("option");
|
||||
for (const button of found) {
|
||||
expect(button.disabled).toBe(true);
|
||||
await userEvent.click(button);
|
||||
}
|
||||
await userEvent.click(form);
|
||||
await userEvent.keyboard("{Escape}");
|
||||
const backdrop = document.querySelector<HTMLElement>(".prefs")!;
|
||||
fireEvent.mouseDown(backdrop);
|
||||
fireEvent.mouseUp(backdrop);
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
expect(form.isConnected).toBe(true);
|
||||
expect(field("名称").value).toBe("notes-kit");
|
||||
expect(form.getAttribute("aria-busy")).toBe("true");
|
||||
expect(publish).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ name: "notes-kit", visibility: privateDraft ? "private" : "public" }));
|
||||
await act(async () => {
|
||||
if (outcome === "success") pending.resolve(out);
|
||||
else pending.reject(new Error("registry temporarily unavailable"));
|
||||
});
|
||||
for (const button of [...controls, ...found]) expect(button.disabled).toBe(false);
|
||||
if (outcome === "success") {
|
||||
expect(published()?.textContent).toContain(out.package.slug);
|
||||
expect(screen.queryByRole("alert")).toBeNull();
|
||||
await userEvent.click(screen.getByRole("button", { name: t("再发布一个") }));
|
||||
expect(field("名称").value).toBe("");
|
||||
expect(field("来源地址").value).toBe("");
|
||||
} else {
|
||||
expect(screen.getByRole("alert").textContent).toContain("registry temporarily unavailable");
|
||||
expect(field("名称").value).toBe("notes-kit");
|
||||
expect(field("来源地址").value).toBe("https://example.com/notes-kit/SKILL.md");
|
||||
await userEvent.click(screen.getByRole("button", { name: t(privateDraft ? "保存为私有" : "提交审核") }));
|
||||
expect(published()?.textContent).toContain(out.package.slug);
|
||||
expect(publish).toHaveBeenCalledTimes(2);
|
||||
expect(screen.queryByRole("alert")).toBeNull();
|
||||
}
|
||||
await userEvent.click(controls[0]!);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each(["connection", "account"].flatMap((owner) => ["success", "failure"].map((outcome) => ({ owner, outcome }))))(
|
||||
"releases a changed $owner but does not let its late $outcome unlock a new submission",
|
||||
async ({ owner, outcome }) => {
|
||||
const port = new MockPort() as unknown as AgentPort;
|
||||
const next = new MockPort() as unknown as AgentPort;
|
||||
const out = await receipt(port);
|
||||
const old = deferred<MarketPublished>();
|
||||
const fresh = deferred<MarketPublished>();
|
||||
vi.spyOn(port, "publishMarket").mockImplementationOnce(() => old.promise).mockImplementationOnce(() => fresh.promise);
|
||||
vi.spyOn(next, "publishMarket").mockImplementation(() => fresh.promise);
|
||||
const applying = vi.fn();
|
||||
const group = (p: AgentPort, handle = "demo") => <MarketGroup port={p} account={account(handle)} onInstalled={() => {}} onSignIn={() => {}} onApplying={applying} />;
|
||||
const view = render(group(port));
|
||||
await userEvent.click(screen.getByRole("radio", { name: "发布" }));
|
||||
fill();
|
||||
await userEvent.click(screen.getByRole("button", { name: "提交审核" }));
|
||||
expect(applying).toHaveBeenLastCalledWith(true);
|
||||
view.rerender(group(owner === "connection" ? next : port, owner === "account" ? "other" : "demo"));
|
||||
expect(applying).toHaveBeenLastCalledWith(false);
|
||||
expect(screen.getByRole<HTMLButtonElement>("radio", { name: "浏览" }).disabled).toBe(false);
|
||||
fill("fresh-kit");
|
||||
await userEvent.click(screen.getByRole("button", { name: "提交审核" }));
|
||||
expect(applying).toHaveBeenLastCalledWith(true);
|
||||
const calls = applying.mock.calls.length;
|
||||
await act(async () => {
|
||||
if (outcome === "success") old.resolve(out);
|
||||
else old.reject(new Error("old publish refused"));
|
||||
});
|
||||
expect(applying).toHaveBeenCalledTimes(calls);
|
||||
expect(screen.getByRole<HTMLButtonElement>("radio", { name: "浏览" }).disabled).toBe(true);
|
||||
expect(screen.queryByRole("status")).toBeNull();
|
||||
expect(screen.queryByRole("alert")).toBeNull();
|
||||
await act(async () => fresh.resolve({ ...out, package: { ...out.package, name: "fresh-kit", slug: "demo/fresh-kit" } }));
|
||||
expect(applying).toHaveBeenLastCalledWith(false);
|
||||
expect(screen.getByRole<HTMLButtonElement>("radio", { name: "浏览" }).disabled).toBe(false);
|
||||
expect(published()?.textContent).toContain("demo/fresh-kit");
|
||||
},
|
||||
);
|
||||
|
||||
it("releases the navigation channel on unmount during a StrictMode submission", async () => {
|
||||
const port = new MockPort() as unknown as AgentPort;
|
||||
const out = await receipt(port);
|
||||
const pending = deferred<MarketPublished>();
|
||||
vi.spyOn(port, "publishMarket").mockImplementation(() => pending.promise);
|
||||
const applying = vi.fn();
|
||||
const view = render(<StrictMode><MarketGroup port={port} account={account()} onInstalled={() => {}} onSignIn={() => {}} onApplying={applying} /></StrictMode>);
|
||||
await userEvent.click(screen.getByRole("radio", { name: "发布" }));
|
||||
fill();
|
||||
await userEvent.click(screen.getByRole("button", { name: "提交审核" }));
|
||||
expect(applying).toHaveBeenLastCalledWith(true);
|
||||
view.unmount();
|
||||
expect(applying).toHaveBeenLastCalledWith(false);
|
||||
const calls = applying.mock.calls.length;
|
||||
await act(async () => pending.resolve(out));
|
||||
expect(applying).toHaveBeenCalledTimes(calls);
|
||||
});
|
||||
|
||||
it.each(["publish", "install"].flatMap((first) => ["success", "failure"].flatMap((outcome) =>
|
||||
[false, true].map((strict) => ({ first, outcome, strict })),
|
||||
)))("keeps views exclusive and the replacement hold during old $first $outcome (StrictMode=$strict)", async ({ first, outcome, strict }) => {
|
||||
const old = new MockPort() as unknown as AgentPort;
|
||||
const current = new MockPort() as unknown as AgentPort;
|
||||
const pkg = (await old.myMarket()).find((p) => p.slug === "demo/ship-notes")!;
|
||||
const shown = await old.planOwnMarket({ slug: pkg.slug });
|
||||
const installed: MarketPlan = { ...shown, applied: true, status: "done" };
|
||||
const out = await receipt(old);
|
||||
const oldPublish = deferred<MarketPublished>();
|
||||
const newPublish = deferred<MarketPublished>();
|
||||
const oldInstall = deferred<MarketPlan>();
|
||||
const newInstall = deferred<MarketPlan>();
|
||||
const publishOld = vi.spyOn(old, "publishMarket").mockReturnValue(oldPublish.promise);
|
||||
const publishNew = vi.spyOn(current, "publishMarket").mockReturnValue(newPublish.promise);
|
||||
const installOld = vi.spyOn(old, "installOwnMarket").mockReturnValue(oldInstall.promise);
|
||||
const installNew = vi.spyOn(current, "installOwnMarket").mockReturnValue(newInstall.promise);
|
||||
for (const port of [old, current]) {
|
||||
vi.spyOn(port, "myMarket").mockResolvedValue([pkg]);
|
||||
vi.spyOn(port, "planOwnMarket").mockResolvedValue(shown);
|
||||
}
|
||||
const applying = vi.fn();
|
||||
const onInstalled = vi.fn();
|
||||
const group = (port: AgentPort) => {
|
||||
const child = <MarketGroup port={port} account={account()} onInstalled={onInstalled} onSignIn={() => {}} onApplying={applying} />;
|
||||
return strict ? <StrictMode>{child}</StrictMode> : child;
|
||||
};
|
||||
const begin = async (kind: string) => {
|
||||
if (kind === "publish") {
|
||||
await userEvent.click(screen.getByRole("radio", { name: "发布" }));
|
||||
fill();
|
||||
await userEvent.click(screen.getByRole("button", { name: "提交审核" }));
|
||||
expect(document.querySelector("form.mkt-pub")).not.toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "安装中…" })).toBeNull();
|
||||
} else {
|
||||
await userEvent.click(screen.getByRole("radio", { name: "我的发布" }));
|
||||
const row = (await screen.findByText(pkg.name)).closest("li")!;
|
||||
await userEvent.click(row.querySelector<HTMLButtonElement>('[data-action="market.own-inspect"]')!);
|
||||
await userEvent.click(await screen.findByRole("button", { name: "安装" }));
|
||||
expect(screen.getByRole<HTMLButtonElement>("button", { name: "安装中…" }).disabled).toBe(true);
|
||||
expect(document.querySelector("form.mkt-pub")).toBeNull();
|
||||
}
|
||||
};
|
||||
const view = render(group(old));
|
||||
await begin(first);
|
||||
expect(applying).toHaveBeenLastCalledWith(true);
|
||||
for (const radio of screen.getAllByRole<HTMLButtonElement>("radio")) {
|
||||
expect(radio.disabled).toBe(true);
|
||||
await userEvent.click(radio);
|
||||
}
|
||||
expect(first === "publish" ? publishOld : installOld).toHaveBeenCalledTimes(1);
|
||||
expect(first === "publish" ? installOld : publishOld).not.toHaveBeenCalled();
|
||||
view.rerender(group(current));
|
||||
expect(applying).toHaveBeenLastCalledWith(false);
|
||||
await begin(first === "publish" ? "install" : "publish");
|
||||
expect(applying).toHaveBeenLastCalledWith(true);
|
||||
expect(first === "publish" ? installNew : publishNew).toHaveBeenCalledTimes(1);
|
||||
const calls = applying.mock.calls.length;
|
||||
await act(async () => {
|
||||
if (first === "publish") {
|
||||
if (outcome === "success") oldPublish.resolve(out);
|
||||
else oldPublish.reject(new Error("old publish refused"));
|
||||
} else {
|
||||
if (outcome === "success") oldInstall.resolve(installed);
|
||||
else oldInstall.reject(new Error("old install refused"));
|
||||
}
|
||||
});
|
||||
expect(applying).toHaveBeenCalledTimes(calls);
|
||||
expect(onInstalled).not.toHaveBeenCalled();
|
||||
expect(screen.queryByRole("alert")).toBeNull();
|
||||
for (const radio of screen.getAllByRole<HTMLButtonElement>("radio")) expect(radio.disabled).toBe(true);
|
||||
await act(async () => first === "publish" ? newInstall.resolve(installed) : newPublish.resolve(out));
|
||||
expect(applying).toHaveBeenLastCalledWith(false);
|
||||
for (const radio of screen.getAllByRole<HTMLButtonElement>("radio")) expect(radio.disabled).toBe(false);
|
||||
expect(onInstalled).toHaveBeenCalledTimes(first === "publish" ? 1 : 0);
|
||||
});
|
||||
Reference in new issue
Block a user