Skip to content
Open
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
30 changes: 30 additions & 0 deletions __tests__/components/FeeBreakdownBottomSheet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,34 @@ describe("FeeBreakdownBottomSheet", () => {
expect(queryByText(`${BASE_FEE} XLM`)).toBeNull();
});
});

describe("inclusion fee override (preview)", () => {
it("previews the override inclusion fee and total without using the stored fee", () => {
mockUseTransactionBuilderStore.mockReturnValue({
...defaultBuilderState,
sorobanInclusionFeeXlm: INCLUSION_FEE,
sorobanResourceFeeXlm: RESOURCE_FEE,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);

const overrideFee = "0.027";
const { getByText, queryByText } = render(
<FeeBreakdownBottomSheet
onClose={mockOnClose}
isSorobanContext
inclusionFeeXlmOverride={overrideFee}
/>,
);

// Inclusion reflects the in-progress override, not the stored fee.
expect(getByText(`${overrideFee} XLM`)).toBeTruthy();
expect(queryByText(`${INCLUSION_FEE} XLM`)).toBeNull();

// Total = override inclusion + resource fee.
const expectedTotal = (parseFloat(overrideFee) + parseFloat(RESOURCE_FEE))
.toFixed(7)
.replace(/\.?0+$/, "");
expect(getByText(`${expectedTotal} XLM`)).toBeTruthy();
});
});
});
237 changes: 232 additions & 5 deletions __tests__/components/TransactionSettingsBottomSheet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jest.mock("ducks/swapSettings", () => ({
saveSwapFee: jest.fn(),
saveSwapTimeout: jest.fn(),
saveSwapSlippage: jest.fn(),
feePriority: "medium",
saveFeePriority: jest.fn(),
})),
}));
jest.mock("hooks/useInitialRecommendedFee", () => ({
Expand All @@ -48,18 +50,29 @@ jest.mock("hooks/useColors", () => ({
foreground: { primary: "#000000" },
gray: { 8: "#666666" },
status: { error: "#ff0000", warning: "#ffaa00" },
background: { primary: "#ffffff" },
background: { primary: "#ffffff", tertiary: "#f0f0f0" },
text: { secondary: "#999999" },
lilac: {
9: "#6e56cf",
11: "#9d8bff",
},
},
}),
}));
// Mutable so individual tests can simulate the 30s-poll refetch returning new
// preset values (must be `mock`-prefixed to be usable inside the jest.mock factory).
const mockDefaultNetworkFees = {
recommendedFee: "100",
networkCongestion: "LOW",
feePresets: {
low: "0.0001",
medium: "0.001",
high: "0.01",
},
};
let mockNetworkFees = mockDefaultNetworkFees;
jest.mock("hooks/useNetworkFees", () => ({
useNetworkFees: () => ({
recommendedFee: "100",
networkCongestion: "LOW",
}),
useNetworkFees: () => mockNetworkFees,
}));
jest.mock("hooks/useValidateMemo", () => ({
useValidateMemo: () => ({ error: null }),
Expand All @@ -82,6 +95,18 @@ jest.mock("@gorhom/bottom-sheet", () => ({
jest.mock("helpers/soroban", () => ({
isContractId: jest.fn(),
isSorobanTransaction: jest.fn(),
computeTotalFeeXlm: jest.fn(() => "0"),
}));

// The fee info icon mounts FeeBreakdownBottomSheet (via useFeeDetailsBottomSheet),
// which reads the transaction builder store.
jest.mock("ducks/transactionBuilder", () => ({
useTransactionBuilderStore: jest.fn(() => ({
sorobanResourceFeeXlm: null,
sorobanInclusionFeeXlm: null,
isBuilding: false,
error: null,
})),
}));

jest.mock("services/backend", () => ({
Expand Down Expand Up @@ -137,9 +162,11 @@ describe("TransactionSettingsBottomSheet - onSettingsChange Integration", () =>
collectionAddress: "",
tokenId: "",
},
feePriority: "medium",
saveMemo: jest.fn(),
saveTransactionFee: jest.fn(),
saveTransactionTimeout: jest.fn(),
saveFeePriority: jest.fn(),
saveRecipientAddress: jest.fn(),
saveSelectedTokenId: jest.fn(),
saveSelectedCollectibleDetails: jest.fn(),
Expand All @@ -148,6 +175,7 @@ describe("TransactionSettingsBottomSheet - onSettingsChange Integration", () =>

beforeEach(() => {
jest.clearAllMocks();
mockNetworkFees = mockDefaultNetworkFees;
mockUseTransactionSettingsStore.mockReturnValue(
mockTransactionSettingsState,
);
Expand Down Expand Up @@ -289,6 +317,203 @@ describe("TransactionSettingsBottomSheet - onSettingsChange Integration", () =>
"Required memo for GA6SXIZIKLJHCZI2KEOBEUUOFMM4JUPPM2UTWX6STAWT25JWIEUFIMFF",
);
});

it("sets the fee to the High preset when the High tab is pressed", async () => {
const { getByText } = renderWithProviders(
<TransactionSettingsBottomSheet
onCancel={mockOnCancel}
onConfirm={mockOnConfirm}
context={TransactionContext.Send}
onSettingsChange={mockOnSettingsChange}
/>,
);

fireEvent.press(getByText("transactionSettings.priorityHigh"));
fireEvent.press(getByText("common.save"));

await waitFor(() => {
expect(
mockTransactionSettingsState.saveTransactionFee,
).toHaveBeenCalledWith("0.01");
// The chosen tier is persisted so it survives refetches and re-entry.
expect(mockTransactionSettingsState.saveFeePriority).toHaveBeenCalledWith(
"high",
);
});
});

it("sets the fee to the Low preset when the Low tab is pressed", async () => {
const { getByText } = renderWithProviders(
<TransactionSettingsBottomSheet
onCancel={mockOnCancel}
onConfirm={mockOnConfirm}
context={TransactionContext.Send}
onSettingsChange={mockOnSettingsChange}
/>,
);

fireEvent.press(getByText("transactionSettings.priorityLow"));
fireEvent.press(getByText("common.save"));

await waitFor(() => {
expect(
mockTransactionSettingsState.saveTransactionFee,
).toHaveBeenCalledWith("0.0001");
});
});

it("locks the fee input for presets and unlocks it for Custom", async () => {
const { getByText, getByTestId } = renderWithProviders(
<TransactionSettingsBottomSheet
onCancel={mockOnCancel}
onConfirm={mockOnConfirm}
context={TransactionContext.Send}
onSettingsChange={mockOnSettingsChange}
/>,
);

// The fee hasn't been manually changed, so the default tier is Med and the
// input is locked.
expect(getByTestId("fee-input").props.editable).toBe(false);

// "Custom" unlocks the input for manual entry.
fireEvent.press(getByText("transactionSettings.priorityCustom"));
await waitFor(() => {
expect(getByTestId("fee-input").props.editable).toBe(true);
});

// Selecting a preset locks it again.
fireEvent.press(getByText("transactionSettings.priorityLow"));
await waitFor(() => {
expect(getByTestId("fee-input").props.editable).toBe(false);
});
});

it("opening the fee details does not persist the fee (preview only)", async () => {
mockIsContractId.mockReturnValue(true);

const { getByText, getByTestId } = renderWithProviders(
<TransactionSettingsBottomSheet
onCancel={mockOnCancel}
onConfirm={mockOnConfirm}
context={TransactionContext.Send}
onSettingsChange={mockOnSettingsChange}
/>,
);

// Pick the High preset, then open the fee details via the info icon.
fireEvent.press(getByText("transactionSettings.priorityHigh"));
fireEvent.press(getByTestId("fee-info-button"));

// The fee is only persisted on Save, not when opening the details.
await waitFor(() => {
expect(
mockTransactionSettingsState.saveTransactionFee,
).not.toHaveBeenCalled();
});
});

it("opens on the stored fee priority tier (preset → input locked)", async () => {
// The sheet reflects the persisted tier directly — a stored High tier opens
// locked, regardless of how the fee amount compares to the current presets.
mockUseTransactionSettingsStore.mockReturnValue({
...mockTransactionSettingsState,
feePriority: "high",
});

const { getByTestId } = renderWithProviders(
<TransactionSettingsBottomSheet
onCancel={mockOnCancel}
onConfirm={mockOnConfirm}
context={TransactionContext.Send}
onSettingsChange={mockOnSettingsChange}
/>,
);

await waitFor(() => {
expect(getByTestId("fee-input").props.editable).toBe(false);
});
});

it("opens editable when the stored tier is Custom", async () => {
mockUseTransactionSettingsStore.mockReturnValue({
...mockTransactionSettingsState,
feePriority: "custom",
});

const { getByTestId } = renderWithProviders(
<TransactionSettingsBottomSheet
onCancel={mockOnCancel}
onConfirm={mockOnConfirm}
context={TransactionContext.Send}
onSettingsChange={mockOnSettingsChange}
/>,
);

await waitFor(() => {
expect(getByTestId("fee-input").props.editable).toBe(true);
});
});

it("follows the stored tier when it updates before the user interacts", async () => {
// Stored tier starts Custom (editable); then the store updates (e.g. the
// frozen congestion snapshot lands) and the tab follows + locks the input.
mockUseTransactionSettingsStore.mockReturnValue({
...mockTransactionSettingsState,
feePriority: "custom",
});
const props = {
onCancel: mockOnCancel,
onConfirm: mockOnConfirm,
context: TransactionContext.Send,
onSettingsChange: mockOnSettingsChange,
};
const { getByTestId, rerender } = renderWithProviders(
<TransactionSettingsBottomSheet {...props} />,
);
await waitFor(() => {
expect(getByTestId("fee-input").props.editable).toBe(true);
});

mockUseTransactionSettingsStore.mockReturnValue({
...mockTransactionSettingsState,
feePriority: "high",
});
rerender(<TransactionSettingsBottomSheet {...props} />);

await waitFor(() => {
expect(getByTestId("fee-input").props.editable).toBe(false);
});
});

it("keeps the selected tier when network presets refetch (no flicker to Custom)", async () => {
// Stored tier is Med (locked input).
const props = {
onCancel: mockOnCancel,
onConfirm: mockOnConfirm,
context: TransactionContext.Send,
onSettingsChange: mockOnSettingsChange,
};
const { getByTestId, rerender } = renderWithProviders(
<TransactionSettingsBottomSheet {...props} />,
);

await waitFor(() => {
expect(getByTestId("fee-input").props.editable).toBe(false);
});

// Simulate the 30s poll returning different preset values.
mockNetworkFees = {
...mockDefaultNetworkFees,
feePresets: { low: "0.0002", medium: "0.0021", high: "0.02" },
};
rerender(<TransactionSettingsBottomSheet {...props} />);

// The tier stays Med (input still locked) — it does NOT flip to Custom.
await waitFor(() => {
expect(getByTestId("fee-input").props.editable).toBe(false);
});
});
});

describe("TransactionSettingsBottomSheet - Soroban Transaction Tests", () => {
Expand All @@ -308,9 +533,11 @@ describe("TransactionSettingsBottomSheet - Soroban Transaction Tests", () => {
collectionAddress: "",
tokenId: "",
},
feePriority: "medium",
saveMemo: jest.fn(),
saveTransactionFee: jest.fn(),
saveTransactionTimeout: jest.fn(),
saveFeePriority: jest.fn(),
saveRecipientAddress: jest.fn(),
saveSelectedTokenId: jest.fn(),
saveSelectedCollectibleDetails: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ jest.mock("ducks/swapSettings", () => ({
saveSwapFee: mockSaveSwapFee,
feeManuallyChanged: false,
markFeeManuallyChanged: jest.fn(),
feePriority: "medium",
saveFeePriority: jest.fn(),
})),
}));
// Deterministic network fee so the fee-freeze behavior is testable and
Expand All @@ -193,6 +195,7 @@ jest.mock("hooks/useNetworkFees", () => ({
recommendedFee: "0.001",
networkCongestion: "LOW",
}),
clearNetworkFeesCache: jest.fn(),
}));
jest.mock("components/screens/SwapScreen/hooks/useSwapTransaction", () => ({
useSwapTransaction: jest.fn(() => ({
Expand Down
12 changes: 12 additions & 0 deletions __tests__/ducks/transactionSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
DEFAULT_TRANSACTION_TIMEOUT,
MIN_TRANSACTION_FEE,
} from "config/constants";
import { FeePriority } from "config/types";
import { useTransactionSettingsStore } from "ducks/transactionSettings";

const store = useTransactionSettingsStore;
Expand All @@ -23,6 +24,14 @@ describe("transactionSettings Duck", () => {
expect(initialState.federationAddress).toBe("");
expect(initialState.recipientName).toBe("");
expect(initialState.selectedTokenId).toBe("");
expect(initialState.feePriority).toBe(FeePriority.MEDIUM);
});

it("should save fee priority", () => {
act(() => {
store.getState().saveFeePriority(FeePriority.HIGH);
});
expect(store.getState().feePriority).toBe(FeePriority.HIGH);
});

it("should save memo", () => {
Expand Down Expand Up @@ -109,6 +118,7 @@ describe("transactionSettings Duck", () => {
store.getState().saveFederationAddress(newFederationAddress);
store.getState().saveRecipientName(newRecipientName);
store.getState().saveSelectedTokenId(newTokenId);
store.getState().saveFeePriority(FeePriority.HIGH);
});

expect(store.getState().transactionMemo).toBe(newMemo);
Expand All @@ -118,6 +128,7 @@ describe("transactionSettings Duck", () => {
expect(store.getState().federationAddress).toBe(newFederationAddress);
expect(store.getState().recipientName).toBe(newRecipientName);
expect(store.getState().selectedTokenId).toBe(newTokenId);
expect(store.getState().feePriority).toBe(FeePriority.HIGH);

act(() => {
store.getState().resetSettings();
Expand All @@ -132,6 +143,7 @@ describe("transactionSettings Duck", () => {
expect(store.getState().federationAddress).toBe("");
expect(store.getState().recipientName).toBe("");
expect(store.getState().selectedTokenId).toBe("");
expect(store.getState().feePriority).toBe(FeePriority.MEDIUM);
});

describe("selectedCollectibleDetails", () => {
Expand Down
Loading
Loading