Skip to content

Commit c95004f

Browse files
piyalbasuCopilot
andauthored
expand contract id on screen width change (#2588)
* Make e2e tests more reliable (#2562) * refactor e2e tests for scalability * restore missing tests * remove extra stubbing * test context * test context * allow only * try moving context * test ci * try moving stub inside test * Revert "try moving stub inside test" This reverts commit 0f1dafa. * move inside login * try moving context inside login * try just 2 test * try a few more * try tests with ai tips * can we use multiple workers * reduce workers * try 3 workers * try larger machine * try more workers and fix flake * 5 workers * readd stubs; decrease workers * rm unused imports * fix integration tests * rm debugging stuff * add better documentation * copilot pr comments * fix flakey sendcollectibles tests * add readme * expand contract id on screen width change * elaborate on comment * restore unrelated changes * Update extension/src/helpers/hooks/useIsWideScreen.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * pr comment * fix flakey test * fix another flakey test --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 7b05448 commit c95004f

7 files changed

Lines changed: 248 additions & 15 deletions

File tree

config/jest/setupTests.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,26 @@ Object.defineProperty(global.self, "crypto", {
3232
},
3333
});
3434

35+
Object.defineProperty(window, "matchMedia", {
36+
writable: true,
37+
value: jest.fn().mockImplementation((query: string) => ({
38+
matches: false,
39+
media: query,
40+
addEventListener: jest.fn(),
41+
removeEventListener: jest.fn(),
42+
})),
43+
});
44+
45+
Object.defineProperty(global, "matchMedia", {
46+
writable: true,
47+
value: jest.fn().mockImplementation((query: string) => ({
48+
matches: false,
49+
media: query,
50+
addEventListener: jest.fn(),
51+
removeEventListener: jest.fn(),
52+
})),
53+
});
54+
3555
process.env.INDEXER_URL = "http://localhost:3002/api/v1";
3656
process.env.INDEXER_V2_URL = "http://localhost:3003/api/v1";
3757

extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,8 @@ test("should sign auth entry for a selected account when allowed", async ({
403403
const pageTwo = await page.context().newPage();
404404
await pageTwo.waitForLoadState();
405405

406-
page.getByTestId("account-view-account-name").click();
407-
page.getByText("Account 2").click();
406+
await page.getByTestId("account-view-account-name").click();
407+
await page.getByText("Account 2").click();
408408
await expect(page.getByTestId("account-header")).toBeVisible();
409409
await allowDapp({ page });
410410

@@ -633,8 +633,8 @@ test("should sign message for a specific account when allowed", async ({
633633
const pageTwo = await page.context().newPage();
634634
await pageTwo.waitForLoadState();
635635

636-
page.getByTestId("account-view-account-name").click();
637-
page.getByText("Account 2").click();
636+
await page.getByTestId("account-view-account-name").click();
637+
await page.getByText("Account 2").click();
638638
await expect(page.getByTestId("account-header")).toBeVisible();
639639
await allowDapp({ page });
640640

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { renderHook, act } from "@testing-library/react";
2+
import { useIsLargeWidthScreen } from "../hooks/useIsLargeWidthScreen";
3+
4+
describe("useIsLargeWidthScreen", () => {
5+
let listeners: Array<(e: { matches: boolean }) => void>;
6+
let currentMatches: boolean;
7+
8+
beforeEach(() => {
9+
listeners = [];
10+
currentMatches = false;
11+
12+
Object.defineProperty(window, "matchMedia", {
13+
writable: true,
14+
value: jest.fn().mockImplementation((query: string) => ({
15+
matches: currentMatches,
16+
media: query,
17+
addEventListener: jest.fn(
18+
(_event: string, handler: (e: { matches: boolean }) => void) => {
19+
listeners.push(handler);
20+
},
21+
),
22+
removeEventListener: jest.fn(
23+
(_event: string, handler: (e: { matches: boolean }) => void) => {
24+
listeners = listeners.filter((l) => l !== handler);
25+
},
26+
),
27+
})),
28+
});
29+
});
30+
31+
it("returns false when viewport is below the default threshold", () => {
32+
currentMatches = false;
33+
const { result } = renderHook(() => useIsLargeWidthScreen());
34+
expect(result.current).toBe(false);
35+
});
36+
37+
it("returns true when viewport is at or above the default threshold", () => {
38+
currentMatches = true;
39+
const { result } = renderHook(() => useIsLargeWidthScreen());
40+
expect(result.current).toBe(true);
41+
});
42+
43+
it("accepts a custom minWidth", () => {
44+
currentMatches = true;
45+
renderHook(() => useIsLargeWidthScreen(1024));
46+
expect(window.matchMedia).toHaveBeenCalledWith("(min-width: 1024px)");
47+
});
48+
49+
it("updates when the media query match state changes", () => {
50+
currentMatches = false;
51+
const { result } = renderHook(() => useIsLargeWidthScreen());
52+
expect(result.current).toBe(false);
53+
54+
act(() => {
55+
listeners.forEach((l) => l({ matches: true }));
56+
});
57+
expect(result.current).toBe(true);
58+
59+
act(() => {
60+
listeners.forEach((l) => l({ matches: false }));
61+
});
62+
expect(result.current).toBe(false);
63+
});
64+
65+
it("cleans up the listener on unmount", () => {
66+
const { unmount } = renderHook(() => useIsLargeWidthScreen());
67+
expect(listeners).toHaveLength(1);
68+
unmount();
69+
expect(listeners).toHaveLength(0);
70+
});
71+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useEffect, useState } from "react";
2+
3+
/**
4+
* Reactively tracks whether the viewport meets a minimum width threshold.
5+
* Updates automatically when the browser window is resized across the breakpoint.
6+
*
7+
* @param minWidth - Minimum viewport width in pixels (default: 780 to accommodate full Contract ID display in Sign Transaction flow)
8+
* @returns `true` when the viewport is at or above `minWidth`, `false` otherwise
9+
*/
10+
export const useIsLargeWidthScreen = (minWidth = 780) => {
11+
const query = `(min-width: ${minWidth}px)`;
12+
const [matches, setMatches] = useState(
13+
() => window.matchMedia(query).matches,
14+
);
15+
16+
useEffect(() => {
17+
const mql = window.matchMedia(query);
18+
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
19+
20+
mql.addEventListener("change", handler);
21+
return () => mql.removeEventListener("change", handler);
22+
}, [query]);
23+
24+
return matches;
25+
};

extension/src/popup/components/__tests__/OperationsKeyVal.test.tsx

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from "react";
2-
import { render, waitFor, screen } from "@testing-library/react";
3-
import { Address, Keypair, Operation, xdr } from "stellar-sdk";
2+
import { render, waitFor, screen, cleanup } from "@testing-library/react";
3+
import { Address, Keypair, Operation, StrKey, xdr } from "stellar-sdk";
44

55
import { mockAccounts, TEST_PUBLIC_KEY, Wrapper } from "popup/__testHelpers__";
66
import { KeyValueInvokeHostFn } from "../signTransaction/Operations/KeyVal";
@@ -147,5 +147,104 @@ describe("Operations KeyVal", () => {
147147
);
148148
expect(execTypeValue).toHaveTextContent("contractExecutableStellarAsset");
149149
});
150+
151+
describe("invoke contract - Contract ID truncation", () => {
152+
const CONTRACT =
153+
"CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE";
154+
let matchMediaListeners: Array<(e: { matches: boolean }) => void>;
155+
let currentMatches: boolean;
156+
157+
function mockMatchMedia(matches: boolean) {
158+
currentMatches = matches;
159+
matchMediaListeners = [];
160+
Object.defineProperty(window, "matchMedia", {
161+
writable: true,
162+
value: jest.fn().mockImplementation((query: string) => ({
163+
matches: currentMatches,
164+
media: query,
165+
addEventListener: jest.fn(
166+
(_event: string, handler: (e: { matches: boolean }) => void) => {
167+
matchMediaListeners.push(handler);
168+
},
169+
),
170+
removeEventListener: jest.fn(),
171+
})),
172+
});
173+
}
174+
175+
function buildInvokeContractOp() {
176+
const func = xdr.HostFunction.hostFunctionTypeInvokeContract(
177+
new xdr.InvokeContractArgs({
178+
contractAddress: xdr.ScAddress.scAddressTypeContract(
179+
StrKey.decodeContract(CONTRACT) as any,
180+
),
181+
functionName: Buffer.from("transfer"),
182+
args: [],
183+
}),
184+
);
185+
return { func } as Operation.InvokeHostFunction;
186+
}
187+
188+
afterEach(() => {
189+
cleanup();
190+
});
191+
192+
it("truncates the Contract ID on narrow screens", async () => {
193+
mockMatchMedia(false);
194+
const op = buildInvokeContractOp();
195+
render(<KeyValueInvokeHostFn op={op} />);
196+
await waitFor(() => screen.getAllByTestId("OperationKeyVal"));
197+
198+
const contractIdLabel = screen.getByText("Contract ID");
199+
const contractIdValue = contractIdLabel.parentNode?.querySelector(
200+
"[data-testid='OperationKeyVal__value']",
201+
);
202+
expect(contractIdValue).toHaveTextContent("CA3D…GAXE");
203+
expect(contractIdValue).not.toHaveTextContent(CONTRACT);
204+
});
205+
206+
it("shows the full Contract ID on wide screens", async () => {
207+
mockMatchMedia(true);
208+
const op = buildInvokeContractOp();
209+
render(<KeyValueInvokeHostFn op={op} />);
210+
await waitFor(() => screen.getAllByTestId("OperationKeyVal"));
211+
212+
const contractIdLabel = screen.getByText("Contract ID");
213+
const contractIdValue = contractIdLabel.parentNode?.querySelector(
214+
"[data-testid='OperationKeyVal__value']",
215+
);
216+
expect(contractIdValue).toHaveTextContent(CONTRACT);
217+
});
218+
219+
it("applies the expanded class only on wide screens", async () => {
220+
mockMatchMedia(true);
221+
const op = buildInvokeContractOp();
222+
render(<KeyValueInvokeHostFn op={op} />);
223+
await waitFor(() => screen.getAllByTestId("OperationKeyVal"));
224+
225+
const contractIdLabel = screen.getByText("Contract ID");
226+
const contractIdValue = contractIdLabel.parentNode?.querySelector(
227+
"[data-testid='OperationKeyVal__value']",
228+
);
229+
expect(contractIdValue?.className).toContain(
230+
"Operations__pair--value-expanded",
231+
);
232+
});
233+
234+
it("does not apply the expanded class on narrow screens", async () => {
235+
mockMatchMedia(false);
236+
const op = buildInvokeContractOp();
237+
render(<KeyValueInvokeHostFn op={op} />);
238+
await waitFor(() => screen.getAllByTestId("OperationKeyVal"));
239+
240+
const contractIdLabel = screen.getByText("Contract ID");
241+
const contractIdValue = contractIdLabel.parentNode?.querySelector(
242+
"[data-testid='OperationKeyVal__value']",
243+
);
244+
expect(contractIdValue?.className).not.toContain(
245+
"Operations__pair--value-expanded",
246+
);
247+
});
248+
});
150249
});
151250
});

extension/src/popup/components/signTransaction/Operations/KeyVal/index.tsx

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { CLAIM_PREDICATES } from "constants/transaction";
2020
import { KeyIdenticon } from "popup/components/identicons/KeyIdenticon";
2121
import { CopyValue } from "popup/components/CopyValue";
2222
import { truncateString } from "helpers/stellar";
23+
import { useIsLargeWidthScreen } from "helpers/hooks/useIsLargeWidthScreen";
2324
import { formattedBuffer } from "popup/helpers/formatters";
2425

2526
import {
@@ -469,6 +470,7 @@ export const KeyValueInvokeHostFn = ({
469470
op: Operation.InvokeHostFunction;
470471
}) => {
471472
const { t } = useTranslation();
473+
const isWide = useIsLargeWidthScreen();
472474
const hostfn = op.func;
473475

474476
function renderDetails() {
@@ -641,15 +643,27 @@ export const KeyValueInvokeHostFn = ({
641643
operationKey={t("Type")}
642644
operationValue={t("Invoke Contract")}
643645
/>
644-
<KeyValueList
645-
operationKey={t("Contract ID")}
646-
operationValue={
647-
<CopyValue
648-
value={contractId}
649-
displayValue={truncateString(contractId)}
650-
/>
651-
}
652-
/>
646+
<div className="Operations__pair" data-testid="OperationKeyVal">
647+
<div
648+
className="Operations__pair--key"
649+
data-testid="OperationKeyVal__key"
650+
>
651+
{t("Contract ID")}
652+
</div>
653+
<div
654+
className={`Operations__pair--value${isWide ? " Operations__pair--value-expanded" : ""}`}
655+
data-testid="OperationKeyVal__value"
656+
>
657+
<span className="Operations__pair--value-text">
658+
<CopyValue
659+
value={contractId}
660+
displayValue={
661+
isWide ? contractId : truncateString(contractId)
662+
}
663+
/>
664+
</span>
665+
</div>
666+
</div>
653667
<KeyValueList
654668
operationKey={t("Function Name")}
655669
operationValue={fnName}

extension/src/popup/components/signTransaction/Operations/styles.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@
6363
align-items: center;
6464
text-align: right;
6565

66+
&.Operations__pair--value-expanded {
67+
flex: auto;
68+
}
69+
6670
.Operations__pair--value-text {
6771
display: block;
6872
overflow: hidden;

0 commit comments

Comments
 (0)