Skip to content

Commit e226668

Browse files
committed
add sequenceFollower testing util, release action
1 parent d64ce36 commit e226668

File tree

5 files changed

+387
-23
lines changed

5 files changed

+387
-23
lines changed

.github/workflows/make-release.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version_type:
7+
type: choice
8+
description: 'Version type'
9+
required: true
10+
default: 'patch'
11+
options:
12+
- patch
13+
- minor
14+
- major
15+
16+
jobs:
17+
release:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout code
21+
uses: actions/checkout@v3
22+
with:
23+
fetch-depth: 0 # Fetch all history for accurate tagging
24+
25+
- name: Configure Git
26+
run: |
27+
git config user.name "GitHub Actions"
28+
git config user.email "[email protected]"
29+
git checkout main
30+
31+
- name: Set up Node.js
32+
uses: actions/setup-node@v3
33+
with:
34+
node-version: '22' # Specify your Node.js version
35+
registry-url: 'https://registry.npmjs.org/'
36+
37+
- name: Install dependencies
38+
run: npm install
39+
40+
- name: Run tests
41+
run: npm test
42+
43+
- name: Bump version and push changes
44+
id: npm_version
45+
env:
46+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47+
run: |
48+
npm version ${{ github.event.inputs.version_type }} -m "chore(release): %s"
49+
NEW_VERSION=$(node -p "require('./package.json').version")
50+
git push origin HEAD:main --follow-tags
51+
echo "NEW_VERSION=v${NEW_VERSION}" >> $GITHUB_OUTPUT
52+
53+
- name: Create GitHub Release
54+
uses: actions/create-release@v1
55+
env:
56+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
57+
with:
58+
tag_name: ${{ steps.npm_version.outputs.NEW_VERSION }}
59+
release_name: Release ${{ steps.npm_version.outputs.NEW_VERSION }}
60+
draft: false
61+
prerelease: false
62+
63+
- name: Publish to npm
64+
env:
65+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
66+
run: npm publish

biome.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
},
3030
"suspicious": {
3131
"noExplicitAny": "off"
32+
},
33+
"style": {
34+
"noParameterAssign": "off"
3235
}
3336
}
3437
},

