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
7 changes: 5 additions & 2 deletions apps/deploy-web/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { getUserAgent } from "./tests/ui/fixture/user-agent"; // for process.env
dotenv.config({ path: path.resolve(__dirname, "env/.env.test.local") });
dotenv.config({ path: path.resolve(__dirname, "env/.env.test") });

const slowMo = Number(process.env.PW_SLOW_MO) || 0;

/**
* See https://playwright.dev/docs/test-configuration.
*/
Expand All @@ -27,9 +29,10 @@ export default defineConfig({

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "retain-on-failure",
video: "retain-on-failure",
video: slowMo > 0 ? "on" : "retain-on-failure",
actionTimeout: 15_000,
permissions: ["clipboard-read", "clipboard-write"]
permissions: ["clipboard-read", "clipboard-write"],
launchOptions: { slowMo }
},

/* Configure projects for major browsers */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useEffect } from "react";
import { describe, expect, it, vi } from "vitest";

import { AddCreditsSheet, DEPENDENCIES } from "./AddCreditsSheet";

import { act, render } from "@testing-library/react";
import { MockComponents } from "@tests/unit/mocks";

describe(AddCreditsSheet.name, () => {
it("renders the AddCreditsForm when open", () => {
const { dependencies } = setup({ open: true });

expect(dependencies.AddCreditsForm).toHaveBeenCalled();
});

it("does not render the AddCreditsForm while closed", () => {
const { dependencies } = setup({ open: false });

expect(dependencies.AddCreditsForm).not.toHaveBeenCalled();
});

it("forwards onDone to the form", () => {
const onDone = vi.fn();
const { dependencies } = setup({ open: true, onDone });

expect(dependencies.AddCreditsForm).toHaveBeenCalledWith(expect.objectContaining({ onDone }), expect.anything());
});

it("forwards isWalletReady to the form", () => {
const { dependencies } = setup({ open: true, isWalletReady: false });

expect(dependencies.AddCreditsForm).toHaveBeenCalledWith(expect.objectContaining({ isWalletReady: false }), expect.anything());
});

it("blocks closing while the form reports a payment in progress", () => {
const onOpenChange = vi.fn();
const { dependencies } = setup({ open: true, onOpenChange, dependencies: { AddCreditsForm: reportingForm(true) } });

act(() => dependencies.Sheet.mock.calls.at(-1)![0].onOpenChange?.(false));

expect(onOpenChange).not.toHaveBeenCalled();
});

it("allows closing when no payment is in progress", () => {
const onOpenChange = vi.fn();
const { dependencies } = setup({ open: true, onOpenChange, dependencies: { AddCreditsForm: reportingForm(false) } });

act(() => dependencies.Sheet.mock.calls.at(-1)![0].onOpenChange?.(false));

expect(onOpenChange).toHaveBeenCalledWith(false);
});

it("hides the close button while the form reports a payment in progress", () => {
const { dependencies } = setup({ open: true, dependencies: { AddCreditsForm: reportingForm(true) } });

expect(dependencies.SheetContent.mock.calls.at(-1)![0].hideCloseButton).toBe(true);
});

function reportingForm(isProcessing: boolean) {
return ({ onProcessingChange }: Parameters<typeof DEPENDENCIES.AddCreditsForm>[0]) => {
useEffect(() => onProcessingChange?.(isProcessing), [onProcessingChange]);
return <></>;
};
}

function setup(input: {
open: boolean;
onOpenChange?: (open: boolean) => void;
onDone?: (amount: number, organization?: string) => void;
isWalletReady?: boolean;
dependencies?: Partial<typeof DEPENDENCIES>;
}) {
const dependencies = MockComponents(DEPENDENCIES, input.dependencies);

render(
<AddCreditsSheet
open={input.open}
onOpenChange={input.onOpenChange ?? vi.fn()}
onDone={input.onDone ?? vi.fn()}
isWalletReady={input.isWalletReady}
dependencies={dependencies}
/>
);

return { dependencies };
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client";

import React, { useState } from "react";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@akashnetwork/ui/components";

import { AddCreditsForm } from "@src/components/billing-usage/AddCreditsForm/AddCreditsForm";

export const DEPENDENCIES = {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetDescription,
AddCreditsForm
};

interface AddCreditsSheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onDone: (amount: number, organization?: string) => void;
isWalletReady?: boolean;
dependencies?: typeof DEPENDENCIES;
}

export function AddCreditsSheet({ open, onOpenChange, onDone, isWalletReady, dependencies: d = DEPENDENCIES }: AddCreditsSheetProps) {
const [isProcessing, setIsProcessing] = useState(false);

const requestOpenChange = (next: boolean) => {
if (!next && isProcessing) return;
onOpenChange(next);
};

return (
<d.Sheet open={open} onOpenChange={requestOpenChange}>
<d.SheetContent side="right" hideCloseButton={isProcessing} className="w-full space-y-6 overflow-y-auto p-6 sm:max-w-[546px]">
<d.SheetHeader className="space-y-2 text-left">
<d.SheetTitle className="text-3xl font-medium leading-9">Add credits</d.SheetTitle>
<d.SheetDescription className="text-sm leading-5 text-muted-foreground">
This template needs a top-tier GPU, which isn&apos;t covered by your free trial. Add credits to unlock high-end GPUs, longer runtimes, and the full
Console.
</d.SheetDescription>
</d.SheetHeader>

{open && <d.AddCreditsForm onDone={onDone} isWalletReady={isWalletReady} onProcessingChange={setIsProcessing} />}
</d.SheetContent>
</d.Sheet>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it, vi } from "vitest";

import type { AddCreditsAmountValue } from "./AddCreditsAmountFields";
import { AddCreditsAmountFields } from "./AddCreditsAmountFields";

import { fireEvent, render, screen } from "@testing-library/react";

describe(AddCreditsAmountFields.name, () => {
it("emits the chosen predefined amount and clears the custom amount", () => {
const { onChange } = setup({ value: { predefinedAmount: "", customAmount: "75" } });

fireEvent.click(screen.getByRole("radio", { name: /100/i }));

expect(onChange).toHaveBeenCalledWith({ predefinedAmount: "100", customAmount: "" });
});

it("emits the custom amount and clears the predefined amount", () => {
const { onChange } = setup({ value: { predefinedAmount: "100", customAmount: "" } });

fireEvent.change(screen.getByLabelText("custom-amount"), { target: { value: "42" } });

expect(onChange).toHaveBeenCalledWith({ predefinedAmount: "", customAmount: "42" });
});

function setup(input: { value: AddCreditsAmountValue }) {
const onChange = vi.fn();
render(<AddCreditsAmountFields value={input.value} onChange={onChange} />);
return { onChange };
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"use client";

import type { ChangeEventHandler } from "react";
import React, { useCallback } from "react";
import { Field, FieldContent, FieldLabel, FieldTitle, Input, RadioGroup, RadioGroupItem } from "@akashnetwork/ui/components";

export interface AddCreditsAmountValue {
predefinedAmount?: string;
customAmount: string;
}

interface AddCreditsAmountFieldsProps {
value: AddCreditsAmountValue;
onChange: (value: AddCreditsAmountValue) => void;
}

export function AddCreditsAmountFields({ value, onChange }: AddCreditsAmountFieldsProps) {
const changePredefinedAmount = useCallback(
(predefinedAmount: string) => {
onChange({ predefinedAmount, customAmount: "" });
},
[onChange]
);

const changeCustomAmount: ChangeEventHandler<HTMLInputElement> = useCallback(
e => {
onChange({ customAmount: e.target.value, predefinedAmount: "" });
},
[onChange]
);

return (
<div className="space-y-3">
<h3 className="text-left text-sm font-medium text-muted-foreground">Credit amount</h3>

<div className="space-y-1">
<h3 className="text-sm font-medium leading-snug">Choose your amount</h3>
<RadioGroup value={value.predefinedAmount} className="grid-cols-3" onValueChange={changePredefinedAmount}>
<FieldLabel htmlFor="plus-plan">
<Field orientation="horizontal" className="cursor-pointer p-2">
<RadioGroupItem value="50" id="plus-plan" className="self-center" />
<FieldContent>
<FieldTitle className="font-medium">50</FieldTitle>
</FieldContent>
</Field>
</FieldLabel>
<FieldLabel htmlFor="pro-plan">
<Field orientation="horizontal" className="cursor-pointer p-2">
<RadioGroupItem value="100" id="pro-plan" className="self-center" />
<FieldContent>
<FieldTitle>100</FieldTitle>
</FieldContent>
</Field>
</FieldLabel>
<FieldLabel htmlFor="enterprise-plan">
<Field orientation="horizontal" className="cursor-pointer p-2">
<RadioGroupItem value="500" id="enterprise-plan" className="self-center" />
<FieldContent>
<FieldTitle>500</FieldTitle>
</FieldContent>
</Field>
</FieldLabel>
</RadioGroup>
</div>

<Field className="gap-1">
<FieldLabel htmlFor="custom-amount" className="font-medium">
Or enter custom amount <span className="text-muted-foreground">(minimum 20)</span>
</FieldLabel>
<Input
id="custom-amount"
aria-label="custom-amount"
inputClassName="h-9"
type="number"
value={value.customAmount}
onChange={changeCustomAmount}
min={20}
/>
</Field>
</div>
);
}
Loading