Compare commits

..
Author SHA1 Message Date
Mikołaj Młodzikowski c543a0d507 fix(ci): publish of static dist 2023-07-27 12:55:24 +02:00
Sam KeenandMatthew Russell 6a07127185 feat(governance): diff highlighter for update market proposals (#4260)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-07-27 11:26:32 +01:00
Sam Keen 45f0ba7c0e fix(explorer): explorer validator page filtering (#4354) 2023-07-27 09:04:54 +00:00
John WalleyandMatthew Russell 6f4ea68778 chore(trading): upgrade pennant to v1.10.0 (#4331)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-07-26 15:35:57 +01:00
Mikołaj Młodzikowski 90dda20891 feat(ci): migrate ci-cd trigger to python (#4330) 2023-07-26 16:08:57 +02:00
Art 278eb01c2f feat(governance): successor market proposal details (#4393) 2023-07-26 14:52:12 +02:00
Sam Keen c3157de146 fix(governance): removed icons test from proposal list item details (#4392) 2023-07-25 15:59:58 +00:00
d28db485c5 chore(trading): market relationship - parent market in proposals list (#4339)
Co-authored-by: asiaznik <artur@vegaprotocol.io>
Co-authored-by: Edd <edd@vega.xyz>
2023-07-25 15:32:40 +00:00
Maciek caa0076396 fix(deal-ticket): 4046 wrong margin requirement in order (#4378) 2023-07-25 14:52:32 +00:00
Edd 451da40ed2 feat(explorer): iceberg details on order tx (#4380) 2023-07-25 13:34:04 +00:00
Joe Tsang 47694e7b72 chore(explorer): explorer market e2e test (#4355) 2023-07-25 11:25:37 +01:00
Sam Keen e01fb7f9ae feat(governance): proposal list tile and summary enhancements (#4326) 2023-07-25 11:23:25 +01:00
Joe Tsang ffcdfb6a6a chore(governance): add e2e test for network upgrade banner (#4361) 2023-07-25 10:48:59 +01:00
Joe Tsang 86f0d013bb chore(governance): minor fixes to flaky nightly tests (#4335) 2023-07-25 10:48:49 +01:00
ArtandEdd c12ac45890 feat(environment): feature flags (#4343)
Co-authored-by: Edd <edd@vega.xyz>
2023-07-25 11:12:53 +02:00
47ce68455a chore(trading): fix flakey trading-trades test (#4363)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: Art <artur@vegaprotocol.io>
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
2023-07-24 16:41:44 +01:00
c1675e4b49 feat(trading): design changes (#4264)
Co-authored-by: Art <artur@vegaprotocol.io>
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
2023-07-24 09:37:18 +01:00
Matthew Russell f8c4d39b93 fix(positions): fix valueGetter for realisedPNL in positions table 2023-07-20 17:55:21 +01:00
Matthew Russell e55cf5ecc6 chore(trading): cherry pick position pnl formatting fix (#4347) 2023-07-20 12:52:33 +01:00
Matthew Russell 406cf22810 chore(trading): remove user pnl from closed market list (#4312) 2023-07-20 12:52:13 +01:00
Edd 8ff46bab5b fix(explorer): fix valueformatter in enactment date on proposal table (#4358) 2023-07-20 10:52:49 +02:00
Art 5f177785b6 feat(governance): protocol upgrade notification, est. time (#4289) 2023-07-20 10:27:43 +02:00
Mikołaj Młodzikowski 06779f36ca feat(ci): fix typo 2023-07-19 16:42:47 +02:00
Sam Keen 29991015b5 fix(governance): filtered out nodes with 0 performance score from homepage list (#4267) 2023-07-19 13:32:57 +01:00
Matthew Russell 071a6ff8bb chore(explorer): clear out slack webhook (#4320) 2023-07-19 11:36:34 +01:00
5b8df4c414 chore(trading): orderbook adjustments (#4293)
Co-authored-by: Ben <ben@vega.xyz>
Co-authored-by: Mikołaj Młodzikowski <mikolaj.mlodzikowski@gmail.com>
Co-authored-by: Joe Tsang <30622993+jtsang586@users.noreply.github.com>
Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>
Co-authored-by: m.ray <16125548+MadalinaRaicu@users.noreply.github.com>
Co-authored-by: Art <artur@vegaprotocol.io>
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
Co-authored-by: Gordsport <83510148+gordsport@users.noreply.github.com>
2023-07-18 14:48:51 +00:00
Daniel ce6873fe54 feat(environment): add mainnet mirror config (#4287) 2023-07-18 12:23:58 +01:00
Gordsport d0e64364e3 chore: update the PM action secret token (#4325) 2023-07-18 12:16:57 +01:00
Bartłomiej Głownia 737ffb4c35 feat(trading): copy order book volume value to deal ticket (#4333) 2023-07-18 12:16:07 +01:00
Maciek 53048ac8ef chore(trading): 4134 see if a closed market was succeeded (#4336) 2023-07-18 09:37:25 +00:00
Art 05ca1a09b9 chore(explorer): align staking terminology (#4290) 2023-07-18 09:03:48 +00:00
m.ray 94e398dd1c feat(trading): submit iceberg orders (#4230) 2023-07-17 14:24:51 +00:00
m.ray 7c0a4f61e9 fix(trading): add internal termination oracle (#4208) 2023-07-17 13:33:52 +00:00
m.ray 056d3215e8 feat(trading): view iceberg orders (#4207) 2023-07-17 13:04:03 +00:00
Dexter Edwards a68b2093e4 fix(ui-toolkit): toolkit bugs (#4329) 2023-07-17 10:59:22 +00:00
Mikołaj Młodzikowski f440f57be2 fix(ci): do not trigger releasing to s3 on pull requests 2023-07-14 13:45:06 +02:00
337 changed files with 8136 additions and 3974 deletions
+16 -120
View File
@@ -81,6 +81,22 @@ jobs:
with: with:
main-branch-name: develop main-branch-name: develop
# See affected apps
- name: See affected apps
run: |
branch_slug="$(echo '${{ github.head_ref || github.ref_name }}' | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | cut -c 1-50 )"
python3 tools/ci/check-affected.py --github-ref="${{ github.ref }}" --branch-slug="$branch_slug" --event-name="${{ github.event_name }}"
- name: Verify script result
run: |
echo "Check outputs from script"
echo "projects: ${{ env.PROJECTS }}"
echo "projects-e2e: ${{ env.PROJECTS_E2E }}"
echo "preview_governance: ${{ env.PREVIEW_GOVERNANCE }}"
echo "preview_trading: ${{ env.PREVIEW_TRADING }}"
echo "preview_explorer: ${{ env.PREVIEW_EXPLORER }}"
echo "preview_tools: ${{ env.PREVIEW_TOOLS }}"
- name: Check formatting - name: Check formatting
run: yarn nx format:check run: yarn nx format:check
@@ -96,126 +112,6 @@ jobs:
- name: Build affected - name: Build affected
run: yarn nx affected:build || (yarn install && yarn nx affected:build) run: yarn nx affected:build || (yarn install && yarn nx affected:build)
# See affected apps
- name: See affected apps
run: |
affected="$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)"
branch_slug="$(echo '${{ github.head_ref || github.ref_name }}' | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | cut -c 1-50 )"
echo ">>>> debug"
echo "NX_BASE: ${{ env.NX_BASE }}"
echo "NX_HEAD: ${{ env.NX_HEAD }}"
echo "Affected: ${affected}"
echo "Branch slug: ${branch_slug}"
echo "Current ref: ${{ github.ref }}"
echo ">>>> eof debug"
projects_array=()
preview_governance="not deployed"
preview_trading="not deployed"
preview_explorer="not deployed"
preview_tools="not deployed"
# parse if affected is any of three main applications, if none - use all of them
if echo "$affected" | grep -q governance; then
echo "Governance is affected"
projects_array+=("governance")
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
fi
if echo "$affected" | grep -q trading; then
echo "Trading is affected"
projects_array+=("trading")
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
fi
if echo "$affected" | grep -q explorer; then
echo "Explorer is affected"
projects_array+=("explorer")
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
fi
if [[ ${#projects_array[@]} -eq 0 ]]; then
projects_array=("governance" "trading" "explorer")
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
fi
# applications parsed before this loop are applicable for running e2e-tests
projects_e2e_array=()
for project in "${projects_array[@]}"; do
projects_e2e_array+=("${project}-e2e")
done
# all applications below this loop are not applicable for running e2e-test
# check if pull request event to deploy tools
if [[ "${{ github.event_name }}" = "pull_request" ]]; then
if echo "$affected" | grep -q multisig-signer; then
echo "Tools are affected"
echo "Deploying tools on preview"
preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug")
projects_array+=("multisig-signer")
fi
# those apps deploy only from develop to mainnet
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
if echo "$affected" | grep -q multisig-signer; then
echo "Tools are affected"
echo "Deploying tools on s3"
projects_array+=("multisig-signer")
fi
if echo "$affected" | grep -q static; then
echo "static is affected"
echo "Deploying static on s3"
projects_array+=("static")
fi
if echo "$affected" | grep -q ui-toolkit; then
echo "ui-toolkit is affected"
echo "Deploying ui-toolkit on s3"
projects_array+=("ui-toolkit")
fi
fi
# if branch starts with release/ and ends with trading / governance or explorer - overwrite the array of affected projects with fixed single application
if [[ "${{ github.ref }}" == *release* ]]; then
echo ">> This is a relase branch"
case "${{ github.ref }}" in
*trading)
echo ">> Only trading will be deployed"
projects_array=(trading)
projects_e2e_array=(trading)
;;
*governance)
echo ">> Only governance will be deployed"
projects_array=(governance)
projects_e2e_array=(governance)
;;
*explorer)
echo ">> Only explorer will be deployed"
projects_array=(explorer)
projects_e2e_array=(explorer)
;;
*)
echo ">> All apps will be deployed"
;;
esac
fi
echo "Projects: ${projects_array[@]}"
echo "Projects E2E: ${projects_e2e_array[@]}"
projects_json=$(jq -M --compact-output --null-input '$ARGS.positional' --args -- "${projects_array[@]}")
projects_e2e_json=$(jq -M --compact-output --null-input '$ARGS.positional' --args -- "${projects_e2e_array[@]}")
echo PROJECTS_E2E=$projects_e2e_json >> $GITHUB_ENV
echo PROJECTS=$projects_json >> $GITHUB_ENV
echo PREVIEW_GOVERNANCE=$preview_governance >> $GITHUB_ENV
echo PREVIEW_TRADING=$preview_trading >> $GITHUB_ENV
echo PREVIEW_EXPLORER=$preview_explorer >> $GITHUB_ENV
echo PREVIEW_TOOLS=$preview_tools >> $GITHUB_ENV
outputs: outputs:
projects: ${{ env.PROJECTS }} projects: ${{ env.PROJECTS }}
projects-e2e: ${{ env.PROJECTS_E2E }} projects-e2e: ${{ env.PROJECTS_E2E }}
+19 -48
View File
@@ -28,6 +28,7 @@ jobs:
echo IS_MAINNET_RELEASE=false >> $GITHUB_ENV echo IS_MAINNET_RELEASE=false >> $GITHUB_ENV
echo IS_TESTNET_RELEASE=false >> $GITHUB_ENV echo IS_TESTNET_RELEASE=false >> $GITHUB_ENV
echo IS_IPFS_RELEASE=false >> $GITHUB_ENV echo IS_IPFS_RELEASE=false >> $GITHUB_ENV
echo IS_S3_RELEASE=false >> $GITHUB_ENV
- name: Is PR - name: Is PR
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
@@ -49,6 +50,11 @@ jobs:
run: | run: |
echo IS_IPFS_RELEASE=true >> $GITHUB_ENV echo IS_IPFS_RELEASE=true >> $GITHUB_ENV
- name: Is S3 Release
if: ${{ env.IS_IPFS_RELEASE == 'false' && github.event_name == 'push' }}
run: |
echo IS_S3_RELEASE=true >> $GITHUB_ENV
- name: Set up QEMU - name: Set up QEMU
id: quemu id: quemu
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
@@ -91,52 +97,13 @@ jobs:
- name: Define dist variables - name: Define dist variables
if: ${{ github.event_name == 'push' }} if: ${{ github.event_name == 'push' }}
run: | run: |
envName='' python3 tools/ci/define-dist-variables.py --github-ref="${{ github.ref }}" --app="${{ matrix.app }}"
domain="vega.rocks"
bucketName=''
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then - name: Verify script result
# remove prefixing release/ and take the first string limited by - which is supposed to be name of the environment for releasing (format: release/testnet-trading) if: ${{ github.event_name == 'push' }}
envName="$(echo ${{ github.ref }} | sed -e "s|refs/heads/release/||" | cut -d '-' -f 1 )" run: |
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then echo "BUCKET_NAME=${{ env.BUCKET_NAME }}"
envName="stagnet1" echo "ENV_NAME=${{ env.ENV_NAME }}"
if [[ "${{ matrix.app }}" = "multisig-signer" ]]; then
envName="mainnet"
bucketName="tools.vega.xyz"
fi
if [[ "${{ matrix.app }}" = "static" ]]; then
envName="mainnet"
bucketName="static.vega.xyz"
fi
if [[ "${{ matrix.app }}" = "ui-toolkit" ]]; then
envName="mainnet"
bucketName="ui.vega.rocks"
fi
elif [[ "${{ github.ref }}" =~ .*mainnet$ ]]; then
envName="mainnet"
fi
if [[ "${envName}" = "mainnet" ]]; then
domain="vega.xyz"
if [[ -z "${bucketName}" ]]; then
bucketName="${{ matrix.app }}.${domain}"
fi
elif [[ "${envName}" = "testnet" ]]; then
domain="fairground.wtf"
if [[ -z "${bucketName}" ]]; then
bucketName="${{ matrix.app }}.${domain}"
fi
fi
if [[ -z "${bucketName}" ]]; then
bucketName="${{ matrix.app }}.${envName}.${domain}"
fi
echo "bucket name: ${bucketName}"
echo "env name: ${envName}"
echo BUCKET_NAME=${bucketName} >> $GITHUB_ENV
echo ENV_NAME=${envName} >> $GITHUB_ENV
- name: Build local dist - name: Build local dist
run: | run: |
@@ -151,8 +118,12 @@ jobs:
elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then
NODE_ENV=production yarn nx run ui-toolkit:build-storybook NODE_ENV=production yarn nx run ui-toolkit:build-storybook
DIST_LOCATION=dist/storybook/ui-toolkit DIST_LOCATION=dist/storybook/ui-toolkit
elif [ "${{ matrix.app }}" = "static" ]; then
yarn nx build static || (yarn install && yarn nx build static)
else else
$envCmd yarn nx build ${{ matrix.app }} || (yarn install && $envCmd yarn nx build ${{ matrix.app }}) $envCmd yarn nx build ${{ matrix.app }} || (yarn install && $envCmd yarn nx build ${{ matrix.app }})
fi
if [[ -z "$DIST_LOCATION" ]]; then
DIST_LOCATION=dist/apps/${{ matrix.app }} DIST_LOCATION=dist/apps/${{ matrix.app }}
fi fi
mv $DIST_LOCATION dist-result mv $DIST_LOCATION dist-result
@@ -245,7 +216,7 @@ jobs:
- name: Publish dist to s3 - name: Publish dist to s3
uses: jakejarvis/s3-sync-action@master uses: jakejarvis/s3-sync-action@master
# s3 releases are not happening for trading on mainnet - it's IPFS # s3 releases are not happening for trading on mainnet - it's IPFS
if: ${{ env.IS_IPFS_RELEASE == 'false' }} if: ${{ env.IS_S3_RELEASE == 'true' }}
with: with:
args: --acl private --follow-symlinks --delete args: --acl private --follow-symlinks --delete
env: env:
@@ -256,11 +227,11 @@ jobs:
SOURCE_DIR: 'dist-result' SOURCE_DIR: 'dist-result'
- name: Install aws CLI - name: Install aws CLI
if: ${{ env.IS_IPFS_RELEASE == 'false' }} if: ${{ env.IS_S3_RELEASE == 'true' }}
uses: unfor19/install-aws-cli-action@master uses: unfor19/install-aws-cli-action@master
- name: Perform cache invalidation - name: Perform cache invalidation
if: ${{ env.IS_IPFS_RELEASE == 'false' }} if: ${{ env.IS_S3_RELEASE == 'true' }}
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+6 -1
View File
@@ -16,5 +16,10 @@ NX_EXPLORER_NETWORK_PARAMETERS=1
NX_EXPLORER_PARTIES=1 NX_EXPLORER_PARTIES=1
NX_EXPLORER_VALIDATORS=1 NX_EXPLORER_VALIDATORS=1
CYPRESS_VEGA_WALLET_API_TOKEN=
CYPRESS_VEGA_URL=http://localhost:3008/graphql CYPRESS_VEGA_URL=http://localhost:3008/graphql
CYPRESS_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit supply green clown session
CYPRESS_ETHEREUM_PROVIDER_URL=http://localhost:8545
CYPRESS_FAUCET_URL=http://localhost:1790/api/v1/mint
CYPRESS_VEGA_PUBLIC_KEY=02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65
CYPRESS_VEGA_WALLET_URL=http://localhost:1789
CYPRESS_VEGA_WALLET_API_TOKEN=
+121 -10
View File
@@ -1,19 +1,130 @@
//Tests set to skip until market bug for capsule checkpoint is fixed context('Market page', { tags: '@regression' }, function () {
context.skip('Market page', { tags: '@regression' }, function () {
describe('Verify elements on page', function () { describe('Verify elements on page', function () {
const marketHeaders = '[data-testid="markets-header"]'; const marketHeaders = 'markets-heading';
const marketNavigation = 'a[href="/markets"]'; const createdMarketId =
'2eab0e66545a789047561bc5a2e5cbc3b19eb708da41104e3cac2474ee36c4d4';
it('Markets page is displayed', function () { before('Create market', function () {
cy.visit('/'); cy.visit('/');
cy.get(marketNavigation).click(); cy.createMarket();
cy.common_validate_blocks_data_displayed(marketHeaders);
}); });
it('Markets page displayed on mobile', function () { beforeEach('Get market id', function () {
cy.navigate_to('markets');
cy.get('[col-id="id"]').eq(1).invoke('text').as('createdMarketId');
});
it('Market displayed on market page', function () {
cy.navigate_to('markets');
cy.getByTestId(marketHeaders).should('be.visible');
cy.get(`[row-id="${this.createdMarketId}"]`)
.should('be.visible')
.within(() => {
cy.get_element_by_col_id('code').should('have.text', 'TEST.24h');
cy.get_element_by_col_id('name').should('have.text', 'Test market 1');
cy.get_element_by_col_id('state').should('have.text', 'Pending');
cy.get_element_by_col_id('asset').should('have.text', 'fUSDC');
cy.get_element_by_col_id('id').should(
'have.text',
this.createdMarketId
);
cy.get_element_by_col_id('actions')
.find('a')
.should('have.attr', 'href', `/markets/${this.createdMarketId}`);
});
});
it('Able to go to market details page', function () {
cy.navigate_to('markets');
cy.get_element_by_col_id('actions').eq(1).click();
cy.getByTestId(marketHeaders).should('have.text', 'Test market 1');
cy.validate_element_from_table('Name', 'Test market 1');
cy.validate_element_from_table('Market ID', this.createdMarketId);
cy.validate_element_from_table('Trading Mode', 'Opening auction');
cy.validate_element_from_table('Market Decimal Places', '5');
cy.validate_element_from_table('Position Decimal Places', '5');
cy.validate_element_from_table('Settlement Asset Decimal Places', '5');
// Instrument
cy.validate_element_from_table('Market Name', 'Test market 1');
cy.validate_element_from_table('Code', 'TEST.24h');
cy.validate_element_from_table('Product Type', 'Future');
cy.validate_element_from_table('Quote Name', 'fUSDC');
// Settlement Asset
cy.validate_element_from_table('Quote Name', 'fUSDC');
cy.validate_element_from_table('Symbol', 'fUSDC');
cy.validate_element_from_table('Decimals', '5');
cy.validate_element_from_table('Quantum', '0.00');
cy.validate_element_from_table('Status', 'Enabled');
cy.validate_element_from_table('Max faucet amount', '10,000,000.00');
cy.validate_element_from_table(
'Infrastructure fee account balance',
'0.00'
);
cy.validate_element_from_table(
'Global reward pool account balance',
'0.00'
);
// Metadata
cy.validate_element_from_table('Sector', 'tech');
cy.validate_element_from_table('Source', 'docs.vega.xyz');
// Risk model
cy.validate_element_from_table('Tau', '0.0001140771161');
cy.validate_element_from_table('Risk Aversion Parameter', '0.01');
// Risk parameters
// cy.validate_element_from_table('R', '0.016')
cy.validate_element_from_table('Sigma', '0.5');
// Risk factors
cy.validate_element_from_table('Short', '0.0143218738374871');
cy.validate_element_from_table('Long', '0.0141450471498822');
// Price monitoring settings 1
cy.validate_element_from_table('Horizon Secs', '43,200');
cy.validate_element_from_table('Probability', '1');
cy.validate_element_from_table('Auction Extension Secs', '600');
// Liquidity Monitoring
cy.validate_element_from_table('Triggering Ratio', '0.7');
cy.validate_element_from_table('Time Window', '3,600');
cy.validate_element_from_table('Scaling Factor', '10');
// Liquidity
cy.validate_element_from_table('Target Stake', '0.00 fUSDC');
cy.validate_element_from_table('Supplied Stake', '0.00 fUSDC');
cy.validate_element_from_table('Market Value Proxy', '0.00 fUSDC');
// Liquidity price range
cy.validate_element_from_table(
'Liquidity Price Range',
'1,000.00% of mid price'
);
cy.validate_element_from_table('Lowest Price', '0.00 fUSDC');
cy.validate_element_from_table('Highest Price', '0.00 fUSDC');
cy.getByTestId('oracle-spec-links')
.should('have.attr', 'href')
.and(
'contain',
'/oracles/bf242aa5c9f64fcbb77808aa8582e73711519f4b35264eb797a80f1803590a24'
);
// Able to view Json
cy.contains('View JSON').click();
cy.get('.language-json').should('exist');
});
// Skipping due to resize observer loop limit error
it.skip('Markets page displayed on mobile', function () {
cy.common_switch_to_mobile_and_click_toggle(); cy.common_switch_to_mobile_and_click_toggle();
cy.get(marketNavigation).click(); cy.navigate_to('markets', true);
cy.common_validate_blocks_data_displayed(marketHeaders); cy.getByTestId(marketHeaders).should('be.visible');
cy.get(`[row-id="${createdMarketId}"]`)
.should('be.visible')
.within(() => {
cy.get_element_by_col_id('code').should('have.text', 'TEST.24h');
cy.get_element_by_col_id('name').should('have.text', 'Test market 1');
cy.get_element_by_col_id('state').should('have.text', 'Pending');
cy.get_element_by_col_id('asset').should('have.text', 'fUSDC');
cy.get_element_by_col_id('id').should('have.text', createdMarketId);
cy.get_element_by_col_id('actions')
.find('a')
.should('have.attr', 'href', `/markets/${createdMarketId}`);
});
}); });
}); });
}); });
@@ -40,7 +40,7 @@ context.skip('Transactions page', function () {
.first() .first()
.click({ force: true }); .click({ force: true });
} else { } else {
cy.slack('Unable to find any transactions on page'); cy.log('Unable to find any transactions on page');
cy.screenshot(); cy.screenshot();
} }
}); });
@@ -78,7 +78,7 @@ context.skip('Transactions page', function () {
} }
}); });
} else { } else {
cy.slack('Unable to find any transactions on page'); cy.log('Unable to find any transactions on page');
cy.screenshot(); cy.screenshot();
} }
}); });
@@ -7,7 +7,11 @@ context('Validator page', { tags: '@smoke' }, function () {
it('should be able to see validator tiles', function () { it('should be able to see validator tiles', function () {
cy.getNodes().then((nodes) => { cy.getNodes().then((nodes) => {
nodes.forEach((node) => { nodes.forEach((node) => {
cy.get(`[validator-id="${node.id}"]`).should('be.visible'); if (node.rankingScore.performanceScore > 0) {
cy.get(`[validator-id="${node.id}"]`).should('be.visible');
} else {
cy.get(`[validator-id="${node.id}"]`).should('not.exist');
}
}); });
}); });
}); });
@@ -28,7 +28,7 @@ Cypress.Commands.add('switchToMobile', () => {
Cypress.Commands.add('common_switch_to_mobile_and_click_toggle', function () { Cypress.Commands.add('common_switch_to_mobile_and_click_toggle', function () {
cy.viewport('iphone-x'); cy.viewport('iphone-x');
cy.visit('/'); cy.visit('/');
cy.get('[data-testid="open-menu"]').click(); cy.get('[data-testid="button-menu-drawer"]').click();
}); });
Cypress.Commands.add('monitor_clipboard', () => { Cypress.Commands.add('monitor_clipboard', () => {
@@ -88,3 +88,42 @@ Cypress.Commands.add('convert_number_to_max_four_decimal', (number) => {
maximumFractionDigits: 4, maximumFractionDigits: 4,
}); });
}); });
Cypress.Commands.add('navigate_to', (page, mobileView = false) => {
const navigation = {
transactions: '/txs',
blocks: '/blocks',
oracles: '/oracles',
validators: '/validators',
parties: '/parties',
assets: '/assets',
markets: '/markets',
governanceProposals: '/governance',
networkParameters: '/network-parameters',
genesisParameters: '/genesis',
};
const topLevelRoutes = ['transactions', 'blocks', 'oracles', 'validators'];
cy.getByTestId('navigation').within(() => {
if (!topLevelRoutes.includes(page) && mobileView === false) {
cy.getByTestId('other').first().click();
}
cy.get(`[href="${navigation[page]}"]`).first().click();
});
cy.url().should('include', navigation[page]);
});
Cypress.Commands.add('get_element_by_col_id', (elementName) => {
cy.get(`[col-id="${elementName}"]`);
});
Cypress.Commands.add(
'validate_element_from_table',
(tableRowName, tableRowValue) => {
cy.contains(tableRowName)
.parentsUntil("[data-testid='key-value-table-row']")
.siblings()
.should('have.text', tableRowValue);
}
);
+1 -1
View File
@@ -4,7 +4,7 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996 NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
NX_VEGA_URL=https://api.mainnet-mirror.vega.rocks/graphql NX_VEGA_URL=https://api.mainnet-mirror.vega.rocks/graphql
NX_VEGA_ENV=MAINNET-MIRROR NX_VEGA_ENV=MAINNET_MIRROR
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.rocks/rest NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.rocks/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://governance.mainnet-mirror.vega.rocks NX_VEGA_GOVERNANCE_URL=https://governance.mainnet-mirror.vega.rocks
@@ -95,7 +95,10 @@ export const Header = () => {
{mainItems.map(routeToNavigationItem)} {mainItems.map(routeToNavigationItem)}
{groupedItems && groupedItems.length > 0 && ( {groupedItems && groupedItems.length > 0 && (
<NavigationItem> <NavigationItem>
<NavigationTrigger isActive={Boolean(isOnOther)}> <NavigationTrigger
isActive={Boolean(isOnOther)}
data-testid="other"
>
{t('Other')} {t('Other')}
</NavigationTrigger> </NavigationTrigger>
<NavigationContent> <NavigationContent>
@@ -137,7 +137,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
hide: window.innerWidth <= BREAKPOINT_MD, hide: window.innerWidth <= BREAKPOINT_MD,
headerName: t('Enactment date'), headerName: t('Enactment date'),
field: 'terms.enactmentDatetime', field: 'terms.enactmentDatetime',
valueFormatte: ({ valueFormatter: ({
value, value,
}: VegaValueFormatterParams< }: VegaValueFormatterParams<
ProposalListFieldsFragment, ProposalListFieldsFragment,
@@ -0,0 +1,52 @@
import { t } from '@vegaprotocol/i18n';
import type { components } from '../../../../../types/explorer';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
export interface TxDetailsOrderIcebergDetailsProps {
iceberg: components['schemas']['v1IcebergOpts'];
size: components['schemas']['v1OrderSubmission']['size'];
}
/**
.iceberg .sub {
vertical-align: bottom;
color: #954F01;
}
.iceberg .sup {
vertical-align: top;
color: #515E1E;
}
*/
/**
* Visualises the iceberg details of an order, showing the minimum visible size,
* the full size and the peak size.
*/
export const TxOrderIcebergDetails = ({
iceberg,
size,
}: TxDetailsOrderIcebergDetailsProps) => {
return (
<div
className="inline-block iceberg bg-vega-blue-300 border-vega-blue-350 border-[1px] text-[70%] font-mono py-[2px] pl-2 pr-3 height-[1em] leading-[1em] cursor-pointer"
style={{ clipPath: 'polygon(10% 0%, 100% 0%, 90% 100%, 0% 100%)' }}
>
<Tooltip description={t('Iceberg: Minimum visible size')}>
<span className="align-bottom text-vega-orange-650">
{iceberg.minimumVisibleSize || '-'}
</span>
</Tooltip>
<Tooltip description={t('Iceberg: Total size')}>
<span className="text-sm text-vega-blue-600 mx-3">{size}</span>
</Tooltip>
<Tooltip description={t('Iceberg: Visible peak')}>
<span className="align-top text-vega-yellow-600">
{iceberg.peakSize || '-'}
</span>
</Tooltip>
</div>
);
};
@@ -8,6 +8,7 @@ import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import DeterministicOrderDetails from '../../order-details/deterministic-order-details'; import DeterministicOrderDetails from '../../order-details/deterministic-order-details';
import Hash from '../../links/hash'; import Hash from '../../links/hash';
import { TxOrderPeggedReferenceRow } from './order/tx-order-peg'; import { TxOrderPeggedReferenceRow } from './order/tx-order-peg';
import { TxOrderIcebergDetails } from './order/tx-order-iceberg';
interface TxDetailsOrderProps { interface TxDetailsOrderProps {
txData: BlockExplorerTransactionResult | undefined; txData: BlockExplorerTransactionResult | undefined;
@@ -32,6 +33,8 @@ export const TxDetailsOrder = ({
const marketId = txData.command.orderSubmission.marketId || '-'; const marketId = txData.command.orderSubmission.marketId || '-';
const reference = txData.command.orderSubmission.peggedOrder; const reference = txData.command.orderSubmission.peggedOrder;
const side = txData.command.orderSubmission.side; const side = txData.command.orderSubmission.side;
const iceberg = txData.command.orderSubmission.icebergOpts;
const size = txData.command.orderSubmission.size;
let deterministicId = ''; let deterministicId = '';
@@ -74,6 +77,14 @@ export const TxDetailsOrder = ({
marketId={marketId} marketId={marketId}
/> />
) : null} ) : null}
{iceberg ? (
<TableRow modifier="bordered">
<TableCell>{t('Iceberg details')}</TableCell>
<TableCell>
<TxOrderIcebergDetails iceberg={iceberg} size={size} />
</TableCell>
</TableRow>
) : null}
</TableWithTbody> </TableWithTbody>
{deterministicId.length > 0 ? ( {deterministicId.length > 0 ? (
@@ -166,6 +166,7 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-250'; 'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-250';
// This will get unwieldy and should probably produce a different colour of tag // This will get unwieldy and should probably produce a different colour of tag
// Note that colours are currently arbitrary
if (type === 'Chain Event' && !!command?.chainEvent) { if (type === 'Chain Event' && !!command?.chainEvent) {
type = getLabelForChainEvent(command.chainEvent); type = getLabelForChainEvent(command.chainEvent);
colours = 'text-white dark-text-white bg-vega-pink dark:bg-vega-pink'; colours = 'text-white dark-text-white bg-vega-pink dark:bg-vega-pink';
@@ -177,6 +178,9 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
type = getLabelForProposal(command.proposalSubmission); type = getLabelForProposal(command.proposalSubmission);
} }
colours = 'text-black bg-vega-yellow'; colours = 'text-black bg-vega-yellow';
} else if (type === 'Order' && command) {
type = getLabelForOrderType(orderType, command);
colours = 'text-white dark-text-white bg-vega-blue dark:bg-vega-blue';
} }
if (type === 'Vote on Proposal') { if (type === 'Vote on Proposal') {
-15
View File
@@ -5,8 +5,6 @@ const windowOrDefault = (key: string) => {
return (process.env[key] as string) || ''; return (process.env[key] as string) || '';
}; };
const truthy = ['1', 'true'];
export const ENV = { export const ENV = {
// Data sources // Data sources
// Environment // Environment
@@ -20,19 +18,6 @@ export const ENV = {
governanceUrl: windowOrDefault('NX_VEGA_GOVERNANCE_URL'), governanceUrl: windowOrDefault('NX_VEGA_GOVERNANCE_URL'),
vegaRepoUrl: windowOrDefault('NX_VEGA_REPO_URL'), vegaRepoUrl: windowOrDefault('NX_VEGA_REPO_URL'),
}, },
flags: {
assets: truthy.includes(windowOrDefault('NX_EXPLORER_ASSETS')),
genesis: truthy.includes(windowOrDefault('NX_EXPLORER_GENESIS')),
governance: truthy.includes(windowOrDefault('NX_EXPLORER_GOVERNANCE')),
markets: truthy.includes(windowOrDefault('NX_EXPLORER_MARKETS')),
oracles: truthy.includes(windowOrDefault('NX_EXPLORER_ORACLES')),
txsList: truthy.includes(windowOrDefault('NX_EXPLORER_TXS_LIST')),
networkParameters: truthy.includes(
windowOrDefault('NX_EXPLORER_NETWORK_PARAMETERS')
),
parties: truthy.includes(windowOrDefault('NX_EXPLORER_PARTIES')),
validators: truthy.includes(windowOrDefault('NX_EXPLORER_VALIDATORS')),
},
addresses: { addresses: {
feedback: windowOrDefault('NX_GITHUB_FEEDBACK_URL'), feedback: windowOrDefault('NX_GITHUB_FEEDBACK_URL'),
}, },
-5
View File
@@ -1,5 +0,0 @@
import { ENV } from './env';
export default {
...ENV.flags,
};
@@ -1,7 +1,5 @@
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { DATA_SOURCES } from '../../../config';
import { getDateTimeFormat } from '@vegaprotocol/utils'; import { getDateTimeFormat } from '@vegaprotocol/utils';
import type { TendermintBlocksResponse } from '../tendermint-blocks-response';
import { RouteTitle } from '../../../components/route-title'; import { RouteTitle } from '../../../components/route-title';
import { TimeAgo } from '../../../components/time-ago'; import { TimeAgo } from '../../../components/time-ago';
import { import {
@@ -14,7 +12,7 @@ import { TxsPerBlock } from '../../../components/txs/txs-per-block';
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
import { Routes } from '../../route-names'; import { Routes } from '../../route-names';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useFetch } from '@vegaprotocol/react-helpers'; import { useBlockInfo } from '@vegaprotocol/tendermint';
import { NodeLink } from '../../../components/links'; import { NodeLink } from '../../../components/links';
import { useDocumentTitle } from '../../../hooks/use-document-title'; import { useDocumentTitle } from '../../../hooks/use-document-title';
import EmptyList from '../../../components/empty-list/empty-list'; import EmptyList from '../../../components/empty-list/empty-list';
@@ -24,10 +22,7 @@ const Block = () => {
useDocumentTitle(['Blocks', `Block #${block}`]); useDocumentTitle(['Blocks', `Block #${block}`]);
const { const {
state: { data: blockData, loading, error }, state: { data: blockData, loading, error },
} = useFetch<TendermintBlocksResponse>( } = useBlockInfo(Number(block));
`${DATA_SOURCES.tendermintUrl}/block?height=${block}`,
{ cache: 'force-cache' }
);
return ( return (
<section> <section>
@@ -54,7 +54,7 @@ export const PartyBlockStake = ({
{p?.stakingSummary.currentStakeAvailable ? ( {p?.stakingSummary.currentStakeAvailable ? (
<KeyValueTable> <KeyValueTable>
<KeyValueTableRow noBorder={true}> <KeyValueTableRow noBorder={true}>
<div>{t('Available stake')}</div> <div>{t('Associated to key')}</div>
<div> <div>
<GovernanceAssetBalance <GovernanceAssetBalance
price={p.stakingSummary.currentStakeAvailable} price={p.stakingSummary.currentStakeAvailable}
@@ -62,7 +62,7 @@ export const PartyBlockStake = ({
</div> </div>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow noBorder={true}> <KeyValueTableRow noBorder={true}>
<div>{t('Active stake')}</div> <div>{t('Staked to validator')}</div>
<div> <div>
<GovernanceAssetBalance price={linkedStake || '0'} /> <GovernanceAssetBalance price={linkedStake || '0'} />
</div> </div>
@@ -67,7 +67,7 @@ const Party = () => {
text: t('Go back'), text: t('Go back'),
action: () => navigate(-1), action: () => navigate(-1),
className: 'py-1', className: 'py-1',
size: 'sm', size: 'small',
}} }}
/> />
</div> </div>
@@ -14,7 +14,6 @@ import { Block } from './blocks/id';
import { Blocks } from './blocks/home'; import { Blocks } from './blocks/home';
import { Tx } from './txs/id'; import { Tx } from './txs/id';
import { TxsList } from './txs/home'; import { TxsList } from './txs/home';
import flags from '../config/flags';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { Routes } from './route-names'; import { Routes } from './route-names';
import { NetworkParameters } from './network-parameters'; import { NetworkParameters } from './network-parameters';
@@ -30,6 +29,7 @@ import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
import { remove0x } from '@vegaprotocol/utils'; import { remove0x } from '@vegaprotocol/utils';
import { PartyAccountsByAsset } from './parties/id/accounts'; import { PartyAccountsByAsset } from './parties/id/accounts';
import { Disclaimer } from './pages/disclaimer'; import { Disclaimer } from './pages/disclaimer';
import { FLAGS } from '@vegaprotocol/environment';
export type Navigable = { export type Navigable = {
path: string; path: string;
@@ -59,7 +59,7 @@ type Route = RouteItem & {
children?: RouteItem[]; children?: RouteItem[];
}; };
const partiesRoutes: Route[] = flags.parties const partiesRoutes: Route[] = FLAGS.EXPLORER_PARTIES
? [ ? [
{ {
path: Routes.PARTIES, path: Routes.PARTIES,
@@ -119,7 +119,7 @@ const partiesRoutes: Route[] = flags.parties
] ]
: []; : [];
const assetsRoutes: Route[] = flags.assets const assetsRoutes: Route[] = FLAGS.EXPLORER_ASSETS
? [ ? [
{ {
path: Routes.ASSETS, path: Routes.ASSETS,
@@ -147,7 +147,7 @@ const assetsRoutes: Route[] = flags.assets
] ]
: []; : [];
const genesisRoutes: Route[] = flags.genesis const genesisRoutes: Route[] = FLAGS.EXPLORER_GENESIS
? [ ? [
{ {
path: Routes.GENESIS, path: Routes.GENESIS,
@@ -163,7 +163,7 @@ const genesisRoutes: Route[] = flags.genesis
] ]
: []; : [];
const governanceRoutes: Route[] = flags.governance const governanceRoutes: Route[] = FLAGS.EXPLORER_GOVERNANCE
? [ ? [
{ {
path: Routes.GOVERNANCE, path: Routes.GOVERNANCE,
@@ -179,7 +179,7 @@ const governanceRoutes: Route[] = flags.governance
] ]
: []; : [];
const marketsRoutes: Route[] = flags.markets const marketsRoutes: Route[] = FLAGS.EXPLORER_MARKETS
? [ ? [
{ {
path: Routes.MARKETS, path: Routes.MARKETS,
@@ -207,7 +207,7 @@ const marketsRoutes: Route[] = flags.markets
] ]
: []; : [];
const networkParametersRoutes: Route[] = flags.networkParameters const networkParametersRoutes: Route[] = FLAGS.EXPLORER_NETWORK_PARAMETERS
? [ ? [
{ {
path: Routes.NETWORK_PARAMETERS, path: Routes.NETWORK_PARAMETERS,
@@ -225,7 +225,7 @@ const networkParametersRoutes: Route[] = flags.networkParameters
] ]
: []; : [];
const validators: Route[] = flags.validators const validators: Route[] = FLAGS.EXPLORER_VALIDATORS
? [ ? [
{ {
path: Routes.VALIDATORS, path: Routes.VALIDATORS,
@@ -15,6 +15,9 @@ query ExplorerNodes {
stakedByDelegates stakedByDelegates
stakedTotal stakedTotal
pendingStake pendingStake
rankingScore {
performanceScore
}
epochData { epochData {
total total
offline offline
@@ -6,7 +6,7 @@ const defaultOptions = {} as const;
export type ExplorerNodesQueryVariables = Types.Exact<{ [key: string]: never; }>; export type ExplorerNodesQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerNodesQuery = { __typename?: 'Query', nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, infoUrl: string, avatarUrl?: string | null, pubkey: string, tmPubkey: string, ethereumAddress: string, location: string, status: Types.NodeStatus, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null } } | null> | null } }; export type ExplorerNodesQuery = { __typename?: 'Query', nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, infoUrl: string, avatarUrl?: string | null, pubkey: string, tmPubkey: string, ethereumAddress: string, location: string, status: Types.NodeStatus, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, rankingScore: { __typename?: 'RankingScore', performanceScore: string }, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null } } | null> | null } };
export const ExplorerNodesDocument = gql` export const ExplorerNodesDocument = gql`
@@ -27,6 +27,9 @@ export const ExplorerNodesDocument = gql`
stakedByDelegates stakedByDelegates
stakedTotal stakedTotal
pendingStake pendingStake
rankingScore {
performanceScore
}
epochData { epochData {
total total
offline offline
@@ -91,7 +91,15 @@ export const ValidatorsPage = () => {
const { data: tmData } = useTendermintValidators(5000); const { data: tmData } = useTendermintValidators(5000);
const { data, loading, error, refetch } = useExplorerNodesQuery(); const { data, loading, error, refetch } = useExplorerNodesQuery();
const validators = compact(data?.nodesConnection.edges?.map((e) => e?.node)); const validators = compact(
data?.nodesConnection.edges
?.map((e) => e?.node)
.filter(
(node) =>
node?.rankingScore?.performanceScore &&
new BigNumber(node.rankingScore.performanceScore).isGreaterThan(0)
)
);
// voting power // voting power
const powers = compact(tmData?.result.validators).map( const powers = compact(tmData?.result.validators).map(
@@ -144,7 +144,7 @@ context(
closeStakingDialog(); closeStakingDialog();
navigateTo(navigation.validators); navigateTo(navigation.validators);
cy.getByTestId(viewStakedByMeToggle).click(); cy.getByTestId(viewStakedByMeToggle).click();
cy.getByTestId(userStakeBtn).should('have.length', 2); cy.getByTestId(userStakeBtn, epochTimeout).should('have.length', 2);
}); });
it('Able to stake against a validator - using vega from vesting contract', function () { it('Able to stake against a validator - using vega from vesting contract', function () {
@@ -11,7 +11,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
cy.getByTestId('home-proposals').within(() => { cy.getByTestId('home-proposals').within(() => {
cy.get('[href="/proposals"]') cy.get('[href="/proposals"]')
.should('exist') .should('exist')
.and('have.text', 'Browse, vote, and propose'); .and('have.text', 'See all proposals');
}); });
}); });
@@ -161,7 +161,7 @@ context(
); );
cy.getByTestId('protocol-upgrade-proposal-status').should( cy.getByTestId('protocol-upgrade-proposal-status').should(
'have.text', 'have.text',
'Approved by validators ' 'Approved by validators'
); );
}); });
}); });
@@ -227,5 +227,42 @@ context(
cy.getByTestId(networkUpgradeProposalListItem).should('not.exist'); cy.getByTestId(networkUpgradeProposalListItem).should('not.exist');
cy.getByTestId(closedProposalToggle).should('not.exist'); cy.getByTestId(closedProposalToggle).should('not.exist');
}); });
it('should display network upgrade banner with estimate', function () {
mockNetworkUpgradeProposal();
cy.visit('/');
cy.getByTestId('banners').within(() => {
cy.get('div')
.should('contain.text', 'The network will upgrade to v1 in ')
.and(
'contain.text',
'Trading activity will be interrupted, manage your risk appropriately.'
);
cy.getByTestId('external-link')
.should('have.attr', 'href')
.and('contain', '/proposals/protocol-upgrade/v1');
});
// estimate does not display possibly due to mocks or Cypress unless the proposal is clicked on several times
// By default the application waits for 10 blocks until showing estimate - roughly 10 seconds
for (let i = 0; i < 3; i++) {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(3000);
navigateTo(navigation.proposals);
cy.getByTestId(networkUpgradeProposalListItem)
.first()
.find('[data-testid="view-proposal-btn"]')
.click();
}
cy.getByTestId('upgrade-proposal-estimate')
.invoke('text')
.as('displayedEstimate');
cy.get('@displayedEstimate').then((estimateText) => {
// Estimated time should automatically update every second
cy.getByTestId('protocol-upgrade-time')
.invoke('text')
.should('not.eq', estimateText);
});
});
} }
); );
@@ -174,7 +174,8 @@ export function clickOnValidatorFromList(
cy.get(`[row-id="${validatorNumber}"]`) cy.get(`[row-id="${validatorNumber}"]`)
.should('be.visible') .should('be.visible')
.find(stakeValidatorListName) .find(stakeValidatorListName)
.click(); .as('validatorOnList');
cy.get('@validatorOnList').click();
} }
} }
+6
View File
@@ -18,6 +18,12 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72 NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/ NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
#Test configuration variables #Test configuration variables
CYPRESS_FAIRGROUND=false CYPRESS_FAIRGROUND=false
LC_ALL="en_US.UTF-8" LC_ALL="en_US.UTF-8"
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+5
View File
@@ -19,6 +19,11 @@ NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_VEGA_REST_URL=http://localhost:3008/api/v2/ NX_VEGA_REST_URL=http://localhost:3008/api/v2/
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
#Test configuration variables #Test configuration variables
CYPRESS_FAIRGROUND=false CYPRESS_FAIRGROUND=false
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
+6
View File
@@ -13,3 +13,9 @@ NX_TRANCHES_SERVICE_URL=https://tranches-devnet1-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_REST_URL=https://api.n00.devnet1.vega.xyz/api/v2/ NX_VEGA_REST_URL=https://api.n00.devnet1.vega.xyz/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996 NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+6
View File
@@ -14,3 +14,9 @@ NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.vega.community/api/v2/ NX_VEGA_REST_URL=https://api.vega.community/api/v2/
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
+8 -2
View File
@@ -1,8 +1,8 @@
# App configuration variables # App configuration variables
NX_VEGA_ENV=MAINNET-MIRROR NX_VEGA_ENV=MAINNET_MIRROR
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
NX_VEGA_URL=https://api.mainnet-mirror.vega.rocks/graphql NX_VEGA_URL=https://api.mainnet-mirror.vega.rocks/graphql
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.governance.vega.xyz","TESTNET":"https://governance.fairground.wtf","MAINNET":"https://governance.vega.xyz","MAINNET-MIRROR":"https://governance.mainnet-mirror.vega.rocks","STAGNET1":"https://trading.stagnet1.vega.rocks"}' NX_VEGA_NETWORKS='{"DEVNET":"https://dev.governance.vega.xyz","TESTNET":"https://governance.fairground.wtf","MAINNET":"https://governance.vega.xyz","MAINNET_MIRROR":"https://governance.mainnet-mirror.vega.rocks","STAGNET1":"https://trading.stagnet1.vega.rocks"}'
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
@@ -13,3 +13,9 @@ NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-mirror-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-mirror-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.mainnet-mirror.vega.rocks/api/v2/ NX_VEGA_REST_URL=https://api.mainnet-mirror.vega.rocks/api/v2/
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
+6
View File
@@ -9,3 +9,9 @@ NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/ NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+6
View File
@@ -14,3 +14,9 @@ NX_TRANCHES_SERVICE_URL=https://tranches-testnet-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_REST_URL=https://api.n07.testnet.vega.xyz/api/v2/ NX_VEGA_REST_URL=https://api.n07.testnet.vega.xyz/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996 NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+6
View File
@@ -11,3 +11,9 @@ NX_VEGA_EXPLORER_URL=https://explorer.validators-testnet.vega.rocks/
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_REST_URL=https://api-validators-testnet.vega.rocks/api/v2/ NX_VEGA_REST_URL=https://api-validators-testnet.vega.rocks/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996 NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
+4 -5
View File
@@ -2,14 +2,13 @@ import * as Sentry from '@sentry/react';
import { toBigNum } from '@vegaprotocol/utils'; import { toBigNum } from '@vegaprotocol/utils';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useEagerConnect } from '@vegaprotocol/wallet'; import { useVegaWallet, useEagerConnect } from '@vegaprotocol/wallet';
import { useEnvironment } from '@vegaprotocol/environment'; import { FLAGS, useEnvironment } from '@vegaprotocol/environment';
import { useWeb3React } from '@web3-react/core'; import { useWeb3React } from '@web3-react/core';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SplashError } from './components/splash-error'; import { SplashError } from './components/splash-error';
import { SplashLoader } from './components/splash-loader'; import { SplashLoader } from './components/splash-loader';
import { Flags } from './config';
import { import {
AppStateActionType, AppStateActionType,
useAppState, useAppState,
@@ -80,7 +79,7 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
} }
}; };
if (!Flags.NETWORK_DOWN) { if (!FLAGS.GOVERNANCE_NETWORK_DOWN) {
run(); run();
} }
}, [token, appDispatch, staking, vesting]); }, [token, appDispatch, staking, vesting]);
@@ -148,7 +147,7 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
}; };
// Only begin polling if network limits flag is set, as this is a new API not yet on mainnet 7/3/22 // Only begin polling if network limits flag is set, as this is a new API not yet on mainnet 7/3/22
if (Flags.NETWORK_LIMITS) { if (FLAGS.GOVERNANCE_NETWORK_LIMITS) {
getNetworkLimits(); getNetworkLimits();
} }
@@ -157,7 +156,7 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
}; };
}, [appDispatch, VEGA_URL, t]); }, [appDispatch, VEGA_URL, t]);
if (Flags.NETWORK_DOWN) { if (FLAGS.GOVERNANCE_NETWORK_DOWN) {
return ( return (
<Splash> <Splash>
<SplashError /> <SplashError />
@@ -0,0 +1 @@
export * from './json-diff';
@@ -0,0 +1,30 @@
import { render, screen, cleanup } from '@testing-library/react';
import { JsonDiff } from './json-diff';
describe('JsonDiff', () => {
afterEach(cleanup);
it('renders without crashing', () => {
render(<JsonDiff left={{}} right={{}} />);
expect(screen.getByTestId('json-diff')).toBeInTheDocument();
});
it('shows the correct message when both objects are identical', () => {
render(<JsonDiff left={{}} right={{}} />);
expect(screen.getByText('Data is identical')).toBeInTheDocument();
});
it('does not show the "identical" message when both objects are not identical', () => {
render(
<JsonDiff
left={{
name: 'test',
}}
right={{
name: 'test2',
}}
/>
);
expect(screen.queryByText('Data is identical')).not.toBeInTheDocument();
});
});
@@ -0,0 +1,46 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { formatters, create } from 'jsondiffpatch';
import 'jsondiffpatch/dist/formatters-styles/html.css';
import 'jsondiffpatch/dist/formatters-styles/annotated.css';
export type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| { [key: string]: JsonValue };
interface JsonDiffProps {
left: JsonValue;
right: JsonValue;
objectHash?: (obj: unknown) => string | undefined;
}
export const JsonDiff = ({ right, left, objectHash }: JsonDiffProps) => {
const { t } = useTranslation();
const [html, setHtml] = useState<string | undefined>();
useEffect(() => {
const delta = create({
objectHash,
}).diff(left, right);
if (delta) {
const deltaHtml = formatters.html.format(delta, left);
formatters.html.hideUnchanged();
setHtml(deltaHtml);
} else {
setHtml(undefined);
}
}, [right, left, objectHash]);
return html ? (
<div data-testid="json-diff" dangerouslySetInnerHTML={{ __html: html }} />
) : (
<p data-testid="json-diff">{t('dataIsIdentical')}</p>
);
};
+12 -1
View File
@@ -18,6 +18,10 @@ import { VegaWallet } from '../vega-wallet';
import { useLocation, useMatch } from 'react-router-dom'; import { useLocation, useMatch } from 'react-router-dom';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useTelemetryDialog } from '../telemetry-dialog/telemetry-dialog'; import { useTelemetryDialog } from '../telemetry-dialog/telemetry-dialog';
import {
ProtocolUpgradeCountdown,
ProtocolUpgradeCountdownMode,
} from '@vegaprotocol/proposals';
export const SettingsLink = () => { export const SettingsLink = () => {
const { open, isOpen, close } = useTelemetryDialog(); const { open, isOpen, close } = useTelemetryDialog();
@@ -61,7 +65,14 @@ export const Nav = ({ theme }: Pick<NavigationProps, 'theme'>) => {
appName="Governance" appName="Governance"
theme={theme} theme={theme}
breakpoints={[458, 959]} breakpoints={[458, 959]}
actions={<SettingsLink />} actions={
<>
<SettingsLink />
<ProtocolUpgradeCountdown
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
</>
}
> >
<NavigationList <NavigationList
className="[.drawer-content_&]:border-b [.drawer-content_&]:border-b-vega-light-200 dark:[.drawer-content_&]:border-b-vega-dark-200 [.drawer-content_&]:pb-8 [.drawer-content_&]:mb-2" className="[.drawer-content_&]:border-b [.drawer-content_&]:border-b-vega-light-200 dark:[.drawer-content_&]:border-b-vega-dark-200 [.drawer-content_&]:pb-8 [.drawer-content_&]:mb-2"
@@ -4,7 +4,11 @@ import type { ReactNode } from 'react';
import { AnnouncementBanner } from '@vegaprotocol/announcements'; import { AnnouncementBanner } from '@vegaprotocol/announcements';
import { Nav } from '../nav'; import { Nav } from '../nav';
import { Networks, useEnvironment } from '@vegaprotocol/environment'; import { Networks, useEnvironment } from '@vegaprotocol/environment';
import React from 'react'; import {
ProtocolUpgradeCountdownMode,
ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals';
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
interface AppLayoutProps { interface AppLayoutProps {
children: ReactNode; children: ReactNode;
@@ -31,8 +35,23 @@ export const AppLayout = ({ children }: AppLayoutProps) => {
/> />
)} )}
<Nav theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} /> <Nav theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
<NotificationsContainer />
</div> </div>
<div className={AppLayoutClasses}>{children}</div> <div className={AppLayoutClasses}>{children}</div>
</div> </div>
); );
}; };
const NotificationsContainer = () => {
const { isReadOnly, pubKey, disconnect } = useVegaWallet();
return (
<div data-testid="banners">
<ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
{isReadOnly ? (
<ViewingAsBanner pubKey={pubKey} disconnect={disconnect} />
) : null}
</div>
);
};
@@ -1,5 +1,3 @@
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import React from 'react'; import React from 'react';
export interface TemplateSidebarProps { export interface TemplateSidebarProps {
@@ -8,24 +6,18 @@ export interface TemplateSidebarProps {
} }
export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) { export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) {
const { isReadOnly, pubKey, disconnect } = useVegaWallet();
return ( return (
<> <div className="w-full border-b border-neutral-700 lg:grid lg:grid-rows-[1fr] lg:grid-cols-[1fr_450px]">
{isReadOnly ? ( <main className="max-w-[100vw] col-start-1 p-4 overflow-auto">
<ViewingAsBanner pubKey={pubKey} disconnect={disconnect} /> {children}
) : null} </main>
<div className="w-full border-b border-neutral-700 lg:grid lg:grid-rows-[1fr] lg:grid-cols-[1fr_450px]"> <aside className="col-start-2 row-start-1 row-span-2 hidden lg:block p-4 bg-banner bg-contain border-l border-neutral-700">
<main className="max-w-[100vw] col-start-1 p-4 overflow-auto"> {sidebar.map((Component, i) => (
{children} <section className="mb-4 last:mb-0" key={i}>
</main> {Component}
<aside className="col-start-2 row-start-1 row-span-2 hidden lg:block p-4 bg-banner bg-contain border-l border-neutral-700"> </section>
{sidebar.map((Component, i) => ( ))}
<section className="mb-4 last:mb-0" key={i}> </aside>
{Component} </div>
</section>
))}
</aside>
</div>
</>
); );
} }
+4 -6
View File
@@ -25,6 +25,10 @@ export const ContractAddresses: {
claimAddress: customClaimAddress ?? '0x0', claimAddress: customClaimAddress ?? '0x0',
lockedAddress: customLockedAddress ?? '0x0', lockedAddress: customLockedAddress ?? '0x0',
}, },
MAINNET_MIRROR: {
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
lockedAddress: '0x0', // TODO not deployed to this env
},
DEVNET: { DEVNET: {
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
lockedAddress: '0x0', // TODO not deployed to this env lockedAddress: '0x0', // TODO not deployed to this env
@@ -63,12 +67,6 @@ export const ENV = {
localProviderUrl: windowOrDefault('NX_LOCAL_PROVIDER_URL'), localProviderUrl: windowOrDefault('NX_LOCAL_PROVIDER_URL'),
delegationsPagination: windowOrDefault('NX_DELEGATIONS_PAGINATION'), delegationsPagination: windowOrDefault('NX_DELEGATIONS_PAGINATION'),
rest: windowOrDefault('NX_VEGA_REST_URL'), rest: windowOrDefault('NX_VEGA_REST_URL'),
flags: {
NETWORK_DOWN: TRUTHY.includes(windowOrDefault('NX_NETWORK_DOWN')),
MOCK: TRUTHY.includes(windowOrDefault('NX_MOCKED')),
FAIRGROUND: TRUTHY.includes(windowOrDefault('NX_FAIRGROUND')),
NETWORK_LIMITS: TRUTHY.includes(windowOrDefault('NX_NETWORK_LIMITS')),
},
addresses: addresses:
ContractAddresses[(envName === 'local' ? 'CUSTOM' : envName) as Networks], ContractAddresses[(envName === 'local' ? 'CUSTOM' : envName) as Networks],
}; };
-5
View File
@@ -1,5 +0,0 @@
import { ENV } from './env';
export const Flags = {
...ENV.flags,
};
-1
View File
@@ -1,2 +1 @@
export * from './flags';
export * from './env'; export * from './env';
+21 -3
View File
@@ -166,6 +166,7 @@
"proposedEnactment": "Proposed enactment", "proposedEnactment": "Proposed enactment",
"Enacted": "Enacted", "Enacted": "Enacted",
"enactedOn": "Enacted on", "enactedOn": "Enacted on",
"enactedOn{{date}}": "Enacted on {{enactmentDate}}",
"status": "Status", "status": "Status",
"state": "State", "state": "State",
"shouldPass": "Should pass", "shouldPass": "Should pass",
@@ -185,6 +186,7 @@
"proposedOn": "Proposed on", "proposedOn": "Proposed on",
"proposedBy": "Proposed by", "proposedBy": "Proposed by",
"toEnactOn": "Enacts on", "toEnactOn": "Enacts on",
"enactsOn{{date}}": "Enacts on {{enactmentDate}}",
"closesOn": "Closes on", "closesOn": "Closes on",
"closedOn": "Closed on", "closedOn": "Closed on",
"errorDetails": "Error details", "errorDetails": "Error details",
@@ -217,6 +219,7 @@
"votingThresholdInfo": "If the token vote passes the participation threshold it will be the deciding vote. If not, the outcome will be determined by liquidity providers on this market.", "votingThresholdInfo": "If the token vote passes the participation threshold it will be the deciding vote. If not, the outcome will be determined by liquidity providers on this market.",
"noGovernanceTokens": "You need some VEGA tokens to participate in governance", "noGovernanceTokens": "You need some VEGA tokens to participate in governance",
"youVoted": "You voted", "youVoted": "You voted",
"voted": "Voted",
"changeVote": "Change vote", "changeVote": "Change vote",
"txRequested": "Confirm transaction in wallet", "txRequested": "Confirm transaction in wallet",
"votePending": "Casting vote", "votePending": "Casting vote",
@@ -628,7 +631,6 @@
"pendingDescriptionLinkText": "set up and run a node on Vega", "pendingDescriptionLinkText": "set up and run a node on Vega",
"pendingDescription2": ". A node can move from being a candidate into standby based on how much nomination it attracts, assuming it has proven reliability by sending heartbeats to the network.", "pendingDescription2": ". A node can move from being a candidate into standby based on how much nomination it attracts, assuming it has proven reliability by sending heartbeats to the network.",
"n/a": "N/A", "n/a": "N/A",
"Set to": "Set to",
"pass": "pass", "pass": "pass",
"fail": "fail", "fail": "fail",
"New asset": "New asset", "New asset": "New asset",
@@ -788,7 +790,7 @@
"overstakedPenalty": "Overstaked penalty", "overstakedPenalty": "Overstaked penalty",
"multisigPenalty": "Multisig penalty", "multisigPenalty": "Multisig penalty",
"homeProposalsIntro": "Decisions on the Vega network are on-chain, with tokenholders creating proposals that other tokenholders vote to approve or reject. Network upgrades are proposed and approved by validators.", "homeProposalsIntro": "Decisions on the Vega network are on-chain, with tokenholders creating proposals that other tokenholders vote to approve or reject. Network upgrades are proposed and approved by validators.",
"homeProposalsButtonText": "Browse, vote, and propose", "homeProposalsButtonText": "See all proposals",
"homeValidatorsIntro": "Vega runs on a delegated proof of stake blockchain, where validators earn fees for validating block transactions. Tokenholders can nominate validators by staking tokens to them.", "homeValidatorsIntro": "Vega runs on a delegated proof of stake blockchain, where validators earn fees for validating block transactions. Tokenholders can nominate validators by staking tokens to them.",
"homeValidatorsButtonText": "Browse, and stake", "homeValidatorsButtonText": "Browse, and stake",
"homeRewardsIntro": "Track rewards you've earned for trading, liquidity provision, market creation, and staking.", "homeRewardsIntro": "Track rewards you've earned for trading, liquidity provision, market creation, and staking.",
@@ -837,5 +839,21 @@
"RejectedProposals": "Rejected proposals", "RejectedProposals": "Rejected proposals",
"networkGovernance": "Network governance", "networkGovernance": "Network governance",
"networkUpgrades": "Network upgrades", "networkUpgrades": "Network upgrades",
"assetSpecification": "Asset specification" "assetSpecification": "Asset specification",
"viewDetails": "View details",
"vegaGovernance": "Vega Governance",
"latestProposals": "Latest proposals",
"vegaToken": "VEGA Token",
"majorityVotedForProposal": "majority voted for this proposal",
"majorityNotVotedForProposal": "majority not voted for this proposal",
"requiredMajorityVotedForProposal": "Required majority voted for this proposal",
"requiredMajorityNotVotedForProposal": "Required majority not voted for this proposal",
"minParticipationReached": "Min. participation reached",
"minParticipationNotReached": "Min. participation not reached",
"consensusNodes": "consensus nodes",
"activeNodes": "active nodes",
"Estimated time to upgrade": "Estimated time to upgrade",
"Upgraded at": "Upgraded at",
"dataIsIdentical": "Data is identical",
"updatesToMarket": "Updates to market"
} }
+59 -28
View File
@@ -20,7 +20,7 @@ import {
getNotRejectedProposals, getNotRejectedProposals,
getNotRejectedProtocolUpgradeProposals, getNotRejectedProtocolUpgradeProposals,
} from '../proposals/proposals/proposals-container'; } from '../proposals/proposals/proposals-container';
import { Heading } from '../../components/heading'; import { Heading, SubHeading } from '../../components/heading';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import type { RouteChildProps } from '..'; import type { RouteChildProps } from '..';
import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals';
@@ -31,6 +31,11 @@ import {
orderByDate, orderByDate,
orderByUpgradeBlockHeight, orderByUpgradeBlockHeight,
} from '../proposals/components/proposals-list/proposals-list'; } from '../proposals/components/proposals-list/proposals-list';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { BigNumber } from '../../lib/bignumber';
const nodesToShow = 6; const nodesToShow = 6;
@@ -42,33 +47,57 @@ const HomeProposals = ({
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[]; protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const {
params: networkParams,
loading: networkParamsLoading,
error: networkParamsError,
} = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
NetworkParams.governance_proposal_asset_requiredMajority,
NetworkParams.governance_proposal_updateAsset_requiredMajority,
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
NetworkParams.governance_proposal_freeform_requiredMajority,
]);
return ( return (
<section className="mb-16" data-testid="home-proposals"> <AsyncRenderer
<Heading title={t('Proposals')} /> loading={networkParamsLoading}
<h3 className="mb-6">{t('homeProposalsIntro')}</h3> error={networkParamsError}
<div className="flex items-center mb-8 gap-8"> data={networkParams}
<Link to={`${Routes.PROPOSALS}`}> >
<Button size="md">{t('homeProposalsButtonText')}</Button> <section className="mb-16" data-testid="home-proposals">
</Link> <Heading title={t('vegaGovernance')} />
<h3 className="mb-6">{t('homeProposalsIntro')}</h3>
<div className="mb-8">
<ExternalLink href={ExternalLinks.GOVERNANCE_PAGE}>
{t(`readMoreGovernance`)}
</ExternalLink>
</div>
<ExternalLink href={ExternalLinks.GOVERNANCE_PAGE}> <SubHeading title={t('latestProposals')} />
{t(`readMoreGovernance`)} <ul data-testid="home-proposal-list" className="grid gap-6">
</ExternalLink> {protocolUpgradeProposals.map((proposal, index) => (
</div> <ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
<ul ))}
data-testid="home-proposal-list"
className="grid md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-6"
>
{protocolUpgradeProposals.map((proposal, index) => (
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
))}
{proposals.map((proposal) => ( {proposals.map((proposal) => (
<ProposalsListItem key={proposal.id} proposal={proposal} /> <ProposalsListItem
))} key={proposal.id}
</ul> proposal={proposal}
</section> networkParams={networkParams}
/>
))}
</ul>
<div className="mt-6">
<Link to={`${Routes.PROPOSALS}`}>
<Button size="md">{t('homeProposalsButtonText')}</Button>
</Link>
</div>
</section>
</AsyncRenderer>
); );
}; };
@@ -86,8 +115,8 @@ const HomeNodes = ({
const { t } = useTranslation(); const { t } = useTranslation();
const highlightedNodeData = [ const highlightedNodeData = [
{ title: t('active nodes'), length: activeNodes.length }, { title: t('activeNodes'), length: activeNodes.length },
{ title: t('consensus nodes'), length: consensusNodes.length }, { title: t('consensusNodes'), length: consensusNodes.length },
]; ];
return ( return (
@@ -233,7 +262,7 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
[protocolUpgradeProposals] [protocolUpgradeProposals]
); );
const totalProposalsDesired = 4; const totalProposalsDesired = 3;
const protocolUpgradeProposalsToShow = sortedProtocolUpgradeProposals.slice( const protocolUpgradeProposalsToShow = sortedProtocolUpgradeProposals.slice(
0, 0,
totalProposalsDesired totalProposalsDesired
@@ -248,6 +277,8 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
const activeNodes = removePaginationWrapper( const activeNodes = removePaginationWrapper(
validatorsData?.nodesConnection.edges validatorsData?.nodesConnection.edges
).filter((node) =>
new BigNumber(node.rankingScore.performanceScore).isGreaterThan(0)
); );
const trimmedActiveNodes = activeNodes?.slice(0, nodesToShow); const trimmedActiveNodes = activeNodes?.slice(0, nodesToShow);
@@ -287,7 +318,7 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
</div> </div>
<div data-testid="home-vega-token"> <div data-testid="home-vega-token">
<Heading title={t('VEGA Token')} marginTop={false} /> <Heading title={t('vegaToken')} marginTop={false} />
<h3 className="mb-6">{t('homeVegaTokenIntro')}</h3> <h3 className="mb-6">{t('homeVegaTokenIntro')}</h3>
<div className="flex items-center mb-8 gap-4"> <div className="flex items-center mb-8 gap-4">
<Link to={Routes.WITHDRAWALS}> <Link to={Routes.WITHDRAWALS}>
@@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Icon } from '@vegaprotocol/ui-toolkit';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
@@ -18,53 +17,32 @@ export const CurrentProposalState = ({
switch (proposal?.state) { switch (proposal?.state) {
case ProposalState.STATE_ENACTED: { case ProposalState.STATE_ENACTED: {
proposalStatus = ( proposalStatus = t('voteState_Enacted');
<>
<span className="mr-2">{t('voteState_Enacted')}</span>
<Icon name={'tick'} />
</>
);
break; break;
} }
case ProposalState.STATE_PASSED: { case ProposalState.STATE_PASSED: {
proposalStatus = ( proposalStatus = t('voteState_Passed');
<>
<span className="mr-2">{t('voteState_Passed')}</span>
<Icon name={'tick'} />
</>
);
break; break;
} }
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: { case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
proposalStatus = ( proposalStatus = t('voteState_WaitingForNodeVote');
<>
<span className="mr-2">{t('voteState_WaitingForNodeVote')}</span>
<Icon name={'time'} />
</>
);
break; break;
} }
case ProposalState.STATE_OPEN: { case ProposalState.STATE_OPEN: {
variant = 'primary' as ProposalInfoLabelVariant; variant = 'primary' as ProposalInfoLabelVariant;
proposalStatus = <>{t('voteState_Open')}</>; proposalStatus = t('voteState_Open');
break; break;
} }
case ProposalState.STATE_DECLINED: { case ProposalState.STATE_DECLINED: {
proposalStatus = ( proposalStatus = t('voteState_Declined');
<>
<span className="mr-2">{t('voteState_Declined')}</span>
<Icon name={'cross'} />
</>
);
break; break;
} }
case ProposalState.STATE_REJECTED: { case ProposalState.STATE_REJECTED: {
proposalStatus = ( proposalStatus = t('voteState_Rejected');
<> break;
<span className="mr-2">{t('voteState_Rejected')}</span> }
<Icon name={'warning-sign'} /> case ProposalState.STATE_FAILED: {
</> proposalStatus = t('voteState_Failed');
);
break; break;
} }
} }
@@ -1,21 +1,66 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import {
ProposalRejectionReason,
ProposalState,
VoteValue,
} from '@vegaprotocol/types';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { import {
generateNoVotes, generateNoVotes,
generateProposal, generateProposal,
generateYesVotes, generateYesVotes,
} from '../../test-helpers/generate-proposals'; } from '../../test-helpers/generate-proposals';
import { ProposalHeader } from './proposal-header'; import { ProposalHeader } from './proposal-header';
import {
lastWeek,
nextWeek,
mockNetworkParams,
mockWalletContext,
createUserVoteQueryMock,
} from '../../test-helpers/mocks';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types'; import type { MockedResponse } from '@apollo/client/testing';
import { lastWeek, nextWeek } from '../../test-helpers/mocks'; import { FLAGS } from '@vegaprotocol/environment';
import { BrowserRouter } from 'react-router-dom';
jest.mock('@vegaprotocol/proposals', () => ({
...jest.requireActual('@vegaprotocol/proposals'),
useSuccessorMarketProposalDetails: () => ({
code: 'PARENT_CODE',
parentMarketId: 'PARENT_ID',
}),
}));
const renderComponent = ( const renderComponent = (
proposal: ProposalQuery['proposal'], proposal: ProposalQuery['proposal'],
isListItem = true isListItem = true,
) => render(<ProposalHeader proposal={proposal} isListItem={isListItem} />); mocks: MockedResponse[] = []
) =>
render(
<AppStateProvider>
<BrowserRouter>
<MockedProvider mocks={mocks}>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalHeader
proposal={proposal}
isListItem={isListItem}
networkParams={mockNetworkParams}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</BrowserRouter>
</AppStateProvider>
);
describe('Proposal header', () => { describe('Proposal header', () => {
afterAll(() => {
jest.clearAllMocks();
});
it('Renders New market proposal', () => { it('Renders New market proposal', () => {
const mockedFlags = jest.mocked(FLAGS);
mockedFlags.SUCCESSOR_MARKETS = true;
renderComponent( renderComponent(
generateProposal({ generateProposal({
rationale: { rationale: {
@@ -48,6 +93,9 @@ describe('Proposal header', () => {
expect(screen.getByTestId('proposal-details')).toHaveTextContent( expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'tGBP settled future.' 'tGBP settled future.'
); );
expect(screen.getByTestId('proposal-successor-info')).toHaveTextContent(
'PARENT_CODE'
);
}); });
it('Renders Update market proposal', () => { it('Renders Update market proposal', () => {
@@ -317,22 +365,6 @@ describe('Proposal header', () => {
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
}); });
it('Renders proposal state: Declined - majority not reached', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_DECLINED,
terms: {
enactmentDatetime: lastWeek.toString(),
},
votes: {
no: generateNoVotes(1, 1000000000000000000),
yes: generateYesVotes(1, 1000000000000000000),
},
})
);
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined');
});
it('Renders proposal state: Rejected', () => { it('Renders proposal state: Rejected', () => {
renderComponent( renderComponent(
generateProposal({ generateProposal({
@@ -346,4 +378,32 @@ describe('Proposal header', () => {
); );
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Rejected'); expect(screen.getByTestId('proposal-status')).toHaveTextContent('Rejected');
}); });
it('Renders proposal state: Open - user voted against', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
closingDatetime: nextWeek.toString(),
},
});
renderComponent(proposal, true, [
// @ts-ignore generateProposal always creates an id
createUserVoteQueryMock(proposal.id, VoteValue.VALUE_NO),
]);
expect(await screen.findByTestId('user-voted-no')).toBeInTheDocument();
});
it('Renders proposal state: Open - user voted for', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
closingDatetime: nextWeek.toString(),
},
});
renderComponent(proposal, true, [
// @ts-ignore generateProposal always creates an id
createUserVoteQueryMock(proposal.id, VoteValue.VALUE_YES),
]);
expect(await screen.findByTestId('user-voted-yes')).toBeInTheDocument();
});
}); });
@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Lozenge } from '@vegaprotocol/ui-toolkit'; import { Lozenge, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { shorten } from '@vegaprotocol/utils'; import { shorten } from '@vegaprotocol/utils';
import { Heading, SubHeading } from '../../../../components/heading'; import { Heading, SubHeading } from '../../../../components/heading';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
@@ -8,15 +8,25 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { truncateMiddle } from '../../../../lib/truncate-middle'; import { truncateMiddle } from '../../../../lib/truncate-middle';
import { CurrentProposalState } from '../current-proposal-state'; import { CurrentProposalState } from '../current-proposal-state';
import { ProposalInfoLabel } from '../proposal-info-label'; import { ProposalInfoLabel } from '../proposal-info-label';
import { useUserVote } from '../vote-details/use-user-vote';
import { ProposalVotingStatus } from '../proposal-voting-status';
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
import { FLAGS } from '@vegaprotocol/environment';
import Routes from '../../../routes';
import { Link } from 'react-router-dom';
export const ProposalHeader = ({ export const ProposalHeader = ({
proposal, proposal,
networkParams,
isListItem = true, isListItem = true,
}: { }: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal']; proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
networkParams: Partial<NetworkParamsResult>;
isListItem?: boolean; isListItem?: boolean;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { voteState } = useUserVote(proposal?.id);
const change = proposal?.terms.change; const change = proposal?.terms.change;
let details: ReactNode; let details: ReactNode;
@@ -33,6 +43,9 @@ export const ProposalHeader = ({
fallbackTitle = t('NewMarketProposal'); fallbackTitle = t('NewMarketProposal');
details = ( details = (
<> <>
{FLAGS.SUCCESSOR_MARKETS && (
<SuccessorCode proposalId={proposal?.id} />
)}
<span> <span>
{t('Code')}: {change.instrument.code}. {t('Code')}: {change.instrument.code}.
</span>{' '} </span>{' '}
@@ -119,6 +132,35 @@ export const ProposalHeader = ({
return ( return (
<> <>
<div className="flex items-center justify-between gap-4 mb-6 text-sm">
<div data-testid="proposal-type">
<ProposalInfoLabel variant="secondary">
{t(`${proposalType}`)}
</ProposalInfoLabel>
</div>
<div className="flex items-center gap-6">
{(voteState === 'Yes' || voteState === 'No') && (
<div
className="flex items-center gap-2"
data-testid={`user-voted-${voteState.toLowerCase()}`}
>
<div className="text-vega-green" data-testid="you-voted-icon">
<VegaIcon name={VegaIconNames.VOTE} size={24} />
</div>
<div>
{t('voted')}{' '}
<span className="uppercase">{t(`voteState_${voteState}`)}</span>
</div>
</div>
)}
<div data-testid="proposal-status">
<CurrentProposalState proposal={proposal} />
</div>
</div>
</div>
<div data-testid="proposal-title"> <div data-testid="proposal-title">
{isListItem ? ( {isListItem ? (
<header> <header>
@@ -133,23 +175,33 @@ export const ProposalHeader = ({
)} )}
</div> </div>
<div className="flex items-center gap-2 mb-4">
<div data-testid="proposal-type">
<ProposalInfoLabel variant="secondary">
{t(`${proposalType}`)}
</ProposalInfoLabel>
</div>
<div data-testid="proposal-status">
<CurrentProposalState proposal={proposal} />
</div>
</div>
{details && ( {details && (
<div data-testid="proposal-details" className="break-words my-10"> <div
data-testid="proposal-details"
className="break-words mb-6 text-vega-light-200"
>
{details} {details}
</div> </div>
)} )}
<ProposalVotingStatus proposal={proposal} networkParams={networkParams} />
</> </>
); );
}; };
const SuccessorCode = ({ proposalId }: { proposalId?: string | null }) => {
const { t } = useTranslation();
const successor = useSuccessorMarketProposalDetails(proposalId);
return successor.parentMarketId || successor.code ? (
<span className="block" data-testid="proposal-successor-info">
{t('Successor market to')}:{' '}
<Link
to={`${Routes.PROPOSALS}/${successor.parentMarketId}`}
className="hover:underline"
>
{successor.code || successor.parentMarketId}
</Link>
</span>
) : null;
};
@@ -7,10 +7,10 @@ export type ProposalInfoLabelVariant =
| 'tertiary' | 'tertiary'
| 'highlight'; | 'highlight';
const base = 'rounded-md px-2 py-1 font-alpha'; const base = 'rounded-full px-3 py-1 font-alpha';
const primary = 'bg-vega-light-150 text-black'; const primary = 'bg-vega-green text-black';
const secondary = 'bg-vega-dark-200 text-white'; const secondary = 'bg-vega-dark-200 text-vega-light-200';
const tertiary = 'bg-vega-dark-150 text-white'; const tertiary = 'bg-vega-dark-150 text-vega-light-200';
const highlight = 'bg-vega-yellow text-black'; const highlight = 'bg-vega-yellow text-black';
const getClassname = (variant: ProposalInfoLabelVariant) => { const getClassname = (variant: ProposalInfoLabelVariant) => {
@@ -0,0 +1 @@
export * from './proposal-market-changes';
@@ -0,0 +1,91 @@
import { render, fireEvent } from '@testing-library/react';
import {
ProposalMarketChanges,
applyImmutableKeysFromEarlierVersion,
} from './proposal-market-changes';
import type { JsonValue } from '../../../../components/json-diff';
describe('applyImmutableKeysFromEarlierVersion', () => {
it('returns an empty object if any argument is not an object or null', () => {
const earlierVersion: JsonValue = null;
const updatedVersion: JsonValue = null;
expect(
applyImmutableKeysFromEarlierVersion(earlierVersion, updatedVersion)
).toEqual({});
});
it('overrides updatedVersion with values from earlierVersion for immutable keys', () => {
const earlierVersion: JsonValue = {
decimalPlaces: 2,
positionDecimalPlaces: 3,
instrument: {
name: 'Instrument1',
future: {
settlementAsset: 'Asset1',
},
},
};
const updatedVersion: JsonValue = {
decimalPlaces: 3, // should be overridden by 2
positionDecimalPlaces: 4, // should be overridden by 3
instrument: {
name: 'Instrument2', // should be overridden by 'Instrument1'
future: {
settlementAsset: 'Asset2', // should be overridden by 'Asset1'
},
},
};
const expected: JsonValue = {
decimalPlaces: 2,
positionDecimalPlaces: 3,
instrument: {
name: 'Instrument1',
future: {
settlementAsset: 'Asset1',
},
},
};
expect(
applyImmutableKeysFromEarlierVersion(earlierVersion, updatedVersion)
).toEqual(expected);
});
});
describe('ProposalMarketChanges', () => {
it('renders correctly', () => {
const { getByTestId } = render(
<ProposalMarketChanges
originalProposal={{}}
latestEnactedProposal={{}}
updatedProposal={{}}
/>
);
expect(getByTestId('proposal-market-changes')).toBeInTheDocument();
});
it('JsonDiff is not visible when showChanges is false', () => {
const { queryByTestId } = render(
<ProposalMarketChanges
originalProposal={{}}
latestEnactedProposal={{}}
updatedProposal={{}}
/>
);
expect(queryByTestId('json-diff')).not.toBeInTheDocument();
});
it('JsonDiff is visible when showChanges is true', async () => {
const { getByTestId } = render(
<ProposalMarketChanges
originalProposal={{}}
latestEnactedProposal={{}}
updatedProposal={{}}
/>
);
fireEvent.click(getByTestId('proposal-market-changes-toggle'));
expect(getByTestId('json-diff')).toBeInTheDocument();
});
});
@@ -0,0 +1,86 @@
import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set';
import get from 'lodash/get';
import { JsonDiff } from '../../../../components/json-diff';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading';
import type { JsonValue } from '../../../../components/json-diff';
const immutableKeys = [
'decimalPlaces',
'positionDecimalPlaces',
'instrument.name',
'instrument.future.settlementAsset',
];
export const applyImmutableKeysFromEarlierVersion = (
earlierVersion: JsonValue,
updatedVersion: JsonValue
) => {
if (
typeof earlierVersion !== 'object' ||
earlierVersion === null ||
typeof updatedVersion !== 'object' ||
updatedVersion === null
) {
// If either version is not an object or is null, return null or throw an error
return {};
}
const updatedVersionCopy = cloneDeep(updatedVersion);
// Overwrite the immutable keys in the updatedVersionCopy with the earlier values
immutableKeys.forEach((key) => {
set(updatedVersionCopy, key, get(earlierVersion, key));
});
return updatedVersionCopy;
};
interface ProposalMarketChangesProps {
originalProposal: JsonValue;
latestEnactedProposal: JsonValue | undefined;
updatedProposal: JsonValue;
}
export const ProposalMarketChanges = ({
originalProposal,
latestEnactedProposal,
updatedProposal,
}: ProposalMarketChangesProps) => {
const { t } = useTranslation();
const [showChanges, setShowChanges] = useState(false);
return (
<section data-testid="proposal-market-changes">
<CollapsibleToggle
toggleState={showChanges}
setToggleState={setShowChanges}
dataTestId={'proposal-market-changes-toggle'}
>
<SubHeading title={t('updatesToMarket')} />
</CollapsibleToggle>
{showChanges && (
<div className="mb-6">
<JsonDiff
left={latestEnactedProposal || originalProposal}
right={
latestEnactedProposal
? applyImmutableKeysFromEarlierVersion(
latestEnactedProposal,
updatedProposal
)
: applyImmutableKeysFromEarlierVersion(
originalProposal,
updatedProposal
)
}
/>
</div>
)}
</section>
);
};
@@ -0,0 +1 @@
export * from './proposal-voting-status';
@@ -0,0 +1,153 @@
import { render, screen } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import {
lastWeek,
mockWalletContext,
networkParamsQueryMock,
nextWeek,
mockNetworkParams,
} from '../../test-helpers/mocks';
import { ProposalVotingStatus } from './proposal-voting-status';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import {
generateNoVotes,
generateProposal,
generateYesVotes,
} from '../../test-helpers/generate-proposals';
import { ProposalState } from '@vegaprotocol/types';
import { BigNumber } from '../../../../lib/bignumber';
import type { AppState } from '../../../../contexts/app-state/app-state-context';
const mockTotalSupply = new BigNumber(100);
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :)
const fixedTokenValue = 1000000000000000000;
const mockAppState: AppState = {
totalAssociated: new BigNumber('50063005'),
decimals: 18,
totalSupply: mockTotalSupply,
vegaWalletManageOverlay: false,
transactionOverlay: false,
bannerMessage: '',
disconnectNotice: false,
};
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
appState: mockAppState,
}),
}));
const renderComponent = (
proposal: ProposalQuery['proposal'],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
) =>
render(
<Router>
<MockedProvider mocks={mocks}>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalVotingStatus
proposal={proposal}
networkParams={mockNetworkParams}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</Router>
);
describe('ProposalVotingStatus', () => {
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterAll(() => {
jest.useRealTimers();
});
it('Renders majority reached', () => {
const yesVotes = 100;
const noVotes = 0;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('majority-reached')).toBeInTheDocument();
});
it('Renders majority not reached', () => {
const yesVotes = 20;
const noVotes = 80;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('majority-not-reached')).toBeInTheDocument();
});
it('Renders participation reached', () => {
const yesVotes = 1000;
const noVotes = 0;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('participation-reached')).toBeInTheDocument();
});
it('Renders participation not reached', () => {
const yesVotes = 0;
const noVotes = 0;
renderComponent(
generateProposal({
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('participation-not-reached')).toBeInTheDocument();
});
});
@@ -0,0 +1,174 @@
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Icon } from '@vegaprotocol/ui-toolkit';
import { useVoteInformation } from '../../hooks';
import { BigNumber } from '../../../../lib/bignumber';
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
const statusClasses = (reached: boolean) =>
classNames('flex items-center gap-2 px-4 py-2 rounded-md', {
'bg-vega-green-700': reached,
'bg-vega-red-700': !reached,
});
const MajorityStatus = ({
reached,
requiredMajority,
}: {
reached: boolean;
requiredMajority: string | null | undefined;
}) => {
const { t } = useTranslation();
return (
<div
className={statusClasses(reached)}
data-testid="proposal-majority-status"
>
{reached ? <Icon name="tick" /> : <Icon name="cross" />}
{reached ? (
<div data-testid="majority-reached">
{requiredMajority ? (
<>
{new BigNumber(requiredMajority).times(100).toString()}%{' '}
{t('majorityVotedForProposal')}
</>
) : (
t('requiredMajorityVotedForProposal')
)}
</div>
) : (
<div data-testid="majority-not-reached">
{requiredMajority ? (
<>
{new BigNumber(requiredMajority).times(100).toString()}%{' '}
{t('majorityNotVotedForProposal')}
</>
) : (
t('requiredMajorityNotVotedForProposal')
)}
</div>
)}
</div>
);
};
const ParticipationStatus = ({ reached }: { reached: boolean }) => {
const { t } = useTranslation();
return (
<div className={statusClasses(reached)}>
{reached ? (
<>
<Icon name="tick" />
<div data-testid="participation-reached">
{t('minParticipationReached')}
</div>
</>
) : (
<>
<Icon name="cross" />
<div data-testid="participation-not-reached">
{t('minParticipationNotReached')}
</div>
</>
)}
</div>
);
};
export const ProposalVotingStatus = ({
proposal,
networkParams,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
networkParams: Partial<NetworkParamsResult>;
}) => {
const { t } = useTranslation();
const { majorityMet, majorityLPMet, participationMet, participationLPMet } =
useVoteInformation({
proposal,
});
if (!proposal) {
return null;
}
const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket';
let requiredVotingMajority = null;
let requiredVotingMajorityLP = null;
if (networkParams) {
switch (proposal.terms.change.__typename) {
case 'NewMarket':
requiredVotingMajority =
networkParams.governance_proposal_market_requiredMajority;
break;
case 'UpdateMarket':
requiredVotingMajority =
networkParams.governance_proposal_updateMarket_requiredMajority;
requiredVotingMajorityLP =
networkParams.governance_proposal_updateMarket_requiredMajorityLP;
break;
case 'NewAsset':
requiredVotingMajority =
networkParams.governance_proposal_asset_requiredMajority;
break;
case 'UpdateAsset':
requiredVotingMajority =
networkParams.governance_proposal_updateAsset_requiredMajority;
break;
case 'UpdateNetworkParameter':
requiredVotingMajority =
networkParams.governance_proposal_updateNetParam_requiredMajority;
break;
case 'NewFreeform':
requiredVotingMajority =
networkParams.governance_proposal_freeform_requiredMajority;
break;
}
}
if (isUpdateMarket) {
return (
<div className="mb-6">
<p>{t('Token vote')}</p>
<div
className="grid grid-cols-2 gap-4 items-center"
data-testid="token-vote-statuses"
>
<MajorityStatus
reached={majorityMet}
requiredMajority={requiredVotingMajority}
/>{' '}
<ParticipationStatus reached={participationMet} />
</div>
<p className="mt-4">{t('Liquidity provider vote')}</p>
<div
className="grid grid-cols-2 gap-4 items-center"
data-testid="lp-vote-statuses"
>
<MajorityStatus
reached={majorityLPMet}
requiredMajority={requiredVotingMajorityLP}
/>{' '}
<ParticipationStatus reached={participationLPMet} />
</div>
</div>
);
}
return (
<div className="grid grid-cols-2 gap-4 items-center mb-6">
<MajorityStatus
reached={majorityMet}
requiredMajority={requiredVotingMajority}
/>{' '}
<ParticipationStatus reached={participationMet} />
</div>
);
};
@@ -4,6 +4,7 @@ import { generateProposal } from '../../test-helpers/generate-proposals';
import { Proposal } from './proposal'; import { Proposal } from './proposal';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { mockNetworkParams } from '../../test-helpers/mocks';
jest.mock('@vegaprotocol/network-parameters', () => ({ jest.mock('@vegaprotocol/network-parameters', () => ({
...jest.requireActual('@vegaprotocol/network-parameters'), ...jest.requireActual('@vegaprotocol/network-parameters'),
@@ -46,6 +47,7 @@ const renderComponent = (proposal: ProposalQuery['proposal']) => {
<Proposal <Proposal
restData={{}} restData={{}}
proposal={proposal as ProposalQuery['proposal']} proposal={proposal as ProposalQuery['proposal']}
networkParams={mockNetworkParams}
/> />
</MemoryRouter> </MemoryRouter>
); );
@@ -1,10 +1,6 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { import { Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { AsyncRenderer, Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header'; import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import { ProposalDescription } from '../proposal-description'; import { ProposalDescription } from '../proposal-description';
import { ProposalChangeTable } from '../proposal-change-table'; import { ProposalChangeTable } from '../proposal-change-table';
@@ -21,6 +17,8 @@ import type { MarketInfoWithData } from '@vegaprotocol/markets';
import type { AssetQuery } from '@vegaprotocol/assets'; import type { AssetQuery } from '@vegaprotocol/assets';
import { removePaginationWrapper } from '@vegaprotocol/utils'; import { removePaginationWrapper } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { ProposalMarketChanges } from '../proposal-market-changes';
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
export enum ProposalType { export enum ProposalType {
PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET', PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET',
@@ -32,28 +30,27 @@ export enum ProposalType {
} }
export interface ProposalProps { export interface ProposalProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal']; proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
networkParams: Partial<NetworkParamsResult>;
newMarketData?: MarketInfoWithData | null; newMarketData?: MarketInfoWithData | null;
assetData?: AssetQuery | null; assetData?: AssetQuery | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
restData: any; restData: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
originalMarketProposalRestData?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mostRecentlyEnactedAssociatedMarketProposal?: any;
} }
export const Proposal = ({ export const Proposal = ({
proposal, proposal,
networkParams,
restData, restData,
newMarketData, newMarketData,
assetData, assetData,
originalMarketProposalRestData,
mostRecentlyEnactedAssociatedMarketProposal,
}: ProposalProps) => { }: ProposalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { params, loading, error } = useNetworkParams([
NetworkParams.governance_proposal_market_minVoterBalance,
NetworkParams.governance_proposal_updateMarket_minVoterBalance,
NetworkParams.governance_proposal_asset_minVoterBalance,
NetworkParams.governance_proposal_updateAsset_minVoterBalance,
NetworkParams.governance_proposal_updateNetParam_minVoterBalance,
NetworkParams.governance_proposal_freeform_minVoterBalance,
NetworkParams.spam_protection_voting_min_tokens,
]);
if (!proposal) { if (!proposal) {
return null; return null;
@@ -79,122 +76,140 @@ export const Proposal = ({
let minVoterBalance = null; let minVoterBalance = null;
let proposalType = null; let proposalType = null;
if (params) { if (networkParams) {
switch (proposal.terms.change.__typename) { switch (proposal.terms.change.__typename) {
case 'NewMarket': case 'NewMarket':
minVoterBalance = params.governance_proposal_market_minVoterBalance; minVoterBalance =
networkParams.governance_proposal_market_minVoterBalance;
proposalType = ProposalType.PROPOSAL_NEW_MARKET; proposalType = ProposalType.PROPOSAL_NEW_MARKET;
break; break;
case 'UpdateMarket': case 'UpdateMarket':
minVoterBalance = minVoterBalance =
params.governance_proposal_updateMarket_minVoterBalance; networkParams.governance_proposal_updateMarket_minVoterBalance;
proposalType = ProposalType.PROPOSAL_UPDATE_MARKET; proposalType = ProposalType.PROPOSAL_UPDATE_MARKET;
break; break;
case 'NewAsset': case 'NewAsset':
minVoterBalance = params.governance_proposal_asset_minVoterBalance; minVoterBalance =
networkParams.governance_proposal_asset_minVoterBalance;
proposalType = ProposalType.PROPOSAL_NEW_ASSET; proposalType = ProposalType.PROPOSAL_NEW_ASSET;
break; break;
case 'UpdateAsset': case 'UpdateAsset':
minVoterBalance = minVoterBalance =
params.governance_proposal_updateAsset_minVoterBalance; networkParams.governance_proposal_updateAsset_minVoterBalance;
proposalType = ProposalType.PROPOSAL_UPDATE_ASSET; proposalType = ProposalType.PROPOSAL_UPDATE_ASSET;
break; break;
case 'UpdateNetworkParameter': case 'UpdateNetworkParameter':
minVoterBalance = minVoterBalance =
params.governance_proposal_updateNetParam_minVoterBalance; networkParams.governance_proposal_updateNetParam_minVoterBalance;
proposalType = ProposalType.PROPOSAL_NETWORK_PARAMETER; proposalType = ProposalType.PROPOSAL_NETWORK_PARAMETER;
break; break;
case 'NewFreeform': case 'NewFreeform':
minVoterBalance = params.governance_proposal_freeform_minVoterBalance; minVoterBalance =
networkParams.governance_proposal_freeform_minVoterBalance;
proposalType = ProposalType.PROPOSAL_FREEFORM; proposalType = ProposalType.PROPOSAL_FREEFORM;
break; break;
} }
} }
return ( return (
<AsyncRenderer data={params} loading={loading} error={error}> <section data-testid="proposal">
<section data-testid="proposal"> <div className="flex items-center gap-1 mb-6">
<div className="flex items-center gap-1"> <Icon name={'chevron-left'} />
<Icon name={'chevron-left'} />
{proposal.state === ProposalState.STATE_REJECTED ? ( {proposal.state === ProposalState.STATE_REJECTED ? (
<div data-testid="rejected-proposals-link"> <div data-testid="rejected-proposals-link">
<Link className="underline" to={Routes.PROPOSALS_REJECTED}> <Link className="underline" to={Routes.PROPOSALS_REJECTED}>
{t('RejectedProposals')} {t('RejectedProposals')}
</Link> </Link>
</div>
) : (
<div data-testid="all-proposals-link">
<Link className="underline" to={Routes.PROPOSALS}>
{t('AllProposals')}
</Link>
</div>
)}
</div>
<ProposalHeader proposal={proposal} isListItem={false} />
<div id="details">
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div> </div>
) : (
<div data-testid="all-proposals-link">
<Link className="underline" to={Routes.PROPOSALS}>
{t('AllProposals')}
</Link>
</div>
)}
</div>
<ProposalHeader
proposal={proposal}
isListItem={false}
networkParams={networkParams}
/>
{proposal.terms.change.__typename === 'NewAsset' && <div id="details">
proposal.terms.change.source.__typename === 'ERC20' && <div className="my-10">
proposal.id ? ( <ProposalChangeTable proposal={proposal} />
<ListAsset </div>
assetId={proposal.id}
withdrawalThreshold={ {proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.withdrawThreshold proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={proposal.terms.change.source.withdrawThreshold}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
{newMarketData && (
<div className="mb-4">
<ProposalMarketData marketData={newMarketData} />
</div>
)}
{proposal.terms.change.__typename === 'UpdateMarket' && (
<div className="mb-4">
<ProposalMarketChanges
originalProposal={
originalMarketProposalRestData?.data?.proposal?.terms?.newMarket
?.changes || {}
}
latestEnactedProposal={
mostRecentlyEnactedAssociatedMarketProposal?.node?.proposal
?.terms?.updateMarket?.changes || {}
}
updatedProposal={
restData?.data?.proposal?.terms?.updateMarket?.changes || {}
} }
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/> />
) : null}
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div> </div>
)}
{newMarketData && ( {(proposal.terms.change.__typename === 'NewAsset' ||
proposal.terms.change.__typename === 'UpdateAsset') &&
asset && (
<div className="mb-4"> <div className="mb-4">
<ProposalMarketData marketData={newMarketData} /> <ProposalAssetDetails asset={asset} />
</div> </div>
)} )}
{(proposal.terms.change.__typename === 'NewAsset' || <div className="mb-6">
proposal.terms.change.__typename === 'UpdateAsset') && <ProposalJson proposal={restData?.data?.proposal} />
asset && (
<div className="mb-4">
<ProposalAssetDetails asset={asset} />
</div>
)}
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
</div>
</div> </div>
</div>
<div id="voting"> <div id="voting">
<div className="mb-10"> <div className="mb-10">
<RoundedWrapper paddingBottom={true}> <RoundedWrapper paddingBottom={true}>
<VoteDetails <VoteDetails
proposal={proposal}
proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
params?.spam_protection_voting_min_tokens
}
/>
</RoundedWrapper>
</div>
<div className="mb-4">
<ProposalVotesTable
proposal={proposal} proposal={proposal}
proposalType={proposalType} proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
networkParams?.spam_protection_voting_min_tokens
}
/> />
</div> </RoundedWrapper>
</div> </div>
</section>
</AsyncRenderer> <div className="mb-4">
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
</div>
</div>
</section>
); );
}; };
@@ -5,11 +5,7 @@ import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
ProposalRejectionReason,
ProposalState,
VoteValue,
} from '@vegaprotocol/types';
import { import {
generateNoVotes, generateNoVotes,
generateProposal, generateProposal,
@@ -18,7 +14,6 @@ import {
import { ProposalsListItemDetails } from './proposals-list-item-details'; import { ProposalsListItemDetails } from './proposals-list-item-details';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats'; import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import { import {
mockPubkey,
mockWalletContext, mockWalletContext,
networkParamsQueryMock, networkParamsQueryMock,
fiveMinutes, fiveMinutes,
@@ -28,39 +23,6 @@ import {
nextWeek, nextWeek,
} from '../../test-helpers/mocks'; } from '../../test-helpers/mocks';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { UserVoteDocument } from '../vote-details/__generated__/Vote';
import faker from 'faker';
const createUserVoteQueryMock = (
proposalId: string | undefined | null,
value: VoteValue
) => ({
request: {
query: UserVoteDocument,
variables: {
partyId: mockPubkey.publicKey,
},
},
result: {
data: {
party: {
votesConnection: {
edges: [
{
node: {
proposalId,
vote: {
value,
datetime: faker.date.past().toISOString(),
},
},
},
],
},
},
},
},
});
const renderComponent = ( const renderComponent = (
proposal: ProposalQuery['proposal'], proposal: ProposalQuery['proposal'],
@@ -131,7 +93,7 @@ describe('Proposals list item details', () => {
); );
}); });
it('Renders proposal state: Update market proposal - set to pass by LP vote', () => { it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => {
renderComponent( renderComponent(
generateProposal({ generateProposal({
state: ProposalState.STATE_OPEN, state: ProposalState.STATE_OPEN,
@@ -153,11 +115,11 @@ describe('Proposals list item details', () => {
}) })
); );
expect(screen.getByTestId('vote-status')).toHaveTextContent( expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Set to pass by LP vote' 'Currently expected to pass by LP vote'
); );
}); });
it('Renders proposal state: Update market proposal - set to pass by token vote', () => { it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => {
renderComponent( renderComponent(
generateProposal({ generateProposal({
state: ProposalState.STATE_OPEN, state: ProposalState.STATE_OPEN,
@@ -179,11 +141,11 @@ describe('Proposals list item details', () => {
}) })
); );
expect(screen.getByTestId('vote-status')).toHaveTextContent( expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Set to pass by token vote' 'Currently expected to pass by token vote'
); );
}); });
it('Renders proposal state: Update market proposal - set to fail', () => { it('Renders proposal state: Update market proposal - Currently expected to fail', () => {
renderComponent( renderComponent(
generateProposal({ generateProposal({
state: ProposalState.STATE_OPEN, state: ProposalState.STATE_OPEN,
@@ -204,7 +166,9 @@ describe('Proposals list item details', () => {
}, },
}) })
); );
expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to fail'); expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Currently expected to fail'
);
}); });
it('Renders proposal state: Open - 5 minutes left to vote', () => { it('Renders proposal state: Open - 5 minutes left to vote', () => {
@@ -249,52 +213,6 @@ describe('Proposals list item details', () => {
); );
}); });
it('Renders proposal state: Open - user voted for', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
closingDatetime: nextWeek.toString(),
},
});
renderComponent(proposal, [
networkParamsQueryMock,
createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_YES),
]);
expect(await screen.findByText('You voted For')).toBeInTheDocument();
});
it('Renders proposal state: Open - user voted against', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
closingDatetime: nextWeek.toString(),
},
});
renderComponent(proposal, [
networkParamsQueryMock,
createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_NO),
]);
expect(await screen.findByText('You voted Against')).toBeInTheDocument();
});
it('Renders proposal state: Open - participation not reached', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
enactmentDatetime: nextWeek.toString(),
},
votes: {
no: generateNoVotes(0),
yes: generateYesVotes(0),
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Participation not reached'
);
});
it('Renders proposal state: Open - majority not reached', () => { it('Renders proposal state: Open - majority not reached', () => {
renderComponent( renderComponent(
generateProposal({ generateProposal({
@@ -309,7 +227,7 @@ describe('Proposals list item details', () => {
}) })
); );
expect(screen.getByTestId('vote-status')).toHaveTextContent( expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Majority not reached' 'Currently expected to fail'
); );
}); });
@@ -327,42 +245,8 @@ describe('Proposals list item details', () => {
}, },
}) })
); );
expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to pass');
});
it('Renders proposal state: Declined - participation not reached', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_DECLINED,
terms: {
enactmentDatetime: lastWeek.toString(),
},
votes: {
no: generateNoVotes(0),
yes: generateYesVotes(0),
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent( expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Participation not reached' 'Currently expected to pass'
);
});
it('Renders proposal state: Declined - majority not reached', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_DECLINED,
terms: {
enactmentDatetime: lastWeek.toString(),
},
votes: {
no: generateNoVotes(1, 1000000000000000000),
yes: generateYesVotes(1, 1000000000000000000),
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Majority not reached'
); );
}); });
@@ -1,9 +1,6 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit';
import { useVoteInformation } from '../../hooks'; import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
import { useUserVote } from '../vote-details/use-user-vote';
import { StatusPass } from '../current-proposal-status/current-proposal-status';
import { format, formatDistanceToNowStrict } from 'date-fns';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats'; import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
@@ -14,138 +11,100 @@ import {
import Routes from '../../../routes'; import Routes from '../../../routes';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { useVoteInformation } from '../../hooks';
const MajorityNotReached = () => {
const { t } = useTranslation();
return (
<>
{t('Majority')} {t('not reached')}
</>
);
};
const ParticipationNotReached = () => {
const { t } = useTranslation();
return (
<>
{t('Participation')} {t('not reached')}
</>
);
};
export const ProposalsListItemDetails = ({ export const ProposalsListItemDetails = ({
proposal, proposal,
}: { }: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal']; proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => { }) => {
const { t } = useTranslation();
const state = proposal?.state; const state = proposal?.state;
const { const { willPassByTokenVote, willPassByLPVote } = useVoteInformation({
willPassByTokenVote,
willPassByLPVote,
majorityMet,
participationMet,
} = useVoteInformation({
proposal, proposal,
}); });
const { t } = useTranslation();
const { voteState } = useUserVote(proposal?.id);
const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket';
const updateMarketWillPass = willPassByTokenVote || willPassByLPVote; const updateMarketWillPass = willPassByTokenVote || willPassByLPVote;
const updateMarketVotePassMethod = willPassByTokenVote const updateMarketVotePassMethod = willPassByTokenVote
? t('byTokenVote') ? t('byTokenVote')
: t('byLPVote'); : t('byLPVote');
const nowToEnactmentInHours = differenceInHours(
new Date(proposal?.terms.closingDatetime),
new Date()
);
const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket';
let voteDetails: ReactNode; let voteDetails: ReactNode;
let voteStatus: ReactNode; let voteStatus: ReactNode;
switch (state) { switch (state) {
case ProposalState.STATE_ENACTED: { case ProposalState.STATE_ENACTED: {
voteDetails = proposal?.terms.enactmentDatetime && ( voteDetails =
<> proposal?.terms.enactmentDatetime &&
{format( t('enactedOn{{date}}', {
new Date(proposal?.terms.enactmentDatetime), enactmentDate:
DATE_FORMAT_DETAILED proposal?.terms.enactmentDatetime &&
)} format(
</> new Date(proposal?.terms.enactmentDatetime),
); DATE_FORMAT_DETAILED
),
});
break; break;
} }
case ProposalState.STATE_PASSED: { case ProposalState.STATE_PASSED: {
voteDetails = proposal?.terms.change.__typename !== 'NewFreeform' && ( voteDetails =
<> proposal?.terms.change.__typename !== 'NewFreeform' &&
{t('toEnactOn')}{' '} t('enactsOn{{date}}', {
{proposal?.terms.enactmentDatetime && enactmentDate:
proposal?.terms.enactmentDatetime &&
format( format(
new Date(proposal.terms.enactmentDatetime), new Date(proposal.terms.enactmentDatetime),
DATE_FORMAT_DETAILED DATE_FORMAT_DETAILED
)} ),
</> });
);
break; break;
} }
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: { case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
voteDetails = proposal?.terms.change.__typename !== 'NewFreeform' && ( voteDetails =
<> proposal?.terms.change.__typename !== 'NewFreeform' &&
{t('toEnactOn')}{' '} t('enactsOn{{date}}', {
{proposal?.terms.enactmentDatetime && enactmentDate:
proposal?.terms.enactmentDatetime &&
format( format(
new Date(proposal.terms.enactmentDatetime), new Date(proposal.terms.enactmentDatetime),
DATE_FORMAT_DETAILED DATE_FORMAT_DETAILED
)} ),
</> });
);
break; break;
} }
case ProposalState.STATE_OPEN: { case ProposalState.STATE_OPEN: {
voteDetails = (voteState === 'Yes' && ( voteDetails = (
<> <span className={nowToEnactmentInHours < 6 ? 'text-vega-pink' : ''}>
{t('youVoted')} {t('voteState_Yes')} {formatDistanceToNowStrict(new Date(proposal?.terms.closingDatetime))}{' '}
</> {t('left to vote')}
)) || </span>
(voteState === 'No' && ( );
<>
{t('youVoted')} {t('voteState_No')}
</>
)) || (
<>
{formatDistanceToNowStrict(
new Date(proposal?.terms.closingDatetime)
)}{' '}
{t('left to vote')}
</>
);
voteStatus = voteStatus =
(isUpdateMarket && (isUpdateMarket &&
(updateMarketWillPass ? ( (updateMarketWillPass ? (
<> <>
{t('Set to')}{' '} {t('currentlySetTo')} {t('pass')} {updateMarketVotePassMethod}
<StatusPass>
{t('pass')} {updateMarketVotePassMethod}
</StatusPass>
</> </>
) : ( ) : (
<> <>
{t('Set to')} {t('fail')} {t('currentlySetTo')} {t('fail')}
</> </>
))) || ))) ||
(!participationMet && <ParticipationNotReached />) ||
(!majorityMet && <MajorityNotReached />) ||
(willPassByTokenVote ? ( (willPassByTokenVote ? (
<> <>
{t('Set to')} {t('pass')} {t('currentlySetTo')} {t('pass')}
</> </>
) : ( ) : (
<> <>
{t('Set to')} {t('fail')} {t('currentlySetTo')} {t('fail')}
</> </>
)); ));
break; break;
} }
case ProposalState.STATE_DECLINED: {
voteStatus =
(!participationMet && <ParticipationNotReached />) ||
(!majorityMet && <MajorityNotReached />);
break;
}
case ProposalState.STATE_REJECTED: { case ProposalState.STATE_REJECTED: {
voteStatus = proposal?.rejectionReason && ( voteStatus = proposal?.rejectionReason && (
<>{t(ProposalRejectionReasonMapping[proposal.rejectionReason])}</> <>{t(ProposalRejectionReasonMapping[proposal.rejectionReason])}</>
@@ -155,29 +114,17 @@ export const ProposalsListItemDetails = ({
} }
return ( return (
<div className="grid grid-cols-[1fr_auto] mt-4 items-start gap-2 text-sm"> <div className="mt-4 items-start text-sm">
{voteDetails && ( <div className="flex items-center gap-2 text-vega-light-300 mb-2">
<div {voteDetails && <span data-testid="vote-details">{voteDetails}</span>}
className="col-start-1 row-start-2 text-vega-light-300" {voteDetails && voteStatus && <span>&middot;</span>}
data-testid="vote-details" {voteStatus && <span data-testid="vote-status">{voteStatus}</span>}
> </div>
{voteDetails}
</div>
)}
{voteStatus && (
<div
className="col-start-2 row-start-1 justify-self-end"
data-testid="vote-status"
>
{voteStatus}
</div>
)}
{proposal?.id && ( {proposal?.id && (
<div className="col-start-2 row-start-2 justify-self-end"> <Link to={`${Routes.PROPOSALS}/${proposal.id}`}>
<Link to={`${Routes.PROPOSALS}/${proposal.id}`}> <Button data-testid="view-proposal-btn">{t('viewDetails')}</Button>
<Button data-testid="view-proposal-btn">{t('View')}</Button> </Link>
</Link>
</div>
)} )}
</div> </div>
); );
@@ -3,18 +3,23 @@ import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import { ProposalsListItemDetails } from './proposals-list-item-details'; import { ProposalsListItemDetails } from './proposals-list-item-details';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
interface ProposalsListItemProps { interface ProposalsListItemProps {
proposal?: ProposalFieldsFragment | ProposalQuery['proposal'] | null; proposal?: ProposalFieldsFragment | ProposalQuery['proposal'] | null;
networkParams: Partial<NetworkParamsResult> | null;
} }
export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => { export const ProposalsListItem = ({
if (!proposal || !proposal.id) return null; proposal,
networkParams,
}: ProposalsListItemProps) => {
if (!proposal || !proposal.id || !networkParams) return null;
return ( return (
<li id={proposal.id} data-testid="proposals-list-item"> <li id={proposal.id} data-testid="proposals-list-item">
<RoundedWrapper paddingBottom={true} heightFull={true}> <RoundedWrapper paddingBottom={true} heightFull={true}>
<ProposalHeader proposal={proposal} /> <ProposalHeader proposal={proposal} networkParams={networkParams} />
<ProposalsListItemDetails proposal={proposal} /> <ProposalsListItemDetails proposal={proposal} />
</RoundedWrapper> </RoundedWrapper>
</li> </li>
@@ -88,31 +88,39 @@ afterAll(() => {
jest.useRealTimers(); jest.useRealTimers();
}); });
jest.mock('../vote-details/use-user-vote', () => ({
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
}));
describe('Proposals list', () => { describe('Proposals list', () => {
it('Render a page title and link to the make proposal form', () => { it('Render a page title and link to the make proposal form', async () => {
render(renderComponent([])); render(renderComponent([]));
await screen.findByTestId('proposals-list');
expect(screen.getByText('Proposals')).toBeInTheDocument(); expect(screen.getByText('Proposals')).toBeInTheDocument();
expect(screen.getByTestId('new-proposal-link')).toBeInTheDocument(); expect(screen.getByTestId('new-proposal-link')).toBeInTheDocument();
}); });
it('Will hide filter if no proposals', () => { it('Will hide filter if no proposals', async () => {
render(renderComponent([])); render(renderComponent([]));
await screen.findByTestId('proposals-list');
expect( expect(
screen.queryByTestId('proposals-list-filter') screen.queryByTestId('proposals-list-filter')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it('Will show filter if there are proposals', () => { it('Will show filter if there are proposals', async () => {
render(renderComponent([enactedProposalClosedLastWeek])); render(renderComponent([enactedProposalClosedLastWeek]));
await screen.findByTestId('proposals-list');
expect(screen.queryByTestId('proposals-list-filter')).toBeInTheDocument(); expect(screen.queryByTestId('proposals-list-filter')).toBeInTheDocument();
}); });
it('Will render a link to rejected proposals', () => { it('Will render a link to rejected proposals', async () => {
render(renderComponent([])); render(renderComponent([]));
await screen.findByTestId('proposals-list');
expect(screen.getByText('See rejected proposals')).toBeInTheDocument(); expect(screen.getByText('See rejected proposals')).toBeInTheDocument();
}); });
it('Places proposals correctly in open or closed lists', () => { it('Places proposals correctly in open or closed lists', async () => {
render( render(
renderComponent([ renderComponent([
openProposalClosesNextWeek, openProposalClosesNextWeek,
@@ -121,6 +129,7 @@ describe('Proposals list', () => {
failedProposalClosedLastMonth, failedProposalClosedLastMonth,
]) ])
); );
await screen.findByTestId('proposals-list');
const openProposals = within(screen.getByTestId('open-proposals')); const openProposals = within(screen.getByTestId('open-proposals'));
const closedProposals = within(screen.getByTestId('closed-proposals')); const closedProposals = within(screen.getByTestId('closed-proposals'));
expect(openProposals.getAllByTestId('proposals-list-item').length).toBe(2); expect(openProposals.getAllByTestId('proposals-list-item').length).toBe(2);
@@ -129,42 +138,47 @@ describe('Proposals list', () => {
); );
}); });
it('Displays info on no proposals', () => { it('Displays info on no proposals', async () => {
render(renderComponent([])); render(renderComponent([]));
await screen.findByTestId('proposals-list');
expect(screen.queryByTestId('open-proposals')).not.toBeInTheDocument(); expect(screen.queryByTestId('open-proposals')).not.toBeInTheDocument();
expect(screen.getByTestId('no-open-proposals')).toBeInTheDocument(); expect(screen.getByTestId('no-open-proposals')).toBeInTheDocument();
expect(screen.queryByTestId('closed-proposals')).not.toBeInTheDocument(); expect(screen.queryByTestId('closed-proposals')).not.toBeInTheDocument();
expect(screen.getByTestId('no-closed-proposals')).toBeInTheDocument(); expect(screen.getByTestId('no-closed-proposals')).toBeInTheDocument();
}); });
it('Displays info on no open proposals if only closed are present', () => { it('Displays info on no open proposals if only closed are present', async () => {
render(renderComponent([enactedProposalClosedLastWeek])); render(renderComponent([enactedProposalClosedLastWeek]));
await screen.findByTestId('proposals-list');
expect(screen.queryByTestId('open-proposals')).not.toBeInTheDocument(); expect(screen.queryByTestId('open-proposals')).not.toBeInTheDocument();
expect(screen.getByTestId('no-open-proposals')).toBeInTheDocument(); expect(screen.getByTestId('no-open-proposals')).toBeInTheDocument();
expect(screen.getByTestId('closed-proposals')).toBeInTheDocument(); expect(screen.getByTestId('closed-proposals')).toBeInTheDocument();
expect(screen.queryByTestId('no-closed-proposals')).not.toBeInTheDocument(); expect(screen.queryByTestId('no-closed-proposals')).not.toBeInTheDocument();
}); });
it('Displays info on no closed proposals if only open are present', () => { it('Displays info on no closed proposals if only open are present', async () => {
render(renderComponent([openProposalClosesNextWeek])); render(renderComponent([openProposalClosesNextWeek]));
await screen.findByTestId('proposals-list');
expect(screen.getByTestId('open-proposals')).toBeInTheDocument(); expect(screen.getByTestId('open-proposals')).toBeInTheDocument();
expect(screen.queryByTestId('no-open-proposals')).not.toBeInTheDocument(); expect(screen.queryByTestId('no-open-proposals')).not.toBeInTheDocument();
expect(screen.queryByTestId('closed-proposals')).not.toBeInTheDocument(); expect(screen.queryByTestId('closed-proposals')).not.toBeInTheDocument();
expect(screen.getByTestId('no-closed-proposals')).toBeInTheDocument(); expect(screen.getByTestId('no-closed-proposals')).toBeInTheDocument();
}); });
it('Opens filter form when button is clicked', () => { it('Opens filter form when button is clicked', async () => {
render( render(
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek]) renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
); );
await screen.findByTestId('proposals-list');
fireEvent.click(screen.getByTestId('proposal-filter-toggle')); fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
expect(screen.getByTestId('proposals-list-filter')).toBeInTheDocument(); expect(screen.getByTestId('proposals-list-filter')).toBeInTheDocument();
}); });
it('Filters list by text - party id', () => { it('Filters list by text - party id', async () => {
render( render(
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek]) renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
); );
await screen.findByTestId('proposals-list');
fireEvent.click(screen.getByTestId('proposal-filter-toggle')); fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
fireEvent.change(screen.getByTestId('filter-input'), { fireEvent.change(screen.getByTestId('filter-input'), {
target: { value: 'bvcx' }, target: { value: 'bvcx' },
@@ -174,10 +188,11 @@ describe('Proposals list', () => {
expect(container.querySelector('#proposal1')).not.toBeInTheDocument(); expect(container.querySelector('#proposal1')).not.toBeInTheDocument();
}); });
it('Filters list by text - proposal id', () => { it('Filters list by text - proposal id', async () => {
render( render(
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek]) renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
); );
await screen.findByTestId('proposals-list');
fireEvent.click(screen.getByTestId('proposal-filter-toggle')); fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
fireEvent.change(screen.getByTestId('filter-input'), { fireEvent.change(screen.getByTestId('filter-input'), {
target: { value: 'proposal1' }, target: { value: 'proposal1' },
@@ -187,10 +202,11 @@ describe('Proposals list', () => {
expect(container.querySelector('#proposal2')).not.toBeInTheDocument(); expect(container.querySelector('#proposal2')).not.toBeInTheDocument();
}); });
it('Filters list by text - check for substring matching', () => { it('Filters list by text - check for substring matching', async () => {
render( render(
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek]) renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
); );
await screen.findByTestId('proposals-list');
fireEvent.click(screen.getByTestId('proposal-filter-toggle')); fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
fireEvent.change(screen.getByTestId('filter-input'), { fireEvent.change(screen.getByTestId('filter-input'), {
target: { value: 'osal1' }, target: { value: 'osal1' },
@@ -200,10 +216,11 @@ describe('Proposals list', () => {
expect(container.querySelector('#proposal2')).not.toBeInTheDocument(); expect(container.querySelector('#proposal2')).not.toBeInTheDocument();
}); });
it('When filter is used, clear button is visible', () => { it('When filter is used, clear button is visible', async () => {
render( render(
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek]) renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
); );
await screen.findByTestId('proposals-list');
fireEvent.click(screen.getByTestId('proposal-filter-toggle')); fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
fireEvent.change(screen.getByTestId('filter-input'), { fireEvent.change(screen.getByTestId('filter-input'), {
target: { value: 'test' }, target: { value: 'test' },
@@ -211,10 +228,11 @@ describe('Proposals list', () => {
expect(screen.getByTestId('clear-filter')).toBeInTheDocument(); expect(screen.getByTestId('clear-filter')).toBeInTheDocument();
}); });
it('When clear filter button is used, input is cleared', () => { it('When clear filter button is used, input is cleared', async () => {
render( render(
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek]) renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
); );
await screen.findByTestId('proposals-list');
fireEvent.click(screen.getByTestId('proposal-filter-toggle')); fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
fireEvent.change(screen.getByTestId('filter-input'), { fireEvent.change(screen.getByTestId('filter-input'), {
target: { value: 'test' }, target: { value: 'test' },
@@ -225,37 +243,41 @@ describe('Proposals list', () => {
); );
}); });
it('Displays a toggle for closed proposals if there are both closed governance proposals and closed upgrade proposals', () => { it('Displays a toggle for closed proposals if there are both closed governance proposals and closed upgrade proposals', async () => {
render( render(
renderComponent( renderComponent(
[enactedProposalClosedLastWeek], [enactedProposalClosedLastWeek],
[closedProtocolUpgradeProposal] [closedProtocolUpgradeProposal]
) )
); );
await screen.findByTestId('proposals-list');
expect(screen.getByTestId('toggle-closed-proposals')).toBeInTheDocument(); expect(screen.getByTestId('toggle-closed-proposals')).toBeInTheDocument();
}); });
it('Does not display a toggle for closed proposals if there are only closed upgrade proposals', () => { it('Does not display a toggle for closed proposals if there are only closed upgrade proposals', async () => {
render(renderComponent([], [closedProtocolUpgradeProposal])); render(renderComponent([], [closedProtocolUpgradeProposal]));
await screen.findByTestId('proposals-list');
expect( expect(
screen.queryByTestId('toggle-closed-proposals') screen.queryByTestId('toggle-closed-proposals')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it('Does not display a toggle for closed proposals if there are only closed governance proposals', () => { it('Does not display a toggle for closed proposals if there are only closed governance proposals', async () => {
render(renderComponent([enactedProposalClosedLastWeek])); render(renderComponent([enactedProposalClosedLastWeek]));
await screen.findByTestId('proposals-list');
expect( expect(
screen.queryByTestId('toggle-closed-proposals') screen.queryByTestId('toggle-closed-proposals')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it('Does not display a toggle for closed proposals if the proposal filter is engaged', () => { it('Does not display a toggle for closed proposals if the proposal filter is engaged', async () => {
render( render(
renderComponent( renderComponent(
[enactedProposalClosedLastWeek], [enactedProposalClosedLastWeek],
[closedProtocolUpgradeProposal] [closedProtocolUpgradeProposal]
) )
); );
await screen.findByTestId('proposal-filter-toggle');
fireEvent.click(screen.getByTestId('proposal-filter-toggle')); fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
fireEvent.change(screen.getByTestId('filter-input'), { fireEvent.change(screen.getByTestId('filter-input'), {
target: { value: 'test' }, target: { value: 'test' },
@@ -265,7 +287,7 @@ describe('Proposals list', () => {
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it('Displays closed governance proposals by default due to default for the toggle', () => { it('Displays closed governance proposals by default due to default for the toggle', async () => {
render( render(
renderComponent( renderComponent(
[enactedProposalClosedLastWeek], [enactedProposalClosedLastWeek],
@@ -273,17 +295,19 @@ describe('Proposals list', () => {
) )
); );
expect( expect(
screen.getByTestId('closed-governance-proposals') await screen.findByTestId('closed-governance-proposals')
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it('Displays closed upgrade proposals when the toggle is clicked', () => { it('Displays closed upgrade proposals when the toggle is clicked', async () => {
render( render(
renderComponent( renderComponent(
[enactedProposalClosedLastWeek], [enactedProposalClosedLastWeek],
[closedProtocolUpgradeProposal] [closedProtocolUpgradeProposal]
) )
); );
await screen.findByTestId('toggle-closed-proposals');
fireEvent.click(screen.getByText('Network upgrades')); fireEvent.click(screen.getByText('Network upgrades'));
expect(screen.getByTestId('closed-upgrade-proposals')).toBeInTheDocument(); expect(screen.getByTestId('closed-upgrade-proposals')).toBeInTheDocument();
}); });
@@ -8,6 +8,7 @@ import { ProtocolUpgradeProposalsListItem } from '../protocol-upgrade-proposals-
import { ProposalsListFilter } from '../proposals-list-filter'; import { ProposalsListFilter } from '../proposals-list-filter';
import Routes from '../../../routes'; import Routes from '../../../routes';
import { import {
AsyncRenderer,
Button, Button,
Toggle, Toggle,
VegaIcon, VegaIcon,
@@ -19,6 +20,10 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals'; import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment'; import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
interface ProposalsListProps { interface ProposalsListProps {
proposals: Array<ProposalFieldsFragment | ProposalQuery['proposal']>; proposals: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
@@ -70,6 +75,20 @@ export const ProposalsList = ({
lastBlockHeight, lastBlockHeight,
}: ProposalsListProps) => { }: ProposalsListProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const {
params: networkParams,
loading: networkParamsLoading,
error: networkParamsError,
} = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
NetworkParams.governance_proposal_asset_requiredMajority,
NetworkParams.governance_proposal_updateAsset_requiredMajority,
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
NetworkParams.governance_proposal_freeform_requiredMajority,
]);
const [filterString, setFilterString] = useState(''); const [filterString, setFilterString] = useState('');
const [closedProposalsView, setClosedProposalsView] = const [closedProposalsView, setClosedProposalsView] =
useState<ClosedProposalsViewOptions>( useState<ClosedProposalsViewOptions>(
@@ -134,166 +153,181 @@ export const ProposalsList = ({
p?.party?.id?.toString().includes(filterString); p?.party?.id?.toString().includes(filterString);
return ( return (
<> <AsyncRenderer
<div className="grid xs:grid-cols-2 items-center"> loading={networkParamsLoading}
<Heading error={networkParamsError}
centerContent={false} data={networkParams}
marginBottom={false} >
title={t('pageTitleProposals')} <div data-testid="proposals-list">
/> <div className="grid xs:grid-cols-2 items-center">
<Heading
centerContent={false}
marginBottom={false}
title={t('pageTitleProposals')}
/>
{DocsLinks && ( {DocsLinks && (
<div className="xs:justify-self-end" data-testid="new-proposal-link"> <div
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE}> className="xs:justify-self-end"
<Button variant="primary" size="sm"> data-testid="new-proposal-link"
<div className="flex items-center gap-1"> >
{t('NewProposal')} <ExternalLink href={DocsLinks.PROPOSALS_GUIDE}>
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} /> <Button variant="primary" size="sm">
</div> <div className="flex items-center gap-1">
</Button> {t('NewProposal')}
</ExternalLink> <VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
</div> </div>
</Button>
</ExternalLink>
</div>
)}
</div>
<p className="mb-8">
{t(
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network. Network upgrades are proposed and approved by validators.`
)}{' '}
<ExternalLink
data-testid="proposal-documentation-link"
href={ExternalLinks.GOVERNANCE_PAGE}
className="text-white"
>
{t(`Find out more about Vega governance`)}
</ExternalLink>
</p>
{proposals.length > 0 && (
<ProposalsListFilter
filterString={filterString}
setFilterString={(value) => {
setFilterString(value);
if (value.length > 0) {
// If the filter is engaged, ensure the user is viewing governance proposals,
// as network upgrades do not have IDs to filter by and will be excluded.
setClosedProposalsView(
ClosedProposalsViewOptions.NetworkGovernance
);
}
}}
/>
)} )}
</div>
<p className="mb-8"> <section className="-mx-4 p-4 mb-8 bg-vega-dark-100">
{t( <SubHeading title={t('openProposals')} />
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network. Network upgrades are proposed and approved by validators.`
)}{' '}
<ExternalLink
data-testid="proposal-documentation-link"
href={ExternalLinks.GOVERNANCE_PAGE}
className="text-white"
>
{t(`Find out more about Vega governance`)}
</ExternalLink>
</p>
{proposals.length > 0 && ( {sortedProposals.open.length > 0 ||
<ProposalsListFilter sortedProtocolUpgradeProposals.open.length > 0 ? (
filterString={filterString} <ul data-testid="open-proposals">
setFilterString={(value) => { {filterString.length < 1 &&
setFilterString(value); sortedProtocolUpgradeProposals.open.map((proposal) => (
if (value.length > 0) { <ProtocolUpgradeProposalsListItem
// If the filter is engaged, ensure the user is viewing governance proposals, key={proposal.upgradeBlockHeight}
// as network upgrades do not have IDs to filter by and will be excluded. proposal={proposal}
setClosedProposalsView( />
ClosedProposalsViewOptions.NetworkGovernance ))}
);
}
}}
/>
)}
<section className="-mx-4 p-4 mb-8 bg-vega-dark-100"> {sortedProposals.open.filter(filterPredicate).map((proposal) => (
<SubHeading title={t('openProposals')} /> <ProposalsListItem
key={proposal?.id}
{sortedProposals.open.length > 0 ||
sortedProtocolUpgradeProposals.open.length > 0 ? (
<ul data-testid="open-proposals">
{filterString.length < 1 &&
sortedProtocolUpgradeProposals.open.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
proposal={proposal} proposal={proposal}
networkParams={networkParams}
/> />
))} ))}
</ul>
) : (
<p className="mb-0" data-testid="no-open-proposals">
{t('noOpenProposals')}
</p>
)}
</section>
{sortedProposals.open.filter(filterPredicate).map((proposal) => ( <section className="relative">
<ProposalsListItem key={proposal?.id} proposal={proposal} /> <SubHeading title={t('closedProposals')} />
))} {sortedProposals.closed.length > 0 ||
</ul> sortedProtocolUpgradeProposals.closed.length > 0 ? (
) : ( <>
<p className="mb-0" data-testid="no-open-proposals"> {
{t('noOpenProposals')} // We need both the closed proposals and closed protocol upgrade
</p> // proposals to be present for there to be a toggle. It also gets
)} // hidden if the user has filtered the list, as the upgrade proposals
</section> // do not have the necessary fields for filtering.
sortedProposals.closed.length > 0 &&
<section className="relative"> sortedProtocolUpgradeProposals.closed.length > 0 &&
<SubHeading title={t('closedProposals')} /> filterString.length < 1 && (
{sortedProposals.closed.length > 0 || <div
sortedProtocolUpgradeProposals.closed.length > 0 ? ( className="grid w-full justify-end xl:-mt-12 pb-6"
<> data-testid="toggle-closed-proposals"
{ >
// We need both the closed proposals and closed protocol upgrade <div className="w-[440px]">
// proposals to be present for there to be a toggle. It also gets <Toggle
// hidden if the user has filtered the list, as the upgrade proposals name="closed-proposals-toggle"
// do not have the necessary fields for filtering. toggles={[
sortedProposals.closed.length > 0 && {
sortedProtocolUpgradeProposals.closed.length > 0 && label: t(
filterString.length < 1 && ( ClosedProposalsViewOptions.NetworkGovernance
<div ),
className="grid w-full justify-end xl:-mt-12 pb-6" value:
data-testid="toggle-closed-proposals" ClosedProposalsViewOptions.NetworkGovernance,
> },
<div className="w-[440px]"> {
<Toggle label: t(
name="closed-proposals-toggle" ClosedProposalsViewOptions.NetworkUpgrades
toggles={[ ),
{ value: ClosedProposalsViewOptions.NetworkUpgrades,
label: t( },
ClosedProposalsViewOptions.NetworkGovernance ]}
), checkedValue={closedProposalsView}
value: ClosedProposalsViewOptions.NetworkGovernance, onChange={(e) =>
}, setClosedProposalsView(
{ e.target.value as ClosedProposalsViewOptions
label: t( )
ClosedProposalsViewOptions.NetworkUpgrades }
), />
value: ClosedProposalsViewOptions.NetworkUpgrades, </div>
},
]}
checkedValue={closedProposalsView}
onChange={(e) =>
setClosedProposalsView(
e.target.value as ClosedProposalsViewOptions
)
}
/>
</div> </div>
</div> )
) }
}
<ul data-testid="closed-proposals"> <ul data-testid="closed-proposals">
{closedProposalsView === {closedProposalsView ===
ClosedProposalsViewOptions.NetworkUpgrades && ( ClosedProposalsViewOptions.NetworkUpgrades && (
<div data-testid="closed-upgrade-proposals"> <div data-testid="closed-upgrade-proposals">
{sortedProtocolUpgradeProposals.closed.map((proposal) => ( {sortedProtocolUpgradeProposals.closed.map((proposal) => (
<ProtocolUpgradeProposalsListItem <ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight} key={proposal.upgradeBlockHeight}
proposal={proposal}
/>
))}
</div>
)}
{closedProposalsView ===
ClosedProposalsViewOptions.NetworkGovernance && (
<div data-testid="closed-governance-proposals">
{sortedProposals.closed
.filter(filterPredicate)
.map((proposal) => (
<ProposalsListItem
key={proposal?.id}
proposal={proposal} proposal={proposal}
/> />
))} ))}
</div> </div>
)} )}
</ul>
</>
) : (
<p className="mb-0" data-testid="no-closed-proposals">
{t('noClosedProposals')}
</p>
)}
</section>
<Link className="underline" to={Routes.PROPOSALS_REJECTED}> {closedProposalsView ===
{t('seeRejectedProposals')} ClosedProposalsViewOptions.NetworkGovernance && (
</Link> <div data-testid="closed-governance-proposals">
</> {sortedProposals.closed
.filter(filterPredicate)
.map((proposal) => (
<ProposalsListItem
key={proposal?.id}
proposal={proposal}
networkParams={networkParams}
/>
))}
</div>
)}
</ul>
</>
) : (
<p className="mb-0" data-testid="no-closed-proposals">
{t('noClosedProposals')}
</p>
)}
</section>
<Link className="underline" to={Routes.PROPOSALS_REJECTED}>
{t('seeRejectedProposals')}
</Link>
</div>
</AsyncRenderer>
); );
}; };
@@ -5,7 +5,7 @@ import { BrowserRouter as Router } from 'react-router-dom';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { RejectedProposalsList } from './rejected-proposals-list'; import { RejectedProposalsList } from './rejected-proposals-list';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { render, screen, within } from '@testing-library/react'; import { render, screen, waitFor, within } from '@testing-library/react';
import { import {
mockWalletContext, mockWalletContext,
networkParamsQueryMock, networkParamsQueryMock,
@@ -55,24 +55,38 @@ afterAll(() => {
jest.useRealTimers(); jest.useRealTimers();
}); });
jest.mock('../vote-details/use-user-vote', () => ({
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
}));
describe('Rejected proposals list', () => { describe('Rejected proposals list', () => {
it('Renders a list of proposals', () => { it('Renders a list of proposals', async () => {
render( render(
renderComponent([ renderComponent([
rejectedProposalClosedLastMonth, rejectedProposalClosedLastMonth,
rejectedProposalClosesNextWeek, rejectedProposalClosesNextWeek,
]) ])
); );
const rejectedProposals = within(screen.getByTestId('rejected-proposals'));
const rejectedProposalsItems = rejectedProposals.getAllByTestId( await waitFor(() => {
'proposals-list-item' const rejectedProposals = within(
); screen.getByTestId('rejected-proposals')
expect(rejectedProposalsItems).toHaveLength(2); );
const rejectedProposalsItems = rejectedProposals.getAllByTestId(
'proposals-list-item'
);
expect(rejectedProposalsItems).toHaveLength(2);
});
}); });
it('Displays text when there are no proposals', () => { it('Displays text when there are no proposals', async () => {
render(renderComponent([])); render(renderComponent([]));
expect(screen.queryByTestId('rejected-proposals')).not.toBeInTheDocument();
expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument(); await waitFor(() => {
expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument();
expect(
screen.queryByTestId('rejected-proposals')
).not.toBeInTheDocument();
});
}); });
}); });
@@ -1,10 +1,15 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { Heading } from '../../../../components/heading'; import { Heading } from '../../../../components/heading';
import { ProposalsListItem } from '../proposals-list-item'; import { ProposalsListItem } from '../proposals-list-item';
import { ProposalsListFilter } from '../proposals-list-filter'; import { ProposalsListFilter } from '../proposals-list-filter';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
interface ProposalsListProps { interface ProposalsListProps {
proposals: Array<ProposalQuery['proposal'] | ProposalFieldsFragment>; proposals: Array<ProposalQuery['proposal'] | ProposalFieldsFragment>;
@@ -12,6 +17,19 @@ interface ProposalsListProps {
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => { export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const {
params: networkParams,
loading: networkParamsLoading,
error: networkParamsError,
} = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
NetworkParams.governance_proposal_asset_requiredMajority,
NetworkParams.governance_proposal_updateAsset_requiredMajority,
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
NetworkParams.governance_proposal_freeform_requiredMajority,
]);
const [filterString, setFilterString] = useState(''); const [filterString, setFilterString] = useState('');
const filterPredicate = ( const filterPredicate = (
@@ -21,7 +39,11 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
p?.party?.id?.toString().includes(filterString); p?.party?.id?.toString().includes(filterString);
return ( return (
<> <AsyncRenderer
loading={networkParamsLoading}
error={networkParamsError}
data={networkParams}
>
<Heading title={t('pageTitleRejectedProposals')} /> <Heading title={t('pageTitleRejectedProposals')} />
<ProposalsListFilter <ProposalsListFilter
filterString={filterString} filterString={filterString}
@@ -31,7 +53,11 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
{proposals.length > 0 ? ( {proposals.length > 0 ? (
<ul data-testid="rejected-proposals"> <ul data-testid="rejected-proposals">
{proposals.filter(filterPredicate).map((proposal) => ( {proposals.filter(filterPredicate).map((proposal) => (
<ProposalsListItem key={proposal?.id} proposal={proposal} /> <ProposalsListItem
key={proposal?.id}
proposal={proposal}
networkParams={networkParams}
/>
))} ))}
</ul> </ul>
) : ( ) : (
@@ -40,6 +66,6 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
</p> </p>
)} )}
</section> </section>
</> </AsyncRenderer>
); );
}; };
@@ -4,16 +4,20 @@ import { render } from '@testing-library/react';
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types'; import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
import { ProtocolUpgradeProposalDetailInfo } from './protocol-upgrade-proposal-detail-info'; import { ProtocolUpgradeProposalDetailInfo } from './protocol-upgrade-proposal-detail-info';
const renderComponent = () => const PROPOSAL = {
vegaReleaseTag: 'v0.1.234',
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
upgradeBlockHeight: '12345',
approvers: [],
};
const renderComponent = (lastBlockHeight?: string, time?: string) =>
render( render(
<ProtocolUpgradeProposalDetailInfo <ProtocolUpgradeProposalDetailInfo
proposal={{ proposal={PROPOSAL}
vegaReleaseTag: 'v0.1.234', lastBlockHeight={lastBlockHeight}
status: time={time}
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
upgradeBlockHeight: '12345',
approvers: [],
}}
/> />
); );
@@ -43,4 +47,28 @@ describe('ProtocolUpgradeProposalDetailInfo', () => {
'v0.1.234' 'v0.1.234'
); );
}); });
it('should display estimated time if proposal is still pending', () => {
const { getByTestId } = renderComponent('12', 'time');
expect(getByTestId('protocol-upgrade-time-label')).toHaveTextContent(
'Estimated time to upgrade'
);
expect(getByTestId('protocol-upgrade-time')).toHaveTextContent('time');
});
it('should display upgraded at if proposal is done', () => {
const { getByTestId } = renderComponent('123456', 'time');
expect(getByTestId('protocol-upgrade-time-label')).toHaveTextContent(
'Upgraded at'
);
expect(getByTestId('protocol-upgrade-time')).toHaveTextContent('time');
});
it('should not display time if none provided', () => {
const { queryByTestId } = renderComponent('123456', undefined);
expect(
queryByTestId('protocol-upgrade-time-label')
).not.toBeInTheDocument();
expect(queryByTestId('protocol-upgrade-time')).not.toBeInTheDocument();
});
}); });
@@ -5,19 +5,33 @@ import {
RoundedWrapper, RoundedWrapper,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals'; import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import * as Types from '@vegaprotocol/types';
import type { ReactNode } from 'react';
export interface ProtocolUpgradeProposalDetailInfoProps { export interface ProtocolUpgradeProposalDetailInfoProps {
proposal: ProtocolUpgradeProposalFieldsFragment; proposal: ProtocolUpgradeProposalFieldsFragment;
lastBlockHeight?: string; lastBlockHeight?: string;
time?: string | ReactNode;
} }
export const ProtocolUpgradeProposalDetailInfo = ({ export const ProtocolUpgradeProposalDetailInfo = ({
proposal, proposal,
lastBlockHeight, lastBlockHeight,
time,
}: ProtocolUpgradeProposalDetailInfoProps) => { }: ProtocolUpgradeProposalDetailInfoProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const shouldShowUpgradeTime =
![
Types.ProtocolUpgradeProposalStatus
.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED,
Types.ProtocolUpgradeProposalStatus
.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED,
].includes(proposal.status) && time;
const pending = Number(proposal.upgradeBlockHeight) > Number(lastBlockHeight);
return ( return (
<div className="mb-10"> <div className="mb-10">
<SubHeading title={t('proposal')} /> <SubHeading title={t('proposal')} />
@@ -29,13 +43,26 @@ export const ProtocolUpgradeProposalDetailInfo = ({
<span data-testid="protocol-upgrade-block-height"> <span data-testid="protocol-upgrade-block-height">
{proposal.upgradeBlockHeight}{' '} {proposal.upgradeBlockHeight}{' '}
{lastBlockHeight && ( {lastBlockHeight && (
<> <span className="text-vega-light-300 dark:text-vega-dark-300">
({t('currently')} {lastBlockHeight}) ({t('currently')} {lastBlockHeight})
</> </span>
)} )}
</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
{shouldShowUpgradeTime && (
<KeyValueTableRow>
<span
data-testid="protocol-upgrade-time-label"
className="uppercase"
>
{pending ? t('Estimated time to upgrade') : t('Upgraded at')}
</span>
<span data-testid="protocol-upgrade-time">{time}</span>
</KeyValueTableRow>
)}
<KeyValueTableRow> <KeyValueTableRow>
<span className="uppercase">{t('state')}</span> <span className="uppercase">{t('state')}</span>
@@ -20,43 +20,6 @@ const renderComponent = (proposal: ProtocolUpgradeProposalFieldsFragment) =>
); );
describe('ProtocolUpgradeProposalsListItem', () => { describe('ProtocolUpgradeProposalsListItem', () => {
it('renders the correct status icon for each proposal status', () => {
const statuses = [
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED,
icon: 'protocol-upgrade-proposal-status-icon-rejected',
text: 'Declined by validators',
},
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
icon: 'protocol-upgrade-proposal-status-icon-pending',
text: 'Waiting for validator votes',
},
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED,
icon: 'protocol-upgrade-proposal-status-icon-approved',
text: 'Approved by validators',
},
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED,
icon: 'protocol-upgrade-proposal-status-icon-unspecified',
text: 'Unspecified',
},
];
statuses.forEach(({ status, icon, text }) => {
renderComponent({ ...proposal, status });
const statusIcon = screen.getByTestId(icon);
const textContent = screen.getByText(text);
expect(statusIcon).toBeInTheDocument();
expect(textContent).toBeInTheDocument();
});
});
it('renders the correct Vega release tag', () => { it('renders the correct Vega release tag', () => {
renderComponent(proposal); renderComponent(proposal);
const releaseTag = screen.getByTestId( const releaseTag = screen.getByTestId(
@@ -1,17 +1,10 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { import { Button, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
Button,
Icon,
Lozenge,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { stripFullStops } from '@vegaprotocol/utils'; import { stripFullStops } from '@vegaprotocol/utils';
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import { ProposalInfoLabel } from '../proposal-info-label'; import { ProposalInfoLabel } from '../proposal-info-label';
import Routes from '../../../routes'; import Routes from '../../../routes';
import type { ReactNode } from 'react';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals'; import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
interface ProtocolProposalsListItemProps { interface ProtocolProposalsListItemProps {
@@ -24,45 +17,21 @@ export const ProtocolUpgradeProposalsListItem = ({
const { t } = useTranslation(); const { t } = useTranslation();
if (!proposal || !proposal.upgradeBlockHeight) return null; if (!proposal || !proposal.upgradeBlockHeight) return null;
let proposalStatusIcon: ReactNode;
switch (proposal.status) {
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED:
proposalStatusIcon = (
<span data-testid="protocol-upgrade-proposal-status-icon-rejected">
<Icon name={'cross'} />
</span>
);
break;
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING:
proposalStatusIcon = (
<span data-testid="protocol-upgrade-proposal-status-icon-pending">
<Icon name={'time'} />
</span>
);
break;
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED:
proposalStatusIcon = (
<span data-testid="protocol-upgrade-proposal-status-icon-approved">
<Icon name={'tick'} />
</span>
);
break;
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED:
proposalStatusIcon = (
<span data-testid="protocol-upgrade-proposal-status-icon-unspecified">
<Icon name={'disable'} />
</span>
);
break;
}
return ( return (
<li <li
id={proposal.upgradeBlockHeight} id={proposal.upgradeBlockHeight}
data-testid="protocol-upgrade-proposals-list-item" data-testid="protocol-upgrade-proposals-list-item"
> >
<RoundedWrapper paddingBottom={true} heightFull={true}> <RoundedWrapper paddingBottom={true} heightFull={true}>
<div
data-testid="protocol-upgrade-proposal-type"
className="flex items-center gap-2 mb-4 text-sm"
>
<ProposalInfoLabel variant="highlight">
{t('networkUpgrade')}
</ProposalInfoLabel>
</div>
<div <div
data-testid="protocol-upgrade-proposal-title" data-testid="protocol-upgrade-proposal-title"
className="text-sm mb-2" className="text-sm mb-2"
@@ -70,30 +39,13 @@ export const ProtocolUpgradeProposalsListItem = ({
<SubHeading title={`Vega release ${proposal.vegaReleaseTag}`} /> <SubHeading title={`Vega release ${proposal.vegaReleaseTag}`} />
</div> </div>
<div className="text-sm"> <div className="flex items-center gap-4 text-vega-light-200">
<div className="flex gap-2">
<div
data-testid="protocol-upgrade-proposal-type"
className="flex items-center gap-2 mb-4"
>
<ProposalInfoLabel variant="highlight">
{t('networkUpgrade')}
</ProposalInfoLabel>
</div>
<div data-testid="protocol-upgrade-proposal-status">
<ProposalInfoLabel>
{t(`${proposal.status}`)} {proposalStatusIcon}
</ProposalInfoLabel>
</div>
</div>
<div <div
data-testid="protocol-upgrade-proposal-release-tag" data-testid="protocol-upgrade-proposal-release-tag"
className="mb-2" className="mb-2"
> >
<span>{t('vegaReleaseTag')}:</span>{' '} <span>{t('vegaReleaseTag')}:</span>{' '}
<Lozenge>{proposal.vegaReleaseTag}</Lozenge> <span>{proposal.vegaReleaseTag}</span>
</div> </div>
<div <div
@@ -101,21 +53,24 @@ export const ProtocolUpgradeProposalsListItem = ({
className="mb-2" className="mb-2"
> >
<span>{t('upgradeBlockHeight')}:</span>{' '} <span>{t('upgradeBlockHeight')}:</span>{' '}
<Lozenge>{proposal.upgradeBlockHeight}</Lozenge> <span>{proposal.upgradeBlockHeight}</span>
</div>
<div className="grid grid-cols-1 mt-3">
<div className="justify-self-end">
<Link
to={`${Routes.PROTOCOL_UPGRADES}/${stripFullStops(
proposal.vegaReleaseTag
)}`}
>
<Button data-testid="view-proposal-btn">{t('View')}</Button>
</Link>
</div>
</div> </div>
</div> </div>
<div
className="text-sm text-vega-light-300 mb-4"
data-testid="protocol-upgrade-proposal-status"
>
{t(`${proposal.status}`)}
</div>
<Link
to={`${Routes.PROTOCOL_UPGRADES}/${stripFullStops(
proposal.vegaReleaseTag
)}`}
>
<Button data-testid="view-proposal-btn">{t('viewDetails')}</Button>
</Link>
</RoundedWrapper> </RoundedWrapper>
</li> </li>
); );
@@ -24,8 +24,8 @@ interface VoteButtonsContainerProps {
voteDatetime: Date | null; voteDatetime: Date | null;
proposalId: string | null; proposalId: string | null;
proposalState: ProposalState; proposalState: ProposalState;
minVoterBalance: string | null; minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null; spamProtectionMinTokens: string | null | undefined;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>; submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
dialog: (props: DialogProps) => JSX.Element; dialog: (props: DialogProps) => JSX.Element;
className?: string; className?: string;
@@ -17,8 +17,8 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
interface VoteDetailsProps { interface VoteDetailsProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal']; proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
minVoterBalance: string | null; minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null; spamProtectionMinTokens: string | null | undefined;
proposalType: ProposalType | null; proposalType: ProposalType | null;
} }
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Proposal } from '../components/proposal'; import { Proposal } from '../components/proposal';
@@ -10,12 +10,43 @@ import { ENV } from '../../../config';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketInfoWithDataProvider } from '@vegaprotocol/markets'; import { marketInfoWithDataProvider } from '@vegaprotocol/markets';
import { useAssetQuery } from '@vegaprotocol/assets'; import { useAssetQuery } from '@vegaprotocol/assets';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
export const ProposalContainer = () => { export const ProposalContainer = () => {
const [
mostRecentlyEnactedAssociatedMarketProposal,
setMostRecentlyEnactedAssociatedMarketProposal,
] = useState(undefined);
const params = useParams<{ proposalId: string }>(); const params = useParams<{ proposalId: string }>();
const { const {
state: { data: restData }, params: networkParams,
loading: networkParamsLoading,
error: networkParamsError,
} = useNetworkParams([
NetworkParams.governance_proposal_market_minVoterBalance,
NetworkParams.governance_proposal_updateMarket_minVoterBalance,
NetworkParams.governance_proposal_asset_minVoterBalance,
NetworkParams.governance_proposal_updateAsset_minVoterBalance,
NetworkParams.governance_proposal_updateNetParam_minVoterBalance,
NetworkParams.governance_proposal_freeform_minVoterBalance,
NetworkParams.spam_protection_voting_min_tokens,
NetworkParams.governance_proposal_market_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
NetworkParams.governance_proposal_asset_requiredMajority,
NetworkParams.governance_proposal_updateAsset_requiredMajority,
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
NetworkParams.governance_proposal_freeform_requiredMajority,
]);
const {
state: { data: restData, loading: restLoading, error: restError },
} = useFetch(`${ENV.rest}governance?proposalId=${params.proposalId}`); } = useFetch(`${ENV.rest}governance?proposalId=${params.proposalId}`);
const { data, loading, error, refetch } = useProposalQuery({ const { data, loading, error, refetch } = useProposalQuery({
fetchPolicy: 'network-only', fetchPolicy: 'network-only',
errorPolicy: 'ignore', errorPolicy: 'ignore',
@@ -23,6 +54,35 @@ export const ProposalContainer = () => {
skip: !params.proposalId, skip: !params.proposalId,
}); });
const {
state: {
data: originalMarketProposalRestData,
loading: originalMarketProposalRestLoading,
error: originalMarketProposalRestError,
},
} = useFetch(
`${ENV.rest}governance?proposalId=${
data?.proposal?.terms.change.__typename === 'UpdateMarket' &&
data?.proposal.terms.change.marketId
}`,
undefined,
true,
data?.proposal?.terms.change.__typename !== 'UpdateMarket'
);
const {
state: {
data: previouslyEnactedMarketProposalsRestData,
loading: previouslyEnactedMarketProposalsRestLoading,
error: previouslyEnactedMarketProposalsRestError,
},
} = useFetch(
`${ENV.rest}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`,
undefined,
true,
data?.proposal?.terms.change.__typename !== 'UpdateMarket'
);
const { const {
data: newMarketData, data: newMarketData,
loading: newMarketLoading, loading: newMarketLoading,
@@ -55,6 +115,39 @@ export const ProposalContainer = () => {
), ),
}); });
useEffect(() => {
if (
previouslyEnactedMarketProposalsRestData &&
data?.proposal?.terms.change.__typename === 'UpdateMarket'
) {
const change = data?.proposal?.terms?.change as { marketId: string };
const filteredProposals =
// @ts-ignore rest data is not typed
previouslyEnactedMarketProposalsRestData.connection.edges.filter(
// @ts-ignore rest data is not typed
({ node }) =>
node?.proposal?.terms?.updateMarket?.marketId === change.marketId
);
const sortedProposals = filteredProposals.sort(
// @ts-ignore rest data is not typed
(a, b) =>
new Date(a?.node?.terms?.enactmentTimestamp).getTime() -
new Date(b?.node?.terms?.enactmentTimestamp).getTime()
);
setMostRecentlyEnactedAssociatedMarketProposal(
sortedProposals[sortedProposals.length - 1]
);
}
}, [
previouslyEnactedMarketProposalsRestData,
params.proposalId,
data?.proposal?.terms.change.__typename,
data?.proposal?.terms.change,
]);
useEffect(() => { useEffect(() => {
const interval = setInterval(refetch, 2000); const interval = setInterval(refetch, 2000);
return () => clearInterval(interval); return () => clearInterval(interval);
@@ -62,20 +155,53 @@ export const ProposalContainer = () => {
return ( return (
<AsyncRenderer <AsyncRenderer
loading={loading || newMarketLoading || assetLoading} loading={
error={error || newMarketError || assetError} loading ||
newMarketLoading ||
assetLoading ||
networkParamsLoading ||
(restLoading ? (restLoading as boolean) : false) ||
(originalMarketProposalRestLoading
? (originalMarketProposalRestLoading as boolean)
: false) ||
(previouslyEnactedMarketProposalsRestLoading
? (previouslyEnactedMarketProposalsRestLoading as boolean)
: false)
}
error={
error ||
newMarketError ||
assetError ||
restError ||
originalMarketProposalRestError ||
previouslyEnactedMarketProposalsRestError ||
networkParamsError
}
data={{ data={{
...data, ...data,
...networkParams,
...(newMarketData ? { newMarketData } : {}), ...(newMarketData ? { newMarketData } : {}),
...(assetData ? { assetData } : {}), ...(assetData ? { assetData } : {}),
...(restData ? { restData } : {}),
...(originalMarketProposalRestData
? { originalMarketProposalRestData }
: {}),
...(previouslyEnactedMarketProposalsRestData
? { previouslyEnactedMarketProposalsRestData }
: {}),
}} }}
> >
{data?.proposal ? ( {data?.proposal ? (
<Proposal <Proposal
proposal={data.proposal} proposal={data.proposal}
networkParams={networkParams}
restData={restData} restData={restData}
newMarketData={newMarketData} newMarketData={newMarketData}
assetData={assetData} assetData={assetData}
originalMarketProposalRestData={originalMarketProposalRestData}
mostRecentlyEnactedAssociatedMarketProposal={
mostRecentlyEnactedAssociatedMarketProposal
}
/> />
) : ( ) : (
<ProposalNotFound /> <ProposalNotFound />
@@ -3,12 +3,12 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client'; import * as Apollo from '@apollo/client';
const defaultOptions = {} as const; const defaultOptions = {} as const;
export type ProposalFieldsFragment = { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null } } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } }; export type ProposalFieldsFragment = { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null } } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } };
export type ProposalsQueryVariables = Types.Exact<{ [key: string]: never; }>; export type ProposalsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null } } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null }; export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null } } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null };
export const ProposalFieldsFragmentDoc = gql` export const ProposalFieldsFragmentDoc = gql`
fragment ProposalFields on Proposal { fragment ProposalFields on Proposal {
@@ -1,14 +1,23 @@
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { removePaginationWrapper, stripFullStops } from '@vegaprotocol/utils'; import {
convertToCountdownString,
formatDateWithLocalTimezone,
removePaginationWrapper,
stripFullStops,
} from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { ProtocolUpgradeProposal } from './protocol-upgrade-proposal'; import { ProtocolUpgradeProposal } from './protocol-upgrade-proposal';
import { ProposalNotFound } from '../components/proposal-not-found'; import { ProposalNotFound } from '../components/proposal-not-found';
import { useNodesQuery } from '../../staking/home/__generated__/Nodes'; import { useNodesQuery } from '../../staking/home/__generated__/Nodes';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch'; import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { useProtocolUpgradeProposalsQuery } from '@vegaprotocol/proposals'; import {
useProtocolUpgradeProposalsQuery,
useTimeToUpgrade,
} from '@vegaprotocol/proposals';
import { useBlockInfo } from '@vegaprotocol/tendermint';
export const ProtocolUpgradeProposalContainer = () => { export const ProtocolUpgradeProposalContainer = () => {
const params = useParams<{ proposalReleaseTag: string }>(); const params = useParams<{ proposalReleaseTag: string }>();
@@ -58,6 +67,20 @@ export const ProtocolUpgradeProposalContainer = () => {
); );
}, [nodesData]); }, [nodesData]);
const pending =
Number(protocolUpgradeProposal?.upgradeBlockHeight) >
Number(data?.lastBlockHeight);
const {
state: { data: blockInfo },
} = useBlockInfo(
Number(protocolUpgradeProposal?.upgradeBlockHeight),
!pending
);
const time = useTimeToUpgrade(
Number(protocolUpgradeProposal?.upgradeBlockHeight)
);
return ( return (
<AsyncRenderer <AsyncRenderer
loading={loading || nodesLoading} loading={loading || nodesLoading}
@@ -69,6 +92,19 @@ export const ProtocolUpgradeProposalContainer = () => {
proposal={protocolUpgradeProposal} proposal={protocolUpgradeProposal}
lastBlockHeight={data?.lastBlockHeight} lastBlockHeight={data?.lastBlockHeight}
consensusValidators={consensusValidators} consensusValidators={consensusValidators}
time={
pending && time ? (
convertToCountdownString(time, '0:00:00:00')
) : blockInfo?.result ? (
<span title={blockInfo.result.block.header.time}>
{formatDateWithLocalTimezone(
new Date(blockInfo.result.block.header.time)
)}
</span>
) : (
'-'
)
}
/> />
) : ( ) : (
<ProposalNotFound /> <ProposalNotFound />
@@ -60,6 +60,11 @@ const mockConsensusValidators: NodesFragmentFragment[] = [
}, },
]; ];
jest.mock('@vegaprotocol/environment', () => ({
useVegaRelease: jest.fn(),
useVegaReleases: jest.fn(),
}));
const renderComponent = () => const renderComponent = () =>
render( render(
<ProtocolUpgradeProposal proposal={mockProposal} consensusValidators={[]} /> <ProtocolUpgradeProposal proposal={mockProposal} consensusValidators={[]} />
@@ -1,10 +1,11 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import type { ReactNode } from 'react';
import { ProtocolUpgradeProposalDetailHeader } from '../components/protocol-upgrade-proposal-detail-header'; import { ProtocolUpgradeProposalDetailHeader } from '../components/protocol-upgrade-proposal-detail-header';
import { ProtocolUpdateProposalDetailApprovals } from '../components/protocol-upgrade-proposal-detail-approvals'; import { ProtocolUpdateProposalDetailApprovals } from '../components/protocol-upgrade-proposal-detail-approvals';
import { ProtocolUpgradeProposalDetailInfo } from '../components/protocol-upgrade-proposal-detail-info'; import { ProtocolUpgradeProposalDetailInfo } from '../components/protocol-upgrade-proposal-detail-info';
import { getNormalisedVotingPower } from '../../staking/shared'; import { getNormalisedVotingPower } from '../../staking/shared';
import type { NodesFragmentFragment } from '../../staking/home/__generated__/Nodes'; import type { NodesFragmentFragment } from '../../staking/home/__generated__/Nodes';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals'; import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { useVegaRelease } from '@vegaprotocol/environment'; import { useVegaRelease } from '@vegaprotocol/environment';
import { ExternalLink } from '@vegaprotocol/ui-toolkit'; import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -13,6 +14,7 @@ export interface ProtocolUpgradeProposalProps {
proposal: ProtocolUpgradeProposalFieldsFragment; proposal: ProtocolUpgradeProposalFieldsFragment;
consensusValidators: NodesFragmentFragment[] | null; consensusValidators: NodesFragmentFragment[] | null;
lastBlockHeight?: string; lastBlockHeight?: string;
time?: string | ReactNode;
} }
export const getConsensusApprovals = ( export const getConsensusApprovals = (
@@ -44,6 +46,7 @@ export const ProtocolUpgradeProposal = ({
proposal, proposal,
lastBlockHeight, lastBlockHeight,
consensusValidators, consensusValidators,
time,
}: ProtocolUpgradeProposalProps) => { }: ProtocolUpgradeProposalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const releaseInfo = useVegaRelease(proposal.vegaReleaseTag); const releaseInfo = useVegaRelease(proposal.vegaReleaseTag);
@@ -73,6 +76,7 @@ export const ProtocolUpgradeProposal = ({
<ProtocolUpgradeProposalDetailInfo <ProtocolUpgradeProposalDetailInfo
proposal={proposal} proposal={proposal}
lastBlockHeight={lastBlockHeight} lastBlockHeight={lastBlockHeight}
time={time}
/> />
{consensusValidators && consensusApprovals && ( {consensusValidators && consensusApprovals && (
@@ -2,6 +2,10 @@ import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import type { MockedResponse } from '@apollo/client/testing'; import type { MockedResponse } from '@apollo/client/testing';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import type { PubKey } from '@vegaprotocol/wallet'; import type { PubKey } from '@vegaprotocol/wallet';
import type { VoteValue } from '@vegaprotocol/types';
import type { UserVoteQuery } from '../components/vote-details/__generated__/Vote';
import { UserVoteDocument } from '../components/vote-details/__generated__/Vote';
import faker from 'faker';
export const mockPubkey: PubKey = { export const mockPubkey: PubKey = {
publicKey: '0x123', publicKey: '0x123',
@@ -49,6 +53,16 @@ export const networkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
}, },
}; };
export const mockNetworkParams = {
governance_proposal_asset_requiredMajority: '0.66',
governance_proposal_freeform_requiredMajority: '0.66',
governance_proposal_market_requiredMajority: '0.66',
governance_proposal_updateAsset_requiredMajority: '0.66',
governance_proposal_updateMarket_requiredMajority: '0.66',
governance_proposal_updateMarket_requiredMajorityLP: '0.66',
governance_proposal_updateNetParam_requiredMajority: '0.5',
};
const oneMinute = 1000 * 60; const oneMinute = 1000 * 60;
const oneHour = oneMinute * 60; const oneHour = oneMinute * 60;
const oneDay = oneHour * 24; const oneDay = oneHour * 24;
@@ -62,3 +76,34 @@ export const lastWeek = new Date(-oneWeek);
export const nextWeek = new Date(oneWeek); export const nextWeek = new Date(oneWeek);
export const lastMonth = new Date(-oneMonth); export const lastMonth = new Date(-oneMonth);
export const nextMonth = new Date(oneMonth); export const nextMonth = new Date(oneMonth);
export const createUserVoteQueryMock = (
proposalId: string,
value: VoteValue
): MockedResponse<UserVoteQuery> => ({
request: {
query: UserVoteDocument,
variables: {
partyId: mockPubkey.publicKey,
},
},
result: {
data: {
party: {
votesConnection: {
edges: [
{
node: {
proposalId,
vote: {
value,
datetime: faker.date.past().toISOString(),
},
},
},
],
},
},
},
},
});
@@ -216,7 +216,7 @@ export const StakingForm = ({
text: t('associateVegaNow'), text: t('associateVegaNow'),
action: () => navigate(Routes.ASSOCIATE), action: () => navigate(Routes.ASSOCIATE),
className: 'py-1', className: 'py-1',
size: 'sm', size: 'small',
}} }}
/> />
)} )}
+30
View File
@@ -97,3 +97,33 @@
.react-markdown-container ul li { .react-markdown-container ul li {
list-style: circle; list-style: circle;
} }
.jsondiffpatch-delta,
.jsondiffpatch-delta pre {
font-family: 'Roboto Mono', monospace !important;
}
.jsondiffpatch-delta pre {
padding: 0 0.25em !important;
}
.jsondiffpatch-added .jsondiffpatch-property-name,
.jsondiffpatch-added .jsondiffpatch-value pre,
.jsondiffpatch-modified .jsondiffpatch-right-value pre,
.jsondiffpatch-textdiff-added {
background: theme(colors.vega.green[650]) !important;
color: theme(colors.white) !important;
}
.jsondiffpatch-deleted .jsondiffpatch-property-name,
.jsondiffpatch-deleted pre,
.jsondiffpatch-modified .jsondiffpatch-left-value pre,
.jsondiffpatch-textdiff-deleted {
background: theme(colors.vega.pink[650]) !important;
color: theme(colors.white) !important;
}
.jsondiffpatch-moved .jsondiffpatch-moved-destination {
background: theme(colors.vega.yellow[350]) !important;
color: theme(colors.vega.dark[200]) !important;
}
+4
View File
@@ -33,3 +33,7 @@ CYPRESS_VEGA_TOKEN_URL=https://governance.fairground.wtf
CYPRESS_VEGA_URL=http://localhost:3008/graphql CYPRESS_VEGA_URL=http://localhost:3008/graphql
CYPRESS_VEGA_WALLET_URL=http://localhost:1789 CYPRESS_VEGA_WALLET_URL=http://localhost:1789
CYPRESS_VEGA_WALLET_API_TOKEN= CYPRESS_VEGA_WALLET_API_TOKEN=
# Cosmic elevator flags (MUST be doubled with CYPRESS_ prefix)
NX_SUCCESSOR_MARKETS=true
CYPRESS_NX_SUCCESSOR_MARKETS=true
+11 -9
View File
@@ -54,7 +54,7 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
it('can deposit', function () { it('can deposit', function () {
cy.visit('/#/portfolio'); cy.visit('/#/portfolio');
cy.get('main[data-testid="/portfolio"]').should('exist'); cy.get('[data-testid="pathname-/portfolio"]').should('exist');
// 1001-DEPO-001 // 1001-DEPO-001
// 1001-DEPO-002 // 1001-DEPO-002
@@ -117,10 +117,10 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
it('can key to key transfers', function () { it('can key to key transfers', function () {
// 1003-TRAN-023 // 1003-TRAN-023
// 1003-TRAN-006 // 1003-TRAN-006
cy.get('main[data-testid="/portfolio"]').should('exist'); cy.get('[data-testid="pathname-/portfolio"]').should('exist');
cy.getByTestId(collateralTab).click(); cy.getByTestId(collateralTab).click();
cy.getByTestId('open-transfer-dialog').click(); cy.getByTestId('open-transfer').click();
cy.getByTestId('transfer-form').should('be.visible'); cy.getByTestId('transfer-form').should('be.visible');
cy.getByTestId('transfer-form').find('[name="toAddress"]').select(1); cy.getByTestId('transfer-form').find('[name="toAddress"]').select(1);
cy.get('select option') cy.get('select option')
@@ -187,19 +187,21 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
// 0006-NETW-010 // 0006-NETW-010
const market = this.market; const market = this.market;
cy.visit(`/#/markets/${market.id}`); cy.visit(`/#/markets/${market.id}`);
cy.getByTestId('node-health-trigger').realHover();
cy.getByTestId('node-health') cy.getByTestId('node-health')
.children() .children()
.first() .first()
.should('contain.text', 'Operational') .should('contain.text', 'Operational')
.next()
.should('contain.text', new URL(Cypress.env('VEGA_URL')).hostname)
.next()
.then(($el) => { .then(($el) => {
const blockHeight = parseInt($el.text()); const blockHeight = parseInt($el.text());
// block height will increase over the course of the test run so best // block height will increase over the course of the test run so best
// we can do here is check that its showing something sensible // we can do here is check that its showing something sensible
expect(blockHeight).to.be.greaterThan(0); expect(blockHeight).to.be.greaterThan(0);
}); });
cy.getByTestId('node-health')
.children()
.eq(1)
.should('contain.text', new URL(Cypress.env('VEGA_URL')).hostname);
}); });
it('can place and receive an order', function () { it('can place and receive an order', function () {
@@ -342,7 +344,7 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
const ethWalletAddress = Cypress.env('ETHEREUM_WALLET_ADDRESS'); const ethWalletAddress = Cypress.env('ETHEREUM_WALLET_ADDRESS');
cy.visit('/#/portfolio'); cy.visit('/#/portfolio');
cy.get('main[data-testid="/portfolio"]').should('exist'); cy.get('[data-testid="pathname-/portfolio"]').should('exist');
cy.getByTestId(toastCloseBtn, txTimeout).click(); cy.getByTestId(toastCloseBtn, txTimeout).click();
cy.getByTestId('Withdrawals').click(); cy.getByTestId('Withdrawals').click();
cy.getByTestId('withdraw-dialog-button').click(); cy.getByTestId('withdraw-dialog-button').click();
@@ -429,7 +431,7 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
// 1001-DEPO-006 // 1001-DEPO-006
// 1001-DEPO-007 // 1001-DEPO-007
cy.visit('/#/portfolio'); cy.visit('/#/portfolio');
cy.get('main[data-testid="/portfolio"]').should('exist'); cy.get('[data-testid="pathname-/portfolio"]').should('exist');
cy.getByTestId(toastCloseBtn, txTimeout).click(); cy.getByTestId(toastCloseBtn, txTimeout).click();
cy.getByTestId(depositsTab).click(); cy.getByTestId(depositsTab).click();
cy.getByTestId('deposit-button').click(); cy.getByTestId('deposit-button').click();
@@ -451,7 +453,7 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
// 1002-WITH-007 // 1002-WITH-007
cy.visit('/#/portfolio'); cy.visit('/#/portfolio');
cy.get('main[data-testid="/portfolio"]', txTimeout).should('exist'); cy.get('[data-testid="pathname-/portfolio"]', txTimeout).should('exist');
cy.getByTestId(toastCloseBtn, txTimeout).click(); cy.getByTestId(toastCloseBtn, txTimeout).click();
cy.getByTestId(depositsTab).click(); cy.getByTestId(depositsTab).click();
cy.getByTestId('deposit-button').click(); cy.getByTestId('deposit-button').click();
@@ -351,12 +351,6 @@ describe('Closed markets', { tags: '@smoke' }, () => {
) )
); );
// 6001-MARK-017
cy.get(rowSelector)
.first()
.find('[col-id="realisedPNL"]')
.should('have.text', '-');
// 6001-MARK-018 // 6001-MARK-018
cy.get(rowSelector) cy.get(rowSelector)
.first() .first()
@@ -369,6 +363,12 @@ describe('Closed markets', { tags: '@smoke' }, () => {
.first() .first()
.find('button svg') .find('button svg')
.should('exist'); .should('exist');
if (Cypress.env('NX_SUCCESSOR_MARKETS')) {
cy.get(rowSelector)
.find('[col-id="successorMarket"]')
.first()
.should('have.text', '-');
}
}); });
// test market list for market in terminated state // test market list for market in terminated state
@@ -17,11 +17,11 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.setVegaWallet(); cy.setVegaWallet();
cy.visit('/#/portfolio'); cy.visit('/#/portfolio');
cy.get('main[data-testid="/portfolio"]').should('exist'); cy.get('[data-testid="pathname-/portfolio"]').should('exist');
cy.getByTestId('Deposits').click(); cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click(); cy.getByTestId('deposit-button').click();
cy.wait('@Assets');
connectEthereumWallet('MetaMask'); connectEthereumWallet('MetaMask');
cy.wait('@Assets');
} }
before(() => { before(() => {
@@ -102,8 +102,6 @@ describe('deposit actions', { tags: '@smoke' }, () => {
cy.mockSubscription(); cy.mockSubscription();
cy.setVegaWallet(); cy.setVegaWallet();
cy.visit('/#/markets/market-1'); cy.visit('/#/markets/market-1');
cy.wait('@MarketsCandles');
cy.getByTestId('dialog-close').click();
}); });
it('Deposit to trade is visble', () => { it('Deposit to trade is visble', () => {
@@ -1,5 +1,6 @@
const dialogContent = 'dialog-content'; const dialogContent = 'dialog-content';
const nodeHealth = 'node-health'; const nodeHealth = 'node-health';
const nodeHealthTrigger = 'node-health-trigger';
describe('home', { tags: '@regression' }, () => { describe('home', { tags: '@regression' }, () => {
before(() => { before(() => {
@@ -8,22 +9,23 @@ describe('home', { tags: '@regression' }, () => {
cy.visit('/'); cy.visit('/');
}); });
describe('footer', () => { describe('node health', () => {
it('shows current block height', () => { it('shows current block height', () => {
// 0006-NETW-004 // 0006-NETW-004
// 0006-NETW-008 // 0006-NETW-008
// 0006-NETW-009 // 0006-NETW-009
cy.getByTestId(nodeHealthTrigger).realHover();
cy.getByTestId(nodeHealth) cy.getByTestId(nodeHealth)
.children() .children()
.first() .first()
.should('contain.text', 'Operational', { .should('contain.text', 'Operational', {
timeout: 10000, timeout: 10000,
}) })
.next()
.should('contain.text', new URL(Cypress.env('VEGA_URL')).hostname)
.next()
.should('contain.text', '100'); // all mocked queries have x-block-height header set to 100 .should('contain.text', '100'); // all mocked queries have x-block-height header set to 100
cy.getByTestId(nodeHealth)
.children()
.eq(1)
.should('contain.text', new URL(Cypress.env('VEGA_URL')).hostname);
}); });
it('shows node switcher details', () => { it('shows node switcher details', () => {
@@ -32,7 +34,7 @@ describe('home', { tags: '@regression' }, () => {
// 0006-NETW-014 // 0006-NETW-014
// 0006-NETW-015 // 0006-NETW-015
// 0006-NETW-016 // 0006-NETW-016
cy.getByTestId(nodeHealth).click(); cy.getByTestId(nodeHealthTrigger).click();
cy.getByTestId(dialogContent).should('contain.text', 'Connected node'); cy.getByTestId(dialogContent).should('contain.text', 'Connected node');
cy.getByTestId(dialogContent).should( cy.getByTestId(dialogContent).should(
'contain.text', 'contain.text',
@@ -56,7 +58,7 @@ describe('home', { tags: '@regression' }, () => {
// 0006-NETW-018 // 0006-NETW-018
// 0006-NETW-019 // 0006-NETW-019
// 0006-NETW-020 // 0006-NETW-020
cy.getByTestId(nodeHealth).click(); cy.getByTestId(nodeHealthTrigger).click();
cy.getByTestId('connect').should('be.disabled'); cy.getByTestId('connect').should('be.disabled');
cy.getByTestId('node-url-custom').click(); cy.getByTestId('node-url-custom').click();
cy.getByTestId('connect').should('be.disabled'); cy.getByTestId('connect').should('be.disabled');
+1 -1
View File
@@ -106,7 +106,7 @@ describe('home', { tags: '@regression' }, () => {
cy.visit('/'); cy.visit('/');
cy.wait('@Markets'); cy.wait('@Markets');
cy.get('main[data-testid^="/markets/"]'); cy.get('[data-testid^="pathname-/markets/"]');
// the choose market overlay is no longer showing // the choose market overlay is no longer showing
cy.contains('Loading...').should('not.exist'); cy.contains('Loading...').should('not.exist');
@@ -27,6 +27,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
'Description', 'Description',
'Trading mode', 'Trading mode',
'Status', 'Status',
'Successor market',
'Best bid', 'Best bid',
'Best offer', 'Best offer',
'Mark price', 'Mark price',
@@ -1,5 +1,6 @@
import { MarketTradingModeMapping } from '@vegaprotocol/types'; import { MarketTradingModeMapping } from '@vegaprotocol/types';
import { MarketState } from '@vegaprotocol/types'; import { MarketState } from '@vegaprotocol/types';
import compact from 'lodash/compact';
const accordionContent = 'accordion-content'; const accordionContent = 'accordion-content';
const blockExplorerLink = 'block-explorer-link'; const blockExplorerLink = 'block-explorer-link';
@@ -66,17 +67,24 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
// 6002-MDET-201 // 6002-MDET-201
cy.getByTestId(marketTitle).contains('Key details').click(); cy.getByTestId(marketTitle).contains('Key details').click();
validateMarketDataRow(0, 'Name', 'BTCUSD Monthly (30 Jun 2022)'); const rows: [string, string][] = compact([
validateMarketDataRow(1, 'Market ID', 'market-0'); ['Name', 'BTCUSD Monthly (30 Jun 2022)'],
validateMarketDataRow(2, 'Parent Market ID', 'market-1'); ['Market ID', 'market-0'],
validateMarketDataRow( Cypress.env('NX_SUCCESSOR_MARKETS') && ['Parent Market ID', 'PARENT-A'],
3, Cypress.env('NX_SUCCESSOR_MARKETS') && [
'Trading Mode', 'Insurance Pool Fraction',
MarketTradingModeMapping.TRADING_MODE_CONTINUOUS '0.75',
); ],
validateMarketDataRow(4, 'Market Decimal Places', '5'); ['Trading Mode', MarketTradingModeMapping.TRADING_MODE_CONTINUOUS],
validateMarketDataRow(5, 'Position Decimal Places', '0'); ['Market Decimal Places', '5'],
validateMarketDataRow(6, 'Settlement Asset Decimal Places', '5'); ['Position Decimal Places', '0'],
['Settlement Asset Decimal Places', '5'],
]);
for (const rowNumber in rows) {
const [name, value] = rows[rowNumber];
validateMarketDataRow(Number(rowNumber), name, value);
}
}); });
it('instrument displayed', () => { it('instrument displayed', () => {
@@ -132,9 +132,10 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
it('can see header title', () => { it('can see header title', () => {
// 5002-LIQP-004 // 5002-LIQP-004
// 5002-LIQP-005 // 5002-LIQP-005
cy.getByTestId('header-title') cy.getByTestId('header-title').should(
.should('contain.text', 'BTCUSD.MF21 liquidity provision') 'contain.text',
.and('contain.text', 'Go to trading'); 'BTCUSD.MF21 liquidity provision'
);
}); });
it('can see target stake', () => { it('can see target stake', () => {
@@ -171,7 +172,7 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
cy.getByTestId('liquidity-supplied').within(() => { cy.getByTestId('liquidity-supplied').within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Liquidity supplied'); cy.getByTestId(itemHeader).should('have.text', 'Liquidity supplied');
cy.getByTestId('indicator').should('be.visible'); cy.getByTestId('indicator').should('be.visible');
cy.getByTestId(itemValue).should('have.text', '0.10%').realHover(); cy.getByTestId(itemValue).should('have.text', ' 0.10%').realHover();
}); });
}); });
}); });
@@ -22,15 +22,12 @@ describe('markets selector', { tags: '@smoke' }, () => {
cy.wait('@Markets'); cy.wait('@Markets');
cy.wait('@MarketsData'); cy.wait('@MarketsData');
cy.wait('@MarketsCandles');
}); });
// 6001-MARK-066 // 6001-MARK-066
it('can toggle the sidebar', () => { it('can open popover to view markets', () => {
cy.getByTestId('market-selector').should('be.visible');
cy.getByTestId('sidebar-toggle').click();
cy.getByTestId('market-selector').should('not.exist'); cy.getByTestId('market-selector').should('not.exist');
cy.getByTestId('sidebar-toggle').click(); cy.getByTestId('header-title').should('be.visible').click();
cy.getByTestId('market-selector').should('be.visible'); cy.getByTestId('market-selector').should('be.visible');
}); });
@@ -40,29 +37,26 @@ describe('markets selector', { tags: '@smoke' }, () => {
const data = [ const data = [
{ {
code: 'SOLUSD', code: 'SOLUSD',
markPrice: '84.41XYZalpha', markPrice: '84.41',
change: '', vol: '0.00',
vol: '0.0024h vol',
}, },
{ {
code: 'ETHBTC.QM21', code: 'ETHBTC.QM21',
markPrice: '46,126.90058tBTC', markPrice: '46,126.90058',
change: '', vol: '0.00',
vol: '0.0024h vol',
}, },
{ {
code: 'BTCUSD.MF21', code: 'BTCUSD.MF21',
markPrice: '46,126.90058tDAI', markPrice: '46,126.90058',
change: '', vol: '0.00',
vol: '0.0024h vol',
}, },
{ {
code: 'AAPL.MF21', code: 'AAPL.MF21',
markPrice: '46,126.90058tUSDC', markPrice: '46,126.90058',
change: '', vol: '0.00',
vol: '0.0024h vol',
}, },
]; ];
cy.getByTestId('header-title').should('be.visible').click();
cy.getByTestId(list) cy.getByTestId(list)
.find('a') .find('a')
.each((item, i) => { .each((item, i) => {
@@ -71,33 +65,20 @@ describe('markets selector', { tags: '@smoke' }, () => {
// 6001-MARK-022 // 6001-MARK-022
expect(item.find('h3').text()).equals(market.code); expect(item.find('h3').text()).equals(market.code);
expect( expect(
item.find('[data-testid="market-selector-data-row"]').eq(0).text() item.find('[data-testid="market-selector-volume"]').text()
).contains(market.vol); ).contains(market.vol);
// 6001-MARK-024 // 6001-MARK-024
expect( expect(
item.find('[data-testid="market-selector-data-row"]').eq(1).text() item.find('[data-testid="market-selector-price"]').text()
).contains(market.markPrice); ).contains(market.markPrice);
// 6001-MARK-023
expect(item.find('[data-testid="market-item-change"]').text()).equals(
market.change
);
// 6001-MARK-025 // 6001-MARK-025
expect(item.find('[data-testid="sparkline-svg"]')).to.not.exist; expect(item.find('[data-testid="sparkline-svg"]')).to.not.exist;
}); });
}); });
it('can see all markets link', () => {
// 6001-MARK-026
cy.getByTestId('market-selector').within(() => {
cy.getByTestId('all-markets-link')
.should('be.visible')
.and('have.text', 'All markets')
.and('have.attr', 'href')
.and('contain', '#/markets/all');
});
});
it('can use the filter options', () => { it('can use the filter options', () => {
cy.getByTestId('header-title').should('be.visible').click();
// 6001-MARK-027 // 6001-MARK-027
// product type // product type
cy.getByTestId('product-Spot').click(); cy.getByTestId('product-Spot').click();
@@ -118,6 +99,8 @@ describe('markets selector', { tags: '@smoke' }, () => {
}); });
it('can sort by by top gaining and top losing market', () => { it('can sort by by top gaining and top losing market', () => {
cy.getByTestId('header-title').should('be.visible').click();
// 6001-MARK-030 // 6001-MARK-030
// 6001-MARK-031 // 6001-MARK-031
// 6001-MARK-032 // 6001-MARK-032
@@ -135,6 +118,8 @@ describe('markets selector', { tags: '@smoke' }, () => {
}); });
it('can filter by settlement asset', () => { it('can filter by settlement asset', () => {
cy.getByTestId('header-title').should('be.visible').click();
// 6001-MARK-028 // 6001-MARK-028
cy.getByTestId('asset-trigger').click(); cy.getByTestId('asset-trigger').click();
cy.getByTestId('asset-id-asset-3').contains('tBTC').click(); cy.getByTestId('asset-id-asset-3').contains('tBTC').click();
@@ -1,71 +0,0 @@
describe('market bottom panel', { tags: '@smoke' }, () => {
before(() => {
cy.clearAllLocalStorage();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@MarketData');
});
it('on xxl screen should be splitted out into two tables', () => {
cy.getByTestId('tab-positions').should('have.attr', 'data-state', 'active');
cy.getByTestId('tab-open-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-closed-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-rejected-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-orders').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-accounts').should(
'have.attr',
'data-state',
'inactive'
);
cy.viewport(1801, 1000);
cy.getByTestId('tab-positions').should('have.attr', 'data-state', 'active');
cy.getByTestId('tab-open-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-closed-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-rejected-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-orders').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-accounts').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('Fills').click();
cy.getByTestId('Collateral').click();
cy.getByTestId('tab-positions').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-orders').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'active');
cy.getByTestId('tab-accounts').should('have.attr', 'data-state', 'active');
});
});
@@ -19,6 +19,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
'Description', 'Description',
'Settlement asset', 'Settlement asset',
'State', 'State',
'Parent market',
'Voting', 'Voting',
'Closing date', 'Closing date',
'Enactment date', 'Enactment date',
@@ -99,9 +99,6 @@ describe('Navbar', { tags: '@smoke' }, () => {
cy.getByTestId('menu-drawer').should('not.be.visible'); cy.getByTestId('menu-drawer').should('not.be.visible');
cy.getByTestId('button-menu-drawer').click(); cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').should('be.visible'); cy.getByTestId('menu-drawer').should('be.visible');
cy.getByTestId('menu-drawer')
.find('[data-testid="Settings"]')
.should('be.visible');
cy.getByTestId('button-menu-drawer').click(); cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').should('not.be.visible'); cy.getByTestId('menu-drawer').should('not.be.visible');
}); });
@@ -1,14 +1,15 @@
const orderbookTab = 'Orderbook'; const orderbookTab = 'Orderbook';
const orderbookTable = 'tab-orderbook'; const orderbookTable = 'tab-orderbook';
const askPrice = 'price-9894585'; const askPrice = 'price-9894185';
const bidPrice = 'price-9889001'; const bidPrice = 'price-9889001';
const askVolume = 'ask-vol-9894585'; const askVolume = 'ask-vol-9894185';
const bidVolume = 'bid-vol-9889001'; const bidVolume = 'bid-vol-9889001';
const askCumulative = 'cumulative-vol-9894585'; const askCumulative = 'cumulative-vol-9894185';
const bidCumulative = 'cumulative-vol-9889001'; const bidCumulative = 'cumulative-vol-9889001';
const midPrice = 'middle-mark-price-4612690000'; const midPrice = 'middle-mark-price-4612690000';
const priceResolution = 'resolution'; const priceResolution = 'resolution';
const dealTicketPrice = 'order-price'; const dealTicketPrice = 'order-price';
const dealTicketSize = 'order-size';
const resPrice = 'price-990'; const resPrice = 'price-990';
describe('order book', { tags: '@smoke' }, () => { describe('order book', { tags: '@smoke' }, () => {
@@ -33,7 +34,7 @@ describe('order book', { tags: '@smoke' }, () => {
it('show orders prices', () => { it('show orders prices', () => {
// 6003-ORDB-003 // 6003-ORDB-003
cy.getByTestId(askPrice).should('have.text', '98.94585'); cy.getByTestId(askPrice).should('have.text', '98.94185');
cy.getByTestId(bidPrice).should('have.text', '98.89001'); cy.getByTestId(bidPrice).should('have.text', '98.89001');
}); });
@@ -45,7 +46,7 @@ describe('order book', { tags: '@smoke' }, () => {
it('show prices cumulative volumes', () => { it('show prices cumulative volumes', () => {
// 6003-ORDB-005 // 6003-ORDB-005
cy.getByTestId(askCumulative).should('have.text', '39'); cy.getByTestId(askCumulative).should('have.text', '38');
cy.getByTestId(bidCumulative).should('have.text', '7'); cy.getByTestId(bidCumulative).should('have.text', '7');
}); });
@@ -71,7 +72,19 @@ describe('order book', { tags: '@smoke' }, () => {
it('copy price to deal ticket form', () => { it('copy price to deal ticket form', () => {
// 6003-ORDB-009 // 6003-ORDB-009
cy.getByTestId(askPrice).click(); cy.getByTestId(askPrice).click();
cy.getByTestId(dealTicketPrice).should('have.value', '98.94585'); cy.getByTestId(dealTicketPrice).should('have.value', '98.94185');
});
it('copy size to deal ticket form', () => {
// 6003-ORDB-009
cy.getByTestId(bidCumulative).click();
cy.getByTestId(dealTicketSize).should('have.value', '7');
});
it('copy size to deal ticket form', () => {
// 6003-ORDB-009
cy.getByTestId(bidVolume).click();
cy.getByTestId(dealTicketSize).should('have.value', '1');
}); });
it('change price resolution', () => { it('change price resolution', () => {
@@ -88,13 +101,14 @@ describe('order book', { tags: '@smoke' }, () => {
'1,000', '1,000',
'10,000', '10,000',
]; ];
cy.getByTestId(priceResolution) cy.getByTestId(priceResolution).click();
.find('option') cy.get('[role="menu"]')
.find('[role="menuitem"]')
.each(($el, index) => { .each(($el, index) => {
expect($el.text()).to.equal(resolutions[index]); expect($el.text()).to.equal(resolutions[index]);
}); });
cy.getByTestId(priceResolution).select('0.0'); cy.get('[role="menuitem"]').eq(4).click();
cy.getByTestId(resPrice).should('have.text', '99.0'); cy.getByTestId(resPrice).should('have.text', '99.0');
cy.getByTestId(askPrice).should('not.exist'); cy.getByTestId(askPrice).should('not.exist');
cy.getByTestId(bidPrice).should('not.exist'); cy.getByTestId(bidPrice).should('not.exist');
+20 -33
View File
@@ -1,42 +1,29 @@
describe('Settings page', { tags: '@smoke' }, () => { describe('Settings page', { tags: '@smoke' }, () => {
beforeEach(() => { beforeEach(() => {
cy.clearLocalStorage().then(() => { cy.clearLocalStorage();
cy.mockTradingPage();
cy.mockSubscription(); cy.mockTradingPage();
cy.visit('/'); cy.mockSubscription();
cy.get('[aria-label="cog icon"]').click(); cy.visit('/');
// Only click if not already active otherwise sidebar will close
cy.get('[data-testid="sidebar-content"]').then(($sidebarContent) => {
if ($sidebarContent.find('h2').text() !== 'Settings') {
cy.get('[data-testid="sidebar"] [data-testid="Settings"]').click();
}
}); });
}); });
it('telemetry checkbox should work well', () => { it('telemetry checkbox should work well', () => {
cy.location('hash').should('equal', '#/settings'); const telemetrySwitch = '#switch-settings-telemetry-switch';
cy.getByTestId('telemetry-approval').should( cy.get(telemetrySwitch).should('have.attr', 'data-state', 'unchecked');
'have.attr', cy.get(telemetrySwitch).click();
'data-state', cy.get(telemetrySwitch).should('have.attr', 'data-state', 'checked');
'unchecked'
);
cy.get('[for="telemetry-approval"]').click();
cy.getByTestId('telemetry-approval').should(
'have.attr',
'data-state',
'checked'
);
cy.reload(); cy.reload();
cy.getByTestId('telemetry-approval').should( cy.get(telemetrySwitch).should('have.attr', 'data-state', 'checked');
'have.attr', cy.get(telemetrySwitch).click();
'data-state', cy.get(telemetrySwitch).should('have.attr', 'data-state', 'unchecked');
'checked'
);
cy.get('[for="telemetry-approval"]').click();
cy.getByTestId('telemetry-approval').should(
'have.attr',
'data-state',
'unchecked'
);
cy.reload(); cy.reload();
cy.getByTestId('telemetry-approval').should( cy.get(telemetrySwitch).should('have.attr', 'data-state', 'unchecked');
'have.attr',
'data-state',
'unchecked'
);
}); });
}); });
@@ -127,7 +127,7 @@ describe(
{ {
name: 'Price monitoring bounds', name: 'Price monitoring bounds',
infoText: infoText:
'Price Monitoring Bounds: Min 162.56291Max 182.96869Reference 172.47489', 'Price Monitoring Bounds 1: Min 162.56291Max 182.96869Reference 172.47489',
}, },
]; ];
@@ -14,6 +14,12 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.mockSubscription(); cy.mockSubscription();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
cy.wait('@Markets'); cy.wait('@Markets');
cy.get('[data-testid="deal-ticket-form"]').then(($form) => {
if (!$form.length) {
cy.getByTestId('Order').click();
}
});
}); });
beforeEach(() => { beforeEach(() => {
@@ -12,7 +12,7 @@ describe(
'account validation', 'account validation',
{ tags: '@regression', testIsolation: true }, { tags: '@regression', testIsolation: true },
() => { () => {
describe('zero balance error', () => { describe.skip('zero balance error', () => {
beforeEach(() => { beforeEach(() => {
cy.setVegaWallet(); cy.setVegaWallet();
cy.mockTradingPage(); cy.mockTradingPage();
@@ -59,6 +59,12 @@ describe(
cy.mockSubscription(); cy.mockSubscription();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
cy.wait('@Markets'); cy.wait('@Markets');
cy.get('[data-testid="deal-ticket-form"]').then(($form) => {
if (!$form.length) {
cy.getByTestId('Order').click();
}
});
}); });
it('should display info and button for deposit', () => { it('should display info and button for deposit', () => {
@@ -74,8 +80,8 @@ describe(
'You may not have enough margin available to open this position. 5.00 tDAI is currently required. You have only 0.01001 tDAI available.' 'You may not have enough margin available to open this position. 5.00 tDAI is currently required. You have only 0.01001 tDAI available.'
); );
cy.getByTestId('deal-ticket-deposit-dialog-button').click(); cy.getByTestId('deal-ticket-deposit-dialog-button').click();
cy.getByTestId('dialog-content') cy.getByTestId('sidebar-content')
.find('h1') .find('h2')
.eq(0) .eq(0)
.should('have.text', 'Deposit'); .should('have.text', 'Deposit');
}); });
@@ -37,7 +37,7 @@ describe('fills', { tags: '@regression' }, () => {
it('renders fills on portfolio page', () => { it('renders fills on portfolio page', () => {
cy.visit('/#/portfolio'); cy.visit('/#/portfolio');
cy.get('main[data-testid="/portfolio"]').should('exist'); cy.get('[data-testid="pathname-/portfolio"]').should('exist');
cy.getByTestId('Fills').click(); cy.getByTestId('Fills').click();
validateFillsDisplayed(); validateFillsDisplayed();
}); });
@@ -431,6 +431,8 @@ describe('amend and cancel order', { tags: '@smoke' }, () => {
}); });
const orderId = '1234567890'; const orderId = '1234567890';
// this test is flakey
it('must be able to amend the price of an order', () => { it('must be able to amend the price of an order', () => {
// 7003-MORD-007 // 7003-MORD-007
// 7003-MORD-012 // 7003-MORD-012

Some files were not shown because too many files have changed in this diff Show More