diff --git a/apps/web/codegen.ts b/apps/web/codegen.ts index 194e5d7b..992699ff 100644 --- a/apps/web/codegen.ts +++ b/apps/web/codegen.ts @@ -47,12 +47,20 @@ const generateConfig = (configs: CommonConfig[]): Generates => { schema, documents, preset: "import-types", - plugins: ["typescript-operations", "typescript-urql"], + plugins: [ + "typescript-operations", + "typescript-urql", + "named-operations-object", + ], presetConfig: { typesPath: `.${sep}types`, }, config: { withHooks: false, + // named-operations-object plugin configs + enumAsTypes: true, + useConsts: true, + identifierName: "allOperations", }, }, [join(path, "hooks", "queries.tsx")]: { diff --git a/apps/web/e2e/fixtures/test.ts b/apps/web/e2e/fixtures/test.ts new file mode 100644 index 00000000..1aa259a0 --- /dev/null +++ b/apps/web/e2e/fixtures/test.ts @@ -0,0 +1,50 @@ +import { Page, Route, test as baseTest } from "@playwright/test"; + +/** + * For validations purposes if needed. e.g. check if API + * was called correctly. + */ +type CalledWith = Record; + +/** + * Function to register as an interceptor. + * Interceptions are per-operation, so multiple can be registered without the risk + * of overwriting one another. + * @param page {Page} playwright page client. + * @param operationName Graphql operation name. + * @param resp The expected return for the mock. + * @returns + */ +export async function interceptGQL( + page: Page, + operationName: string, + resp: Record, +): Promise { + const reqs: CalledWith[] = []; + + await page.route("**/graphql", function (route: Route) { + const req = route.request().postDataJSON(); + + // Pass along to the previous handler in case the operation does not match. + if (req.operationName !== operationName) { + return route.fallback(); + } + + // Store what variables the API was called with; + reqs.push(req.variables); + + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ data: resp }), + }); + }); + + return reqs; +} + +export const test = baseTest.extend<{ interceptGQL: typeof interceptGQL }>({ + interceptGQL: async ({ browser }, use) => { + await use(interceptGQL); + }, +}); diff --git a/apps/web/e2e/pages/connections.spec.ts b/apps/web/e2e/pages/connections.spec.ts index 529b547a..13fd2551 100644 --- a/apps/web/e2e/pages/connections.spec.ts +++ b/apps/web/e2e/pages/connections.spec.ts @@ -1,9 +1,17 @@ -import { expect, test } from "@playwright/test"; +import { expect } from "@playwright/test"; import { Address } from "viem"; +import { allOperations } from "../../src/graphql/rollups/operations"; +import { test } from "../fixtures/test"; +import { checkStatusSuccessResponse } from "../utils/checkStatus.data"; import { createConnection } from "../utils/connection"; -test.beforeEach(async ({ page }) => { +test.beforeEach(async ({ page, interceptGQL }) => { await page.goto("/connections"); + await interceptGQL( + page, + allOperations.Query.checkStatus, + checkStatusSuccessResponse, + ); }); test("should have correct page title", async ({ page }) => { diff --git a/apps/web/e2e/pages/inputs.spec.ts b/apps/web/e2e/pages/inputs.spec.ts new file mode 100644 index 00000000..46296912 --- /dev/null +++ b/apps/web/e2e/pages/inputs.spec.ts @@ -0,0 +1,82 @@ +import { expect, test } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("inputs"); +}); + +test("should have correct page title", async ({ page }) => { + await expect(page).toHaveTitle(/Inputs \| CartesiScan/); +}); + +test("should have correct title", async ({ page }) => { + const title = page.getByRole("heading", { name: "Inputs" }); + await expect(title.first()).toBeVisible(); +}); + +test("should display 'All inputs' table", async ({ page }) => { + await expect( + page.getByRole("row", { name: "From To Method Index Age Data" }), + ).toBeVisible(); + await expect(page.getByRole("row")).toHaveCount(31); +}); + +test("should open application inputs page", async ({ page }) => { + await expect(page.getByTestId("inputs-table-spinner")).not.toBeVisible(); + const applicationSummaryLinks = page + .getByTestId("application-inputs-link") + .getByRole("link"); + + const firstLink = applicationSummaryLinks.first(); + const href = (await firstLink.getAttribute("href")) as string; + const [hrefPart] = href + .split(/applications|inputs|\//) + .filter((p) => p !== ""); + + await firstLink.click(); + + await page.waitForURL(`/applications/${hrefPart}/inputs`); +}); + +test("should open input details", async ({ page }) => { + await expect(page.getByTestId("inputs-table-spinner")).not.toBeVisible(); + const inputRowToggle = await page.getByTestId("input-row-toggle"); + + const firstInputRowToggle = inputRowToggle.first(); + await firstInputRowToggle.click(); + await expect(page.getByText("Notices")).toBeVisible(); + await expect(page.getByText("Reports")).toBeVisible(); + await expect(page.getByText("Vouchers")).toBeVisible(); + await expect(page.getByText("Raw")).toBeVisible(); + await expect(page.getByText("As Text")).toBeVisible(); + await expect(page.getByText("As JSON")).toBeVisible(); +}); + +test("should search for specific input", async ({ page }) => { + await expect(page.getByTestId("inputs-table-spinner")).not.toBeVisible(); + let fromAddress = page + .getByTestId("application-from-address") + .getByRole("paragraph"); + + const firstLink = fromAddress.first(); + const href = (await firstLink.textContent()) as string; + const [addressPrefix] = href.split("..."); + + const search = await page.getByTestId("search-input"); + await search.focus(); + await page.keyboard.type(addressPrefix); + await page.keyboard.press("Enter"); + await page.waitForTimeout(2000); + + fromAddress = page + .getByTestId("application-from-address") + .getByRole("paragraph"); + + const addresses = await fromAddress.all(); + addresses.map(async (address) => { + const linkHref = (await address.textContent()) as string; + + expect( + linkHref.toLowerCase().startsWith(addressPrefix.toLowerCase()), + ).toBe(true); + }); +}); diff --git a/apps/web/e2e/utils/checkStatus.data.ts b/apps/web/e2e/utils/checkStatus.data.ts new file mode 100644 index 00000000..6b05b708 --- /dev/null +++ b/apps/web/e2e/utils/checkStatus.data.ts @@ -0,0 +1,14 @@ +export const checkStatusSuccessResponse = { + inputs: { + totalCount: 41, + }, + vouchers: { + totalCount: 0, + }, + reports: { + totalCount: 41, + }, + notices: { + totalCount: 0, + }, +}; diff --git a/apps/web/e2e/utils/connection.ts b/apps/web/e2e/utils/connection.ts index 08025cec..f63fda54 100644 --- a/apps/web/e2e/utils/connection.ts +++ b/apps/web/e2e/utils/connection.ts @@ -4,7 +4,7 @@ import { Address } from "viem"; export const createConnection = async ( page: Page, address: Address, - url = "https://honeypot.sepolia.rollups.staging.cartesi.io/graphql", + url = "http://rollups-mocked.calls.to/graphql", ) => { // Find and click the button for displaying the connection modal const button = page.getByTestId("add-connection"); diff --git a/apps/web/package.json b/apps/web/package.json index 838d2402..f626b963 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -53,6 +53,7 @@ "@graphql-codegen/cli": "^5", "@graphql-codegen/client-preset": "^4", "@graphql-codegen/import-types-preset": "^3.0.0", + "@graphql-codegen/named-operations-object": "^3.0.0", "@graphql-codegen/typed-document-node": "^5", "@graphql-codegen/typescript": "^4", "@graphql-codegen/typescript-operations": "^4", diff --git a/apps/web/src/components/inputs/inputRow.tsx b/apps/web/src/components/inputs/inputRow.tsx index 5fd76d4f..6cb23679 100644 --- a/apps/web/src/components/inputs/inputRow.tsx +++ b/apps/web/src/components/inputs/inputRow.tsx @@ -105,6 +105,7 @@ const InputRow: FC = ({ alignItems: "center", justifyContent: "center", }} + data-testid="application-inputs-link" >