diff --git a/playwright.config.ts b/playwright.config.ts index 4ad263307b..ebc547013d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, + workers: process.env.CI ? 3 : undefined, reporter: "html", outputDir: "playwright/results/", @@ -41,7 +41,7 @@ export default defineConfig({ webServer: [ { - command: `anvil --fork-url=${process.env.ANVIL_FORK_URL} --fork-block-number=6373425 -m='${process.env.NEXT_PUBLIC_E2E_WALLET_MNEMONIC}' --fork-header='Authorization: ${process.env.ANVIL_FORK_KEY}'`, + command: `anvil --fork-url=${process.env.ANVIL_FORK_URL} --fork-block-number=6697000 -m='${process.env.NEXT_PUBLIC_E2E_WALLET_MNEMONIC}' --balance=1 --fork-header='Authorization: ${process.env.ANVIL_FORK_KEY}'`, port: 8545, }, { diff --git a/playwright/01-auth.setup.ts b/playwright/01-auth.setup.ts index 8ea52212c7..5d225cd137 100644 --- a/playwright/01-auth.setup.ts +++ b/playwright/01-auth.setup.ts @@ -34,7 +34,9 @@ setup("authenticate", async ({ page }) => { await page.getByTestId("verify-address-button").click() const publicKeyResponse = await page - .waitForResponse(`**/v2/users/${TEST_USER.id}/public-key`) + .waitForResponse(`**/v2/users/${TEST_USER.id}/public-key`, { + timeout: 60_000, + }) .then((res) => res.json()) const storedKeyPairToSave = await page.evaluate( diff --git a/playwright/02-dummy.spec.ts b/playwright/02-restore-auth-state.spec.ts similarity index 67% rename from playwright/02-dummy.spec.ts rename to playwright/02-restore-auth-state.spec.ts index 0ec5e69c7d..239097c6c2 100644 --- a/playwright/02-dummy.spec.ts +++ b/playwright/02-restore-auth-state.spec.ts @@ -2,11 +2,13 @@ import { expect } from "@playwright/test" import { TEST_USER } from "./constants" import { test } from "./fixtures" -test("dummy", async ({ pageWithKeyPair: { page } }) => { +test("restore auth state from json", async ({ pageWithKeyPair: { page } }) => { await page.goto("/explorer") const accountCard = await page.getByTestId("account-card") - await expect(accountCard).toBeVisible() + await expect(accountCard).toBeVisible({ + timeout: 30_000, + }) accountCard.click() await page.waitForResponse(`**/v2/users/${TEST_USER.id}/profile`) diff --git a/playwright/03-guild-pin.spec.ts b/playwright/03-guild-pin.spec.ts index 1e89e1656c..1f23cedf98 100644 --- a/playwright/03-guild-pin.spec.ts +++ b/playwright/03-guild-pin.spec.ts @@ -5,7 +5,9 @@ import { test } from "./fixtures" test("can mint guild pin", async ({ pageWithKeyPair: { page } }) => { await page.goto(GUILD_CHECKOUT_TEST_GUILD_URL_NAME) - await page.waitForResponse("**/v2/users/*/memberships?guildId=*") + await page.waitForResponse("**/v2/users/*/memberships?guildId=*", { + timeout: 30_000, + }) const mintGuildPinButton = await page.getByTestId("mint-guild-pin-button") await mintGuildPinButton.click() diff --git a/playwright/04-nft-reward.spec.ts b/playwright/04-nft-reward.spec.ts new file mode 100644 index 0000000000..b5260bd63a --- /dev/null +++ b/playwright/04-nft-reward.spec.ts @@ -0,0 +1,294 @@ +import path from "path" +import { Locator, expect } from "@playwright/test" +import { GUILD_CHECKOUT_TEST_GUILD_URL_NAME, TEST_USER } from "./constants" +import { test } from "./fixtures" + +test("fill nft form and deploy a contract", async ({ + pageWithKeyPair: { page }, +}) => { + await page.goto(GUILD_CHECKOUT_TEST_GUILD_URL_NAME) + + await page.waitForResponse(`**/v2/users/${TEST_USER.id}/profile`, { + timeout: 30_000, + }) + + // Open the "Guild solutions" modal + const addSolutionButton = await page + .getByTestId("layout-action") + .getByTestId("add-solutions-button") + await addSolutionButton.click() + const addSolutionsModal = await page.getByRole("dialog", { + name: "Guild Solutions", + }) + await expect(addSolutionsModal).toBeVisible() + + // Open the "Add NFT reward" modal + const addContractCallRewardCard = await page.getByTestId("contract-call-solution") + await addContractCallRewardCard.click() + + const addNFTModal = await page.getByRole("dialog", { + name: "Add NFT reward", + }) + await expect(addNFTModal).toBeVisible() + + // Test every basic input field inside the NFT form + const nameInput = await addNFTModal.locator("input[name='name']") + await nameInput.focus() + await nameInput.blur() + await expect(nameInput).toHaveAttribute("aria-invalid", "true") + await nameInput.fill("E2E test NFT") + + const metadataDescriptionTextarea = await addNFTModal.getByLabel( + "Metadata description" + ) + await metadataDescriptionTextarea.fill("E2E test NFT metadata description") + + const payoutAddressInput = await addNFTModal.getByLabel("Payout address") + await expect(payoutAddressInput).toHaveValue(new RegExp(TEST_USER.address, "i")) + await payoutAddressInput.fill("") + await expect(payoutAddressInput).toHaveAttribute("aria-invalid", "true") + await payoutAddressInput.fill(TEST_USER.address) + + // TODO: Seems like the chain select doesn't work inside Playwright, make sure to write tests for that once we migrate to the Radix UI select + + // Test the metadata attribute fieldArray + const addAttributeButton = await addNFTModal.getByText("Add attribute") + addAttributeButton.click() + const attributeNameInput = await addNFTModal.locator( + "input[name='attributes.0.name']" + ) + await expect(attributeNameInput).toBeVisible() + const attributeValueInput = await addNFTModal.locator( + "input[name='attributes.0.value']" + ) + await expect(attributeValueInput).toBeVisible() + const removeAttributeButton = await addNFTModal.getByLabel("Remove attribute") + await removeAttributeButton.click() + await expect(attributeNameInput).toBeHidden() + + // Test the "Limit supply" panel + const limitSupplyPanel = await addNFTModal.getByText("Limit supply") + await limitSupplyPanel.click() + + const supplyTypeSegmentedControl = await addNFTModal.getByTestId( + "supply-type-segmented-control" + ) + await expect(supplyTypeSegmentedControl).toBeVisible() + await testSegmentedControlWithNumberInput( + addNFTModal, + supplyTypeSegmentedControl, + "maxSupply" + ) + + const mintLimitTypeSegmentedControl = await addNFTModal.getByTestId( + "mint-limit-type-segmented-control" + ) + await expect(mintLimitTypeSegmentedControl).toBeVisible() + await testSegmentedControlWithNumberInput( + addNFTModal, + mintLimitTypeSegmentedControl, + "mintableAmountPerUser" + ) + + await limitSupplyPanel.click() + await expect(supplyTypeSegmentedControl).toBeHidden() + await expect(mintLimitTypeSegmentedControl).toBeHidden() + + await limitSupplyPanel.click() + await testSegmentedControlWithNumberInputReset( + addNFTModal, + supplyTypeSegmentedControl, + "maxSupply" + ) + await testSegmentedControlWithNumberInputReset( + addNFTModal, + mintLimitTypeSegmentedControl, + "mintableAmountPerUser" + ) + await limitSupplyPanel.click() + + // Test the "Limit claiming time" panel + const limitClaimingTimePanel = await addNFTModal.getByText("Limit claiming time") + await limitClaimingTimePanel.click() + const startTimeInput = await addNFTModal.locator("input[name='startTime']") + await expect(startTimeInput).toBeVisible() + const endTimeInput = await addNFTModal.locator("input[name='endTime']") + await expect(endTimeInput).toBeVisible() + await limitClaimingTimePanel.click() + await expect(startTimeInput).toBeHidden() + await expect(endTimeInput).toBeHidden() + + /** + * Media input - Playwright doesn't work with react-dropzone, so we just set the file input's value manually here + * + * GitHub issue: https://github.com/microsoft/playwright/issues/8850 + */ + const imagePicker = await addNFTModal.getByTestId("nft-image-picker") + const fileInput = await imagePicker.locator("input[type='file']") + await fileInput.setInputFiles(path.join(__dirname, "files/nft-image.png")) + const uploadedImage = await imagePicker.locator("img[alt='NFT image']") + await expect(uploadedImage).toBeVisible({ + timeout: 30_000, + }) + await expect(uploadedImage).toHaveAttribute("src", /^https:\/\/*/) + + // Finally, submit the transaction + const createNFTButton = await addNFTModal.getByTestId("create-nft-button") + await createNFTButton.click() + + const successToast = await page.getByText("Successfully deployed NFT contract", { + exact: true, + }) + await expect(successToast).toBeVisible({ timeout: 30_000 }) +}) + +test("user is not eligible - can't mint nft", async ({ + pageWithKeyPair: { page }, +}) => { + await page.goto(GUILD_CHECKOUT_TEST_GUILD_URL_NAME) + + await page.waitForResponse("**/v2/users/*/memberships?guildId=*", { + timeout: 30_000, + }) + + const roleCard = await page.locator(`#role-${UNHAPPY_PATH_ROLE_ID}`) + const nftRewardCardButton = await roleCard.locator("a", { + hasText: NFT_REWARD_CARD_BUTTON_TEXT, + }) + const collectNFTPageURL = await nftRewardCardButton.getAttribute("href") + await nftRewardCardButton.click() + await page.waitForURL(collectNFTPageURL ?? "") + + const title = await page.locator("h2") + await expect(title).toHaveText(NFT_1_NAME) + + const collectNFTButton = await page.getByTestId("collect-nft-button") + await expect(collectNFTButton).toBeEnabled({ + timeout: 30_000, + }) + const whaleButton = await page.locator("button", { + hasText: "whale", + }) + await whaleButton.click() + await expect(collectNFTButton).toBeDisabled() + await expect(collectNFTButton).toHaveText("Insufficient balance") + const shrimpButton = await page.locator("button", { + hasText: "Shrimp", + }) + await shrimpButton.click() + await expect(collectNFTButton).toBeEnabled() + await expect(collectNFTButton).toHaveText(COLLECT_BUTTON_TEXT_REGEX) + + await collectNFTButton.click() + await page.waitForResponse("**/v2/guilds/*/roles/*/role-platforms/*/claim") + + const errorToast = await page.getByText(NOT_ELIGIBLE_TOAST_TEXT) + await expect(errorToast).toBeVisible({ + timeout: 30_000, + }) +}) + +test("user is eligible - can mint nft", async ({ pageWithKeyPair: { page } }) => { + await page.goto(GUILD_CHECKOUT_TEST_GUILD_URL_NAME) + + await page.waitForResponse("**/v2/users/*/memberships?guildId=*", { + timeout: 30_000, + }) + + const roleCard = await page.locator(`#role-${HAPPY_PATH_ROLE_ID}`) + const nftRewardCardButton = await roleCard.locator("a", { + hasText: NFT_REWARD_CARD_BUTTON_TEXT, + }) + const collectNFTPageURL = await nftRewardCardButton.getAttribute("href") + await nftRewardCardButton.click() + await page.waitForURL(collectNFTPageURL ?? "") + + const title = await page.locator("h2") + await expect(title).toHaveText(NFT_2_NAME) + + const collectNFTButton = await page.getByTestId("collect-nft-button") + await expect(collectNFTButton).toBeEnabled({ + timeout: 30_000, + }) + + await collectNFTButton.click() + await page.waitForResponse("**/v2/guilds/*/roles/*/role-platforms/*/claim") + + const successToast = await page.locator("li[role='status']", { + hasText: SUCCESS_TOAST_TEXT, + }) + await expect(successToast).toBeVisible({ + timeout: 30_000, + }) + + const successModal = await page.getByRole("dialog", { + name: "Success", + }) + await expect(successModal).toBeVisible({ + timeout: 30_000, + }) + const modalCloseButton = await successModal.getByText("Close") + await modalCloseButton.click() +}) + +// Utils, constants + +const testSegmentedControlWithNumberInput = async ( + addNFTModal: Locator, + segmentedControlLocator: Locator, + numberInputName: string +) => { + const unlimitedSegment = await segmentedControlLocator.locator( + UNLIMITED_SEGMENT_LOCATOR + ) + const numberInput = await addNFTModal.locator( + getNumberInputLocator(numberInputName) + ) + const limitedSegment = await segmentedControlLocator.locator( + "> label:last-child > div:last-child" + ) + + await testSegmentedControlWithNumberInputReset( + addNFTModal, + segmentedControlLocator, + numberInputName + ) + + await limitedSegment.click() + await expect(limitedSegment).toHaveAttribute("data-checked") + await expect(numberInput).toHaveValue("1") + await unlimitedSegment.click() + await expect(numberInput).toHaveValue("0") + await numberInput.fill("2") + await expect(limitedSegment).toHaveAttribute("data-checked") + await numberInput.fill("0") + await expect(unlimitedSegment).toHaveAttribute("data-checked") +} + +const testSegmentedControlWithNumberInputReset = async ( + addNFTModal: Locator, + segmentedControlLocator: Locator, + numberInputName: string +) => { + const unlimitedSegment = await segmentedControlLocator.locator( + UNLIMITED_SEGMENT_LOCATOR + ) + await expect(unlimitedSegment).toHaveAttribute("data-checked") + const numberInput = await addNFTModal.locator( + getNumberInputLocator(numberInputName) + ) + await expect(numberInput).toHaveValue("0") +} + +const UNLIMITED_SEGMENT_LOCATOR = "> label:first-child > div:last-child" +const getNumberInputLocator = (numberInputName: string) => + `input[name='${numberInputName}']` + +const NFT_REWARD_CARD_BUTTON_TEXT = new RegExp("(Collect NFT|View NFT details)") +const NFT_1_NAME = "Cypress Gang #1" +const UNHAPPY_PATH_ROLE_ID = 91062 +const NOT_ELIGIBLE_TOAST_TEXT = "You're not eligible for claiming this reward" +const NFT_2_NAME = "Cypress Gang #2" +const HAPPY_PATH_ROLE_ID = 91063 +const COLLECT_BUTTON_TEXT_REGEX = new RegExp("(Check access & collect|Collect now)") +const SUCCESS_TOAST_TEXT = "Successfully collected NFT!" diff --git a/playwright/README.md b/playwright/README.md index 9e966a29f1..a52ca8475c 100644 --- a/playwright/README.md +++ b/playwright/README.md @@ -10,7 +10,11 @@ We use Playwright for E2E testing. When running our tests, we create a viem [Tes curl -L https://foundry.paradigm.xyz | bash foundryup ``` -3. Either run the frontend in dev mode (`npm run dev`) or build it (`npm run build`) and run the tests. You can pick between several different modes, e.g. `npm run test` will just run the tests and print the output, `npm run test:ui` will open the Playwright runner, where you can see the console, network tab, etc., and `npm run test:debug` will open a Chromium browser where you can inspect the page during the test. +3. You might also need to install a browser which you can use for testing: +```sh +npx playwright install chromium +``` +4. Either run the frontend in dev mode (`npm run dev`) or build it (`npm run build`) and run the tests. You can pick between several different modes, e.g. `npm run test` will just run the tests and print the output, `npm run test:ui` will open the Playwright runner, where you can see the console, network tab, etc., and `npm run test:debug` will open a Chromium browser where you can inspect the page during the test. ## Writing tests diff --git a/playwright/files/nft-image.png b/playwright/files/nft-image.png new file mode 100644 index 0000000000..11aa220855 Binary files /dev/null and b/playwright/files/nft-image.png differ diff --git a/src/components/[guild]/RolePlatforms/components/AddRoleRewardModal/components/AddContractCallPanel/components/CreateNftForm/CreateNftForm.tsx b/src/components/[guild]/RolePlatforms/components/AddRoleRewardModal/components/AddContractCallPanel/components/CreateNftForm/CreateNftForm.tsx index 878a1d2f1a..95b64f1ea2 100644 --- a/src/components/[guild]/RolePlatforms/components/AddRoleRewardModal/components/AddContractCallPanel/components/CreateNftForm/CreateNftForm.tsx +++ b/src/components/[guild]/RolePlatforms/components/AddRoleRewardModal/components/AddContractCallPanel/components/CreateNftForm/CreateNftForm.tsx @@ -63,7 +63,7 @@ const CreateNftForm = ({ onSuccess }: Props) => { { diff --git a/src/solutions/components/SolutionCard.tsx b/src/solutions/components/SolutionCard.tsx index 8fd4a54a32..74159a8986 100644 --- a/src/solutions/components/SolutionCard.tsx +++ b/src/solutions/components/SolutionCard.tsx @@ -8,24 +8,18 @@ import { } from "@chakra-ui/react" import DisplayCard from "components/common/DisplayCard" import Image from "next/image" - -export type Props = { - title: string - imageUrl: string - description: string - bgImageUrl: string - onClick?: (data?: any) => void - children?: JSX.Element -} +import { PropsWithChildren } from "react" +import { SolutionCardData } from "solutions" const SolutionCard = ({ title, description, imageUrl, bgImageUrl, + handlerParam, onClick, children, -}: Props) => { +}: PropsWithChildren void }>) => { const circleBgColor = useColorModeValue("whiteAlpha.300", "blackAlpha.300") const borderColor = useColorModeValue("blackAlpha.300", "whiteAlpha.200") const cardBg = useColorModeValue("white", "var(--chakra-colors-gray-800)") @@ -38,6 +32,7 @@ const SolutionCard = ({ return ( { if (platform === "CONTRACT_CALL") startSessionRecording() setSelection(platform) diff --git a/src/v2/components/StickyAction.tsx b/src/v2/components/StickyAction.tsx index 97553e66f7..c2eeb4a8c5 100644 --- a/src/v2/components/StickyAction.tsx +++ b/src/v2/components/StickyAction.tsx @@ -15,6 +15,7 @@ const StickyAction = ({ children }: PropsWithChildren) => { <> {children}