diff --git a/explorer/src/lib/components/__tests__/BlocksCard.spec.js b/explorer/src/lib/components/__tests__/BlocksCard.spec.js
index 7c752b643f..fcba536e1c 100644
--- a/explorer/src/lib/components/__tests__/BlocksCard.spec.js
+++ b/explorer/src/lib/components/__tests__/BlocksCard.spec.js
@@ -15,10 +15,10 @@ describe("Blocks Card", () => {
const data = getTenBlocks(gqlBlocks.blocks);
const baseProps = {
+ appStore: appStore,
blocks: null,
error: null,
loading: false,
- appStore: appStore
};
const baseOptions = {
props: baseProps,
diff --git a/explorer/src/lib/components/__tests__/TransactionsCard.spec.js b/explorer/src/lib/components/__tests__/TransactionsCard.spec.js
index 56fcdcf580..4abfad8c48 100644
--- a/explorer/src/lib/components/__tests__/TransactionsCard.spec.js
+++ b/explorer/src/lib/components/__tests__/TransactionsCard.spec.js
@@ -16,10 +16,10 @@ describe("Transactions Card", () => {
const data = getTenTransactions(gqlTransactions.transactions);
const baseProps = {
+ appStore: appStore,
error: null,
loading: false,
txns: null,
- appStore: appStore
};
const baseOptions = {
props: baseProps,
diff --git a/explorer/src/lib/components/__tests__/__snapshots__/BlocksCard.spec.js.snap b/explorer/src/lib/components/__tests__/__snapshots__/BlocksCard.spec.js.snap
index b730965b42..df4f9f7f4e 100644
--- a/explorer/src/lib/components/__tests__/__snapshots__/BlocksCard.spec.js.snap
+++ b/explorer/src/lib/components/__tests__/__snapshots__/BlocksCard.spec.js.snap
@@ -745,36 +745,5 @@ exports[`Blocks Card > should render the \`BlocksCard\` component 1`] = `
-
-`;
-
-exports[`Blocks Card > should render the \`BlocksCard\` component with the mobile layout 1`] = `
-
+
{/if}
diff --git a/explorer/src/lib/components/latest-transactions-card/LatestTransactionsCard.svelte b/explorer/src/lib/components/latest-transactions-card/LatestTransactionsCard.svelte
index 0e67230975..108b7c4ca1 100644
--- a/explorer/src/lib/components/latest-transactions-card/LatestTransactionsCard.svelte
+++ b/explorer/src/lib/components/latest-transactions-card/LatestTransactionsCard.svelte
@@ -27,8 +27,8 @@
/** @type {Boolean} */
export let displayTooltips = false;
- /** @type {AppStore} */
- export let appStore;
+ /** @type {boolean} */
+ export let isSmallScreen;
$: classes = makeClassName(["latest-transactions-card", className]);
@@ -48,7 +48,7 @@
}
: undefined}
>
- {#if $appStore.isSmallScreen}
+ {#if isSmallScreen}
{#each txns as txn (txn)}
{/each}
{:else}
-
+
{/if}
diff --git a/explorer/src/lib/components/transactions-card/TransactionsCard.svelte b/explorer/src/lib/components/transactions-card/TransactionsCard.svelte
index b60e6f7556..d38bbad2a1 100644
--- a/explorer/src/lib/components/transactions-card/TransactionsCard.svelte
+++ b/explorer/src/lib/components/transactions-card/TransactionsCard.svelte
@@ -31,6 +31,7 @@
$: displayedTxns = txns ? txns.slice(0, itemsToDisplay) : [];
$: isLoadMoreDisabled =
(txns && itemsToDisplay >= txns.length) || (loading && txns === null);
+ $: ({ isSmallScreen } = $appStore);
const loadMoreItems = () => {
if (txns && itemsToDisplay < txns.length) {
@@ -51,7 +52,7 @@
label: "Show More",
}}
>
- {#if $appStore.isSmallScreen}
+ {#if isSmallScreen}
{#each displayedTxns as txn (txn)}
diff --git a/explorer/src/lib/dusk/mocks/MediaQueryList.js b/explorer/src/lib/dusk/mocks/MediaQueryList.js
index 890a741bf3..28fca92a1a 100644
--- a/explorer/src/lib/dusk/mocks/MediaQueryList.js
+++ b/explorer/src/lib/dusk/mocks/MediaQueryList.js
@@ -1,38 +1,68 @@
-export default class MediaQueryList {
- /**
- * @param {String} query
- */
- constructor(query) {
- this.matches = false;
- this.media = query;
- this.listeners = [];
+import { afterAll } from "vitest";
+
+const controllers = new Set();
+
+afterAll(() => {
+ controllers.forEach((controller) => {
+ controller.abort();
+ controllers.delete(controller);
+ });
+});
+
+/**
+ * Mocks the `MediaQueryList` object and listens to the
+ * "DuskMediaQueryMatchesChange" custom event.
+ * Fire one manually or with the `changeMediaQueryMatches`
+ * helper function to simulate media query changes.
+ */
+export default class MediaQueryList extends EventTarget {
+ #matches;
+
+ #media;
+
+ /**
+ * @param {string} mediaQuery
+ * @param {boolean} initialMatches
+ */
+ constructor(mediaQuery, initialMatches) {
+ super();
+
+ this.#matches = initialMatches;
+ this.#media = mediaQuery;
+
+ const abortController = new AbortController();
+
+ controllers.add(abortController);
+
+ global.addEventListener("DuskMediaQueryMatchesChange", this, {
+ signal: abortController.signal,
+ });
+ }
+
+ get matches() {
+ return this.#matches;
+ }
+
+ get media() {
+ return this.#media;
+ }
+
+ /** @param {CustomEvent<{ media: string, matches: boolean }>} evt */
+ handleEvent(evt) {
+ const { detail, type } = evt;
+
+ if (
+ type === "DuskMediaQueryMatchesChange" &&
+ detail.media === this.#media
+ ) {
+ this.#matches = detail.matches;
+
+ this.dispatchEvent(
+ new MediaQueryListEvent("change", {
+ matches: this.#matches,
+ media: this.#media,
+ })
+ );
}
-
- /**
- * @param {String} event
- * @param {Function} callback
- */
- addEventListener(event, callback) {
- if (event === 'change') {
- this.listeners.push(callback);
- }
- }
-
- /**
- * @param {String} event
- * @param {Function} callback
- */
- removeEventListener(event, callback) {
- if (event === 'change') {
- this.listeners = this.listeners.filter(listener => listener !== callback);
- }
- }
-
- /**
- * @param {Boolean} matches
- */
- change(matches) {
- this.matches = matches;
- this.listeners.forEach(listener => listener({ matches }));
- }
- }
\ No newline at end of file
+ }
+}
diff --git a/explorer/src/lib/dusk/mocks/MediaQueryListEvent.js b/explorer/src/lib/dusk/mocks/MediaQueryListEvent.js
new file mode 100644
index 0000000000..6f076c1137
--- /dev/null
+++ b/explorer/src/lib/dusk/mocks/MediaQueryListEvent.js
@@ -0,0 +1,26 @@
+import { pickIn } from "lamb";
+
+export default class MediaQueryListEvent extends Event {
+ #matches;
+
+ #media;
+
+ /**
+ * @param {string} type
+ * @param {MediaQueryListEventInit} options
+ */
+ constructor(type, options) {
+ super(type, pickIn(options, ["bubbles", "cancelable", "composed"]));
+
+ this.#matches = options.matches;
+ this.#media = options.media;
+ }
+
+ get matches() {
+ return this.#matches;
+ }
+
+ get media() {
+ return this.#media;
+ }
+}
diff --git a/explorer/src/lib/dusk/mocks/index.js b/explorer/src/lib/dusk/mocks/index.js
index cf6664d7c2..f31648fc8d 100644
--- a/explorer/src/lib/dusk/mocks/index.js
+++ b/explorer/src/lib/dusk/mocks/index.js
@@ -1,2 +1,3 @@
export { default as IntersectionObserver } from "./IntersectionObserver";
export { default as MediaQueryList } from "./MediaQueryList";
+export { default as MediaQueryListEvent } from "./MediaQueryListEvent";
diff --git a/explorer/src/lib/dusk/test-helpers/__tests__/changeMediaQueryMatches.spec.js b/explorer/src/lib/dusk/test-helpers/__tests__/changeMediaQueryMatches.spec.js
new file mode 100644
index 0000000000..1c0e6a3aaa
--- /dev/null
+++ b/explorer/src/lib/dusk/test-helpers/__tests__/changeMediaQueryMatches.spec.js
@@ -0,0 +1,27 @@
+import { describe, expect, it } from "vitest";
+
+import { changeMediaQueryMatches } from "..";
+
+describe("changeMediaQueryMatches", () => {
+ it('should dispatch "DuskMediaQueryMatchesChange" custom events', () => {
+ const media = "(max-width: 1024px)";
+ const matches = true;
+
+ /** @param {Event} evt */
+ const handler = (evt) => {
+ expect(evt).toBeInstanceOf(CustomEvent);
+ expect(evt.type).toBe("DuskMediaQueryMatchesChange");
+
+ // @ts-ignore see https://github.com/Microsoft/TypeScript/issues/28357
+ expect(evt.detail).toStrictEqual({ matches, media });
+ };
+
+ global.addEventListener("DuskMediaQueryMatchesChange", handler);
+
+ changeMediaQueryMatches(media, matches);
+
+ global.removeEventListener("DuskMediaQueryMatchesChange", handler);
+
+ expect.assertions(3);
+ });
+});
diff --git a/explorer/src/lib/dusk/test-helpers/changeMediaQueryMatches.js b/explorer/src/lib/dusk/test-helpers/changeMediaQueryMatches.js
new file mode 100644
index 0000000000..ec0b6e16ad
--- /dev/null
+++ b/explorer/src/lib/dusk/test-helpers/changeMediaQueryMatches.js
@@ -0,0 +1,14 @@
+/**
+ * Helper to fire "DuskMediaQueryMatchesChange" custom
+ * events that are listened by our `MediaQueryList` mock.
+ *
+ * @param {string} media
+ * @param {boolean} matches
+ */
+export default function changeMediaQueryMatches(media, matches) {
+ dispatchEvent(
+ new CustomEvent("DuskMediaQueryMatchesChange", {
+ detail: { matches, media },
+ })
+ );
+}
diff --git a/explorer/src/lib/dusk/test-helpers/index.js b/explorer/src/lib/dusk/test-helpers/index.js
index 307c8f5ed6..1a8924b35f 100644
--- a/explorer/src/lib/dusk/test-helpers/index.js
+++ b/explorer/src/lib/dusk/test-helpers/index.js
@@ -1,3 +1,4 @@
+export { default as changeMediaQueryMatches } from "./changeMediaQueryMatches";
export { default as mockReadableStore } from "./mockReadableStore";
export { default as renderWithSimpleContent } from "./renderWithSimpleContent";
export { default as renderWithSlots } from "./renderWithSlots";
diff --git a/explorer/src/lib/stores/__tests__/appStore.spec.js b/explorer/src/lib/stores/__tests__/appStore.spec.js
index 4abc951909..1314b714d7 100644
--- a/explorer/src/lib/stores/__tests__/appStore.spec.js
+++ b/explorer/src/lib/stores/__tests__/appStore.spec.js
@@ -1,6 +1,7 @@
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import { get } from "svelte/store";
-import appStore from "../appStore";
+
+import { changeMediaQueryMatches } from "$lib/dusk/test-helpers";
describe("appStore", () => {
const originalTouchStart = window.ontouchstart;
@@ -40,8 +41,8 @@ describe("appStore", () => {
chainInfoEntries: Number(env.VITE_CHAIN_INFO_ENTRIES),
darkMode: false,
fetchInterval: Number(env.VITE_REFETCH_INTERVAL),
- isSmallScreen: false,
hasTouchSupport: false,
+ isSmallScreen: false,
marketDataFetchInterval: Number(env.VITE_MARKET_DATA_REFETCH_INTERVAL),
network: expectedNetworks[0].value,
networks: expectedNetworks,
@@ -104,27 +105,31 @@ describe("appStore", () => {
expect(get(appStore).darkMode).toBe(true);
});
- it.only("should update the `isSmallScreen` property when the window width changes respective to the provided media query", async () => {
- let changeCallback;
+ it("should set the `isSmallScreen` property to `false` when the related media query doesn't match", async () => {
+ const { appStore } = await import("..");
- const mqAddListenerSpy = vi.spyOn(MediaQueryList.prototype, "addEventListener").mockImplementation((eventName, callback) => {
- if (eventName === "change") {
- changeCallback = callback;
- }
- });
+ expect(get(appStore).isSmallScreen).toBe(false);
+ });
-
+ it("should set the `isSmallScreen` property to `true` when the related media query matches", async () => {
+ const mqMatchesSpy = vi
+ .spyOn(MediaQueryList.prototype, "matches", "get")
+ .mockReturnValue(true);
- Object.defineProperty(window, 'innerWidth', {
- writable: true,
- configurable: true,
- value: 150,
- });
+ const { appStore } = await import("..");
+
+ expect(get(appStore).isSmallScreen).toBe(true);
+
+ mqMatchesSpy.mockRestore();
+ });
+
+ it("should update the `isSmallScreen` property when the media query match changes", async () => {
+ const { appStore } = await import("..");
- window.dispatchEvent(new Event('resize'));
+ expect(get(appStore).isSmallScreen).toBe(false);
- expect(mqAddListenerSpy).toHaveBeenCalledOnce();
+ changeMediaQueryMatches("(max-width: 1024px)", true);
- mqAddListenerSpy.mockRestore();
+ expect(get(appStore).isSmallScreen).toBe(true);
});
});
diff --git a/explorer/src/lib/stores/appStore.js b/explorer/src/lib/stores/appStore.js
index d2a5aa2309..5acb8f1f8c 100644
--- a/explorer/src/lib/stores/appStore.js
+++ b/explorer/src/lib/stores/appStore.js
@@ -6,7 +6,7 @@ const networks = [
{ label: "Testnet", value: import.meta.env.VITE_DUSK_TESTNET_NODE },
];
-const mql = window.matchMedia("(max-width: 1024px)");
+const maxWidthMediaQuery = window.matchMedia("(max-width: 1024px)");
const browserDefaults = browser
? {
@@ -23,7 +23,7 @@ const initialState = {
chainInfoEntries: Number(import.meta.env.VITE_CHAIN_INFO_ENTRIES),
fetchInterval: Number(import.meta.env.VITE_REFETCH_INTERVAL) || 1000,
hasTouchSupport: "ontouchstart" in window || navigator.maxTouchPoints > 0,
- isSmallScreen: mql.matches,
+ isSmallScreen: maxWidthMediaQuery.matches,
marketDataFetchInterval:
Number(import.meta.env.VITE_MARKET_DATA_REFETCH_INTERVAL) || 120000,
network: networks[0].value,
@@ -38,18 +38,11 @@ const initialState = {
const store = writable(initialState);
const { set, subscribe } = store;
-mql.addEventListener("change", (event) => {
- if(event.matches){
- set({
- ...get(store),
- isSmallScreen: true,
- });
- } else {
- set({
- ...get(store),
- isSmallScreen: false,
- });
- }
+maxWidthMediaQuery.addEventListener("change", (event) => {
+ set({
+ ...get(store),
+ isSmallScreen: event.matches,
+ });
});
/** @param {string} network */
diff --git a/explorer/src/routes/+page.svelte b/explorer/src/routes/+page.svelte
index de495dc968..46f2e489a1 100644
--- a/explorer/src/routes/+page.svelte
+++ b/explorer/src/routes/+page.svelte
@@ -21,7 +21,7 @@
onDestroy(pollingDataStore.stop);
$: ({ data, error, isLoading } = $pollingDataStore);
- $: ({ chainInfoEntries, network } = $appStore);
+ $: ({ chainInfoEntries, isSmallScreen, network } = $appStore);
const retry = () => {
pollingDataStore.start(network, chainInfoEntries);
@@ -38,8 +38,8 @@
className="tables-layout"
blocks={data?.blocks}
{error}
+ {isSmallScreen}
loading={isLoading}
- {appStore}
/>
diff --git a/explorer/src/routes/blocks/block/+page.svelte b/explorer/src/routes/blocks/block/+page.svelte
index 7423dc2169..218b84820a 100644
--- a/explorer/src/routes/blocks/block/+page.svelte
+++ b/explorer/src/routes/blocks/block/+page.svelte
@@ -30,6 +30,7 @@
$navigating.complete.then(updateData);
}
+ $: ({ isSmallScreen } = $appStore);
$: ({ data, error, isLoading } = $dataStore);
@@ -45,8 +46,8 @@
{error}
loading={isLoading}
isOnHomeScreen={false}
+ {isSmallScreen}
displayTooltips={true}
- {appStore}
/>
diff --git a/explorer/vite-setup.js b/explorer/vite-setup.js
index 1d1ac682f4..9baada8813 100644
--- a/explorer/vite-setup.js
+++ b/explorer/vite-setup.js
@@ -8,7 +8,11 @@ import { readable } from "svelte/store";
import { ResizeObserver } from "@juggle/resize-observer";
import "jsdom-worker";
-import { IntersectionObserver, MediaQueryList } from "./src/lib/dusk/mocks";
+import {
+ IntersectionObserver,
+ MediaQueryList,
+ MediaQueryListEvent,
+} from "./src/lib/dusk/mocks";
/*
* Mocking deprecated `atob` and `btoa` functions in Node.
@@ -24,10 +28,10 @@ vi.spyOn(global, "btoa").mockImplementation((data) =>
// Adding missing bits in JSDOM
vi.mock("./src/lib/dusk/mocks/IntersectionObserver");
-vi.mock("./src/lib/dusk/mocks/MediaQueryList");
global.IntersectionObserver = IntersectionObserver;
global.ResizeObserver = ResizeObserver;
+global.MediaQueryListEvent = MediaQueryListEvent;
global.MediaQueryList = MediaQueryList;
const elementMethods = ["scrollBy", "scrollTo", "scrollIntoView"];
@@ -121,7 +125,7 @@ vi.mock("$app/stores", () => {
});
// Define matchMedia property
-Object.defineProperty(window, 'matchMedia', {
+Object.defineProperty(window, "matchMedia", {
+ value: (query) => new MediaQueryList(query, false),
writable: true,
- value: query => new MediaQueryList(query),
-});
\ No newline at end of file
+});