Skip to main content

Layout and Testing

Understanding how widgets adapt to different screen sizes and ensuring they are accessible and well-tested is crucial for building robust, production-ready widgets. This guide covers responsive design patterns, comprehensive testing strategies, and accessibility best practices.

Table of Contents

  1. Layout and Responsive Design
  2. Testing and Accessibility

Layout and Responsive Design

Reference: Layout Calculation

Layout Props

Widgets receive layout information via props:

interface IWidgetProps {
layout?: {
w: number; // Width in grid units
h: number; // Height in grid units
x: number; // X position in grid
y: number; // Y position in grid
};
}

Layout Constraints

Defined in widget-config.json:

{
"layout": {
"minW": 5,
"maxW": 24,
"minH": 5,
"maxH": 12
}
}

React Grid Layout Integration

The Dashboard Composer (Host application) uses a responsive grid system based on viewport breakpoints. The number of grid columns changes based on the current breakpoint.

Grid Configuration:

const rgl = {
cols: {
lg: 36, // Large screens
md: 24, // Medium screens
sm: 12, // Small screens
xs: 6, // Extra small screens
},
};

Viewport Breakpoints:

const VIEWPORT_BREAKPOINTS = {
EXTRA_EXTRA_SMALL: 0,
EXTRA_SMALL: 320,
SMALL: 768,
MEDIUM: 1220,
LARGE: 1920,
};

Breakpoint Examples:

  • Resolution 321px - 767px: Grid has 6 columns (xs)
  • Resolution 768px - 1219px: Grid has 12 columns (sm)
  • Resolution 1220px - 1919px: Grid has 24 columns (md)
  • Resolution 1920px+: Grid has 36 columns (lg)

Dashboard Composer Breakpoint Controls

The Dashboard Composer provides 4 predefined breakpoints for users to switch between during dashboard configuration. When switching to a breakpoint different from the current viewport, the dashboard width is set to a specific value:

  • XS: 390px
  • SM: 1024px
  • MD: 1480px
  • LG: 1921px

This allows users to preview and configure their dashboards for different screen sizes.

Responsive Widget Example

import React from "react";
import styled from "styled-components";
import { getSizeBy } from "@invent/wl-ui-kit";
import type { IWidgetProps } from "../types/widget-props";

const VIEWPORT_BREAKPOINTS = {
EXTRA_EXTRA_SMALL: 0,
EXTRA_SMALL: 320,
SMALL: 768,
MEDIUM: 1220,
LARGE: 1920,
};

const ResponsiveKpiTracker: React.FC<IWidgetProps> = ({ layout }) => {
const isSmall = layout && layout.w < 12;
const isMedium = layout && layout.w >= 12 && layout.w < 24;
const isLarge = layout && layout.w >= 24;

return (
<ResponsiveContainer $isSmall={isSmall}>
<KpiGrid $columns={isSmall ? 1 : isMedium ? 2 : 3}>
<KpiCard>Revenue</KpiCard>
<KpiCard>Users</KpiCard>
<KpiCard>Conversion</KpiCard>
</KpiGrid>
</ResponsiveContainer>
);
};

const ResponsiveContainer = styled.div<{ $isSmall?: boolean }>`
padding: ${(props) => (props.$isSmall ? getSizeBy(1) : getSizeBy(3))};

@media screen and (max-width: ${VIEWPORT_BREAKPOINTS.SMALL}px) {
padding: ${getSizeBy(2)};
}
`;

const KpiGrid = styled.div<{ $columns: number }>`
display: grid;
grid-template-columns: repeat(${(props) => props.$columns}, 1fr);
gap: ${getSizeBy(2)};

@media screen and (max-width: ${VIEWPORT_BREAKPOINTS.SMALL}px) {
grid-template-columns: 1fr;
gap: ${getSizeBy(1)};
}
`;

const KpiCard = styled.div`
padding: ${getSizeBy(2)};
background-color: ${getColor("tertiary")};
border-radius: ${(props) => props.theme.borderRadius}px;
`;

export default ResponsiveKpiTracker;

Testing and Accessibility

Component Testing

Reference: UI-Kit Testing

Test Scenarios:

  1. Testing new components
  2. Testing edge cases
  3. Fixing broken tests
  4. Refactoring tests

RTL Query Priority:

  1. Accessible: getByRole, getByLabelText, getByPlaceholderText, getByText
  2. Semantic: getByAltText, getByTitle
  3. Test ID: getByTestId (last resort)

Example Test

import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "react-query";
import "@testing-library/jest-dom";
import KpiTracker from "./widget";

const mockHttpClient = jest.fn();
const mockShowNotification = jest.fn();
const mockUseShareValue = jest.fn();
const mockUseSelectSharedValue = jest.fn(() => null);

