Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
835538067b | ||
|
|
8e6d8517cf | ||
|
|
5f9ec222c1 | ||
|
|
8954c41c0a | ||
|
|
4414ab4f47 | ||
|
|
d1d4bacc68 | ||
|
|
f476688c7f | ||
|
|
8845222700 | ||
|
|
e64c464091 | ||
|
|
b2c2d0d7d6 | ||
|
|
1fb61d313c | ||
|
|
ccf5ff632c | ||
|
|
54d6aaf56f | ||
|
|
736b262947 | ||
|
|
6a07127185 | ||
|
|
45f0ba7c0e | ||
|
|
6f4ea68778 | ||
|
|
90dda20891 | ||
|
|
278eb01c2f | ||
|
|
c3157de146 | ||
|
|
d28db485c5 | ||
|
|
caa0076396 | ||
|
|
451da40ed2 | ||
|
|
47694e7b72 | ||
|
|
e01fb7f9ae | ||
|
|
ffcdfb6a6a | ||
|
|
86f0d013bb | ||
|
|
c12ac45890 | ||
|
|
47ce68455a |
+2
-1
@@ -51,7 +51,8 @@
|
||||
"ul": ["list"],
|
||||
"ol": ["list"]
|
||||
}
|
||||
]
|
||||
],
|
||||
"no-console": ["error", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
after-release:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -30,18 +30,20 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Wait for publish to complete
|
||||
uses: lewagon/wait-on-check-action@v1.3.1
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
check-name: '(CD) publish dist / trading'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-interval: 10
|
||||
|
||||
- name: resolve ipfs hashes for release
|
||||
run: |
|
||||
echo "Name: ${{ github.event.release.name }}"
|
||||
echo "Description: ${{ github.event.release.body }}"
|
||||
echo "Tag: ${{ github.event.release.tag_name }}"
|
||||
commit="$(git rev-list -n 1 ${{ github.event.release.tag_name }})"
|
||||
echo "Commit: $commit"
|
||||
until docker pull vegaprotocol/trading:$commit; do
|
||||
echo "Image not pushed yet, waiting 60 seconds"
|
||||
sleep 60
|
||||
done
|
||||
docker run --rm vegaprotocol/trading:$commit cat /ipfs-hash > ipfs-hash
|
||||
docker run --rm vegaprotocol/trading:mainnet cat /ipfs-hash > ipfs-hash
|
||||
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
|
||||
tar -xzf kubo.tgz
|
||||
export PATH="$PATH:$PWD/kubo"
|
||||
|
||||
@@ -81,6 +81,22 @@ jobs:
|
||||
with:
|
||||
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
|
||||
run: yarn nx format:check
|
||||
|
||||
@@ -96,126 +112,6 @@ jobs:
|
||||
- name: Build affected
|
||||
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:
|
||||
projects: ${{ env.PROJECTS }}
|
||||
projects-e2e: ${{ env.PROJECTS_E2E }}
|
||||
@@ -224,6 +120,15 @@ jobs:
|
||||
preview_explorer: ${{ env.PREVIEW_EXPLORER }}
|
||||
preview_tools: ${{ env.PREVIEW_TOOLS }}
|
||||
|
||||
console-e2e:
|
||||
needs: lint-test-build
|
||||
name: '(CI) console python'
|
||||
uses: ./.github/workflows/console-test-run.yml
|
||||
secrets: inherit
|
||||
if: ${{ contains(fromJSON(needs.lint-test-build.outputs.projects), 'trading') && github.event_name == 'pull_request' }}
|
||||
with:
|
||||
github-sha: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
cypress:
|
||||
needs: lint-test-build
|
||||
name: '(CI) cypress'
|
||||
|
||||
@@ -1,55 +1,138 @@
|
||||
name: console-test-run
|
||||
name: (CI) Console tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
github-sha:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
console-test:
|
||||
timeout-minutes: 5
|
||||
runs-on: self-hosted-runner
|
||||
run-tests:
|
||||
name: run-tests
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
#----------------------------------------------
|
||||
# check-out frontend-monorepo
|
||||
#----------------------------------------------
|
||||
- name: Checkout console test repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ inputs.github-sha }}
|
||||
#----------------------------------------------
|
||||
# cache node modules
|
||||
#----------------------------------------------
|
||||
- name: Cache node modules
|
||||
id: cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
|
||||
# comment out "restore-keys" if you need to rebuild yarn from 0
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cache-node-modules-
|
||||
#----------------------------------------------
|
||||
# setup node
|
||||
#----------------------------------------------
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
|
||||
cache: yarn
|
||||
#----------------------------------------------
|
||||
# install deps if cache missing
|
||||
#----------------------------------------------
|
||||
- name: yarn install
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --pure-lockfile
|
||||
#----------------------------------------------
|
||||
# build trading
|
||||
#----------------------------------------------
|
||||
- name: Build affected spec
|
||||
run: |
|
||||
yarn env-cmd -f ./apps/trading/.env.stagnet1 yarn nx export trading
|
||||
#----------------------------------------------
|
||||
# run trading server
|
||||
#----------------------------------------------
|
||||
- name: Run trading server
|
||||
run: |
|
||||
docker run -d -p 80:4200 -v $PWD/docker/nginx.conf:/etc/nginx/conf.d/default.conf -v $PWD/dist/apps/trading/exported:/usr/share/nginx/html nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a116c34cc7241532d0d94198fb2c9629
|
||||
sleep 5
|
||||
docker ps
|
||||
#----------------------------------------------
|
||||
# check if container persists between runs
|
||||
#----------------------------------------------
|
||||
- name: Check server
|
||||
run: |
|
||||
docker ps
|
||||
#----------------------------------------------
|
||||
# check-out tests repo
|
||||
#----------------------------------------------
|
||||
- name: Checkout console test repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: vegaprotocol/console-test
|
||||
path: './console-test'
|
||||
|
||||
- name: Set up Python
|
||||
#----------------------------------------------
|
||||
# set-up python
|
||||
#----------------------------------------------
|
||||
- name: Set up python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10.11'
|
||||
|
||||
#----------------------------------------------
|
||||
# ----- install & configure poetry -----
|
||||
#----------------------------------------------
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
|
||||
with:
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
virtualenvs-path: console-test/.venv
|
||||
#----------------------------------------------
|
||||
# load cached venv if cache exists
|
||||
#----------------------------------------------
|
||||
- name: Load cached venv
|
||||
id: cached-poetry-dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: console-test/.venv
|
||||
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
|
||||
#----------------------------------------------
|
||||
# install dependencies if cache does not exist
|
||||
#----------------------------------------------
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --no-root
|
||||
working-directory: ./console-test
|
||||
|
||||
- name: load Binaries
|
||||
run: |
|
||||
poetry run python -m vega_sim.tools.load_binaries
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||
run: poetry install --no-interaction --no-root
|
||||
#----------------------------------------------
|
||||
# install vega binaries
|
||||
#----------------------------------------------
|
||||
- name: Install vega binaries
|
||||
working-directory: ./console-test
|
||||
|
||||
- name: pull console
|
||||
run: |
|
||||
poetry run docker pull ghcr.io/vegaprotocol/frontend/trading:${{ inputs.github-sha }}
|
||||
|
||||
- name: Update container_name in config.py
|
||||
run: |
|
||||
sed -i "s/container_name = \".*\"/container_name = \"vegaprotocol\/frontend\/trading:${{ inputs.github-sha }}\"/g" config.py
|
||||
|
||||
run: poetry run python -m vega_sim.tools.load_binaries --force
|
||||
#----------------------------------------------
|
||||
# install playwright
|
||||
#----------------------------------------------
|
||||
- name: install playwright
|
||||
run: poetry run playwright install
|
||||
working-directory: ./console-test
|
||||
|
||||
- name: run tests
|
||||
run: poetry run pytest --numprocesses auto
|
||||
#----------------------------------------------
|
||||
# run tests
|
||||
#----------------------------------------------
|
||||
- name: Run tests
|
||||
working-directory: ./console-test
|
||||
|
||||
run: poetry run pytest --numprocesses auto
|
||||
- name: Check files
|
||||
run: |
|
||||
ls -al .
|
||||
ls -al console-test
|
||||
#----------------------------------------------
|
||||
# upload traces
|
||||
#----------------------------------------------
|
||||
- name: Upload Playwright Trace
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
echo IS_PR=true >> $GITHUB_ENV
|
||||
|
||||
- name: Is mainnet release
|
||||
if: ${{ contains(github.ref, 'release/mainnnet') && !contains(github.ref, 'mirror') }}
|
||||
if: ${{ contains(github.ref, 'release/mainnet') && !contains(github.ref, 'mirror') }}
|
||||
run: |
|
||||
echo IS_MAINNET_RELEASE=true >> $GITHUB_ENV
|
||||
|
||||
@@ -97,52 +97,13 @@ jobs:
|
||||
- name: Define dist variables
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: |
|
||||
envName=''
|
||||
domain="vega.rocks"
|
||||
bucketName=''
|
||||
python3 tools/ci/define-dist-variables.py --github-ref="${{ github.ref }}" --app="${{ matrix.app }}"
|
||||
|
||||
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then
|
||||
# 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)
|
||||
envName="$(echo ${{ github.ref }} | sed -e "s|refs/heads/release/||" | cut -d '-' -f 1 )"
|
||||
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
|
||||
envName="stagnet1"
|
||||
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: Verify script result
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: |
|
||||
echo "BUCKET_NAME=${{ env.BUCKET_NAME }}"
|
||||
echo "ENV_NAME=${{ env.ENV_NAME }}"
|
||||
|
||||
- name: Build local dist
|
||||
run: |
|
||||
@@ -157,8 +118,12 @@ jobs:
|
||||
elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then
|
||||
NODE_ENV=production yarn nx run ui-toolkit:build-storybook
|
||||
DIST_LOCATION=dist/storybook/ui-toolkit
|
||||
elif [ "${{ matrix.app }}" = "static" ]; then
|
||||
yarn nx build static || (yarn install && yarn nx build static)
|
||||
else
|
||||
$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 }}
|
||||
fi
|
||||
mv $DIST_LOCATION dist-result
|
||||
|
||||
@@ -16,5 +16,10 @@ NX_EXPLORER_NETWORK_PARAMETERS=1
|
||||
NX_EXPLORER_PARTIES=1
|
||||
NX_EXPLORER_VALIDATORS=1
|
||||
|
||||
CYPRESS_VEGA_WALLET_API_TOKEN=
|
||||
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=
|
||||
|
||||
@@ -1,19 +1,130 @@
|
||||
//Tests set to skip until market bug for capsule checkpoint is fixed
|
||||
context.skip('Market page', { tags: '@regression' }, function () {
|
||||
context('Market page', { tags: '@regression' }, function () {
|
||||
describe('Verify elements on page', function () {
|
||||
const marketHeaders = '[data-testid="markets-header"]';
|
||||
const marketNavigation = 'a[href="/markets"]';
|
||||
const marketHeaders = 'markets-heading';
|
||||
const createdMarketId =
|
||||
'2eab0e66545a789047561bc5a2e5cbc3b19eb708da41104e3cac2474ee36c4d4';
|
||||
|
||||
it('Markets page is displayed', function () {
|
||||
before('Create market', function () {
|
||||
cy.visit('/');
|
||||
cy.get(marketNavigation).click();
|
||||
cy.common_validate_blocks_data_displayed(marketHeaders);
|
||||
cy.createMarket();
|
||||
});
|
||||
|
||||
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.get(marketNavigation).click();
|
||||
cy.common_validate_blocks_data_displayed(marketHeaders);
|
||||
cy.navigate_to('markets', true);
|
||||
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}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,11 @@ context('Validator page', { tags: '@smoke' }, function () {
|
||||
it('should be able to see validator tiles', function () {
|
||||
cy.getNodes().then((nodes) => {
|
||||
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 () {
|
||||
cy.viewport('iphone-x');
|
||||
cy.visit('/');
|
||||
cy.get('[data-testid="open-menu"]').click();
|
||||
cy.get('[data-testid="button-menu-drawer"]').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('monitor_clipboard', () => {
|
||||
@@ -88,3 +88,42 @@ Cypress.Commands.add('convert_number_to_max_four_decimal', (number) => {
|
||||
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);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -95,7 +95,10 @@ export const Header = () => {
|
||||
{mainItems.map(routeToNavigationItem)}
|
||||
{groupedItems && groupedItems.length > 0 && (
|
||||
<NavigationItem>
|
||||
<NavigationTrigger isActive={Boolean(isOnOther)}>
|
||||
<NavigationTrigger
|
||||
isActive={Boolean(isOnOther)}
|
||||
data-testid="other"
|
||||
>
|
||||
{t('Other')}
|
||||
</NavigationTrigger>
|
||||
<NavigationContent>
|
||||
|
||||
@@ -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 Hash from '../../links/hash';
|
||||
import { TxOrderPeggedReferenceRow } from './order/tx-order-peg';
|
||||
import { TxOrderIcebergDetails } from './order/tx-order-iceberg';
|
||||
|
||||
interface TxDetailsOrderProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@@ -32,6 +33,8 @@ export const TxDetailsOrder = ({
|
||||
const marketId = txData.command.orderSubmission.marketId || '-';
|
||||
const reference = txData.command.orderSubmission.peggedOrder;
|
||||
const side = txData.command.orderSubmission.side;
|
||||
const iceberg = txData.command.orderSubmission.icebergOpts;
|
||||
const size = txData.command.orderSubmission.size;
|
||||
|
||||
let deterministicId = '';
|
||||
|
||||
@@ -74,6 +77,14 @@ export const TxDetailsOrder = ({
|
||||
marketId={marketId}
|
||||
/>
|
||||
) : null}
|
||||
{iceberg ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Iceberg details')}</TableCell>
|
||||
<TableCell>
|
||||
<TxOrderIcebergDetails iceberg={iceberg} size={size} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
|
||||
{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';
|
||||
|
||||
// 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) {
|
||||
type = getLabelForChainEvent(command.chainEvent);
|
||||
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);
|
||||
}
|
||||
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') {
|
||||
|
||||
@@ -15,6 +15,9 @@ query ExplorerNodes {
|
||||
stakedByDelegates
|
||||
stakedTotal
|
||||
pendingStake
|
||||
rankingScore {
|
||||
performanceScore
|
||||
}
|
||||
epochData {
|
||||
total
|
||||
offline
|
||||
|
||||
@@ -6,7 +6,7 @@ const defaultOptions = {} as const;
|
||||
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`
|
||||
@@ -27,6 +27,9 @@ export const ExplorerNodesDocument = gql`
|
||||
stakedByDelegates
|
||||
stakedTotal
|
||||
pendingStake
|
||||
rankingScore {
|
||||
performanceScore
|
||||
}
|
||||
epochData {
|
||||
total
|
||||
offline
|
||||
|
||||
@@ -91,7 +91,15 @@ export const ValidatorsPage = () => {
|
||||
const { data: tmData } = useTendermintValidators(5000);
|
||||
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
|
||||
const powers = compact(tmData?.result.validators).map(
|
||||
|
||||
@@ -218,17 +218,33 @@ describe(
|
||||
getProposalInformationFromTable('Tokens for proposal')
|
||||
.should('have.text', (1).toFixed(2))
|
||||
.and('be.visible');
|
||||
navigateTo(navigation.proposals);
|
||||
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
|
||||
getProposalFromTitle(rawProposal.rationale.title).within(() => {
|
||||
// 3002-PROP-021
|
||||
cy.getByTestId('user-voted-yes').should('exist');
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
});
|
||||
cy.getByTestId(changeVoteButton).should('be.visible').click();
|
||||
voteForProposal('against');
|
||||
cy.getByTestId(proposalVoteProgressAgainstPercentage)
|
||||
.contains('100.00%')
|
||||
.and('be.visible');
|
||||
cy.getByTestId(voteBreakdownToggle).click();
|
||||
getProposalInformationFromTable('Tokens against proposal')
|
||||
.should('have.text', (1).toFixed(2))
|
||||
.and('be.visible');
|
||||
getProposalInformationFromTable('Number of voting parties')
|
||||
.should('have.text', '1')
|
||||
.and('be.visible');
|
||||
navigateTo(navigation.proposals);
|
||||
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
|
||||
getProposalFromTitle(rawProposal.rationale.title).within(() => {
|
||||
cy.getByTestId('user-voted-no').should('exist');
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 3001-VOTE-042, 3001-VOTE-057, 3001-VOTE-058, 3001-VOTE-059, 3001-VOTE-060
|
||||
|
||||
@@ -26,6 +26,8 @@ const votesTable = 'votes-table';
|
||||
const openProposals = 'open-proposals';
|
||||
const proposalVoteProgressForPercentage =
|
||||
'vote-progress-indicator-percentage-for';
|
||||
const majorityVoteReached = 'majority-reached';
|
||||
const minParticipationReached = 'participation-reached';
|
||||
const proposalTimeout = { timeout: 8000 };
|
||||
|
||||
context(
|
||||
@@ -72,7 +74,7 @@ context(
|
||||
});
|
||||
});
|
||||
|
||||
// 3001-VOTE-046 3001-VOTE-044 3001-VOTE-074 3001-VOTE-074
|
||||
// 3001-VOTE-020 3001-VOTE-021 3001-VOTE-046 3001-VOTE-044 3001-VOTE-074 3001-VOTE-074
|
||||
it('Able to enact proposal by voting', function () {
|
||||
const proposalTitle = 'Add New proposal with short enactment';
|
||||
const proposalTx = createUpdateNetworkProposalTxBody();
|
||||
@@ -85,10 +87,18 @@ context(
|
||||
cy.contains(proposalTitle)
|
||||
.parentsUntil(proposalListItem)
|
||||
.last()
|
||||
.within(() => cy.getByTestId(viewProposalButton).click());
|
||||
.within(() => {
|
||||
// 3001-VOTE-019 time to vote is highlighted red
|
||||
cy.getByTestId('vote-details')
|
||||
.find('span')
|
||||
.should('have.class', 'text-vega-pink');
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
});
|
||||
cy.getByTestId(proposalStatus).should('have.text', 'Open');
|
||||
voteForProposal('for');
|
||||
cy.getByTestId(majorityVoteReached).should('exist');
|
||||
cy.getByTestId(minParticipationReached).should('exist');
|
||||
cy.getByTestId(proposalStatus, proposalTimeout)
|
||||
.should('have.text', 'Passed')
|
||||
.then(() => {
|
||||
@@ -104,6 +114,14 @@ context(
|
||||
cy.getByTestId(proposalVoteProgressForPercentage)
|
||||
.contains('100.00%')
|
||||
.and('be.visible');
|
||||
navigateTo(navigation.proposals);
|
||||
cy.contains(proposalTitle)
|
||||
.parentsUntil(proposalListItem)
|
||||
.last()
|
||||
.within(() => {
|
||||
cy.getByTestId(majorityVoteReached).should('exist');
|
||||
cy.getByTestId(minParticipationReached).should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
// 3001-VOTE-047
|
||||
|
||||
@@ -49,8 +49,8 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
|
||||
navigateTo(navigation.proposals);
|
||||
});
|
||||
|
||||
// 3001-VOTE-018
|
||||
it('Newly created proposals list - proposals closest to closing date appear higher in list', function () {
|
||||
// 3001-VOTE-005
|
||||
const proposalDays = [364, 50, 2];
|
||||
for (let index = 0; index < proposalDays.length; index++) {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
@@ -120,16 +120,24 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
|
||||
submitUniqueRawProposal({ proposalTitle: proposalTitle });
|
||||
getProposalFromTitle(proposalTitle).within(() => {
|
||||
// 3001-VOTE-039
|
||||
cy.getByTestId(voteStatus).should(
|
||||
cy.getByTestId('participation-not-reached').should(
|
||||
'have.text',
|
||||
'Participation not reached'
|
||||
'Min. participation not reached'
|
||||
);
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
voteForProposal('for');
|
||||
navigateTo(navigation.proposals);
|
||||
getProposalFromTitle(proposalTitle).within(() => {
|
||||
cy.getByTestId(voteStatus).should('have.text', 'Set to pass');
|
||||
cy.getByTestId(voteStatus).should(
|
||||
'have.text',
|
||||
'Currently expected to pass'
|
||||
);
|
||||
cy.getByTestId('user-voted-yes').should('exist');
|
||||
cy.getByTestId('participation-reached').should(
|
||||
'have.text',
|
||||
'Min. participation reached'
|
||||
);
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
cy.getByTestId(voteBreakDownToggle).click();
|
||||
|
||||
@@ -144,7 +144,7 @@ context(
|
||||
closeStakingDialog();
|
||||
navigateTo(navigation.validators);
|
||||
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 () {
|
||||
|
||||
@@ -351,6 +351,12 @@ context(
|
||||
);
|
||||
stakingPageDisassociateAllTokens();
|
||||
});
|
||||
|
||||
it('Able to associate over 1 million tokens', function () {
|
||||
cy.get(ethWalletAssociateButton).click();
|
||||
stakingPageAssociateTokens('2000000', { approve: true });
|
||||
stakingPageDisassociateAllTokens();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
cy.getByTestId('home-proposals').within(() => {
|
||||
cy.get('[href="/proposals"]')
|
||||
.should('exist')
|
||||
.and('have.text', 'Browse, vote, and propose');
|
||||
.and('have.text', 'See all proposals');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const governanceDocsUrl = 'https://vega.xyz/governance';
|
||||
const networkUpgradeProposalListItem = 'protocol-upgrade-proposals-list-item';
|
||||
const closedProposals = 'closed-proposals';
|
||||
const closedProposalToggle = 'closed-proposals-toggle-networkUpgrades';
|
||||
const protocolUpgradeTime = 'protocol-upgrade-time';
|
||||
|
||||
context(
|
||||
'Governance Page - verify elements on page',
|
||||
@@ -161,7 +162,7 @@ context(
|
||||
);
|
||||
cy.getByTestId('protocol-upgrade-proposal-status').should(
|
||||
'have.text',
|
||||
'Approved by validators '
|
||||
'Approved by validators'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -174,6 +175,7 @@ context(
|
||||
});
|
||||
});
|
||||
|
||||
// 3009-NTWU-003 3009-NTWU-004 3009-NTWU-007
|
||||
it('should see details of network upgrade proposal', function () {
|
||||
mockNetworkUpgradeProposal();
|
||||
navigateTo(navigation.proposals);
|
||||
@@ -227,5 +229,51 @@ context(
|
||||
cy.getByTestId(networkUpgradeProposalListItem).should('not.exist');
|
||||
cy.getByTestId(closedProposalToggle).should('not.exist');
|
||||
});
|
||||
|
||||
// 3009-NTWU-001 3009-NTWU-002 3009-NTWU-006 3009-NTWU-009
|
||||
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(protocolUpgradeTime)
|
||||
.invoke('text')
|
||||
.should('not.eq', estimateText);
|
||||
});
|
||||
// time estimate on proposal detail
|
||||
cy.getByTestId('protocol-upgrade-proposal').within(() => {
|
||||
cy.get('@displayedEstimate').then((estimateText) => {
|
||||
cy.getByTestId(protocolUpgradeTime)
|
||||
.invoke('text')
|
||||
.should('not.eq', estimateText);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -174,7 +174,8 @@ export function clickOnValidatorFromList(
|
||||
cy.get(`[row-id="${validatorNumber}"]`)
|
||||
.should('be.visible')
|
||||
.find(stakeValidatorListName)
|
||||
.click();
|
||||
.as('validatorOnList');
|
||||
cy.get('@validatorOnList').click();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,3 +25,5 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
||||
CYPRESS_FAIRGROUND=false
|
||||
LC_ALL="en_US.UTF-8"
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
@@ -25,3 +25,5 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
|
||||
#Test configuration variables
|
||||
CYPRESS_FAIRGROUND=false
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
@@ -16,3 +16,6 @@ NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/
|
||||
|
||||
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
|
||||
@@ -17,3 +17,6 @@ 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
|
||||
@@ -16,3 +16,6 @@ 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
|
||||
@@ -11,4 +11,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
|
||||
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
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
@@ -16,4 +16,7 @@ NX_VEGA_REST_URL=https://api.n07.testnet.vega.xyz/api/v2/
|
||||
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
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
@@ -13,4 +13,7 @@ NX_VEGA_REST_URL=https://api-validators-testnet.vega.rocks/api/v2/
|
||||
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.
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -18,10 +18,7 @@ import { VegaWallet } from '../vega-wallet';
|
||||
import { useLocation, useMatch } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { useTelemetryDialog } from '../telemetry-dialog/telemetry-dialog';
|
||||
import {
|
||||
ProtocolUpgradeCountdown,
|
||||
ProtocolUpgradeCountdownMode,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { ProtocolUpgradeCountdown } from '@vegaprotocol/proposals';
|
||||
|
||||
export const SettingsLink = () => {
|
||||
const { open, isOpen, close } = useTelemetryDialog();
|
||||
@@ -68,9 +65,7 @@ export const Nav = ({ theme }: Pick<NavigationProps, 'theme'>) => {
|
||||
actions={
|
||||
<>
|
||||
<SettingsLink />
|
||||
<ProtocolUpgradeCountdown
|
||||
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
|
||||
/>
|
||||
<ProtocolUpgradeCountdown />
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
"proposedEnactment": "Proposed enactment",
|
||||
"Enacted": "Enacted",
|
||||
"enactedOn": "Enacted on",
|
||||
"enactedOn{{date}}": "Enacted on {{enactmentDate}}",
|
||||
"status": "Status",
|
||||
"state": "State",
|
||||
"shouldPass": "Should pass",
|
||||
@@ -185,6 +186,7 @@
|
||||
"proposedOn": "Proposed on",
|
||||
"proposedBy": "Proposed by",
|
||||
"toEnactOn": "Enacts on",
|
||||
"enactsOn{{date}}": "Enacts on {{enactmentDate}}",
|
||||
"closesOn": "Closes on",
|
||||
"closedOn": "Closed on",
|
||||
"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.",
|
||||
"noGovernanceTokens": "You need some VEGA tokens to participate in governance",
|
||||
"youVoted": "You voted",
|
||||
"voted": "Voted",
|
||||
"changeVote": "Change vote",
|
||||
"txRequested": "Confirm transaction in wallet",
|
||||
"votePending": "Casting vote",
|
||||
@@ -628,7 +631,6 @@
|
||||
"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.",
|
||||
"n/a": "N/A",
|
||||
"Set to": "Set to",
|
||||
"pass": "pass",
|
||||
"fail": "fail",
|
||||
"New asset": "New asset",
|
||||
@@ -788,7 +790,7 @@
|
||||
"overstakedPenalty": "Overstaked 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.",
|
||||
"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.",
|
||||
"homeValidatorsButtonText": "Browse, and stake",
|
||||
"homeRewardsIntro": "Track rewards you've earned for trading, liquidity provision, market creation, and staking.",
|
||||
@@ -838,6 +840,20 @@
|
||||
"networkGovernance": "Network governance",
|
||||
"networkUpgrades": "Network upgrades",
|
||||
"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"
|
||||
"Upgraded at": "Upgraded at",
|
||||
"dataIsIdentical": "Data is identical",
|
||||
"updatesToMarket": "Updates to market"
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@ export const downloadJson = (jsonString: string, proposalTitle: string) => {
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
getNotRejectedProposals,
|
||||
getNotRejectedProtocolUpgradeProposals,
|
||||
} from '../proposals/proposals/proposals-container';
|
||||
import { Heading } from '../../components/heading';
|
||||
import { Heading, SubHeading } from '../../components/heading';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { RouteChildProps } from '..';
|
||||
import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals';
|
||||
@@ -31,6 +31,10 @@ import {
|
||||
orderByDate,
|
||||
orderByUpgradeBlockHeight,
|
||||
} from '../proposals/components/proposals-list/proposals-list';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { BigNumber } from '../../lib/bignumber';
|
||||
|
||||
const nodesToShow = 6;
|
||||
@@ -43,33 +47,57 @@ const HomeProposals = ({
|
||||
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
|
||||
}) => {
|
||||
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 (
|
||||
<section className="mb-16" data-testid="home-proposals">
|
||||
<Heading title={t('Proposals')} />
|
||||
<h3 className="mb-6">{t('homeProposalsIntro')}</h3>
|
||||
<div className="flex items-center mb-8 gap-8">
|
||||
<Link to={`${Routes.PROPOSALS}`}>
|
||||
<Button size="md">{t('homeProposalsButtonText')}</Button>
|
||||
</Link>
|
||||
<AsyncRenderer
|
||||
loading={networkParamsLoading}
|
||||
error={networkParamsError}
|
||||
data={networkParams}
|
||||
>
|
||||
<section className="mb-16" data-testid="home-proposals">
|
||||
<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}>
|
||||
{t(`readMoreGovernance`)}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
<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} />
|
||||
))}
|
||||
<SubHeading title={t('latestProposals')} />
|
||||
<ul data-testid="home-proposal-list" className="grid gap-6">
|
||||
{protocolUpgradeProposals.map((proposal, index) => (
|
||||
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
|
||||
))}
|
||||
|
||||
{proposals.map((proposal) => (
|
||||
<ProposalsListItem key={proposal.id} proposal={proposal} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
{proposals.map((proposal) => (
|
||||
<ProposalsListItem
|
||||
key={proposal.id}
|
||||
proposal={proposal}
|
||||
networkParams={networkParams}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="mt-6">
|
||||
<Link to={`${Routes.PROPOSALS}`}>
|
||||
<Button size="md">{t('homeProposalsButtonText')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -87,8 +115,8 @@ const HomeNodes = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
const highlightedNodeData = [
|
||||
{ title: t('active nodes'), length: activeNodes.length },
|
||||
{ title: t('consensus nodes'), length: consensusNodes.length },
|
||||
{ title: t('activeNodes'), length: activeNodes.length },
|
||||
{ title: t('consensusNodes'), length: consensusNodes.length },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -234,7 +262,7 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||
[protocolUpgradeProposals]
|
||||
);
|
||||
|
||||
const totalProposalsDesired = 4;
|
||||
const totalProposalsDesired = 3;
|
||||
const protocolUpgradeProposalsToShow = sortedProtocolUpgradeProposals.slice(
|
||||
0,
|
||||
totalProposalsDesired
|
||||
@@ -290,7 +318,7 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div className="flex items-center mb-8 gap-4">
|
||||
<Link to={Routes.WITHDRAWALS}>
|
||||
|
||||
+10
-32
@@ -1,5 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
@@ -18,53 +17,32 @@ export const CurrentProposalState = ({
|
||||
|
||||
switch (proposal?.state) {
|
||||
case ProposalState.STATE_ENACTED: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
<span className="mr-2">{t('voteState_Enacted')}</span>
|
||||
<Icon name={'tick'} />
|
||||
</>
|
||||
);
|
||||
proposalStatus = t('voteState_Enacted');
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_PASSED: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
<span className="mr-2">{t('voteState_Passed')}</span>
|
||||
<Icon name={'tick'} />
|
||||
</>
|
||||
);
|
||||
proposalStatus = t('voteState_Passed');
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
<span className="mr-2">{t('voteState_WaitingForNodeVote')}</span>
|
||||
<Icon name={'time'} />
|
||||
</>
|
||||
);
|
||||
proposalStatus = t('voteState_WaitingForNodeVote');
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_OPEN: {
|
||||
variant = 'primary' as ProposalInfoLabelVariant;
|
||||
proposalStatus = <>{t('voteState_Open')}</>;
|
||||
proposalStatus = t('voteState_Open');
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_DECLINED: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
<span className="mr-2">{t('voteState_Declined')}</span>
|
||||
<Icon name={'cross'} />
|
||||
</>
|
||||
);
|
||||
proposalStatus = t('voteState_Declined');
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_REJECTED: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
<span className="mr-2">{t('voteState_Rejected')}</span>
|
||||
<Icon name={'warning-sign'} />
|
||||
</>
|
||||
);
|
||||
proposalStatus = t('voteState_Rejected');
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_FAILED: {
|
||||
proposalStatus = t('voteState_Failed');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+80
-20
@@ -1,21 +1,66 @@
|
||||
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 {
|
||||
generateNoVotes,
|
||||
generateProposal,
|
||||
generateYesVotes,
|
||||
} from '../../test-helpers/generate-proposals';
|
||||
import { ProposalHeader } from './proposal-header';
|
||||
import {
|
||||
lastWeek,
|
||||
nextWeek,
|
||||
mockNetworkParams,
|
||||
mockWalletContext,
|
||||
createUserVoteQueryMock,
|
||||
} from '../../test-helpers/mocks';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
|
||||
import { lastWeek, nextWeek } from '../../test-helpers/mocks';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
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 = (
|
||||
proposal: ProposalQuery['proposal'],
|
||||
isListItem = true
|
||||
) => render(<ProposalHeader proposal={proposal} isListItem={isListItem} />);
|
||||
isListItem = true,
|
||||
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', () => {
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Renders New market proposal', () => {
|
||||
const mockedFlags = jest.mocked(FLAGS);
|
||||
mockedFlags.SUCCESSOR_MARKETS = true;
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
rationale: {
|
||||
@@ -48,6 +93,9 @@ describe('Proposal header', () => {
|
||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||
'tGBP settled future.'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-successor-info')).toHaveTextContent(
|
||||
'PARENT_CODE'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders Update market proposal', () => {
|
||||
@@ -317,22 +365,6 @@ describe('Proposal header', () => {
|
||||
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', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
@@ -346,4 +378,32 @@ describe('Proposal header', () => {
|
||||
);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
+66
-14
@@ -1,5 +1,5 @@
|
||||
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 { Heading, SubHeading } from '../../../../components/heading';
|
||||
import type { ReactNode } from 'react';
|
||||
@@ -8,15 +8,25 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { truncateMiddle } from '../../../../lib/truncate-middle';
|
||||
import { CurrentProposalState } from '../current-proposal-state';
|
||||
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 = ({
|
||||
proposal,
|
||||
networkParams,
|
||||
isListItem = true,
|
||||
}: {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
networkParams: Partial<NetworkParamsResult>;
|
||||
isListItem?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { voteState } = useUserVote(proposal?.id);
|
||||
const change = proposal?.terms.change;
|
||||
|
||||
let details: ReactNode;
|
||||
@@ -33,6 +43,9 @@ export const ProposalHeader = ({
|
||||
fallbackTitle = t('NewMarketProposal');
|
||||
details = (
|
||||
<>
|
||||
{FLAGS.SUCCESSOR_MARKETS && (
|
||||
<SuccessorCode proposalId={proposal?.id} />
|
||||
)}
|
||||
<span>
|
||||
{t('Code')}: {change.instrument.code}.
|
||||
</span>{' '}
|
||||
@@ -119,6 +132,35 @@ export const ProposalHeader = ({
|
||||
|
||||
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">
|
||||
{isListItem ? (
|
||||
<header>
|
||||
@@ -133,23 +175,33 @@ export const ProposalHeader = ({
|
||||
)}
|
||||
</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 && (
|
||||
<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}
|
||||
</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;
|
||||
};
|
||||
|
||||
+4
-4
@@ -7,10 +7,10 @@ export type ProposalInfoLabelVariant =
|
||||
| 'tertiary'
|
||||
| 'highlight';
|
||||
|
||||
const base = 'rounded-md px-2 py-1 font-alpha';
|
||||
const primary = 'bg-vega-light-150 text-black';
|
||||
const secondary = 'bg-vega-dark-200 text-white';
|
||||
const tertiary = 'bg-vega-dark-150 text-white';
|
||||
const base = 'rounded-full px-3 py-1 font-alpha';
|
||||
const primary = 'bg-vega-green text-black';
|
||||
const secondary = 'bg-vega-dark-200 text-vega-light-200';
|
||||
const tertiary = 'bg-vega-dark-150 text-vega-light-200';
|
||||
const highlight = 'bg-vega-yellow text-black';
|
||||
|
||||
const getClassname = (variant: ProposalInfoLabelVariant) => {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './proposal-market-changes';
|
||||
+91
@@ -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();
|
||||
});
|
||||
});
|
||||
+86
@@ -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';
|
||||
+153
@@ -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();
|
||||
});
|
||||
});
|
||||
+174
@@ -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 type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { mockNetworkParams } from '../../test-helpers/mocks';
|
||||
|
||||
jest.mock('@vegaprotocol/network-parameters', () => ({
|
||||
...jest.requireActual('@vegaprotocol/network-parameters'),
|
||||
@@ -46,6 +47,7 @@ const renderComponent = (proposal: ProposalQuery['proposal']) => {
|
||||
<Proposal
|
||||
restData={{}}
|
||||
proposal={proposal as ProposalQuery['proposal']}
|
||||
networkParams={mockNetworkParams}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { AsyncRenderer, Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
|
||||
import { Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
|
||||
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
||||
import { ProposalDescription } from '../proposal-description';
|
||||
import { ProposalChangeTable } from '../proposal-change-table';
|
||||
@@ -21,6 +17,8 @@ import type { MarketInfoWithData } from '@vegaprotocol/markets';
|
||||
import type { AssetQuery } from '@vegaprotocol/assets';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { ProposalMarketChanges } from '../proposal-market-changes';
|
||||
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
|
||||
|
||||
export enum ProposalType {
|
||||
PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET',
|
||||
@@ -32,28 +30,27 @@ export enum ProposalType {
|
||||
}
|
||||
export interface ProposalProps {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
networkParams: Partial<NetworkParamsResult>;
|
||||
newMarketData?: MarketInfoWithData | null;
|
||||
assetData?: AssetQuery | null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-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 = ({
|
||||
proposal,
|
||||
networkParams,
|
||||
restData,
|
||||
newMarketData,
|
||||
assetData,
|
||||
originalMarketProposalRestData,
|
||||
mostRecentlyEnactedAssociatedMarketProposal,
|
||||
}: ProposalProps) => {
|
||||
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) {
|
||||
return null;
|
||||
@@ -79,122 +76,140 @@ export const Proposal = ({
|
||||
let minVoterBalance = null;
|
||||
let proposalType = null;
|
||||
|
||||
if (params) {
|
||||
if (networkParams) {
|
||||
switch (proposal.terms.change.__typename) {
|
||||
case 'NewMarket':
|
||||
minVoterBalance = params.governance_proposal_market_minVoterBalance;
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_market_minVoterBalance;
|
||||
proposalType = ProposalType.PROPOSAL_NEW_MARKET;
|
||||
break;
|
||||
case 'UpdateMarket':
|
||||
minVoterBalance =
|
||||
params.governance_proposal_updateMarket_minVoterBalance;
|
||||
networkParams.governance_proposal_updateMarket_minVoterBalance;
|
||||
proposalType = ProposalType.PROPOSAL_UPDATE_MARKET;
|
||||
break;
|
||||
case 'NewAsset':
|
||||
minVoterBalance = params.governance_proposal_asset_minVoterBalance;
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_asset_minVoterBalance;
|
||||
proposalType = ProposalType.PROPOSAL_NEW_ASSET;
|
||||
break;
|
||||
case 'UpdateAsset':
|
||||
minVoterBalance =
|
||||
params.governance_proposal_updateAsset_minVoterBalance;
|
||||
networkParams.governance_proposal_updateAsset_minVoterBalance;
|
||||
proposalType = ProposalType.PROPOSAL_UPDATE_ASSET;
|
||||
break;
|
||||
case 'UpdateNetworkParameter':
|
||||
minVoterBalance =
|
||||
params.governance_proposal_updateNetParam_minVoterBalance;
|
||||
networkParams.governance_proposal_updateNetParam_minVoterBalance;
|
||||
proposalType = ProposalType.PROPOSAL_NETWORK_PARAMETER;
|
||||
break;
|
||||
case 'NewFreeform':
|
||||
minVoterBalance = params.governance_proposal_freeform_minVoterBalance;
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_freeform_minVoterBalance;
|
||||
proposalType = ProposalType.PROPOSAL_FREEFORM;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AsyncRenderer data={params} loading={loading} error={error}>
|
||||
<section data-testid="proposal">
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon name={'chevron-left'} />
|
||||
<section data-testid="proposal">
|
||||
<div className="flex items-center gap-1 mb-6">
|
||||
<Icon name={'chevron-left'} />
|
||||
|
||||
{proposal.state === ProposalState.STATE_REJECTED ? (
|
||||
<div data-testid="rejected-proposals-link">
|
||||
<Link className="underline" to={Routes.PROPOSALS_REJECTED}>
|
||||
{t('RejectedProposals')}
|
||||
</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} />
|
||||
{proposal.state === ProposalState.STATE_REJECTED ? (
|
||||
<div data-testid="rejected-proposals-link">
|
||||
<Link className="underline" to={Routes.PROPOSALS_REJECTED}>
|
||||
{t('RejectedProposals')}
|
||||
</Link>
|
||||
</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' &&
|
||||
proposal.terms.change.source.__typename === 'ERC20' &&
|
||||
proposal.id ? (
|
||||
<ListAsset
|
||||
assetId={proposal.id}
|
||||
withdrawalThreshold={
|
||||
proposal.terms.change.source.withdrawThreshold
|
||||
<div id="details">
|
||||
<div className="my-10">
|
||||
<ProposalChangeTable proposal={proposal} />
|
||||
</div>
|
||||
|
||||
{proposal.terms.change.__typename === 'NewAsset' &&
|
||||
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>
|
||||
)}
|
||||
|
||||
{newMarketData && (
|
||||
{(proposal.terms.change.__typename === 'NewAsset' ||
|
||||
proposal.terms.change.__typename === 'UpdateAsset') &&
|
||||
asset && (
|
||||
<div className="mb-4">
|
||||
<ProposalMarketData marketData={newMarketData} />
|
||||
<ProposalAssetDetails asset={asset} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(proposal.terms.change.__typename === 'NewAsset' ||
|
||||
proposal.terms.change.__typename === 'UpdateAsset') &&
|
||||
asset && (
|
||||
<div className="mb-4">
|
||||
<ProposalAssetDetails asset={asset} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-6">
|
||||
<ProposalJson proposal={restData?.data?.proposal} />
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<ProposalJson proposal={restData?.data?.proposal} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="voting">
|
||||
<div className="mb-10">
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
<VoteDetails
|
||||
proposal={proposal}
|
||||
proposalType={proposalType}
|
||||
minVoterBalance={minVoterBalance}
|
||||
spamProtectionMinTokens={
|
||||
params?.spam_protection_voting_min_tokens
|
||||
}
|
||||
/>
|
||||
</RoundedWrapper>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<ProposalVotesTable
|
||||
<div id="voting">
|
||||
<div className="mb-10">
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
<VoteDetails
|
||||
proposal={proposal}
|
||||
proposalType={proposalType}
|
||||
minVoterBalance={minVoterBalance}
|
||||
spamProtectionMinTokens={
|
||||
networkParams?.spam_protection_voting_min_tokens
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</RoundedWrapper>
|
||||
</div>
|
||||
</section>
|
||||
</AsyncRenderer>
|
||||
|
||||
<div className="mb-4">
|
||||
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
+11
-127
@@ -5,11 +5,7 @@ import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
ProposalRejectionReason,
|
||||
ProposalState,
|
||||
VoteValue,
|
||||
} from '@vegaprotocol/types';
|
||||
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
|
||||
import {
|
||||
generateNoVotes,
|
||||
generateProposal,
|
||||
@@ -18,7 +14,6 @@ import {
|
||||
import { ProposalsListItemDetails } from './proposals-list-item-details';
|
||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||
import {
|
||||
mockPubkey,
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
fiveMinutes,
|
||||
@@ -28,39 +23,6 @@ import {
|
||||
nextWeek,
|
||||
} from '../../test-helpers/mocks';
|
||||
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 = (
|
||||
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(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
@@ -153,11 +115,11 @@ describe('Proposals list item details', () => {
|
||||
})
|
||||
);
|
||||
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(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
@@ -179,11 +141,11 @@ describe('Proposals list item details', () => {
|
||||
})
|
||||
);
|
||||
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(
|
||||
generateProposal({
|
||||
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', () => {
|
||||
@@ -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', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
@@ -309,7 +227,7 @@ describe('Proposals list item details', () => {
|
||||
})
|
||||
);
|
||||
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(
|
||||
'Participation not reached'
|
||||
);
|
||||
});
|
||||
|
||||
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'
|
||||
'Currently expected to pass'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+53
-106
@@ -1,9 +1,6 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVoteInformation } from '../../hooks';
|
||||
import { useUserVote } from '../vote-details/use-user-vote';
|
||||
import { StatusPass } from '../current-proposal-status/current-proposal-status';
|
||||
import { format, formatDistanceToNowStrict } from 'date-fns';
|
||||
import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||
import type { ReactNode } from 'react';
|
||||
@@ -14,138 +11,100 @@ import {
|
||||
import Routes from '../../../routes';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
|
||||
const MajorityNotReached = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
{t('Majority')} {t('not reached')}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const ParticipationNotReached = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
{t('Participation')} {t('not reached')}
|
||||
</>
|
||||
);
|
||||
};
|
||||
import { useVoteInformation } from '../../hooks';
|
||||
|
||||
export const ProposalsListItemDetails = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const state = proposal?.state;
|
||||
const {
|
||||
willPassByTokenVote,
|
||||
willPassByLPVote,
|
||||
majorityMet,
|
||||
participationMet,
|
||||
} = useVoteInformation({
|
||||
const { willPassByTokenVote, willPassByLPVote } = useVoteInformation({
|
||||
proposal,
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const { voteState } = useUserVote(proposal?.id);
|
||||
const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket';
|
||||
const updateMarketWillPass = willPassByTokenVote || willPassByLPVote;
|
||||
const updateMarketVotePassMethod = willPassByTokenVote
|
||||
? t('byTokenVote')
|
||||
: t('byLPVote');
|
||||
const nowToEnactmentInHours = differenceInHours(
|
||||
new Date(proposal?.terms.closingDatetime),
|
||||
new Date()
|
||||
);
|
||||
const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket';
|
||||
|
||||
let voteDetails: ReactNode;
|
||||
let voteStatus: ReactNode;
|
||||
|
||||
switch (state) {
|
||||
case ProposalState.STATE_ENACTED: {
|
||||
voteDetails = proposal?.terms.enactmentDatetime && (
|
||||
<>
|
||||
{format(
|
||||
new Date(proposal?.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
)}
|
||||
</>
|
||||
);
|
||||
voteDetails =
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
t('enactedOn{{date}}', {
|
||||
enactmentDate:
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
format(
|
||||
new Date(proposal?.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_PASSED: {
|
||||
voteDetails = proposal?.terms.change.__typename !== 'NewFreeform' && (
|
||||
<>
|
||||
{t('toEnactOn')}{' '}
|
||||
{proposal?.terms.enactmentDatetime &&
|
||||
voteDetails =
|
||||
proposal?.terms.change.__typename !== 'NewFreeform' &&
|
||||
t('enactsOn{{date}}', {
|
||||
enactmentDate:
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
format(
|
||||
new Date(proposal.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
)}
|
||||
</>
|
||||
);
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
|
||||
voteDetails = proposal?.terms.change.__typename !== 'NewFreeform' && (
|
||||
<>
|
||||
{t('toEnactOn')}{' '}
|
||||
{proposal?.terms.enactmentDatetime &&
|
||||
voteDetails =
|
||||
proposal?.terms.change.__typename !== 'NewFreeform' &&
|
||||
t('enactsOn{{date}}', {
|
||||
enactmentDate:
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
format(
|
||||
new Date(proposal.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
)}
|
||||
</>
|
||||
);
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_OPEN: {
|
||||
voteDetails = (voteState === 'Yes' && (
|
||||
<>
|
||||
{t('youVoted')} {t('voteState_Yes')}
|
||||
</>
|
||||
)) ||
|
||||
(voteState === 'No' && (
|
||||
<>
|
||||
{t('youVoted')} {t('voteState_No')}
|
||||
</>
|
||||
)) || (
|
||||
<>
|
||||
{formatDistanceToNowStrict(
|
||||
new Date(proposal?.terms.closingDatetime)
|
||||
)}{' '}
|
||||
{t('left to vote')}
|
||||
</>
|
||||
);
|
||||
voteDetails = (
|
||||
<span className={nowToEnactmentInHours < 6 ? 'text-vega-pink' : ''}>
|
||||
{formatDistanceToNowStrict(new Date(proposal?.terms.closingDatetime))}{' '}
|
||||
{t('left to vote')}
|
||||
</span>
|
||||
);
|
||||
voteStatus =
|
||||
(isUpdateMarket &&
|
||||
(updateMarketWillPass ? (
|
||||
<>
|
||||
{t('Set to')}{' '}
|
||||
<StatusPass>
|
||||
{t('pass')} {updateMarketVotePassMethod}
|
||||
</StatusPass>
|
||||
{t('currentlySetTo')} {t('pass')} {updateMarketVotePassMethod}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('Set to')} {t('fail')}
|
||||
{t('currentlySetTo')} {t('fail')}
|
||||
</>
|
||||
))) ||
|
||||
(!participationMet && <ParticipationNotReached />) ||
|
||||
(!majorityMet && <MajorityNotReached />) ||
|
||||
(willPassByTokenVote ? (
|
||||
<>
|
||||
{t('Set to')} {t('pass')}
|
||||
{t('currentlySetTo')} {t('pass')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('Set to')} {t('fail')}
|
||||
{t('currentlySetTo')} {t('fail')}
|
||||
</>
|
||||
));
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_DECLINED: {
|
||||
voteStatus =
|
||||
(!participationMet && <ParticipationNotReached />) ||
|
||||
(!majorityMet && <MajorityNotReached />);
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_REJECTED: {
|
||||
voteStatus = proposal?.rejectionReason && (
|
||||
<>{t(ProposalRejectionReasonMapping[proposal.rejectionReason])}</>
|
||||
@@ -155,29 +114,17 @@ export const ProposalsListItemDetails = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[1fr_auto] mt-4 items-start gap-2 text-sm">
|
||||
{voteDetails && (
|
||||
<div
|
||||
className="col-start-1 row-start-2 text-vega-light-300"
|
||||
data-testid="vote-details"
|
||||
>
|
||||
{voteDetails}
|
||||
</div>
|
||||
)}
|
||||
{voteStatus && (
|
||||
<div
|
||||
className="col-start-2 row-start-1 justify-self-end"
|
||||
data-testid="vote-status"
|
||||
>
|
||||
{voteStatus}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4 items-start text-sm">
|
||||
<div className="flex items-center gap-2 text-vega-light-300 mb-2">
|
||||
{voteDetails && <span data-testid="vote-details">{voteDetails}</span>}
|
||||
{voteDetails && voteStatus && <span>·</span>}
|
||||
{voteStatus && <span data-testid="vote-status">{voteStatus}</span>}
|
||||
</div>
|
||||
|
||||
{proposal?.id && (
|
||||
<div className="col-start-2 row-start-2 justify-self-end">
|
||||
<Link to={`${Routes.PROPOSALS}/${proposal.id}`}>
|
||||
<Button data-testid="view-proposal-btn">{t('View')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<Link to={`${Routes.PROPOSALS}/${proposal.id}`}>
|
||||
<Button data-testid="view-proposal-btn">{t('viewDetails')}</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
+8
-3
@@ -3,18 +3,23 @@ import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
||||
import { ProposalsListItemDetails } from './proposals-list-item-details';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
|
||||
|
||||
interface ProposalsListItemProps {
|
||||
proposal?: ProposalFieldsFragment | ProposalQuery['proposal'] | null;
|
||||
networkParams: Partial<NetworkParamsResult> | null;
|
||||
}
|
||||
|
||||
export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
|
||||
if (!proposal || !proposal.id) return null;
|
||||
export const ProposalsListItem = ({
|
||||
proposal,
|
||||
networkParams,
|
||||
}: ProposalsListItemProps) => {
|
||||
if (!proposal || !proposal.id || !networkParams) return null;
|
||||
|
||||
return (
|
||||
<li id={proposal.id} data-testid="proposals-list-item">
|
||||
<RoundedWrapper paddingBottom={true} heightFull={true}>
|
||||
<ProposalHeader proposal={proposal} />
|
||||
<ProposalHeader proposal={proposal} networkParams={networkParams} />
|
||||
<ProposalsListItemDetails proposal={proposal} />
|
||||
</RoundedWrapper>
|
||||
</li>
|
||||
|
||||
+45
-21
@@ -88,31 +88,39 @@ afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
jest.mock('../vote-details/use-user-vote', () => ({
|
||||
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
|
||||
}));
|
||||
|
||||
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([]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(screen.getByText('Proposals')).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([]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(
|
||||
screen.queryByTestId('proposals-list-filter')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Will show filter if there are proposals', () => {
|
||||
it('Will show filter if there are proposals', async () => {
|
||||
render(renderComponent([enactedProposalClosedLastWeek]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
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([]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
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(
|
||||
renderComponent([
|
||||
openProposalClosesNextWeek,
|
||||
@@ -121,6 +129,7 @@ describe('Proposals list', () => {
|
||||
failedProposalClosedLastMonth,
|
||||
])
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
const openProposals = within(screen.getByTestId('open-proposals'));
|
||||
const closedProposals = within(screen.getByTestId('closed-proposals'));
|
||||
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([]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(screen.queryByTestId('open-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('no-open-proposals')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('closed-proposals')).not.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]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(screen.queryByTestId('open-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('no-open-proposals')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('closed-proposals')).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]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(screen.getByTestId('open-proposals')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('no-open-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('closed-proposals')).not.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(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
|
||||
expect(screen.getByTestId('proposals-list-filter')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Filters list by text - party id', () => {
|
||||
it('Filters list by text - party id', async () => {
|
||||
render(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
|
||||
fireEvent.change(screen.getByTestId('filter-input'), {
|
||||
target: { value: 'bvcx' },
|
||||
@@ -174,10 +188,11 @@ describe('Proposals list', () => {
|
||||
expect(container.querySelector('#proposal1')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Filters list by text - proposal id', () => {
|
||||
it('Filters list by text - proposal id', async () => {
|
||||
render(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
|
||||
fireEvent.change(screen.getByTestId('filter-input'), {
|
||||
target: { value: 'proposal1' },
|
||||
@@ -187,10 +202,11 @@ describe('Proposals list', () => {
|
||||
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(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
|
||||
fireEvent.change(screen.getByTestId('filter-input'), {
|
||||
target: { value: 'osal1' },
|
||||
@@ -200,10 +216,11 @@ describe('Proposals list', () => {
|
||||
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(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
|
||||
fireEvent.change(screen.getByTestId('filter-input'), {
|
||||
target: { value: 'test' },
|
||||
@@ -211,10 +228,11 @@ describe('Proposals list', () => {
|
||||
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(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
|
||||
fireEvent.change(screen.getByTestId('filter-input'), {
|
||||
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(
|
||||
renderComponent(
|
||||
[enactedProposalClosedLastWeek],
|
||||
[closedProtocolUpgradeProposal]
|
||||
)
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
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]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(
|
||||
screen.queryByTestId('toggle-closed-proposals')
|
||||
).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]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(
|
||||
screen.queryByTestId('toggle-closed-proposals')
|
||||
).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(
|
||||
renderComponent(
|
||||
[enactedProposalClosedLastWeek],
|
||||
[closedProtocolUpgradeProposal]
|
||||
)
|
||||
);
|
||||
await screen.findByTestId('proposal-filter-toggle');
|
||||
fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
|
||||
fireEvent.change(screen.getByTestId('filter-input'), {
|
||||
target: { value: 'test' },
|
||||
@@ -265,7 +287,7 @@ describe('Proposals list', () => {
|
||||
).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(
|
||||
renderComponent(
|
||||
[enactedProposalClosedLastWeek],
|
||||
@@ -273,17 +295,19 @@ describe('Proposals list', () => {
|
||||
)
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('closed-governance-proposals')
|
||||
await screen.findByTestId('closed-governance-proposals')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Displays closed upgrade proposals when the toggle is clicked', () => {
|
||||
it('Displays closed upgrade proposals when the toggle is clicked', async () => {
|
||||
render(
|
||||
renderComponent(
|
||||
[enactedProposalClosedLastWeek],
|
||||
[closedProtocolUpgradeProposal]
|
||||
)
|
||||
);
|
||||
|
||||
await screen.findByTestId('toggle-closed-proposals');
|
||||
fireEvent.click(screen.getByText('Network upgrades'));
|
||||
expect(screen.getByTestId('closed-upgrade-proposals')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
+180
-146
@@ -8,6 +8,7 @@ import { ProtocolUpgradeProposalsListItem } from '../protocol-upgrade-proposals-
|
||||
import { ProposalsListFilter } from '../proposals-list-filter';
|
||||
import Routes from '../../../routes';
|
||||
import {
|
||||
AsyncRenderer,
|
||||
Button,
|
||||
Toggle,
|
||||
VegaIcon,
|
||||
@@ -19,6 +20,10 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
|
||||
interface ProposalsListProps {
|
||||
proposals: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
|
||||
@@ -70,6 +75,20 @@ export const ProposalsList = ({
|
||||
lastBlockHeight,
|
||||
}: ProposalsListProps) => {
|
||||
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 [closedProposalsView, setClosedProposalsView] =
|
||||
useState<ClosedProposalsViewOptions>(
|
||||
@@ -134,166 +153,181 @@ export const ProposalsList = ({
|
||||
p?.party?.id?.toString().includes(filterString);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid xs:grid-cols-2 items-center">
|
||||
<Heading
|
||||
centerContent={false}
|
||||
marginBottom={false}
|
||||
title={t('pageTitleProposals')}
|
||||
/>
|
||||
<AsyncRenderer
|
||||
loading={networkParamsLoading}
|
||||
error={networkParamsError}
|
||||
data={networkParams}
|
||||
>
|
||||
<div data-testid="proposals-list">
|
||||
<div className="grid xs:grid-cols-2 items-center">
|
||||
<Heading
|
||||
centerContent={false}
|
||||
marginBottom={false}
|
||||
title={t('pageTitleProposals')}
|
||||
/>
|
||||
|
||||
{DocsLinks && (
|
||||
<div className="xs:justify-self-end" data-testid="new-proposal-link">
|
||||
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE}>
|
||||
<Button variant="primary" size="sm">
|
||||
<div className="flex items-center gap-1">
|
||||
{t('NewProposal')}
|
||||
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
|
||||
</div>
|
||||
</Button>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
{DocsLinks && (
|
||||
<div
|
||||
className="xs:justify-self-end"
|
||||
data-testid="new-proposal-link"
|
||||
>
|
||||
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE}>
|
||||
<Button variant="primary" size="sm">
|
||||
<div className="flex items-center gap-1">
|
||||
{t('NewProposal')}
|
||||
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
|
||||
</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">
|
||||
{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>
|
||||
<section className="-mx-4 p-4 mb-8 bg-vega-dark-100">
|
||||
<SubHeading title={t('openProposals')} />
|
||||
|
||||
{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
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{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}
|
||||
/>
|
||||
))}
|
||||
|
||||
<section className="-mx-4 p-4 mb-8 bg-vega-dark-100">
|
||||
<SubHeading title={t('openProposals')} />
|
||||
|
||||
{sortedProposals.open.length > 0 ||
|
||||
sortedProtocolUpgradeProposals.open.length > 0 ? (
|
||||
<ul data-testid="open-proposals">
|
||||
{filterString.length < 1 &&
|
||||
sortedProtocolUpgradeProposals.open.map((proposal) => (
|
||||
<ProtocolUpgradeProposalsListItem
|
||||
key={proposal.upgradeBlockHeight}
|
||||
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
|
||||
<ProposalsListItem
|
||||
key={proposal?.id}
|
||||
proposal={proposal}
|
||||
networkParams={networkParams}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="mb-0" data-testid="no-open-proposals">
|
||||
{t('noOpenProposals')}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
|
||||
<ProposalsListItem key={proposal?.id} proposal={proposal} />
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="mb-0" data-testid="no-open-proposals">
|
||||
{t('noOpenProposals')}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="relative">
|
||||
<SubHeading title={t('closedProposals')} />
|
||||
{sortedProposals.closed.length > 0 ||
|
||||
sortedProtocolUpgradeProposals.closed.length > 0 ? (
|
||||
<>
|
||||
{
|
||||
// We need both the closed proposals and closed protocol upgrade
|
||||
// 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
|
||||
// do not have the necessary fields for filtering.
|
||||
sortedProposals.closed.length > 0 &&
|
||||
sortedProtocolUpgradeProposals.closed.length > 0 &&
|
||||
filterString.length < 1 && (
|
||||
<div
|
||||
className="grid w-full justify-end xl:-mt-12 pb-6"
|
||||
data-testid="toggle-closed-proposals"
|
||||
>
|
||||
<div className="w-[440px]">
|
||||
<Toggle
|
||||
name="closed-proposals-toggle"
|
||||
toggles={[
|
||||
{
|
||||
label: t(
|
||||
ClosedProposalsViewOptions.NetworkGovernance
|
||||
),
|
||||
value: ClosedProposalsViewOptions.NetworkGovernance,
|
||||
},
|
||||
{
|
||||
label: t(
|
||||
ClosedProposalsViewOptions.NetworkUpgrades
|
||||
),
|
||||
value: ClosedProposalsViewOptions.NetworkUpgrades,
|
||||
},
|
||||
]}
|
||||
checkedValue={closedProposalsView}
|
||||
onChange={(e) =>
|
||||
setClosedProposalsView(
|
||||
e.target.value as ClosedProposalsViewOptions
|
||||
)
|
||||
}
|
||||
/>
|
||||
<section className="relative">
|
||||
<SubHeading title={t('closedProposals')} />
|
||||
{sortedProposals.closed.length > 0 ||
|
||||
sortedProtocolUpgradeProposals.closed.length > 0 ? (
|
||||
<>
|
||||
{
|
||||
// We need both the closed proposals and closed protocol upgrade
|
||||
// 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
|
||||
// do not have the necessary fields for filtering.
|
||||
sortedProposals.closed.length > 0 &&
|
||||
sortedProtocolUpgradeProposals.closed.length > 0 &&
|
||||
filterString.length < 1 && (
|
||||
<div
|
||||
className="grid w-full justify-end xl:-mt-12 pb-6"
|
||||
data-testid="toggle-closed-proposals"
|
||||
>
|
||||
<div className="w-[440px]">
|
||||
<Toggle
|
||||
name="closed-proposals-toggle"
|
||||
toggles={[
|
||||
{
|
||||
label: t(
|
||||
ClosedProposalsViewOptions.NetworkGovernance
|
||||
),
|
||||
value:
|
||||
ClosedProposalsViewOptions.NetworkGovernance,
|
||||
},
|
||||
{
|
||||
label: t(
|
||||
ClosedProposalsViewOptions.NetworkUpgrades
|
||||
),
|
||||
value: ClosedProposalsViewOptions.NetworkUpgrades,
|
||||
},
|
||||
]}
|
||||
checkedValue={closedProposalsView}
|
||||
onChange={(e) =>
|
||||
setClosedProposalsView(
|
||||
e.target.value as ClosedProposalsViewOptions
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
<ul data-testid="closed-proposals">
|
||||
{closedProposalsView ===
|
||||
ClosedProposalsViewOptions.NetworkUpgrades && (
|
||||
<div data-testid="closed-upgrade-proposals">
|
||||
{sortedProtocolUpgradeProposals.closed.map((proposal) => (
|
||||
<ProtocolUpgradeProposalsListItem
|
||||
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}
|
||||
<ul data-testid="closed-proposals">
|
||||
{closedProposalsView ===
|
||||
ClosedProposalsViewOptions.NetworkUpgrades && (
|
||||
<div data-testid="closed-upgrade-proposals">
|
||||
{sortedProtocolUpgradeProposals.closed.map((proposal) => (
|
||||
<ProtocolUpgradeProposalsListItem
|
||||
key={proposal.upgradeBlockHeight}
|
||||
proposal={proposal}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<p className="mb-0" data-testid="no-closed-proposals">
|
||||
{t('noClosedProposals')}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Link className="underline" to={Routes.PROPOSALS_REJECTED}>
|
||||
{t('seeRejectedProposals')}
|
||||
</Link>
|
||||
</>
|
||||
{closedProposalsView ===
|
||||
ClosedProposalsViewOptions.NetworkGovernance && (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
+24
-10
@@ -5,7 +5,7 @@ import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { RejectedProposalsList } from './rejected-proposals-list';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import {
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
@@ -55,24 +55,38 @@ afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
jest.mock('../vote-details/use-user-vote', () => ({
|
||||
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
|
||||
}));
|
||||
|
||||
describe('Rejected proposals list', () => {
|
||||
it('Renders a list of proposals', () => {
|
||||
it('Renders a list of proposals', async () => {
|
||||
render(
|
||||
renderComponent([
|
||||
rejectedProposalClosedLastMonth,
|
||||
rejectedProposalClosesNextWeek,
|
||||
])
|
||||
);
|
||||
const rejectedProposals = within(screen.getByTestId('rejected-proposals'));
|
||||
const rejectedProposalsItems = rejectedProposals.getAllByTestId(
|
||||
'proposals-list-item'
|
||||
);
|
||||
expect(rejectedProposalsItems).toHaveLength(2);
|
||||
|
||||
await waitFor(() => {
|
||||
const rejectedProposals = within(
|
||||
screen.getByTestId('rejected-proposals')
|
||||
);
|
||||
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([]));
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+29
-3
@@ -1,10 +1,15 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { ProposalsListItem } from '../proposals-list-item';
|
||||
import { ProposalsListFilter } from '../proposals-list-filter';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
|
||||
interface ProposalsListProps {
|
||||
proposals: Array<ProposalQuery['proposal'] | ProposalFieldsFragment>;
|
||||
@@ -12,6 +17,19 @@ interface ProposalsListProps {
|
||||
|
||||
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
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 filterPredicate = (
|
||||
@@ -21,7 +39,11 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
p?.party?.id?.toString().includes(filterString);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AsyncRenderer
|
||||
loading={networkParamsLoading}
|
||||
error={networkParamsError}
|
||||
data={networkParams}
|
||||
>
|
||||
<Heading title={t('pageTitleRejectedProposals')} />
|
||||
<ProposalsListFilter
|
||||
filterString={filterString}
|
||||
@@ -31,7 +53,11 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
{proposals.length > 0 ? (
|
||||
<ul data-testid="rejected-proposals">
|
||||
{proposals.filter(filterPredicate).map((proposal) => (
|
||||
<ProposalsListItem key={proposal?.id} proposal={proposal} />
|
||||
<ProposalsListItem
|
||||
key={proposal?.id}
|
||||
proposal={proposal}
|
||||
networkParams={networkParams}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
@@ -40,6 +66,6 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
-37
@@ -20,43 +20,6 @@ const renderComponent = (proposal: ProtocolUpgradeProposalFieldsFragment) =>
|
||||
);
|
||||
|
||||
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', () => {
|
||||
renderComponent(proposal);
|
||||
const releaseTag = screen.getByTestId(
|
||||
|
||||
+28
-73
@@ -1,17 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
Lozenge,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Button, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
|
||||
import { stripFullStops } from '@vegaprotocol/utils';
|
||||
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { ProposalInfoLabel } from '../proposal-info-label';
|
||||
import Routes from '../../../routes';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
|
||||
interface ProtocolProposalsListItemProps {
|
||||
@@ -24,45 +17,21 @@ export const ProtocolUpgradeProposalsListItem = ({
|
||||
const { t } = useTranslation();
|
||||
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 (
|
||||
<li
|
||||
id={proposal.upgradeBlockHeight}
|
||||
data-testid="protocol-upgrade-proposals-list-item"
|
||||
>
|
||||
<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
|
||||
data-testid="protocol-upgrade-proposal-title"
|
||||
className="text-sm mb-2"
|
||||
@@ -70,30 +39,13 @@ export const ProtocolUpgradeProposalsListItem = ({
|
||||
<SubHeading title={`Vega release ${proposal.vegaReleaseTag}`} />
|
||||
</div>
|
||||
|
||||
<div className="text-sm">
|
||||
<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 className="flex items-center gap-4 text-vega-light-200">
|
||||
<div
|
||||
data-testid="protocol-upgrade-proposal-release-tag"
|
||||
className="mb-2"
|
||||
>
|
||||
<span>{t('vegaReleaseTag')}:</span>{' '}
|
||||
<Lozenge>{proposal.vegaReleaseTag}</Lozenge>
|
||||
<span>{proposal.vegaReleaseTag}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -101,21 +53,24 @@ export const ProtocolUpgradeProposalsListItem = ({
|
||||
className="mb-2"
|
||||
>
|
||||
<span>{t('upgradeBlockHeight')}:</span>{' '}
|
||||
<Lozenge>{proposal.upgradeBlockHeight}</Lozenge>
|
||||
</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>
|
||||
<span>{proposal.upgradeBlockHeight}</span>
|
||||
</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>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -24,8 +24,8 @@ interface VoteButtonsContainerProps {
|
||||
voteDatetime: Date | null;
|
||||
proposalId: string | null;
|
||||
proposalState: ProposalState;
|
||||
minVoterBalance: string | null;
|
||||
spamProtectionMinTokens: string | null;
|
||||
minVoterBalance: string | null | undefined;
|
||||
spamProtectionMinTokens: string | null | undefined;
|
||||
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
|
||||
dialog: (props: DialogProps) => JSX.Element;
|
||||
className?: string;
|
||||
|
||||
@@ -17,8 +17,8 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
|
||||
interface VoteDetailsProps {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
minVoterBalance: string | null;
|
||||
spamProtectionMinTokens: string | null;
|
||||
minVoterBalance: string | null | undefined;
|
||||
spamProtectionMinTokens: string | null | undefined;
|
||||
proposalType: ProposalType | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { Proposal } from '../components/proposal';
|
||||
@@ -10,12 +10,43 @@ import { ENV } from '../../../config';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { marketInfoWithDataProvider } from '@vegaprotocol/markets';
|
||||
import { useAssetQuery } from '@vegaprotocol/assets';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
|
||||
export const ProposalContainer = () => {
|
||||
const [
|
||||
mostRecentlyEnactedAssociatedMarketProposal,
|
||||
setMostRecentlyEnactedAssociatedMarketProposal,
|
||||
] = useState(undefined);
|
||||
const params = useParams<{ proposalId: string }>();
|
||||
|
||||
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}`);
|
||||
|
||||
const { data, loading, error, refetch } = useProposalQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
errorPolicy: 'ignore',
|
||||
@@ -23,6 +54,35 @@ export const ProposalContainer = () => {
|
||||
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 {
|
||||
data: newMarketData,
|
||||
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(() => {
|
||||
const interval = setInterval(refetch, 2000);
|
||||
return () => clearInterval(interval);
|
||||
@@ -62,20 +155,53 @@ export const ProposalContainer = () => {
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={loading || newMarketLoading || assetLoading}
|
||||
error={error || newMarketError || assetError}
|
||||
loading={
|
||||
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,
|
||||
...networkParams,
|
||||
...(newMarketData ? { newMarketData } : {}),
|
||||
...(assetData ? { assetData } : {}),
|
||||
...(restData ? { restData } : {}),
|
||||
...(originalMarketProposalRestData
|
||||
? { originalMarketProposalRestData }
|
||||
: {}),
|
||||
...(previouslyEnactedMarketProposalsRestData
|
||||
? { previouslyEnactedMarketProposalsRestData }
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{data?.proposal ? (
|
||||
<Proposal
|
||||
proposal={data.proposal}
|
||||
networkParams={networkParams}
|
||||
restData={restData}
|
||||
newMarketData={newMarketData}
|
||||
assetData={assetData}
|
||||
originalMarketProposalRestData={originalMarketProposalRestData}
|
||||
mostRecentlyEnactedAssociatedMarketProposal={
|
||||
mostRecentlyEnactedAssociatedMarketProposal
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<ProposalNotFound />
|
||||
|
||||
+5
@@ -60,6 +60,11 @@ const mockConsensusValidators: NodesFragmentFragment[] = [
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
useVegaRelease: jest.fn(),
|
||||
useVegaReleases: jest.fn(),
|
||||
}));
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<ProtocolUpgradeProposal proposal={mockProposal} consensusValidators={[]} />
|
||||
|
||||
@@ -2,6 +2,10 @@ import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
|
||||
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 = {
|
||||
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 oneHour = oneMinute * 60;
|
||||
const oneDay = oneHour * 24;
|
||||
@@ -62,3 +76,34 @@ export const lastWeek = new Date(-oneWeek);
|
||||
export const nextWeek = new Date(oneWeek);
|
||||
export const lastMonth = 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(),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
-15
@@ -5,7 +5,6 @@ import {
|
||||
RewardsTable,
|
||||
} from '../shared-rewards-table-assets/shared-rewards-table-assets';
|
||||
import type { EpochIndividualReward } from './generate-epoch-individual-rewards-list';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface EpochIndividualRewardsGridProps {
|
||||
data: EpochIndividualReward;
|
||||
@@ -22,14 +21,11 @@ interface RewardItemProps {
|
||||
const DisplayReward = ({
|
||||
reward,
|
||||
decimals,
|
||||
percentageOfTotal,
|
||||
}: {
|
||||
reward: string;
|
||||
decimals: number;
|
||||
percentageOfTotal?: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (Number(reward) === 0) {
|
||||
return <span className="text-vega-dark-300">-</span>;
|
||||
}
|
||||
@@ -39,23 +35,12 @@ const DisplayReward = ({
|
||||
description={
|
||||
<div className="flex flex-col items-start">
|
||||
<span>{formatNumber(toBigNum(reward, decimals), decimals)}</span>
|
||||
{percentageOfTotal && (
|
||||
<span className="text-vega-dark-300">
|
||||
({percentageOfTotal}% {t('ofTotalDistributed')})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button>
|
||||
<div className="flex flex-col items-start">
|
||||
<span>{formatNumber(toBigNum(reward, decimals), 4)}</span>
|
||||
{percentageOfTotal && (
|
||||
<span className="text-vega-dark-300">
|
||||
({formatNumber(percentageOfTotal, 4).toString()}
|
||||
%)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
-76
@@ -168,17 +168,6 @@ describe('generateEpochIndividualRewardsList', () => {
|
||||
expect(result2[1].epoch).toEqual(1);
|
||||
});
|
||||
|
||||
it('correctly calculates the total value of rewards for an asset', () => {
|
||||
const rewards = [reward1, reward4];
|
||||
const result = generateEpochIndividualRewardsList({
|
||||
rewards,
|
||||
epochId: 1,
|
||||
epochRewardSummaries: [],
|
||||
});
|
||||
|
||||
expect(result[0].rewards[0].totalAmount).toEqual('200');
|
||||
});
|
||||
|
||||
it('returns data in the expected shape', () => {
|
||||
// Just sanity checking the whole structure here
|
||||
const rewards = [reward1, reward2, reward3, reward4];
|
||||
@@ -458,69 +447,4 @@ describe('generateEpochIndividualRewardsList', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('correctly calculates the percentage of two or more rewards by referencing the total rewards amount', () => {
|
||||
const result = generateEpochIndividualRewardsList({
|
||||
rewards: [
|
||||
// reward1 is 100 usd, which is 10% of the total rewards amount
|
||||
reward1,
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||
amount: '200',
|
||||
percentageOfTotal: '0.2',
|
||||
receivedAt: new Date(),
|
||||
asset: { id: 'usd', symbol: 'USD', name: 'USD', decimals: 6 },
|
||||
party: { id: 'blah' },
|
||||
epoch: { id: '1' },
|
||||
},
|
||||
],
|
||||
epochId: 1,
|
||||
epochRewardSummaries: [
|
||||
{
|
||||
__typename: 'EpochRewardSummary',
|
||||
epoch: 1,
|
||||
assetId: 'usd',
|
||||
amount: '1000',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
epoch: 1,
|
||||
rewards: [
|
||||
{
|
||||
asset: 'USD',
|
||||
decimals: 6,
|
||||
totalAmount: '300',
|
||||
rewardTypes: {
|
||||
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
|
||||
amount: '300',
|
||||
percentageOfTotal: '30',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
|
||||
amount: '0',
|
||||
percentageOfTotal: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-16
@@ -59,7 +59,6 @@ export const generateEpochIndividualRewardsList = ({
|
||||
const epochIndividualRewards = rewards.reduce((acc, reward) => {
|
||||
const epochId = reward.epoch.id;
|
||||
const assetName = reward.asset.name;
|
||||
const assetId = reward.asset.id;
|
||||
const assetDecimals = reward.asset.decimals;
|
||||
const rewardType = reward.rewardType;
|
||||
const amount = reward.amount;
|
||||
@@ -77,13 +76,6 @@ export const generateEpochIndividualRewardsList = ({
|
||||
const epoch = acc.get(epochId);
|
||||
|
||||
// matchingTotalReward is the total awarded for all users for the reward type in the epoch of the asset
|
||||
const matchingTotalRewardAmount = epochRewardSummaries.find(
|
||||
(summary) =>
|
||||
summary.epoch === Number(epochId) &&
|
||||
summary.assetId === assetId &&
|
||||
summary.rewardType === rewardType
|
||||
)?.amount;
|
||||
|
||||
let asset = epoch?.rewards.find((r) => r.asset === assetName);
|
||||
|
||||
if (!asset) {
|
||||
@@ -106,14 +98,7 @@ export const generateEpochIndividualRewardsList = ({
|
||||
|
||||
asset.rewardTypes[rewardType] = {
|
||||
amount: newAmount,
|
||||
percentageOfTotal: matchingTotalRewardAmount
|
||||
? new BigNumber(newAmount)
|
||||
.dividedBy(matchingTotalRewardAmount)
|
||||
.multipliedBy(100)
|
||||
.toString()
|
||||
: // this should never be reached, if there's an individual reward there should
|
||||
// always be a reward total from the api too, but set it as a fallback just in case
|
||||
percentageOfTotal,
|
||||
percentageOfTotal: percentageOfTotal,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -61,8 +61,6 @@ export const RewardsPage = () => {
|
||||
error: paramsError,
|
||||
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
|
||||
|
||||
console.log('params', params);
|
||||
|
||||
const payoutDuration = useMemo(() => {
|
||||
if (!params) {
|
||||
return 0;
|
||||
|
||||
@@ -8,9 +8,10 @@ import { TxState } from '../../../hooks/transaction-reducer';
|
||||
import { useTransaction } from '../../../hooks/use-transaction';
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { AssociateInfo } from './associate-info';
|
||||
import { removeDecimal, toBigNum } from '@vegaprotocol/utils';
|
||||
import { toBigNum } from '@vegaprotocol/utils';
|
||||
import type { EthereumConfig } from '@vegaprotocol/web3';
|
||||
import { useBalances } from '../../../lib/balances/balances-store';
|
||||
import { MaxUint256 } from '@ethersproject/constants';
|
||||
|
||||
export const WalletAssociate = ({
|
||||
perform,
|
||||
@@ -42,7 +43,7 @@ export const WalletAssociate = ({
|
||||
} = useTransaction(() => {
|
||||
return token.approve(
|
||||
ethereumConfig.staking_bridge_contract.address,
|
||||
removeDecimal('1000000', decimals).toString()
|
||||
MaxUint256.toString()
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Styles required to (effectively) un-override the
|
||||
/* Styles required to (effectively) un-override the
|
||||
* reset styles so that the Proposal description fields
|
||||
* render as you'd expect them to.
|
||||
*
|
||||
@@ -97,3 +97,33 @@
|
||||
.react-markdown-container ul li {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -535,6 +535,7 @@ function checkIfDataAndTimeOfCreationAndUpdateIsEqual(date: string) {
|
||||
// unexpected latency
|
||||
const minBefore = subSeconds(new Date(), 5);
|
||||
const maxAfter = addSeconds(new Date(), 5);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(maxAfter);
|
||||
const date = new Date($dateTime.toString());
|
||||
expect(isAfter(date, minBefore) && isBefore(date, maxAfter)).to.equal(
|
||||
|
||||
@@ -363,12 +363,11 @@ describe('Closed markets', { tags: '@smoke' }, () => {
|
||||
.first()
|
||||
.find('button svg')
|
||||
.should('exist');
|
||||
|
||||
if (Cypress.env('NX_SUCCESSOR_MARKETS')) {
|
||||
cy.get(rowSelector)
|
||||
.find('[col-id="successorMarketID"]')
|
||||
.find('[col-id="successorMarket"]')
|
||||
.first()
|
||||
.should('have.text', ' - ');
|
||||
.should('have.text', '-');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -68,22 +68,4 @@ describe('home', { tags: '@regression' }, () => {
|
||||
cy.getByTestId('connect').click();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Network switcher', () => {
|
||||
before(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it('switch to fairground network and check status & incidents link', () => {
|
||||
// 0006-NETW-002
|
||||
// 0006-NETW-003
|
||||
cy.getByTestId('navigation')
|
||||
.find('[data-testid="network-switcher"]')
|
||||
.should('have.text', 'Custom')
|
||||
.click();
|
||||
cy.getByTestId('network-item').contains('Fairground testnet');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { MarketsQuery } from '@vegaprotocol/markets';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const rowSelector =
|
||||
'[data-testid="tab-all-markets"] .ag-center-cols-container .ag-row';
|
||||
'[data-testid="tab-open-markets"] .ag-center-cols-container .ag-row';
|
||||
const colInstrumentCode = '[col-id="tradableInstrument.instrument.code"]';
|
||||
|
||||
describe('markets all table', { tags: '@smoke' }, () => {
|
||||
@@ -27,13 +27,14 @@ describe('markets all table', { tags: '@smoke' }, () => {
|
||||
'Description',
|
||||
'Trading mode',
|
||||
'Status',
|
||||
'Successor market',
|
||||
'Best bid',
|
||||
'Best offer',
|
||||
'Mark price',
|
||||
'Settlement asset',
|
||||
'',
|
||||
];
|
||||
cy.getByTestId('tab-all-markets').within(($headers) => {
|
||||
cy.getByTestId('tab-open-markets').within(($headers) => {
|
||||
cy.wrap($headers)
|
||||
.get('.ag-header-cell-text')
|
||||
.each(($header, i) => {
|
||||
@@ -43,7 +44,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('markets tab should be rendered properly', () => {
|
||||
cy.get('[data-testid="All markets"]').should(
|
||||
cy.get('[data-testid="Open markets"]').should(
|
||||
'have.attr',
|
||||
'data-state',
|
||||
'active'
|
||||
@@ -165,7 +166,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
|
||||
'ETHBTC.QM21',
|
||||
'SOLUSD',
|
||||
];
|
||||
cy.get('[data-testid="All markets"]').click({ force: true });
|
||||
cy.get('[data-testid="Open markets"]').click({ force: true });
|
||||
cy.url().should('eq', Cypress.config('baseUrl') + '/#/markets/all');
|
||||
cy.contains('AAPL.MF21').should('be.visible');
|
||||
cy.get('.ag-header-cell-label').contains('Market').click(); // sort by market name
|
||||
@@ -190,7 +191,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('no all markets', { tags: '@smoke', testIsolation: true }, () => {
|
||||
describe('no open markets', { tags: '@smoke', testIsolation: true }, () => {
|
||||
before(() => {
|
||||
cy.mockTradingPage();
|
||||
const markets: MarketsQuery = {};
|
||||
@@ -203,6 +204,6 @@ describe('no all markets', { tags: '@smoke', testIsolation: true }, () => {
|
||||
|
||||
it.skip('can see no markets message', () => {
|
||||
// 6001-MARK-048
|
||||
cy.getByTestId('tab-all-markets').should('contain.text', 'No markets');
|
||||
cy.getByTestId('tab-open-markets').should('contain.text', 'No markets');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MarketTradingModeMapping } from '@vegaprotocol/types';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import compact from 'lodash/compact';
|
||||
|
||||
const accordionContent = 'accordion-content';
|
||||
const blockExplorerLink = 'block-explorer-link';
|
||||
@@ -66,20 +67,24 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
// 6002-MDET-201
|
||||
cy.getByTestId(marketTitle).contains('Key details').click();
|
||||
|
||||
validateMarketDataRow(0, 'Name', 'BTCUSD Monthly (30 Jun 2022)');
|
||||
validateMarketDataRow(1, 'Market ID', 'market-0');
|
||||
const rows: [string, string][] = compact([
|
||||
['Name', 'BTCUSD Monthly (30 Jun 2022)'],
|
||||
['Market ID', 'market-0'],
|
||||
Cypress.env('NX_SUCCESSOR_MARKETS') && ['Parent Market ID', 'PARENT-A'],
|
||||
Cypress.env('NX_SUCCESSOR_MARKETS') && [
|
||||
'Insurance Pool Fraction',
|
||||
'0.75',
|
||||
],
|
||||
['Trading Mode', MarketTradingModeMapping.TRADING_MODE_CONTINUOUS],
|
||||
['Market Decimal Places', '5'],
|
||||
['Position Decimal Places', '0'],
|
||||
['Settlement Asset Decimal Places', '5'],
|
||||
]);
|
||||
|
||||
if (Cypress.env('NX_SUCCESSOR_MARKETS')) {
|
||||
validateMarketDataRow(2, 'Parent Market ID', 'PARENT-A');
|
||||
for (const rowNumber in rows) {
|
||||
const [name, value] = rows[rowNumber];
|
||||
validateMarketDataRow(Number(rowNumber), name, value);
|
||||
}
|
||||
validateMarketDataRow(
|
||||
3,
|
||||
'Trading Mode',
|
||||
MarketTradingModeMapping.TRADING_MODE_CONTINUOUS
|
||||
);
|
||||
validateMarketDataRow(4, 'Market Decimal Places', '5');
|
||||
validateMarketDataRow(5, 'Position Decimal Places', '0');
|
||||
validateMarketDataRow(6, 'Settlement Asset Decimal Places', '5');
|
||||
});
|
||||
|
||||
it('instrument displayed', () => {
|
||||
|
||||
@@ -19,6 +19,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
|
||||
'Description',
|
||||
'Settlement asset',
|
||||
'State',
|
||||
'Parent market',
|
||||
'Voting',
|
||||
'Closing date',
|
||||
'Enactment date',
|
||||
@@ -83,7 +84,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
|
||||
.find('button')
|
||||
.click();
|
||||
|
||||
const dropdownContent = '[data-testid="market-actions-content"]';
|
||||
const dropdownContent = '[data-testid="proposal-actions-content"]';
|
||||
const dropdownContentItem = '[role="menuitem"]';
|
||||
|
||||
// 6001-MARK-059
|
||||
@@ -99,7 +100,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
|
||||
'VEGA_TOKEN_URL'
|
||||
)}/proposals/e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829`
|
||||
);
|
||||
cy.getByTestId('market-actions-content').click();
|
||||
cy.getByTestId('proposal-actions-content').click();
|
||||
});
|
||||
|
||||
// 6001-MARK-060
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import { mockConnectWallet } from '@vegaprotocol/cypress';
|
||||
|
||||
describe('Navbar', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.clearAllLocalStorage();
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/');
|
||||
cy.wait('@Markets');
|
||||
cy.wait('@MarketsData');
|
||||
});
|
||||
|
||||
const pages = [
|
||||
{ name: 'Markets', link: '#/markets/all' },
|
||||
{ name: 'Trading', link: '#/markets' },
|
||||
{ name: 'Portfolio', link: '#/portfolio' },
|
||||
];
|
||||
|
||||
describe('desktop view', () => {
|
||||
pages.forEach(({ name, link }) => {
|
||||
it(`${name} should be correctly rendered`, () => {
|
||||
cy.get('nav')
|
||||
.find(`a[data-testid=${name}]:visible`)
|
||||
.then((element) => {
|
||||
cy.wrap(element).click();
|
||||
cy.location('hash').should('contain', link);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Resources dropdown should be correctly rendered', () => {
|
||||
const resourceSelector = 'ul li:contains(Resources)';
|
||||
['Docs', 'Give Feedback'].forEach((text, index) => {
|
||||
cy.get('nav').find(resourceSelector).contains('Resources').click();
|
||||
cy.get('nav')
|
||||
.find(resourceSelector)
|
||||
.find('.navigation-content li')
|
||||
.eq(index)
|
||||
.find('a')
|
||||
.then((element) => {
|
||||
expect(element.attr('target')).to.eq('_blank');
|
||||
expect(element.attr('href')).to.not.be.empty;
|
||||
expect(element.text()).to.eq(text);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Disclaimer should be presented after choosing from menu', () => {
|
||||
cy.get('nav')
|
||||
.find('ul li:contains(Resources)')
|
||||
.contains('Resources')
|
||||
.click();
|
||||
cy.getByTestId('Disclaimer').eq(0).click();
|
||||
cy.location('hash').should('equal', '#/disclaimer');
|
||||
cy.get('p').contains(
|
||||
'Vega is a decentralised peer-to-peer protocol that can be used to trade derivatives with cryptoassets.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mobile view', () => {
|
||||
const viewportHeight = Cypress.config('viewportHeight');
|
||||
const viewportWidth = Cypress.config('viewportWidth');
|
||||
before(() => {
|
||||
// a little hack to keep the viewport size between tests (cypress bug)
|
||||
Cypress.config({
|
||||
viewportWidth: 560,
|
||||
viewportHeight: 890,
|
||||
});
|
||||
cy.viewport(560, 890);
|
||||
});
|
||||
|
||||
describe('wallet drawer', () => {
|
||||
it('wallet drawer should be correctly rendered', () => {
|
||||
mockConnectWallet();
|
||||
cy.connectVegaWallet(true);
|
||||
cy.getByTestId('connect-vega-wallet-mobile').click();
|
||||
cy.getByTestId('wallets-drawer').should('be.visible');
|
||||
cy.getByTestId('wallets-drawer').within((el) => {
|
||||
cy.wrap(el).get('button').contains('Disconnect').click();
|
||||
});
|
||||
cy.getByTestId('wallets-drawer').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('menu drawer', () => {
|
||||
pages.forEach(({ name, link }) => {
|
||||
it(`${name} should be correctly rendered`, () => {
|
||||
cy.getByTestId('button-menu-drawer').click();
|
||||
cy.getByTestId('menu-drawer').should('be.visible');
|
||||
cy.getByTestId('menu-drawer').within((el) => {
|
||||
cy.wrap(el).getByTestId(name).click();
|
||||
cy.location('hash').should('contain', link);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Menu drawer should not be visible until opened', () => {
|
||||
cy.getByTestId('menu-drawer').should('not.be.visible');
|
||||
cy.getByTestId('button-menu-drawer').click();
|
||||
cy.getByTestId('menu-drawer').should('be.visible');
|
||||
cy.getByTestId('button-menu-drawer').click();
|
||||
cy.getByTestId('menu-drawer').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
after(() => {
|
||||
// a little hack to keep the viewport size between tests (cypress bug)
|
||||
Cypress.config({
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -127,7 +127,7 @@ describe(
|
||||
{
|
||||
name: 'Price monitoring bounds',
|
||||
infoText:
|
||||
'Price Monitoring Bounds: Min 162.56291Max 182.96869Reference 172.47489',
|
||||
'Price Monitoring Bounds 1: Min 162.56291Max 182.96869Reference 172.47489',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -171,6 +171,7 @@ describe(
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
const actualDate = text.slice(0, -67);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(actualDate);
|
||||
const actualOhlc = text.slice(-67);
|
||||
assert.isTrue(expectedDateRegex.test(actualDate));
|
||||
|
||||
@@ -7,7 +7,7 @@ const closePosition = 'close-position';
|
||||
const dialogCloseX = 'dialog-close';
|
||||
const dialogContent = 'dialog-content';
|
||||
const dropDownMenu = 'dropdown-menu';
|
||||
const marketActionsContent = 'market-actions-content';
|
||||
const marketActionsContent = 'position-actions-content';
|
||||
const positions = 'Positions';
|
||||
const tabPositions = 'tab-positions';
|
||||
const toastContent = 'toast-content';
|
||||
|
||||
@@ -6,16 +6,17 @@ const tradesTab = 'Trades';
|
||||
const tradesTable = 'tab-trades';
|
||||
|
||||
describe('trades', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
});
|
||||
|
||||
before(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.intercept('POST', '/graphql', (req) => {
|
||||
if (req.body.operationName === 'Trades') {
|
||||
req.alias = '@Trades';
|
||||
}
|
||||
});
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId(tradesTab).click();
|
||||
cy.wait('@Trades');
|
||||
});
|
||||
|
||||
it('show trades', () => {
|
||||
@@ -52,8 +53,7 @@ describe('trades', { tags: '@smoke' }, () => {
|
||||
});
|
||||
});
|
||||
|
||||
// This won't pass in CI, but does locally
|
||||
it.skip('show trades date and time', () => {
|
||||
it('show trades date and time', () => {
|
||||
// 6005-THIS-005
|
||||
cy.getByTestId(tradesTable) // order table shares identical col id
|
||||
.find(`${colIdCreatedAt} ${colHeader}`)
|
||||
@@ -85,15 +85,9 @@ describe('trades', { tags: '@smoke' }, () => {
|
||||
});
|
||||
});
|
||||
|
||||
// this passes locally but doesn't in CI
|
||||
it.skip('copy price to deal ticket form', () => {
|
||||
cy.getByTestId('order-type-TYPE_LIMIT').click(); // make sure on limit
|
||||
it('copy price to deal ticket form', () => {
|
||||
// 6005-THIS-007
|
||||
cy.getByTestId(tradesTable)
|
||||
.find(colIdPrice)
|
||||
.last()
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.get(colIdPrice).last().should('be.visible').click();
|
||||
cy.getByTestId('order-price').should('have.value', '171.16898');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
successorMarketQuery,
|
||||
parentMarketIdQuery,
|
||||
successorMarketIdsQuery,
|
||||
successorMarketProposalDetailsQuery,
|
||||
} from '@vegaprotocol/mock';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type { MarketDataQuery, MarketsQuery } from '@vegaprotocol/markets';
|
||||
@@ -186,6 +187,11 @@ const mockTradingPage = (
|
||||
aliasGQLQuery(req, 'SuccessorMarket', successorMarketQuery());
|
||||
aliasGQLQuery(req, 'ParentMarketId', parentMarketIdQuery());
|
||||
aliasGQLQuery(req, 'SuccessorMarketIds', successorMarketIdsQuery());
|
||||
aliasGQLQuery(
|
||||
req,
|
||||
'SuccessorMarketProposalDetails',
|
||||
successorMarketProposalDetailsQuery()
|
||||
);
|
||||
};
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
|
||||
@@ -17,7 +17,10 @@ import {
|
||||
usePaneLayout,
|
||||
} from '../../components/resizable-grid';
|
||||
import { TradingViews } from './trade-views';
|
||||
import { MarketSuccessorBanner } from '../../components/market-banner';
|
||||
import {
|
||||
MarketSuccessorBanner,
|
||||
MarketSuccessorProposalBanner,
|
||||
} from '../../components/market-banner';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
|
||||
interface TradeGridProps {
|
||||
@@ -162,7 +165,12 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
<div>
|
||||
{FLAGS.SUCCESSOR_MARKETS && <MarketSuccessorBanner market={market} />}
|
||||
{FLAGS.SUCCESSOR_MARKETS && (
|
||||
<>
|
||||
<MarketSuccessorBanner market={market} />
|
||||
<MarketSuccessorProposalBanner marketId={market?.id} />
|
||||
</>
|
||||
)}
|
||||
<OracleBanner marketId={market?.id || ''} />
|
||||
</div>
|
||||
<div className="min-h-0 p-0.5">
|
||||
|
||||
@@ -12,7 +12,10 @@ import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { NO_MARKET } from './constants';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import classNames from 'classnames';
|
||||
import { MarketSuccessorBanner } from '../../components/market-banner';
|
||||
import {
|
||||
MarketSuccessorBanner,
|
||||
MarketSuccessorProposalBanner,
|
||||
} from '../../components/market-banner';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
|
||||
interface TradePanelsProps {
|
||||
@@ -65,7 +68,12 @@ export const TradePanels = ({
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
|
||||
<div>
|
||||
{FLAGS.SUCCESSOR_MARKETS && <MarketSuccessorBanner market={market} />}
|
||||
{FLAGS.SUCCESSOR_MARKETS && (
|
||||
<>
|
||||
<MarketSuccessorBanner market={market} />
|
||||
<MarketSuccessorProposalBanner marketId={market?.id} />
|
||||
</>
|
||||
)}
|
||||
<OracleBanner marketId={market?.id || ''} />
|
||||
</div>
|
||||
<div className="h-full">
|
||||
@@ -80,9 +88,12 @@ export const TradePanels = ({
|
||||
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
||||
{Object.keys(TradingViews).map((key) => {
|
||||
const isActive = view === key;
|
||||
const className = classNames('p-4 min-w-[100px] capitalize', {
|
||||
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
|
||||
});
|
||||
const className = classNames(
|
||||
'py-2 px-4 min-w-[100px] capitalize text-sm',
|
||||
{
|
||||
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
|
||||
}
|
||||
);
|
||||
return (
|
||||
<button
|
||||
data-testid={key}
|
||||
|
||||
@@ -10,15 +10,18 @@ import type {
|
||||
OracleSpecDataConnectionQuery,
|
||||
MarketsDataQuery,
|
||||
MarketsQuery,
|
||||
SuccessorMarketIdsQuery,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
OracleSpecDataConnectionDocument,
|
||||
MarketsDataDocument,
|
||||
MarketsDocument,
|
||||
SuccessorMarketIdsDocument,
|
||||
} from '@vegaprotocol/markets';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import {
|
||||
createMarketFragment,
|
||||
marketsQuery,
|
||||
@@ -46,10 +49,16 @@ jest.mock('@vegaprotocol/markets', () => ({
|
||||
: { data: undefined },
|
||||
}));
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
...jest.requireActual('@vegaprotocol/environment'),
|
||||
FLAGS: { SUCCESSOR_MARKETS: true } as Partial<FeatureFlags>,
|
||||
}));
|
||||
jest.mock('@vegaprotocol/environment', () => {
|
||||
const actual = jest.requireActual('@vegaprotocol/environment');
|
||||
return {
|
||||
...actual,
|
||||
FLAGS: {
|
||||
...actual.FLAGS,
|
||||
SUCCESSOR_MARKETS: true,
|
||||
} as FeatureFlags,
|
||||
};
|
||||
});
|
||||
|
||||
describe('Closed', () => {
|
||||
let originalNow: typeof Date.now;
|
||||
@@ -349,8 +358,25 @@ describe('Closed', () => {
|
||||
state: MarketState.STATE_SETTLED,
|
||||
}),
|
||||
},
|
||||
{
|
||||
__typename: 'MarketEdge' as const,
|
||||
node: {
|
||||
...createMarketFragment({
|
||||
id: 'successorMarketID',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
...createMarketFragment().tradableInstrument,
|
||||
instrument: {
|
||||
...createMarketFragment().tradableInstrument.instrument,
|
||||
id: 'successorAssset',
|
||||
name: 'Successor Market Name',
|
||||
code: 'SuccessorCode',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mixedMarketsMock: MockedResponse<MarketsQuery> = {
|
||||
request: {
|
||||
query: MarketsDocument,
|
||||
@@ -364,11 +390,36 @@ describe('Closed', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const successorMarketsMock: MockedResponse<SuccessorMarketIdsQuery> = {
|
||||
request: {
|
||||
query: SuccessorMarketIdsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
__typename: 'MarketConnection',
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: 'include-0',
|
||||
successorMarketID: 'successorMarketID',
|
||||
parentMarketID: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[mixedMarketsMock, marketsDataMock, oracleDataMock]}
|
||||
mocks={[
|
||||
mixedMarketsMock,
|
||||
marketsDataMock,
|
||||
oracleDataMock,
|
||||
successorMarketsMock,
|
||||
]}
|
||||
>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
@@ -384,5 +435,107 @@ describe('Closed', () => {
|
||||
screen.getByRole('button', { name: 'SuccessorCode' })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.getByRole('columnheader', {
|
||||
name: (_name, element) =>
|
||||
element.getAttribute('col-id') === 'successorMarket',
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('feature flag should hide successors', async () => {
|
||||
const mockedFlags = jest.mocked(FLAGS);
|
||||
mockedFlags.SUCCESSOR_MARKETS = false;
|
||||
|
||||
const mixedMarkets = [
|
||||
{
|
||||
__typename: 'MarketEdge' as const,
|
||||
node: createMarketFragment({
|
||||
id: 'include-0',
|
||||
state: MarketState.STATE_SETTLED,
|
||||
}),
|
||||
},
|
||||
{
|
||||
__typename: 'MarketEdge' as const,
|
||||
node: {
|
||||
...createMarketFragment({
|
||||
id: 'successorMarketID',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
...createMarketFragment().tradableInstrument,
|
||||
instrument: {
|
||||
...createMarketFragment().tradableInstrument.instrument,
|
||||
id: 'successorAssset',
|
||||
name: 'Successor Market Name',
|
||||
code: 'SuccessorCode',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const mixedMarketsMock: MockedResponse<MarketsQuery> = {
|
||||
request: {
|
||||
query: MarketsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
__typename: 'MarketConnection',
|
||||
edges: mixedMarkets,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const successorMarketsMock: MockedResponse<SuccessorMarketIdsQuery> = {
|
||||
request: {
|
||||
query: SuccessorMarketIdsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
__typename: 'MarketConnection',
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: 'include-0',
|
||||
successorMarketID: 'successorMarketID',
|
||||
parentMarketID: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[
|
||||
mixedMarketsMock,
|
||||
marketsDataMock,
|
||||
oracleDataMock,
|
||||
successorMarketsMock,
|
||||
]}
|
||||
>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
<Closed />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('columnheader', {
|
||||
name: (_name, element) =>
|
||||
element.getAttribute('col-id') === 'settlementDate',
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
screen.getAllByRole('columnheader').forEach((element) => {
|
||||
expect(element.getAttribute('col-id')).not.toEqual('successorMarket');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,7 @@ import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import {
|
||||
AgGridLazy as AgGrid,
|
||||
COL_DEFS,
|
||||
MarketNameCell,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { AgGridLazy as AgGrid, COL_DEFS } from '@vegaprotocol/datagrid';
|
||||
import { useMemo } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { MarketState, MarketStateMapping } from '@vegaprotocol/types';
|
||||
@@ -23,15 +19,14 @@ import type {
|
||||
import {
|
||||
MarketActionsDropdown,
|
||||
closedMarketsWithDataProvider,
|
||||
useSuccessorMarket,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import type { ColDef } from 'ag-grid-community';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { SettlementDateCell } from './settlement-date-cell';
|
||||
import { SettlementPriceCell } from './settlement-price-cell';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { SuccessorMarketRenderer } from './successor-market-cell';
|
||||
|
||||
type SettlementAsset =
|
||||
MarketMaybeWithData['tradableInstrument']['instrument']['product']['settlementAsset'];
|
||||
@@ -106,22 +101,6 @@ export const Closed = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const SuccessorMarketRenderer = ({
|
||||
value,
|
||||
}: VegaICellRendererParams<Row, 'id'>) => {
|
||||
const { data } = useSuccessorMarket(value);
|
||||
const onMarketClick = useMarketClickHandler();
|
||||
return data ? (
|
||||
<MarketNameCell
|
||||
value={data.tradableInstrument.instrument.code}
|
||||
data={data}
|
||||
onMarketClick={onMarketClick}
|
||||
/>
|
||||
) : (
|
||||
' - '
|
||||
);
|
||||
};
|
||||
|
||||
const ClosedMarketsDataGrid = ({
|
||||
rowData,
|
||||
error,
|
||||
@@ -202,8 +181,8 @@ const ClosedMarketsDataGrid = ({
|
||||
},
|
||||
FLAGS.SUCCESSOR_MARKETS && {
|
||||
headerName: t('Successor market'),
|
||||
colId: 'successorMarketID',
|
||||
field: 'id',
|
||||
colId: 'successorMarket',
|
||||
cellRenderer: 'SuccessorMarketRenderer',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ export const MarketsPage = () => {
|
||||
<div className="h-full pt-0.5 pb-3 px-1.5">
|
||||
<div className="h-full my-1 border border-default rounded-sm">
|
||||
<Tabs storageKey="console-markets">
|
||||
<Tab id="all-markets" name={t('All markets')}>
|
||||
<Tab id="open-markets" name={t('Open markets')}>
|
||||
<Markets />
|
||||
</Tab>
|
||||
<Tab id="proposed-markets" name={t('Proposed markets')}>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { MarketsContainer } from '@vegaprotocol/markets';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
import { SuccessorMarketRenderer } from './successor-market-cell';
|
||||
|
||||
export const Markets = () => {
|
||||
const handleOnSelect = useMarketClickHandler();
|
||||
return <MarketsContainer onSelect={handleOnSelect} />;
|
||||
return (
|
||||
<MarketsContainer
|
||||
onSelect={handleOnSelect}
|
||||
SuccessorMarketRenderer={SuccessorMarketRenderer}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '@vegaprotocol/environment';
|
||||
import { ProposalsList } from '@vegaprotocol/proposals';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { SuccessorMarketRenderer } from './successor-market-cell';
|
||||
|
||||
export const Proposed = () => {
|
||||
const tokenLink = useLinks(DApp.Token);
|
||||
@@ -13,7 +14,7 @@ export const Proposed = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="h-[400px]">
|
||||
<ProposalsList />
|
||||
<ProposalsList SuccessorMarketRenderer={SuccessorMarketRenderer} />
|
||||
</div>
|
||||
<ExternalLink className="py-4 px-[11px] text-sm" href={externalLink}>
|
||||
{t('Propose a new market')}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { MarketNameCell } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { marketProvider, useSuccessorMarketIds } from '@vegaprotocol/markets';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
import React from 'react';
|
||||
|
||||
export const SuccessorMarketRenderer = ({
|
||||
value,
|
||||
parent,
|
||||
}: {
|
||||
value: string;
|
||||
parent?: boolean;
|
||||
}) => {
|
||||
const successors = useSuccessorMarketIds(value);
|
||||
const onMarketClick = useMarketClickHandler();
|
||||
|
||||
const lookupValue = successors
|
||||
? parent
|
||||
? successors.parentMarketID
|
||||
: successors.successorMarketID
|
||||
: '';
|
||||
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: marketProvider,
|
||||
variables: {
|
||||
marketId: lookupValue || '',
|
||||
},
|
||||
skip: !lookupValue,
|
||||
});
|
||||
|
||||
return data ? (
|
||||
<MarketNameCell
|
||||
value={data.tradableInstrument.instrument.code}
|
||||
data={data}
|
||||
onMarketClick={onMarketClick}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
};
|
||||
@@ -17,7 +17,7 @@ export const Header = ({ title, children }: TradeMarketHeaderProps) => {
|
||||
);
|
||||
return (
|
||||
<header className={headerClasses}>
|
||||
<div className="flex flex-col justify-center items-start pl-3 lg:pl-4 pt-2 xl:pb-2 pb-0">
|
||||
<div className="hidden lg:flex flex-col justify-center items-start pl-3 lg:pl-4 pt-2 xl:pb-2 pb-0">
|
||||
{title}
|
||||
</div>
|
||||
<div data-testid="header-summary" className="min-w-0">
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const WalletIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
width="26"
|
||||
height="18"
|
||||
viewBox="0 0 26 18"
|
||||
className={className}
|
||||
className={classNames('fill-current', className)}
|
||||
data-testid="wallet-icon"
|
||||
>
|
||||
<path d="M4.77437 17.7499H4.74987C3.6504 17.7368 2.77439 16.8489 2.77439 15.7772V12.8495V12.6343L2.5615 12.6023C1.59672 12.4575 0.849609 11.6116 0.849609 10.6266V7.40064C0.849609 6.39018 1.59509 5.56985 2.56147 5.4249L2.77439 5.39297V5.17767V2.24998C2.77439 1.14102 3.66537 0.25 4.77437 0.25H23.7501C24.8591 0.25 25.7501 1.14098 25.7501 2.24998V15.7499C25.7501 16.8588 24.8591 17.7499 23.7501 17.7499H4.77437ZM4.44917 12.5992H4.19917L4.77441 16.075V16.325H4.77466H23.7502C24.0778 16.325 24.3254 16.0777 24.3254 15.7497V2.24984C24.3254 1.9222 24.0782 1.6746 23.7502 1.6746H4.77441C4.44677 1.6746 4.19917 1.92182 4.19917 2.24984V5.12306V5.37306H4.44917H7.0244C8.51139 5.37306 9.67508 6.56094 9.67508 8.02374V9.94852C9.67508 11.4355 8.4872 12.5992 7.0244 12.5992H4.44917ZM2.84963 6.8253C2.52199 6.8253 2.27439 7.07253 2.27439 7.40054V10.6264C2.27439 10.9541 2.52161 11.2017 2.84962 11.2017L7.02419 11.2019C7.73619 11.2019 8.25009 10.6515 8.25009 9.97598V8.0512C8.25009 7.3392 7.69976 6.8253 7.0242 6.8253H2.84963Z" />
|
||||
|
||||
@@ -11,9 +11,9 @@ export const LayoutWithSidebar = () => {
|
||||
|
||||
const gridClasses = classNames(
|
||||
'h-full relative z-0 grid',
|
||||
'grid-rows-[min-content_1fr]',
|
||||
'grid-cols-[1fr_45px]',
|
||||
'lg:grid-cols-[1fr_350px_45px]'
|
||||
'grid-rows-[min-content_1fr_40px]',
|
||||
'lg:grid-rows-[min-content_1fr]',
|
||||
'lg:grid-cols-[1fr_350px_40px]'
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -40,7 +40,14 @@ export const LayoutWithSidebar = () => {
|
||||
>
|
||||
<SidebarContent />
|
||||
</aside>
|
||||
<div className="col-start-2 lg:col-start-3 bg-vega-clight-800 dark:bg-vega-cdark-800 border-l border-default">
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800',
|
||||
'border-t lg:border-l lg:border-t-0 border-default',
|
||||
'row-start-3 col-start-1 cols-span-full',
|
||||
'lg:row-start-2 lg:row-span-full lg:col-start-3'
|
||||
)}
|
||||
>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,8 +42,6 @@ export const LiquidityHeader = () => {
|
||||
triggeringRatio,
|
||||
});
|
||||
|
||||
console.log(market);
|
||||
|
||||
return (
|
||||
<Header
|
||||
title={
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './market-successor-banner';
|
||||
export * from './market-successor-proposal-banner';
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import type { SingleExecutionResult } from '@apollo/client';
|
||||
import type { MockedResponse } from '@apollo/react-testing';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
import { MarketSuccessorProposalBanner } from './market-successor-proposal-banner';
|
||||
import type { SuccessorProposalsListQuery } from '@vegaprotocol/proposals';
|
||||
import { SuccessorProposalsListDocument } from '@vegaprotocol/proposals';
|
||||
|
||||
const marketProposalMock: MockedResponse<SuccessorProposalsListQuery> = {
|
||||
request: {
|
||||
query: SuccessorProposalsListDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposalsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
__typename: 'Proposal',
|
||||
id: 'proposal-1',
|
||||
terms: {
|
||||
__typename: 'ProposalTerms',
|
||||
change: {
|
||||
__typename: 'NewMarket',
|
||||
instrument: {
|
||||
name: 'New proposal of the market successor',
|
||||
},
|
||||
successorConfiguration: {
|
||||
parentMarketId: 'marketId',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('MarketSuccessorProposalBanner', () => {
|
||||
it('should display single proposal', async () => {
|
||||
render(
|
||||
<MockedProvider mocks={[marketProposalMock]}>
|
||||
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('A successors to this market has been proposed')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen
|
||||
.getByRole('link')
|
||||
.getAttribute('href')
|
||||
?.endsWith('/proposals/proposal-1') ?? false
|
||||
).toBe(true);
|
||||
});
|
||||
it('should display plural proposals', async () => {
|
||||
const dualProposalMock = {
|
||||
...marketProposalMock,
|
||||
result: {
|
||||
...marketProposalMock.result,
|
||||
data: {
|
||||
proposalsConnection: {
|
||||
edges: [
|
||||
...((
|
||||
marketProposalMock?.result as SingleExecutionResult<SuccessorProposalsListQuery>
|
||||
)?.data?.proposalsConnection?.edges ?? []),
|
||||
{
|
||||
node: {
|
||||
__typename: 'Proposal',
|
||||
id: 'proposal-2',
|
||||
terms: {
|
||||
__typename: 'ProposalTerms',
|
||||
change: {
|
||||
__typename: 'NewMarket',
|
||||
instrument: {
|
||||
name: 'New second proposal of the market successor',
|
||||
},
|
||||
successorConfiguration: {
|
||||
parentMarketId: 'marketId',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
render(
|
||||
<MockedProvider mocks={[dualProposalMock]}>
|
||||
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('Successors to this market have been proposed')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen
|
||||
.getAllByRole('link')[0]
|
||||
.getAttribute('href')
|
||||
?.endsWith('/proposals/proposal-1') ?? false
|
||||
).toBe(true);
|
||||
expect(
|
||||
screen
|
||||
.getAllByRole('link')[1]
|
||||
.getAttribute('href')
|
||||
?.endsWith('/proposals/proposal-2') ?? false
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('banner should be hidden because no proposals', () => {
|
||||
const { container } = render(
|
||||
<MockedProvider>
|
||||
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('banner should be hidden because no proposals for the market', () => {
|
||||
const { container } = render(
|
||||
<MockedProvider mocks={[marketProposalMock]}>
|
||||
<MarketSuccessorProposalBanner marketId="otherMarketId" />
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('banner should be hidden after user close click', async () => {
|
||||
const { container } = render(
|
||||
<MockedProvider mocks={[marketProposalMock]}>
|
||||
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('A successors to this market has been proposed')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
await act(() => {
|
||||
screen.getByTestId('notification-banner-close').click();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useState } from 'react';
|
||||
import type {
|
||||
SuccessorProposalListFieldsFragment,
|
||||
NewMarketSuccessorFieldsFragment,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { useSuccessorProposalsListQuery } from '@vegaprotocol/proposals';
|
||||
import {
|
||||
ExternalLink,
|
||||
Intent,
|
||||
NotificationBanner,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
|
||||
export const MarketSuccessorProposalBanner = ({
|
||||
marketId,
|
||||
}: {
|
||||
marketId?: string;
|
||||
}) => {
|
||||
const { data: proposals } = useSuccessorProposalsListQuery({
|
||||
skip: !marketId,
|
||||
});
|
||||
const successors =
|
||||
proposals?.proposalsConnection?.edges
|
||||
?.map((item) => item?.node as SuccessorProposalListFieldsFragment)
|
||||
.filter(
|
||||
(item: SuccessorProposalListFieldsFragment) =>
|
||||
(item.terms?.change as NewMarketSuccessorFieldsFragment)
|
||||
?.successorConfiguration?.parentMarketId === marketId
|
||||
) ?? [];
|
||||
const [visible, setVisible] = useState(true);
|
||||
const tokenLink = useLinks(DApp.Token);
|
||||
if (visible && successors.length) {
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={Intent.Primary}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<div className="uppercase mb-1">
|
||||
{successors.length === 1
|
||||
? t('A successors to this market has been proposed')
|
||||
: t('Successors to this market have been proposed')}
|
||||
</div>
|
||||
<div>
|
||||
{successors.length === 1
|
||||
? t('Check out the terms of the proposal and vote:')
|
||||
: t('Check out the terms of the proposals and vote:')}{' '}
|
||||
{successors.map((item, i) => {
|
||||
const externalLink = tokenLink(
|
||||
TOKEN_PROPOSAL.replace(':id', item.id || '')
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ExternalLink href={externalLink} key={i}>
|
||||
{
|
||||
(item.terms?.change as NewMarketSuccessorFieldsFragment)
|
||||
?.instrument.name
|
||||
}
|
||||
</ExternalLink>
|
||||
{i < successors.length - 1 && ', '}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</NotificationBanner>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -4,10 +4,12 @@ import { useParams } from 'react-router-dom';
|
||||
import { MarketSelector } from '../../components/market-selector/market-selector';
|
||||
import { MarketHeaderStats } from '../../client-pages/market/market-header-stats';
|
||||
import { useMarket } from '@vegaprotocol/markets';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const MarketHeader = () => {
|
||||
const { marketId } = useParams();
|
||||
const { data } = useMarket(marketId);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
@@ -15,6 +17,8 @@ export const MarketHeader = () => {
|
||||
<Header
|
||||
title={
|
||||
<Popover
|
||||
open={open}
|
||||
onChange={setOpen}
|
||||
trigger={
|
||||
<HeaderTitle>
|
||||
{data.tradableInstrument.instrument.code}
|
||||
@@ -23,7 +27,10 @@ export const MarketHeader = () => {
|
||||
}
|
||||
alignOffset={-10}
|
||||
>
|
||||
<MarketSelector currentMarketId={marketId} />
|
||||
<MarketSelector
|
||||
currentMarketId={marketId}
|
||||
onSelect={() => setOpen(false)}
|
||||
/>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -98,6 +98,7 @@ describe('MarketSelectorItem', () => {
|
||||
market={market}
|
||||
currentMarketId={market.id}
|
||||
style={{}}
|
||||
onSelect={jest.fn()}
|
||||
/>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
|
||||
@@ -17,10 +17,12 @@ export const MarketSelectorItem = ({
|
||||
market,
|
||||
style,
|
||||
currentMarketId,
|
||||
onSelect,
|
||||
}: {
|
||||
market: MarketMaybeWithDataAndCandles;
|
||||
style: CSSProperties;
|
||||
currentMarketId?: string;
|
||||
onSelect: (marketId: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div style={style} role="row">
|
||||
@@ -32,6 +34,7 @@ export const MarketSelectorItem = ({
|
||||
'bg-vega-clight-600 dark:bg-vega-cdark-600':
|
||||
market.id === currentMarketId,
|
||||
})}
|
||||
onClick={() => onSelect(market.id)}
|
||||
>
|
||||
<MarketData market={market} />
|
||||
</Link>
|
||||
@@ -80,7 +83,7 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="w-2/5" role="gridcell">
|
||||
<h3 className="text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
<h3 className="text-ellipsis text-sm lg:text-base whitespace-nowrap overflow-hidden">
|
||||
{market.tradableInstrument.instrument.code}
|
||||
</h3>
|
||||
{mode && (
|
||||
@@ -90,7 +93,7 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="w-1/5 text-sm whitespace-nowrap text-ellipsis overflow-hidden"
|
||||
className="w-1/5 text-xs lg:text-sm whitespace-nowrap text-ellipsis overflow-hidden"
|
||||
title={instrument.product.settlementAsset.symbol}
|
||||
data-testid="market-selector-price"
|
||||
role="gridcell"
|
||||
@@ -98,7 +101,7 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
|
||||
{price} {instrument.product.settlementAsset.symbol}
|
||||
</div>
|
||||
<div
|
||||
className="w-1/5 text-sm text-right whitespace-nowrap text-ellipsis overflow-hidden"
|
||||
className="w-1/5 text-xs lg:text-sm text-right whitespace-nowrap text-ellipsis overflow-hidden"
|
||||
title={t('24h vol')}
|
||||
data-testid="market-selector-volume"
|
||||
role="gridcell"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user