@asteby/metacore-runtime-react 22.0.0 → 23.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ // @vitest-environment happy-dom
2
+ //
3
+ // FilterChipsRow — the removable active-filter chip row shared by DynamicTable
4
+ // and DynamicKanban: one chip per active field (label + summarized value + a
5
+ // value-color dot when the option carries a color), an X that clears that field,
6
+ // and a trailing "Limpiar todo".
7
+ import { afterEach, describe, expect, it, vi } from 'vitest'
8
+ import { cleanup, render, screen, fireEvent } from '@testing-library/react'
9
+
10
+ vi.mock('react-i18next', () => ({
11
+ useTranslation: () => ({
12
+ t: (_k: string, opts?: { defaultValue?: string }) => opts?.defaultValue ?? _k,
13
+ }),
14
+ }))
15
+
16
+ import {
17
+ FilterChipsRow,
18
+ summarizeFilterValues,
19
+ chipValueColor,
20
+ type FilterChipField,
21
+ } from '../filter-chips'
22
+
23
+ afterEach(cleanup)
24
+
25
+ function field(over: Partial<FilterChipField> & { key: string }): FilterChipField {
26
+ return {
27
+ label: over.key,
28
+ config: {
29
+ selectedValues: [],
30
+ options: [],
31
+ filterKey: over.key,
32
+ onFilterChange: vi.fn(),
33
+ ...(over.config as any),
34
+ },
35
+ ...over,
36
+ }
37
+ }
38
+
39
+ describe('summarizeFilterValues / chipValueColor', () => {
40
+ const opts = [
41
+ { value: 'backlog', label: 'Pendiente', color: 'slate' },
42
+ { value: 'done', label: 'Hecho', color: 'green' },
43
+ ]
44
+ it('summarizes a single value to its (translated) option label', () => {
45
+ expect(summarizeFilterValues(['done'], opts)).toBe('Hecho')
46
+ })
47
+ it('resolves the option color for the chip dot', () => {
48
+ expect(chipValueColor({ selectedValues: ['done'], options: opts })).toBeTruthy()
49
+ })
50
+ it('has no dot for a free-text (ILIKE) value', () => {
51
+ expect(
52
+ chipValueColor({ selectedValues: ['ILIKE:foo'], options: opts }),
53
+ ).toBeUndefined()
54
+ })
55
+ })
56
+
57
+ describe('FilterChipsRow', () => {
58
+ const stageOptions = [
59
+ { value: 'backlog', label: 'Pendiente', color: 'slate' },
60
+ { value: 'done', label: 'Hecho', color: 'green' },
61
+ ]
62
+
63
+ it('renders nothing when there are no active fields', () => {
64
+ const { container } = render(
65
+ <FilterChipsRow fields={[]} onClearAll={() => {}} />,
66
+ )
67
+ expect(container.firstChild).toBeNull()
68
+ })
69
+
70
+ it('renders a chip per active field with its translated value summary', () => {
71
+ render(
72
+ <FilterChipsRow
73
+ fields={[
74
+ field({
75
+ key: 'stage',
76
+ label: 'Etapa',
77
+ config: {
78
+ selectedValues: ['done'],
79
+ options: stageOptions,
80
+ filterKey: 'stage',
81
+ onFilterChange: vi.fn(),
82
+ },
83
+ }),
84
+ ]}
85
+ onClearAll={() => {}}
86
+ data-testid="chips"
87
+ />,
88
+ )
89
+ expect(screen.getByText('Etapa:')).toBeTruthy()
90
+ // the value shows the TRANSLATED option label, not the raw key
91
+ expect(screen.getByText('Hecho')).toBeTruthy()
92
+ })
93
+
94
+ it('clears one field via its X and everything via "Limpiar todo"', () => {
95
+ const onFilterChange = vi.fn()
96
+ const onClearAll = vi.fn()
97
+ render(
98
+ <FilterChipsRow
99
+ fields={[
100
+ field({
101
+ key: 'stage',
102
+ label: 'Etapa',
103
+ config: {
104
+ selectedValues: ['done'],
105
+ options: stageOptions,
106
+ filterKey: 'stage',
107
+ onFilterChange,
108
+ },
109
+ }),
110
+ ]}
111
+ onClearAll={onClearAll}
112
+ />,
113
+ )
114
+ fireEvent.click(screen.getByLabelText('Quitar filtro'))
115
+ expect(onFilterChange).toHaveBeenCalledWith('stage', [])
116
+ fireEvent.click(screen.getByText('Limpiar todo'))
117
+ expect(onClearAll).toHaveBeenCalled()
118
+ })
119
+ })
@@ -169,14 +169,75 @@ describe('useDynamicFilters — facet upgrade + fallback (B)', () => {
169
169
  () => useDynamicFilters(meta(), { model: 'issue' }),
170
170
  { wrapper: wrapper(api) },
171
171
  )
172
+ const titleFacetCalls = () =>
173
+ (api.get as any).mock.calls.filter(
174
+ (c: any[]) =>
175
+ String(c[0]).endsWith('/facets') &&
176
+ c[1]?.params?.field === 'title' &&
177
+ !c[1]?.params?.q,
178
+ ).length
179
+ // Let the prewarm settle first, then reopening (empty query) is cached.
180
+ await waitFor(() => expect(titleFacetCalls()).toBe(1))
172
181
  const load = result.current.columnFilterConfigs.get('title')!.loadOptions!
173
182
  await load('')
174
183
  await load('')
175
- // Second call served from cache → only one network hit for that key.
184
+ expect(titleFacetCalls()).toBe(1)
185
+ })
186
+ })
187
+
188
+ describe('useDynamicFilters — facet prefetch', () => {
189
+ it('prewarms facet options on mount so the config carries them (no lazy open)', async () => {
190
+ const api = fakeApi()
191
+ const { result } = renderHook(
192
+ () => useDynamicFilters(meta(), { model: 'issue' }),
193
+ { wrapper: wrapper(api) },
194
+ )
195
+ // The prefetch burst fires an empty-query request per facet field.
196
+ await waitFor(() =>
197
+ expect(
198
+ (api.get as any).mock.calls.some(
199
+ (c: any[]) =>
200
+ String(c[0]) === '/data/issue/facets' &&
201
+ c[1]?.params?.field === 'title' &&
202
+ c[1]?.params?.q === undefined,
203
+ ),
204
+ ).toBe(true),
205
+ )
206
+ // …and those values land directly on the config (popover opens instantly).
207
+ await waitFor(() =>
208
+ expect(
209
+ result.current.columnFilterConfigs.get('title')?.options,
210
+ ).toEqual([
211
+ { value: 'ana', label: 'Ana', count: 3 },
212
+ { value: 'beto', label: 'Beto', count: 1 },
213
+ ]),
214
+ )
215
+ })
216
+
217
+ it('opening a prewarmed facet is a cache hit (no extra request)', async () => {
218
+ const api = fakeApi()
219
+ const { result } = renderHook(
220
+ () => useDynamicFilters(meta(), { model: 'issue' }),
221
+ { wrapper: wrapper(api) },
222
+ )
223
+ // wait for prefetch to settle
176
224
  await waitFor(() =>
177
- expect((api.get as any).mock.calls.filter((c: any[]) =>
178
- String(c[0]).endsWith('/facets'),
179
- ).length).toBe(1),
225
+ expect(
226
+ result.current.columnFilterConfigs.get('title')?.options?.length,
227
+ ).toBe(2),
180
228
  )
229
+ const before = (api.get as any).mock.calls.filter(
230
+ (c: any[]) =>
231
+ String(c[0]).endsWith('/facets') &&
232
+ c[1]?.params?.field === 'title',
233
+ ).length
234
+ // the popover's empty-query load reuses the prewarmed cache
235
+ await result.current.columnFilterConfigs.get('title')!.loadOptions!('')
236
+ const after = (api.get as any).mock.calls.filter(
237
+ (c: any[]) =>
238
+ String(c[0]).endsWith('/facets') &&
239
+ c[1]?.params?.field === 'title',
240
+ ).length
241
+ expect(after).toBe(before)
181
242
  })
182
243
  })