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`] = ` -
-
-

- Blocks — 0 Displayed Items -

- - -
- - -
`; diff --git a/explorer/src/lib/components/blocks-card/BlocksCard.svelte b/explorer/src/lib/components/blocks-card/BlocksCard.svelte index 1cbabab4cc..b493ec1721 100644 --- a/explorer/src/lib/components/blocks-card/BlocksCard.svelte +++ b/explorer/src/lib/components/blocks-card/BlocksCard.svelte @@ -27,6 +27,7 @@ $: displayedBlocks = blocks ? blocks.slice(0, itemsToDisplay) : []; $: isLoadMoreDisabled = (blocks && itemsToDisplay >= blocks.length) || (loading && blocks === null); + $: ({ isSmallScreen } = $appStore); const loadMoreItems = () => { if (blocks && itemsToDisplay < blocks.length) { @@ -47,16 +48,13 @@ label: "Show More", }} > - {#if $appStore.isSmallScreen} + {#if isSmallScreen}
{#each displayedBlocks as block (block)} {/each}
{:else} - + {/if} diff --git a/explorer/src/lib/components/latest-blocks-card/LatestBlocksCard.svelte b/explorer/src/lib/components/latest-blocks-card/LatestBlocksCard.svelte index 99254f63bf..20d3099000 100644 --- a/explorer/src/lib/components/latest-blocks-card/LatestBlocksCard.svelte +++ b/explorer/src/lib/components/latest-blocks-card/LatestBlocksCard.svelte @@ -17,8 +17,8 @@ /** @type {Boolean} */ export let loading; - /** @type {AppStore} */ - export let appStore; + /** @type {boolean} */ + export let isSmallScreen; $: classes = makeClassName(["latest-blocks-card", className]); @@ -36,11 +36,11 @@ label: "All Blocks", }} > - {#if $appStore.isSmallScreen} + {#if isSmallScreen} {#each blocks as block (block)} {/each} {:else} - + {/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 +});