Skip to content

Commit 5a170d0

Browse files
committed
feat(variations): add lazy and eager generator functions
1 parent 11d4407 commit 5a170d0

File tree

6 files changed

+1152
-2
lines changed

6 files changed

+1152
-2
lines changed

packages/variations/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@wluwd/variations",
3-
"description": "",
3+
"description": "Generate all possible variations of object properties using cartesian products",
44
"version": "0.1.0",
55
"license": "MIT",
66
"repository": {

packages/variations/readme.md

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,266 @@
11
# @wluwd/variations
2+
3+
> Generate all possible variations of object properties using cartesian products
4+
5+
A lightweight, type-safe utility for generating all combinations of object properties. Perfect for testing component variants, creating configuration matrices, or any scenario where you need exhaustive combinations.
6+
7+
## Features
8+
9+
- 📦 **Type-safety**: full TypeScript support with precise type inference
10+
- 🪶 **Lightweight**: zero dependencies, ESM-only, minimal bundle size
11+
- 🏎️ **Efficient**: memory-efficient lazy evaluation for large datasets
12+
- 🌐 **Universal**: works in LTS Node.js (20+) and modern browsers
13+
14+
## Installation
15+
16+
> [!WARNING]
17+
> This package is ESM only. Ensure your project uses `"type": "module"` in `package.json`.
18+
19+
```bash
20+
npm install @wluwd/variations
21+
```
22+
23+
```bash
24+
pnpm add @wluwd/variations
25+
```
26+
27+
```bash
28+
yarn add @wluwd/variations
29+
```
30+
31+
## The Problem
32+
33+
When testing UI components with multiple props, manually writing test cases becomes unsustainable.
34+
35+
```tsx
36+
// Manual approach - error-prone and difficult and exhausting to maintain
37+
it('renders primary small button', () => { /* ... */ })
38+
it('renders primary normal button', () => { /* ... */ })
39+
it('renders primary large button', () => { /* ... */ })
40+
it('renders secondary small button', () => { /* ... */ })
41+
// ... many more tests to write by hand
42+
```
43+
44+
What if there's a better way though? That's where this library comes into play:
45+
46+
```tsx
47+
// Automated approach - declarative and maintainable
48+
const testCases = eagerVariations({
49+
variant: ['primary', 'secondary', 'destructive'],
50+
size: ['small', 'normal', 'large'],
51+
status: ['idle', 'loading', 'disabled']
52+
});
53+
54+
it.for(testCases)('Button: %o', (props) => { /* ... */ });
55+
// ✨ all tests generated automatically
56+
```
57+
58+
## Quick Start
59+
60+
### Basic Usage
61+
62+
```ts
63+
import { eagerVariations } from '@wluwd/variations';
64+
65+
const configs = eagerVariations({
66+
color: ['red', 'blue', 'green'],
67+
size: ['small', 'large']
68+
});
69+
70+
console.log(configs);
71+
// [
72+
// { color: 'red', size: 'small' },
73+
// { color: 'red', size: 'large' },
74+
// { color: 'blue', size: 'small' },
75+
// { color: 'blue', size: 'large' },
76+
// { color: 'green', size: 'small' },
77+
// { color: 'green', size: 'large' }
78+
// ]
79+
```
80+
81+
### Type-Safe Factory
82+
83+
For better autocomplete and output type inference, use `defineVariations`:
84+
```ts
85+
import { defineVariations } from '@wluwd/variations';
86+
87+
interface ButtonProps {
88+
variant: 'primary' | 'secondary';
89+
size: 'small' | 'large';
90+
}
91+
92+
const { eager, lazy } = defineVariations<ButtonProps>();
93+
94+
// 🪄 Full autocomplete with inferred output type:
95+
// { variant: 'primary'; size: 'small' | 'large' }
96+
const variations = eager({
97+
variant: ['primary'],
98+
size: ['small', 'large']
99+
});
100+
```
101+
102+
### With Filtering
103+
104+
```ts
105+
const validConfigs = eagerVariations(
106+
{
107+
variant: ['primary', 'link'],
108+
size: ['small', 'large']
109+
},
110+
{
111+
// Skip invalid combinations
112+
filter: v => !(v.variant === 'link' && v.size === 'large')
113+
}
114+
);
115+
```
116+
117+
### Memory-Efficient Processing
118+
119+
For large datasets, use `lazyVariations` to process one variation at a time:
120+
121+
```ts
122+
import { lazyVariations } from '@wluwd/variations';
123+
124+
for (const config of lazyVariations({
125+
variant: ['primary', 'secondary', 'destructive'],
126+
size: ['small', 'normal', 'large'],
127+
status: ['idle', 'loading', 'disabled']
128+
})) {
129+
// Process each of 27 variations without loading all into memory
130+
await processConfig(config);
131+
}
132+
```
133+
134+
## API
135+
136+
### `eagerVariations(base, options?)`
137+
138+
Generates all variations and returns them as an array.
139+
140+
**Parameters:**
141+
- `base` - Object where each property is an array of possible values
142+
- `options?` - Optional configuration object
143+
- `filter?` - Function to filter which variations to include
144+
- `safe?` - If `true`, returns empty array for invalid input instead of throwing
145+
146+
**Returns:** Array of all variation objects
147+
148+
### `lazyVariations(base, options?)`
149+
150+
Generates variations one at a time using a generator.
151+
152+
**Parameters:** Same as `eagerVariations`
153+
154+
**Yields:** Individual variation objects
155+
156+
### `defineVariations<T>()`
157+
158+
Creates a type-safe factory bound to a specific interface. It returns an object with two methods: `eager` and `lazy`. These work the same way as their stand-alone counterparts.
159+
160+
```ts
161+
interface ButtonProps {
162+
variant: 'primary' | 'secondary';
163+
size: 'small' | 'large';
164+
}
165+
166+
const { eager } = defineVariations<ButtonProps>();
167+
168+
// 🪄 `eager` provides autocomplete for properties
169+
const variations = eager({
170+
variant: ['primary'],
171+
size: ['small', 'large']
172+
});
173+
// typeof variations → Array<{ variant: 'primary', size: 'small' | 'large' }>
174+
```
175+
176+
### `VariationsInput<T>`
177+
178+
Type helper for explicitly typing variation inputs.
179+
180+
```ts
181+
interface ButtonProps {
182+
variant: 'primary' | 'secondary';
183+
size: 'small' | 'large';
184+
}
185+
186+
const input = {
187+
variant: ['primary', 'secondary'],
188+
size: ['small']
189+
} satisfies VariationsInput<ButtonProps>;
190+
```
191+
192+
This type is useful when directly using `eagerVariations` or `lazyVariations` as it allows to get the same output type as the helpers bound by `defineVariations`:
193+
194+
```ts
195+
interface ButtonProps {
196+
variant: 'primary' | 'secondary';
197+
size: 'small' | 'large';
198+
}
199+
200+
const variations = eagerVariations({
201+
variant: ['primary'],
202+
size: ['small', 'large']
203+
} satisfies VariationsInput<ButtonProps>);
204+
205+
// typeof variations → Array<{ variant: 'primary', size: 'small' | 'large' }>
206+
```
207+
208+
## Real-World Example
209+
210+
Visual regression testing for a design system:
211+
212+
```tsx
213+
import { eagerVariations } from '@wluwd/variations';
214+
import { render } from '@testing-library/react';
215+
216+
interface ButtonProps {
217+
variant: 'primary' | 'secondary' | 'destructive' | 'link';
218+
size: 'small' | 'normal' | 'large';
219+
status?: 'loading' | 'disabled';
220+
}
221+
222+
describe('Button visual regression', () => {
223+
const cases = eagerVariations({
224+
variant: ['primary', 'secondary', 'destructive', 'link'],
225+
size: ['small', 'normal', 'large'],
226+
status: [undefined, 'loading', 'disabled']
227+
} satisfies VariationsInput<ButtonProps>);
228+
229+
it.for(cases)('matches snapshot: %o', async (props) => {
230+
const screen = render(<Button {...props}>Click me</Button>);
231+
232+
// Test default state
233+
expect(screen.getButton()).toMatchSnapshot();
234+
235+
// Test hover state
236+
await userEvent.hover(screen.getButton());
237+
expect(screen.getButton()).toMatchSnapshot();
238+
239+
// Test active state
240+
await userEvent.click(screen.getButton());
241+
expect(screen.getButton()).toMatchSnapshot();
242+
});
243+
});
244+
245+
// ✨ Generates 108 comprehensive tests automatically
246+
```
247+
248+
## Background
249+
250+
This library was born from the need to test design system components exhaustively without manual overhead. What started as checking a handful of button variants eventually grew to 120+ combinations that needed verification for every CSS change.
251+
252+
Read the full story: [When Manual Testing Becomes Unsustainable](https://macarie.me/writing/when-manual-testing-becomes-unsustainable/).
253+
254+
## Performance
255+
256+
The algorithm uses an odometer/mixed-radix counter approach that:
257+
258+
- Only updates changed dimensions between iterations
259+
- Maintains stable object shapes for optimization
260+
- Supports lazy evaluation to avoid loading all combinations into memory
261+
262+
Keys are processed left-to-right, with leftmost keys being most stable (changing least frequently).
263+
264+
## License
265+
266+
MIT

0 commit comments

Comments
 (0)