const defaultProps = {
platformMeta: {
storeWidgetsById: {},
platformWidgetsById: {},
currentDashboardId: "dash-123",
currentDashboard: undefined,
},
httpClient: mockHttpClient,
useShareValue: mockUseShareValue,
useSelectSharedValue: mockUseSelectSharedValue,
useDeleteSharedValue: jest.fn(),
title: "Test KPI Tracker",
apiUrl: "https://api.test.com",
refreshInterval: 30,
showTrend: true,
enableNotifications: false,
remoteModule: jest.fn(),
showNotification: mockShowNotification,
useQueryData: jest.fn(),
useDashboardNavigationInfo: jest.fn(),
getExtension: jest.fn(),
installedExtensions: [],
dashboardWidgetInstanceId: "widget-123",
};

const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});

const renderWidget = (props = {}) => {
return render(
<QueryClientProvider client={queryClient}>
<KpiTracker {...defaultProps} {...props} />
</QueryClientProvider>
);
};

describe("KpiTracker Widget", () => {
beforeEach(() => {
jest.clearAllMocks();
});

it("renders widget title", () => {
renderWidget();
expect(screen.getByText("Test KPI Tracker")).toBeInTheDocument();
});

it("displays loading state initially", () => {
renderWidget();
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});

it("fetches KPI data on mount", async () => {
mockHttpClient.mockResolvedValueOnce({
revenue: 125000,
users: 5280,
conversion: 3.42,
timestamp: Date.now(),
});

renderWidget();

await waitFor(() => {
expect(mockHttpClient).toHaveBeenCalledWith(
"https://api.test.com/kpi",
expect.objectContaining({
traceId: expect.stringContaining("kpi-tracker"),
})
);
});
});

it("displays KPI data when loaded", async () => {
const kpiData = {
revenue: 125000,
users: 5280,
conversion: 3.42,
timestamp: Date.now(),
};

mockHttpClient.mockResolvedValueOnce(kpiData);

renderWidget();

await waitFor(() => {
expect(screen.getByText(/\$125,000/)).toBeInTheDocument();
expect(screen.getByText(/5,280/)).toBeInTheDocument();
expect(screen.getByText(/3.42%/)).toBeInTheDocument();
});
});

it("shows error notification on API failure", async () => {
mockHttpClient.mockRejectedValueOnce(new Error("API Error"));

renderWidget();

await waitFor(() => {
expect(mockShowNotification).toHaveBeenCalledWith(
"error",
"Failed to load KPI data",
expect.objectContaining({
position: "top-center",
autoClose: 5000,
})
);
});
});

it("shares data via useShareValue when data loads", async () => {
const kpiData = {
revenue: 125000,
users: 5280,
conversion: 3.42,
timestamp: Date.now(),
};

mockHttpClient.mockResolvedValueOnce(kpiData);
const mockShareValue = jest.fn();
mockUseShareValue.mockReturnValue(mockShareValue);

renderWidget();

await waitFor(() => {
expect(mockShareValue).toHaveBeenCalledWith(["kpiData"], kpiData);
});
});

it("respects showTrend setting", () => {
renderWidget({ showTrend: false });

expect(screen.queryByText(//)).not.toBeInTheDocument();
});
});

Accessibility Testing

Reference: A11y Support

Tools:

  • eslint-plugin-jsx-a11y: Linting for accessibility issues
  • jest-axe: Automated accessibility testing
  • storybook-a11y-addon: Accessibility testing in Storybook
  • @react-aria: Accessible React components
  • Browser tools: axe DevTools extension, Lighthouse

Jest-Axe Example

import React from "react";
import { render } from "@testing-library/react";
import { axe, toHaveNoViolations } from "jest-axe";
import KpiTracker from "./widget";

expect.extend(toHaveNoViolations);

describe("KpiTracker Accessibility", () => {
it("should have no accessibility violations", async () => {
const { container } = render(<KpiTracker {...defaultProps} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});

Accessible Component Example

import React from "react";
import styled from "styled-components";

const AccessibleKpiCard: React.FC<{ label: string; value: string }> = ({
label,
value,
}) => {
return (
<Card role="region" aria-labelledby="kpi-label">
<Label id="kpi-label">{label}</Label>
<Value aria-live="polite" aria-atomic="true">
{value}
</Value>
</Card>
);
};

const Card = styled.div`
padding: 16px;
border-radius: 4px;
background-color: #f5f5f5;
`;

const Label = styled.div`
font-size: 14px;
color: #666;
`;

const Value = styled.div`
font-size: 24px;
font-weight: bold;
color: #333;
`;

export default AccessibleKpiCard;

Code Style

Reference: Code Style

Tools:

  • wl-linters: Centralized linting configuration
  • eslint: JavaScript/TypeScript linting
  • stylelint: CSS linting
  • prettier-eslint: Code formatting
  • Husky: Pre-commit hooks

Commands:

# Lint JavaScript/TypeScript
npm run lint

# Lint styles
npm run lint:styles

# Format code
npm run format

# Run all checks
npm run lint && npm run lint:styles && npm run format

Next Steps

Continue to: Development Workflow