@cdc/dashboard 4.25.11 → 4.26.1
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.
- package/Dynamic_Data.md +66 -0
- package/dist/cdcdashboard.js +78783 -76370
- package/examples/api-dashboard-data.json +272 -0
- package/examples/api-dashboard-years.json +11 -0
- package/examples/api-geographies-data.json +11 -0
- package/examples/private/cat-y.json +1235 -0
- package/examples/private/chronic-dash.json +1584 -0
- package/examples/private/map-issue.json +2260 -0
- package/examples/private/mpinc-state-reports.json +2260 -0
- package/examples/private/nwss/rsv.json +1240 -0
- package/examples/private/simple-dash.json +490 -0
- package/examples/private/test-dash.json +0 -0
- package/examples/private/test123.json +491 -0
- package/examples/test-dashboard-simple.json +503 -0
- package/index.html +24 -25
- package/package.json +12 -11
- package/src/CdcDashboardComponent.tsx +18 -2
- package/src/_stories/Dashboard.DataSetup.stories.tsx +203 -0
- package/src/_stories/Dashboard.stories.tsx +385 -1
- package/src/_stories/_mock/filter-cascade.json +3350 -0
- package/src/_stories/_mock/gallery-data-bite-dashboard.json +3500 -0
- package/src/_stories/_mock/nested-parent-child-filters.json +392 -0
- package/src/_stories/_mock/parent-child-filters.json +233 -0
- package/src/components/DashboardFilters/DashboardFilters.tsx +20 -11
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +89 -38
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +51 -29
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +146 -9
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +11 -7
- package/src/components/DataDesignerModal.tsx +6 -1
- package/src/components/Header/Header.tsx +51 -20
- package/src/components/VisualizationRow.tsx +71 -5
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -3
- package/src/components/Widget/Widget.tsx +1 -1
- package/src/data/initial-state.js +1 -0
- package/src/helpers/addValuesToDashboardFilters.ts +15 -22
- package/src/helpers/changeFilterActive.ts +67 -65
- package/src/helpers/formatConfigBeforeSave.ts +6 -5
- package/src/helpers/getUpdateConfig.ts +91 -91
- package/src/helpers/tests/updatesChildFilters.test.ts +53 -22
- package/src/helpers/updateChildFilters.ts +50 -27
- package/src/scss/main.scss +141 -1
- package/src/test/CdcDashboard.test.jsx +9 -4
- package/src/types/Dashboard.ts +1 -0
- package/src/types/FilterStyles.ts +8 -7
- package/src/types/SharedFilter.ts +13 -0
- package/LICENSE +0 -201
- package/examples/private/DEV-10538.json +0 -407
- package/examples/private/DEV-11072.json +0 -7591
- package/examples/private/DEV-11405.json +0 -39112
- package/examples/private/delete.json +0 -32919
- package/examples/private/pedro.json +0 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { within, expect, waitFor, userEvent } from 'storybook/test'
|
|
3
|
+
import CdcEditor from '@cdc/editor/src/CdcEditor'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof CdcEditor> = {
|
|
6
|
+
title: 'Components/Pages/Dashboard/Data Setup Integration Tests',
|
|
7
|
+
component: CdcEditor
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
type Story = StoryObj<typeof CdcEditor>
|
|
13
|
+
|
|
14
|
+
// Helper function to wait
|
|
15
|
+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Multi-Visualization Configuration Workflow
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// This test matches the Playwright codegen test exactly
|
|
21
|
+
|
|
22
|
+
export const MultiVizConfigurationWorkflow: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
config: {}
|
|
25
|
+
},
|
|
26
|
+
parameters: {
|
|
27
|
+
docs: {
|
|
28
|
+
description: {
|
|
29
|
+
story:
|
|
30
|
+
'Tests multi-visualization configuration workflow (matches Playwright codegen):\n' +
|
|
31
|
+
'1. Click "Choose Visualization Type"\n' +
|
|
32
|
+
'2. Click "Dashboard" button\n' +
|
|
33
|
+
'3. Click "Load from URL"\n' +
|
|
34
|
+
'4. Enter "Demo" in dataset name textbox\n' +
|
|
35
|
+
'5. Fill "/custom-order-demo-data.csv" in URL textbox\n' +
|
|
36
|
+
'6. Click "Always load from URL"\n' +
|
|
37
|
+
'7. Click "Save & Load" button\n' +
|
|
38
|
+
'8. Click "Configure your visualization" button\n' +
|
|
39
|
+
'9. Click "gearMulti" button\n' +
|
|
40
|
+
'10. Select "Demo" from combobox\n' +
|
|
41
|
+
'11. Click "Vertical Values for map" button\n' +
|
|
42
|
+
'12. Click "No" button\n' +
|
|
43
|
+
'13. Click "Configure Multiple"\n' +
|
|
44
|
+
'14. Select "Location" from Multi-Visualization Column dropdown\n' +
|
|
45
|
+
'15. Click "Continue" button'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
play: async ({ canvasElement }) => {
|
|
50
|
+
const canvas = within(canvasElement)
|
|
51
|
+
const user = userEvent.setup()
|
|
52
|
+
|
|
53
|
+
// ========================================================================
|
|
54
|
+
// STEP 1: Click "Choose Visualization Type" (verify it's visible)
|
|
55
|
+
// ========================================================================
|
|
56
|
+
await waitFor(
|
|
57
|
+
async () => {
|
|
58
|
+
const chooseVizText = canvas.queryByText(/choose visualization type/i)
|
|
59
|
+
expect(chooseVizText).toBeTruthy()
|
|
60
|
+
},
|
|
61
|
+
{ timeout: 5000 }
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const chooseVizText = canvas.getByText(/choose visualization type/i)
|
|
65
|
+
await user.click(chooseVizText)
|
|
66
|
+
await sleep(500)
|
|
67
|
+
|
|
68
|
+
// ========================================================================
|
|
69
|
+
// STEP 2: Click "Dashboard" button
|
|
70
|
+
// ========================================================================
|
|
71
|
+
const dashboardButton = canvas.getByRole('button', { name: /dashboard/i })
|
|
72
|
+
await user.click(dashboardButton)
|
|
73
|
+
await sleep(1500)
|
|
74
|
+
|
|
75
|
+
// ========================================================================
|
|
76
|
+
// STEP 3: Click "Load from URL"
|
|
77
|
+
// ========================================================================
|
|
78
|
+
await sleep(500)
|
|
79
|
+
|
|
80
|
+
const loadFromUrlButton =
|
|
81
|
+
canvas.queryAllByText(/load from url/i)[0] ||
|
|
82
|
+
canvas.queryByRole('button', { name: /url/i }) ||
|
|
83
|
+
canvas.queryByRole('tab', { name: /url/i })
|
|
84
|
+
|
|
85
|
+
if (loadFromUrlButton) {
|
|
86
|
+
await user.click(loadFromUrlButton)
|
|
87
|
+
await sleep(500)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ========================================================================
|
|
91
|
+
// STEP 4: Enter "Demo" in dataset name textbox
|
|
92
|
+
// ========================================================================
|
|
93
|
+
await sleep(500)
|
|
94
|
+
|
|
95
|
+
const datasetNameInput = canvas.getByRole('textbox', { name: /enter dataset name/i })
|
|
96
|
+
await user.click(datasetNameInput)
|
|
97
|
+
await user.clear(datasetNameInput)
|
|
98
|
+
await user.type(datasetNameInput, 'Demo')
|
|
99
|
+
await sleep(300)
|
|
100
|
+
|
|
101
|
+
// ========================================================================
|
|
102
|
+
// STEP 5: Fill "/custom-order-demo-data.csv" in URL textbox
|
|
103
|
+
// ========================================================================
|
|
104
|
+
|
|
105
|
+
// Press Tab to move to URL field
|
|
106
|
+
await user.tab()
|
|
107
|
+
|
|
108
|
+
const urlInput = canvas.getByRole('textbox', { name: /load data from external url/i })
|
|
109
|
+
await user.clear(urlInput)
|
|
110
|
+
await user.type(urlInput, '/custom-order-demo-data.csv')
|
|
111
|
+
await sleep(300)
|
|
112
|
+
|
|
113
|
+
// ========================================================================
|
|
114
|
+
// STEP 6: Click "Always load from URL"
|
|
115
|
+
// ========================================================================
|
|
116
|
+
await sleep(500)
|
|
117
|
+
|
|
118
|
+
const alwaysLoadText = canvas.getByText(/always load from url/i)
|
|
119
|
+
await user.click(alwaysLoadText)
|
|
120
|
+
await sleep(300)
|
|
121
|
+
|
|
122
|
+
// ========================================================================
|
|
123
|
+
// STEP 7: Click "Save & Load" button
|
|
124
|
+
// ========================================================================
|
|
125
|
+
await sleep(500)
|
|
126
|
+
|
|
127
|
+
const saveLoadButton = canvas.getByRole('button', { name: /save & load/i })
|
|
128
|
+
await user.click(saveLoadButton)
|
|
129
|
+
await sleep(2000) // Wait for data to load
|
|
130
|
+
|
|
131
|
+
// ========================================================================
|
|
132
|
+
// STEP 8: Click "Configure your visualization" button
|
|
133
|
+
// ========================================================================
|
|
134
|
+
await sleep(1000)
|
|
135
|
+
|
|
136
|
+
const configureButton = canvas.getByRole('button', { name: /configure your visualization/i })
|
|
137
|
+
await user.click(configureButton)
|
|
138
|
+
await sleep(500)
|
|
139
|
+
|
|
140
|
+
// ========================================================================
|
|
141
|
+
// STEP 9: Click "gearMulti" button
|
|
142
|
+
// ========================================================================
|
|
143
|
+
await sleep(500)
|
|
144
|
+
|
|
145
|
+
const gearMultiButton = canvas.getByRole('button', { name: /gearMulti/i })
|
|
146
|
+
await user.click(gearMultiButton)
|
|
147
|
+
await sleep(500)
|
|
148
|
+
|
|
149
|
+
// ========================================================================
|
|
150
|
+
// STEP 10: Select "Demo" from combobox
|
|
151
|
+
// ========================================================================
|
|
152
|
+
await sleep(500)
|
|
153
|
+
|
|
154
|
+
const dataSourceSelect = canvas.getByRole('combobox')
|
|
155
|
+
await user.selectOptions(dataSourceSelect, 'Demo')
|
|
156
|
+
await sleep(500)
|
|
157
|
+
|
|
158
|
+
// ========================================================================
|
|
159
|
+
// STEP 11: Click "Vertical Values for map" button
|
|
160
|
+
// ========================================================================
|
|
161
|
+
await sleep(500)
|
|
162
|
+
|
|
163
|
+
const verticalValuesButton = canvas.getByRole('button', { name: /vertical values for map/i })
|
|
164
|
+
await user.click(verticalValuesButton)
|
|
165
|
+
await sleep(500)
|
|
166
|
+
|
|
167
|
+
// ========================================================================
|
|
168
|
+
// STEP 12: Click "No" button
|
|
169
|
+
// ========================================================================
|
|
170
|
+
await sleep(500)
|
|
171
|
+
|
|
172
|
+
const noButton = canvas.getByRole('button', { name: /^no$/i })
|
|
173
|
+
await user.click(noButton)
|
|
174
|
+
await sleep(500)
|
|
175
|
+
|
|
176
|
+
// ========================================================================
|
|
177
|
+
// STEP 13: Click "Configure Multiple"
|
|
178
|
+
// ========================================================================
|
|
179
|
+
await sleep(500)
|
|
180
|
+
|
|
181
|
+
const configureMultipleText = canvas.getByText(/configure multiple/i)
|
|
182
|
+
await user.click(configureMultipleText)
|
|
183
|
+
await sleep(500)
|
|
184
|
+
|
|
185
|
+
// ========================================================================
|
|
186
|
+
// STEP 14: Select "Location" from Multi-Visualization Column dropdown
|
|
187
|
+
// ========================================================================
|
|
188
|
+
await sleep(500)
|
|
189
|
+
|
|
190
|
+
const multiVizColumnSelect = canvas.getByLabelText(/multi-visualization column/i)
|
|
191
|
+
await user.selectOptions(multiVizColumnSelect, 'Location')
|
|
192
|
+
await sleep(500)
|
|
193
|
+
|
|
194
|
+
// ========================================================================
|
|
195
|
+
// STEP 15: Click "Continue" button
|
|
196
|
+
// ========================================================================
|
|
197
|
+
await sleep(500)
|
|
198
|
+
|
|
199
|
+
const continueButton = canvas.getByRole('button', { name: /continue/i })
|
|
200
|
+
await user.click(continueButton)
|
|
201
|
+
await sleep(500)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
2
|
import { faker } from '@faker-js/faker'
|
|
3
|
+
import { waitForOptionsToPopulate, performAndAssert } from '@cdc/core/helpers/testing'
|
|
3
4
|
import APIFiltersMapData from './_mock/api-filter-map.json'
|
|
4
5
|
import APIFiltersChartData from './_mock/api-filter-chart.json'
|
|
5
6
|
import APIFilterErrorConfig from './_mock/api-filter-error.json'
|
|
@@ -15,7 +16,7 @@ import StandaloneTable from './_mock/standalone-table.json'
|
|
|
15
16
|
import GroupPivotConfig from './_mock/group-pivot-filter.json'
|
|
16
17
|
import PivotFitlerConfig from './_mock/pivot-filter.json'
|
|
17
18
|
import { type DashboardConfig as Config } from '../types/DashboardConfig'
|
|
18
|
-
import { userEvent, within } from 'storybook/test'
|
|
19
|
+
import { userEvent, within, expect } from 'storybook/test'
|
|
19
20
|
import ToggleExampleConfig from './_mock/toggle-example.json'
|
|
20
21
|
import _ from 'lodash'
|
|
21
22
|
import { footnotesSymbols } from '@cdc/core/helpers/footnoteSymbols'
|
|
@@ -30,6 +31,10 @@ import TopSpacing_3 from './_mock/data-bite-dash-test_1_1.json'
|
|
|
30
31
|
import TopSpacing_4 from './_mock/data-bite-dash-test_1_1_1.json'
|
|
31
32
|
import CustomOrderNewValues from './_mock/custom-order-new-values.json'
|
|
32
33
|
import APIFilterResetConfig from '../../examples/test-api-filter-reset.json'
|
|
34
|
+
import CascadingDataFilters from './_mock/filter-cascade.json'
|
|
35
|
+
import ParentChildFilters from './_mock/parent-child-filters.json'
|
|
36
|
+
import NestedParentChildFilters from './_mock/nested-parent-child-filters.json'
|
|
37
|
+
import GalleryDataBiteDashboard from './_mock/gallery-data-bite-dashboard.json'
|
|
33
38
|
|
|
34
39
|
// Dashboard Filter Updates for Ascending, Descending, and Custom Order
|
|
35
40
|
import DashboardFilterAsc from './_mock/dashboard-filter-asc.json'
|
|
@@ -482,6 +487,13 @@ export const Top_Spacing_4: Story = {
|
|
|
482
487
|
}
|
|
483
488
|
}
|
|
484
489
|
|
|
490
|
+
export const Gallery_Data_Bite_Dashboard: Story = {
|
|
491
|
+
args: {
|
|
492
|
+
config: GalleryDataBiteDashboard,
|
|
493
|
+
isEditor: false
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
485
497
|
export const Clear_Filters_Button: Story = {
|
|
486
498
|
args: {
|
|
487
499
|
config: APIFilterResetConfig as unknown as Config,
|
|
@@ -489,4 +501,376 @@ export const Clear_Filters_Button: Story = {
|
|
|
489
501
|
}
|
|
490
502
|
}
|
|
491
503
|
|
|
504
|
+
export const Cascading_Multi_Parent_Data_Filters: Story = {
|
|
505
|
+
args: {
|
|
506
|
+
config: CascadingDataFilters as unknown as Config,
|
|
507
|
+
isEditor: false
|
|
508
|
+
},
|
|
509
|
+
parameters: {
|
|
510
|
+
docs: {
|
|
511
|
+
description: {
|
|
512
|
+
story:
|
|
513
|
+
'Demonstrates cascading data filters with multiple parent relationships. The size filter depends on color, and weight depends on both color and size, creating a multi-tier cascading filter system with deterministic test data.'
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
play: async ({ canvasElement }) => {
|
|
518
|
+
const canvas = within(canvasElement)
|
|
519
|
+
const user = userEvent.setup()
|
|
520
|
+
|
|
521
|
+
// Get all filter dropdowns (using findBy to wait for them)
|
|
522
|
+
const colorFilter = (await canvas.findByLabelText('color', { selector: 'select' })) as HTMLSelectElement
|
|
523
|
+
const sizeFilter = (await canvas.findByLabelText('size', { selector: 'select' })) as HTMLSelectElement
|
|
524
|
+
const weightFilter = (await canvas.findByLabelText('weight', { selector: 'select' })) as HTMLSelectElement
|
|
525
|
+
const smellFilter = (await canvas.findByLabelText('smell', { selector: 'select' })) as HTMLSelectElement
|
|
526
|
+
|
|
527
|
+
// Helper to get non-empty filter options
|
|
528
|
+
const getFilterOptions = (filter: HTMLSelectElement) =>
|
|
529
|
+
Array.from(filter.querySelectorAll('option'))
|
|
530
|
+
.map(opt => opt.value)
|
|
531
|
+
.filter(v => v)
|
|
532
|
+
|
|
533
|
+
// Helper to get state of filters and chart
|
|
534
|
+
const getState = () => ({
|
|
535
|
+
colorOptions: getFilterOptions(colorFilter),
|
|
536
|
+
colorSelected: colorFilter.value,
|
|
537
|
+
sizeOptions: getFilterOptions(sizeFilter),
|
|
538
|
+
sizeSelected: sizeFilter.value,
|
|
539
|
+
weightOptions: getFilterOptions(weightFilter),
|
|
540
|
+
weightSelected: weightFilter.value,
|
|
541
|
+
smellOptions: getFilterOptions(smellFilter),
|
|
542
|
+
smellSelected: smellFilter.value,
|
|
543
|
+
chartRendered: !!canvasElement.querySelector('svg'),
|
|
544
|
+
noDataVisible: !!canvas.queryByText('No Data Available')
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
// Initial verification - wait for options to populate
|
|
548
|
+
await waitForOptionsToPopulate(colorFilter, 3)
|
|
549
|
+
const initialState = getState()
|
|
550
|
+
expect(initialState.chartRendered).toBe(true)
|
|
551
|
+
expect(initialState.noDataVisible).toBe(false)
|
|
552
|
+
|
|
553
|
+
// ============================================================================
|
|
554
|
+
// TEST: Color → Size cascade (single parent)
|
|
555
|
+
// ============================================================================
|
|
556
|
+
|
|
557
|
+
// Test 1: Select blue → size should show all 3 sizes
|
|
558
|
+
await performAndAssert(
|
|
559
|
+
'Select color=blue → size shows large, medium, small',
|
|
560
|
+
getState,
|
|
561
|
+
async () => await user.selectOptions(colorFilter, ['blue']),
|
|
562
|
+
(before, after) =>
|
|
563
|
+
after.colorSelected === 'blue' &&
|
|
564
|
+
after.sizeOptions.includes('large') &&
|
|
565
|
+
after.sizeOptions.includes('medium') &&
|
|
566
|
+
after.sizeOptions.includes('small') &&
|
|
567
|
+
after.sizeOptions.length === 3 &&
|
|
568
|
+
after.chartRendered &&
|
|
569
|
+
!after.noDataVisible
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
// Test 2: Select red → size should show only small, medium
|
|
573
|
+
await performAndAssert(
|
|
574
|
+
'Select color=red → size shows medium, small (no large)',
|
|
575
|
+
getState,
|
|
576
|
+
async () => await user.selectOptions(colorFilter, ['red']),
|
|
577
|
+
(before, after) =>
|
|
578
|
+
after.colorSelected === 'red' &&
|
|
579
|
+
after.sizeOptions.includes('medium') &&
|
|
580
|
+
after.sizeOptions.includes('small') &&
|
|
581
|
+
!after.sizeOptions.includes('large') &&
|
|
582
|
+
after.sizeOptions.length === 2 &&
|
|
583
|
+
after.chartRendered &&
|
|
584
|
+
!after.noDataVisible
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
// Test 3: Select green → size should show only large
|
|
588
|
+
await performAndAssert(
|
|
589
|
+
'Select color=green → size shows only large',
|
|
590
|
+
getState,
|
|
591
|
+
async () => await user.selectOptions(colorFilter, ['green']),
|
|
592
|
+
(before, after) =>
|
|
593
|
+
after.colorSelected === 'green' &&
|
|
594
|
+
after.sizeOptions.length === 1 &&
|
|
595
|
+
after.sizeOptions[0] === 'large' &&
|
|
596
|
+
after.sizeSelected === 'large' &&
|
|
597
|
+
after.chartRendered &&
|
|
598
|
+
!after.noDataVisible
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
// ============================================================================
|
|
602
|
+
// TEST: Color + Size → Weight cascade (multiple parents)
|
|
603
|
+
// ============================================================================
|
|
604
|
+
|
|
605
|
+
// Test 4: Select blue + small → weight updates based on both parents
|
|
606
|
+
await performAndAssert(
|
|
607
|
+
'Select color=blue → size options repopulate',
|
|
608
|
+
getState,
|
|
609
|
+
async () => await user.selectOptions(colorFilter, ['blue']),
|
|
610
|
+
(before, after) =>
|
|
611
|
+
after.colorSelected === 'blue' && after.sizeSelected === 'large' && after.sizeOptions.length === 3
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
await performAndAssert(
|
|
615
|
+
'Select size=small → weight updates based on color+size',
|
|
616
|
+
getState,
|
|
617
|
+
async () => await user.selectOptions(sizeFilter, ['small']),
|
|
618
|
+
(before, after) =>
|
|
619
|
+
after.colorSelected === 'blue' &&
|
|
620
|
+
after.sizeSelected === 'small' &&
|
|
621
|
+
after.weightOptions.length === 3 &&
|
|
622
|
+
after.chartRendered &&
|
|
623
|
+
!after.noDataVisible
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
// ============================================================================
|
|
627
|
+
// TEST: Weight → Smell cascade
|
|
628
|
+
// ============================================================================
|
|
629
|
+
|
|
630
|
+
// Test 5: Select light → smell shows only neutral and sweet (2 options)
|
|
631
|
+
await performAndAssert(
|
|
632
|
+
'Select weight=light → smell shows neutral, sweet (not bitter)',
|
|
633
|
+
getState,
|
|
634
|
+
async () => await user.selectOptions(weightFilter, ['light']),
|
|
635
|
+
(before, after) =>
|
|
636
|
+
after.colorSelected === 'blue' &&
|
|
637
|
+
after.sizeSelected === 'small' &&
|
|
638
|
+
after.weightSelected === 'light' &&
|
|
639
|
+
after.smellOptions.includes('neutral') &&
|
|
640
|
+
after.smellOptions.includes('sweet') &&
|
|
641
|
+
!after.smellOptions.includes('bitter') &&
|
|
642
|
+
after.smellOptions.length === 2 &&
|
|
643
|
+
after.chartRendered &&
|
|
644
|
+
!after.noDataVisible
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
// ============================================================================
|
|
648
|
+
// TEST: Full cascade verification - change parent and verify all children
|
|
649
|
+
// ============================================================================
|
|
650
|
+
|
|
651
|
+
// Test 6: Final - select red again and verify entire cascade updates
|
|
652
|
+
await performAndAssert(
|
|
653
|
+
'Select color=red → all filters update; size shows medium, small',
|
|
654
|
+
getState,
|
|
655
|
+
async () => await user.selectOptions(colorFilter, ['red']),
|
|
656
|
+
(before, after) =>
|
|
657
|
+
after.colorSelected === 'red' &&
|
|
658
|
+
after.sizeOptions.length === 2 &&
|
|
659
|
+
after.sizeOptions.includes('medium') &&
|
|
660
|
+
after.sizeOptions.includes('small') &&
|
|
661
|
+
// Size, weight, and smell selected values should be valid in their updated options
|
|
662
|
+
after.sizeOptions.includes(after.sizeSelected) &&
|
|
663
|
+
after.weightOptions.includes(after.weightSelected) &&
|
|
664
|
+
after.smellOptions.includes(after.smellSelected) &&
|
|
665
|
+
after.chartRendered &&
|
|
666
|
+
!after.noDataVisible
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export const Parent_Child_Filters_With_DefaultValue: Story = {
|
|
672
|
+
args: {
|
|
673
|
+
config: ParentChildFilters as unknown as Config,
|
|
674
|
+
isEditor: false
|
|
675
|
+
},
|
|
676
|
+
parameters: {
|
|
677
|
+
docs: {
|
|
678
|
+
description: {
|
|
679
|
+
story:
|
|
680
|
+
'Demonstrates parent-child filter relationships (State → County → City) with defaultValue support. Shows how child filter options update based on parent selection, and how defaultValue is applied on initial load.'
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
play: async ({ canvasElement }) => {
|
|
685
|
+
const canvas = within(canvasElement)
|
|
686
|
+
const user = userEvent.setup()
|
|
687
|
+
|
|
688
|
+
// Get filter dropdowns
|
|
689
|
+
const stateFilter = (await canvas.findByLabelText('State', { selector: 'select' })) as HTMLSelectElement
|
|
690
|
+
const countyFilter = (await canvas.findByLabelText('County', { selector: 'select' })) as HTMLSelectElement
|
|
691
|
+
const cityFilter = (await canvas.findByLabelText('City', { selector: 'select' })) as HTMLSelectElement
|
|
692
|
+
|
|
693
|
+
const getFilterOptions = (select: HTMLSelectElement) =>
|
|
694
|
+
Array.from(select.options)
|
|
695
|
+
.map(o => o.value)
|
|
696
|
+
.filter(Boolean)
|
|
697
|
+
|
|
698
|
+
const getState = () => ({
|
|
699
|
+
stateOptions: getFilterOptions(stateFilter),
|
|
700
|
+
stateSelected: stateFilter.value,
|
|
701
|
+
countyOptions: getFilterOptions(countyFilter),
|
|
702
|
+
countySelected: countyFilter.value,
|
|
703
|
+
cityOptions: getFilterOptions(cityFilter),
|
|
704
|
+
citySelected: cityFilter.value,
|
|
705
|
+
chartRendered: !!canvasElement.querySelector('svg')
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
// Wait for initial load
|
|
709
|
+
await waitForOptionsToPopulate(stateFilter, 3)
|
|
710
|
+
const initialState = getState()
|
|
711
|
+
|
|
712
|
+
// Verify defaultValue is applied on initial load
|
|
713
|
+
expect(initialState.stateSelected).toBe('California')
|
|
714
|
+
expect(initialState.countySelected).toBe('Los Angeles')
|
|
715
|
+
expect(initialState.citySelected).toBe('Los Angeles')
|
|
716
|
+
expect(initialState.chartRendered).toBe(true)
|
|
717
|
+
|
|
718
|
+
// Test 1: Change state to Texas → county and city should update
|
|
719
|
+
await performAndAssert(
|
|
720
|
+
'Select state=Texas → county shows Texas counties',
|
|
721
|
+
getState,
|
|
722
|
+
async () => await user.selectOptions(stateFilter, ['Texas']),
|
|
723
|
+
(before, after) =>
|
|
724
|
+
after.stateSelected === 'Texas' &&
|
|
725
|
+
after.countyOptions.includes('Harris') &&
|
|
726
|
+
after.countyOptions.includes('Dallas') &&
|
|
727
|
+
after.countyOptions.includes('Bexar') &&
|
|
728
|
+
after.countyOptions.includes('Travis') &&
|
|
729
|
+
!after.countyOptions.includes('Los Angeles') &&
|
|
730
|
+
after.chartRendered
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
// Test 2: Select county → city options update
|
|
734
|
+
await performAndAssert(
|
|
735
|
+
'Select county=Harris → city shows Houston, Pasadena',
|
|
736
|
+
getState,
|
|
737
|
+
async () => await user.selectOptions(countyFilter, ['Harris']),
|
|
738
|
+
(before, after) =>
|
|
739
|
+
after.countySelected === 'Harris' &&
|
|
740
|
+
after.cityOptions.includes('Houston') &&
|
|
741
|
+
after.cityOptions.includes('Pasadena') &&
|
|
742
|
+
after.cityOptions.length === 2 &&
|
|
743
|
+
after.chartRendered
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
// Test 3: Change state back to California → verify cascade updates
|
|
747
|
+
await performAndAssert(
|
|
748
|
+
'Select state=California → filters reset to California data',
|
|
749
|
+
getState,
|
|
750
|
+
async () => await user.selectOptions(stateFilter, ['California']),
|
|
751
|
+
(before, after) =>
|
|
752
|
+
after.stateSelected === 'California' &&
|
|
753
|
+
after.countyOptions.includes('Los Angeles') &&
|
|
754
|
+
after.countyOptions.includes('San Diego') &&
|
|
755
|
+
after.countyOptions.includes('Orange') &&
|
|
756
|
+
after.chartRendered
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
// Test 4: Select Orange county → verify city options
|
|
760
|
+
await performAndAssert(
|
|
761
|
+
'Select county=Orange → city shows Anaheim, Santa Ana, Irvine',
|
|
762
|
+
getState,
|
|
763
|
+
async () => await user.selectOptions(countyFilter, ['Orange']),
|
|
764
|
+
(before, after) =>
|
|
765
|
+
after.countySelected === 'Orange' &&
|
|
766
|
+
after.cityOptions.includes('Anaheim') &&
|
|
767
|
+
after.cityOptions.includes('Santa Ana') &&
|
|
768
|
+
after.cityOptions.includes('Irvine') &&
|
|
769
|
+
after.cityOptions.length === 3 &&
|
|
770
|
+
!after.cityOptions.includes('Los Angeles') &&
|
|
771
|
+
after.chartRendered
|
|
772
|
+
)
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export const Nested_Dropdown_With_Parent_Child: Story = {
|
|
777
|
+
args: {
|
|
778
|
+
config: NestedParentChildFilters as unknown as Config,
|
|
779
|
+
isEditor: false
|
|
780
|
+
},
|
|
781
|
+
parameters: {
|
|
782
|
+
docs: {
|
|
783
|
+
description: {
|
|
784
|
+
story:
|
|
785
|
+
'Demonstrates nested dropdown filters (Year/Quarter subGrouping) with parent-child relationships. The Year/Quarter filter depends on Region selection, and both Year and Quarter defaultValue properties are respected on initial load.'
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
play: async ({ canvasElement }) => {
|
|
790
|
+
const canvas = within(canvasElement)
|
|
791
|
+
const user = userEvent.setup()
|
|
792
|
+
|
|
793
|
+
// Get filter dropdowns
|
|
794
|
+
const regionFilter = (await canvas.findByLabelText('Region', { selector: 'select' })) as HTMLSelectElement
|
|
795
|
+
const yearQuarterFilter = (await canvas.findByLabelText('Year and Quarter', {
|
|
796
|
+
selector: 'select'
|
|
797
|
+
})) as HTMLSelectElement
|
|
798
|
+
|
|
799
|
+
const getFilterOptions = (select: HTMLSelectElement) =>
|
|
800
|
+
Array.from(select.options)
|
|
801
|
+
.map(o => o.text)
|
|
802
|
+
.filter(Boolean)
|
|
803
|
+
|
|
804
|
+
const getState = () => ({
|
|
805
|
+
regionOptions: getFilterOptions(regionFilter),
|
|
806
|
+
regionSelected: regionFilter.value,
|
|
807
|
+
yearQuarterOptions: getFilterOptions(yearQuarterFilter),
|
|
808
|
+
yearQuarterSelected: yearQuarterFilter.value,
|
|
809
|
+
chartRendered: !!canvasElement.querySelector('svg')
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
// Wait for initial load
|
|
813
|
+
await waitForOptionsToPopulate(regionFilter, 4)
|
|
814
|
+
const initialState = getState()
|
|
815
|
+
|
|
816
|
+
// Verify defaultValue is applied on initial load (North region, 2023 Q2)
|
|
817
|
+
expect(initialState.regionSelected).toBe('North')
|
|
818
|
+
expect(initialState.yearQuarterSelected).toContain('2023')
|
|
819
|
+
expect(initialState.yearQuarterSelected).toContain('Q2')
|
|
820
|
+
expect(initialState.chartRendered).toBe(true)
|
|
821
|
+
|
|
822
|
+
// Test 1: Change region to South → year options should update based on available data
|
|
823
|
+
await performAndAssert(
|
|
824
|
+
'Select region=South → year/quarter filter updates',
|
|
825
|
+
getState,
|
|
826
|
+
async () => await user.selectOptions(regionFilter, ['South']),
|
|
827
|
+
(before, after) =>
|
|
828
|
+
after.regionSelected === 'South' &&
|
|
829
|
+
after.yearQuarterOptions.some(opt => opt.includes('2022')) &&
|
|
830
|
+
after.yearQuarterOptions.some(opt => opt.includes('2023')) &&
|
|
831
|
+
after.yearQuarterOptions.some(opt => opt.includes('2024')) &&
|
|
832
|
+
after.chartRendered
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
// Test 2: Change region to East → should only show 2023 and 2024 (no 2022 data for East)
|
|
836
|
+
await performAndAssert(
|
|
837
|
+
'Select region=East → year shows only 2023, 2024',
|
|
838
|
+
getState,
|
|
839
|
+
async () => await user.selectOptions(regionFilter, ['East']),
|
|
840
|
+
(before, after) =>
|
|
841
|
+
after.regionSelected === 'East' &&
|
|
842
|
+
after.yearQuarterOptions.some(opt => opt.includes('2023')) &&
|
|
843
|
+
after.yearQuarterOptions.some(opt => opt.includes('2024')) &&
|
|
844
|
+
!after.yearQuarterOptions.some(opt => opt.includes('2022')) &&
|
|
845
|
+
after.chartRendered
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
// Test 3: Change region to West → should only show 2023 Q3-Q4 and 2024
|
|
849
|
+
await performAndAssert(
|
|
850
|
+
'Select region=West → limited year/quarter options',
|
|
851
|
+
getState,
|
|
852
|
+
async () => await user.selectOptions(regionFilter, ['West']),
|
|
853
|
+
(before, after) =>
|
|
854
|
+
after.regionSelected === 'West' &&
|
|
855
|
+
(after.yearQuarterOptions.some(opt => opt.includes('2023')) ||
|
|
856
|
+
after.yearQuarterOptions.some(opt => opt.includes('2024'))) &&
|
|
857
|
+
!after.yearQuarterOptions.some(opt => opt.includes('2022')) &&
|
|
858
|
+
after.chartRendered
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
// Test 4: Change back to North → verify all years available again
|
|
862
|
+
await performAndAssert(
|
|
863
|
+
'Select region=North → all years (2022-2024) available',
|
|
864
|
+
getState,
|
|
865
|
+
async () => await user.selectOptions(regionFilter, ['North']),
|
|
866
|
+
(before, after) =>
|
|
867
|
+
after.regionSelected === 'North' &&
|
|
868
|
+
after.yearQuarterOptions.some(opt => opt.includes('2022')) &&
|
|
869
|
+
after.yearQuarterOptions.some(opt => opt.includes('2023')) &&
|
|
870
|
+
after.yearQuarterOptions.some(opt => opt.includes('2024')) &&
|
|
871
|
+
after.chartRendered
|
|
872
|
+
)
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
492
876
|
export default meta
|