docs/src/app/docs/testing/page.mdx

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ export const metadata = {
66

77
React Multi Page Forms provides two tools to make testing and development easier.
88

9-
## `flattenPages`
9+
## Testing the sequence with `followSequence`
1010

11-
This method takes any sequence and flattens it, using a depth first search. This is the same utility used internally to determine the order of pages and sequences.
11+
This function takes a sequence or an array of pages and sequences, some data, and runs through the form. It returns an array of visited pages.
1212

1313
This utility enables you to test sequences easily.
1414

1515
```typescript
16-
import { flattenPages } from 'react-multi-page-form/utils';
16+
import { followSequence } from 'react-multi-page-form/testUtils/followSequence';
1717
import { wyomingSequence } from '../stateSequences';
1818

1919
describe('wyoming sequence', () => {
@@ -25,7 +25,9 @@ describe('wyoming sequence', () => {
2525
acceptsVideoVisits: true
2626
};
2727

28-
const pageIds = flattened.filter(page => page.isRequired(data));
28+
const visited = followSequence(wyomingSequence, data)
29+
30+
const pageIds = visited.map(page => page.id);
2931

3032
expect(pageIds).toEqual([
3133
'page-one',
@@ -34,27 +36,10 @@ describe('wyoming sequence', () => {
3436
'last-page'
3537
]);
3638
})
37-
38-
it("should exclude video visits when they're not accepted", () => {
39-
const flattened = flattenPages(wyomingSequence);
40-
41-
const data = {
42-
state: 'WY',
43-
acceptsVideoVisits: false
44-
};
45-
46-
const pageIds = flattened.filter(page => page.isRequired(data));
47-
48-
expect(pageIds).toEqual([
49-
'page-one',
50-
'page-two',
51-
'last-page'
52-
]);
53-
} )
54-
39+
});
5540
```
5641

57-
## The `FormPagesTester` Component
42+
## Viewing all forms with the `FormPagesTester` component
5843

5944
Building and ensuring accurate validation on your forms can be very challenging. This utility component takes an array of pages and sample data, and makes a single page that contains all the forms for the sequence. This component can be useful when combined with Storybook and visual testing.
6045

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
// tests/followSequence.test.js
2+
3+
import React from 'react';
4+
import { followSequence } from '../testUtils/followSequence';
5+
import type { SequenceChild } from '../types';
6+
7+
describe('followSequence', () => {
8+
beforeEach(() => {
9+
jest.clearAllMocks();
10+
});
11+
12+
it('should return all required pages in order for a simple linear sequence', () => {
13+
const pages = [
14+
{
15+
id: 'page1',
16+
isComplete: () => false,
17+
Component: () => <div />,
18+
},
19+
{
20+
id: 'page2',
21+
isComplete: () => false,
22+
Component: () => <div />,
23+
},
24+
{
25+
id: 'page3',
26+
isComplete: () => false,
27+
Component: () => <div />,
28+
},
29+
];
30+
31+
const data = {};
32+
33+
const visitedPages = followSequence(pages, data);
34+
35+
expect(visitedPages.map((page) => page.id)).toEqual([
36+
'page1',
37+
'page2',
38+
'page3',
39+
]);
40+
});
41+
42+
it('should skip pages where isRequired returns false', () => {
43+
const pages = [
44+
{
45+
id: 'page1',
46+
isRequired: () => true,
47+
isComplete: () => false,
48+
Component: () => <div />,
49+
},
50+
{
51+
id: 'page2',
52+
isRequired: () => false,
53+
isComplete: () => false,
54+
Component: () => <div />,
55+
},
56+
{
57+
id: 'page3',
58+
isRequired: () => true,
59+
isComplete: () => false,
60+
Component: () => <div />,
61+
},
62+
];
63+
64+
const data = {};
65+
66+
const visitedPages = followSequence(pages, data);
67+
68+
expect(visitedPages.map((page) => page.id)).toEqual(['page1', 'page3']);
69+
});
70+
71+
it('should navigate to alternate next page when alternateNextPage returns a valid page ID', () => {
72+
const pages = [
73+
{
74+
id: 'page1',
75+
alternateNextPage: () => 'page3',
76+
isComplete: () => false,
77+
Component: () => <div />,
78+
},
79+
{ id: 'page2', isComplete: () => false, Component: () => <div /> },
80+
{ id: 'page3', isComplete: () => false, Component: () => <div /> },
81+
{ id: 'page4', isComplete: () => false, Component: () => <div /> },
82+
];
83+
84+
const data = {};
85+
86+
const visitedPages = followSequence(pages, data);
87+
88+
expect(visitedPages.map((page) => page.id)).toEqual([
89+
'page1',
90+
'page3',
91+
'page4',
92+
]);
93+
});
94+
95+
it('should stop navigation when a page with isFinal returns true', () => {
96+
const pages = [
97+
{ id: 'page1', isComplete: () => false, Component: () => <div /> },
98+
{
99+
id: 'page2',
100+
isFinal: () => true,
101+
isComplete: () => false,
102+
Component: () => <div />,
103+
},
104+
{ id: 'page3', isComplete: () => false, Component: () => <div /> },
105+
];
106+
107+
const data = {};
108+
109+
const visitedPages = followSequence(pages, data);
110+
111+
expect(visitedPages.map((page) => page.id)).toEqual(['page1', 'page2']);
112+
});
113+
114+
it('should proceed to next page when alternateNextPage returns an invalid page ID', () => {
115+
console.warn = jest.fn();
116+
117+
const pages = [
118+
{
119+
id: 'page1',
120+
alternateNextPage: () => 'invalidPage',
121+
isComplete: () => false,
122+
Component: () => <div />,
123+
},
124+
{ id: 'page2', isComplete: () => false, Component: () => <div /> },
125+
{ id: 'page3', isComplete: () => false, Component: () => <div /> },
126+
];
127+
128+
const data = {};
129+
130+
const visitedPages = followSequence(pages, data);
131+
132+
expect(visitedPages.map((page) => page.id)).toEqual([
133+
'page1',
134+
'page2',
135+
'page3',
136+
]);
137+
});
138+
139+
it('should handle data-dependent isRequired and alternateNextPage functions', () => {
140+
const formData = { includePage1: true, skipToPage4: true };
141+
142+
const pages = [
143+
{
144+
id: 'page1',
145+
isRequired: (data: Partial<typeof formData>) =>
146+
data.includePage1,
147+
isComplete: () => false,
148+
Component: () => <div />,
149+
},
150+
{
151+
id: 'page2',
152+
alternateNextPage: (data: Partial<typeof formData>) =>
153+
data.skipToPage4 ? 'page4' : undefined,
154+
isComplete: () => false,
155+
Component: () => <div />,
156+
},
157+
{ id: 'page3', isComplete: () => false, Component: () => <div /> },
158+
{ id: 'page4', isComplete: () => false, Component: () => <div /> },
159+
];
160+
161+
const visitedPages = followSequence(pages, formData);
162+
163+
expect(visitedPages.map((page) => page.id)).toEqual([
164+
'page1',
165+
'page2',
166+
'page4',
167+
]);
168+
});
169+
170+
it('should handle an empty sequence', () => {
171+
const pages: SequenceChild<any, any, any>[] = [];
172+
173+
const data = {};
174+
175+
const visitedPages = followSequence(pages, data);
176+
177+
expect(visitedPages).toEqual([]);
178+
});
179+
180+
it('should prevent infinite loops when alternateNextPage creates a loop', () => {
181+
const pages = [
182+
{
183+
id: 'page1',
184+
alternateNextPage: () => 'page2',
185+
isComplete: () => false,
186+
Component: () => <div />,
187+
},
188+
{
189+
id: 'page2',
190+
alternateNextPage: () => 'page1',
191+
isComplete: () => false,
192+
Component: () => <div />,
193+
},
194+
{ id: 'page3', isComplete: () => false, Component: () => <div /> },
195+
];
196+
197+
const data = {};
198+
199+
expect(() =>
200+
followSequence(pages, data),
201+
).toThrowErrorMatchingInlineSnapshot(
202+
`"Loop detected at page 'page1'. Navigation stopped. Full path: page1 -> page2 -> page1"`,
203+
);
204+
});
205+
206+
it('should handle pages with isFinal depending on data', () => {
207+
const formData = { endHere: true };
208+
209+
const pages = [
210+
{
211+
id: 'page1',
212+
isFinal: (data: Partial<typeof formData>) => !!data.endHere,
213+
isComplete: () => false,
214+
Component: () => <div />,
215+
},
216+
{ id: 'page2', isComplete: () => false, Component: () => <div /> },
217+
];
218+
219+
const visitedPages = followSequence(pages, formData);
220+
221+
expect(visitedPages.map((page) => page.id)).toEqual(['page1']);
222+
});
223+
224+
it('should continue navigation when isFinal returns false', () => {
225+
const pages = [
226+
{
227+
id: 'page1',
228+
isFinal: () => false,
229+
isComplete: () => false,
230+
Component: () => <div />,
231+
},
232+
{ id: 'page2', isComplete: () => false, Component: () => <div /> },
233+
];
234+
235+
const data = {};
236+
237+
const visitedPages = followSequence(pages, data);
238+
239+
expect(visitedPages.map((page) => page.id)).toEqual(['page1', 'page2']);
240+
});
241+
});

0 commit comments

Comments
 (0)