From 8f70d0d23f2a595982b1baebc854af8ce83fe436 Mon Sep 17 00:00:00 2001 From: Vasyl Ivanchuk Date: Mon, 4 Dec 2023 13:10:00 +0200 Subject: [PATCH] fix: different no transaction messages for non-existing and empty batches/blocks (#107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ Different _no transaction_ messages for non-existing and empty batches/blocks: Empty batch: image Non-existing batch: image ## Why ❔ For better UX we want to show different _no transaction_ messages in different cases: `This Batch doesn’t have any transactions` - for empty batch, `This Batch has not been created or sealed yet` - for non-existing batch, same for blocks. ## Checklist - [X] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [X] Tests for the changes have been added / updated. --- .../app/src/components/batches/InfoTable.vue | 21 +++++++--- .../batches/TransactionEmptyState.vue | 27 ++++++------ .../app/src/components/blocks/InfoTable.vue | 17 +++++--- .../blocks/TransactionEmptyState.vue | 30 ++++++-------- packages/app/src/locales/en.json | 16 +++----- packages/app/src/locales/uk.json | 2 +- packages/app/src/views/BatchView.vue | 12 +++--- packages/app/src/views/BlockView.vue | 12 +++--- .../components/batches/InfoTable.spec.ts | 31 ++++++++++++++ .../batches/TransactionEmptyState.spec.ts | 17 +++++++- .../tests/components/blocks/InfoTable.spec.ts | 34 +++++++++++++++ .../blocks/TransactionEmptyState.spec.ts | 41 +++++++++++++++++++ packages/app/tests/views/BatchView.spec.ts | 29 ++----------- packages/app/tests/views/BlockView.spec.ts | 27 +----------- 14 files changed, 194 insertions(+), 122 deletions(-) create mode 100644 packages/app/tests/components/blocks/TransactionEmptyState.spec.ts diff --git a/packages/app/src/components/batches/InfoTable.vue b/packages/app/src/components/batches/InfoTable.vue index e0a0874d46..1784549814 100644 --- a/packages/app/src/components/batches/InfoTable.vue +++ b/packages/app/src/components/batches/InfoTable.vue @@ -31,6 +31,10 @@ const props = defineProps({ type: Object as PropType, default: null, }, + batchNumber: { + type: String, + required: true, + }, loading: { type: Boolean, default: true, @@ -45,15 +49,20 @@ const tableInfoItems = computed(() => { component?: Component; url?: string; }; - if (!props.batch) { - return []; - } + let tableItems: InfoTableItem[] = [ { label: t("batches.index"), tooltip: t("batches.indexTooltip"), - value: props.batch.number, + value: props.batchNumber, }, + ]; + + if (!props.batch) { + return [tableItems]; + } + + tableItems.push( { label: t("batches.size"), tooltip: t("batches.sizeTooltip"), @@ -70,8 +79,8 @@ const tableInfoItems = computed(() => { tooltip: t("batches.rootHashTooltip"), value: props.batch.rootHash ? { value: props.batch.rootHash } : t("batches.noRootHashYet"), component: props.batch.rootHash ? CopyContent : undefined, - }, - ]; + } + ); for (const [key, timeKey] of [ ["commitTxHash", "committedAt", "notYetCommitted"], ["proveTxHash", "provenAt", "notYetProven"], diff --git a/packages/app/src/components/batches/TransactionEmptyState.vue b/packages/app/src/components/batches/TransactionEmptyState.vue index 73f7dc0b42..f7271c2287 100644 --- a/packages/app/src/components/batches/TransactionEmptyState.vue +++ b/packages/app/src/components/batches/TransactionEmptyState.vue @@ -1,14 +1,10 @@ @@ -19,16 +15,17 @@ import { useI18n } from "vue-i18n"; import EmptyState from "@/components/common/EmptyState.vue"; const { t } = useI18n(); + +defineProps({ + batchExists: { + type: Boolean, + required: true, + }, +}); diff --git a/packages/app/src/components/blocks/InfoTable.vue b/packages/app/src/components/blocks/InfoTable.vue index 9c2ddc43c4..63ddc25491 100644 --- a/packages/app/src/components/blocks/InfoTable.vue +++ b/packages/app/src/components/blocks/InfoTable.vue @@ -32,6 +32,10 @@ const props = defineProps({ type: Object as PropType, default: null, }, + blockNumber: { + type: String, + required: true, + }, loading: { type: Boolean, default: true, @@ -52,11 +56,13 @@ const tableInfoItems = computed(() => { disabledTooltip?: string; }; }; + let tableItems: InfoTableItem[] = [ + { label: t("blocks.table.blockNumber"), tooltip: t("blocks.table.blockNumberTooltip"), value: props.blockNumber }, + ]; if (!props.block) { - return []; + return [tableItems]; } - let tableItems: InfoTableItem[] = [ - { label: t("blocks.table.blockNumber"), tooltip: t("blocks.table.blockNumberTooltip"), value: props.block.number }, + tableItems.push( { label: t("blocks.table.blockSize"), tooltip: t("blocks.table.blockSizeTooltip"), @@ -95,8 +101,8 @@ const tableInfoItems = computed(() => { tooltip: t("blocks.table.timestampTooltip"), value: { value: props.block.timestamp }, component: TimeField, - }, - ]; + } + ); for (const [key, timeKey] of [ ["commitTxHash", "committedAt", "notYetCommitted"], ["proveTxHash", "provenAt", "notYetProven"], @@ -135,6 +141,7 @@ const tableInfoItems = computed(() => { .two-section-view { @apply grid gap-4 pb-1.5 lg:grid-cols-2; } + .hide-mobile { @apply hidden lg:block; } diff --git a/packages/app/src/components/blocks/TransactionEmptyState.vue b/packages/app/src/components/blocks/TransactionEmptyState.vue index 77699cfb06..e5837e96e8 100644 --- a/packages/app/src/components/blocks/TransactionEmptyState.vue +++ b/packages/app/src/components/blocks/TransactionEmptyState.vue @@ -1,17 +1,10 @@ @@ -22,16 +15,17 @@ import { useI18n } from "vue-i18n"; import EmptyState from "@/components/common/EmptyState.vue"; const { t } = useI18n(); + +defineProps({ + blockExists: { + type: Boolean, + required: true, + }, +}); diff --git a/packages/app/src/locales/en.json b/packages/app/src/locales/en.json index f9480fd9e3..b62ec696f3 100644 --- a/packages/app/src/locales/en.json +++ b/packages/app/src/locales/en.json @@ -73,12 +73,8 @@ "transactionTable": { "title": "Block Transactions", "showMore": "Show more transactions ->", - "notFound": { - "title": "This Block doesn't have any transactions", - "subtitle": "We always have a zero block at the end of the batch. Want to know why?", - "urlTitle": "Visit our docs page", - "url": "https://docs-v2-zksync.web.app/dev/developer-guides/transactions/blocks.html#blocks-in-zksync-2-0" - } + "noTransactions": "This Block doesn't have any transactions", + "blockNotFound": "This Block has not been created or sealed yet" } }, "transfers": { @@ -258,10 +254,8 @@ "transactionTable": { "title": "Batch Transactions", "error": "Something went wrong", - "notFound": { - "title": "This Batch doesn't have any transactions", - "subtitle": "We can't find transactions for this batch \n We'll fix it in a moment; please refresh the page" - } + "noTransactions": "This Batch doesn't have any transactions", + "batchNotFound": "This Batch has not been created or sealed yet" }, "table": { "status": "Status", @@ -409,7 +403,7 @@ }, "contractVerification": { "title": "Smart Contract Verification", - "subtitle": "Source code verification provides transparency for users interacting with smart contracts. By uploading the source code, zkScan will match the compiled code with that on the blockchain.", + "subtitle": "Source code verification provides transparency for users interacting with smart contracts. By uploading the source code, Era Explorer will match the compiled code with that on the blockchain.", "resources": { "title": "You can also verify your smart-contract using {hardhat}", "links": { diff --git a/packages/app/src/locales/uk.json b/packages/app/src/locales/uk.json index 898260232f..57b0dd91d0 100644 --- a/packages/app/src/locales/uk.json +++ b/packages/app/src/locales/uk.json @@ -212,7 +212,7 @@ }, "contractVerification": { "title": "Верифікація Смарт контракту", - "subtitle": "Перевірка вихідного коду забезпечує прозорість для користувачів, які взаємодіють зі смарт-контрактами. Завантаживши вихідний код, zkScan зіставить скомпільований код із кодом у блокчейні.", + "subtitle": "Перевірка вихідного коду забезпечує прозорість для користувачів, які взаємодіють зі смарт-контрактами. Завантаживши вихідний код, Era Explorer зіставить скомпільований код із кодом у блокчейні.", "form": { "title": "Деталі Контракту", "compilationInfo": "Деталі компіляції", diff --git a/packages/app/src/views/BatchView.vue b/packages/app/src/views/BatchView.vue index 50b415d158..1472dc4664 100644 --- a/packages/app/src/views/BatchView.vue +++ b/packages/app/src/views/BatchView.vue @@ -7,19 +7,19 @@ - + <Title v-if="!batchPending" :title="t('batches.batchNumber')" :value="id"> {{ parseInt(id) }}
- + -
+

{{ t("batches.transactionTable.title") }}

@@ -49,7 +49,7 @@ import { isBlockNumber } from "@/utils/validators"; const { t } = useI18n(); -const { useNotFoundView, setNotFoundView } = useNotFound(); +const { setNotFoundView } = useNotFound(); const { getById, batchItem, isRequestPending: batchPending, isRequestFailed: batchFailed } = useBatch(); const props = defineProps({ @@ -89,8 +89,6 @@ const transactionsSearchParams = computed(() => ({ l1BatchNumber: parseInt(props.id), })); -useNotFoundView(batchPending, batchFailed, batchItem); - watchEffect(() => { if (!props.id || !isBlockNumber(props.id)) { return setNotFoundView(); diff --git a/packages/app/src/views/BlockView.vue b/packages/app/src/views/BlockView.vue index 548ed82edc..ce8fe4d117 100644 --- a/packages/app/src/views/BlockView.vue +++ b/packages/app/src/views/BlockView.vue @@ -7,15 +7,15 @@
- + <Title v-if="!blockPending" :title="t('blocks.blockNumber')" :value="id"> {{ parseInt(id) }}
- +
-
+

{{ t("blocks.transactionTable.title") }}

@@ -53,7 +53,7 @@ import { isBlockNumber } from "@/utils/validators"; const { t } = useI18n(); -const { useNotFoundView, setNotFoundView } = useNotFound(); +const { setNotFoundView } = useNotFound(); const { getById, blockItem, isRequestPending: blockPending, isRequestFailed: blockFailed } = useBlock(); const props = defineProps({ @@ -81,8 +81,6 @@ const transactionsSearchParams = computed(() => ({ blockNumber: parseInt(props.id), })); -useNotFoundView(blockPending, blockFailed, blockItem); - watchEffect(() => { if (!props.id || !isBlockNumber(props.id)) { return setNotFoundView(); diff --git a/packages/app/tests/components/batches/InfoTable.spec.ts b/packages/app/tests/components/batches/InfoTable.spec.ts index 63fc2948a8..db21332a81 100644 --- a/packages/app/tests/components/batches/InfoTable.spec.ts +++ b/packages/app/tests/components/batches/InfoTable.spec.ts @@ -60,6 +60,7 @@ describe("InfoTable:", () => { }, props: { batch: batchItem, + batchNumber: batchItem.number.toString(), loading: false, }, }); @@ -118,6 +119,33 @@ describe("InfoTable:", () => { wrapper.unmount(); }); + + describe("when batch is not set", () => { + it("renders only batch number", () => { + const wrapper = mount(InfoTable, { + global: { + stubs: { + InfoTooltip: { template: "
" }, + }, + plugins: [i18n], + }, + props: { + batchNumber: batchItem.number.toString(), + loading: false, + }, + }); + + const rowArray = wrapper.findAll("tr"); + expect(rowArray.length).toBe(1); + + const batchIndex = rowArray[0].findAll("td"); + expect(batchIndex[0].find(".batch-info-field-label").text()).toBe(i18n.global.t("batches.index")); + expect(batchIndex[0].findComponent(InfoTooltip).text()).toBe(i18n.global.t("batches.indexTooltip")); + expect(batchIndex[1].text()).toBe("42"); + wrapper.unmount(); + }); + }); + it("renders loading state", () => { const wrapper = mount(InfoTable, { global: { @@ -125,6 +153,7 @@ describe("InfoTable:", () => { }, props: { loading: true, + batchNumber: batchItem.number.toString(), }, }); expect(wrapper.findAll(".content-loader").length).toBe(20); @@ -141,6 +170,7 @@ describe("InfoTable:", () => { props: { batch: batchItem, loading: false, + batchNumber: batchItem.number.toString(), }, }); @@ -175,6 +205,7 @@ describe("InfoTable:", () => { }, props: { batch: batchItem, + batchNumber: batchItem.number.toString(), loading: false, }, }); diff --git a/packages/app/tests/components/batches/TransactionEmptyState.spec.ts b/packages/app/tests/components/batches/TransactionEmptyState.spec.ts index bda6806b78..f683d2923f 100644 --- a/packages/app/tests/components/batches/TransactionEmptyState.spec.ts +++ b/packages/app/tests/components/batches/TransactionEmptyState.spec.ts @@ -16,13 +16,26 @@ describe("TransactionEmptyState", () => { en: enUS, }, }); - it("renders component properly", async () => { + it("renders component properly for existing batch", async () => { const { getByText } = render(TransactionEmptyState, { global: { plugins: [i18n], }, + props: { + batchExists: true, + }, }); getByText("This Batch doesn't have any transactions"); - getByText("We can't find transactions for this batch We'll fix it in a moment; please refresh the page"); + }); + it("renders component properly for nonexisting batch", async () => { + const { getByText } = render(TransactionEmptyState, { + global: { + plugins: [i18n], + }, + props: { + batchExists: false, + }, + }); + getByText("This Batch has not been created or sealed yet"); }); }); diff --git a/packages/app/tests/components/blocks/InfoTable.spec.ts b/packages/app/tests/components/blocks/InfoTable.spec.ts index 17ca8b8227..c2422369e7 100644 --- a/packages/app/tests/components/blocks/InfoTable.spec.ts +++ b/packages/app/tests/components/blocks/InfoTable.spec.ts @@ -59,6 +59,7 @@ describe("InfoTable:", () => { executeTxHash: "0x8d1a78d1da5aba1d0755ec9dbcba938f3920681d2a3d4d374ef265a50858f364", executedAt: "2022-04-13T16:54:37.784185Z", }, + blockNumber: "1", loading: false, }, }); @@ -113,6 +114,33 @@ describe("InfoTable:", () => { expect(executed[0].findComponent(InfoTooltip).text()).toBe(i18n.global.t("blocks.table.executedAtTooltip")); expect(executed[1].text()).includes(localDateFromISOString("2022-04-13T16:54:37.784185Z")); }); + describe("when block is not set", () => { + it("renders only block number", () => { + const wrapper = mount(InfoTable, { + global: { + stubs: { + RouterLink: RouterLinkStub, + InfoTooltip: { template: "
" }, + }, + plugins: [i18n], + }, + props: { + blockNumber: "1", + loading: false, + }, + }); + + const rowArray = wrapper.findAll("tr"); + expect(rowArray.length).toBe(1); + + const blockNumber = rowArray[0].findAll("td"); + expect(blockNumber[0].find(".block-info-field-label").text()).toBe(i18n.global.t("blocks.table.blockNumber")); + expect(blockNumber[0].findComponent(InfoTooltip).text()).toBe(i18n.global.t("blocks.table.blockNumberTooltip")); + expect(blockNumber[1].text()).toBe("1"); + + wrapper.unmount(); + }); + }); it("renders loading state", () => { const wrapper = mount(InfoTable, { global: { @@ -123,6 +151,7 @@ describe("InfoTable:", () => { }, props: { loading: true, + blockNumber: "1", }, }); expect(wrapper.findAll(".content-loader").length).toBe(24); @@ -150,6 +179,7 @@ describe("InfoTable:", () => { executeTxHash: "0x8d1a78d1da5aba1d0755ec9dbcba938f3920681d2a3d4d374ef265a50858f364", executedAt: "2022-04-13T16:54:37.784185Z", }, + blockNumber: "1", loading: false, }, }); @@ -180,6 +210,7 @@ describe("InfoTable:", () => { executeTxHash: "0x8d1a78d1da5aba1d0755ec9dbcba938f3920681d2a3d4d374ef265a50858f364", executedAt: "2022-04-13T16:54:37.784185Z", }, + blockNumber: "1", loading: false, }, }); @@ -210,6 +241,7 @@ describe("InfoTable:", () => { executeTxHash: "0x8d1a78d1da5aba1d0755ec9dbcba938f3920681d2a3d4d374ef265a50858f364", executedAt: "2022-04-13T16:54:37.784185Z", }, + blockNumber: "1", loading: false, }, }); @@ -243,6 +275,7 @@ describe("InfoTable:", () => { executeTxHash: "0x8d1a78d1da5aba1d0755ec9dbcba938f3920681d2a3d4d374ef265a50858f364", executedAt: "2022-04-13T16:54:37.784185Z", }, + blockNumber: "1", loading: false, }, }); @@ -284,6 +317,7 @@ describe("InfoTable:", () => { executeTxHash: "0x8d1a78d1da5aba1d0755ec9dbcba938f3920681d2a3d4d374ef265a50858f364", executedAt: "2022-04-13T16:54:37.784185Z", }, + blockNumber: "1", loading: false, }, }); diff --git a/packages/app/tests/components/blocks/TransactionEmptyState.spec.ts b/packages/app/tests/components/blocks/TransactionEmptyState.spec.ts new file mode 100644 index 0000000000..d13a086010 --- /dev/null +++ b/packages/app/tests/components/blocks/TransactionEmptyState.spec.ts @@ -0,0 +1,41 @@ +import { createI18n } from "vue-i18n"; + +import { describe, it } from "vitest"; + +import { render } from "@testing-library/vue"; + +import TransactionEmptyState from "@/components/blocks/TransactionEmptyState.vue"; + +import enUS from "@/locales/en.json"; + +describe("TransactionEmptyState", () => { + const i18n = createI18n({ + locale: "en", + allowComposition: true, + messages: { + en: enUS, + }, + }); + it("renders component properly for existing block", async () => { + const { getByText } = render(TransactionEmptyState, { + global: { + plugins: [i18n], + }, + props: { + blockExists: true, + }, + }); + getByText("This Block doesn't have any transactions"); + }); + it("renders component properly for nonexisting block", async () => { + const { getByText } = render(TransactionEmptyState, { + global: { + plugins: [i18n], + }, + props: { + blockExists: false, + }, + }); + getByText("This Block has not been created or sealed yet"); + }); +}); diff --git a/packages/app/tests/views/BatchView.spec.ts b/packages/app/tests/views/BatchView.spec.ts index d94fff9c21..3f86549bd5 100644 --- a/packages/app/tests/views/BatchView.spec.ts +++ b/packages/app/tests/views/BatchView.spec.ts @@ -3,16 +3,15 @@ import { createI18n } from "vue-i18n"; import { describe, expect, it, vi } from "vitest"; import { mount } from "@vue/test-utils"; -import { $fetch, FetchError } from "ohmyfetch"; import enUS from "@/locales/en.json"; +import $testId from "@/plugins/testId"; import routes from "@/router/routes"; import BatchView from "@/views/BatchView.vue"; -const notFoundRoute = { name: "not-found", meta: { title: "404 Not Found" } }; const router = { - resolve: vi.fn(() => notFoundRoute), + resolve: vi.fn(), replace: vi.fn(), currentRoute: { value: {}, @@ -54,27 +53,6 @@ describe("BatchView:", () => { expect(i18n.global.t(routes.find((e) => e.name === "batch")?.meta?.title as string)).toBe("Batch"); }); - it("route is replaced with not found view on request 404 error", async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const error: any = new FetchError("404"); - error.response = { - status: 404, - }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const mock = ($fetch as any).mockRejectedValue(error); - mount(BatchView, { - props: { - id: "42", - }, - global: { - stubs: ["router-link"], - plugins: [i18n], - }, - }); - await new Promise((resolve) => setImmediate(resolve)); - expect(router.replace).toHaveBeenCalledWith(notFoundRoute); - mock.mockRestore(); - }); it("shows correct trimmed title", () => { const wrapper = mount(BatchView, { props: { @@ -82,9 +60,10 @@ describe("BatchView:", () => { }, global: { stubs: ["router-link"], - plugins: [i18n], + plugins: [i18n, $testId], }, }); + expect(wrapper.find(".breadcrumb-item span").text()).toBe("Batch #42"); }); }); diff --git a/packages/app/tests/views/BlockView.spec.ts b/packages/app/tests/views/BlockView.spec.ts index 6e8d60ca7a..4e80c08a8a 100644 --- a/packages/app/tests/views/BlockView.spec.ts +++ b/packages/app/tests/views/BlockView.spec.ts @@ -3,7 +3,6 @@ import { createI18n } from "vue-i18n"; import { describe, expect, it, vi } from "vitest"; import { mount } from "@vue/test-utils"; -import { $fetch, FetchError } from "ohmyfetch"; import enUS from "@/locales/en.json"; @@ -11,9 +10,8 @@ import $testId from "@/plugins/testId"; import routes from "@/router/routes"; import BlockView from "@/views/BlockView.vue"; -const notFoundRoute = { name: "not-found", meta: { title: "404 Not Found" } }; const router = { - resolve: vi.fn(() => notFoundRoute), + resolve: vi.fn(), replace: vi.fn(), currentRoute: { value: {}, @@ -55,27 +53,6 @@ describe("BlockView:", () => { expect(i18n.global.t(routes.find((e) => e.name === "block")?.meta?.title as string)).toBe("Block"); }); - it("route is replaced with not found view on request 404 error", async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const error: any = new FetchError("404"); - error.response = { - status: 404, - }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const mock = ($fetch as any).mockRejectedValue(error); - mount(BlockView, { - props: { - id: "12", - }, - global: { - stubs: ["router-link"], - plugins: [i18n, $testId], - }, - }); - await new Promise((resolve) => setImmediate(resolve)); - expect(router.replace).toHaveBeenCalledWith(notFoundRoute); - mock.mockRestore(); - }); it("shows correct trimmed title", () => { const wrapper = mount(BlockView, { props: { @@ -83,7 +60,7 @@ describe("BlockView:", () => { }, global: { stubs: ["router-link"], - plugins: [i18n], + plugins: [i18n, $testId], }, }); expect(wrapper.find(".breadcrumb-item span").text()).toBe("Block #42");