Skip to content

Commit 9468354

Browse files
feat(frontend): add Jest unit testing infrastructure (#6432)
1 parent 7e2d9fb commit 9468354

10 files changed

Lines changed: 10355 additions & 6714 deletions

File tree

.github/workflows/frontend-deploy-production.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,33 @@ on:
99
- .github/**
1010

1111
jobs:
12+
run-unit-tests:
13+
runs-on: ubuntu-latest
14+
name: Run Unit Tests
15+
16+
defaults:
17+
run:
18+
working-directory: frontend
19+
20+
steps:
21+
- name: Cloning repo
22+
uses: actions/checkout@v5
23+
24+
- name: Setup Node.js
25+
uses: actions/setup-node@v4
26+
with:
27+
node-version-file: frontend/.nvmrc
28+
cache: npm
29+
cache-dependency-path: frontend/package-lock.json
30+
31+
- name: Install dependencies
32+
run: npm ci
33+
34+
- name: Run unit tests
35+
run: npm run test:unit -- --passWithNoTests
36+
1237
run-tests:
38+
needs: run-unit-tests
1339
runs-on: depot-ubuntu-latest
1440
name: Run E2E Tests
1541
environment: production
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Frontend Pull Requests
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, ready_for_review]
6+
paths:
7+
- frontend/**
8+
- .github/workflows/frontend-pull-request.yml
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
unit-tests:
15+
name: Unit Tests
16+
runs-on: ubuntu-latest
17+
18+
defaults:
19+
run:
20+
working-directory: frontend
21+
22+
steps:
23+
- uses: actions/checkout@v5
24+
25+
- name: Setup Node.js
26+
uses: actions/setup-node@v4
27+
with:
28+
node-version-file: frontend/.nvmrc
29+
cache: npm
30+
cache-dependency-path: frontend/package-lock.json
31+
32+
- name: Install dependencies
33+
run: npm ci
34+
35+
- name: Run unit tests
36+
run: npm run test:unit -- --passWithNoTests
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
// Mock useProjectFlag to avoid deep dependency chain with legacy JS files
2+
jest.mock('common/services/useProjectFlag', () => ({
3+
FEATURES_PAGE_SIZE: 100,
4+
}))
5+
6+
import {
7+
buildUrlParams,
8+
buildApiFilterParams,
9+
getFiltersFromParams,
10+
hasActiveFilters,
11+
} from 'common/utils/featureFilterParams'
12+
import { SortOrder } from 'common/types/requests'
13+
import { TagStrategy } from 'common/types/responses'
14+
import type { FilterState } from 'common/types/featureFilters'
15+
16+
const createDefaultFilters = (
17+
overrides?: Partial<FilterState>,
18+
): FilterState => ({
19+
group_owners: [],
20+
is_enabled: null,
21+
owners: [],
22+
search: null,
23+
showArchived: false,
24+
sort: {
25+
label: 'Name',
26+
sortBy: 'name',
27+
sortOrder: SortOrder.ASC,
28+
},
29+
tag_strategy: TagStrategy.INTERSECTION,
30+
tags: [],
31+
value_search: '',
32+
...overrides,
33+
})
34+
35+
describe('featureFilterParams', () => {
36+
describe('buildUrlParams', () => {
37+
it.each`
38+
showArchived | expected
39+
${false} | ${'false'}
40+
${true} | ${'true'}
41+
`(
42+
'sets is_archived to "$expected" when showArchived is $showArchived',
43+
({ expected, showArchived }) => {
44+
const result = buildUrlParams(createDefaultFilters({ showArchived }), 1)
45+
expect(result.is_archived).toBe(expected)
46+
},
47+
)
48+
49+
it('always includes is_archived (never undefined)', () => {
50+
const result = buildUrlParams(createDefaultFilters(), 1)
51+
expect(result.is_archived).toBeDefined()
52+
})
53+
54+
it('includes page number', () => {
55+
const result = buildUrlParams(createDefaultFilters(), 5)
56+
expect(result.page).toBe(5)
57+
})
58+
59+
it('includes sort parameters', () => {
60+
const filters = createDefaultFilters({
61+
sort: {
62+
label: 'Created',
63+
sortBy: 'created_date',
64+
sortOrder: SortOrder.DESC,
65+
},
66+
})
67+
const result = buildUrlParams(filters, 1)
68+
expect(result.sortBy).toBe('created_date')
69+
expect(result.sortOrder).toBe('desc')
70+
})
71+
72+
it('includes tags when present', () => {
73+
const result = buildUrlParams(
74+
createDefaultFilters({ tags: [1, 2, 3] }),
75+
1,
76+
)
77+
expect(result.tags).toBe('1,2,3')
78+
})
79+
80+
it('excludes empty arrays and search', () => {
81+
const filters = createDefaultFilters({ owners: [], search: '', tags: [] })
82+
const result = buildUrlParams(filters, 1)
83+
expect(result.tags).toBeUndefined()
84+
expect(result.owners).toBeUndefined()
85+
expect(result.search).toBeUndefined()
86+
})
87+
88+
it('includes search when present', () => {
89+
const result = buildUrlParams(createDefaultFilters({ search: 'test' }), 1)
90+
expect(result.search).toBe('test')
91+
})
92+
})
93+
94+
describe('buildApiFilterParams', () => {
95+
const mockResolver = (apiKey: string) =>
96+
apiKey === 'test-key' ? 123 : undefined
97+
98+
it.each`
99+
showArchived | expected
100+
${false} | ${'false'}
101+
${true} | ${'true'}
102+
`(
103+
'sets is_archived to "$expected" when showArchived is $showArchived',
104+
({ expected, showArchived }) => {
105+
const result = buildApiFilterParams(
106+
createDefaultFilters({ showArchived }),
107+
1,
108+
'test-key',
109+
1,
110+
mockResolver,
111+
)
112+
expect(result?.is_archived).toBe(expected)
113+
},
114+
)
115+
116+
it('always includes is_archived (never undefined)', () => {
117+
const result = buildApiFilterParams(
118+
createDefaultFilters(),
119+
1,
120+
'test-key',
121+
1,
122+
mockResolver,
123+
)
124+
expect(result).not.toBeNull()
125+
expect(result?.is_archived).toBeDefined()
126+
})
127+
128+
it('returns null when environment ID cannot be resolved', () => {
129+
const result = buildApiFilterParams(
130+
createDefaultFilters(),
131+
1,
132+
'invalid-key',
133+
1,
134+
mockResolver,
135+
)
136+
expect(result).toBeNull()
137+
})
138+
139+
it('includes environmentId and projectId', () => {
140+
const result = buildApiFilterParams(
141+
createDefaultFilters(),
142+
1,
143+
'test-key',
144+
42,
145+
mockResolver,
146+
)
147+
expect(result?.environmentId).toBe('123')
148+
expect(result?.projectId).toBe(42)
149+
})
150+
})
151+
152+
describe('getFiltersFromParams', () => {
153+
it.each`
154+
is_archived | expected
155+
${'true'} | ${true}
156+
${'false'} | ${false}
157+
${undefined} | ${false}
158+
`(
159+
'parses is_archived=$is_archived to showArchived=$expected',
160+
({ expected, is_archived }) => {
161+
const result = getFiltersFromParams(is_archived ? { is_archived } : {})
162+
expect(result.showArchived).toBe(expected)
163+
},
164+
)
165+
166+
it.each`
167+
page | expected
168+
${'3'} | ${3}
169+
${undefined} | ${1}
170+
`('parses page=$page to $expected', ({ expected, page }) => {
171+
const result = getFiltersFromParams(page ? { page } : {})
172+
expect(result.page).toBe(expected)
173+
})
174+
175+
it('parses tags as array of numbers', () => {
176+
const result = getFiltersFromParams({ tags: '1,2,3' })
177+
expect(result.tags).toEqual([1, 2, 3])
178+
})
179+
180+
it('parses sort order', () => {
181+
const result = getFiltersFromParams({
182+
sortBy: 'created_date',
183+
sortOrder: 'desc',
184+
})
185+
expect(result.sort.sortBy).toBe('created_date')
186+
expect(result.sort.sortOrder).toBe(SortOrder.DESC)
187+
})
188+
})
189+
190+
describe('hasActiveFilters', () => {
191+
it('returns false for default filters', () => {
192+
expect(hasActiveFilters(createDefaultFilters())).toBe(false)
193+
})
194+
195+
it.each`
196+
scenario | overrides
197+
${'tags present'} | ${{ tags: [1] }}
198+
${'showArchived'} | ${{ showArchived: true }}
199+
${'search present'} | ${{ search: 'test' }}
200+
${'is_enabled set'} | ${{ is_enabled: true }}
201+
${'owners present'} | ${{ owners: [1] }}
202+
`('returns true when $scenario', ({ overrides }) => {
203+
expect(hasActiveFilters(createDefaultFilters(overrides))).toBe(true)
204+
})
205+
})
206+
})

0 commit comments

Comments
 (0)