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
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ export const AllowanceIssuedRow: React.FunctionComponent<Props> = ({ allowance,
}}
/>
</div>
<Button variant="ghost" size="icon" onClick={() => onEditAllowance(allowance)}>
<Button variant="ghost" size="icon" onClick={() => onEditAllowance(allowance)} aria-label="Edit Authorization">
<Edit className="text-xs" />
</Button>
<Button variant="ghost" size="icon" onClick={() => setDeletingAllowance(allowance)}>
<Button variant="ghost" size="icon" onClick={() => setDeletingAllowance(allowance)} aria-label="Revoke Authorization">
<Bin className="text-xs" />
</Button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,14 @@ export const Authorizations: React.FunctionComponent = () => {
<div className="flex flex-wrap items-center py-4">
<Title>Tx Fee Authorizations</Title>
{address && (
<Button onClick={onCreateNewAllowance} color="secondary" variant="default" className="md:ml-4" type="button" size="sm">
<Button
onClick={onCreateNewAllowance}
color="secondary"
variant="default"
className="md:ml-4"
type="button"
size="sm"
>
<Bank />
&nbsp;Authorize Fee Spend
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,10 @@ export const DeploymentGrantTable: React.FC<Props> = ({
}}
/>
</div>
<Button variant="ghost" size="icon" onClick={() => onEditGrant(grant)}>
<Button variant="ghost" size="icon" onClick={() => onEditGrant(grant)} aria-label="Edit Authorization">
<Edit className="text-xs" />
</Button>
<Button variant="ghost" size="icon" onClick={() => setDeletingGrants([grant])}>
<Button variant="ghost" size="icon" onClick={() => setDeletingGrants([grant])} aria-label="Revoke Authorization">
<Bin className="text-xs" />
</Button>
</div>
Expand Down Expand Up @@ -164,7 +164,7 @@ export const DeploymentGrantTable: React.FC<Props> = ({

return (
<div>
<Table>
<Table aria-label="Deployment Authorization List">
<TableHeader>
{table.getHeaderGroups().map(headerGroup => (
<TableRow key={headerGroup.id}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const FeeGrantTable: React.FC<Props> = ({

return (
<div>
<Table>
<Table aria-label="Tx Fee Authorization List">
<TableHeader>
<TableRow>
<TableHead className="w-1/5">Type</TableHead>
Expand Down
80 changes: 80 additions & 0 deletions apps/deploy-web/tests/ui/authorize-spending.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { shortenAddress } from "@akashnetwork/ui/components";
import type { BrowserContext, Page } from "@playwright/test";

import { expect, test } from "./fixture/context-with-extension";
import { clickCopyAddressButton } from "./fixture/testing-helpers";
import { getExtensionPage } from "./fixture/wallet-setup";
import type { AuthorizationListLabel, AuthorizeButtonLabel } from "./pages/AuthorizationsPage";
import { AuthorizationsPage } from "./pages/AuthorizationsPage";
import { LeapExt } from "./pages/LeapExt";

type TestProps = {
name: string;
buttonLabel: AuthorizeButtonLabel;
listLabel: AuthorizationListLabel;
};

const runAuthorizationTest = ({ name, buttonLabel, listLabel }: TestProps) => {
test.describe(`${name} Authorizations`, () => {
test("can authorize spending", async ({ page, context, extensionId }) => {
test.setTimeout(5 * 60 * 1000);

const { authorizationsPage, address } = await setup({ page, context, extensionId, buttonLabel });

const shortenedAddress = shortenAddress(address);
const grantList = authorizationsPage.page.getByLabel(listLabel);
await expect(grantList.locator("tr", { hasText: shortenedAddress })).toBeVisible();
});

test("can edit spending", async ({ page, context, extensionId }) => {
test.setTimeout(5 * 60 * 1000);

const { authorizationsPage, address, extension } = await setup({ page, context, extensionId, buttonLabel });
await authorizationsPage.editSpending(address, listLabel);
await extension.acceptTransaction(context);

const grantList = authorizationsPage.page.getByLabel(listLabel);
await expect(grantList.locator("tr", { hasText: "10.000000 AKT" })).toBeVisible();
});

test("can revoke spending", async ({ page, context, extensionId }) => {
test.setTimeout(5 * 60 * 1000);

const { authorizationsPage, address, extension } = await setup({ page, context, extensionId, buttonLabel });
await authorizationsPage.revokeSpending(address, listLabel);
await extension.acceptTransaction(context);

const shortenedAddress = shortenAddress(address);
const grantList = authorizationsPage.page.getByLabel(listLabel);
await expect(grantList.locator("tr", { hasText: shortenedAddress })).not.toBeVisible();
});
});
};

runAuthorizationTest({ name: "Deployment", buttonLabel: "Authorize Spend", listLabel: "Deployment Authorization List" });
runAuthorizationTest({ name: "Tx Fee", buttonLabel: "Authorize Fee Spend", listLabel: "Tx Fee Authorization List" });

type SetupProps = {
page: Page;
context: BrowserContext;
extensionId: string;
buttonLabel: AuthorizeButtonLabel;
};

const setup = async ({ page, context, extensionId, buttonLabel }: SetupProps) => {
const extension = new LeapExt(context, page);
const address = await clickCopyAddressButton(await getExtensionPage(context, extensionId));
await extension.createWallet(extensionId);

const authorizationsPage = new AuthorizationsPage(context, page);
await authorizationsPage.goto();

await authorizationsPage.authorizeSpending(address, buttonLabel);
await extension.acceptTransaction(context);

return {
authorizationsPage,
address,
extension
};
};
4 changes: 2 additions & 2 deletions apps/deploy-web/tests/ui/change-wallets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { LeapExt } from "./pages/LeapExt";
test("switching to another wallet in the extension affects Console", async ({ page, context, extensionId }) => {
test.setTimeout(5 * 60 * 1000);

const frontPage = new LeapExt(context, page);
const extension = new LeapExt(context, page);

const newWalletName = await frontPage.createWallet(extensionId);
const newWalletName = await extension.createWallet(extensionId);

const container = page.getByLabel("Connected wallet name and balance");
await container.waitFor({ state: "visible", timeout: 20_000 });
Expand Down
4 changes: 2 additions & 2 deletions apps/deploy-web/tests/ui/disconnect-wallet.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { LeapExt } from "./pages/LeapExt";

test("wallet stays disconnected after disconnecting and reloading", async ({ page, context }) => {
test.setTimeout(5 * 60 * 1000);
const frontPage = new LeapExt(context, page);
await frontPage.disconnectWallet();
const extension = new LeapExt(context, page);
await extension.disconnectWallet();

await expect(page.getByTestId("connect-wallet-btn")).toBeVisible();
});
56 changes: 30 additions & 26 deletions apps/deploy-web/tests/ui/fixture/context-with-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export const test = baseTest.extend<{

const context = await chromium.launchPersistentContext(userDataDir, {
channel: "chromium",
args
args,
permissions: ["clipboard-read", "clipboard-write"]
});

await use(context);
Expand All @@ -42,36 +43,39 @@ export const test = baseTest.extend<{
const extensionId = background.url().split("/")[2];
await use(extensionId);
},
page: async ({ context, extensionId }, use) => {
try {
await context.waitForEvent("page", { timeout: 5000 });
} catch {
// ignore timeout error
}
page: [
async ({ context, extensionId }, use) => {
try {
await context.waitForEvent("page", { timeout: 5000 });
} catch {
// ignore timeout error
}

const extPage = await getExtensionPage(context, extensionId);
const extPage = await getExtensionPage(context, extensionId);

await setupWallet(context, extPage);
await extPage.close();
const page = await context.newPage();
await injectUIConfig(page);
await setupWallet(context, extPage);
await extPage.close();
const page = await context.newPage();
await injectUIConfig(page);

if (testEnvConfig.NETWORK_ID !== "mainnet") {
try {
await page.goto(testEnvConfig.BASE_URL);
await connectWalletViaLeap(context, page);
await selectChainNetwork(page, testEnvConfig.NETWORK_ID);
await connectWalletViaLeap(context, page);
} catch {
// Fallback in case the default network is non-functional.
// E.g., during network upgrade when sandbox is already on a different version from mainnet
await page.goto(`${testEnvConfig.BASE_URL}?network=${testEnvConfig.NETWORK_ID}`);
await awaitWalletAndApprove(context, page);
if (testEnvConfig.NETWORK_ID !== "mainnet") {
try {
await page.goto(testEnvConfig.BASE_URL);
await connectWalletViaLeap(context, page);
await selectChainNetwork(page, testEnvConfig.NETWORK_ID);
await connectWalletViaLeap(context, page);
} catch {
// Fallback in case the default network is non-functional.
// E.g., during network upgrade when sandbox is already on a different version from mainnet
await page.goto(`${testEnvConfig.BASE_URL}?network=${testEnvConfig.NETWORK_ID}`);
await awaitWalletAndApprove(context, page, extensionId);
}
}
}

await use(page);
}
await use(page);
},
{ scope: "test", timeout: 5 * 60 * 1000 }
]
Comment on lines +46 to +78
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the 5-minute timeout is necessary.

The page fixture now has explicit scope and timeout configuration. The test scope is appropriate for isolation, but the 5-minute timeout seems quite long for fixture setup.

Is the 5-minute timeout necessary for your test environment? Consider whether a shorter timeout (e.g., 2-3 minutes) would be sufficient, as excessively long timeouts can mask real issues with slow setup operations.


🌐 Web query:

What is a typical timeout for Playwright page fixtures in E2E tests?

💡 Result:

  • Default: Playwright’s per-test timeout is 30,000 ms (30s), and fixtures share that timeout by default. [1]
  • Common practice: keep quick page fixtures within the default 30s; for slower (worker-scoped or heavy) page fixtures people commonly set a separate fixture timeout of 60,000 ms (60s) or similar. Example: { timeout: 60000 } on the fixture. [1][2]

References:
[1] Playwright Test — Timeouts (defaults and fixture behavior).
[2] Playwright docs/examples showing fixture timeout usage (example uses 60000 ms).


Reduce fixture timeout from 5 minutes to align with best practices.

The 5-minute timeout (300,000 ms) is significantly excessive. Playwright's default per-test timeout is 30 seconds, and even for slower or heavy fixtures, common practice is to use a separate fixture timeout of around 60 seconds.

Set the timeout to 60,000 ms (1 minute) or 90,000 ms if the wallet setup consistently requires that duration. Overly long timeouts mask real issues with slow operations.

🤖 Prompt for AI Agents
In apps/deploy-web/tests/ui/fixture/context-with-extension.ts around lines 46 to
78, the fixture timeout is set to 5 * 60 * 1000 (300000 ms) which is excessive;
change the timeout value in the fixture options to 60000 (60,000 ms) to align
with best practices (or 90000 if you have measured that the wallet setup
consistently needs more time), keeping the same scope and behavior otherwise.

});

export const expect = test.expect;
Expand Down
13 changes: 11 additions & 2 deletions apps/deploy-web/tests/ui/fixture/testing-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ export const clickCreateNewWalletButton = async (page: Page) => {
};

export const fillWalletName = async (page: Page, name: string) => {
const input = await waitForLocator(page.getByPlaceholder("Enter wallet Name"));
return await input.fill(name);
return await page.getByPlaceholder("Enter wallet Name").fill(name);
};

export const clickCreateWalletButton = async (page: Page) => {
Expand All @@ -29,6 +28,16 @@ export const clickConnectWalletButton = async (page: Page) => {
return await button.click();
};

export const clickCopyAddressButton = async (page: Page) => {
await page.getByRole("button", { name: /akash\.\.\.[a-z0-9]{5}/ }).click();

const clipboardContents = await page.evaluate(async () => {
return await navigator.clipboard.readText();
});

return clipboardContents;
};

export const waitForLocator = async (locator: Locator) => {
await locator.waitFor({ state: "visible", timeout: 20_000 });
return locator;
Expand Down
7 changes: 5 additions & 2 deletions apps/deploy-web/tests/ui/fixture/wallet-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,11 @@ export async function connectWalletViaLeap(context: BrowserContext, page: Page)
}
}

export async function awaitWalletAndApprove(context: BrowserContext, page: Page) {
const popupPage = await context.waitForEvent("page", { timeout: 5_000 });
export async function awaitWalletAndApprove(context: BrowserContext, page: Page, extensionId: string) {
const popupPage = await Promise.race([
context.waitForEvent("page", { timeout: 5_000 }),
getExtensionPage(context, extensionId),
]);
await approveWalletOperation(popupPage);
await isWalletConnected(page);
}
Expand Down
42 changes: 42 additions & 0 deletions apps/deploy-web/tests/ui/pages/AuthorizationsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { shortenAddress } from "@akashnetwork/ui/components";
import type { BrowserContext as Context, Page } from "@playwright/test";

import { testEnvConfig } from "../fixture/test-env.config";

export type AuthorizeButtonLabel = "Authorize Spend" | "Authorize Fee Spend";
export type AuthorizationListLabel = "Deployment Authorization List" | "Tx Fee Authorization List";

export class AuthorizationsPage {
constructor(
readonly context: Context,
readonly page: Page
) {}

async goto(url = `${testEnvConfig.BASE_URL}/settings/authorizations`) {
await this.page.goto(url);
}

async clickGrantButton() {
return await this.page.getByRole("button", { name: "Grant" }).click();
}

async authorizeSpending(address: string, buttonLabel: AuthorizeButtonLabel) {
await this.page.getByRole("button", { name: buttonLabel }).click();
await this.page.getByLabel("Spending Limit").fill("5");
await this.page.getByLabel("Grantee Address").fill(address);
await this.clickGrantButton();
}

async editSpending(address: string, listLabel: AuthorizationListLabel) {
const shortenedAddress = shortenAddress(address);
await this.page.getByLabel(listLabel).locator("tr", { hasText: shortenedAddress }).getByLabel("Edit Authorization").click();
await this.page.getByLabel("Spending Limit").fill("10");
await this.clickGrantButton();
}

async revokeSpending(address: string, listLabel: AuthorizationListLabel) {
const shortenedAddress = shortenAddress(address);
await this.page.getByLabel(listLabel).locator("tr", { hasText: shortenedAddress }).getByLabel("Revoke Authorization").click();
await this.page.getByTestId("confirm-button").click();
}
}
11 changes: 8 additions & 3 deletions apps/deploy-web/tests/ui/pages/LeapExt.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { faker } from "@faker-js/faker";
import type { BrowserContext as Context, Page } from "@playwright/test";
import type { BrowserContext, Page } from "@playwright/test";

import { wait } from "@src/utils/timer";
import { testEnvConfig } from "../fixture/test-env.config";
import { clickConnectWalletButton } from "../fixture/testing-helpers";
import { createWallet } from "../fixture/wallet-setup";
import { approveWalletOperation, createWallet } from "../fixture/wallet-setup";

export class LeapExt {
constructor(
readonly context: Context,
readonly context: BrowserContext,
readonly page: Page
) {}

Expand Down Expand Up @@ -37,4 +37,9 @@ export class LeapExt {

await this.page.reload({ waitUntil: "domcontentloaded" });
}

async acceptTransaction(context: BrowserContext) {
const popupPage = await context.waitForEvent("page", { timeout: 5_000 });
await approveWalletOperation(popupPage);
}
}
4 changes: 2 additions & 2 deletions apps/deploy-web/tests/ui/uiState/isWalletConnected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ export async function isWalletConnected(page: Page) {
const result = await Promise.race([
page
.getByLabel("Connected wallet name and balance")
.waitFor({ state: "visible", timeout: 10_000 })
.waitFor({ state: "visible", timeout: 30_000 })
.then(() => true)
.catch(() => null),
page
.getByTestId("connect-wallet-btn")
.waitFor({ state: "visible", timeout: 10_000 })
.waitFor({ state: "visible", timeout: 30_000 })
.then(() => false)
.catch(() => null)
]);
Expand Down
6 changes: 5 additions & 1 deletion packages/ui/components/custom/address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { cn, copyTextToClipboard } from "../../utils";
import { CustomTooltip } from "../tooltip";
import { Snackbar } from "./snackbar";

export const shortenAddress = (address: string) => {
return `${address.slice(0, 8)}...${address.slice(-5)}`;
};

type Props = {
address: string;
isCopyable?: boolean;
Expand All @@ -21,7 +25,7 @@ type Props = {
export const Address: React.FunctionComponent<Props> = ({ address, isCopyable, disableTruncate, disableTooltip, showIcon, className, ...rest }) => {
const [isOver, setIsOver] = useState(false);
const { enqueueSnackbar } = useSnackbar();
const formattedAddress = disableTruncate ? address : [address?.slice(0, 8), "...", address?.slice(address?.length - 5)].join("");
const formattedAddress = disableTruncate ? address : shortenAddress(address);

const onClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (isCopyable) {
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/components/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(

return (
<div className={cn("space-y-1", className)}>
{label && (formField.id ? <FormLabel>{label}</FormLabel> : <Label htmlFor={`${finalId}-input`}>{label}</Label>)}
{label && (formField.id ? <FormLabel htmlFor={`${finalId}-input`}>{label}</FormLabel> : <Label htmlFor={`${finalId}-input`}>{label}</Label>)}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Good accessibility improvement; verify consistency with Textarea.

The addition of {label} as children to FormLabel and the htmlFor attribute improves accessibility by ensuring the label renders correctly and explicitly links to the input.

However, the Textarea component at line 107 uses FormLabel without the htmlFor attribute. For consistency, verify whether Textarea's FormLabel should also include htmlFor={${formField.id ? formField.id : id}-input} to match this pattern.


🏁 Script executed:

#!/bin/bash
# Verify if FormLabel in other similar components should also have htmlFor attribute
rg -n -C3 -P '<FormLabel[^>]*>.*?</FormLabel>' --type=tsx --type=ts

Length of output: 27





---

🏁 Script executed:

```shell
#!/bin/bash
# Alternative approach: find and examine the input.tsx file directly
echo "=== Examining input.tsx file ==="
fd -e tsx -e ts input.tsx

Length of output: 177


🏁 Script executed:

#!/bin/bash
# Search for FormLabel patterns using file glob instead of type
rg -n -C3 '<FormLabel' packages/ui/ 

Length of output: 1199


🏁 Script executed:

#!/bin/bash
# Look for all tsx files in the ui components directory
fd -e tsx packages/ui/components/

Length of output: 397



---

🏁 Script executed:

```shell
#!/bin/bash
# Examine the full input.tsx file to understand context around both components
cat packages/ui/components/input.tsx

Length of output: 4778


Add htmlFor attribute to Textarea's FormLabel for consistency with Input component.

The accessibility improvement at line 59 (adding htmlFor to Input's FormLabel) is good. However, Textarea at line 107 uses FormLabel without the htmlFor attribute. Both components follow identical patterns when formField.id exists, so Textarea's FormLabel should also include htmlFor={${formField.id ? formField.id : id}-input} to maintain consistent accessibility.

Update line 107:

{label && (formField.id ? <FormLabel htmlFor={`${formField.id ? formField.id : id}-input`}>{label}</FormLabel> : <Label htmlFor={`${id}-input`}>{label}</Label>)}
🤖 Prompt for AI Agents
In packages/ui/components/input.tsx around lines 59 and 107, the Textarea
component's FormLabel is missing the htmlFor attribute (unlike the Input
component), so add htmlFor using the same id expression as Input: set
FormLabel's htmlFor to `${formField.id ? formField.id : id}-input` and keep the
existing Label fallback using `${id}-input`; this ensures consistent accessible
labeling for both Input and Textarea.

<div className="relative flex items-center">
{startIcon && <div className={cn("absolute inset-y-0 left-0 flex items-center", startIconClassName)}>{startIcon}</div>}
<input
Expand Down