diff --git a/.github/workflows/test_storybook.yml b/.github/workflows/test_storybook.yml index 0f0f14c92..45c4ca641 100644 --- a/.github/workflows/test_storybook.yml +++ b/.github/workflows/test_storybook.yml @@ -14,15 +14,16 @@ jobs: storybook: timeout-minutes: 60 runs-on: ubuntu-latest - env: - NODE_OPTIONS: --max_old_space_size=4096 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: 16 - name: Install dependencies - run: npm install + run: | + npm config set @intersect.mbo:registry "https://registry.npmjs.org/" --location=global + npm config set //registry.npmjs.org/:_authToken ${NPMRC_TOKEN} --location=global + npm install - name: Install Playwright run: npx playwright install --with-deps - name: Build Storybook @@ -31,4 +32,8 @@ jobs: run: | npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \ "npx http-server storybook-static --port 6006 --silent" \ - "npx wait-on tcp:6006 && npm run test-storybook" + "npx wait-on tcp:6006 && npm run test:storybook" + + env: + NODE_OPTIONS: --max_old_space_size=4096 + NPMRC_TOKEN: ${{ secrets.NPMRC_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 10abcf407..f2720488b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,7 @@ changes. - Fix endless spinner on a dashboard [Issue 539](https://github.com/IntersectMBO/govtool/issues/539) - Remove wrongly appended `Yourself` filter on DRep Directory [Issue 1028](https://github.com/IntersectMBO/govtool/issues/1028) - Fix validation of uris in metadata [Issue 1011](https://github.com/IntersectMBO/govtool/issues/1011) +- Fix wrong link to the GA Details once it is in progress [Issue 1252](https://github.com/IntersectMBO/govtool/issues/1252) ### Changed @@ -124,6 +125,7 @@ changes. - Change input selection strategy to 3 (random) [Issue 575](https://github.com/IntersectMBO/govtool/issues/575) - Changed documents to prepare for open source [Issue 737](https://github.com/IntersectMBO/govtool/issues/737) - Changed copy on maintenance page [Issue 753](https://github.com/IntersectMBO/govtool/issues/753) +- Update link to docs [Issue 1246](https://github.com/IntersectMBO/govtool/issues/1246) ### Removed diff --git a/docs/GOVERNANCE_ACTION_SUBMISSION.md b/docs/GOVERNANCE_ACTION_SUBMISSION.md index acba1c469..1f41a31fd 100644 --- a/docs/GOVERNANCE_ACTION_SUBMISSION.md +++ b/docs/GOVERNANCE_ACTION_SUBMISSION.md @@ -12,6 +12,51 @@ For creating the Governance Action, you need to consume 2 utility methods provided by `GovernanceActionProvided` (documented later within this document), and 3 exported from `CardanoProvider` wallet actions (2 for the 2 types of supported by GovTool Governance Actions and 1 for Signing and Submitting the transaction) +### Types + +```typescript +import { VotingProposalBuilder } from "@emurgo/cardano-serialization-lib-nodejs"; + +interface GovernanceAction { + title: string; + abstract: string; + motivation: string; + rationale: string; + references: [{ label: string; uri: string }]; +} + +interface InfoProps { + hash: string; + url: string; +} + +interface TreasuryProps { + amount: string; + hash: string; + receivingAddress: string; + url: string; +} + +const createGovernanceActionJsonLD: ( + governanceAction: GovernanceAction +) => NodeObject; + +const createHash: (jsonLd: NodeObject) => string; + +const buildNewInfoGovernanceAction: ( + infoProps: InfoProps +) => Promise; + +const buildTreasuryGovernanceAction: ( + treasuryProps: TreasuryProps +) => Promise; + +const buildSignSubmitConwayCertTx: (params: { + govActionBuilder: VotingProposalBuilder; + type: "createGovAction"; +}) => Promise; +``` + ### Step 1: Create the Governance Action metadata object Create the Governance Action object with the fields specified by [CIP-108](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0108), which are: diff --git a/govtool/frontend/.storybook/preview.tsx b/govtool/frontend/.storybook/preview.tsx index 406ac7970..87b01a7d2 100644 --- a/govtool/frontend/.storybook/preview.tsx +++ b/govtool/frontend/.storybook/preview.tsx @@ -1,12 +1,12 @@ -import React from "react"; -import type { Preview } from "@storybook/react"; import { ThemeProvider } from "@emotion/react"; -import { theme } from "../src/theme"; -import { MemoryRouter, Routes, Route } from "react-router-dom"; -import { QueryClient, QueryClientProvider } from "react-query"; +import type { Preview } from "@storybook/react"; +import React from "react"; import { I18nextProvider } from "react-i18next"; -import i18n from "../src/i18n"; +import { QueryClient, QueryClientProvider } from "react-query"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; import { ModalProvider } from "../src/context/modal"; +import i18n from "../src/i18n"; +import { theme } from "../src/theme"; const queryClient = new QueryClient(); @@ -23,9 +23,9 @@ const preview: Preview = { decorators: [ (Story) => ( - - - + + + - - - + + + ), ], diff --git a/govtool/frontend/.storybook/test-runner-jest.js b/govtool/frontend/.storybook/test-runner-jest.js new file mode 100644 index 000000000..42a229c17 --- /dev/null +++ b/govtool/frontend/.storybook/test-runner-jest.js @@ -0,0 +1,13 @@ +const { getJestConfig } = require("@storybook/test-runner"); + +/** + * @type {import('@jest/types').Config.InitialOptions} + */ +module.exports = { + // The default configuration comes from @storybook/test-runner + ...getJestConfig(), + /** Add your own overrides below + * @see https://jestjs.io/docs/configuration + */ + testTimeout: 30000, +}; diff --git a/govtool/frontend/src/components/molecules/DataMissingHeader.tsx b/govtool/frontend/src/components/molecules/DataMissingHeader.tsx index f4f566d1b..7e19844e9 100644 --- a/govtool/frontend/src/components/molecules/DataMissingHeader.tsx +++ b/govtool/frontend/src/components/molecules/DataMissingHeader.tsx @@ -1,4 +1,4 @@ -import { Box } from "@mui/material"; +import { Box, SxProps } from "@mui/material"; import { Typography } from "@atoms"; import { Share } from "@molecules"; @@ -9,12 +9,14 @@ type DataMissingHeaderProps = { isDataMissing: MetadataValidationStatus | null; shareLink?: string; title?: string; + titleStyle?: SxProps; }; export const DataMissingHeader = ({ title, isDataMissing, shareLink, + titleStyle, }: DataMissingHeaderProps) => ( diff --git a/govtool/frontend/src/components/molecules/DelegationAction.tsx b/govtool/frontend/src/components/molecules/DelegationAction.tsx index 2132a7bed..c48ecc84c 100644 --- a/govtool/frontend/src/components/molecules/DelegationAction.tsx +++ b/govtool/frontend/src/components/molecules/DelegationAction.tsx @@ -3,6 +3,7 @@ import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; import { Typography } from "@atoms"; import { gray } from "@consts"; +import { ellipsizeText } from "@utils"; import { Card } from "./Card"; import { DirectVoterActionProps } from "./types"; @@ -31,7 +32,7 @@ export const DelegationAction = ({ > - {drepName} + {ellipsizeText(drepName, 30)} = ({ ...props }) => { }} data-testid={`govaction-${govActionId}-view-detail`} > - {t("govActions.viewDetails")} + {t( + inProgress + ? "govActions.viewDetails" + : "govActions.viewDetailsAndVote", + )} diff --git a/govtool/frontend/src/components/organisms/AutomatedVotingOptions.tsx b/govtool/frontend/src/components/organisms/AutomatedVotingOptions.tsx index b6d28139a..e2566254a 100644 --- a/govtool/frontend/src/components/organisms/AutomatedVotingOptions.tsx +++ b/govtool/frontend/src/components/organisms/AutomatedVotingOptions.tsx @@ -44,9 +44,6 @@ export const AutomatedVotingOptions = ({ const { networkMetrics } = useGetNetworkMetrics(); - // TODO: Change to certain automated voted option if available - const onClickInfo = () => openInNewTab("https://docs.sanchogov.tools/"); - const isDelegatedToAbstain = currentDelegation === AutomatedVotingOptionCurrentDelegation.drep_always_abstain; @@ -117,7 +114,11 @@ export const AutomatedVotingOptions = ({ onClickDelegate={() => delegate(AutomatedVotingOptionDelegationId.abstain) } - onClickInfo={onClickInfo} + onClickInfo={() => + openInNewTab( + "https://docs.sanchogov.tools/how-to-use-the-govtool/using-govtool/delegating-overview/abstain-from-every-vote", + ) + } title={ isDelegatedToAbstain ? t("dRepDirectory.delegatedToAbstainTitle", { @@ -156,7 +157,11 @@ export const AutomatedVotingOptions = ({ onClickDelegate={() => delegate(AutomatedVotingOptionDelegationId.no_confidence) } - onClickInfo={onClickInfo} + onClickInfo={() => + openInNewTab( + "https://docs.sanchogov.tools/how-to-use-the-govtool/using-govtool/delegating-overview/signal-no-confidence-on-every-vote", + ) + } title={ isDelegatedToNoConfidence ? t("dRepDirectory.delegatedToNoConfidenceTitle", { diff --git a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx index 28117472b..db44e7aed 100644 --- a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx +++ b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx @@ -30,11 +30,11 @@ export const CreateGovernanceActionForm = ({ const type = getValues("governance_action_type"); const { append, - fields: links, + fields: references, remove, } = useFieldArray({ control, - name: "links", + name: "references", }); const isContinueButtonDisabled = @@ -83,18 +83,18 @@ export const CreateGovernanceActionForm = ({ } }); - const addLink = useCallback(() => append({ link: "" }), [append]); + const addLink = useCallback(() => append({ uri: "" }), [append]); const removeLink = useCallback((index: number) => remove(index), [remove]); const renderLinks = useCallback( () => - links.map((field, index) => ( + references.map((field, index) => ( 1 ? ( + references.length > 1 ? ( )), - [links], + [references], ); return ( @@ -145,7 +145,7 @@ export const CreateGovernanceActionForm = ({ {renderLinks()} - {links?.length < MAX_NUMBER_OF_LINKS ? ( + {references?.length < MAX_NUMBER_OF_LINKS ? ( ), }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + await expect(canvas.getByText(args.stepNumber)).toBeVisible(); + await expect(canvas.getByText(args.label)).toBeVisible(); + await userEvent.click(canvas.getByRole("button")); + + await expect(infoMock).toHaveBeenCalled(); + }, }; +const readFullGuideMock = jest.fn(); export const WithIconButton: StoryObj = { args: { label: @@ -33,6 +50,7 @@ export const WithIconButton: StoryObj = { stepNumber: 2, component: ( ), }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + await expect(canvas.getByText(args.stepNumber)).toBeVisible(); + await expect(canvas.getByText(args.label)).toBeVisible(); + await userEvent.click(canvas.getByRole("button")); + + await expect(readFullGuideMock).toHaveBeenCalled(); + }, }; export const WithInput: StoryObj = { @@ -57,6 +84,15 @@ export const WithInput: StoryObj = { label: "Save this file in a location that provides a public URL (ex. github)", stepNumber: 2, - component: , + component: ( + + ), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + await expect(canvas.getByText(args.stepNumber)).toBeVisible(); + await expect(canvas.getByText(args.label)).toBeVisible(); + await expect(canvas.getByTestId("url-input")).toBeVisible(); }, }; diff --git a/govtool/frontend/src/stories/TextArea.stories.tsx b/govtool/frontend/src/stories/TextArea.stories.tsx index 669202c09..30614dfac 100644 --- a/govtool/frontend/src/stories/TextArea.stories.tsx +++ b/govtool/frontend/src/stories/TextArea.stories.tsx @@ -1,6 +1,8 @@ +import { expect } from "@storybook/jest"; import type { Meta, StoryFn } from "@storybook/react"; import { Field } from "@molecules"; +import { userEvent, within } from "@storybook/testing-library"; import { ComponentProps } from "react"; const meta: Meta = { @@ -18,28 +20,74 @@ const Template: StoryFn> = (args) => ( ); +async function assertTextbox(canvas: ReturnType) { + const text = "test"; + const inputElement = canvas.getByRole("textbox"); + await userEvent.type(inputElement, text); + await expect(inputElement).toHaveValue(text); + await expect(canvas.getByText(`${text.length}/500`)).toBeVisible(); +} + export const Default = Template.bind({}); +Default.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + await assertTextbox(canvas); +}; export const WithLabel = Template.bind({}); WithLabel.args = { label: "Label", }; +WithLabel.play = async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + await assertTextbox(canvas); + + await expect(canvas.getByText(args.label!)).toBeVisible(); +}; export const WithHelpfulText = Template.bind({}); WithHelpfulText.args = { helpfulText: "Helpful text here", }; +WithHelpfulText.play = async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + await assertTextbox(canvas); + + await expect(canvas.getByText(args.helpfulText!)).toBeVisible(); +}; export const Error = Template.bind({}); Error.args = { errorMessage: "Error message", }; +Error.play = async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + await assertTextbox(canvas); + await expect( + canvas.getByTestId( + `${args.errorMessage!.replace(/\s+/g, "-").toLowerCase()}-error`, + ), + ).toBeVisible(); +}; export const ErrorAndLabel = Template.bind({}); ErrorAndLabel.args = { errorMessage: "Error message", label: "Label", }; +ErrorAndLabel.play = async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + await assertTextbox(canvas); + + await expect(canvas.getByText(args.label!)).toBeVisible(); + await expect( + canvas.getByTestId( + `${args.errorMessage!.replace(/\s+/g, "-").toLowerCase()}-error`, + ), + ).toBeVisible(); +}; export const WithAllProps = Template.bind({}); WithAllProps.args = { @@ -47,3 +95,16 @@ WithAllProps.args = { helpfulText: "Helpful text", errorMessage: "Error message", }; +WithAllProps.play = async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + await assertTextbox(canvas); + + await expect(canvas.getByText(args.label!)).toBeVisible(); + await expect( + canvas.getByTestId( + `${args.errorMessage!.replace(/\s+/g, "-").toLowerCase()}-error`, + ), + ).toBeVisible(); + await expect(canvas.getByText(args.helpfulText!)).toBeVisible(); +}; diff --git a/govtool/frontend/src/stories/TopNav.stories.ts b/govtool/frontend/src/stories/TopNav.stories.ts index 8770c65cd..776266bbb 100644 --- a/govtool/frontend/src/stories/TopNav.stories.ts +++ b/govtool/frontend/src/stories/TopNav.stories.ts @@ -1,7 +1,7 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { userEvent, within } from "@storybook/testing-library"; import { TopNav } from "@organisms"; import { expect, jest } from "@storybook/jest"; +import type { Meta, StoryObj } from "@storybook/react"; +import { userEvent, within } from "@storybook/testing-library"; const meta = { title: "Example/TopNav", @@ -16,10 +16,7 @@ const performCommonActions = async (canvas: ReturnType) => { window.open = jest.fn(); await userEvent.click(canvas.getByTestId("logo-button")); - const homeLink = canvas.getByTestId("home-link"); const governanceActionsLink = canvas.getByTestId("governance-actions-link"); - await userEvent.click(homeLink); - await expect(homeLink).toHaveClass("active"); await userEvent.click(canvas.getByTestId("governance-actions-link")); await expect(governanceActionsLink).toHaveClass("active"); await userEvent.click(canvas.getByTestId("guides-link")); diff --git a/govtool/frontend/src/stories/modals/LoadingModal.stories.tsx b/govtool/frontend/src/stories/modals/LoadingModal.stories.tsx index 6bc59375d..5f1320e5a 100644 --- a/govtool/frontend/src/stories/modals/LoadingModal.stories.tsx +++ b/govtool/frontend/src/stories/modals/LoadingModal.stories.tsx @@ -1,8 +1,10 @@ -import { useEffect } from "react"; import { Meta, StoryFn } from "@storybook/react"; +import { useEffect } from "react"; import { Modal } from "@atoms"; import { LoadingModal, LoadingModalState } from "@organisms"; +import { expect } from "@storybook/jest"; +import { screen, waitFor, within } from "@storybook/testing-library"; import { callAll } from "@utils"; import { useModal } from "../../context/modal"; @@ -65,3 +67,18 @@ Loading.args = { title: "GovTool Is Checking Your Data", dataTestId: "loading-modal", }; +Loading.play = async ({ args }) => { + waitFor(async () => { + const modalScreen = screen.getAllByTestId("loading-modal")[0]; + const loadingModalCanvas = within(modalScreen); + + await expect(loadingModalCanvas.getByRole("img")).toHaveAttribute( + "alt", + "loader", + ); + await expect(loadingModalCanvas.getByText(args.title)).toBeVisible(); + await expect( + loadingModalCanvas.getByText(args.message as string), + ).toBeVisible(); + }); +}; diff --git a/govtool/frontend/src/stories/modals/VotingPowerModal.stories.tsx b/govtool/frontend/src/stories/modals/VotingPowerModal.stories.tsx index 9faf804e9..c4bca132c 100644 --- a/govtool/frontend/src/stories/modals/VotingPowerModal.stories.tsx +++ b/govtool/frontend/src/stories/modals/VotingPowerModal.stories.tsx @@ -1,9 +1,12 @@ +import { expect } from "@storybook/jest"; import { Meta, StoryFn } from "@storybook/react"; import { Modal } from "@atoms"; -import { StatusModal, VotingPowerModalState } from "@organisms"; import { useModal } from "@context"; -import { callAll } from "@utils"; +import { StatusModal, VotingPowerModalState } from "@organisms"; +import { screen, waitFor, within } from "@storybook/testing-library"; +import { callAll, correctAdaFormat } from "@utils"; +import { useEffect } from "react"; const meta = { title: "Example/Modals/VotingPowerModal", @@ -24,6 +27,10 @@ const Template: StoryFn = (args) => { }); }; + useEffect(() => { + open(); + }, [openModal]); + return ( <>