@carto/ps-react-ui 4.3.3 → 4.3.4
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/dist/components.js +3 -3
- package/dist/components.js.map +1 -1
- package/dist/{lasso-tool-BwRzEW7k.js → lasso-tool-wFqOD6wk.js} +13 -13
- package/dist/lasso-tool-wFqOD6wk.js.map +1 -0
- package/dist/types/components/common-types.d.ts +41 -0
- package/dist/types/components/types.d.ts +1 -1
- package/dist/widgets/actions.js +1 -1
- package/dist/widgets/bar.js +1 -1
- package/dist/widgets/category.js +1 -1
- package/dist/widgets/formula.js +1 -1
- package/dist/widgets/histogram.js +1 -1
- package/dist/widgets/markdown.js +1 -1
- package/dist/widgets/pie.js +1 -1
- package/dist/widgets/scatterplot.js +1 -1
- package/dist/widgets/spread.js +1 -1
- package/dist/widgets/table.js +1 -1
- package/dist/widgets/timeseries.js +1 -1
- package/dist/widgets/toolbar-actions.js.map +1 -1
- package/dist/widgets/wrapper.js +1 -1
- package/package.json +8 -3
- package/src/components/basemaps/basemaps.test.tsx +196 -0
- package/src/components/basemaps/basemaps.tsx +128 -0
- package/src/components/basemaps/const.ts +13 -0
- package/src/components/basemaps/group-wrapper.test.tsx +38 -0
- package/src/components/basemaps/group-wrapper.tsx +28 -0
- package/src/components/basemaps/group.test.tsx +52 -0
- package/src/components/basemaps/group.tsx +42 -0
- package/src/components/basemaps/header.test.tsx +54 -0
- package/src/components/basemaps/header.tsx +36 -0
- package/src/components/basemaps/styles.ts +76 -0
- package/src/components/basemaps/types.ts +30 -0
- package/src/components/common-types.ts +1 -0
- package/src/components/geolocation-controls/const.ts +6 -0
- package/src/components/geolocation-controls/geolocation-controls.test.tsx +133 -0
- package/src/components/geolocation-controls/geolocation-controls.tsx +95 -0
- package/src/components/geolocation-controls/types.ts +17 -0
- package/src/components/index.ts +64 -0
- package/src/components/lasso-tool/chip.tsx +37 -0
- package/src/components/lasso-tool/const.tsx +70 -0
- package/src/components/lasso-tool/icons.tsx +91 -0
- package/src/components/lasso-tool/lasso-tool-inline.test.tsx +168 -0
- package/src/components/lasso-tool/lasso-tool-inline.tsx +245 -0
- package/src/components/lasso-tool/lasso-tool.test.tsx +212 -0
- package/src/components/lasso-tool/lasso-tool.tsx +479 -0
- package/src/components/lasso-tool/styles.ts +143 -0
- package/src/components/lasso-tool/types.ts +114 -0
- package/src/components/list-data/list-data-skeleton.test.tsx +10 -0
- package/src/components/list-data/list-data-skeleton.tsx +40 -0
- package/src/components/list-data/list-data.test.tsx +94 -0
- package/src/components/list-data/list-data.tsx +106 -0
- package/src/components/list-data/styles.ts +37 -0
- package/src/components/list-data/types.ts +25 -0
- package/src/components/measurement-tools/const.tsx +108 -0
- package/src/components/measurement-tools/icons.tsx +54 -0
- package/src/components/measurement-tools/measurement-tools.test.tsx +165 -0
- package/src/components/measurement-tools/measurement-tools.tsx +443 -0
- package/src/components/measurement-tools/styles.ts +91 -0
- package/src/components/measurement-tools/types.ts +77 -0
- package/src/components/no-data-alert/no-data-alert.test.tsx +31 -0
- package/src/components/no-data-alert/no-data-alert.tsx +59 -0
- package/src/components/responsive-drawer/responsive-drawer.test.tsx +91 -0
- package/src/components/responsive-drawer/responsive-drawer.tsx +53 -0
- package/src/components/smart-tooltip/smart-tooltip.test.tsx +168 -0
- package/src/components/smart-tooltip/smart-tooltip.tsx +40 -0
- package/src/components/tooltip/tooltip.test.tsx +86 -0
- package/src/components/tooltip/tooltip.tsx +30 -0
- package/src/components/types.ts +1 -0
- package/src/components/zoom-controls/styles.ts +27 -0
- package/src/components/zoom-controls/types.ts +19 -0
- package/src/components/zoom-controls/zoom-controls.test.tsx +101 -0
- package/src/components/zoom-controls/zoom-controls.tsx +114 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/use-debounce.ts +55 -0
- package/src/hooks/use-skeleton.test.tsx +32 -0
- package/src/hooks/use-skeleton.ts +19 -0
- package/src/hooks/use-widget-ref.ts +33 -0
- package/src/widgets/README.md +277 -0
- package/src/widgets/_shared/chart-config/config-factory.ts +67 -0
- package/src/widgets/_shared/chart-config/csv-modifiers.ts +56 -0
- package/src/widgets/_shared/chart-config/index.ts +21 -0
- package/src/widgets/_shared/chart-config/option-builders.ts +203 -0
- package/src/widgets/_shared/skeleton/index.ts +5 -0
- package/src/widgets/_shared/skeleton/styles.ts +20 -0
- package/src/widgets/actions/change-column/change-column-icon.tsx +10 -0
- package/src/widgets/actions/change-column/change-column.test.tsx +163 -0
- package/src/widgets/actions/change-column/change-column.tsx +141 -0
- package/src/widgets/actions/change-column/sortable-column-item.tsx +49 -0
- package/src/widgets/actions/change-column/types.ts +20 -0
- package/src/widgets/actions/download/download.test.tsx +322 -0
- package/src/widgets/actions/download/download.tsx +118 -0
- package/src/widgets/actions/download/exports.test.tsx +275 -0
- package/src/widgets/actions/download/exports.tsx +103 -0
- package/src/widgets/actions/download/types.ts +21 -0
- package/src/widgets/actions/fullscreen/fullscreen.test.tsx +269 -0
- package/src/widgets/actions/fullscreen/fullscreen.tsx +82 -0
- package/src/widgets/actions/fullscreen/styles.ts +17 -0
- package/src/widgets/actions/fullscreen/types.ts +27 -0
- package/src/widgets/actions/index.ts +51 -0
- package/src/widgets/actions/lock-selection/lock-selection.test.tsx +186 -0
- package/src/widgets/actions/lock-selection/lock-selection.tsx +133 -0
- package/src/widgets/actions/lock-selection/types.ts +41 -0
- package/src/widgets/actions/relative-data/relative-data.test.tsx +267 -0
- package/src/widgets/actions/relative-data/relative-data.tsx +133 -0
- package/src/widgets/actions/relative-data/style.ts +9 -0
- package/src/widgets/actions/relative-data/types.ts +31 -0
- package/src/widgets/actions/relative-data/utils.test.ts +223 -0
- package/src/widgets/actions/relative-data/utils.ts +58 -0
- package/src/widgets/actions/searcher/searcher-toggle.test.tsx +354 -0
- package/src/widgets/actions/searcher/searcher-toggle.tsx +73 -0
- package/src/widgets/actions/searcher/searcher.tsx +205 -0
- package/src/widgets/actions/searcher/types.ts +72 -0
- package/src/widgets/actions/shared/styles.ts +12 -0
- package/src/widgets/actions/stack-toggle/grouped-bar-chart-icon.tsx +14 -0
- package/src/widgets/actions/stack-toggle/stack-toggle.test.tsx +270 -0
- package/src/widgets/actions/stack-toggle/stack-toggle.tsx +146 -0
- package/src/widgets/actions/stack-toggle/types.ts +29 -0
- package/src/widgets/actions/zoom-toggle/index.ts +2 -0
- package/src/widgets/actions/zoom-toggle/style.ts +14 -0
- package/src/widgets/actions/zoom-toggle/types.ts +44 -0
- package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +186 -0
- package/src/widgets/bar/config.ts +122 -0
- package/src/widgets/bar/index.ts +9 -0
- package/src/widgets/bar/skeleton.tsx +60 -0
- package/src/widgets/bar/style.ts +33 -0
- package/src/widgets/bar/types.ts +16 -0
- package/src/widgets/category/category-ui.test.tsx +399 -0
- package/src/widgets/category/category-ui.tsx +156 -0
- package/src/widgets/category/components/category-bar.tsx +28 -0
- package/src/widgets/category/components/category-legend.tsx +30 -0
- package/src/widgets/category/components/category-row-multi.tsx +50 -0
- package/src/widgets/category/components/category-row-other.tsx +23 -0
- package/src/widgets/category/components/category-row-single.tsx +47 -0
- package/src/widgets/category/components/index.ts +14 -0
- package/src/widgets/category/config.ts +85 -0
- package/src/widgets/category/index.ts +30 -0
- package/src/widgets/category/skeleton.tsx +24 -0
- package/src/widgets/category/style.ts +133 -0
- package/src/widgets/category/types.ts +40 -0
- package/src/widgets/echart/const.ts +1 -0
- package/src/widgets/echart/echart-ui.test.tsx +519 -0
- package/src/widgets/echart/echart-ui.tsx +80 -0
- package/src/widgets/echart/echart.test.tsx +537 -0
- package/src/widgets/echart/echart.tsx +60 -0
- package/src/widgets/echart/index.ts +16 -0
- package/src/widgets/echart/options.ts +53 -0
- package/src/widgets/echart/types.ts +41 -0
- package/src/widgets/echart/utils.ts +169 -0
- package/src/widgets/error/error.test.tsx +331 -0
- package/src/widgets/error/error.tsx +40 -0
- package/src/widgets/error/index.ts +2 -0
- package/src/widgets/error/types.ts +14 -0
- package/src/widgets/formula/components/item.test.tsx +249 -0
- package/src/widgets/formula/components/item.tsx +18 -0
- package/src/widgets/formula/components/prefix.test.tsx +341 -0
- package/src/widgets/formula/components/prefix.tsx +18 -0
- package/src/widgets/formula/components/row.test.tsx +364 -0
- package/src/widgets/formula/components/row.tsx +21 -0
- package/src/widgets/formula/components/series.tsx +34 -0
- package/src/widgets/formula/components/suffix.test.tsx +383 -0
- package/src/widgets/formula/components/suffix.tsx +28 -0
- package/src/widgets/formula/components/value.test.tsx +329 -0
- package/src/widgets/formula/components/value.tsx +29 -0
- package/src/widgets/formula/config.ts +27 -0
- package/src/widgets/formula/formula-ui.test.tsx +399 -0
- package/src/widgets/formula/formula-ui.tsx +27 -0
- package/src/widgets/formula/index.ts +24 -0
- package/src/widgets/formula/serializer.test.tsx +144 -0
- package/src/widgets/formula/serializer.ts +28 -0
- package/src/widgets/formula/skeleton.tsx +10 -0
- package/src/widgets/formula/style.ts +23 -0
- package/src/widgets/formula/types.ts +50 -0
- package/src/widgets/histogram/config.ts +143 -0
- package/src/widgets/histogram/index.ts +8 -0
- package/src/widgets/histogram/skeleton.tsx +52 -0
- package/src/widgets/histogram/style.ts +8 -0
- package/src/widgets/histogram/types.ts +17 -0
- package/src/widgets/index.ts +25 -0
- package/src/widgets/loader/index.ts +4 -0
- package/src/widgets/loader/loader.tsx +70 -0
- package/src/widgets/loader/types.ts +11 -0
- package/src/widgets/loader/utils.test.ts +112 -0
- package/src/widgets/loader/utils.ts +35 -0
- package/src/widgets/markdown/config.ts +18 -0
- package/src/widgets/markdown/index.ts +14 -0
- package/src/widgets/markdown/markdown-ui.test.tsx +341 -0
- package/src/widgets/markdown/markdown-ui.tsx +52 -0
- package/src/widgets/markdown/markdown.tsx +20 -0
- package/src/widgets/markdown/skeleton.tsx +12 -0
- package/src/widgets/markdown/style.ts +28 -0
- package/src/widgets/markdown/types.ts +28 -0
- package/src/widgets/no-data/index.ts +2 -0
- package/src/widgets/no-data/no-data.test.tsx +447 -0
- package/src/widgets/no-data/no-data.tsx +116 -0
- package/src/widgets/no-data/style.ts +18 -0
- package/src/widgets/no-data/types.ts +72 -0
- package/src/widgets/note/index.ts +2 -0
- package/src/widgets/note/note.test.tsx +391 -0
- package/src/widgets/note/note.tsx +114 -0
- package/src/widgets/note/style.ts +29 -0
- package/src/widgets/note/types.ts +9 -0
- package/src/widgets/pie/config.ts +177 -0
- package/src/widgets/pie/index.ts +8 -0
- package/src/widgets/pie/skeleton.tsx +70 -0
- package/src/widgets/pie/style.ts +8 -0
- package/src/widgets/pie/types.ts +16 -0
- package/src/widgets/range/components/range-item.tsx +213 -0
- package/src/widgets/range/config.ts +10 -0
- package/src/widgets/range/index.ts +16 -0
- package/src/widgets/range/range-ui.test.tsx +203 -0
- package/src/widgets/range/range-ui.tsx +11 -0
- package/src/widgets/range/serializer.test.ts +70 -0
- package/src/widgets/range/serializer.ts +27 -0
- package/src/widgets/range/skeleton.tsx +14 -0
- package/src/widgets/range/style.ts +37 -0
- package/src/widgets/range/types.ts +39 -0
- package/src/widgets/scatterplot/config.ts +138 -0
- package/src/widgets/scatterplot/index.ts +8 -0
- package/src/widgets/scatterplot/skeleton.tsx +59 -0
- package/src/widgets/scatterplot/style.ts +21 -0
- package/src/widgets/scatterplot/types.ts +17 -0
- package/src/widgets/selection-summary/index.ts +6 -0
- package/src/widgets/selection-summary/selection-summary.tsx +46 -0
- package/src/widgets/selection-summary/style.ts +10 -0
- package/src/widgets/selection-summary/types.ts +14 -0
- package/src/widgets/skeleton-loader/index.ts +2 -0
- package/src/widgets/skeleton-loader/skeleton-loader.test.tsx +139 -0
- package/src/widgets/skeleton-loader/skeleton-loader.tsx +28 -0
- package/src/widgets/skeleton-loader/types.ts +8 -0
- package/src/widgets/spread/components/max-value.tsx +29 -0
- package/src/widgets/spread/components/min-value.tsx +29 -0
- package/src/widgets/spread/components/separator.tsx +6 -0
- package/src/widgets/spread/config.ts +34 -0
- package/src/widgets/spread/index.ts +23 -0
- package/src/widgets/spread/skeleton.tsx +10 -0
- package/src/widgets/spread/spread-ui.test.tsx +368 -0
- package/src/widgets/spread/spread-ui.tsx +29 -0
- package/src/widgets/spread/style.ts +22 -0
- package/src/widgets/spread/types.ts +47 -0
- package/src/widgets/stores/index.ts +9 -0
- package/src/widgets/stores/types.ts +192 -0
- package/src/widgets/stores/widget-store.test.ts +601 -0
- package/src/widgets/stores/widget-store.ts +239 -0
- package/src/widgets/subheader/index.ts +3 -0
- package/src/widgets/subheader/style.ts +20 -0
- package/src/widgets/subheader/subheader.test.tsx +45 -0
- package/src/widgets/subheader/subheader.tsx +16 -0
- package/src/widgets/subheader/types.ts +11 -0
- package/src/widgets/table/components/cell-header.tsx +58 -0
- package/src/widgets/table/components/cell.tsx +80 -0
- package/src/widgets/table/components/index.ts +4 -0
- package/src/widgets/table/components/pagination-actions.tsx +67 -0
- package/src/widgets/table/components/pagination.tsx +41 -0
- package/src/widgets/table/components/row.tsx +60 -0
- package/src/widgets/table/config.ts +71 -0
- package/src/widgets/table/helpers.test.ts +244 -0
- package/src/widgets/table/helpers.ts +107 -0
- package/src/widgets/table/hooks/index.ts +7 -0
- package/src/widgets/table/hooks/use-pagination.test.ts +294 -0
- package/src/widgets/table/hooks/use-pagination.ts +155 -0
- package/src/widgets/table/hooks/use-selection.test.ts +504 -0
- package/src/widgets/table/hooks/use-selection.ts +189 -0
- package/src/widgets/table/hooks/use-sort.test.ts +296 -0
- package/src/widgets/table/hooks/use-sort.ts +138 -0
- package/src/widgets/table/index.ts +53 -0
- package/src/widgets/table/serializer.ts +54 -0
- package/src/widgets/table/skeleton.tsx +48 -0
- package/src/widgets/table/style.ts +34 -0
- package/src/widgets/table/table-ui.tsx +64 -0
- package/src/widgets/table/types.ts +223 -0
- package/src/widgets/timeseries/config.ts +135 -0
- package/src/widgets/timeseries/index.ts +8 -0
- package/src/widgets/timeseries/skeleton.tsx +55 -0
- package/src/widgets/timeseries/style.ts +36 -0
- package/src/widgets/timeseries/types.ts +17 -0
- package/src/widgets/toolbar-actions/index.ts +6 -0
- package/src/widgets/toolbar-actions/styles.ts +38 -0
- package/src/widgets/toolbar-actions/toolbar-actions.test.tsx +691 -0
- package/src/widgets/toolbar-actions/toolbar-actions.tsx +145 -0
- package/src/widgets/toolbar-actions/types.ts +60 -0
- package/src/widgets/wrapper/components/actions.test.tsx +101 -0
- package/src/widgets/wrapper/components/actions.tsx +30 -0
- package/src/widgets/wrapper/components/options.test.tsx +323 -0
- package/src/widgets/wrapper/components/options.tsx +73 -0
- package/src/widgets/wrapper/components/title.test.tsx +126 -0
- package/src/widgets/wrapper/components/title.tsx +32 -0
- package/src/widgets/wrapper/index.ts +16 -0
- package/src/widgets/wrapper/styles.ts +98 -0
- package/src/widgets/wrapper/types.ts +55 -0
- package/src/widgets/wrapper/wrapper-ui.test.tsx +232 -0
- package/src/widgets/wrapper/wrapper-ui.tsx +57 -0
- package/src/widgets/wrapper/wrapper.test.tsx +365 -0
- package/src/widgets/wrapper/wrapper.tsx +50 -0
- package/dist/lasso-tool-BwRzEW7k.js.map +0 -1
- package/dist/types/common/common.d.ts +0 -3
- package/dist/types/common/index.d.ts +0 -26
- package/dist/types/common/lasso-tools.d.ts +0 -36
- package/dist/types/common/measurement-tools.d.ts +0 -65
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
3
|
+
import { Download } from './download'
|
|
4
|
+
import { useWidgetStore } from '../../../widgets'
|
|
5
|
+
import type { DownloadItem } from './types'
|
|
6
|
+
|
|
7
|
+
describe('Download', () => {
|
|
8
|
+
const widgetId = 'test-download-widget'
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Clear store before each test
|
|
12
|
+
useWidgetStore.getState().clearWidgets()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('disables button when download items array is empty', () => {
|
|
16
|
+
render(<Download id={widgetId} items={[]} />)
|
|
17
|
+
const button = screen.getByRole('button', { name: 'download options' })
|
|
18
|
+
expect(button.hasAttribute('disabled')).toBeTruthy()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('renders download button when download options exist', () => {
|
|
22
|
+
const mockDownload: DownloadItem[] = [
|
|
23
|
+
{
|
|
24
|
+
id: 'csv',
|
|
25
|
+
label: 'CSV',
|
|
26
|
+
modifier: vi.fn().mockResolvedValue('data:text/csv;base64,'),
|
|
27
|
+
},
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
useWidgetStore.getState().setWidget(widgetId, {
|
|
31
|
+
data: [['a', 'b']],
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
render(<Download id={widgetId} items={mockDownload} />)
|
|
35
|
+
|
|
36
|
+
const button = screen.getByRole('button', { name: 'download options' })
|
|
37
|
+
expect(button).toBeTruthy()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('uses custom aria label when provided', () => {
|
|
41
|
+
const mockDownload: DownloadItem[] = [
|
|
42
|
+
{
|
|
43
|
+
id: 'csv',
|
|
44
|
+
label: 'CSV',
|
|
45
|
+
modifier: vi.fn().mockResolvedValue('data:text/csv;base64,'),
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
render(
|
|
50
|
+
<Download
|
|
51
|
+
id={widgetId}
|
|
52
|
+
items={mockDownload}
|
|
53
|
+
labels={{ ariaLabel: 'custom download' }}
|
|
54
|
+
/>,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const button = screen.getByRole('button', { name: 'custom download' })
|
|
58
|
+
expect(button).toBeTruthy()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('opens menu when button is clicked', async () => {
|
|
62
|
+
const mockDownload: DownloadItem[] = [
|
|
63
|
+
{
|
|
64
|
+
id: 'csv',
|
|
65
|
+
label: 'CSV',
|
|
66
|
+
modifier: vi.fn().mockResolvedValue('data:text/csv;base64,'),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'png',
|
|
70
|
+
label: 'PNG',
|
|
71
|
+
modifier: vi.fn().mockResolvedValue('data:image/png;base64,'),
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
render(<Download id={widgetId} items={mockDownload} />)
|
|
76
|
+
|
|
77
|
+
const button = screen.getByRole('button', { name: 'download options' })
|
|
78
|
+
fireEvent.click(button)
|
|
79
|
+
|
|
80
|
+
await waitFor(() => {
|
|
81
|
+
expect(screen.getByText('CSV')).toBeTruthy()
|
|
82
|
+
expect(screen.getByText('PNG')).toBeTruthy()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('renders menu items with icons when provided', async () => {
|
|
87
|
+
const mockIcon = <span data-testid='test-icon'>Icon</span>
|
|
88
|
+
const mockDownload: DownloadItem[] = [
|
|
89
|
+
{
|
|
90
|
+
id: 'csv',
|
|
91
|
+
label: 'CSV',
|
|
92
|
+
icon: mockIcon,
|
|
93
|
+
modifier: vi.fn().mockResolvedValue('data:text/csv;base64,'),
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
render(<Download id={widgetId} items={mockDownload} />)
|
|
98
|
+
|
|
99
|
+
const button = screen.getByRole('button', { name: 'download options' })
|
|
100
|
+
fireEvent.click(button)
|
|
101
|
+
|
|
102
|
+
await waitFor(() => {
|
|
103
|
+
expect(screen.getByTestId('test-icon')).toBeTruthy()
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('calls modifier and triggers download when menu item is clicked', async () => {
|
|
108
|
+
const mockModifier = vi.fn().mockResolvedValue('data:text/csv;base64,test')
|
|
109
|
+
const mockCallback = vi.fn()
|
|
110
|
+
const mockDownload: DownloadItem[] = [
|
|
111
|
+
{
|
|
112
|
+
id: 'csv',
|
|
113
|
+
label: 'CSV',
|
|
114
|
+
filename: 'test.csv',
|
|
115
|
+
modifier: mockModifier,
|
|
116
|
+
callback: mockCallback,
|
|
117
|
+
},
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
useWidgetStore.getState().setWidget(widgetId, {
|
|
121
|
+
data: [['test', 'data']],
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Mock document methods
|
|
125
|
+
const createElementSpy = vi.spyOn(document, 'createElement')
|
|
126
|
+
const appendChildSpy = vi.spyOn(document.body, 'appendChild')
|
|
127
|
+
const removeChildSpy = vi.spyOn(document.body, 'removeChild')
|
|
128
|
+
|
|
129
|
+
render(<Download id={widgetId} items={mockDownload} />)
|
|
130
|
+
|
|
131
|
+
const button = screen.getByRole('button', { name: 'download options' })
|
|
132
|
+
fireEvent.click(button)
|
|
133
|
+
|
|
134
|
+
const csvMenuItem = await screen.findByText('CSV')
|
|
135
|
+
fireEvent.click(csvMenuItem)
|
|
136
|
+
|
|
137
|
+
await waitFor(() => {
|
|
138
|
+
expect(mockModifier).toHaveBeenCalledWith([['test', 'data']])
|
|
139
|
+
expect(createElementSpy).toHaveBeenCalledWith('a')
|
|
140
|
+
expect(mockCallback).toHaveBeenCalled()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
createElementSpy.mockRestore()
|
|
144
|
+
appendChildSpy.mockRestore()
|
|
145
|
+
removeChildSpy.mockRestore()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('shows loading spinner during download', async () => {
|
|
149
|
+
const mockModifier = vi.fn().mockImplementation(
|
|
150
|
+
() =>
|
|
151
|
+
new Promise((resolve) => {
|
|
152
|
+
setTimeout(() => resolve('data:text/csv;base64,test'), 100)
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
const mockDownload: DownloadItem[] = [
|
|
157
|
+
{
|
|
158
|
+
id: 'csv',
|
|
159
|
+
label: 'CSV',
|
|
160
|
+
modifier: mockModifier,
|
|
161
|
+
},
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
render(<Download id={widgetId} items={mockDownload} />)
|
|
165
|
+
|
|
166
|
+
const button = screen.getByRole('button', { name: 'download options' })
|
|
167
|
+
fireEvent.click(button)
|
|
168
|
+
|
|
169
|
+
const csvMenuItem = await screen.findByText('CSV')
|
|
170
|
+
fireEvent.click(csvMenuItem)
|
|
171
|
+
|
|
172
|
+
// Check that loading spinner is shown during download
|
|
173
|
+
await waitFor(() => {
|
|
174
|
+
expect(screen.getByRole('progressbar')).toBeTruthy()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Wait for download to complete and spinner to disappear
|
|
178
|
+
await waitFor(
|
|
179
|
+
() => {
|
|
180
|
+
expect(screen.queryByRole('progressbar')).toBeNull()
|
|
181
|
+
},
|
|
182
|
+
{ timeout: 200 },
|
|
183
|
+
)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('handles disabled menu items', async () => {
|
|
187
|
+
const mockDownload: DownloadItem[] = [
|
|
188
|
+
{
|
|
189
|
+
id: 'csv',
|
|
190
|
+
label: 'CSV',
|
|
191
|
+
disabled: true,
|
|
192
|
+
modifier: vi.fn().mockResolvedValue('data:text/csv;base64,'),
|
|
193
|
+
},
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
render(<Download id={widgetId} items={mockDownload} />)
|
|
197
|
+
|
|
198
|
+
const button = screen.getByRole('button', { name: 'download options' })
|
|
199
|
+
fireEvent.click(button)
|
|
200
|
+
|
|
201
|
+
const csvMenuItem = await screen.findByText('CSV')
|
|
202
|
+
expect(csvMenuItem.closest('li')?.getAttribute('aria-disabled')).toBe(
|
|
203
|
+
'true',
|
|
204
|
+
)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('closes menu after clicking a menu item', async () => {
|
|
208
|
+
const mockDownload: DownloadItem[] = [
|
|
209
|
+
{
|
|
210
|
+
id: 'csv',
|
|
211
|
+
label: 'CSV',
|
|
212
|
+
modifier: vi.fn().mockResolvedValue('data:text/csv;base64,'),
|
|
213
|
+
},
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
render(<Download id={widgetId} items={mockDownload} />)
|
|
217
|
+
|
|
218
|
+
const button = screen.getByRole('button', { name: 'download options' })
|
|
219
|
+
fireEvent.click(button)
|
|
220
|
+
|
|
221
|
+
const csvMenuItem = await screen.findByText('CSV')
|
|
222
|
+
fireEvent.click(csvMenuItem)
|
|
223
|
+
|
|
224
|
+
await waitFor(() => {
|
|
225
|
+
expect(screen.queryByRole('menu')).toBeNull()
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('does not download if modifier returns undefined', async () => {
|
|
230
|
+
const mockModifier = vi.fn().mockResolvedValue(undefined)
|
|
231
|
+
const mockCallback = vi.fn()
|
|
232
|
+
const mockDownload: DownloadItem[] = [
|
|
233
|
+
{
|
|
234
|
+
id: 'csv',
|
|
235
|
+
label: 'CSV',
|
|
236
|
+
modifier: mockModifier,
|
|
237
|
+
callback: mockCallback,
|
|
238
|
+
},
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
const createElementSpy = vi.spyOn(document, 'createElement')
|
|
242
|
+
|
|
243
|
+
render(<Download id={widgetId} items={mockDownload} />)
|
|
244
|
+
|
|
245
|
+
const button = screen.getByRole('button', { name: 'download options' })
|
|
246
|
+
fireEvent.click(button)
|
|
247
|
+
|
|
248
|
+
const csvMenuItem = await screen.findByText('CSV')
|
|
249
|
+
fireEvent.click(csvMenuItem)
|
|
250
|
+
|
|
251
|
+
await waitFor(() => {
|
|
252
|
+
expect(mockModifier).toHaveBeenCalled()
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// Should not create download link (check for 'a' element specifically)
|
|
256
|
+
expect(createElementSpy).not.toHaveBeenCalledWith('a')
|
|
257
|
+
expect(mockCallback).not.toHaveBeenCalled()
|
|
258
|
+
|
|
259
|
+
createElementSpy.mockRestore()
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('stops event propagation on button click', () => {
|
|
263
|
+
const mockDownload: DownloadItem[] = [
|
|
264
|
+
{
|
|
265
|
+
id: 'csv',
|
|
266
|
+
label: 'CSV',
|
|
267
|
+
modifier: vi.fn().mockResolvedValue('data:text/csv;base64,'),
|
|
268
|
+
},
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
const parentClickHandler = vi.fn()
|
|
272
|
+
|
|
273
|
+
render(
|
|
274
|
+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
|
275
|
+
<div onClick={parentClickHandler}>
|
|
276
|
+
<Download id={widgetId} items={mockDownload} />
|
|
277
|
+
</div>,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
const button = screen.getByRole('button', { name: 'download options' })
|
|
281
|
+
fireEvent.click(button)
|
|
282
|
+
|
|
283
|
+
expect(parentClickHandler).not.toHaveBeenCalled()
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
test('uses widget id as default filename when filename is not provided', async () => {
|
|
287
|
+
const mockModifier = vi.fn().mockResolvedValue('data:text/csv;base64,test')
|
|
288
|
+
const mockDownload: DownloadItem[] = [
|
|
289
|
+
{
|
|
290
|
+
id: 'csv',
|
|
291
|
+
label: 'CSV',
|
|
292
|
+
modifier: mockModifier,
|
|
293
|
+
},
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
let capturedLink: HTMLAnchorElement | null = null
|
|
297
|
+
const originalCreateElement = document.createElement.bind(document)
|
|
298
|
+
const createElementSpy = vi
|
|
299
|
+
.spyOn(document, 'createElement')
|
|
300
|
+
.mockImplementation((tagName: string) => {
|
|
301
|
+
const element = originalCreateElement(tagName)
|
|
302
|
+
if (tagName === 'a') {
|
|
303
|
+
capturedLink = element as HTMLAnchorElement
|
|
304
|
+
}
|
|
305
|
+
return element
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
render(<Download id={widgetId} items={mockDownload} />)
|
|
309
|
+
|
|
310
|
+
const button = screen.getByRole('button', { name: 'download options' })
|
|
311
|
+
fireEvent.click(button)
|
|
312
|
+
|
|
313
|
+
const csvMenuItem = await screen.findByText('CSV')
|
|
314
|
+
fireEvent.click(csvMenuItem)
|
|
315
|
+
|
|
316
|
+
await waitFor(() => {
|
|
317
|
+
expect(capturedLink?.download).toBe(widgetId)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
createElementSpy.mockRestore()
|
|
321
|
+
})
|
|
322
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CircularProgress,
|
|
3
|
+
IconButton,
|
|
4
|
+
ListItemIcon,
|
|
5
|
+
ListItemText,
|
|
6
|
+
Menu,
|
|
7
|
+
MenuItem,
|
|
8
|
+
} from '@mui/material'
|
|
9
|
+
import type { DownloadItem, DownloadProps } from './types'
|
|
10
|
+
import { FileDownloadOutlined } from '@mui/icons-material'
|
|
11
|
+
import { useState, type MouseEvent } from 'react'
|
|
12
|
+
import { useWidgetStore } from '../../../widgets'
|
|
13
|
+
import { useShallow } from 'zustand/shallow'
|
|
14
|
+
|
|
15
|
+
export function Download({
|
|
16
|
+
id,
|
|
17
|
+
items,
|
|
18
|
+
labels = {},
|
|
19
|
+
Icon,
|
|
20
|
+
IconButtonProps,
|
|
21
|
+
}: DownloadProps) {
|
|
22
|
+
const data = useWidgetStore(useShallow((state) => state.getWidget(id)?.data))
|
|
23
|
+
|
|
24
|
+
const [isDownloading, setIsDownloading] = useState(false)
|
|
25
|
+
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
|
26
|
+
|
|
27
|
+
const handleToggle = (event: MouseEvent<HTMLElement>) => {
|
|
28
|
+
event.stopPropagation()
|
|
29
|
+
setAnchorEl(event.currentTarget)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const handleDownload = (data: string, option: DownloadItem) => {
|
|
33
|
+
const link = document.createElement('a')
|
|
34
|
+
link.href = data
|
|
35
|
+
link.download = option.filename ?? id
|
|
36
|
+
link.style.display = 'none'
|
|
37
|
+
document.body.appendChild(link)
|
|
38
|
+
link.click()
|
|
39
|
+
document.body.removeChild(link)
|
|
40
|
+
option.callback?.(link.href)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const handleClick = async (
|
|
44
|
+
event: MouseEvent<HTMLElement>,
|
|
45
|
+
option: DownloadItem,
|
|
46
|
+
) => {
|
|
47
|
+
event.stopPropagation()
|
|
48
|
+
|
|
49
|
+
setIsDownloading(true)
|
|
50
|
+
setAnchorEl(null)
|
|
51
|
+
|
|
52
|
+
const result = await option.modifier(data)
|
|
53
|
+
|
|
54
|
+
if (!result) {
|
|
55
|
+
setIsDownloading(false)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
handleDownload(result, option)
|
|
60
|
+
setIsDownloading(false)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
<IconButton
|
|
66
|
+
size='small'
|
|
67
|
+
aria-label={labels.ariaLabel ?? 'download options'}
|
|
68
|
+
aria-controls='options-menu'
|
|
69
|
+
aria-haspopup='true'
|
|
70
|
+
onClick={handleToggle}
|
|
71
|
+
{...IconButtonProps}
|
|
72
|
+
disabled={isDownloading || items.length === 0}
|
|
73
|
+
className={isDownloading || anchorEl ? 'active' : ''}
|
|
74
|
+
>
|
|
75
|
+
{isDownloading ? (
|
|
76
|
+
<CircularProgress size={18} color='inherit' />
|
|
77
|
+
) : (
|
|
78
|
+
(Icon ?? <FileDownloadOutlined />)
|
|
79
|
+
)}
|
|
80
|
+
</IconButton>
|
|
81
|
+
<Menu
|
|
82
|
+
variant='menu'
|
|
83
|
+
elevation={8}
|
|
84
|
+
anchorOrigin={{
|
|
85
|
+
vertical: 'bottom',
|
|
86
|
+
horizontal: 'right',
|
|
87
|
+
}}
|
|
88
|
+
transformOrigin={{
|
|
89
|
+
vertical: 'top',
|
|
90
|
+
horizontal: 'right',
|
|
91
|
+
}}
|
|
92
|
+
anchorEl={anchorEl}
|
|
93
|
+
open={Boolean(anchorEl)}
|
|
94
|
+
onClose={() => setAnchorEl(null)}
|
|
95
|
+
MenuListProps={{
|
|
96
|
+
sx: {
|
|
97
|
+
paddingBottom: 0,
|
|
98
|
+
},
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{items.map((option) => (
|
|
102
|
+
<MenuItem
|
|
103
|
+
key={option.id}
|
|
104
|
+
disabled={option.disabled}
|
|
105
|
+
onClick={(e) => void handleClick(e, option)}
|
|
106
|
+
>
|
|
107
|
+
{option.icon && (
|
|
108
|
+
<ListItemIcon sx={{ color: 'text.primary' }}>
|
|
109
|
+
{option.icon}
|
|
110
|
+
</ListItemIcon>
|
|
111
|
+
)}
|
|
112
|
+
<ListItemText>{option.label}</ListItemText>
|
|
113
|
+
</MenuItem>
|
|
114
|
+
))}
|
|
115
|
+
</Menu>
|
|
116
|
+
</>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { downloadToCSV, downloadToPNG } from './exports'
|
|
3
|
+
import { createRef } from 'react'
|
|
4
|
+
import type { RefObject } from 'react'
|
|
5
|
+
|
|
6
|
+
// Mock html2canvas
|
|
7
|
+
vi.mock('html2canvas', () => ({
|
|
8
|
+
default: vi.fn(),
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
describe('downloadToCSV', () => {
|
|
12
|
+
test('has correct label', () => {
|
|
13
|
+
expect(downloadToCSV.label).toBe('CSV')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('has icon', () => {
|
|
17
|
+
expect(downloadToCSV.icon).toBeTruthy()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('converts data to CSV format', async () => {
|
|
21
|
+
const data: string[][] = [
|
|
22
|
+
['Name', 'Age', 'City'],
|
|
23
|
+
['John', '30', 'New York'],
|
|
24
|
+
['Jane', '25', 'Los Angeles'],
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
const result = await downloadToCSV.modifier(data)
|
|
28
|
+
|
|
29
|
+
expect(result).toBeTruthy()
|
|
30
|
+
expect(result).toContain('blob:')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('creates blob URL from CSV data', async () => {
|
|
34
|
+
const data: string[][] = [
|
|
35
|
+
['a', 'b'],
|
|
36
|
+
['c', 'd'],
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
const result = await downloadToCSV.modifier(data)
|
|
40
|
+
|
|
41
|
+
expect(result).toBeTruthy()
|
|
42
|
+
expect(typeof result).toBe('string')
|
|
43
|
+
expect(result!.startsWith('blob:')).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('callback revokes object URL', () => {
|
|
47
|
+
const revokeObjectURLSpy = vi.spyOn(URL, 'revokeObjectURL')
|
|
48
|
+
const testUrl = 'blob:test-url'
|
|
49
|
+
|
|
50
|
+
downloadToCSV.callback?.(testUrl)
|
|
51
|
+
|
|
52
|
+
expect(revokeObjectURLSpy).toHaveBeenCalledWith(testUrl)
|
|
53
|
+
|
|
54
|
+
revokeObjectURLSpy.mockRestore()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('handles empty data array', async () => {
|
|
58
|
+
const data: unknown[][] = []
|
|
59
|
+
|
|
60
|
+
const result = await downloadToCSV.modifier(data)
|
|
61
|
+
|
|
62
|
+
expect(result).toBeTruthy()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('handles single row data', async () => {
|
|
66
|
+
const data: string[][] = [['single', 'row']]
|
|
67
|
+
|
|
68
|
+
const result = await downloadToCSV.modifier(data)
|
|
69
|
+
|
|
70
|
+
expect(result).toBeTruthy()
|
|
71
|
+
expect(result).toContain('blob:')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('handles data with special characters', async () => {
|
|
75
|
+
const data: string[][] = [
|
|
76
|
+
['Name', 'Description'],
|
|
77
|
+
['Test', 'Hello, World!'],
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
const result = await downloadToCSV.modifier(data)
|
|
81
|
+
|
|
82
|
+
expect(result).toBeTruthy()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('downloadToPNG', () => {
|
|
87
|
+
let mockRef: RefObject<HTMLDivElement>
|
|
88
|
+
let mockElement: HTMLDivElement
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
// Create a real DOM element
|
|
92
|
+
mockElement = document.createElement('div')
|
|
93
|
+
mockElement.style.width = '800px'
|
|
94
|
+
mockElement.style.height = '600px'
|
|
95
|
+
document.body.appendChild(mockElement)
|
|
96
|
+
|
|
97
|
+
mockRef = createRef() as RefObject<HTMLDivElement>
|
|
98
|
+
// Manually set the current property
|
|
99
|
+
Object.defineProperty(mockRef, 'current', {
|
|
100
|
+
writable: true,
|
|
101
|
+
value: mockElement,
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
afterEach(() => {
|
|
106
|
+
document.body.removeChild(mockElement)
|
|
107
|
+
vi.clearAllMocks()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('returns download item with correct label', () => {
|
|
111
|
+
expect(downloadToPNG.label).toBe('PNG')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('has icon', () => {
|
|
115
|
+
expect(downloadToPNG.icon).toBeTruthy()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('modifier returns undefined when ref is null', async () => {
|
|
119
|
+
const nullRef = createRef() as RefObject<HTMLDivElement>
|
|
120
|
+
|
|
121
|
+
const result = await downloadToPNG.modifier(nullRef)
|
|
122
|
+
|
|
123
|
+
expect(result).toBeUndefined()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('modifier returns undefined when ref is a function', async () => {
|
|
127
|
+
const functionRef = vi.fn() as unknown as RefObject<HTMLDivElement>
|
|
128
|
+
|
|
129
|
+
const result = await downloadToPNG.modifier(functionRef)
|
|
130
|
+
|
|
131
|
+
expect(result).toBeUndefined()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('removes toolbar and actions containers from clone', async () => {
|
|
135
|
+
const html2canvas = (await import('html2canvas')).default as ReturnType<
|
|
136
|
+
typeof vi.fn
|
|
137
|
+
>
|
|
138
|
+
|
|
139
|
+
const toolbar = document.createElement('div')
|
|
140
|
+
toolbar.className = 'widget-toolbar-container'
|
|
141
|
+
mockElement.appendChild(toolbar)
|
|
142
|
+
|
|
143
|
+
const actions = document.createElement('div')
|
|
144
|
+
actions.className = 'widget-wrapper-actions'
|
|
145
|
+
mockElement.appendChild(actions)
|
|
146
|
+
|
|
147
|
+
const mockCanvas = document.createElement('canvas')
|
|
148
|
+
mockCanvas.toDataURL = vi.fn().mockReturnValue('data:image/png;base64,test')
|
|
149
|
+
|
|
150
|
+
let cloneElement: HTMLElement | undefined
|
|
151
|
+
|
|
152
|
+
html2canvas.mockImplementation((element: HTMLElement) => {
|
|
153
|
+
cloneElement = element
|
|
154
|
+
return mockCanvas
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
await downloadToPNG.modifier(mockRef)
|
|
158
|
+
|
|
159
|
+
// The clone should not have toolbar or actions
|
|
160
|
+
expect(cloneElement?.querySelector('.widget-toolbar-container')).toBeNull()
|
|
161
|
+
expect(cloneElement?.querySelector('.widget-wrapper-actions')).toBeNull()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('does not modify original element', async () => {
|
|
165
|
+
const html2canvas = (await import('html2canvas')).default as ReturnType<
|
|
166
|
+
typeof vi.fn
|
|
167
|
+
>
|
|
168
|
+
|
|
169
|
+
const toolbar = document.createElement('div')
|
|
170
|
+
toolbar.className = 'widget-toolbar-container'
|
|
171
|
+
mockElement.appendChild(toolbar)
|
|
172
|
+
|
|
173
|
+
const actions = document.createElement('div')
|
|
174
|
+
actions.className = 'widget-wrapper-actions'
|
|
175
|
+
mockElement.appendChild(actions)
|
|
176
|
+
|
|
177
|
+
const mockCanvas = document.createElement('canvas')
|
|
178
|
+
mockCanvas.toDataURL = vi.fn().mockReturnValue('data:image/png;base64,test')
|
|
179
|
+
|
|
180
|
+
html2canvas.mockResolvedValue(mockCanvas)
|
|
181
|
+
|
|
182
|
+
await downloadToPNG.modifier(mockRef)
|
|
183
|
+
|
|
184
|
+
// Original element should still have toolbar and actions
|
|
185
|
+
expect(mockElement.querySelector('.widget-toolbar-container')).toBe(toolbar)
|
|
186
|
+
expect(mockElement.querySelector('.widget-wrapper-actions')).toBe(actions)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('calls html2canvas with correct options', async () => {
|
|
190
|
+
const html2canvas = (await import('html2canvas')).default as ReturnType<
|
|
191
|
+
typeof vi.fn
|
|
192
|
+
>
|
|
193
|
+
|
|
194
|
+
const mockCanvas = document.createElement('canvas')
|
|
195
|
+
mockCanvas.toDataURL = vi.fn().mockReturnValue('data:image/png;base64,test')
|
|
196
|
+
|
|
197
|
+
html2canvas.mockResolvedValue(mockCanvas)
|
|
198
|
+
|
|
199
|
+
await downloadToPNG.modifier(mockRef)
|
|
200
|
+
|
|
201
|
+
expect(html2canvas).toHaveBeenCalledWith(
|
|
202
|
+
mockElement,
|
|
203
|
+
expect.objectContaining({
|
|
204
|
+
useCORS: true,
|
|
205
|
+
scale: 2,
|
|
206
|
+
backgroundColor: '#fff',
|
|
207
|
+
}),
|
|
208
|
+
)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('returns PNG data URL', async () => {
|
|
212
|
+
const html2canvas = (await import('html2canvas')).default as ReturnType<
|
|
213
|
+
typeof vi.fn
|
|
214
|
+
>
|
|
215
|
+
|
|
216
|
+
const mockCanvas = document.createElement('canvas')
|
|
217
|
+
const testDataUrl = 'data:image/png;base64,testdata'
|
|
218
|
+
mockCanvas.toDataURL = vi.fn().mockReturnValue(testDataUrl)
|
|
219
|
+
|
|
220
|
+
html2canvas.mockResolvedValue(mockCanvas)
|
|
221
|
+
|
|
222
|
+
const result = await downloadToPNG.modifier(mockRef)
|
|
223
|
+
|
|
224
|
+
expect(result).toBe(testDataUrl)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('calculates dimensions from clone element', async () => {
|
|
228
|
+
const html2canvas = (await import('html2canvas')).default as ReturnType<
|
|
229
|
+
typeof vi.fn
|
|
230
|
+
>
|
|
231
|
+
|
|
232
|
+
const toolbar = document.createElement('div')
|
|
233
|
+
toolbar.className = 'widget-toolbar-container'
|
|
234
|
+
toolbar.style.height = '50px'
|
|
235
|
+
mockElement.appendChild(toolbar)
|
|
236
|
+
|
|
237
|
+
const mockCanvas = document.createElement('canvas')
|
|
238
|
+
mockCanvas.toDataURL = vi.fn().mockReturnValue('data:image/png;base64,test')
|
|
239
|
+
|
|
240
|
+
let calledElement: HTMLElement | null = null
|
|
241
|
+
|
|
242
|
+
html2canvas.mockImplementation((element) => {
|
|
243
|
+
calledElement = element as HTMLElement
|
|
244
|
+
return mockCanvas
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
await downloadToPNG.modifier(mockRef)
|
|
248
|
+
|
|
249
|
+
// Should be called with a clone (different element)
|
|
250
|
+
expect(calledElement).not.toBe(mockElement)
|
|
251
|
+
expect(html2canvas).toHaveBeenCalledWith(
|
|
252
|
+
calledElement,
|
|
253
|
+
expect.objectContaining({
|
|
254
|
+
height: expect.any(Number) as number,
|
|
255
|
+
width: expect.any(Number) as number,
|
|
256
|
+
}),
|
|
257
|
+
)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test('handles element without toolbar or actions', async () => {
|
|
261
|
+
const html2canvas = (await import('html2canvas')).default as ReturnType<
|
|
262
|
+
typeof vi.fn
|
|
263
|
+
>
|
|
264
|
+
|
|
265
|
+
const mockCanvas = document.createElement('canvas')
|
|
266
|
+
mockCanvas.toDataURL = vi.fn().mockReturnValue('data:image/png;base64,test')
|
|
267
|
+
|
|
268
|
+
html2canvas.mockResolvedValue(mockCanvas)
|
|
269
|
+
|
|
270
|
+
const result = await downloadToPNG.modifier(mockRef)
|
|
271
|
+
|
|
272
|
+
expect(result).toBeTruthy()
|
|
273
|
+
expect(result).toContain('data:image/png')
|
|
274
|
+
})
|
|
275
|
+
})
|