Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/web/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ export async function installCreationFixtures(
async verifyRegistry() {
return "verified";
},
async readFactoryOwner() {
return wallet;
},
},
};
},
Expand Down
181 changes: 67 additions & 114 deletions apps/web/src/components/CommunityDeploymentPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createWalletMock } from "@/test-support/stellar";

const mocks = vi.hoisted(() => ({
getE2EBridge: vi.fn(),
Expand Down Expand Up @@ -74,24 +73,41 @@ function adapter() {
}),
transactionStatus: vi.fn().mockResolvedValue("success"),
verifyRegistry: vi.fn().mockResolvedValue("verified"),
readFactoryOwner: vi.fn().mockResolvedValue(address),
};
}

const simulateButtons = () =>
screen.queryAllByRole("button", { name: "Simulate deployment" });
const approveButton = () =>
screen.queryByRole("button", { name: "Approve and deploy" });

/** The owner preflight resolves asynchronously to "ready" before any action. */
async function awaitReady() {
await waitFor(async () => {
expect(
simulateButtons().some((button) => !(button as HTMLButtonElement).disabled),
).toBe(true);
});
}

describe("CommunityDeploymentPanel", () => {
beforeEach(() => {
sessionStorage.clear();
mocks.useWallet.mockReturnValue(createWalletMock({
mocks.useWallet.mockReturnValue({
address,
signTransaction: vi.fn(),
walletNetwork: "testnet",
walletNetworkPassphrase: "Test SDF Network ; September 2015",
}));
});
});

it("shows the exact simulated fee and declares success only after registry verification", async () => {
const deployment = adapter();
mocks.getE2EBridge.mockReturnValue({ deployment });
render(<CommunityDeploymentPanel {...props} />);

await awaitReady();
fireEvent.click(screen.getByRole("button", { name: "Simulate deployment" }));
expect(await screen.findByText(/12345678 stroops/)).toHaveTextContent(
"1.2345678 XLM",
Expand All @@ -111,11 +127,12 @@ describe("CommunityDeploymentPanel", () => {
it("blocks a mismatched wallet network and preserves draft inputs", () => {
const deployment = adapter();
mocks.getE2EBridge.mockReturnValue({ deployment });
mocks.useWallet.mockReturnValue(createWalletMock({
mocks.useWallet.mockReturnValue({
address,
signTransaction: vi.fn(),
walletNetwork: "mainnet",
walletNetworkPassphrase: "Public Global Stellar Network ; September 2015",
}));
});
render(<CommunityDeploymentPanel {...props} />);

expect(screen.getByText(/Expected testnet/)).toHaveTextContent(
Expand All @@ -133,6 +150,7 @@ describe("CommunityDeploymentPanel", () => {
mocks.getE2EBridge.mockReturnValue({ deployment });
render(<CommunityDeploymentPanel {...props} />);

await awaitReady();
fireEvent.click(screen.getByRole("button", { name: "Simulate deployment" }));
await screen.findByText(/12345678 stroops/);
fireEvent.click(screen.getByRole("button", { name: "Approve and deploy" }));
Expand Down Expand Up @@ -195,147 +213,82 @@ describe("CommunityDeploymentPanel", () => {
expect(deployment.signAndSubmit).not.toHaveBeenCalled();
});

it("invalidates a completed simulation when the wallet network changes mid-flow", async () => {
it("blocks the deploy approval action for a non-owner wallet", async () => {
const deployment = adapter();
const other = `G${"B".repeat(55)}`;
mocks.useWallet.mockReturnValue({
address: other,
signTransaction: vi.fn(),
walletNetwork: "testnet",
walletNetworkPassphrase: "Test SDF Network ; September 2015",
});
mocks.getE2EBridge.mockReturnValue({ deployment });
const { rerender } = render(<CommunityDeploymentPanel {...props} />);
render(<CommunityDeploymentPanel {...props} />);

await screen.findByText(/Only the CommunityFactory owner can create communities/);
expect(
screen.getByRole("button", { name: "Simulate deployment" }),
).toBeDisabled();
fireEvent.click(screen.getByRole("button", { name: "Simulate deployment" }));
expect(await screen.findByText(/12345678 stroops/)).toBeInTheDocument();
expect(deployment.simulate).not.toHaveBeenCalled();
expect(approveButton()).not.toBeInTheDocument();
});

it("reports a disconnected wallet as disconnected and holds actions", async () => {
mocks.useWallet.mockReturnValue({
address,
address: null,
signTransaction: vi.fn(),
walletNetwork: "mainnet",
walletNetworkPassphrase: "Public Global Stellar Network ; September 2015",
walletNetwork: null,
walletNetworkPassphrase: null,
});
rerender(<CommunityDeploymentPanel {...props} />);
render(<CommunityDeploymentPanel {...props} />);

await waitFor(() =>
expect(screen.queryByText(/12345678 stroops/)).not.toBeInTheDocument(),
);
expect(
screen.getByText(/Network changed. The previous simulation was invalidated/),
await screen.findByText(/Connect your wallet to check whether this account can create a community/),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Simulate deployment" }),
).toBeDisabled();
expect(deployment.signAndSubmit).not.toHaveBeenCalled();
});

it("requires a fresh simulation after restoring the expected network", async () => {
it("is network-aware and never reports a mismatched wallet as unauthorized", async () => {
const deployment = adapter();
mocks.getE2EBridge.mockReturnValue({ deployment });
const { rerender } = render(<CommunityDeploymentPanel {...props} />);

fireEvent.click(screen.getByRole("button", { name: "Simulate deployment" }));
await screen.findByText(/12345678 stroops/);

mocks.useWallet.mockReturnValue({
address,
signTransaction: vi.fn(),
walletNetwork: "mainnet",
walletNetworkPassphrase: "Public Global Stellar Network ; September 2015",
});
rerender(<CommunityDeploymentPanel {...props} />);
await waitFor(() =>
expect(screen.queryByText(/12345678 stroops/)).not.toBeInTheDocument(),
);

mocks.useWallet.mockReturnValue({
address,
signTransaction: vi.fn(),
walletNetwork: "testnet",
walletNetworkPassphrase: "Test SDF Network ; September 2015",
});
rerender(<CommunityDeploymentPanel {...props} />);

await waitFor(() =>
expect(
screen.getByRole("button", { name: "Simulate deployment" }),
).toBeEnabled(),
);
expect(screen.queryByRole("button", { name: "Approve and deploy" })).not.toBeInTheDocument();

fireEvent.click(screen.getByRole("button", { name: "Simulate deployment" }));
await waitFor(() => expect(deployment.simulate).toHaveBeenCalledTimes(2));
expect(await screen.findByRole("button", { name: "Approve and deploy" })).toBeEnabled();
});

it("drops a simulation that resolves after the wallet network moved", async () => {
const deployment = adapter();
let resolveSimulation: (() => void) | undefined;
deployment.simulate.mockImplementation(
() =>
new Promise((resolve) => {
resolveSimulation = () =>
resolve({
invocation: {
contractId: props.factoryId,
method: "create_community",
sourceAccount: address,
networkPassphrase: "Test SDF Network ; September 2015",
metadataHash: "12".repeat(32),
externalKey: "12".repeat(32),
args: [],
},
feeStroops: "12345678",
expectedRecord,
sequence: "2",
expiresAt: 999,
prepared: {},
});
}),
);
mocks.getE2EBridge.mockReturnValue({ deployment });
const { rerender } = render(<CommunityDeploymentPanel {...props} />);

fireEvent.click(screen.getByRole("button", { name: "Simulate deployment" }));
mocks.useWallet.mockReturnValue({
address,
signTransaction: vi.fn(),
walletNetwork: "mainnet",
walletNetworkPassphrase: "Public Global Stellar Network ; September 2015",
});
rerender(<CommunityDeploymentPanel {...props} />);
resolveSimulation?.();
render(<CommunityDeploymentPanel {...props} />);

await waitFor(() =>
expect(
screen.getByRole("button", { name: "Simulate deployment" }),
).toBeDisabled(),
expect(await screen.findByText(/Expected testnet/)).toHaveTextContent(
"Detected mainnet",
);
expect(screen.queryByText(/12345678 stroops/)).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Approve and deploy" })).not.toBeInTheDocument();
expect(deployment.readFactoryOwner).not.toHaveBeenCalled();
expect(
screen.queryByText(/cannot create/i),
).not.toBeInTheDocument();
expect(
screen.queryByText(/only the communityfactory owner/i),
).not.toBeInTheDocument();
});

it("keeps the submitted transaction explorer link on the network it was submitted to", async () => {
it("treats a factory owner read failure as retryable, not unauthorized", async () => {
const deployment = adapter();
deployment.readFactoryOwner.mockRejectedValueOnce(new Error("RPC unavailable"));
mocks.getE2EBridge.mockReturnValue({ deployment });
const { rerender } = render(<CommunityDeploymentPanel {...props} />);
render(<CommunityDeploymentPanel {...props} />);

fireEvent.click(screen.getByRole("button", { name: "Simulate deployment" }));
await screen.findByText(/12345678 stroops/);
fireEvent.click(screen.getByRole("button", { name: "Approve and deploy" }));
const retry = await screen.findByRole("button", { name: "Retry owner check" });
expect(
await screen.findByRole("heading", {
name: "Community verified in the registry",
}),
).toBeInTheDocument();

mocks.useWallet.mockReturnValue({
address,
signTransaction: vi.fn(),
walletNetwork: "mainnet",
walletNetworkPassphrase: "Public Global Stellar Network ; September 2015",
});
rerender(<CommunityDeploymentPanel {...props} />);
screen.queryByText(/only the communityfactory owner|cannot create/i),
).not.toBeInTheDocument();

expect(
screen.getByRole("heading", {
name: "Community verified in the registry",
}),
).toBeInTheDocument();
expect(deployment.signAndSubmit).toHaveBeenCalledTimes(1);
deployment.readFactoryOwner.mockResolvedValueOnce(address);
fireEvent.click(retry);
await awaitReady();
expect(deployment.readFactoryOwner).toHaveBeenCalledTimes(2);
expect(screen.queryByText("Retry owner check")).not.toBeInTheDocument();
});
});
Loading