diff --git a/instances/treasury-devdao.near/widget/app.jsx b/instances/treasury-devdao.near/widget/app.jsx index edcfc545..d2fcc990 100644 --- a/instances/treasury-devdao.near/widget/app.jsx +++ b/instances/treasury-devdao.near/widget/app.jsx @@ -85,7 +85,12 @@ function Page() { return ( - + diff --git a/instances/treasury-devdao.near/widget/components/BalanceBanner.jsx b/instances/treasury-devdao.near/widget/components/BalanceBanner.jsx new file mode 100644 index 00000000..0a9b2c35 --- /dev/null +++ b/instances/treasury-devdao.near/widget/components/BalanceBanner.jsx @@ -0,0 +1,88 @@ +const { getNearBalances } = VM.require( + "${REPL_BASE_DEPLOYMENT_ACCOUNT}/widget/lib.common" +); + +const Container = styled.div` + .low-balance-warning { + background: rgba(255, 158, 0, 0.1); + color: var(--other-warning); + padding-inline: 0.8rem; + padding-block: 0.5rem; + font-weight: 500; + font-size: 13px; + + h5 { + color: var(--other-warning) !important; + } + } + + .insufficient-balance-warning { + background-color: rgba(217, 92, 74, 0.08); + color: var(--other-red); + padding-inline: 0.8rem; + padding-block: 0.5rem; + font-weight: 500; + font-size: 13px; + + h5 { + color: var(--other-red) !important; + } + } +`; + +function formatNearAmount(amount) { + return parseFloat( + Big(amount ?? "0") + .div(Big(10).pow(24)) + .toFixed(2) + ); +} + +function BalanceBanner({ accountId, treasuryDaoID }) { + if (!treasuryDaoID || typeof getNearBalances !== "function" || !accountId) { + return <>; + } + + const nearBalances = getNearBalances(accountId); + + const LOW_BALANCE_LIMIT = 0.7; // 0.7N + const INSUFFICIENT_BALANCE_LIMIT = 0.1; // 0.1N + + const profile = Social.getr(`${accountId}/profile`); + const name = profile.name ?? accountId; + + return ( + + {!nearBalances || + !nearBalances.availableParsed || + nearBalances.availableParsed === "0.00" || + parseFloat(nearBalances.availableParsed) > LOW_BALANCE_LIMIT ? ( + <> + ) : parseFloat(nearBalances.availableParsed) < + INSUFFICIENT_BALANCE_LIMIT ? ( +
+ +
+
Insufficient Funds
+ Hey {name}, you don't have enough NEAR to complete actions on your + treasury. You need at least {INSUFFICIENT_BALANCE_LIMIT}N. Please + add more funds to your account and try again +
+
+ ) : ( +
+ +
+
Low Balance
+ Hey {name}, your NEAR balance is {nearBalances.availableParsed}N, + which is getting low. The minimum balance required is{" "} + {INSUFFICIENT_BALANCE_LIMIT}N. Please add more NEAR to your account + soon to avoid any issues completing actions on your treasury. +
+
+ )} +
+ ); +} + +return { BalanceBanner }; diff --git a/instances/treasury-devdao.near/widget/components/InsufficientBannerModal.jsx b/instances/treasury-devdao.near/widget/components/InsufficientBannerModal.jsx new file mode 100644 index 00000000..a604b2b8 --- /dev/null +++ b/instances/treasury-devdao.near/widget/components/InsufficientBannerModal.jsx @@ -0,0 +1,74 @@ +const { getNearBalances } = VM.require( + "${REPL_BASE_DEPLOYMENT_ACCOUNT}/widget/lib.common" +); + +const { Modal, ModalContent, ModalHeader, ModalFooter } = VM.require( + "${REPL_BASE_DEPLOYMENT_ACCOUNT}/widget/lib.modal" +); + +function formatNearAmount(amount) { + return parseFloat( + Big(amount ?? "0") + .div(Big(10).pow(24)) + .toFixed(2) + ); +} + +const { ActionButton, checkForDeposit, treasuryDaoID, callbackAction } = props; + +const nearBalances = getNearBalances(context.accountId); +const profile = Social.getr(`${context.accountId}/profile`); +const name = profile.name ?? context.accountId; +const daoPolicy = useCache( + () => Near.asyncView(treasuryDaoID, "get_policy"), + "get_policy", + { subscribe: false } +); + +const [showModal, setShowModal] = useState(false); +const ADDITIONAL_AMOUNT = checkForDeposit + ? formatNearAmount(daoPolicy?.proposal_bond) + : 0; + +const INSUFFICIENT_BALANCE_LIMIT = ADDITIONAL_AMOUNT + 0.1; // 0.1N + +function checkBalance() { + if (parseFloat(nearBalances?.availableParsed) < INSUFFICIENT_BALANCE_LIMIT) { + setShowModal(true); + } else { + callbackAction(); + } +} + +const WarningModal = () => ( + + +
+
+ + Insufficient Funds +
+ setShowModal(false)} + > +
+
+ + Hey {name}, you don't have enough NEAR to complete actions on your + treasury. You need at least {INSUFFICIENT_BALANCE_LIMIT}N{" "} + {checkForDeposit && + ", which includes the proposal bond needed to create a proposal"} + . Please add more funds to your account and try again. + +
+); + +return ( + <> +
+ +
+ {showModal && } + +); diff --git a/instances/treasury-devdao.near/widget/components/VoteActions.jsx b/instances/treasury-devdao.near/widget/components/VoteActions.jsx index 1f35290c..e0afd73a 100644 --- a/instances/treasury-devdao.near/widget/components/VoteActions.jsx +++ b/instances/treasury-devdao.near/widget/components/VoteActions.jsx @@ -229,13 +229,24 @@ return ( hasVotingPermission && (
{ + ActionButton: () => ( + + ), + checkForDeposit: false, + treasuryDaoID, + callbackAction: () => { if (isInsufficientBalance) { setShowWarning(true); } else { @@ -243,23 +254,30 @@ return ( setConfirmModal(true); } }, - loading: isTxnCreated && vote === actions.APPROVE, - disabled: isTxnCreated, }} /> { + ActionButton: () => ( + + ), + checkForDeposit: false, + treasuryDaoID, + callbackAction: () => { setVote(actions.REJECT); setConfirmModal(true); }, - loading: isTxnCreated && vote === actions.REJECT, - disabled: isTxnCreated, }} />
@@ -267,20 +285,29 @@ return ( )} {/* currently showing delete btn only for proposal creator */} {hasDeletePermission && proposalCreator === accountId && ( - + ), + checkForDeposit: false, + treasuryDaoID, + callbackAction: () => { + setVote(actions.REMOVE); + setConfirmModal(true); + }, }} - data-testid="delete-btn" - disabled={isTxnCreated} - > - - + /> )} )} diff --git a/instances/treasury-devdao.near/widget/components/templates/AppLayout.jsx b/instances/treasury-devdao.near/widget/components/templates/AppLayout.jsx index b82075fc..53df5cd8 100644 --- a/instances/treasury-devdao.near/widget/components/templates/AppLayout.jsx +++ b/instances/treasury-devdao.near/widget/components/templates/AppLayout.jsx @@ -1,3 +1,7 @@ +const { BalanceBanner } = VM.require( + `${REPL_BASE_DEPLOYMENT_ACCOUNT}/widget/components.BalanceBanner` +) || { BalanceBanner: () => <> }; + const AppHeader = ({ page, instance }) => ( ( /> ); -function AppLayout({ page, instance, children, treasuryDaoID }) { +function AppLayout({ page, instance, children, treasuryDaoID, accountId }) { const config = treasuryDaoID ? useCache( () => Near.asyncView(treasuryDaoID, "get_config"), @@ -379,6 +383,10 @@ function AppLayout({ page, instance, children, treasuryDaoID }) { .warning-icon { color: var(--other-warning) !important; } + + .error-icon { + color: var(--other-red) !important; + } `; return !config ? ( @@ -387,6 +395,7 @@ function AppLayout({ page, instance, children, treasuryDaoID }) { +
{children}
diff --git a/instances/treasury-devdao.near/widget/lib/common.jsx b/instances/treasury-devdao.near/widget/lib/common.jsx index d9d942ef..3c650c45 100644 --- a/instances/treasury-devdao.near/widget/lib/common.jsx +++ b/instances/treasury-devdao.near/widget/lib/common.jsx @@ -426,10 +426,8 @@ function formatNearAmount(amount) { .toFixed(2); } -function getNearBalances(treasuryDaoID) { - const resp = fetch( - `https://api.fastnear.com/v1/account/${treasuryDaoID}/full` - ); +function getNearBalances(accountId) { + const resp = fetch(`https://api.fastnear.com/v1/account/${accountId}/full`); const storage = Big(resp?.body?.state?.storage_bytes ?? "0") .mul(Big(10).pow(19)) .toFixed(); diff --git a/instances/treasury-devdao.near/widget/lib/modal.jsx b/instances/treasury-devdao.near/widget/lib/modal.jsx index 07abd2c5..36921f0b 100644 --- a/instances/treasury-devdao.near/widget/lib/modal.jsx +++ b/instances/treasury-devdao.near/widget/lib/modal.jsx @@ -95,7 +95,7 @@ const NoButton = styled.button` const ModalHeader = ({ children }) => ( -
{children}
+
{children}
); diff --git a/instances/treasury-devdao.near/widget/pages/payments/index.jsx b/instances/treasury-devdao.near/widget/pages/payments/index.jsx index 1a26c592..e1583adb 100644 --- a/instances/treasury-devdao.near/widget/pages/payments/index.jsx +++ b/instances/treasury-devdao.near/widget/pages/payments/index.jsx @@ -28,12 +28,19 @@ const SidebarMenu = ({ currentTab }) => { style={{ paddingBottom: "7px" }} > {hasCreatePermission && ( - + ( + + ), + checkForDeposit: true, + treasuryDaoID, + callbackAction: () => setShowCreateRequest(true), + }} + /> )} { {hasCreatePermission && (
- { - setSelectedMember(group); - setShowEditor(true); + ( + + ), + checkForDeposit: true, + treasuryDaoID, + callbackAction: () => { + setSelectedMember(group); + setShowEditor(true); + }, }} - > - { - setSelectedMember(group); - setShowDeleteModal(true); + /> + ( + + ), + checkForDeposit: true, + treasuryDaoID, + callbackAction: () => { + setSelectedMember(group); + setShowDeleteModal(true); + }, }} - > + />
)} @@ -310,12 +324,21 @@ return (
All Members
{hasCreatePermission && ( - + ( + + ), + checkForDeposit: true, + treasuryDaoID, + callbackAction: () => { + setShowEditor(true); + }, + }} + /> )}
{loading ? ( diff --git a/instances/treasury-devdao.near/widget/pages/settings/Theme.jsx b/instances/treasury-devdao.near/widget/pages/settings/Theme.jsx index 3c6c1a2d..7c2472e6 100644 --- a/instances/treasury-devdao.near/widget/pages/settings/Theme.jsx +++ b/instances/treasury-devdao.near/widget/pages/settings/Theme.jsx @@ -444,15 +444,23 @@ return ( disabled: !hasCreatePermission, }} /> - ( + + ), + checkForDeposit: true, + treasuryDaoID, + callbackAction: onSubmitClick, }} /> diff --git a/instances/treasury-devdao.near/widget/pages/settings/Thresholds.jsx b/instances/treasury-devdao.near/widget/pages/settings/Thresholds.jsx index 1c52eafc..bcdc71df 100644 --- a/instances/treasury-devdao.near/widget/pages/settings/Thresholds.jsx +++ b/instances/treasury-devdao.near/widget/pages/settings/Thresholds.jsx @@ -481,17 +481,28 @@ return ( }} /> setConfirmModal(true), - loading: isTxnCreated, + ActionButton: () => ( + + ), + checkForDeposit: true, + treasuryDaoID, + callbackAction: () => { + setConfirmModal(true); + }, }} /> diff --git a/instances/treasury-devdao.near/widget/pages/settings/VotingDurationPage.jsx b/instances/treasury-devdao.near/widget/pages/settings/VotingDurationPage.jsx index 5dc25d02..fe06776d 100644 --- a/instances/treasury-devdao.near/widget/pages/settings/VotingDurationPage.jsx +++ b/instances/treasury-devdao.near/widget/pages/settings/VotingDurationPage.jsx @@ -475,18 +475,28 @@ return ( }} /> ( + + ), + checkForDeposit: true, + treasuryDaoID, + callbackAction: submitChangeRequest, }} /> diff --git a/instances/treasury-devdao.near/widget/pages/stake-delegation/CreateButton.jsx b/instances/treasury-devdao.near/widget/pages/stake-delegation/CreateButton.jsx index f9822abc..9c101231 100644 --- a/instances/treasury-devdao.near/widget/pages/stake-delegation/CreateButton.jsx +++ b/instances/treasury-devdao.near/widget/pages/stake-delegation/CreateButton.jsx @@ -52,6 +52,10 @@ function toggleWithdrawPage() { setShowWithdrawRequest((prev) => !prev); } +const toggleDropdown = () => { + setCreateBtnOpen((prev) => !prev); +}; + const CreateBtn = () => { const btnOptions = [ { @@ -85,10 +89,6 @@ const CreateBtn = () => { }, ]; - const toggleDropdown = () => { - setCreateBtnOpen((prev) => !prev); - }; - const DropdowntBtnContainer = styled.div` font-size: 13px; min-width: 150px; @@ -129,7 +129,7 @@ const CreateBtn = () => { display: block; opacity: 1; transform: translateY(0); - z-index:9999; + z-index:1000; } } @@ -199,14 +199,12 @@ const CreateBtn = () => {
Create Request
- {hasCreatePermission && } + {hasCreatePermission && ( + + )} - + diff --git a/instances/treasury-templar.near/widget/app.jsx b/instances/treasury-templar.near/widget/app.jsx index edcfc545..d2fcc990 100644 --- a/instances/treasury-templar.near/widget/app.jsx +++ b/instances/treasury-templar.near/widget/app.jsx @@ -85,7 +85,12 @@ function Page() { return ( - + diff --git a/instances/treasury-testing-infinex.near/widget/app.jsx b/instances/treasury-testing-infinex.near/widget/app.jsx index 3aca2e8b..4859aafb 100644 --- a/instances/treasury-testing-infinex.near/widget/app.jsx +++ b/instances/treasury-testing-infinex.near/widget/app.jsx @@ -96,7 +96,12 @@ function Page() { return ( - + diff --git a/instances/treasury-testing.near/widget/app.jsx b/instances/treasury-testing.near/widget/app.jsx index edcfc545..d2fcc990 100644 --- a/instances/treasury-testing.near/widget/app.jsx +++ b/instances/treasury-testing.near/widget/app.jsx @@ -85,7 +85,12 @@ function Page() { return ( - + diff --git a/playwright-tests/tests/payments/create-payment-request.spec.js b/playwright-tests/tests/payments/create-payment-request.spec.js index 1826f614..08317a33 100644 --- a/playwright-tests/tests/payments/create-payment-request.spec.js +++ b/playwright-tests/tests/payments/create-payment-request.spec.js @@ -5,7 +5,7 @@ import { getTransactionModalObject, mockTransactionSubmitRPCResponses, } from "../../util/transaction"; -import { updateDaoPolicyMembers } from "../../util/rpcmock"; +import { mockNearBalances, updateDaoPolicyMembers } from "../../util/rpcmock"; import { getInstanceConfig } from "../../util/config.js"; import { CurrentTimestampInNanoseconds, @@ -18,6 +18,7 @@ import { focusInputClearAndBlur, focusInputReplaceAndBlur, } from "../../util/forms.js"; +import { InsufficientBalance } from "../../util/lib.js"; async function clickCreatePaymentRequestButton(page) { const createPaymentRequestButton = await page.getByRole("button", { @@ -120,7 +121,23 @@ test.afterEach(async ({ page }, testInfo) => { await page.unrouteAll({ behavior: "ignoreErrors" }); }); -test.describe("admin connected", function () { +test.describe("User is not logged in", function () { + test("should not see 'Create Request' action", async ({ + page, + instanceAccount, + }) => { + await page.goto(`/${instanceAccount}/widget/app?page=payments`); + await expect(page.getByText("Pending Requests")).toBeVisible(); + await expect( + page.getByRole("button", { + name: "Create Request", + }) + ).toBeHidden(); + }); +}); + +test.describe("User is logged in", function () { + const signedUser = "theori.near"; test.use({ contextOptions: { permissions: ["clipboard-read", "clipboard-write"], @@ -128,6 +145,55 @@ test.describe("admin connected", function () { storageState: "playwright-tests/storage-states/wallet-connected-admin.json", }); + test("low account balance should show warning modal, and allow action ", async ({ + page, + instanceAccount, + }) => { + test.setTimeout(60_000); + await mockNearBalances({ + page, + accountId: signedUser, + balance: BigInt(0.6 * 10 ** 24).toString(), + storage: 8, + }); + await page.goto(`/${instanceAccount}/widget/app?page=payments`); + await expect( + page.getByText( + "Please add more NEAR to your account soon to avoid any issues completing actions on your treasury" + ) + ).toBeVisible(); + }); + + test("insufficient account balance should show warning modal, disallow action ", async ({ + page, + instanceAccount, + }) => { + test.setTimeout(60_000); + await updateDaoPolicyMembers({ page }); + await mockNearBalances({ + page, + accountId: signedUser, + balance: InsufficientBalance, + storage: 8, + }); + await page.goto(`/${instanceAccount}/widget/app?page=payments`); + await expect( + page.getByText( + "Hey Ori, you don't have enough NEAR to complete actions on your treasury." + ) + ).toBeVisible(); + await page + .getByRole("button", { + name: "Create Request", + }) + .click(); + await expect( + page + .getByText("Please add more funds to your account and try again") + .nth(1) + ).toBeVisible(); + }); + test("different amount values should not throw any error", async ({ page, instanceAccount, diff --git a/playwright-tests/tests/payments/vote-on-request.spec.js b/playwright-tests/tests/payments/vote-on-request.spec.js index 9304f174..77a649ff 100644 --- a/playwright-tests/tests/payments/vote-on-request.spec.js +++ b/playwright-tests/tests/payments/vote-on-request.spec.js @@ -2,13 +2,18 @@ import { expect } from "@playwright/test"; import { test } from "../../util/test.js"; import { mockTransactionSubmitRPCResponses } from "../../util/transaction"; -import { mockRpcRequest, updateDaoPolicyMembers } from "../../util/rpcmock"; +import { + mockNearBalances, + mockRpcRequest, + updateDaoPolicyMembers, +} from "../../util/rpcmock"; import { setDontAskAgainCacheValues } from "../../util/cache"; import { mockPikespeakFTTokensResponse } from "../../util/pikespeak.js"; import { CurrentTimestampInNanoseconds, TransferProposalData, } from "../../util/inventory.js"; +import { InsufficientBalance } from "../../util/lib.js"; async function mockWithFTBalance({ page, daoAccount, isSufficient }) { await page.route( @@ -153,7 +158,41 @@ test.describe("don't ask again", function () { storageState: "playwright-tests/storage-states/wallet-connected-admin-with-accesskey.json", }); - test("should throw insufficient balance error", async ({ + + test("should throw insufficient signed in account balance error", async ({ + page, + instanceAccount, + daoAccount, + }) => { + test.setTimeout(60_000); + await updateDaoPolicyMembers({ page }); + await mockNearBalances({ + page, + accountId: "theori.near", + balance: InsufficientBalance, + storage: 8, + }); + await voteOnProposal({ + page, + daoAccount, + instanceAccount, + voteStatus: "Approved", + vote: "Approve", + }); + const approveButton = page + .getByRole("button", { + name: "Approve", + }) + .first(); + await expect(approveButton).toBeEnabled({ timeout: 30_000 }); + await approveButton.click(); + await expect( + page + .getByText("Please add more funds to your account and try again") + .nth(1) + ).toBeVisible(); + }); + test("should throw insufficient dao account balance error", async ({ page, instanceAccount, daoAccount, diff --git a/playwright-tests/tests/settings/create-member-request.spec.js b/playwright-tests/tests/settings/create-member-request.spec.js index 7b00f4e5..e2256b87 100644 --- a/playwright-tests/tests/settings/create-member-request.spec.js +++ b/playwright-tests/tests/settings/create-member-request.spec.js @@ -5,9 +5,13 @@ import { getTransactionModalObject, mockTransactionSubmitRPCResponses, } from "../../util/transaction.js"; -import { mockRpcRequest, updateDaoPolicyMembers } from "../../util/rpcmock.js"; +import { + mockNearBalances, + mockRpcRequest, + updateDaoPolicyMembers, +} from "../../util/rpcmock.js"; import { mockInventory } from "../../util/inventory.js"; -import { encodeToMarkdown } from "../../util/lib.js"; +import { InsufficientBalance, encodeToMarkdown } from "../../util/lib.js"; const lastProposalId = 3; @@ -32,7 +36,7 @@ async function updateLastProposalId(page) { async function navigateToMembersPage({ page, instanceAccount }) { await page.goto(`/${instanceAccount}/widget/app?page=settings`); - await page.waitForTimeout(2_000); + await page.waitForTimeout(5_000); await page.getByText("Members").click(); await expect(page.getByText("All Members")).toBeVisible({ timeout: 10_000 }); } @@ -74,18 +78,39 @@ test.describe("User is logged in", function () { storageState: "playwright-tests/storage-states/wallet-connected-admin.json", }); - test.beforeEach(async ({ page, daoAccount, instanceAccount }) => { + test.beforeEach(async ({ page, daoAccount, instanceAccount }, testInfo) => { await mockInventory({ page, account: daoAccount }); await updateDaoPolicyMembers({ page }); await updateLastProposalId(page); + if (testInfo.title.includes("insufficient account balance")) { + await mockNearBalances({ + page, + accountId: "theori.near", + balance: InsufficientBalance, + storage: 8, + }); + } await navigateToMembersPage({ page, instanceAccount }); }); - test("should show members of the DAO", async ({ + test("insufficient account balance should show warning modal, disallow action ", async ({ page, - instanceAccount, - daoAccount, }) => { + test.setTimeout(60_000); + await expect( + page.getByText( + "Hey Ori, you don't have enough NEAR to complete actions on your treasury." + ) + ).toBeVisible(); + await page.getByRole("button", { name: " New Member" }).click(); + await expect( + page + .getByText("Please add more funds to your account and try again") + .nth(1) + ).toBeVisible(); + }); + + test("should show members of the DAO", async ({ page }) => { test.setTimeout(60_000); await expect(page.getByText("Megha", { exact: true })).toBeVisible(); }); diff --git a/playwright-tests/tests/settings/create-threshold-request.spec.js b/playwright-tests/tests/settings/create-threshold-request.spec.js index 4188d438..526d6472 100644 --- a/playwright-tests/tests/settings/create-threshold-request.spec.js +++ b/playwright-tests/tests/settings/create-threshold-request.spec.js @@ -4,10 +4,11 @@ import { test } from "../../util/test.js"; import { getTransactionModalObject } from "../../util/transaction.js"; import { getMockedPolicy, + mockNearBalances, mockRpcRequest, updateDaoPolicyMembers, } from "../../util/rpcmock.js"; -import { encodeToMarkdown } from "../../util/lib.js"; +import { InsufficientBalance, encodeToMarkdown } from "../../util/lib.js"; const lastProposalId = 3; @@ -51,11 +52,19 @@ test.afterEach(async ({ page }, testInfo) => { await page.unrouteAll({ behavior: "ignoreErrors" }); }); +async function navigateToThresholdPage({ page, instanceAccount }) { + await page.goto(`/${instanceAccount}/widget/app?page=settings`); + await page.waitForTimeout(5_000); + await page.getByText("Voting Threshold").click(); + await expect(page.getByText("Permission Groups")).toBeVisible({ + timeout: 10_000, + }); +} + test.describe("User is not logged in", function () { test.beforeEach(async ({ page, instanceAccount }) => { await updateDaoPolicyMembers({ page }); - await page.goto(`/${instanceAccount}/widget/app?page=settings`); - await page.getByText("Voting Thresholds").click({ timeout: 20_000 }); + await navigateToThresholdPage({ page, instanceAccount }); }); test("should show members of different roles", async ({ page }) => { @@ -87,13 +96,13 @@ test.describe("User is not logged in", function () { timeout: 20_000, }); await expect(page.getByTestId("dropdown-btn")).toBeDisabled(); - await expect(page.getByText("Submit")).toBeHidden(); + await expect(page.getByText("Submit Request")).toBeHidden(); await expect(page.getByTestId("threshold-input")).toBeDisabled(); }); }); async function fillInput(page, value) { - const submitBtn = page.getByText("Submit"); + const submitBtn = page.getByText("Submit Request"); const thresholdInput = page.getByTestId("threshold-input"); await thresholdInput.type(value); expect(submitBtn).toBeVisible(); @@ -104,19 +113,46 @@ test.describe("User is logged in", function () { storageState: "playwright-tests/storage-states/wallet-connected-admin.json", }); - test.beforeEach(async ({ page, instanceAccount }) => { + test.beforeEach(async ({ page, instanceAccount }, testInfo) => { await updateLastProposalId(page); await updateDaoPolicyMembers({ page }); - await page.goto(`/${instanceAccount}/widget/app?page=settings`); - await page.getByText("Voting Thresholds").click({ timeout: 20_000 }); - await expect(page.getByText("Submit")).toBeVisible({ - timeout: 20_000, - }); - await page.getByTestId("dropdown-btn").click(); + if (testInfo.title.includes("insufficient account balance")) { + await mockNearBalances({ + page, + accountId: "theori.near", + balance: InsufficientBalance, + storage: 8, + }); + } + await navigateToThresholdPage({ page, instanceAccount }); + }); + + test("insufficient account balance should show warning modal, disallow action ", async ({ + page, + }) => { + test.setTimeout(60_000); + + await expect( + page.getByText( + "Hey Ori, you don't have enough NEAR to complete actions on your treasury." + ) + ).toBeVisible(); + await page.getByTestId("threshold-input").fill("1"); + await page + .getByText("Submit Request", { + exact: true, + }) + .click(); + await expect( + page + .getByText("Please add more funds to your account and try again") + .nth(1) + ).toBeVisible(); }); test("should allow only valid input for threshold", async ({ page }) => { test.setTimeout(60_000); + await page.getByTestId("dropdown-btn").click(); await page.getByRole("list").getByText("Number of votes").click(); await fillInput(page, "20097292"); await fillInput(page, "10.323"); @@ -134,7 +170,8 @@ test.describe("User is logged in", function () { page, }) => { test.setTimeout(150_000); - const submitBtn = page.getByText("Submit"); + const submitBtn = page.getByText("Submit Request"); + await page.getByTestId("dropdown-btn").click(); await page.getByRole("list").getByText("Number of votes").click(); const thresholdInput = page.getByTestId("threshold-input"); await thresholdInput.fill("20"); @@ -173,7 +210,8 @@ test.describe("User is logged in", function () { test("should be able to update policy by percentage", async ({ page }) => { test.setTimeout(150_000); - const submitBtn = page.getByText("Submit"); + const submitBtn = page.getByText("Submit Request"); + await page.getByTestId("dropdown-btn").click(); await page.getByRole("list").getByText("Percentage of members").click(); const thresholdInput = page.getByTestId("threshold-input"); await thresholdInput.fill("101"); diff --git a/playwright-tests/tests/settings/theme.spec.js b/playwright-tests/tests/settings/theme.spec.js index f9ae6fcd..754a838e 100644 --- a/playwright-tests/tests/settings/theme.spec.js +++ b/playwright-tests/tests/settings/theme.spec.js @@ -1,9 +1,14 @@ import { expect } from "@playwright/test"; import { test } from "../../util/test.js"; import { getTransactionModalObject } from "../../util/transaction.js"; -import { mockRpcRequest, updateDaoPolicyMembers } from "../../util/rpcmock.js"; +import { + mockRpcRequest, + updateDaoPolicyMembers, + mockNearBalances, +} from "../../util/rpcmock.js"; import path from "path"; import { fileURLToPath } from "url"; +import { InsufficientBalance } from "../../util/lib.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -59,7 +64,7 @@ test.describe("User is not logged in", function () { await navigateToThemePage({ page, instanceAccount }); await expect(page.locator("input[type='color']")).toBeDisabled(); await expect( - page.getByRole("button", { name: "Save changes" }) + page.getByRole("button", { name: "Submit Request" }) ).toBeDisabled(); }); }); @@ -69,10 +74,39 @@ test.describe("User is logged in", function () { storageState: "playwright-tests/storage-states/wallet-connected-admin.json", }); - test.beforeEach(async ({ page, instanceAccount }) => { + test.beforeEach(async ({ page, instanceAccount }, testInfo) => { + if (testInfo.title.includes("insufficient account balance")) { + await mockNearBalances({ + page, + accountId: "theori.near", + balance: InsufficientBalance, + storage: 8, + }); + } await navigateToThemePage({ page, instanceAccount }); }); + test("insufficient account balance should show warning modal, disallow action ", async ({ + page, + }) => { + test.setTimeout(60_000); + await expect( + page.getByText( + "Hey Ori, you don't have enough NEAR to complete actions on your treasury." + ) + ).toBeVisible(); + await page + .getByText("Submit Request", { + exact: true, + }) + .click(); + await expect( + page + .getByText("Please add more funds to your account and try again") + .nth(1) + ).toBeVisible(); + }); + test("should be able to upload image, should show error with incorrect width and allow correct one", async ({ page, }) => { @@ -91,7 +125,9 @@ test.describe("User is logged in", function () { const logoInput = await page .frameLocator("iframe") .locator("input[type=file]"); - const submitBtn = await page.getByRole("button", { name: "Save changes" }); + const submitBtn = await page.getByRole("button", { + name: "Submit Request", + }); // invalid image await logoInput.setInputFiles(path.join(__dirname, "./assets/invalid.png")); await expect( @@ -128,9 +164,8 @@ test.describe("User is logged in", function () { await page.getByRole("textbox").nth(1).fill(newColor); await page.getByTestId("dropdown-btn").click(); await page.getByText("Light").click(); - await page.getByRole("button", { name: "Save changes" }).click(); await expect(page.getByText("Processing your request ...")).toBeVisible(); - + await page.getByRole("button", { name: "Submit Request" }).click(); await expect(await getTransactionModalObject(page)).toEqual({ proposal: { description: "* Title: Update Config - Theme & logo", diff --git a/playwright-tests/tests/settings/voting-duration.spec.js b/playwright-tests/tests/settings/voting-duration.spec.js index 92ff2991..a7c6f43d 100644 --- a/playwright-tests/tests/settings/voting-duration.spec.js +++ b/playwright-tests/tests/settings/voting-duration.spec.js @@ -2,8 +2,12 @@ import { expect } from "@playwright/test"; import { test } from "../../util/test.js"; import { getTransactionModalObject } from "../../util/transaction.js"; import { SandboxRPC } from "../../util/sandboxrpc.js"; -import { mockRpcRequest, updateDaoPolicyMembers } from "../../util/rpcmock.js"; -import { encodeToMarkdown } from "../../util/lib.js"; +import { + mockNearBalances, + mockRpcRequest, + updateDaoPolicyMembers, +} from "../../util/rpcmock.js"; +import { InsufficientBalance, encodeToMarkdown } from "../../util/lib.js"; test.afterEach(async ({ page }, testInfo) => { console.log(`Finished ${testInfo.title} with status ${testInfo.status}`); @@ -42,6 +46,36 @@ test.describe("User is logged in", function () { storageState: "playwright-tests/storage-states/wallet-connected-admin.json", }); + test("insufficient account balance should show warning modal, disallow action ", async ({ + page, + instanceAccount, + }) => { + test.setTimeout(60_000); + await mockNearBalances({ + page, + accountId: "theori.near", + balance: InsufficientBalance, + storage: 8, + }); + await navigateToVotingDurationPage({ page, instanceAccount }); + await expect( + page.getByText( + "Hey Ori, you don't have enough NEAR to complete actions on your treasury." + ) + ).toBeVisible(); + await page.getByPlaceholder("Enter voting duration days").fill("2"); + await page + .getByText("Submit Request", { + exact: true, + }) + .click(); + await expect( + page + .getByText("Please add more funds to your account and try again") + .nth(1) + ).toBeVisible(); + }); + test("should set voting duration", async ({ page, instanceAccount, diff --git a/playwright-tests/tests/stake-delegation/stake-delegation.spec.js b/playwright-tests/tests/stake-delegation/stake-delegation.spec.js index bd10d964..6294d24f 100644 --- a/playwright-tests/tests/stake-delegation/stake-delegation.spec.js +++ b/playwright-tests/tests/stake-delegation/stake-delegation.spec.js @@ -6,7 +6,11 @@ import { mockTransactionSubmitRPCResponses, } from "../../util/transaction"; import { utils } from "near-api-js"; -import { mockRpcRequest, updateDaoPolicyMembers } from "../../util/rpcmock.js"; +import { + mockNearBalances, + mockRpcRequest, + updateDaoPolicyMembers, +} from "../../util/rpcmock.js"; import { CurrentTimestampInNanoseconds, OldJsonProposalData, @@ -16,6 +20,7 @@ import { } from "../../util/inventory.js"; import { setDontAskAgainCacheValues } from "../../util/cache.js"; import Big from "big.js"; +import { InsufficientBalance } from "../../util/lib.js"; test.afterEach(async ({ page }, testInfo) => { console.log(`Finished ${testInfo.title} with status ${testInfo.status}`); @@ -160,27 +165,6 @@ async function mockStakedPoolBalances({ page }) { }); } -async function mockNearBalances({ page, daoAccount, balance }) { - await page.route( - `https://api.fastnear.com/v1/account/${daoAccount}/full`, - async (route) => { - const json = { - account_id: daoAccount, - nfts: [], - pools: [], - state: { - balance: balance, - locked: "0", - storage_bytes: 677278, - }, - tokens: [], - }; - - await route.fulfill({ json }); - } - ); -} - export async function mockStakedPools({ daoAccount, page, @@ -412,7 +396,7 @@ async function voteOnProposal({ } test.describe("Have valid staked requests and sufficient token balance", function () { - test.beforeEach(async ({ page, instanceAccount, daoAccount }) => { + test.beforeEach(async ({ page, instanceAccount, daoAccount }, testInfo) => { const instanceConfig = await getInstanceConfig({ page, instanceAccount }); if ( !instanceConfig.navbarLinks.find( @@ -425,17 +409,28 @@ test.describe("Have valid staked requests and sufficient token balance", functio await mockStakeProposals({ page }); await updateDaoPolicyMembers({ page }); await mockStakedPools({ page, daoAccount }); - await mockNearBalances({ - page, - daoAccount, - balance: sufficientAvailableBalance, - }); + if (testInfo.title.includes("insufficient account balance")) { + await mockNearBalances({ + page, + accountId: "theori.near", + balance: InsufficientBalance, + storage: 8, + }); + } else { + await mockNearBalances({ + page, + accountId: daoAccount, + balance: sufficientAvailableBalance, + storage: 2323, + }); + } await mockStakedPoolBalances({ page }); await page.goto(`/${instanceAccount}/widget/app?page=stake-delegation`); await expect( await page.locator("div").filter({ hasText: /^Stake Delegation$/ }) ).toBeVisible(); + await page.waitForTimeout(5_000); }); test.describe("User not logged in", function () { @@ -480,6 +475,28 @@ test.describe("Have valid staked requests and sufficient token balance", functio }); }); + test("insufficient account balance should show warning modal, disallow action ", async ({ + page, + }) => { + test.setTimeout(60_000); + + await expect( + page.getByText( + "Hey Ori, you don't have enough NEAR to complete actions on your treasury." + ) + ).toBeVisible(); + await page + .getByText("Create Request", { + exact: true, + }) + .click(); + await expect( + page + .getByText("Please add more funds to your account and try again") + .nth(1) + ).toBeVisible(); + }); + test("Should create stake delegation request, should throw error when invalid data is provided", async ({ page, }) => { @@ -842,9 +859,11 @@ test.describe("Lockup staking", function () { await updateDaoPolicyMembers({ page }); await mockNearBalances({ page, - daoAccount, + accountId: daoAccount, balance: sufficientAvailableBalance, + storage: 2323, }); + await mockLockupNearBalances({ page, balance: sufficientAvailableBalance, @@ -1317,9 +1336,11 @@ test.describe("Insufficient balance ", function () { await mockStakedPoolBalances({ page }); await mockNearBalances({ page, - daoAccount, + accountId: daoAccount, balance: inSufficientAvailableBalance, + storage: 2323, }); + await page.goto(`/${instanceAccount}/widget/app?page=stake-delegation`); await expect( await page.locator("div").filter({ hasText: /^Stake Delegation$/ }) diff --git a/playwright-tests/util/lib.js b/playwright-tests/util/lib.js index 35b81046..e6f7134a 100644 --- a/playwright-tests/util/lib.js +++ b/playwright-tests/util/lib.js @@ -20,3 +20,5 @@ export const encodeToMarkdown = (data) => { }) .join("
"); }; + +export const InsufficientBalance = BigInt(0.05 * 10 ** 24).toString(); diff --git a/playwright-tests/util/rpcmock.js b/playwright-tests/util/rpcmock.js index 72b74c13..1ea4781a 100644 --- a/playwright-tests/util/rpcmock.js +++ b/playwright-tests/util/rpcmock.js @@ -190,3 +190,24 @@ export async function updateDaoPolicyMembers({ page, isMultiVote = false }) { }, }); } + +export async function mockNearBalances({ page, accountId, balance, storage }) { + await page.route( + `https://api.fastnear.com/v1/account/${accountId}/full`, + async (route) => { + const json = { + account_id: accountId, + nfts: [], + pools: [], + state: { + balance: balance, + locked: "0", + storage: storage, + }, + tokens: [], + }; + + await route.fulfill({ json }); + } + ); +}