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
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:
- Testing new components
- Testing edge cases
- Fixing broken tests
- Refactoring tests
RTL Query Priority:
- Accessible: getByRole, getByLabelText, getByPlaceholderText, getByText
- Semantic: getByAltText, getByTitle
- 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 →