Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/app/src/composables/useAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ export default (context = useContext()) => {
}
} catch (error: unknown) {
item.value = null;
isRequestFailed.value = true;
if (!(error instanceof FetchError) || ![403, 404].includes(error.response?.status ?? 0)) {
isRequestFailed.value = true;
}
} finally {
isRequestPending.value = false;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/composables/useBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default (context = useContext()) => {
blockItem.value = await FetchInstance.api(context)(`/blocks/${id}`);
} catch (error: unknown) {
blockItem.value = null;
if (!(error instanceof FetchError) || error.response?.status !== 404) {
Comment thread
tomimor marked this conversation as resolved.
if (!(error instanceof FetchError) || ![403, 404].includes(error.response?.status ?? 0)) {
isRequestFailed.value = true;
}
} finally {
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/composables/useTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export default (context = useContext()) => {
return;
}
transaction.value = null;
if (!(error instanceof FetchError)) {
if (!(error instanceof FetchError) || error.response?.status !== 403) {
isRequestFailed.value = true;
}
} finally {
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,8 @@
"notFound": {
"title": "Oops, we can’t find anything",
"description": "Sorry we can’t find anything on your search result, please try one more time via search bar below",
"prividiumTitle": "Nothing to show",
"prividiumDescription": "This page may not exist, or you might not have permissions to see it.",
"contactSupportTitle": "If you think this is a problem with us, please",
"contactSupport": "contact support"
},
Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/views/BlockView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import { isBlockNumber } from "@/utils/validators";

const { t } = useI18n();

const { setNotFoundView } = useNotFound();
const { useNotFoundView, setNotFoundView } = useNotFound();
const { getById, blockItem, isRequestPending: blockPending, isRequestFailed: blockFailed } = useBlock();

const props = defineProps({
Expand All @@ -63,6 +63,8 @@ const props = defineProps({
},
});

useNotFoundView(blockPending, blockFailed, blockItem);

const breadcrumbItems = computed((): BreadcrumbItem[] => [
{
text: t("breadcrumbs.home"),
Expand Down
9 changes: 7 additions & 2 deletions packages/app/src/views/NotFound.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<template>
<div class="not-found">
<SearchIcon class="search-icon"></SearchIcon>
<h1 class="header">{{ t("notFound.title") }}</h1>
<h1 class="header">{{ isPrividium ? t("notFound.prividiumTitle") : t("notFound.title") }}</h1>
<p class="description">
{{ t("notFound.description") }}
{{ isPrividium ? t("notFound.prividiumDescription") : t("notFound.description") }}
</p>
<SearchForm class="search-form"></SearchForm>
<div class="contact-support">
Expand All @@ -14,13 +14,18 @@
</template>

<script lang="ts" setup>
import { computed } from "vue";
import { useI18n } from "vue-i18n";

import { SearchIcon } from "@heroicons/vue/outline";

import SearchForm from "@/components/SearchForm.vue";

import useRuntimeConfig from "@/composables/useRuntimeConfig";

const { t } = useI18n();
const runtimeConfig = useRuntimeConfig();
const isPrividium = computed(() => runtimeConfig.appEnvironment === "prividium");
</script>

<style lang="scss" scoped>
Expand Down
33 changes: 31 additions & 2 deletions packages/app/tests/composables/useAddress.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { computed } from "vue";

import { afterEach, describe, expect, it, vi } from "vitest";

import { $fetch } from "ohmyfetch";
import { $fetch, FetchError } from "ohmyfetch";

import useAddress from "@/composables/useAddress";

Expand Down Expand Up @@ -44,6 +44,9 @@ vi.mock("ohmyfetch", () => {
(fetchSpy as unknown as { create: SpyInstance }).create = vi.fn(() => fetchSpy);
return {
$fetch: fetchSpy,
FetchError: function error() {
return;
},
};
});

Expand Down Expand Up @@ -118,7 +121,7 @@ describe("useAddresses", () => {
await getByAddress("0xc31f9d4cbf557b6cf0ad2af66d44c358f7fa7a1a");
expect(isRequestPending.value).toEqual(false);
});
it("sets isRequestFailed to true when request failed", async () => {
it("sets isRequestFailed to true when request fails with a non-FetchError", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mock = ($fetch as any).mockRejectedValueOnce(new Error("An error occurred"));
const { isRequestFailed, getByAddress } = useAddress();
Expand All @@ -127,6 +130,32 @@ describe("useAddresses", () => {
expect(isRequestFailed.value).toEqual(true);
mock.mockRestore();
});
it.each([403, 404])("routes FetchError status %i to the not-found view (isRequestFailed false)", async (status) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const error: any = new FetchError(String(status));
error.response = { status };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mock = ($fetch as any).mockRejectedValueOnce(error);
const { isRequestFailed, item, getByAddress } = useAddress();
await getByAddress("0xc31f9d4cbf557b6cf0ad2af66d44c358f7fa7a1a");

expect(item.value).toEqual(null);
expect(isRequestFailed.value).toEqual(false);
mock.mockRestore();
});
it.each([400, 500])("shows the error page for FetchError status %i (isRequestFailed true)", async (status) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const error: any = new FetchError(String(status));
error.response = { status };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mock = ($fetch as any).mockRejectedValueOnce(error);
const { isRequestFailed, item, getByAddress } = useAddress();
await getByAddress("0xc31f9d4cbf557b6cf0ad2af66d44c358f7fa7a1a");

expect(item.value).toEqual(null);
expect(isRequestFailed.value).toEqual(true);
mock.mockRestore();
});
});

describe("when called on contract", () => {
Expand Down
21 changes: 16 additions & 5 deletions packages/app/tests/composables/useBlock.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,10 @@ describe("useBlock:", () => {
expect(isRequestFailed.value).toEqual(true);
mock.mockRestore();
});
it("sets blockItem to null and failed to false when request fails with status code 404", async () => {
it.each([403, 404])("routes FetchError status %i to the not-found view (isRequestFailed false)", async (status) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const error: any = new FetchError("404");
error.response = {
status: 404,
};
const error: any = new FetchError(String(status));
error.response = { status };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mock = ($fetch as any).mockRejectedValue(error);
const { isRequestFailed, blockItem, getById } = useBlock();
Expand All @@ -71,4 +69,17 @@ describe("useBlock:", () => {
expect(isRequestFailed.value).toEqual(false);
mock.mockRestore();
});
it.each([400, 500])("shows the error page for FetchError status %i (isRequestFailed true)", async (status) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const error: any = new FetchError(String(status));
error.response = { status };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mock = ($fetch as any).mockRejectedValue(error);
const { isRequestFailed, blockItem, getById } = useBlock();
await getById("1234");

expect(blockItem.value).toEqual(null);
expect(isRequestFailed.value).toEqual(true);
mock.mockRestore();
});
});
26 changes: 26 additions & 0 deletions packages/app/tests/composables/useTransaction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,32 @@ describe("useTransaction:", () => {
expect(isRequestFailed.value).toEqual(false);
mock.mockRestore();
});
it("routes status code 403 to the not-found view (isRequestFailed false)", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const error: any = new FetchError("403");
error.response = { status: 403 };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mock = ($fetch as any).mockRejectedValue(error);
const { transaction, isRequestFailed, getByHash } = useTransaction();
await getByHash(hash);

expect(transaction.value).toEqual(null);
expect(isRequestFailed.value).toEqual(false);
mock.mockRestore();
});
it("shows the error page for status code 500 (isRequestFailed true)", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const error: any = new FetchError("500");
error.response = { status: 500 };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mock = ($fetch as any).mockRejectedValue(error);
const { transaction, isRequestFailed, getByHash } = useTransaction();
await getByHash(hash);

expect(transaction.value).toEqual(null);
expect(isRequestFailed.value).toEqual(true);
mock.mockRestore();
});
it("requests data successfully", async () => {
const { transaction, isRequestFailed, getByHash } = useTransaction();
await getByHash(hash);
Expand Down
5 changes: 3 additions & 2 deletions packages/app/tests/views/BlockView.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ 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(),
resolve: vi.fn(() => notFoundRoute),
replace: vi.fn(),
currentRoute: {
value: {},
value: { fullPath: "/block/1234" },
},
beforeEach: vi.fn(),
};
Expand Down
45 changes: 45 additions & 0 deletions packages/app/tests/views/NotFoundView.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createI18n } from "vue-i18n";

import { describe, expect, it, vi } from "vitest";

import { mount } from "@vue/test-utils";

import enUS from "@/locales/en.json";

import $testId from "@/plugins/testId";
import NotFound from "@/views/NotFound.vue";

// SearchForm reaches into useContext / useSearch, so stub it out for this view test.
vi.mock("@/components/SearchForm.vue", () => ({
default: { template: '<div data-testid="search-form-stub"></div>' },
}));

const runtimeConfigMock = { appEnvironment: "default" as "default" | "prividium" };
vi.mock("@/composables/useRuntimeConfig", () => ({
default: () => runtimeConfigMock,
}));

function render() {
const i18n = createI18n({
locale: "en",
allowComposition: true,
messages: { en: enUS },
});
return mount(NotFound, { global: { plugins: [i18n, $testId] } });
}

describe("NotFound view", () => {
it("renders the default copy when appEnvironment is not prividium", () => {
runtimeConfigMock.appEnvironment = "default";
const wrapper = render();
expect(wrapper.find(".header").text()).toBe(enUS.notFound.title);
expect(wrapper.find(".description").text()).toBe(enUS.notFound.description);
});

it("renders the prividium copy when appEnvironment is prividium", () => {
runtimeConfigMock.appEnvironment = "prividium";
const wrapper = render();
expect(wrapper.find(".header").text()).toBe(enUS.notFound.prividiumTitle);
expect(wrapper.find(".description").text()).toBe(enUS.notFound.prividiumDescription);
});
});
Loading