Updates to the e2e suite (#659)
* initial version of the update config script * fix duplicated content * update cypress ci settings * add workflow to run e2e tests when pushing * record cypress results to the dashboard * pull the cypress record key and project id from the secrets * add start-server-and-test to replace concurrently * replace concurrently with start-server-and-test * remove concurrently * add new cypress target to record * update cypress to 7.7.0 * add tests for signet * add tests for testnet * run tests on chrome and firefox * update test matrix: add edge and run firefox on container * fix copypasta * update docker image for firefox * fix task name again * fix edge tests task name * improve bisq tests * update workflow config * enable cypress debug logs * add a manual trigger for the e2e tests * add config:defaults target * use more of the GHA options * fix config command * add cypress-fail-on-console-error * upgrade cypress to v8.0.0 * add helper to wait for the loader skeleton to be gone * use skeleton waiter on the tests * remove manual test trigger * fix tv test when only one mempool block is available * add waiter for pagination * add extra steps to debug firefox launch issue * remove whoami step * Revert "upgrade cypress to v8.0.0" This reverts commit cb3ff7d906c2a2219d7e2b2c16a92c311e3f6817. * remove userinfo debug step * enable test retries in run mode * update proxy config to reduce ECONNRESET errors * add mock-socket dev dependency * add helpers to mock websockets and detect page idleness * stabilize mainnet tests * fix tv mode test on Liquid * add basic tests for the mainnet status page * cleanup mainnet tests * update bisq tests * update signet tests * update testnet tests * add initial support for parameterized websocket mocks * move testing dependencies to optionalDependencies * comment out mempool size check until the live updates are fixed * comment out tx regex test * update fixture for the new difficulty adjustment component * fix the assertions on the status page
This commit is contained in:
		
							parent
							
								
									a8ab0a1b4c
								
							
						
					
					
						commit
						98d5c7826c
					
				
							
								
								
									
										68
									
								
								.github/workflows/push.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								.github/workflows/push.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,68 @@
 | 
			
		||||
name: Cypress Tests
 | 
			
		||||
 | 
			
		||||
on: [push]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  cypress-chrome:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v2
 | 
			
		||||
      - name: Chrome Browser Tests
 | 
			
		||||
        uses: cypress-io/github-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
          working-directory: frontend
 | 
			
		||||
          build: npm run config:defaults
 | 
			
		||||
          start: npm run start:local-prod
 | 
			
		||||
          wait-on: 'http://localhost:4200'
 | 
			
		||||
          wait-on-timeout: 120
 | 
			
		||||
          record: true
 | 
			
		||||
          browser: chrome
 | 
			
		||||
        env:
 | 
			
		||||
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
 | 
			
		||||
          #GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
 | 
			
		||||
          DEBUG: 'cypress:*'
 | 
			
		||||
  cypress-firefox:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    container:
 | 
			
		||||
      image: cypress/browsers:node14.17.0-chrome88-ff89
 | 
			
		||||
      options: --user 1001
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v2
 | 
			
		||||
      - name: Firefox Browser Tests
 | 
			
		||||
        uses: cypress-io/github-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
          working-directory: frontend
 | 
			
		||||
          build: npm run config:defaults
 | 
			
		||||
          start: npm run start:local-prod
 | 
			
		||||
          wait-on: 'http://localhost:4200'
 | 
			
		||||
          wait-on-timeout: 120
 | 
			
		||||
          record: true
 | 
			
		||||
          browser: firefox
 | 
			
		||||
        env:
 | 
			
		||||
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
 | 
			
		||||
          #GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
 | 
			
		||||
          DEBUG: 'cypress:*'
 | 
			
		||||
  cypress-edge:
 | 
			
		||||
    runs-on: windows-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v2
 | 
			
		||||
      - name: Edge Browser Tests
 | 
			
		||||
        uses: cypress-io/github-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
          working-directory: frontend
 | 
			
		||||
          build: npm run config:defaults
 | 
			
		||||
          start: npm run start:local-prod
 | 
			
		||||
          wait-on: 'http://localhost:4200'
 | 
			
		||||
          wait-on-timeout: 120
 | 
			
		||||
          record: true
 | 
			
		||||
          browser: edge
 | 
			
		||||
        env:
 | 
			
		||||
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
 | 
			
		||||
          #GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
 | 
			
		||||
          DEBUG: 'cypress:*'
 | 
			
		||||
@ -5,5 +5,9 @@
 | 
			
		||||
  "screenshotsFolder": "cypress/screenshots",
 | 
			
		||||
  "pluginsFile": "cypress/plugins/index.js",
 | 
			
		||||
  "fixturesFolder": "cypress/fixtures",
 | 
			
		||||
  "baseUrl": "http://localhost:4200"
 | 
			
		||||
  "baseUrl": "http://localhost:4200",
 | 
			
		||||
  "retries": {
 | 
			
		||||
    "runMode": 3,
 | 
			
		||||
    "openMode": 0
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								frontend/cypress/fixtures/mainnet_live2hchart.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/cypress/fixtures/mainnet_live2hchart.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
{"live-2h-chart":{"id":1319298,"added":"2021-07-23T18:27:34.000Z","unconfirmed_transactions":546,"tx_per_second":3.93333,"vbytes_per_second":1926,"mempool_byte_weight":1106656,"total_fee":6198583,"vsizes":[255,18128,43701,58534,17144,5532,4483,1759,2394,1089,1683,7409,751,101010,1151,592,1497,703,1369,4747,800,1221,0,0,712,0,0,0,0,0,0,0,0,0,0,0,0,0]}}
 | 
			
		||||
							
								
								
									
										1
									
								
								frontend/cypress/fixtures/mainnet_mempoolInfo.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/cypress/fixtures/mainnet_mempoolInfo.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -1,6 +1,5 @@
 | 
			
		||||
describe('Bisq', () => {
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
 | 
			
		||||
        cy.intercept('/sockjs-node/info*').as('socket');
 | 
			
		||||
        cy.intercept('/bisq/api/markets/hloc?market=btc_usd&interval=day').as('hloc');
 | 
			
		||||
        cy.intercept('/bisq/api/markets/ticker').as('ticker');
 | 
			
		||||
@ -10,32 +9,42 @@ describe('Bisq', () => {
 | 
			
		||||
        cy.intercept('/bisq/api/txs/*/*').as('txs');
 | 
			
		||||
        cy.intercept('/bisq/api/blocks/*/*').as('blocks');
 | 
			
		||||
        cy.intercept('/bisq/api/stats').as('stats');
 | 
			
		||||
        
 | 
			
		||||
        Cypress.Commands.add('waitForDashboard', () => {
 | 
			
		||||
            cy.wait('@socket');
 | 
			
		||||
            cy.wait('@hloc');
 | 
			
		||||
            cy.wait('@ticker');
 | 
			
		||||
            cy.wait('@markets');
 | 
			
		||||
            cy.wait('@7d');
 | 
			
		||||
            cy.wait('@trades');
 | 
			
		||||
          });
 | 
			
		||||
    });
 | 
			
		||||
    it('loads the dashboard', () => {
 | 
			
		||||
      cy.visit('/bisq');
 | 
			
		||||
 | 
			
		||||
      cy.wait('@socket');
 | 
			
		||||
      cy.wait('@hloc');
 | 
			
		||||
      cy.wait('@ticker');
 | 
			
		||||
      cy.wait('@markets');
 | 
			
		||||
      cy.wait('@7d');
 | 
			
		||||
      cy.wait('@trades');
 | 
			
		||||
    it('loads the dashboard', () => {
 | 
			
		||||
        cy.visit('/bisq');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the transactions screen', () => {
 | 
			
		||||
        cy.visit('/bisq');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
        cy.get('li:nth-of-type(2) > a').click().then(() => {
 | 
			
		||||
            cy.wait('@txs');
 | 
			
		||||
            cy.get('.table > tr').should('have.length', 50);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the blocks screen', () => {
 | 
			
		||||
        cy.visit('/bisq');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
        cy.get('li:nth-of-type(3) > a').click().then(() => {
 | 
			
		||||
            cy.wait('@blocks');
 | 
			
		||||
            cy.get('tbody tr').should('have.length', 10);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the stats screen', () => {
 | 
			
		||||
        cy.visit('/bisq');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
        cy.get('li:nth-of-type(4) > a').click().then(() => {
 | 
			
		||||
            cy.wait('@stats');
 | 
			
		||||
        });
 | 
			
		||||
@ -43,14 +52,19 @@ describe('Bisq', () => {
 | 
			
		||||
 | 
			
		||||
    it('loads the api screen', () => {
 | 
			
		||||
        cy.visit('/bisq');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
        cy.get('li:nth-of-type(5) > a').click().then(() => {
 | 
			
		||||
 | 
			
		||||
            cy.get('.card').should('have.length.at.least', 1);
 | 
			
		||||
            cy.get('.card').first().click();
 | 
			
		||||
            cy.get('.card-body');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('shows blocks pagination with 5 pages (desktop)', () => {
 | 
			
		||||
        cy.viewport(760, 800);
 | 
			
		||||
        cy.visit('/bisq/transactions');
 | 
			
		||||
        cy.visit('/bisq/blocks');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
        cy.get('tbody tr').should('have.length', 10);
 | 
			
		||||
        // 5 pages + 4 buttons = 9 buttons
 | 
			
		||||
        cy.get('.pagination-container ul.pagination').first().children().should('have.length', 9);
 | 
			
		||||
    });
 | 
			
		||||
@ -58,6 +72,8 @@ describe('Bisq', () => {
 | 
			
		||||
    it('shows blocks pagination with 3 pages (mobile)', () => {
 | 
			
		||||
        cy.viewport(669, 800);
 | 
			
		||||
        cy.visit('/bisq/blocks');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
        cy.get('tbody tr').should('have.length', 10);
 | 
			
		||||
        // 3 pages + 4 buttons = 7 buttons
 | 
			
		||||
        cy.get('.pagination-container ul.pagination').first().children().should('have.length', 7);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -1,33 +1,41 @@
 | 
			
		||||
describe('Liquid', () => {
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
        // TODO: Fix ng serve to deliver these files
 | 
			
		||||
        cy.fixture('assets.minimal').then((json) => {
 | 
			
		||||
            cy.intercept('/resources/assets.minimal.json', json);
 | 
			
		||||
        });
 | 
			
		||||
        cy.intercept('/liquid/api/block/**').as('block');
 | 
			
		||||
        cy.intercept('/liquid/api/blocks/').as('blocks');
 | 
			
		||||
        cy.intercept('/liquid/api/tx/**/outspends').as('outspends');
 | 
			
		||||
        cy.intercept('/liquid/api/block/**/txs/**').as('block-txs');
 | 
			
		||||
        cy.intercept('/resources/pools.json').as('pools');
 | 
			
		||||
 | 
			
		||||
        cy.fixture('assets').then((json) => {
 | 
			
		||||
            cy.intercept('/resources/assets.json', json);
 | 
			
		||||
        });
 | 
			
		||||
        Cypress.Commands.add('waitForBlockData', () => {
 | 
			
		||||
            cy.wait('@socket');
 | 
			
		||||
            cy.wait('@block');
 | 
			
		||||
            cy.wait('@outspends');
 | 
			
		||||
          });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the dashboard', () => {
 | 
			
		||||
        cy.visit('/liquid');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the blocks page', () => {
 | 
			
		||||
        cy.visit('/liquid/blocks');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads a specific block page', () => {
 | 
			
		||||
        cy.visit('/liquid/blocks');
 | 
			
		||||
        cy.visit('/liquid/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the graphs page', () => {
 | 
			
		||||
        cy.visit('/liquid/graphs');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the tv page - desktop', () => {
 | 
			
		||||
        cy.visit('/liquid');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
        cy.get('li:nth-of-type(3) > a').click().then(() => {
 | 
			
		||||
            cy.wait(1000);
 | 
			
		||||
        });
 | 
			
		||||
@ -35,17 +43,18 @@ describe('Liquid', () => {
 | 
			
		||||
 | 
			
		||||
    it('loads the graphs page - mobile', () => {
 | 
			
		||||
        cy.visit('/liquid');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
        cy.get('li:nth-of-type(3) > a').click().then(() => {
 | 
			
		||||
            cy.viewport('iphone-6');
 | 
			
		||||
            cy.wait(1000);
 | 
			
		||||
            // TODO: Should we really support TV Mode in Mobile for Bisq?
 | 
			
		||||
            // cy.get('.tv-only').should('be.visible')
 | 
			
		||||
            cy.get('.tv-only').should('not.exist');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('assets', () => {
 | 
			
		||||
        it('shows the assets screen', () => {
 | 
			
		||||
            cy.visit('/liquid');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
            cy.get('li:nth-of-type(5) > a').click().then(() => {
 | 
			
		||||
                cy.get('table tr').should('have.length', 5);
 | 
			
		||||
            });
 | 
			
		||||
@ -53,6 +62,7 @@ describe('Liquid', () => {
 | 
			
		||||
 | 
			
		||||
        it('allows searching assets', () => {
 | 
			
		||||
            cy.visit('/liquid');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
            cy.get('li:nth-of-type(5) > a').click().then(() => {
 | 
			
		||||
                cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => {
 | 
			
		||||
                    cy.get('table tr').should('have.length', 1);
 | 
			
		||||
@ -62,6 +72,7 @@ describe('Liquid', () => {
 | 
			
		||||
 | 
			
		||||
        it('shows a specific asset ID', () => {
 | 
			
		||||
            cy.visit('/liquid');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
            cy.get('li:nth-of-type(5) > a').click().then(() => {
 | 
			
		||||
                cy.get('.container-xl input').click().type('Liquid CAD').then(() => {
 | 
			
		||||
                    cy.get('table tr td:nth-of-type(4) a').click();
 | 
			
		||||
@ -71,6 +82,7 @@ describe('Liquid', () => {
 | 
			
		||||
 | 
			
		||||
        it('shows a specific asset issuance TX', () => {
 | 
			
		||||
            cy.visit('/liquid');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
            cy.get('li:nth-of-type(5) > a').click().then(() => {
 | 
			
		||||
                cy.get('.container-xl input').click().type('Liquid CAD').then(() => {
 | 
			
		||||
                    cy.get('table tr td:nth-of-type(5) a').click();
 | 
			
		||||
@ -83,18 +95,21 @@ describe('Liquid', () => {
 | 
			
		||||
    describe('unblinded TX', () => {
 | 
			
		||||
        it('show unblinded TX', () => {
 | 
			
		||||
            cy.visit('/liquid/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc,2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3a');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
            cy.get('#table-tx-vin tr').should('have.class', 'assetBox');
 | 
			
		||||
            cy.get('#table-tx-vout tr').should('have.class', 'assetBox');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('show empty unblinded TX', () => {
 | 
			
		||||
            cy.visit('/liquid/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
            cy.get('#table-tx-vin tr').should('have.class', '');
 | 
			
		||||
            cy.get('#table-tx-vout tr').should('have.class', '');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('show invalid unblinded TX hex', () => {
 | 
			
		||||
            cy.visit('/liquid/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=123');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
            cy.get('#table-tx-vin tr').should('have.class', '');
 | 
			
		||||
            cy.get('#table-tx-vout tr').should('have.class', '');
 | 
			
		||||
            cy.get('.error-unblinded' ).contains('Error: Invalid blinding data (invalid hex)');
 | 
			
		||||
@ -102,6 +117,7 @@ describe('Liquid', () => {
 | 
			
		||||
 | 
			
		||||
        it('show first unblinded vout', () => {
 | 
			
		||||
            cy.visit('/liquid/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
            cy.get('#table-tx-vout tr:first-child()').should('have.class', 'assetBox');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -112,9 +128,15 @@ describe('Liquid', () => {
 | 
			
		||||
 | 
			
		||||
        it('show invalid error unblinded TX', () => {
 | 
			
		||||
            cy.visit('/liquid/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc,2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3c');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
            cy.get('#table-tx-vout tr').should('have.class', 'assetBox');
 | 
			
		||||
            cy.get('.error-unblinded' ).contains('Error: Invalid blinding data.');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('prevents regressing issue #644', () => {
 | 
			
		||||
            cy.visit('/liquid/tx/393b890966f305e7c440fcfb12a13f51a7a9011cc59ff5f14f6f93214261bd82');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
        })
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -1,41 +1,72 @@
 | 
			
		||||
import { emitMempoolInfo } from "../../support/websocket";
 | 
			
		||||
 | 
			
		||||
describe('Mainnet', () => {
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
        //cy.intercept('/sockjs-node/info*').as('socket');
 | 
			
		||||
        cy.intercept('/api/block-height/*').as('block-height');
 | 
			
		||||
        cy.intercept('/api/block/*').as('block');
 | 
			
		||||
        cy.intercept('/api/block/*/txs/0').as('block-txs');
 | 
			
		||||
        cy.intercept('/api/tx/*/outspends').as('tx-outspends');
 | 
			
		||||
        cy.intercept('/resources/pools.json').as('pools');
 | 
			
		||||
 | 
			
		||||
        // TODO: Fix ng serve to deliver this file
 | 
			
		||||
        cy.fixture('pools').then((json) => {
 | 
			
		||||
            cy.intercept('/resources/pools.json', json);
 | 
			
		||||
        });
 | 
			
		||||
        Cypress.Commands.add('waitForBlockData', () => {
 | 
			
		||||
            cy.wait('@tx-outspends');
 | 
			
		||||
            cy.wait('@pools');
 | 
			
		||||
          });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the status screen', () => {
 | 
			
		||||
      cy.visit('/status');
 | 
			
		||||
      cy.get('#mempool-block-0').should('be.visible');
 | 
			
		||||
      cy.get('[id^="bitcoin-block-"]').should('have.length', 8);
 | 
			
		||||
      cy.get('.footer').should('be.visible');
 | 
			
		||||
      cy.get('.row > :nth-child(1)').invoke('text').then((text) => {
 | 
			
		||||
        expect(text).to.match(/Tx vBytes per second:.* vB\/s/);
 | 
			
		||||
      });
 | 
			
		||||
      cy.get('.row > :nth-child(2)').invoke('text').then((text) => {
 | 
			
		||||
          expect(text).to.match(/Unconfirmed:(.*)/);
 | 
			
		||||
      });
 | 
			
		||||
      cy.get('.row > :nth-child(3)').invoke('text').then((text) => {
 | 
			
		||||
        expect(text).to.match(/Mempool size:(.*) (kB|MB) \((\d+) (block|blocks)\)/);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the dashboard', () => {
 | 
			
		||||
      cy.visit('/');
 | 
			
		||||
      cy.wait(1000);
 | 
			
		||||
      cy.waitForSkeletonGone();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the dashboard with the skeleton blocks', () => {
 | 
			
		||||
      cy.visit('/');
 | 
			
		||||
      cy.get('#mempool-block-0').should('be.visible');
 | 
			
		||||
      cy.get('#mempool-block-1').should('be.visible');
 | 
			
		||||
      cy.get('#mempool-block-2').should('be.visible');
 | 
			
		||||
      cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
 | 
			
		||||
      cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
 | 
			
		||||
      cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
 | 
			
		||||
      cy.wait(1000);
 | 
			
		||||
        cy.mockMempoolSocket();
 | 
			
		||||
        cy.visit("/");
 | 
			
		||||
        cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
 | 
			
		||||
        cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
 | 
			
		||||
        cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
 | 
			
		||||
        cy.get('#mempool-block-0').should('be.visible');
 | 
			
		||||
        cy.get('#mempool-block-1').should('be.visible');
 | 
			
		||||
        cy.get('#mempool-block-2').should('be.visible');
 | 
			
		||||
 | 
			
		||||
        emitMempoolInfo({
 | 
			
		||||
            'params': {
 | 
			
		||||
              loaded: true
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
 | 
			
		||||
        cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
 | 
			
		||||
        cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the blocks screen', () => {
 | 
			
		||||
        cy.visit('/');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
        cy.get('li:nth-of-type(2) > a').click().then(() => {
 | 
			
		||||
           cy.wait(1000);
 | 
			
		||||
            cy.waitForPageIdle();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('loads the graphs screen', () => {
 | 
			
		||||
        cy.visit('/');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
        cy.get('li:nth-of-type(3) > a').click().then(() => {
 | 
			
		||||
            cy.wait(1000);
 | 
			
		||||
        });
 | 
			
		||||
@ -45,29 +76,29 @@ describe('Mainnet', () => {
 | 
			
		||||
        it('loads the tv screen - desktop', () => {
 | 
			
		||||
            cy.viewport('macbook-16');
 | 
			
		||||
            cy.visit('/');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
            cy.get('li:nth-of-type(4) > a').click().then(() => {
 | 
			
		||||
                cy.viewport('macbook-16');
 | 
			
		||||
                cy.wait(1000);
 | 
			
		||||
                cy.get('.chart-holder');
 | 
			
		||||
                cy.get('.blockchain-wrapper').should('be.visible');
 | 
			
		||||
                cy.get('#mempool-block-0').should('be.visible');
 | 
			
		||||
                cy.get('#mempool-block-1').should('be.visible');
 | 
			
		||||
                cy.get('#mempool-block-2').should('be.visible');
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('loads the tv screen - mobile', () => {
 | 
			
		||||
            cy.visit('/');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
            cy.get('li:nth-of-type(4) > a').click().then(() => {
 | 
			
		||||
                cy.viewport('iphone-6');
 | 
			
		||||
                cy.wait(1000);
 | 
			
		||||
                cy.get('.chart-holder');
 | 
			
		||||
                cy.get('.blockchain-wrapper').should('not.be.visible');
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    it('loads the api screen', () => {
 | 
			
		||||
        cy.visit('/');
 | 
			
		||||
        cy.waitForSkeletonGone();
 | 
			
		||||
        cy.get('li:nth-of-type(5) > a').click().then(() => {
 | 
			
		||||
            cy.wait(1000);
 | 
			
		||||
        });
 | 
			
		||||
@ -76,12 +107,15 @@ describe('Mainnet', () => {
 | 
			
		||||
    describe('blocks', () => {
 | 
			
		||||
        it('shows empty blocks properly', () => {
 | 
			
		||||
            cy.visit('/block/0000000000000000000bd14f744ef2e006e61c32214670de7eb891a5732ee775');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
            cy.waitForPageIdle();
 | 
			
		||||
            cy.get('h2').invoke('text').should('equal', '1 transaction');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('expands and collapses the block details', () => {
 | 
			
		||||
            cy.visit('/block/0');
 | 
			
		||||
            cy.wait('@tx-outspends');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
            cy.waitForPageIdle();
 | 
			
		||||
            cy.get('.btn.btn-outline-info').click().then(() => {
 | 
			
		||||
                cy.get('#details').should('be.visible');
 | 
			
		||||
            });
 | 
			
		||||
@ -91,17 +125,21 @@ describe('Mainnet', () => {
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        it('shows blocks with no pagination', () => {
 | 
			
		||||
            cy.viewport('iphone-6');
 | 
			
		||||
            cy.visit('/block/00000000000000000001ba40caf1ad4cec0ceb77692662315c151953bfd7c4c4');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
            cy.waitForPageIdle();
 | 
			
		||||
            cy.get('.block-tx-title h2').invoke('text').should('equal', '19 transactions');
 | 
			
		||||
            cy.get('.pagination-container ul.pagination').first().children().should('have.length', 6);
 | 
			
		||||
            cy.get('.pagination-container ul.pagination').first().children().should('have.length', 5);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('supports pagination on the block screen', () => {
 | 
			
		||||
            // 41 txs
 | 
			
		||||
            cy.visit('/block/00000000000000000009f9b7b0f63ad50053ad12ec3b7f5ca951332f134f83d8');
 | 
			
		||||
            cy.waitForSkeletonGone();
 | 
			
		||||
            cy.get('.pagination-container a').invoke('text').then((text1) => {
 | 
			
		||||
                cy.get('.active + li').first().click().then(() => {
 | 
			
		||||
                    cy.waitForSkeletonGone();
 | 
			
		||||
                    cy.waitForPageIdle();
 | 
			
		||||
                    cy.get('.header-bg.box > a').invoke('text').then((text2) => {
 | 
			
		||||
                        expect(text1).not.to.eq(text2);
 | 
			
		||||
                    });
 | 
			
		||||
@ -111,14 +149,22 @@ describe('Mainnet', () => {
 | 
			
		||||
 | 
			
		||||
        it('shows blocks pagination with 5 pages (desktop)', () => {
 | 
			
		||||
            cy.viewport(760, 800);
 | 
			
		||||
            cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3');
 | 
			
		||||
            cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
 | 
			
		||||
                cy.waitForSkeletonGone();
 | 
			
		||||
                cy.waitForPageIdle();
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
            // 5 pages + 4 buttons = 9 buttons
 | 
			
		||||
            cy.get('.pagination-container ul.pagination').first().children().should('have.length', 9);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('shows blocks pagination with 3 pages (mobile)', () => {
 | 
			
		||||
            cy.viewport(669, 800);
 | 
			
		||||
            cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3');
 | 
			
		||||
            cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
 | 
			
		||||
                cy.waitForSkeletonGone();
 | 
			
		||||
                cy.waitForPageIdle();
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
            // 3 pages + 4 buttons = 7 buttons
 | 
			
		||||
            cy.get('.pagination-container ul.pagination').first().children().should('have.length', 7);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,126 @@
 | 
			
		||||
import { emitMempoolInfo } from "../../support/websocket";
 | 
			
		||||
 | 
			
		||||
describe('Signet', () => {
 | 
			
		||||
    it('loads the dashboard', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    cy.intercept('/api/block-height/*').as('block-height');
 | 
			
		||||
    cy.intercept('/api/block/*').as('block');
 | 
			
		||||
    cy.intercept('/api/block/*/txs/0').as('block-txs');
 | 
			
		||||
    cy.intercept('/api/tx/*/outspends').as('tx-outspends');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('loads the dashboard', () => {
 | 
			
		||||
    cy.visit('/signet');
 | 
			
		||||
    cy.waitForSkeletonGone();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('loads the dashboard with the skeleton blocks', () => {
 | 
			
		||||
    cy.mockMempoolSocket();
 | 
			
		||||
    cy.visit("/signet");
 | 
			
		||||
    cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
 | 
			
		||||
    cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
 | 
			
		||||
    cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
 | 
			
		||||
    cy.get('#mempool-block-0').should('be.visible');
 | 
			
		||||
    cy.get('#mempool-block-1').should('be.visible');
 | 
			
		||||
    cy.get('#mempool-block-2').should('be.visible');
 | 
			
		||||
 | 
			
		||||
    emitMempoolInfo({
 | 
			
		||||
        'params': {
 | 
			
		||||
          "network": "signet"
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
 | 
			
		||||
    cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
 | 
			
		||||
    cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
  it('loads the blocks screen', () => {
 | 
			
		||||
      cy.visit('/signet');
 | 
			
		||||
    });
 | 
			
		||||
      cy.waitForSkeletonGone();
 | 
			
		||||
      cy.get('li:nth-of-type(2) > a').click().then(() => {
 | 
			
		||||
         cy.wait(1000);
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
    it.skip('loads all the pages properly', () => {
 | 
			
		||||
  it('loads the graphs screen', () => {
 | 
			
		||||
      cy.visit('/signet');
 | 
			
		||||
      cy.waitForSkeletonGone();
 | 
			
		||||
      cy.get('li:nth-of-type(3) > a').click().then(() => {
 | 
			
		||||
          cy.wait(1000);
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
  describe('tv mode', () => {
 | 
			
		||||
      it('loads the tv screen - desktop', () => {
 | 
			
		||||
          cy.viewport('macbook-16');
 | 
			
		||||
          cy.visit('/signet');
 | 
			
		||||
          cy.waitForSkeletonGone();
 | 
			
		||||
          cy.get('li:nth-of-type(4) > a').click().then(() => {
 | 
			
		||||
            cy.get('.chart-holder').should('be.visible');
 | 
			
		||||
            cy.get('#mempool-block-0').should('be.visible');
 | 
			
		||||
            cy.get('.tv-only').should('not.exist');
 | 
			
		||||
          });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('loads the tv screen - mobile', () => {
 | 
			
		||||
          cy.visit('/signet');
 | 
			
		||||
          cy.waitForSkeletonGone();
 | 
			
		||||
          cy.get('li:nth-of-type(4) > a').click().then(() => {
 | 
			
		||||
              cy.viewport('iphone-8');
 | 
			
		||||
              cy.get('.chart-holder').should('be.visible');
 | 
			
		||||
              //TODO: Remove comment when the bug is fixed
 | 
			
		||||
              //cy.get('#mempool-block-0').should('be.visible');
 | 
			
		||||
              cy.get('.tv-only').should('not.exist');
 | 
			
		||||
          });
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  it('loads the api screen', () => {
 | 
			
		||||
      cy.visit('/signet');
 | 
			
		||||
      cy.waitForSkeletonGone();
 | 
			
		||||
      cy.get('li:nth-of-type(5) > a').click().then(() => {
 | 
			
		||||
          cy.wait(1000);
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('blocks', () => {
 | 
			
		||||
      it('shows empty blocks properly', () => {
 | 
			
		||||
          cy.visit('/signet/block/00000133d54e4589f6436703b067ec23209e0a21b8a9b12f57d0592fd85f7a42');
 | 
			
		||||
          cy.waitForSkeletonGone();
 | 
			
		||||
          cy.get('h2').invoke('text').should('equal', '1 transaction');
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('expands and collapses the block details', () => {
 | 
			
		||||
          cy.visit('/signet/block/0');
 | 
			
		||||
          cy.waitForSkeletonGone();
 | 
			
		||||
          cy.get('.btn.btn-outline-info').click().then(() => {
 | 
			
		||||
              cy.get('#details').should('be.visible');
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          cy.get('.btn.btn-outline-info').click().then(() => {
 | 
			
		||||
              cy.get('#details').should('not.be.visible');
 | 
			
		||||
          });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('shows blocks with no pagination', () => {
 | 
			
		||||
          cy.visit('/signet/block/00000078f920a96a69089877b934ce7fd009ab55e3170920a021262cb258e7cc');
 | 
			
		||||
          cy.waitForSkeletonGone();
 | 
			
		||||
          cy.get('h2').invoke('text').should('equal', '13 transactions');
 | 
			
		||||
          cy.get('ul.pagination').first().children().should('have.length', 5);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('supports pagination on the block screen', () => {
 | 
			
		||||
          // 43 txs
 | 
			
		||||
          cy.visit('/signet/block/00000094bd52f73bdbfc4bece3a94c21fec2dc968cd54210496e69e4059d66a6');
 | 
			
		||||
          cy.waitForSkeletonGone();
 | 
			
		||||
          cy.get('.header-bg.box > a').invoke('text').then((text1) => {
 | 
			
		||||
              cy.get('.active + li').first().click().then(() => {
 | 
			
		||||
                  cy.get('.header-bg.box > a').invoke('text').then((text2) => {
 | 
			
		||||
                      expect(text1).not.to.eq(text2);
 | 
			
		||||
                  });
 | 
			
		||||
              });
 | 
			
		||||
          });
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,123 @@
 | 
			
		||||
import { emitMempoolInfo } from "../../support/websocket";
 | 
			
		||||
 | 
			
		||||
describe('Testnet', () => {
 | 
			
		||||
    it('loads the dashboard', () => {
 | 
			
		||||
      cy.visit('/testnet');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it.skip('loads all the pages properly', () => {
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    cy.intercept('/api/block-height/*').as('block-height');
 | 
			
		||||
    cy.intercept('/api/block/*').as('block');
 | 
			
		||||
    cy.intercept('/api/block/*/txs/0').as('block-txs');
 | 
			
		||||
    cy.intercept('/api/tx/*/outspends').as('tx-outspends');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('loads the dashboard', () => {
 | 
			
		||||
    cy.visit('/testnet');
 | 
			
		||||
    cy.waitForSkeletonGone();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('loads the dashboard with the skeleton blocks', () => {
 | 
			
		||||
    cy.mockMempoolSocket();
 | 
			
		||||
    cy.visit("/signet");
 | 
			
		||||
    cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
 | 
			
		||||
    cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
 | 
			
		||||
    cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
 | 
			
		||||
    cy.get('#mempool-block-0').should('be.visible');
 | 
			
		||||
    cy.get('#mempool-block-1').should('be.visible');
 | 
			
		||||
    cy.get('#mempool-block-2').should('be.visible');
 | 
			
		||||
 | 
			
		||||
    emitMempoolInfo({
 | 
			
		||||
        'params': {
 | 
			
		||||
          loaded: true
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
 | 
			
		||||
    cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
 | 
			
		||||
    cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
  it('loads the blocks screen', () => {
 | 
			
		||||
      cy.visit('/testnet');
 | 
			
		||||
      cy.waitForSkeletonGone();
 | 
			
		||||
      cy.get('li:nth-of-type(2) > a').click().then(() => {
 | 
			
		||||
         cy.wait(1000);
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('loads the graphs screen', () => {
 | 
			
		||||
      cy.visit('/testnet');
 | 
			
		||||
      cy.waitForSkeletonGone();
 | 
			
		||||
      cy.get('li:nth-of-type(3) > a').click().then(() => {
 | 
			
		||||
          cy.wait(1000);
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('tv mode', () => {
 | 
			
		||||
      it('loads the tv screen - desktop', () => {
 | 
			
		||||
          cy.viewport('macbook-16');
 | 
			
		||||
          cy.visit('/testnet');
 | 
			
		||||
          cy.waitForSkeletonGone();
 | 
			
		||||
          cy.get('li:nth-of-type(4) > a').click().then(() => {
 | 
			
		||||
              cy.wait(1000);
 | 
			
		||||
              cy.get('.tv-only').should('not.exist');
 | 
			
		||||
          });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('loads the tv screen - mobile', () => {
 | 
			
		||||
          cy.visit('/testnet');
 | 
			
		||||
          cy.waitForSkeletonGone();
 | 
			
		||||
          cy.get('li:nth-of-type(4) > a').click().then(() => {
 | 
			
		||||
              cy.viewport('iphone-6');
 | 
			
		||||
              cy.wait(1000);
 | 
			
		||||
              cy.get('.tv-only').should('not.exist');
 | 
			
		||||
          });
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  it('loads the api screen', () => {
 | 
			
		||||
      cy.visit('/testnet');
 | 
			
		||||
      cy.waitForSkeletonGone();
 | 
			
		||||
      cy.get('li:nth-of-type(5) > a').click().then(() => {
 | 
			
		||||
          cy.wait(1000);
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('blocks', () => {
 | 
			
		||||
      it('shows empty blocks properly', () => {
 | 
			
		||||
          cy.visit('/testnet/block/0');
 | 
			
		||||
          cy.waitForSkeletonGone();
 | 
			
		||||
          cy.get('h2').invoke('text').should('equal', '1 transaction');
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('expands and collapses the block details', () => {
 | 
			
		||||
          cy.visit('/testnet/block/0');
 | 
			
		||||
          cy.waitForSkeletonGone();
 | 
			
		||||
          cy.get('.btn.btn-outline-info').click().then(() => {
 | 
			
		||||
              cy.get('#details').should('be.visible');
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          cy.get('.btn.btn-outline-info').click().then(() => {
 | 
			
		||||
              cy.get('#details').should('not.be.visible');
 | 
			
		||||
          });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('shows blocks with no pagination', () => {
 | 
			
		||||
          cy.visit('/testnet/block/000000000000002f8ce27716e74ecc7ad9f7b5101fed12d09e28bb721b9460ea');
 | 
			
		||||
          cy.waitForSkeletonGone();
 | 
			
		||||
          cy.get('h2').invoke('text').should('equal', '11 transactions');
 | 
			
		||||
          cy.get('ul.pagination').first().children().should('have.length', 5);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('supports pagination on the block screen', () => {
 | 
			
		||||
          // 48 txs
 | 
			
		||||
          cy.visit('/testnet/block/000000000000002ca3878ebd98b313a1c2d531f2e70a6575d232ca7564dea7a9');
 | 
			
		||||
          cy.waitForSkeletonGone();
 | 
			
		||||
          cy.get('.header-bg.box > a').invoke('text').then((text1) => {
 | 
			
		||||
              cy.get('.active + li').first().click().then(() => {
 | 
			
		||||
                  cy.get('.header-bg.box > a').invoke('text').then((text2) => {
 | 
			
		||||
                      expect(text1).not.to.eq(text2);
 | 
			
		||||
                  });
 | 
			
		||||
              });
 | 
			
		||||
          });
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										63
									
								
								frontend/cypress/support/PageIdleDetector.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								frontend/cypress/support/PageIdleDetector.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,63 @@
 | 
			
		||||
// source: chrisp_68 @ https://stackoverflow.com/questions/50525143/how-do-you-reliably-wait-for-page-idle-in-cypress-io-test
 | 
			
		||||
export class PageIdleDetector
 | 
			
		||||
{   
 | 
			
		||||
    defaultOptions: Object = { timeout: 60000 };
 | 
			
		||||
 | 
			
		||||
    public WaitForPageToBeIdle(): void
 | 
			
		||||
    {
 | 
			
		||||
        this.WaitForPageToLoad();
 | 
			
		||||
        this.WaitForAngularRequestsToComplete();
 | 
			
		||||
        this.WaitForAngularDigestCycleToComplete();
 | 
			
		||||
        this.WaitForAnimationsToStop();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public WaitForPageToLoad(options: Object = this.defaultOptions): void
 | 
			
		||||
    {
 | 
			
		||||
        cy.document(options).should((myDocument: any) =>
 | 
			
		||||
        {
 | 
			
		||||
            expect(myDocument.readyState, "WaitForPageToLoad").to.be.oneOf(["interactive", "complete"]);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public WaitForAngularRequestsToComplete(options: Object = this.defaultOptions): void
 | 
			
		||||
    {
 | 
			
		||||
        cy.window(options).should((myWindow: any) =>
 | 
			
		||||
        {
 | 
			
		||||
            if (!!myWindow.angular)
 | 
			
		||||
            {
 | 
			
		||||
                expect(this.NumberOfPendingAngularRequests(myWindow), "WaitForAngularRequestsToComplete").to.have.length(0);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public WaitForAngularDigestCycleToComplete(options: Object = this.defaultOptions): void
 | 
			
		||||
    {
 | 
			
		||||
        cy.window(options).should((myWindow: any) =>
 | 
			
		||||
        {
 | 
			
		||||
            if (!!myWindow.angular)
 | 
			
		||||
            {
 | 
			
		||||
                expect(this.AngularRootScopePhase(myWindow), "WaitForAngularDigestCycleToComplete").to.be.null;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public WaitForAnimationsToStop(options: Object = this.defaultOptions): void
 | 
			
		||||
    {
 | 
			
		||||
        cy.get(":animated", options).should("not.exist");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private getInjector(myWindow: any)
 | 
			
		||||
    {
 | 
			
		||||
        return myWindow.angular.element(myWindow.document.body).injector();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private NumberOfPendingAngularRequests(myWindow: any)
 | 
			
		||||
    {
 | 
			
		||||
        return this.getInjector(myWindow).get('$http').pendingRequests;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private AngularRootScopePhase(myWindow: any)
 | 
			
		||||
    {
 | 
			
		||||
        return this.getInjector(myWindow).get("$rootScope").$$phase;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -43,3 +43,24 @@
 | 
			
		||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
 | 
			
		||||
 | 
			
		||||
import 'cypress-wait-until';
 | 
			
		||||
import { PageIdleDetector } from './PageIdleDetector';
 | 
			
		||||
import { mockWebSocket } from './websocket';
 | 
			
		||||
 | 
			
		||||
Cypress.Commands.add('waitForSkeletonGone', () => {
 | 
			
		||||
    cy.waitUntil(() => {
 | 
			
		||||
        return Cypress.$('.skeleton-loader').length === 0;
 | 
			
		||||
    }, { verbose: true, description: "waitForSkeletonGone", errorMsg: "skeleton loaders never went away", timeout: 7000, interval: 50});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
Cypress.Commands.add(
 | 
			
		||||
    "waitForPageIdle",
 | 
			
		||||
    () => {
 | 
			
		||||
        console.warn("Waiting for page idle state");
 | 
			
		||||
        const pageIdleDetector = new PageIdleDetector();
 | 
			
		||||
        pageIdleDetector.WaitForPageToBeIdle();
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
Cypress.Commands.add('mockMempoolSocket', () => {
 | 
			
		||||
  mockWebSocket();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -15,3 +15,6 @@
 | 
			
		||||
 | 
			
		||||
// When a command from ./commands is ready to use, import with `import './commands'` syntax
 | 
			
		||||
import './commands';
 | 
			
		||||
import failOnConsoleError from 'cypress-fail-on-console-error';
 | 
			
		||||
 | 
			
		||||
failOnConsoleError();
 | 
			
		||||
							
								
								
									
										84
									
								
								frontend/cypress/support/websocket.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								frontend/cypress/support/websocket.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import { WebSocket, Server } from 'mock-socket';
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
	interface Window {
 | 
			
		||||
		mockServer: Server;
 | 
			
		||||
		mockSocket: WebSocket;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const mocks: { [key: string]: { server: Server; websocket: WebSocket } } = {};
 | 
			
		||||
 | 
			
		||||
const cleanupMock = (url: string) => {
 | 
			
		||||
	if (mocks[url]) {
 | 
			
		||||
		mocks[url].websocket.close();
 | 
			
		||||
		mocks[url].server.stop();
 | 
			
		||||
		delete mocks[url];
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createMock = (url: string) => {
 | 
			
		||||
	cleanupMock(url);
 | 
			
		||||
	const server = new Server(url);
 | 
			
		||||
	const websocket = new WebSocket(url);
 | 
			
		||||
	mocks[url] = { server, websocket };
 | 
			
		||||
 | 
			
		||||
	return mocks[url];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const mockWebSocket = () => {
 | 
			
		||||
	cy.on('window:before:load', (win) => {
 | 
			
		||||
		const winWebSocket = win.WebSocket;
 | 
			
		||||
		cy.stub(win, 'WebSocket').callsFake((url) => {
 | 
			
		||||
            console.log(url);
 | 
			
		||||
			if ((new URL(url).pathname.indexOf('/sockjs-node/') !== 0)) {
 | 
			
		||||
				const { server, websocket } = createMock(url);
 | 
			
		||||
 | 
			
		||||
				win.mockServer = server;
 | 
			
		||||
				win.mockServer.on('connection', (socket) => {
 | 
			
		||||
					win.mockSocket = socket;
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
                win.mockServer.on('message', (message) => {
 | 
			
		||||
                    console.log(message);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
				return websocket;
 | 
			
		||||
			} else {
 | 
			
		||||
				return new winWebSocket(url);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	cy.on('window:before:unload', () => {
 | 
			
		||||
		for (const url in mocks) {
 | 
			
		||||
			cleanupMock(url);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const emitMempoolInfo = ({
 | 
			
		||||
	params
 | 
			
		||||
}: { params?: any } = {}) => {
 | 
			
		||||
	cy.window().then((win) => {
 | 
			
		||||
		//TODO: Refactor to take into account different parameterized mocking scenarios
 | 
			
		||||
		switch (params.network) {
 | 
			
		||||
			//TODO: Use network specific mocks
 | 
			
		||||
			case "signet":
 | 
			
		||||
			case "testnet":
 | 
			
		||||
			default:
 | 
			
		||||
				win.mockSocket.send('{"action":"init"}');
 | 
			
		||||
				win.mockSocket.send('{"action":"want","data":["blocks","stats","mempool-blocks","live-2h-chart"]}');
 | 
			
		||||
				cy.readFile('cypress/fixtures/mainnet_mempoolInfo.json', 'ascii').then((fixture) => {
 | 
			
		||||
					win.mockSocket.send(JSON.stringify(fixture));
 | 
			
		||||
				});
 | 
			
		||||
				win.mockSocket.send('{"conversions":{"USD":32365.338815782445}}');
 | 
			
		||||
				cy.readFile('cypress/fixtures/mainnet_live2hchart.json', 'ascii').then((fixture) => {
 | 
			
		||||
					win.mockSocket.send(JSON.stringify(fixture));
 | 
			
		||||
				});
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
    cy.waitForSkeletonGone();
 | 
			
		||||
    return cy.get('#mempool-block-0');
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										2859
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2859
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -38,12 +38,17 @@
 | 
			
		||||
    "test": "ng test",
 | 
			
		||||
    "lint": "ng lint",
 | 
			
		||||
    "e2e": "ng e2e",
 | 
			
		||||
    "e2e:ci": "npm run cypress:run:ci",
 | 
			
		||||
    "config:defaults": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config",
 | 
			
		||||
    "dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
 | 
			
		||||
    "serve:ssr": "node server.run.js",
 | 
			
		||||
    "build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
 | 
			
		||||
    "prerender": "ng run mempool:prerender",
 | 
			
		||||
    "cypress:open": "concurrently \"ng serve -c local-prod\" \"cypress open\" --kill-others",
 | 
			
		||||
    "cypress:run": "concurrently \"ng serve -c local-prod\" \"cypress run\" --kill-others"
 | 
			
		||||
    "cypress:open": "cypress open",
 | 
			
		||||
    "cypress:run": "cypress run",
 | 
			
		||||
    "cypress:run:record": "cypress run --record",
 | 
			
		||||
    "cypress:open:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open",
 | 
			
		||||
    "cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@angular/animations": "~11.2.8",
 | 
			
		||||
@ -85,15 +90,12 @@
 | 
			
		||||
    "@angular/cli": "~11.2.7",
 | 
			
		||||
    "@angular/compiler-cli": "~11.2.8",
 | 
			
		||||
    "@angular/language-service": "~11.2.8",
 | 
			
		||||
    "@cypress/schematic": "^1.3.0",
 | 
			
		||||
    "@nguniversal/builders": "^11.2.1",
 | 
			
		||||
    "@types/express": "^4.17.0",
 | 
			
		||||
    "@types/jasmine": "~3.6.0",
 | 
			
		||||
    "@types/jasminewd2": "~2.0.3",
 | 
			
		||||
    "@types/node": "^12.11.1",
 | 
			
		||||
    "codelyzer": "^6.0.1",
 | 
			
		||||
    "concurrently": "^6.2.0",
 | 
			
		||||
    "cypress-wait-until": "^1.7.1",
 | 
			
		||||
    "http-proxy-middleware": "^1.0.5",
 | 
			
		||||
    "jasmine-core": "~3.6.0",
 | 
			
		||||
    "jasmine-spec-reporter": "~5.0.0",
 | 
			
		||||
@ -107,6 +109,11 @@
 | 
			
		||||
    "typescript": "~4.1.5"
 | 
			
		||||
  },
 | 
			
		||||
  "optionalDependencies": {
 | 
			
		||||
    "cypress": "^7.4.0"
 | 
			
		||||
    "cypress": "^7.7.0",
 | 
			
		||||
    "@cypress/schematic": "^1.3.0",
 | 
			
		||||
    "cypress-fail-on-console-error": "^2.1.0",
 | 
			
		||||
    "cypress-wait-until": "^1.7.1",
 | 
			
		||||
    "start-server-and-test": "^1.12.6",
 | 
			
		||||
    "mock-socket": "^9.0.3"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,14 +4,11 @@
 | 
			
		||||
    "secure": false,
 | 
			
		||||
    "ws": true
 | 
			
		||||
  },
 | 
			
		||||
  "/api/*": {
 | 
			
		||||
  "/api": {
 | 
			
		||||
    "target": "https://mempool.space",
 | 
			
		||||
    "secure": false,
 | 
			
		||||
    "changeOrigin": true,
 | 
			
		||||
    "logLevel": "debug",
 | 
			
		||||
    "pathRewrite": {
 | 
			
		||||
      "^/api": "https://mempool.space/api"
 | 
			
		||||
    },
 | 
			
		||||
    "timeout": 3600000
 | 
			
		||||
  },
 | 
			
		||||
  "/testnet/api/v1/ws": {
 | 
			
		||||
@ -23,7 +20,7 @@
 | 
			
		||||
      "^/testnet/api": "/api/v1/ws"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "/testnet/api/*": {
 | 
			
		||||
  "/testnet/api": {
 | 
			
		||||
    "target": "https://mempool.space",
 | 
			
		||||
    "secure": true,
 | 
			
		||||
    "changeOrigin": true,
 | 
			
		||||
@ -42,7 +39,7 @@
 | 
			
		||||
      "^/signet/api": "/api/v1/ws"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "/signet/api/*": {
 | 
			
		||||
  "/signet/api": {
 | 
			
		||||
    "target": "https://mempool.space",
 | 
			
		||||
    "secure": true,
 | 
			
		||||
    "changeOrigin": true,
 | 
			
		||||
@ -61,7 +58,7 @@
 | 
			
		||||
      "^/bisq/api": "/api/v1/ws"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "/bisq/api/*": {
 | 
			
		||||
  "/bisq/api": {
 | 
			
		||||
    "target": "https://mempool.space/bisq",
 | 
			
		||||
    "secure": false,
 | 
			
		||||
    "changeOrigin": true,
 | 
			
		||||
@ -75,7 +72,7 @@
 | 
			
		||||
    "secure": false,
 | 
			
		||||
    "ws": true
 | 
			
		||||
  },
 | 
			
		||||
  "/liquid/api/*": {
 | 
			
		||||
  "/liquid/api": {
 | 
			
		||||
    "target": "https://mempool.space",
 | 
			
		||||
    "secure": false,
 | 
			
		||||
    "changeOrigin": true,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										95
									
								
								frontend/update-config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								frontend/update-config.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,95 @@
 | 
			
		||||
const fs = require('fs');
 | 
			
		||||
 | 
			
		||||
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
 | 
			
		||||
const GENERATED_CONFIG_FILE_NAME = 'generated-config.js';
 | 
			
		||||
 | 
			
		||||
let settings = [];
 | 
			
		||||
let configContent = {};
 | 
			
		||||
const packageSettings = ['GIT_COMMIT_HASH', 'PACKAGE_JSON_VERSION']; //These will be handled  by generate-config
 | 
			
		||||
 | 
			
		||||
var args = process.argv.slice(2);
 | 
			
		||||
 | 
			
		||||
function addSetting(key, value) {
 | 
			
		||||
    settings.push({
 | 
			
		||||
        key: key,
 | 
			
		||||
        value: value
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function normalizedValue(value) {
 | 
			
		||||
    if (Number(value)) {
 | 
			
		||||
        value = Number(value);
 | 
			
		||||
    } else if ((value === 'true') || (value !== 'true')) {
 | 
			
		||||
        value = !!JSON.parse(String(value).toLowerCase());
 | 
			
		||||
    }
 | 
			
		||||
    return value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function parseGeneratedFile() {
 | 
			
		||||
    const generatedConfig = fs.readFileSync(GENERATED_CONFIG_FILE_NAME);
 | 
			
		||||
    if (generatedConfig) {
 | 
			
		||||
      const configContents = generatedConfig.toString();
 | 
			
		||||
      const regexp = new RegExp(/window.__env.(\w+) = '(.*)'/,'g');
 | 
			
		||||
      while ((match = regexp.exec(configContents)) !== null) {
 | 
			
		||||
          // Do not add setting if it's the git hash or package json version
 | 
			
		||||
          if (!packageSettings.includes(match[1])) {
 | 
			
		||||
              const key = match[1];
 | 
			
		||||
              const value = match[2];
 | 
			
		||||
              console.log(typeof(value));
 | 
			
		||||
              addSetting(key, value);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function saveSettingsJson() {
 | 
			
		||||
    settings.forEach(setting => {
 | 
			
		||||
        if (configContent.hasOwnProperty(setting['key']) && normalizedValue(configContent[setting['key']]) !== normalizedValue(setting['value'])) {
 | 
			
		||||
            console.log(setting['key'] + " updated from " + configContent[setting['key']] + " to " + setting['value']);
 | 
			
		||||
        } else if (configContent.hasOwnProperty(setting['key']) && normalizedValue(configContent[setting['key']]) === normalizedValue(setting['value'])) {
 | 
			
		||||
            console.log(setting['key'] + " unchanged, skipping");
 | 
			
		||||
        } else {
 | 
			
		||||
            console.log(setting['key'] + " set to " + setting['value']);
 | 
			
		||||
        }
 | 
			
		||||
        configContent[setting['key']] = setting['value'];
 | 
			
		||||
    });
 | 
			
		||||
    fs.writeFileSync(CONFIG_FILE_NAME, JSON.stringify(configContent));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function configToJson() {
 | 
			
		||||
    for (setting in configContent) {
 | 
			
		||||
        settings.push({
 | 
			
		||||
          key: setting,
 | 
			
		||||
          value: configContent[setting]
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
    const rawConfig = fs.readFileSync(CONFIG_FILE_NAME);
 | 
			
		||||
    configContent = JSON.parse(rawConfig);
 | 
			
		||||
    console.log(`${CONFIG_FILE_NAME} file found, using provided config`);
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    if (e.code !== 'ENOENT') {
 | 
			
		||||
      throw new Error(e);
 | 
			
		||||
    } else {
 | 
			
		||||
 | 
			
		||||
      if (fs.existsSync(GENERATED_CONFIG_FILE_NAME)) {
 | 
			
		||||
        console.log(`${CONFIG_FILE_NAME} file not found, reading current config from generated-config.js`);
 | 
			
		||||
        parseGeneratedFile();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (args.length > 0) {
 | 
			
		||||
    args.forEach(setting => {
 | 
			
		||||
        setting = setting.split('=');
 | 
			
		||||
        const key = setting[0];
 | 
			
		||||
        let value = setting[1];
 | 
			
		||||
        addSetting(key, normalizedValue(value));
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
saveSettingsJson();
 | 
			
		||||
console.log('new json',  configContent);
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user