Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

web-wallet: Use @juggle/resize-observer in jsdom for tests #1971

Merged
merged 1 commit into from
Jul 15, 2024
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
8 changes: 8 additions & 0 deletions web-wallet/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions web-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"devDependencies": {
"@dusk-network/eslint-config": "3.1.0",
"@dusk-network/prettier-config": "1.1.0",
"@juggle/resize-observer": "3.4.0",
"@sveltejs/adapter-static": "3.0.2",
"@sveltejs/kit": "2.5.17",
"@testing-library/jest-dom": "6.4.6",
Expand Down
6 changes: 0 additions & 6 deletions web-wallet/src/lib/components/__tests__/AddressPicker.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@ import { addresses } from "$lib/mock-data";

import { AddressPicker } from "..";

global.ResizeObserver = vi.fn().mockImplementation(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
unobserve: vi.fn(),
}));

describe("AddressPicker", () => {
const currentAddress = addresses[0];

Expand Down
161 changes: 98 additions & 63 deletions web-wallet/src/lib/dusk/components/__tests__/Tabs.spec.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, fireEvent, render } from "@testing-library/svelte";
import { cleanup, fireEvent, render } from "@testing-library/svelte";
import { mdiHome } from "@mdi/js";
import { getAsHTMLElement } from "$lib/dusk/test-helpers";
import { Tabs } from "..";

vi.useFakeTimers();
import { Tabs } from "..";

describe("Tabs", () => {
/**
* `@juggle/resize-observer` uses this to get the dimensions of
* the observed element and, by specs, the callback won't fire
* on the `observe` call if the dimensions are both `0`.
*/
// @ts-ignore we don't need to mock the whole CSS declaration
const gcsSpy = vi.spyOn(window, "getComputedStyle").mockReturnValue({
height: "320px",
width: "320px",
});
const rafSpy = vi.spyOn(window, "requestAnimationFrame");
const cafSpy = vi.spyOn(window, "cancelAnimationFrame");
const scrollBySpy = vi.spyOn(HTMLUListElement.prototype, "scrollBy");
Expand All @@ -22,6 +31,11 @@ describe("Tabs", () => {
.spyOn(HTMLUListElement.prototype, "clientWidth", "get")
.mockReturnValue(320);

// needed by `@juggle/resize-observer`
const ulOffsetWidthSpy = vi
.spyOn(HTMLUListElement.prototype, "offsetWidth", "get")
.mockReturnValue(320);

const items = [
"Dashboard",
"User Settings",
Expand Down Expand Up @@ -58,6 +72,22 @@ describe("Tabs", () => {
target: document.body,
};

/** @param {import("svelte").ComponentProps<Tabs>} props */
const renderTabs = async (props) => {
const renderResult = render(Tabs, { ...baseOptions, props });

/**
* `@juggle/resize-observer` uses some scheduling, so we
* need to wait for the first observe to fire.
*/
await vi.waitUntil(() => rafSpy.mock.calls.length > 0);

// clearing `requestAnimationFrame` calls made by `@juggle/resize-observer`
rafSpy.mockClear();

return renderResult;
};

afterEach(() => {
cleanup();
rafSpy.mockClear();
Expand All @@ -67,10 +97,10 @@ describe("Tabs", () => {
scrollLeftSpy.mockClear();
scrollToSpy.mockClear();
scrollWidthSpy.mockClear();
ulClientWidthSpy.mockClear();
});

afterAll(() => {
gcsSpy.mockRestore();
rafSpy.mockRestore();
cafSpy.mockRestore();
scrollBySpy.mockRestore();
Expand All @@ -79,15 +109,14 @@ describe("Tabs", () => {
scrollToSpy.mockRestore();
scrollWidthSpy.mockRestore();
ulClientWidthSpy.mockRestore();
vi.useRealTimers();
ulOffsetWidthSpy.mockRestore();
});

it('should render a "Tabs" component and reset its scroll status if no tab is selected', () => {
const props = {
it('should render a "Tabs" component and reset its scroll status if no tab is selected', async () => {
const { container } = await renderTabs({
...baseProps,
selectedTab: undefined,
};
const { container } = render(Tabs, { ...baseOptions, props });
});
const tabsList = getAsHTMLElement(container, ".dusk-tabs-list");

expect(tabsList.scrollTo).toHaveBeenCalledTimes(1);
Expand All @@ -96,7 +125,7 @@ describe("Tabs", () => {
});

it("should scroll the selected tab into view if there's a selection", async () => {
const { container } = render(Tabs, baseOptions);
const { container } = await renderTabs(baseProps);
const tab = getAsHTMLElement(
container,
`[data-tabid="${baseProps.selectedTab}"]`
Expand All @@ -105,40 +134,37 @@ describe("Tabs", () => {
expect(tab.scrollIntoView).toHaveBeenCalledTimes(1);
});

it("should be able to render tabs with icon and text", () => {
const props = {
it("should be able to render tabs with icon and text", async () => {
const { container } = await renderTabs({
...baseProps,
items: itemsWithTextAndIcon,
};
const { container } = render(Tabs, { ...baseOptions, props });
});

expect(container.firstChild).toMatchSnapshot();
});

it("should be able to render tabs with icons only", () => {
const props = {
it("should be able to render tabs with icons only", async () => {
const { container } = await renderTabs({
...baseProps,
items: itemsWithIcon,
};
const { container } = render(Tabs, { ...baseOptions, props });
});

expect(container.firstChild).toMatchSnapshot();
});

it("should use the id as label if the tab hasn't one and is without icon", () => {
const props = {
it("should use the id as label if the tab hasn't one and is without icon", async () => {
const { container } = await renderTabs({
...baseProps,
items: itemsWithIdOnly,
};
const { container } = render(Tabs, { ...baseOptions, props });
});

expect(container.firstChild).toMatchSnapshot();
});

it("should observe the tab list resize on mounting and stop observing when unmounting", () => {
it("should observe the tab list resize on mounting and stop observing when unmounting", async () => {
const observeSpy = vi.spyOn(ResizeObserver.prototype, "observe");
const disconnectSpy = vi.spyOn(ResizeObserver.prototype, "disconnect");
const { container, unmount } = render(Tabs, baseOptions);
const { container, unmount } = await renderTabs(baseProps);
const tabsList = container.querySelector(".dusk-tabs-list");

expect(observeSpy).toHaveBeenCalledTimes(1);
Expand All @@ -152,19 +178,18 @@ describe("Tabs", () => {
disconnectSpy.mockRestore();
});

it("should pass additional class names and attributes to the root element", () => {
const props = {
it("should pass additional class names and attributes to the root element", async () => {
const { container } = await renderTabs({
...baseProps,
className: "foo bar",
id: "some-id",
};
const { container } = render(Tabs, { ...baseOptions, props });
});

expect(container.firstChild).toMatchSnapshot();
});

it("should fire a change event when a tab is selected and it's not the current selection", async () => {
const { component, getAllByRole } = render(Tabs, baseOptions);
const { component, getAllByRole } = await renderTabs(baseProps);
const tabs = getAllByRole("tab");

let expectedTab = tabs[0];
Expand Down Expand Up @@ -193,7 +218,7 @@ describe("Tabs", () => {
});

it("should scroll a tab into view when it gains focus", async () => {
const { getAllByRole } = render(Tabs, baseOptions);
const { getAllByRole } = await renderTabs(baseProps);
const tabs = getAllByRole("tab");

scrollIntoViewSpy.mockClear();
Expand All @@ -203,10 +228,10 @@ describe("Tabs", () => {
expect(tabs[0].scrollIntoView).toHaveBeenCalledTimes(1);
});

it("should hide and disable the scroll buttons if there is enough horizontal space", () => {
it("should hide and disable the scroll buttons if there is enough horizontal space", async () => {
scrollWidthSpy.mockReturnValueOnce(0);

const { container } = render(Tabs, baseOptions);
const { container } = await renderTabs(baseProps);
const leftBtn = getAsHTMLElement(
container,
".dusk-tab-scroll-button:first-of-type"
Expand All @@ -223,25 +248,7 @@ describe("Tabs", () => {
});

it("should show the scroll buttons when there isn't enough horizontal space and enable the appropriate ones", async () => {
const originalObserver = ResizeObserver;

let callback;

/**
* We don't have a proper mock for the observer right now,
* so we use the proxy to memorize the callback received by the
* observer's constructor.
* This way we can call it at will, simulating updates.
*/
global.ResizeObserver = new Proxy(originalObserver, {
construct(Target, args) {
callback = args[0];

return new Target(args[0]);
},
});

const { container } = render(Tabs, baseOptions);
const { container } = await renderTabs(baseProps);
const tabsList = getAsHTMLElement(container, ".dusk-tabs-list");

let leftBtn = getAsHTMLElement(
Expand All @@ -267,20 +274,13 @@ describe("Tabs", () => {
scrollBySpy.mockClear();
rafSpy.mockClear();

vi.advanceTimersToNextTimer();

expect(rafSpy).toHaveBeenCalledTimes(1);
expect(tabsList.scrollBy).toHaveBeenCalledTimes(1);
expect(tabsList.scrollBy).toHaveBeenCalledWith(5, 0);

await fireEvent.mouseUp(rightBtn);

expect(cafSpy).toHaveBeenCalledTimes(1);

scrollLeftSpy.mockReturnValue(320);
scrollLeftSpy.mockReturnValueOnce(320);

// we don't care for callback parameters right now
await act(callback);
await fireEvent.scroll(tabsList);

leftBtn = getAsHTMLElement(
container,
Expand All @@ -305,12 +305,47 @@ describe("Tabs", () => {
expect(tabsList.scrollBy).toHaveBeenCalledTimes(1);
expect(tabsList.scrollBy).toHaveBeenCalledWith(-5, 0);

global.ResizeObserver = originalObserver;
await fireEvent.mouseUp(rightBtn);
});

it("should ignore mouse down events if the primary button isn't the only one pressed", async () => {
it("should keep scrolling while the scroll button is pressed", async () => {
vi.useFakeTimers();

const { container } = render(Tabs, baseOptions);
const tabsList = getAsHTMLElement(container, ".dusk-tabs-list");
const rightBtn = getAsHTMLElement(
container,
".dusk-tab-scroll-button:last-of-type"
);

await vi.advanceTimersToNextTimerAsync();

expect(rightBtn.getAttribute("hidden")).toBe("false");
expect(rightBtn.getAttribute("disabled")).toBeNull();

await fireEvent.mouseDown(rightBtn, { buttons: 1 });

const n = 10;

for (let i = 0; i < n - 1; i++) {
await vi.advanceTimersToNextTimerAsync();
}

expect(tabsList.scrollBy).toHaveBeenCalledTimes(n);

for (let i = 1; i <= n; i++) {
expect(tabsList.scrollBy).toHaveBeenNthCalledWith(n, 5, 0);
}

await fireEvent.mouseUp(rightBtn);

vi.runAllTimers();
vi.useRealTimers();
});

it("should ignore mouse down events if the primary button isn't the only one pressed", async () => {
const { container } = await renderTabs(baseProps);
const tabsList = getAsHTMLElement(container, ".dusk-tabs-list");
const leftBtn = getAsHTMLElement(
container,
".dusk-tab-scroll-button:first-of-type"
Expand All @@ -333,7 +368,7 @@ describe("Tabs", () => {
});

it("should bring the nearest tab into view on mouse clicks on scroll buttons", async () => {
const { container } = render(Tabs, baseOptions);
const { container } = await renderTabs(baseProps);
const tabsList = getAsHTMLElement(container, ".dusk-tabs-list");
const leftBtn = getAsHTMLElement(
container,
Expand Down
16 changes: 0 additions & 16 deletions web-wallet/src/lib/dusk/mocks/ResizeObserver.js

This file was deleted.

1 change: 0 additions & 1 deletion web-wallet/src/lib/dusk/mocks/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { default as IntersectionObserver } from "./IntersectionObserver";
export { default as ResizeObserver } from "./ResizeObserver";
6 changes: 0 additions & 6 deletions web-wallet/src/routes/(app)/dashboard/__tests__/page.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@ import { createCurrencyFormatter } from "$lib/dusk/currency";
import Dashboard from "../+page.svelte";
import { walletStore } from "$lib/stores";

global.ResizeObserver = vi.fn().mockImplementation(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
unobserve: vi.fn(),
}));

vi.mock("$lib/stores", async (importOriginal) => {
/** @type {WalletStore} */
const original = await importOriginal();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, render } from "@testing-library/svelte";
import Transactions from "../+page.svelte";

global.ResizeObserver = vi.fn().mockImplementation(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
unobserve: vi.fn(),
}));

vi.useFakeTimers();

describe("Dashboard", () => {
Expand Down
Loading
Loading