Fill-in skipped blocks on page render for blocks list and homepage

pull/871/head
Stamates 6 years ago
parent ae6cf0faa7
commit 3254ead1ee
  1. 62
      apps/block_scout_web/assets/__tests__/pages/block.js
  2. 39
      apps/block_scout_web/assets/__tests__/pages/chain.js
  3. 6
      apps/block_scout_web/assets/css/components/_tile.scss
  4. 29
      apps/block_scout_web/assets/js/pages/block.js
  5. 47
      apps/block_scout_web/assets/js/pages/chain.js
  6. 31
      apps/block_scout_web/assets/js/utils.js
  7. 2
      apps/block_scout_web/test/block_scout_web/features/pages/block_list_page.ex
  8. 10
      apps/block_scout_web/test/block_scout_web/features/pages/chain_page.ex
  9. 23
      apps/block_scout_web/test/block_scout_web/features/viewing_blocks_test.exs
  10. 54
      apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs

@ -1,6 +1,5 @@
import { reducer, initialState } from '../../js/pages/block'
test('CHANNEL_DISCONNECTED', () => {
const state = initialState
const action = {
@ -11,6 +10,61 @@ test('CHANNEL_DISCONNECTED', () => {
expect(output.channelDisconnected).toBe(true)
})
describe('PAGE_LOAD', () => {
test('page 1 loads block numbers', () => {
const state = initialState
const action = {
type: 'PAGE_LOAD',
beyondPageOne: false,
blockNumbers: [2, 1]
}
const output = reducer(state, action)
expect(output.beyondPageOne).toBe(false)
expect(output.blockNumbers).toEqual([2, 1])
expect(output.skippedBlockNumbers).toEqual([])
})
test('page 2 loads block numbers', () => {
const state = initialState
const action = {
type: 'PAGE_LOAD',
beyondPageOne: true,
blockNumbers: [2, 1]
}
const output = reducer(state, action)
expect(output.beyondPageOne).toBe(true)
expect(output.blockNumbers).toEqual([2, 1])
expect(output.skippedBlockNumbers).toEqual([])
})
test('page 1 with skipped blocks', () => {
const state = initialState
const action = {
type: 'PAGE_LOAD',
beyondPageOne: false,
blockNumbers: [4, 1]
}
const output = reducer(state, action)
expect(output.beyondPageOne).toBe(false)
expect(output.blockNumbers).toEqual([4, 3, 2, 1])
expect(output.skippedBlockNumbers).toEqual([3, 2])
})
test('page 2 with skipped blocks', () => {
const state = initialState
const action = {
type: 'PAGE_LOAD',
beyondPageOne: true,
blockNumbers: [4, 1]
}
const output = reducer(state, action)
expect(output.beyondPageOne).toBe(true)
expect(output.blockNumbers).toEqual([4, 3, 2, 1])
expect(output.skippedBlockNumbers).toEqual([3, 2])
})
})
describe('RECEIVED_NEW_BLOCK', () => {
test('receives new block', () => {
const action = {
@ -56,12 +110,12 @@ describe('RECEIVED_NEW_BLOCK', () => {
expect(output.newBlock).toBe('test5')
expect(output.blockNumbers).toEqual([5, 4, 3, 2])
expect(output.skippedBlockNumbers).toEqual([3, 4])
expect(output.skippedBlockNumbers).toEqual([4, 3])
})
test('replaces skipped block', () => {
const state = Object.assign({}, initialState, {
blockNumbers: [5, 4, 3, 2, 1],
skippedBlockNumbers: [1, 3, 4]
skippedBlockNumbers: [4, 3, 1]
})
const action = {
type: 'RECEIVED_NEW_BLOCK',
@ -74,7 +128,7 @@ describe('RECEIVED_NEW_BLOCK', () => {
expect(output.newBlock).toBe('test3')
expect(output.blockNumbers).toEqual([5, 4, 3, 2, 1])
expect(output.skippedBlockNumbers).toEqual([1, 4])
expect(output.skippedBlockNumbers).toEqual([4, 1])
})
test('replaces duplicated block', () => {
const state = Object.assign({}, initialState, {

@ -1,5 +1,30 @@
import { reducer, initialState } from '../../js/pages/chain'
describe('PAGE_LOAD', () => {
test('loads block numbers', () => {
const state = initialState
const action = {
type: 'PAGE_LOAD',
blockNumbers: [2, 1]
}
const output = reducer(state, action)
expect(output.blockNumbers).toEqual([2, 1])
expect(output.skippedBlockNumbers).toEqual([])
})
test('loads with skipped blocks', () => {
const state = initialState
const action = {
type: 'PAGE_LOAD',
blockNumbers: [4, 1]
}
const output = reducer(state, action)
expect(output.blockNumbers).toEqual([4, 3, 2, 1])
expect(output.skippedBlockNumbers).toEqual([3, 2])
})
})
test('RECEIVED_NEW_ADDRESS_COUNT', () => {
const state = Object.assign({}, initialState, {
addressCount: '1,000'
@ -54,12 +79,12 @@ describe('RECEIVED_NEW_BLOCK', () => {
expect(output.averageBlockTime).toEqual('5 seconds')
expect(output.newBlock).toBe('test5')
expect(output.blockNumbers).toEqual([5, 4, 3, 2])
expect(output.skippedBlockNumbers).toEqual([3, 4])
expect(output.skippedBlockNumbers).toEqual([4, 3])
})
test('replaces skipped block', () => {
const state = Object.assign({}, initialState, {
blockNumbers: [4, 3, 2, 1],
skippedBlockNumbers: [1, 2, 3]
skippedBlockNumbers: [3, 2, 1]
})
const action = {
type: 'RECEIVED_NEW_BLOCK',
@ -74,7 +99,7 @@ describe('RECEIVED_NEW_BLOCK', () => {
expect(output.averageBlockTime).toEqual('5 seconds')
expect(output.newBlock).toBe('test2')
expect(output.blockNumbers).toEqual([4, 3, 2, 1])
expect(output.skippedBlockNumbers).toEqual([1, 3])
expect(output.skippedBlockNumbers).toEqual([3, 1])
})
test('replaces duplicated block', () => {
const state = Object.assign({}, initialState, {
@ -116,7 +141,7 @@ describe('RECEIVED_NEW_BLOCK', () => {
test('only tracks 4 blocks based on page display limit', () => {
const state = Object.assign({}, initialState, {
blockNumbers: [5, 4, 3, 2],
skippedBlockNumbers: [2, 3, 4]
skippedBlockNumbers: [4, 3, 2]
})
const action = {
type: 'RECEIVED_NEW_BLOCK',
@ -129,12 +154,12 @@ describe('RECEIVED_NEW_BLOCK', () => {
expect(output.newBlock).toBe('test6')
expect(output.blockNumbers).toEqual([6, 5, 4, 3])
expect(output.skippedBlockNumbers).toEqual([3, 4])
expect(output.skippedBlockNumbers).toEqual([4, 3])
})
test('skipped blocks list replaced when another block comes in with +3 blockheight', () => {
const state = Object.assign({}, initialState, {
blockNumbers: [5, 4, 3, 2],
skippedBlockNumbers: [2, 3, 4]
skippedBlockNumbers: [4, 3, 2]
})
const action = {
type: 'RECEIVED_NEW_BLOCK',
@ -147,7 +172,7 @@ describe('RECEIVED_NEW_BLOCK', () => {
expect(output.newBlock).toBe('test10')
expect(output.blockNumbers).toEqual([10, 9, 8, 7])
expect(output.skippedBlockNumbers).toEqual([7, 8, 9])
expect(output.skippedBlockNumbers).toEqual([9, 8, 7])
})
})

@ -17,7 +17,7 @@
&-type {
&-Block {
&-block {
border-left: 4px solid $indigo;
.tile-label {
@ -25,7 +25,7 @@
}
}
&-Uncle {
&-uncle {
border-left: 4px solid $cyan;
.tile-label {
@ -33,7 +33,7 @@
}
}
&-Reorg {
&-reorg {
border-left: 4px solid $purple;
.tile-label {

@ -4,7 +4,7 @@ import URI from 'urijs'
import humps from 'humps'
import socket from '../socket'
import { updateAllAges } from '../lib/from_now'
import { initRedux, prependWithClingBottom } from '../utils'
import { buildFullBlockList, initRedux, beforeWithClingBottom, skippedBlockListBuilder } from '../utils'
export const initialState = {
blockNumbers: [],
@ -18,9 +18,12 @@ export const initialState = {
export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD': {
const blockNumbers = buildFullBlockList(action.blockNumbers)
const skippedBlockNumbers = _.difference(blockNumbers, action.blockNumbers)
return Object.assign({}, state, {
beyondPageOne: action.beyondPageOne,
blockNumbers: action.blockNumbers
blockNumbers,
skippedBlockNumbers
})
}
case 'CHANNEL_DISCONNECTED': {
@ -43,9 +46,7 @@ export function reducer (state = initialState, action) {
} else {
let skippedBlockNumbers = state.skippedBlockNumbers.slice(0)
if (blockNumber > state.blockNumbers[0] + 1) {
for (let i = state.blockNumbers[0] + 1; i < blockNumber; i++) {
skippedBlockNumbers.push(i)
}
skippedBlockListBuilder(skippedBlockNumbers, blockNumber, state.blockNumbers[0])
}
const newBlockNumbers = _.chain([blockNumber])
.union(skippedBlockNumbers, state.blockNumbers)
@ -85,22 +86,21 @@ if ($blockListPage.length) {
},
render (state, oldState) {
const $channelDisconnected = $('[data-selector="channel-disconnected-message"]')
const $blocksList = $('[data-selector="blocks-list"]')
const skippedBlockNumbers = _.difference(state.skippedBlockNumbers, oldState.skippedBlockNumbers)
if (state.channelDisconnected) $channelDisconnected.show()
if (oldState.newBlock !== state.newBlock || (state.replaceBlock && oldState.replaceBlock !== state.replaceBlock)) {
if ((state.newBlock && oldState.newBlock !== state.newBlock) || skippedBlockNumbers.length) {
if (state.replaceBlock && oldState.replaceBlock !== state.replaceBlock) {
const $replaceBlock = $(`[data-block-number="${state.replaceBlock}"]`)
$replaceBlock.addClass('shrink-out')
setTimeout(() => $replaceBlock.replaceWith(state.newBlock), 400)
} else {
if (oldState.skippedBlockNumbers !== state.skippedBlockNumbers) {
const newSkippedBlockNumbers = _.difference(state.skippedBlockNumbers, oldState.skippedBlockNumbers)
_.map(newSkippedBlockNumbers, (skippedBlockNumber) => {
prependWithClingBottom($blocksList, placeHolderBlock(skippedBlockNumber))
if (skippedBlockNumbers.length) {
_.forEachRight(skippedBlockNumbers, (skippedBlockNumber) => {
beforeWithClingBottom($(`[data-block-number="${skippedBlockNumber - 1}"]`), placeHolderBlock(skippedBlockNumber))
})
}
prependWithClingBottom($blocksList, state.newBlock)
beforeWithClingBottom($(`[data-block-number="${state.blockNumbers[0] - 1}"]`), state.newBlock)
}
updateAllAges()
}
@ -110,11 +110,10 @@ if ($blockListPage.length) {
function placeHolderBlock (blockNumber) {
return `
<div class="my-3">
<div class="my-3" style="height: 98px;" data-selector="place-holder" data-block-number="${blockNumber}">
<div
class="tile tile-type-block d-flex align-items-center fade-up"
data-selector="place-holder"
data-block-number="${blockNumber}"
style="height: 98px;"
>
<span class="loading-spinner-small ml-1 mr-4">
<span class="loading-spinner-block-1"></span>

@ -5,7 +5,7 @@ import numeral from 'numeral'
import socket from '../socket'
import { updateAllAges } from '../lib/from_now'
import { exchangeRateChannel, formatUsdValue } from '../lib/currency'
import { batchChannel, initRedux, slideDownPrepend } from '../utils'
import { batchChannel, buildFullBlockList, initRedux, skippedBlockListBuilder, slideDownPrepend } from '../utils'
import { createMarketHistoryChart } from '../lib/market_history_chart'
const BATCH_THRESHOLD = 10
@ -28,9 +28,13 @@ export const initialState = {
export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD': {
const fullBlockNumberList = buildFullBlockList(action.blockNumbers)
const fullSkippedBlockNumberList = _.difference(fullBlockNumberList, action.blockNumbers)
const blockNumbers = fullBlockNumberList.slice(0, 4)
return Object.assign({}, state, {
blockNumbers: action.blockNumbers,
transactionCount: numeral(action.transactionCount).value()
blockNumbers,
transactionCount: numeral(action.transactionCount).value(),
skippedBlockNumbers: _.intersection(fullSkippedBlockNumberList, blockNumbers)
})
}
case 'RECEIVED_NEW_ADDRESS_COUNT': {
@ -52,21 +56,18 @@ export function reducer (state = initialState, action) {
} else {
let skippedBlockNumbers = state.skippedBlockNumbers.slice(0)
if (blockNumber > state.blockNumbers[0] + 1) {
let lastPlaceholder = state.blockNumbers[0] + 1
if (blockNumber - lastPlaceholder > 3) {
lastPlaceholder = blockNumber - 3
let oldestBlock = state.blockNumbers[0]
if (blockNumber - oldestBlock >= 3) {
skippedBlockNumbers = []
if (blockNumber - oldestBlock > 3) oldestBlock = blockNumber - 4
}
for (let i = lastPlaceholder; i < blockNumber; i++) {
skippedBlockNumbers.push(i)
}
skippedBlockListBuilder(skippedBlockNumbers, blockNumber, oldestBlock)
}
const newBlockNumbers = _.chain([blockNumber])
.union(skippedBlockNumbers, state.blockNumbers)
.orderBy([], ['desc'])
.slice(0, 4)
.value()
const newSkippedBlockNumbers = _.intersection(skippedBlockNumbers, newBlockNumbers)
return Object.assign({}, state, {
averageBlockTime: action.msg.averageBlockTime,
@ -142,6 +143,8 @@ if ($chainDetailsPage.length) {
const $marketCap = $('[data-selector="market-cap"]')
const $transactionsList = $('[data-selector="transactions-list"]')
const $transactionCount = $('[data-selector="transaction-count"]')
const newTransactions = _.difference(state.newTransactions, oldState.newTransactions)
const skippedBlockNumbers = _.difference(state.skippedBlockNumbers, oldState.skippedBlockNumbers)
if (oldState.addressCount !== state.addressCount) {
$addressCount.empty().append(state.addressCount)
@ -152,24 +155,23 @@ if ($chainDetailsPage.length) {
if (oldState.usdMarketCap !== state.usdMarketCap) {
$marketCap.empty().append(formatUsdValue(state.usdMarketCap))
}
if (state.newBlock && oldState.newBlock !== state.newBlock) {
if ((state.newBlock && oldState.newBlock !== state.newBlock) || skippedBlockNumbers.length) {
if (state.replaceBlock && oldState.replaceBlock !== state.replaceBlock) {
const $replaceBlock = $(`[data-block-number="${state.replaceBlock}"]`)
$replaceBlock.addClass('shrink-out')
setTimeout(() => $replaceBlock.replaceWith(state.newBlock), 400)
} else {
if (oldState.skippedBlockNumbers !== state.skippedBlockNumbers) {
const newSkippedBlockNumbers = _.chain(state.skippedBlockNumbers)
.difference(oldState.skippedBlockNumbers)
.intersection(state.blockNumbers)
.value()
_.map(newSkippedBlockNumbers, (skippedBlockNumber) => {
if (state.newBlock) {
$blockList.children().last().remove()
$blockList.prepend(placeHolderBlock(skippedBlockNumber))
})
$blockList.prepend(newBlockHtml(state.newBlock))
}
if (skippedBlockNumbers.length) {
const newSkippedBlockNumbers = _.intersection(skippedBlockNumbers, state.blockNumbers)
_.each(newSkippedBlockNumbers, (skippedBlockNumber) => {
$blockList.children().last().remove()
$blockList.prepend(newBlockHtml(state.newBlock))
$(`[data-block-number="${skippedBlockNumber + 1}"]`).parent().after(placeHolderBlock(skippedBlockNumber))
})
}
}
updateAllAges()
}
@ -180,7 +182,7 @@ if ($chainDetailsPage.length) {
} else {
$channelBatching.hide()
}
if (oldState.newTransactions !== state.newTransactions) {
if (newTransactions.length) {
const newTransactionsToInsert = state.newTransactions.slice(oldState.newTransactions.length)
$transactionsList
.children()
@ -190,7 +192,6 @@ if ($chainDetailsPage.length) {
updateAllAges()
}
if (oldState.availableSupply !== state.availableSupply || oldState.marketHistoryData !== state.marketHistoryData) {
chart.update(state.availableSupply, state.marketHistoryData)
}
@ -214,9 +215,9 @@ function placeHolderBlock (blockNumber) {
>
<div
class="tile tile-type-block d-flex align-items-center fade-up"
style="height: 100px;"
data-selector="place-holder"
data-block-number="${blockNumber}"
style="height: 100px;"
>
<span class="loading-spinner-small ml-1 mr-4">
<span class="loading-spinner-block-1"></span>

@ -14,6 +14,12 @@ export function batchChannel (func) {
}
}
export function buildFullBlockList (blockNumbers) {
const newestBlock = _.first(blockNumbers)
const oldestBlock = _.last(blockNumbers)
return skippedBlockListBuilder([], newestBlock + 1, oldestBlock - 1)
}
export function initRedux (reducer, { main, render, debug } = {}) {
if (!reducer) {
console.error('initRedux: You need a reducer to initialize Redux.')
@ -34,16 +40,33 @@ export function initRedux (reducer, { main, render, debug } = {}) {
if (main) main(store)
}
export function skippedBlockListBuilder (skippedBlockNumbers, newestBlock, oldestBlock) {
for (let i = newestBlock - 1; i > oldestBlock; i--) skippedBlockNumbers.push(i)
return skippedBlockNumbers
}
export function slideDownPrepend ($el, content, callback) {
const $content = $(content)
$el.prepend($content.hide())
$content.slideDown({ complete: callback })
}
export function slideDownBefore ($el, content, callback) {
const $content = $(content)
$el.before($content.hide())
$content.slideDown({ complete: callback })
}
export function prependWithClingBottom ($el, content) {
return slideDownPrepend($el, content, clingBottom($el, content))
}
export function beforeWithClingBottom ($el, content) {
return slideDownBefore($el, content, clingBottom($el, content))
}
function clingBottom ($el, content) {
function userAtTop () {
return window.scrollY < $('[data-selector="navbar"]').outerHeight()
}
if (userAtTop()) return slideDownPrepend($el, content)
if (userAtTop()) return true
let isAnimating
function setIsAnimating () {
@ -73,8 +96,10 @@ export function prependWithClingBottom ($el, content) {
$el.off('animationend animationcancel', stopClinging)
}
return slideDownPrepend($el, content, () => {
return {
function () {
$el.on('animationend animationcancel', stopClinging)
setTimeout(() => !isAnimating && stopClinging(), 100)
})
}
}
}

@ -20,7 +20,7 @@ defmodule BlockScoutWeb.BlockListPage do
end
def block(%Block{number: block_number}) do
css("[data-selector='block-tile'][data-block-number='#{block_number}']")
css("[data-block-number='#{block_number}']")
end
def place_holder_blocks(count) do

@ -5,7 +5,11 @@ defmodule BlockScoutWeb.ChainPage do
import Wallaby.Query, only: [css: 1, css: 2]
alias Explorer.Chain.Transaction
alias Explorer.Chain.{Block, Transaction}
def block(%Block{number: block_number}) do
css("[data-block-number='#{block_number}']")
end
def blocks(count: count) do
css("[data-selector='chain-block']", count: count)
@ -15,6 +19,10 @@ defmodule BlockScoutWeb.ChainPage do
css("[data-test='contract-creation'] [data-address-hash='#{hash}']")
end
def place_holder_blocks(count) do
css("[data-selector='place-holder']", count: count)
end
def search(session, text) do
session
|> fill_in(css("[data-test='search_input']"), with: text)

@ -2,6 +2,7 @@ defmodule BlockScoutWeb.ViewingBlocksTest do
use BlockScoutWeb.FeatureCase, async: true
alias BlockScoutWeb.{BlockListPage, BlockPage, Notifier}
alias Explorer.Chain.Block
setup do
timestamp = Timex.now() |> Timex.shift(hours: -1)
@ -159,6 +160,28 @@ defmodule BlockScoutWeb.ViewingBlocksTest do
|> assert_has(BlockListPage.block(skipped_block))
|> assert_has(BlockListPage.place_holder_blocks(2))
end
test "inserts place holder blocks on render for out of order blocks", %{session: session} do
insert(:block, number: 315)
session
|> BlockListPage.visit_page()
|> assert_has(BlockListPage.block(%Block{number: 314}))
|> assert_has(BlockListPage.place_holder_blocks(3))
end
test "replaces rendered place holder block if skipped block received", %{session: session} do
insert(:block, number: 315)
BlockListPage.visit_page(session)
block = insert(:block, number: 314)
Notifier.handle_event({:chain_event, :blocks, [block]})
session
|> assert_has(BlockListPage.block(block))
|> assert_has(BlockListPage.place_holder_blocks(2))
end
end
describe "viewing uncle blocks list" do

@ -3,7 +3,8 @@ defmodule BlockScoutWeb.ViewingChainTest do
use BlockScoutWeb.FeatureCase, async: true
alias BlockScoutWeb.{AddressPage, BlockPage, ChainPage, TransactionPage}
alias BlockScoutWeb.{AddressPage, BlockPage, ChainPage, Notifier, TransactionPage}
alias Explorer.Chain.Block
setup do
Enum.map(401..404, &insert(:block, number: &1))
@ -50,6 +51,57 @@ defmodule BlockScoutWeb.ViewingChainTest do
|> ChainPage.visit_page()
|> assert_has(ChainPage.blocks(count: 4))
end
test "inserts place holder blocks if out of order block received", %{session: session} do
ChainPage.visit_page(session)
block = insert(:block, number: 409)
Notifier.handle_event({:chain_event, :blocks, :realtime, [block]})
session
|> assert_has(ChainPage.block(block))
|> assert_has(ChainPage.place_holder_blocks(3))
end
test "replaces place holder block if skipped block received", %{session: session} do
ChainPage.visit_page(session)
block = insert(:block, number: 409)
Notifier.handle_event({:chain_event, :blocks, :realtime, [block]})
session
|> assert_has(ChainPage.block(block))
|> assert_has(ChainPage.place_holder_blocks(3))
skipped_block = insert(:block, number: 408)
Notifier.handle_event({:chain_event, :blocks, :realtime, [skipped_block]})
session
|> assert_has(ChainPage.block(skipped_block))
|> assert_has(ChainPage.place_holder_blocks(2))
end
test "inserts place holder blocks on render for out of order blocks", %{session: session} do
insert(:block, number: 409)
session
|> ChainPage.visit_page()
|> assert_has(ChainPage.block(%Block{number: 408}))
|> assert_has(ChainPage.place_holder_blocks(3))
end
test "replaces rendered place holder block if skipped block received", %{session: session} do
insert(:block, number: 409)
ChainPage.visit_page(session)
block = insert(:block, number: 408)
Notifier.handle_event({:chain_event, :blocks, [block]})
session
|> assert_has(ChainPage.block(block))
|> assert_has(ChainPage.place_holder_blocks(2))
end
end
describe "viewing transactions" do

Loading…
Cancel
Save