@opendata-ai/openchart-vue 2.0.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.
- package/README.md +98 -0
- package/dist/index.d.ts +521 -0
- package/dist/index.js +715 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
- package/src/Chart.ts +187 -0
- package/src/DataTable.ts +194 -0
- package/src/Graph.ts +179 -0
- package/src/ThemeProvider.ts +41 -0
- package/src/Visualization.ts +61 -0
- package/src/__tests__/Chart.test.ts +139 -0
- package/src/__tests__/DataTable.test.ts +131 -0
- package/src/__tests__/Graph.test.ts +133 -0
- package/src/__tests__/ThemeContext.test.ts +266 -0
- package/src/__tests__/composables.test.ts +158 -0
- package/src/__tests__/useTableState.test.ts +256 -0
- package/src/composables/useChart.ts +95 -0
- package/src/composables/useDarkMode.ts +71 -0
- package/src/composables/useGraph.ts +89 -0
- package/src/composables/useTable.ts +88 -0
- package/src/composables/useTableState.ts +80 -0
- package/src/context.ts +33 -0
- package/src/index.ts +41 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for VizThemeProvider, useVizTheme, and useVizDarkMode.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that themes and dark mode preferences cascade through the
|
|
5
|
+
* provider hierarchy and that nested providers override parent values.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ThemeConfig } from '@opendata-ai/openchart-core';
|
|
9
|
+
import { flushPromises, mount } from '@vue/test-utils';
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import { defineComponent, h } from 'vue';
|
|
12
|
+
import { useVizDarkMode, useVizTheme } from '../context';
|
|
13
|
+
import { VizThemeProvider } from '../ThemeProvider';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Test harness components
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
const ThemeConsumer = defineComponent({
|
|
20
|
+
props: {
|
|
21
|
+
testId: {
|
|
22
|
+
type: String,
|
|
23
|
+
default: 'theme-output',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
setup(props) {
|
|
27
|
+
const theme = useVizTheme();
|
|
28
|
+
return () =>
|
|
29
|
+
h(
|
|
30
|
+
'div',
|
|
31
|
+
{ 'data-testid': props.testId },
|
|
32
|
+
theme.value ? JSON.stringify(theme.value) : 'no-theme',
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const DarkModeConsumer = defineComponent({
|
|
38
|
+
props: {
|
|
39
|
+
testId: {
|
|
40
|
+
type: String,
|
|
41
|
+
default: 'darkmode-output',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
setup(props) {
|
|
45
|
+
const darkMode = useVizDarkMode();
|
|
46
|
+
return () => h('div', { 'data-testid': props.testId }, darkMode.value ?? 'undefined');
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Helpers
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
function findByTestId(wrapper: ReturnType<typeof mount>, testId: string) {
|
|
55
|
+
return wrapper.find(`[data-testid="${testId}"]`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// useVizTheme
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
describe('useVizTheme', () => {
|
|
63
|
+
it('returns undefined when used outside of a provider', async () => {
|
|
64
|
+
const wrapper = mount(ThemeConsumer);
|
|
65
|
+
await flushPromises();
|
|
66
|
+
|
|
67
|
+
const output = findByTestId(wrapper, 'theme-output');
|
|
68
|
+
expect(output.text()).toBe('no-theme');
|
|
69
|
+
wrapper.unmount();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// VizThemeProvider
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
describe('VizThemeProvider', () => {
|
|
78
|
+
it('provides theme to child components', async () => {
|
|
79
|
+
const theme: ThemeConfig = {
|
|
80
|
+
colors: { categorical: ['#ff0000', '#00ff00', '#0000ff'] },
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const wrapper = mount(VizThemeProvider, {
|
|
84
|
+
props: { theme },
|
|
85
|
+
slots: { default: () => h(ThemeConsumer) },
|
|
86
|
+
});
|
|
87
|
+
await flushPromises();
|
|
88
|
+
|
|
89
|
+
const output = findByTestId(wrapper, 'theme-output');
|
|
90
|
+
const parsed = JSON.parse(output.text());
|
|
91
|
+
expect(parsed.colors.categorical).toEqual(['#ff0000', '#00ff00', '#0000ff']);
|
|
92
|
+
wrapper.unmount();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('provides undefined theme when passed undefined', async () => {
|
|
96
|
+
const wrapper = mount(VizThemeProvider, {
|
|
97
|
+
props: { theme: undefined },
|
|
98
|
+
slots: { default: () => h(ThemeConsumer) },
|
|
99
|
+
});
|
|
100
|
+
await flushPromises();
|
|
101
|
+
|
|
102
|
+
const output = findByTestId(wrapper, 'theme-output');
|
|
103
|
+
expect(output.text()).toBe('no-theme');
|
|
104
|
+
wrapper.unmount();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('nested providers override parent theme', async () => {
|
|
108
|
+
const parentTheme: ThemeConfig = {
|
|
109
|
+
colors: { categorical: ['#111'] },
|
|
110
|
+
fonts: { family: 'Arial' },
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const childTheme: ThemeConfig = {
|
|
114
|
+
colors: { categorical: ['#222'] },
|
|
115
|
+
borderRadius: 8,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const TestApp = defineComponent({
|
|
119
|
+
setup() {
|
|
120
|
+
return () =>
|
|
121
|
+
h(VizThemeProvider, { theme: parentTheme }, () =>
|
|
122
|
+
h(VizThemeProvider, { theme: childTheme }, () => h(ThemeConsumer)),
|
|
123
|
+
);
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const wrapper = mount(TestApp);
|
|
128
|
+
await flushPromises();
|
|
129
|
+
|
|
130
|
+
const output = findByTestId(wrapper, 'theme-output');
|
|
131
|
+
const parsed = JSON.parse(output.text());
|
|
132
|
+
|
|
133
|
+
// Inner provider completely replaces the context value (no merging)
|
|
134
|
+
expect(parsed.colors.categorical).toEqual(['#222']);
|
|
135
|
+
expect(parsed.borderRadius).toBe(8);
|
|
136
|
+
// Parent-only fields are absent since inner provider replaces the value
|
|
137
|
+
expect(parsed.fonts).toBeUndefined();
|
|
138
|
+
wrapper.unmount();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('multiple consumers at different nesting levels receive correct themes', async () => {
|
|
142
|
+
const outerTheme: ThemeConfig = {
|
|
143
|
+
colors: { background: '#fff' },
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const innerTheme: ThemeConfig = {
|
|
147
|
+
colors: { background: '#000' },
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const TestApp = defineComponent({
|
|
151
|
+
setup() {
|
|
152
|
+
return () =>
|
|
153
|
+
h(VizThemeProvider, { theme: outerTheme }, () => [
|
|
154
|
+
h(ThemeConsumer, { testId: 'outer-theme' }),
|
|
155
|
+
h(VizThemeProvider, { theme: innerTheme }, () =>
|
|
156
|
+
h(ThemeConsumer, { testId: 'inner-theme' }),
|
|
157
|
+
),
|
|
158
|
+
]);
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const wrapper = mount(TestApp);
|
|
163
|
+
await flushPromises();
|
|
164
|
+
|
|
165
|
+
const outerParsed = JSON.parse(findByTestId(wrapper, 'outer-theme').text());
|
|
166
|
+
const innerParsed = JSON.parse(findByTestId(wrapper, 'inner-theme').text());
|
|
167
|
+
|
|
168
|
+
expect(outerParsed.colors.background).toBe('#fff');
|
|
169
|
+
expect(innerParsed.colors.background).toBe('#000');
|
|
170
|
+
wrapper.unmount();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('theme updates propagate to consumers', async () => {
|
|
174
|
+
const theme1: ThemeConfig = { borderRadius: 4 };
|
|
175
|
+
const theme2: ThemeConfig = { borderRadius: 12 };
|
|
176
|
+
|
|
177
|
+
const wrapper = mount(VizThemeProvider, {
|
|
178
|
+
props: { theme: theme1 },
|
|
179
|
+
slots: { default: () => h(ThemeConsumer) },
|
|
180
|
+
});
|
|
181
|
+
await flushPromises();
|
|
182
|
+
|
|
183
|
+
const parsed1 = JSON.parse(findByTestId(wrapper, 'theme-output').text());
|
|
184
|
+
expect(parsed1.borderRadius).toBe(4);
|
|
185
|
+
|
|
186
|
+
await wrapper.setProps({ theme: theme2 });
|
|
187
|
+
await flushPromises();
|
|
188
|
+
|
|
189
|
+
const parsed2 = JSON.parse(findByTestId(wrapper, 'theme-output').text());
|
|
190
|
+
expect(parsed2.borderRadius).toBe(12);
|
|
191
|
+
wrapper.unmount();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// useVizDarkMode
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
describe('useVizDarkMode', () => {
|
|
200
|
+
it('returns undefined when used outside of a provider', async () => {
|
|
201
|
+
const wrapper = mount(DarkModeConsumer);
|
|
202
|
+
await flushPromises();
|
|
203
|
+
|
|
204
|
+
const output = findByTestId(wrapper, 'darkmode-output');
|
|
205
|
+
expect(output.text()).toBe('undefined');
|
|
206
|
+
wrapper.unmount();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('returns the darkMode value from provider', async () => {
|
|
210
|
+
const wrapper = mount(VizThemeProvider, {
|
|
211
|
+
props: { theme: undefined, darkMode: 'force' },
|
|
212
|
+
slots: { default: () => h(DarkModeConsumer) },
|
|
213
|
+
});
|
|
214
|
+
await flushPromises();
|
|
215
|
+
|
|
216
|
+
const output = findByTestId(wrapper, 'darkmode-output');
|
|
217
|
+
expect(output.text()).toBe('force');
|
|
218
|
+
wrapper.unmount();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('returns undefined when provider omits darkMode', async () => {
|
|
222
|
+
const wrapper = mount(VizThemeProvider, {
|
|
223
|
+
props: { theme: undefined },
|
|
224
|
+
slots: { default: () => h(DarkModeConsumer) },
|
|
225
|
+
});
|
|
226
|
+
await flushPromises();
|
|
227
|
+
|
|
228
|
+
const output = findByTestId(wrapper, 'darkmode-output');
|
|
229
|
+
expect(output.text()).toBe('undefined');
|
|
230
|
+
wrapper.unmount();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('nested provider overrides parent darkMode', async () => {
|
|
234
|
+
const TestApp = defineComponent({
|
|
235
|
+
setup() {
|
|
236
|
+
return () =>
|
|
237
|
+
h(VizThemeProvider, { theme: undefined, darkMode: 'force' }, () =>
|
|
238
|
+
h(VizThemeProvider, { theme: undefined, darkMode: 'off' }, () => h(DarkModeConsumer)),
|
|
239
|
+
);
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const wrapper = mount(TestApp);
|
|
244
|
+
await flushPromises();
|
|
245
|
+
|
|
246
|
+
const output = findByTestId(wrapper, 'darkmode-output');
|
|
247
|
+
expect(output.text()).toBe('off');
|
|
248
|
+
wrapper.unmount();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('darkMode updates propagate to consumers', async () => {
|
|
252
|
+
const wrapper = mount(VizThemeProvider, {
|
|
253
|
+
props: { theme: undefined, darkMode: 'off' },
|
|
254
|
+
slots: { default: () => h(DarkModeConsumer) },
|
|
255
|
+
});
|
|
256
|
+
await flushPromises();
|
|
257
|
+
|
|
258
|
+
expect(findByTestId(wrapper, 'darkmode-output').text()).toBe('off');
|
|
259
|
+
|
|
260
|
+
await wrapper.setProps({ darkMode: 'force' });
|
|
261
|
+
await flushPromises();
|
|
262
|
+
|
|
263
|
+
expect(findByTestId(wrapper, 'darkmode-output').text()).toBe('force');
|
|
264
|
+
wrapper.unmount();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for useDarkMode composable.
|
|
3
|
+
*
|
|
4
|
+
* Uses thin wrapper components that expose composable state via the DOM,
|
|
5
|
+
* then asserts using mount + flushPromises.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { flushPromises, mount } from '@vue/test-utils';
|
|
9
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { defineComponent, h, ref } from 'vue';
|
|
11
|
+
import { useDarkMode } from '../composables/useDarkMode';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// useDarkMode
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const DarkModeHarness = defineComponent({
|
|
18
|
+
props: {
|
|
19
|
+
mode: {
|
|
20
|
+
type: String,
|
|
21
|
+
default: undefined,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
setup(props) {
|
|
25
|
+
const modeRef = ref(props.mode as 'auto' | 'force' | 'off' | undefined);
|
|
26
|
+
|
|
27
|
+
// Watch for prop changes (Vue test-utils setProps updates props but not our ref)
|
|
28
|
+
// We need to use a computed or watchEffect to keep in sync
|
|
29
|
+
const isDark = useDarkMode(modeRef);
|
|
30
|
+
|
|
31
|
+
return { isDark, modeRef };
|
|
32
|
+
},
|
|
33
|
+
render() {
|
|
34
|
+
return h('div', { 'data-testid': 'dark-mode' }, String(this.isDark));
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// A version of the harness that allows mode changes via exposed methods
|
|
39
|
+
const DarkModeInteractiveHarness = defineComponent({
|
|
40
|
+
props: {
|
|
41
|
+
initialMode: {
|
|
42
|
+
type: String,
|
|
43
|
+
default: undefined,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
setup(props) {
|
|
47
|
+
const modeRef = ref(props.initialMode as 'auto' | 'force' | 'off' | undefined);
|
|
48
|
+
const isDark = useDarkMode(modeRef);
|
|
49
|
+
|
|
50
|
+
return { isDark, modeRef };
|
|
51
|
+
},
|
|
52
|
+
render() {
|
|
53
|
+
return h('div', [
|
|
54
|
+
h('div', { 'data-testid': 'dark-mode' }, String(this.isDark)),
|
|
55
|
+
h(
|
|
56
|
+
'button',
|
|
57
|
+
{
|
|
58
|
+
'data-testid': 'set-force',
|
|
59
|
+
onClick: () => {
|
|
60
|
+
this.modeRef = 'force';
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
'Force',
|
|
64
|
+
),
|
|
65
|
+
h(
|
|
66
|
+
'button',
|
|
67
|
+
{
|
|
68
|
+
'data-testid': 'set-off',
|
|
69
|
+
onClick: () => {
|
|
70
|
+
this.modeRef = 'off';
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
'Off',
|
|
74
|
+
),
|
|
75
|
+
]);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('useDarkMode', () => {
|
|
80
|
+
it('returns false when no mode is provided', async () => {
|
|
81
|
+
const wrapper = mount(DarkModeHarness);
|
|
82
|
+
await flushPromises();
|
|
83
|
+
|
|
84
|
+
expect(wrapper.find('[data-testid="dark-mode"]').text()).toBe('false');
|
|
85
|
+
wrapper.unmount();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('returns false for "off" mode', async () => {
|
|
89
|
+
const wrapper = mount(DarkModeHarness, { props: { mode: 'off' } });
|
|
90
|
+
await flushPromises();
|
|
91
|
+
|
|
92
|
+
expect(wrapper.find('[data-testid="dark-mode"]').text()).toBe('false');
|
|
93
|
+
wrapper.unmount();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns true for "force" mode', async () => {
|
|
97
|
+
const wrapper = mount(DarkModeHarness, { props: { mode: 'force' } });
|
|
98
|
+
await flushPromises();
|
|
99
|
+
|
|
100
|
+
expect(wrapper.find('[data-testid="dark-mode"]').text()).toBe('true');
|
|
101
|
+
wrapper.unmount();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('reflects system preference for "auto" mode when dark', async () => {
|
|
105
|
+
const spy = vi.spyOn(window, 'matchMedia').mockImplementation(
|
|
106
|
+
(query: string) =>
|
|
107
|
+
({
|
|
108
|
+
matches: query === '(prefers-color-scheme: dark)',
|
|
109
|
+
media: query,
|
|
110
|
+
addEventListener: vi.fn(),
|
|
111
|
+
removeEventListener: vi.fn(),
|
|
112
|
+
}) as unknown as MediaQueryList,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const wrapper = mount(DarkModeHarness, { props: { mode: 'auto' } });
|
|
116
|
+
await flushPromises();
|
|
117
|
+
|
|
118
|
+
expect(wrapper.find('[data-testid="dark-mode"]').text()).toBe('true');
|
|
119
|
+
|
|
120
|
+
spy.mockRestore();
|
|
121
|
+
wrapper.unmount();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('reflects system preference for "auto" mode when light', async () => {
|
|
125
|
+
const spy = vi.spyOn(window, 'matchMedia').mockImplementation(
|
|
126
|
+
(query: string) =>
|
|
127
|
+
({
|
|
128
|
+
matches: false,
|
|
129
|
+
media: query,
|
|
130
|
+
addEventListener: vi.fn(),
|
|
131
|
+
removeEventListener: vi.fn(),
|
|
132
|
+
}) as unknown as MediaQueryList,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const wrapper = mount(DarkModeHarness, { props: { mode: 'auto' } });
|
|
136
|
+
await flushPromises();
|
|
137
|
+
|
|
138
|
+
expect(wrapper.find('[data-testid="dark-mode"]').text()).toBe('false');
|
|
139
|
+
|
|
140
|
+
spy.mockRestore();
|
|
141
|
+
wrapper.unmount();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('switches from "off" to "force" when mode changes', async () => {
|
|
145
|
+
const wrapper = mount(DarkModeInteractiveHarness, {
|
|
146
|
+
props: { initialMode: 'off' },
|
|
147
|
+
});
|
|
148
|
+
await flushPromises();
|
|
149
|
+
|
|
150
|
+
expect(wrapper.find('[data-testid="dark-mode"]').text()).toBe('false');
|
|
151
|
+
|
|
152
|
+
await wrapper.find('[data-testid="set-force"]').trigger('click');
|
|
153
|
+
await flushPromises();
|
|
154
|
+
|
|
155
|
+
expect(wrapper.find('[data-testid="dark-mode"]').text()).toBe('true');
|
|
156
|
+
wrapper.unmount();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for useTableState composable.
|
|
3
|
+
*
|
|
4
|
+
* Uses thin wrapper components that expose composable state via the DOM
|
|
5
|
+
* and trigger state changes via button clicks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { flushPromises, mount } from '@vue/test-utils';
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { defineComponent, h } from 'vue';
|
|
11
|
+
import { type UseTableStateOptions, useTableState } from '../composables/useTableState';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Test harness: renders composable state to the DOM
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const TableStateHarness = defineComponent({
|
|
18
|
+
props: {
|
|
19
|
+
initialState: {
|
|
20
|
+
type: Object,
|
|
21
|
+
default: undefined,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
setup(props) {
|
|
25
|
+
const { sort, search, page, setSort, setSearch, setPage, resetState } = useTableState(
|
|
26
|
+
props.initialState as UseTableStateOptions | undefined,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return { sort, search, page, setSort, setSearch, setPage, resetState };
|
|
30
|
+
},
|
|
31
|
+
render() {
|
|
32
|
+
return h('div', [
|
|
33
|
+
h(
|
|
34
|
+
'span',
|
|
35
|
+
{ 'data-testid': 'sort' },
|
|
36
|
+
this.sort ? `${this.sort.column}:${this.sort.direction}` : 'null',
|
|
37
|
+
),
|
|
38
|
+
h('span', { 'data-testid': 'search' }, this.search),
|
|
39
|
+
h('span', { 'data-testid': 'page' }, String(this.page)),
|
|
40
|
+
h(
|
|
41
|
+
'button',
|
|
42
|
+
{
|
|
43
|
+
'data-testid': 'set-sort-name-asc',
|
|
44
|
+
onClick: () => this.setSort({ column: 'name', direction: 'asc' }),
|
|
45
|
+
},
|
|
46
|
+
'sort name asc',
|
|
47
|
+
),
|
|
48
|
+
h(
|
|
49
|
+
'button',
|
|
50
|
+
{
|
|
51
|
+
'data-testid': 'set-sort-age-desc',
|
|
52
|
+
onClick: () => this.setSort({ column: 'age', direction: 'desc' }),
|
|
53
|
+
},
|
|
54
|
+
'sort age desc',
|
|
55
|
+
),
|
|
56
|
+
h(
|
|
57
|
+
'button',
|
|
58
|
+
{
|
|
59
|
+
'data-testid': 'clear-sort',
|
|
60
|
+
onClick: () => this.setSort(null),
|
|
61
|
+
},
|
|
62
|
+
'clear sort',
|
|
63
|
+
),
|
|
64
|
+
h(
|
|
65
|
+
'button',
|
|
66
|
+
{
|
|
67
|
+
'data-testid': 'set-search',
|
|
68
|
+
onClick: () => this.setSearch('filter text'),
|
|
69
|
+
},
|
|
70
|
+
'set search',
|
|
71
|
+
),
|
|
72
|
+
h(
|
|
73
|
+
'button',
|
|
74
|
+
{
|
|
75
|
+
'data-testid': 'set-page-5',
|
|
76
|
+
onClick: () => this.setPage(5),
|
|
77
|
+
},
|
|
78
|
+
'page 5',
|
|
79
|
+
),
|
|
80
|
+
h(
|
|
81
|
+
'button',
|
|
82
|
+
{
|
|
83
|
+
'data-testid': 'reset',
|
|
84
|
+
onClick: () => this.resetState(),
|
|
85
|
+
},
|
|
86
|
+
'reset',
|
|
87
|
+
),
|
|
88
|
+
]);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Helpers
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
async function mountHarness(initialState?: UseTableStateOptions) {
|
|
97
|
+
const wrapper = mount(TableStateHarness, {
|
|
98
|
+
props: initialState ? { initialState } : {},
|
|
99
|
+
});
|
|
100
|
+
await flushPromises();
|
|
101
|
+
return wrapper;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getState(wrapper: ReturnType<typeof mount>) {
|
|
105
|
+
return {
|
|
106
|
+
sort: wrapper.find('[data-testid="sort"]').text(),
|
|
107
|
+
search: wrapper.find('[data-testid="search"]').text(),
|
|
108
|
+
page: wrapper.find('[data-testid="page"]').text(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Tests
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
describe('useTableState', () => {
|
|
117
|
+
// -------------------------------------------------------------------------
|
|
118
|
+
// Default initial state
|
|
119
|
+
// -------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
it('initializes with default values when no options provided', async () => {
|
|
122
|
+
const wrapper = await mountHarness();
|
|
123
|
+
const state = getState(wrapper);
|
|
124
|
+
|
|
125
|
+
expect(state.sort).toBe('null');
|
|
126
|
+
expect(state.search).toBe('');
|
|
127
|
+
expect(state.page).toBe('0');
|
|
128
|
+
wrapper.unmount();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// -------------------------------------------------------------------------
|
|
132
|
+
// Custom initial state
|
|
133
|
+
// -------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
it('initializes with provided sort state', async () => {
|
|
136
|
+
const wrapper = await mountHarness({
|
|
137
|
+
sort: { column: 'name', direction: 'asc' },
|
|
138
|
+
});
|
|
139
|
+
expect(getState(wrapper).sort).toBe('name:asc');
|
|
140
|
+
wrapper.unmount();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('initializes with provided search string', async () => {
|
|
144
|
+
const wrapper = await mountHarness({ search: 'hello' });
|
|
145
|
+
expect(getState(wrapper).search).toBe('hello');
|
|
146
|
+
wrapper.unmount();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('initializes with provided page number', async () => {
|
|
150
|
+
const wrapper = await mountHarness({ page: 3 });
|
|
151
|
+
expect(getState(wrapper).page).toBe('3');
|
|
152
|
+
wrapper.unmount();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// -------------------------------------------------------------------------
|
|
156
|
+
// State setters
|
|
157
|
+
// -------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
it('setSort updates the sort state', async () => {
|
|
160
|
+
const wrapper = await mountHarness();
|
|
161
|
+
|
|
162
|
+
await wrapper.find('[data-testid="set-sort-age-desc"]').trigger('click');
|
|
163
|
+
await flushPromises();
|
|
164
|
+
|
|
165
|
+
expect(getState(wrapper).sort).toBe('age:desc');
|
|
166
|
+
wrapper.unmount();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('setSort can clear sort by passing null', async () => {
|
|
170
|
+
const wrapper = await mountHarness({
|
|
171
|
+
sort: { column: 'name', direction: 'asc' },
|
|
172
|
+
});
|
|
173
|
+
expect(getState(wrapper).sort).toBe('name:asc');
|
|
174
|
+
|
|
175
|
+
await wrapper.find('[data-testid="clear-sort"]').trigger('click');
|
|
176
|
+
await flushPromises();
|
|
177
|
+
|
|
178
|
+
expect(getState(wrapper).sort).toBe('null');
|
|
179
|
+
wrapper.unmount();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('setSearch updates the search query', async () => {
|
|
183
|
+
const wrapper = await mountHarness();
|
|
184
|
+
|
|
185
|
+
await wrapper.find('[data-testid="set-search"]').trigger('click');
|
|
186
|
+
await flushPromises();
|
|
187
|
+
|
|
188
|
+
expect(getState(wrapper).search).toBe('filter text');
|
|
189
|
+
wrapper.unmount();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('setPage updates the current page', async () => {
|
|
193
|
+
const wrapper = await mountHarness();
|
|
194
|
+
|
|
195
|
+
await wrapper.find('[data-testid="set-page-5"]').trigger('click');
|
|
196
|
+
await flushPromises();
|
|
197
|
+
|
|
198
|
+
expect(getState(wrapper).page).toBe('5');
|
|
199
|
+
wrapper.unmount();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// -------------------------------------------------------------------------
|
|
203
|
+
// resetState
|
|
204
|
+
// -------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
it('resetState restores default initial values', async () => {
|
|
207
|
+
const wrapper = await mountHarness();
|
|
208
|
+
|
|
209
|
+
// Change all values
|
|
210
|
+
await wrapper.find('[data-testid="set-sort-name-asc"]').trigger('click');
|
|
211
|
+
await wrapper.find('[data-testid="set-search"]').trigger('click');
|
|
212
|
+
await wrapper.find('[data-testid="set-page-5"]').trigger('click');
|
|
213
|
+
await flushPromises();
|
|
214
|
+
|
|
215
|
+
expect(getState(wrapper).sort).toBe('name:asc');
|
|
216
|
+
expect(getState(wrapper).search).toBe('filter text');
|
|
217
|
+
expect(getState(wrapper).page).toBe('5');
|
|
218
|
+
|
|
219
|
+
// Reset
|
|
220
|
+
await wrapper.find('[data-testid="reset"]').trigger('click');
|
|
221
|
+
await flushPromises();
|
|
222
|
+
|
|
223
|
+
expect(getState(wrapper).sort).toBe('null');
|
|
224
|
+
expect(getState(wrapper).search).toBe('');
|
|
225
|
+
expect(getState(wrapper).page).toBe('0');
|
|
226
|
+
wrapper.unmount();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('resetState restores custom initial values', async () => {
|
|
230
|
+
const initialState = {
|
|
231
|
+
sort: { column: 'age', direction: 'desc' as const },
|
|
232
|
+
search: 'initial',
|
|
233
|
+
page: 1,
|
|
234
|
+
};
|
|
235
|
+
const wrapper = await mountHarness(initialState);
|
|
236
|
+
|
|
237
|
+
// Change all values
|
|
238
|
+
await wrapper.find('[data-testid="clear-sort"]').trigger('click');
|
|
239
|
+
await wrapper.find('[data-testid="set-search"]').trigger('click');
|
|
240
|
+
await wrapper.find('[data-testid="set-page-5"]').trigger('click');
|
|
241
|
+
await flushPromises();
|
|
242
|
+
|
|
243
|
+
expect(getState(wrapper).sort).toBe('null');
|
|
244
|
+
expect(getState(wrapper).search).toBe('filter text');
|
|
245
|
+
expect(getState(wrapper).page).toBe('5');
|
|
246
|
+
|
|
247
|
+
// Reset to initial
|
|
248
|
+
await wrapper.find('[data-testid="reset"]').trigger('click');
|
|
249
|
+
await flushPromises();
|
|
250
|
+
|
|
251
|
+
expect(getState(wrapper).sort).toBe('age:desc');
|
|
252
|
+
expect(getState(wrapper).search).toBe('initial');
|
|
253
|
+
expect(getState(wrapper).page).toBe('1');
|
|
254
|
+
wrapper.unmount();
|
|
255
|
+
});
|
|
256
|
+
});
|