-
+
+
);
- const loading = app.container.querySelector('[aria-busy="true"]');
- const skeleton = app.container.querySelector(".react-loading-skeleton");
+ const skeleton = app.container.querySelector(".react-loading-skeleton")!;
- expect(loading).not.toBeNull();
- expect(skeleton).not.toBeNull();
- expect(loading!.getBoundingClientRect().height).toBeGreaterThanOrEqual(320);
- expect(skeleton!.getBoundingClientRect().height).toBe(320);
- expect(skeleton!.getBoundingClientRect().width).toBe(360);
+ expect(skeleton.getBoundingClientRect().height).toBe(16);
+ expect(skeleton.getBoundingClientRect().width).toBe(240);
+
+ await app.rerender(
+
+
+
+ );
+
+ expect(skeleton.getBoundingClientRect().height).toBe(24);
+ expect(skeleton.getBoundingClientRect().width).toBe(180);
+});
+
+it("fills panel and avatar slots without adding a text line below them", async () => {
+ const app = await render(
+
+ );
+ const [panel, avatar] = app.container.querySelectorAll(
+ ".react-loading-skeleton"
+ );
+
+ expect(panel!.getBoundingClientRect().width).toBe(240);
+ expect(panel!.getBoundingClientRect().height).toBe(80);
+ expect(avatar!.getBoundingClientRect().width).toBe(40);
+ expect(avatar!.getBoundingClientRect().height).toBe(40);
+ expect(panel!.parentElement!.getBoundingClientRect().height).toBe(80);
+
+ await app.rerender(
+
+ );
+
+ expect(panel!.getBoundingClientRect().width).toBe(180);
+ expect(panel!.getBoundingClientRect().height).toBe(120);
+ expect(avatar!.getBoundingClientRect().width).toBe(56);
+ expect(avatar!.getBoundingClientRect().height).toBe(56);
+});
+
+it("keeps known metric and detail row geometry when values arrive", async () => {
+ const theme = assignInlineVars(vars, {
+ ...lightTheme,
+ font: { body: "Georgia, serif" },
+ fontSize: { ...lightTheme.fontSize, md: "20px", lg: "24px" },
+ });
+ const Presentation = ({ loading }: { loading: boolean }) => (
+
+ );
+ const app = await render(
);
+ const metric = app.getByTestId("metric").element();
+ const detail = app.getByTestId("detail").element();
+ const metricHeight = metric.getBoundingClientRect().height;
+ const detailHeight = detail.getBoundingClientRect().height;
+
+ await expect.element(app.getByText("Debt")).toBeVisible();
+ await expect.element(app.getByText("Network")).toBeVisible();
+
+ await app.rerender(
);
+
+ await expect.element(app.getByText("$100")).toBeVisible();
+ await expect.element(app.getByText("Ethereum")).toBeVisible();
+ expect(metric.getBoundingClientRect().height).toBeCloseTo(metricHeight, 0);
+ expect(detail.getBoundingClientRect().height).toBeCloseTo(detailHeight, 0);
+});
+it("renders single loaders for amount, carets, and right balance section", async () => {
+ const theme = assignInlineVars(vars, lightTheme);
+ const app = await render(
+
+ );
+
+ // Amount input does not show "0", renders a skeleton loader line instead
+ expect(app.container.textContent).not.toContain("0");
+ expect(app.container.querySelector('input[name="stake-amount"]')).toBeNull();
+
+ // Caret renders a 12x12 skeleton loader
+ const caretSkeleton = app.container.querySelector(
+ '[style*="width: 12px"] .react-loading-skeleton'
+ );
+ expect(caretSkeleton).not.toBeNull();
+ expect(caretSkeleton!.getBoundingClientRect().width).toBe(12);
+ expect(caretSkeleton!.getBoundingClientRect().height).toBe(12);
+
+ // Balance row contains price loader on the left and a single loader line on the right (no Max button)
+ const balanceRow = app.container.querySelector(
+ '[data-rk="stake-token-section-balance"]'
+ )!;
+ expect(balanceRow).not.toBeNull();
+ const rightSection = balanceRow.children[1]!;
+ expect(rightSection.querySelectorAll(".react-loading-skeleton")).toHaveLength(
+ 1
+ );
+ expect(
+ balanceRow.querySelector('[data-rk="stake-token-section-max-button"]')
+ ).toBeNull();
});
diff --git a/packages/widget/tests/features/activity-workflow.dom.test.tsx b/packages/widget/tests/features/activity-workflow.dom.test.tsx
index 2353e9add..16607f697 100644
--- a/packages/widget/tests/features/activity-workflow.dom.test.tsx
+++ b/packages/widget/tests/features/activity-workflow.dom.test.tsx
@@ -16,15 +16,6 @@ import { render } from "../utils/test-utils.dom.tsx";
const i18nInstance = createWidgetI18nInstance();
-vi.mock(
- "../../src/features/activity/ui/activity-page/components/action-list-item",
- () => ({
- ActionListItem: ({ action }: { readonly action: ActivityActionItem }) => (
-
{action.actionData.id}
- ),
- })
-);
-
const settings = {
apiKey: "test-api-key",
variant: "default" as const,
diff --git a/packages/widget/tests/use-cases/classic-mount-geometry.browser.test.tsx b/packages/widget/tests/use-cases/classic-mount-geometry.browser.test.tsx
index 3d917e3a1..e3d51860a 100644
--- a/packages/widget/tests/use-cases/classic-mount-geometry.browser.test.tsx
+++ b/packages/widget/tests/use-cases/classic-mount-geometry.browser.test.tsx
@@ -1,6 +1,7 @@
import { MotionGlobalConfig } from "motion/react";
import { delay, HttpResponse, http } from "msw";
import { afterEach } from "vitest";
+import { userEvent } from "vitest/browser";
import { yieldApiYieldDtoFixture } from "../fixtures";
import { yieldApiRoute } from "../mocks/api-routes";
import { describe, expect, it } from "../utils/test-extend";
@@ -189,4 +190,53 @@ describe("classic mount geometry", () => {
await expect.poll(() => defaultApp.container.textContent).toContain("Earn");
await defaultApp.unmount();
});
+
+ it("keeps the amount layout stable with a loader until the catalog is ready", async ({
+ worker,
+ }) => {
+ const catalog = Promise.withResolvers
();
+ const readyYield = yieldApiYieldDtoFixture();
+ worker.use(
+ http.get(yieldApiRoute("/v1/yields"), async () => {
+ await catalog.promise;
+ return HttpResponse.json({
+ items: [readyYield],
+ total: 1,
+ limit: 20,
+ offset: 0,
+ });
+ }),
+ http.get(yieldApiRoute(`/v1/yields/${readyYield.id}`), () =>
+ HttpResponse.json(readyYield)
+ )
+ );
+ const app = await renderApp({
+ skProps: {
+ apiKey: import.meta.env.VITE_API_KEY,
+ disableInitLayoutAnimation: true,
+ language: "fr",
+ theme: {
+ font: { body: "Georgia, serif" },
+ fontSize: { md: "20px" },
+ },
+ },
+ });
+
+ try {
+ const section = app.getByTestId("stake-token-section");
+ await expect.element(section).toBeVisible();
+ await expect.element(app.getByRole("textbox")).not.toBeInTheDocument();
+
+ catalog.resolve();
+
+ const input = app.getByRole("textbox");
+ await expect.element(input).toBeVisible();
+ await expect.element(input).toBeEnabled();
+ await userEvent.fill(input, "1");
+ await expect.element(input).toHaveValue("1");
+ } finally {
+ catalog.resolve();
+ await app.unmount();
+ }
+ });
});