@svgrid/grid 2.6.22 → 2.6.23

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.
Files changed (115) hide show
  1. package/dist/GridMenus.svelte +184 -6
  2. package/dist/SvAutoComplete.svelte +4 -0
  3. package/dist/SvComboBox.svelte +5 -1
  4. package/dist/SvDateRangeInput.svelte +25 -3
  5. package/dist/SvDropDownList.svelte +4 -0
  6. package/dist/SvForm.svelte +8 -8
  7. package/dist/SvGrid.controller.svelte.d.ts +48 -1
  8. package/dist/SvGrid.controller.svelte.js +214 -52
  9. package/dist/SvGrid.css +221 -0
  10. package/dist/SvGrid.helpers.d.ts +55 -0
  11. package/dist/SvGrid.helpers.js +83 -0
  12. package/dist/SvGrid.svelte +145 -0
  13. package/dist/SvGrid.types.d.ts +105 -0
  14. package/dist/SvGridCellEditor.svelte +99 -17
  15. package/dist/SvGridSelect.svelte +6 -2
  16. package/dist/SvGridSelect.svelte.d.ts +1 -1
  17. package/dist/SvListBox.svelte +8 -0
  18. package/dist/SvMultiSelect.svelte +6 -2
  19. package/dist/SvMultiSelect.svelte.d.ts +1 -1
  20. package/dist/SvNumberInput.svelte +4 -0
  21. package/dist/SvRichCell.svelte +125 -0
  22. package/dist/SvRichCell.svelte.d.ts +15 -0
  23. package/dist/SvTextArea.svelte +5 -1
  24. package/dist/SvTreeSelect.svelte +6 -2
  25. package/dist/SvTreeSelect.svelte.d.ts +1 -1
  26. package/dist/builtin-editors.d.ts +2 -2
  27. package/dist/builtin-editors.js +19 -3
  28. package/dist/cdn/GridMenus-BzWXQjv3.js +600 -0
  29. package/dist/cdn/GridMenus-xr_rHx8F.js +601 -0
  30. package/dist/cdn/SvDateRangeInput-CaOuMs8O.js +198 -0
  31. package/dist/cdn/SvDateRangeInput-DMLKmGEc.js +199 -0
  32. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +816 -0
  33. package/dist/cdn/SvDateTimePicker-sonaH0oh.js +812 -0
  34. package/dist/cdn/SvGridCellEditor-BAPSC7EL.js +550 -0
  35. package/dist/cdn/SvGridCellEditor-BwzmUWtL.js +549 -0
  36. package/dist/cdn/date-format-BNii4zeD.js +1393 -0
  37. package/dist/cdn/date-format-BnnHlqGw.js +1395 -0
  38. package/dist/cdn/{editor-registry-CiI0xlKg.js → editor-registry-BhgE3S84.js} +51 -25
  39. package/dist/cdn/{src-D1lXwq1l.js → src-DGo7IHug.js} +5730 -5198
  40. package/dist/cdn/{src-C9Hihx1W.js → src-skQN5eqh.js} +7216 -6684
  41. package/dist/cdn/svgrid.js +14 -12
  42. package/dist/cdn/svgrid.svelte-external.js +14 -12
  43. package/dist/cell-values.d.ts +16 -0
  44. package/dist/cell-values.js +28 -0
  45. package/dist/clipboard.d.ts +22 -0
  46. package/dist/clipboard.js +405 -27
  47. package/dist/editing.js +12 -7
  48. package/dist/editors/cell-editors.d.ts +11 -0
  49. package/dist/editors/cell-editors.js +18 -1
  50. package/dist/grid-messages.d.ts +15 -0
  51. package/dist/grid-messages.js +15 -0
  52. package/dist/index.d.ts +4 -0
  53. package/dist/index.js +4 -0
  54. package/dist/list-option.d.ts +6 -0
  55. package/dist/menus.js +40 -7
  56. package/dist/sanitize-html.d.ts +37 -0
  57. package/dist/sanitize-html.js +234 -0
  58. package/dist/selection-bar-view.svelte.d.ts +25 -0
  59. package/dist/selection-bar-view.svelte.js +13 -0
  60. package/dist/selection.d.ts +2 -1
  61. package/dist/selection.js +129 -13
  62. package/package.json +1 -1
  63. package/src/GridMenus.svelte +184 -6
  64. package/src/SvAutoComplete.svelte +4 -0
  65. package/src/SvComboBox.svelte +5 -1
  66. package/src/SvDateRangeInput.svelte +25 -3
  67. package/src/SvDropDownList.svelte +4 -0
  68. package/src/SvForm.svelte +8 -8
  69. package/src/SvGrid.controller.svelte.ts +251 -61
  70. package/src/SvGrid.css +221 -0
  71. package/src/SvGrid.helpers.ts +87 -0
  72. package/src/SvGrid.svelte +145 -0
  73. package/src/SvGrid.types.ts +112 -0
  74. package/src/SvGridCellEditor.svelte +99 -17
  75. package/src/SvGridSelect.svelte +6 -2
  76. package/src/SvListBox.svelte +8 -0
  77. package/src/SvMultiSelect.svelte +6 -2
  78. package/src/SvNumberInput.svelte +4 -0
  79. package/src/SvRichCell.svelte +125 -0
  80. package/src/SvTextArea.svelte +5 -1
  81. package/src/SvTreeSelect.svelte +6 -2
  82. package/src/builtin-editors.test.ts +18 -1
  83. package/src/builtin-editors.ts +19 -3
  84. package/src/cell-values.ts +30 -0
  85. package/src/clipboard.test.ts +93 -20
  86. package/src/clipboard.ts +433 -33
  87. package/src/date-string-column.test.ts +87 -0
  88. package/src/editing.test.ts +25 -0
  89. package/src/editing.ts +12 -9
  90. package/src/editor-block-prop.test.ts +132 -0
  91. package/src/editors/cell-editors.ts +27 -1
  92. package/src/grid-messages.ts +34 -0
  93. package/src/index.ts +13 -0
  94. package/src/list-option.ts +6 -0
  95. package/src/menus.test.ts +43 -0
  96. package/src/menus.ts +37 -7
  97. package/src/move-cells.test.ts +850 -0
  98. package/src/number-editor-literal.test.ts +84 -0
  99. package/src/number-editor.grid.test.ts +160 -0
  100. package/src/sanitize-html.test.ts +182 -0
  101. package/src/sanitize-html.ts +235 -0
  102. package/src/selection-bar-view.svelte.ts +36 -0
  103. package/src/selection.test.ts +157 -0
  104. package/src/selection.ts +131 -13
  105. package/src/svgrid.filter-menu-listbox.svelte.test.ts +56 -10
  106. package/src/svgrid.in-suggest-snapshot.svelte.test.ts +91 -0
  107. package/src/svgrid.menu-scroll-close.test.ts +204 -0
  108. package/src/svgrid.selection-bar-seam.test.ts +185 -0
  109. package/src/svgrid.upsell-license.test.ts +117 -0
  110. package/dist/cdn/GridMenus-BuoBPqxx.js +0 -493
  111. package/dist/cdn/GridMenus-n4llxoOI.js +0 -494
  112. package/dist/cdn/SvDateTimePicker-CHnUkWcH.js +0 -2206
  113. package/dist/cdn/SvDateTimePicker-CRQH_U7E.js +0 -2201
  114. package/dist/cdn/SvGridCellEditor-BGUCzuPB.js +0 -523
  115. package/dist/cdn/SvGridCellEditor-G8IMWIws.js +0 -522
@@ -0,0 +1,204 @@
1
+ /**
2
+ * The scroll-close gate for the CELL CONTEXT MENU and the COMMENT EDITOR.
3
+ *
4
+ * Both are positioned from a one-time cursor measurement and rendered
5
+ * `position: fixed`, exactly like the column / filter / operator menus - but
6
+ * both were missing from the effect that closes those on an outside scroll, so
7
+ * after any scroll they floated over unrelated cells.
8
+ *
9
+ * The comment editor is closed by SAVING, not discarding. Backdrop-click and
10
+ * Escape throw the draft away and that is right, because both are the user
11
+ * saying "away with this". A scroll is not a dismissal gesture, so it must not
12
+ * destroy text the user has typed.
13
+ *
14
+ * The sibling file `svgrid.filter-menu-scroll.test.ts` covers the four menus
15
+ * that were already gated, including the must-NOT-close cases.
16
+ */
17
+ import { describe, expect, it, vi } from 'vitest'
18
+ import { mount, unmount } from 'svelte'
19
+ import SvGrid from './SvGrid.svelte'
20
+ import {
21
+ createCoreRowModel,
22
+ createFilteredRowModel,
23
+ createSortedRowModel,
24
+ rowSortingFeature,
25
+ sortFns,
26
+ tableFeatures,
27
+ } from './index'
28
+ import type { ColumnDef, SvGridApi } from './index'
29
+
30
+ type Row = { id: number; name: string; team: string }
31
+ const features = tableFeatures({ rowSortingFeature })
32
+ const rows: Row[] = Array.from({ length: 6 }, (_, i) => ({
33
+ id: i + 1,
34
+ name: `Person ${i + 1}`,
35
+ team: ['A', 'B'][i % 2]!,
36
+ }))
37
+ const cols: ColumnDef<typeof features, Row>[] = [
38
+ { field: 'name', header: 'Name', width: 200, editorType: 'text' },
39
+ { field: 'team', header: 'Team', width: 160, editorType: 'text' },
40
+ ]
41
+
42
+ /** Macrotask boundary - drains the overlay's lazy import() plus its flush. */
43
+ const tick = () => new Promise<void>((r) => setTimeout(r))
44
+
45
+ function mountGrid(extra: Record<string, unknown> = {}) {
46
+ return new Promise<{ api: SvGridApi<typeof features, Row>; target: HTMLElement; destroy: () => void }>(
47
+ (res, rej) => {
48
+ const target = document.createElement('div')
49
+ document.body.appendChild(target)
50
+ const app = mount(SvGrid, {
51
+ target,
52
+ props: {
53
+ data: rows,
54
+ columns: cols,
55
+ features,
56
+ _rowModels: {
57
+ coreRowModel: createCoreRowModel(),
58
+ filteredRowModel: createFilteredRowModel(),
59
+ sortedRowModel: createSortedRowModel(sortFns),
60
+ },
61
+ rowHeight: 32,
62
+ containerHeight: 240,
63
+ virtualization: false,
64
+ // `comment` is not one of the default items - ask for it by id so
65
+ // the comment editor is reachable at all.
66
+ contextMenu: ['copy', 'comment'],
67
+ onApiReady(api: SvGridApi<typeof features, Row>) {
68
+ res({ api, target, destroy: () => { unmount(app); target.remove() } })
69
+ },
70
+ ...extra,
71
+ } as any,
72
+ })
73
+ queueMicrotask(() => { if (!target.querySelector('[role="grid"]')) rej(new Error('no grid')) })
74
+ },
75
+ )
76
+ }
77
+
78
+ async function openContextMenu(target: HTMLElement) {
79
+ const cell = target.querySelector('.sv-grid-cell[data-svgrid-row]') as HTMLElement
80
+ const r = cell.getBoundingClientRect()
81
+ cell.dispatchEvent(
82
+ new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX: r.x + 4, clientY: r.y + 4 }),
83
+ )
84
+ await vi.waitFor(() => {
85
+ const menu = target.querySelector('.sv-grid-context-menu')
86
+ expect(menu).not.toBeNull()
87
+ expect(menu!.querySelectorAll('.sv-grid-menu-item').length).toBeGreaterThan(0)
88
+ })
89
+ return target.querySelector('.sv-grid-context-menu') as HTMLElement
90
+ }
91
+
92
+ /** Right-click, then pick "Edit comment" to get the comment popover open. */
93
+ async function openCommentEditor(target: HTMLElement) {
94
+ const menu = await openContextMenu(target)
95
+ const item = [...menu.querySelectorAll<HTMLElement>('.sv-grid-menu-item')].find((b) =>
96
+ (b.textContent ?? '').includes('Edit comment'),
97
+ )
98
+ expect(item, 'the "Edit comment" item should be in the configured menu').toBeTruthy()
99
+ item!.click()
100
+ await vi.waitFor(() => {
101
+ expect(target.querySelector('.sv-grid-comment-editor')).not.toBeNull()
102
+ })
103
+ return target.querySelector('.sv-grid-comment-editor') as HTMLElement
104
+ }
105
+
106
+ /** Scroll something that is NOT inside any menu. */
107
+ function scrollOutside(target: HTMLElement) {
108
+ const viewport = target.querySelector('.sv-grid-viewport') ?? target
109
+ viewport.dispatchEvent(new Event('scroll'))
110
+ }
111
+
112
+ describe('cell context menu - scroll', () => {
113
+ it('closes when something outside it scrolls', async () => {
114
+ const { target, destroy } = await mountGrid()
115
+ await tick()
116
+ await openContextMenu(target)
117
+
118
+ scrollOutside(target)
119
+ await vi.waitFor(() => expect(target.querySelector('.sv-grid-context-menu')).toBeNull())
120
+
121
+ destroy()
122
+ })
123
+
124
+ it('stays open while the menu itself scrolls', async () => {
125
+ const { target, destroy } = await mountGrid()
126
+ await tick()
127
+ const menu = await openContextMenu(target)
128
+
129
+ menu.dispatchEvent(new Event('scroll'))
130
+ await tick()
131
+ expect(target.querySelector('.sv-grid-context-menu')).not.toBeNull()
132
+
133
+ destroy()
134
+ })
135
+
136
+ it('carries the grid id, so a second grid can tell whose menu it is', async () => {
137
+ const { target, destroy } = await mountGrid()
138
+ await tick()
139
+ const menu = await openContextMenu(target)
140
+
141
+ const id = menu.getAttribute('data-svgrid-menu')
142
+ expect(id).toBeTruthy()
143
+ // The same id the grid stamps on its cells, so the two are one grid.
144
+ const cellId = target.querySelector('.sv-grid-cell[id]')?.getAttribute('id')
145
+ expect(cellId).toContain(id!)
146
+
147
+ destroy()
148
+ })
149
+ })
150
+
151
+ describe('comment editor - scroll', () => {
152
+ it('closes when something outside it scrolls', async () => {
153
+ const { target, destroy } = await mountGrid()
154
+ await tick()
155
+ await openCommentEditor(target)
156
+
157
+ scrollOutside(target)
158
+ await vi.waitFor(() => expect(target.querySelector('.sv-grid-comment-editor')).toBeNull())
159
+
160
+ destroy()
161
+ })
162
+
163
+ it('SAVES the draft rather than throwing it away', async () => {
164
+ const changes: Array<{ note: string }> = []
165
+ const { target, destroy } = await mountGrid({
166
+ onNoteChange: (e: { note: string }) => changes.push(e),
167
+ })
168
+ await tick()
169
+ const editor = await openCommentEditor(target)
170
+
171
+ const textarea = editor.querySelector('textarea') as HTMLTextAreaElement
172
+ textarea.value = 'half-written thought'
173
+ textarea.dispatchEvent(new Event('input', { bubbles: true }))
174
+ await tick()
175
+
176
+ scrollOutside(target)
177
+ await vi.waitFor(() => expect(target.querySelector('.sv-grid-comment-editor')).toBeNull())
178
+
179
+ expect(changes.map((c) => c.note)).toContain('half-written thought')
180
+
181
+ destroy()
182
+ })
183
+
184
+ it('still DISCARDS on Escape, which is a real dismissal', async () => {
185
+ const changes: Array<{ note: string }> = []
186
+ const { target, destroy } = await mountGrid({
187
+ onNoteChange: (e: { note: string }) => changes.push(e),
188
+ })
189
+ await tick()
190
+ const editor = await openCommentEditor(target)
191
+
192
+ const textarea = editor.querySelector('textarea') as HTMLTextAreaElement
193
+ textarea.value = 'never mind'
194
+ textarea.dispatchEvent(new Event('input', { bubbles: true }))
195
+ await tick()
196
+
197
+ textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
198
+ await vi.waitFor(() => expect(target.querySelector('.sv-grid-comment-editor')).toBeNull())
199
+
200
+ expect(changes).toHaveLength(0)
201
+
202
+ destroy()
203
+ })
204
+ })
@@ -0,0 +1,185 @@
1
+ /**
2
+ * The FREE half of the selection bar: the prop, the normalisation of its three
3
+ * shapes, and what shows when no Enterprise renderer is registered.
4
+ *
5
+ * The bar itself is Pro and lives behind `registerSelectionBarView`, so this
6
+ * suite deliberately never registers one - it asserts the upsell path and the
7
+ * config the renderer would receive. The rendered bar is covered by
8
+ * `selection-bar.dom.test.ts` in @svgrid/enterprise.
9
+ */
10
+ import { describe, expect, it, vi } from 'vitest'
11
+ import { mount, unmount } from 'svelte'
12
+ import SvGrid from './SvGrid.svelte'
13
+ import {
14
+ createCoreRowModel,
15
+ hasSelectionBarView,
16
+ rowSelectionFeature,
17
+ rowSortingFeature,
18
+ tableFeatures,
19
+ } from './index'
20
+ import type { ColumnDef, SvGridApi } from './index'
21
+
22
+ type Row = { id: number; name: string }
23
+ const features = tableFeatures({ rowSortingFeature, rowSelectionFeature })
24
+ const rows: Row[] = [
25
+ { id: 1, name: 'Ada' },
26
+ { id: 2, name: 'Grace' },
27
+ ]
28
+ const cols: ColumnDef<typeof features, Row>[] = [{ field: 'name', header: 'Name', width: 160 }]
29
+
30
+ const tick = () => new Promise<void>((r) => setTimeout(r))
31
+ const UPSELL = '.sv-grid-selection-bar-upsell'
32
+
33
+ function mountGrid(selectionBar: unknown) {
34
+ return new Promise<{ api: SvGridApi<typeof features, Row>; target: HTMLElement; destroy: () => void }>(
35
+ (res, rej) => {
36
+ const target = document.createElement('div')
37
+ document.body.appendChild(target)
38
+ const app = mount(SvGrid, {
39
+ target,
40
+ props: {
41
+ data: rows,
42
+ columns: cols,
43
+ features,
44
+ _rowModels: { coreRowModel: createCoreRowModel() },
45
+ getRowId: (r: Row) => String(r.id),
46
+ containerHeight: 200,
47
+ virtualization: false,
48
+ showRowSelection: true,
49
+ selectionBar,
50
+ onApiReady(api: SvGridApi<typeof features, Row>) {
51
+ res({ api, target, destroy: () => { unmount(app); target.remove() } })
52
+ },
53
+ } as any,
54
+ })
55
+ queueMicrotask(() => { if (!target.querySelector('[role="grid"]')) rej(new Error('no grid')) })
56
+ },
57
+ )
58
+ }
59
+
60
+ describe('selectionBar - without the Enterprise renderer', () => {
61
+ it('reports that no renderer is registered', () => {
62
+ // If this is ever true here, the suite is testing the Pro path by accident
63
+ // and every assertion below is meaningless.
64
+ expect(hasSelectionBarView()).toBe(false)
65
+ })
66
+
67
+ it('shows nothing at all until a row is selected', async () => {
68
+ const { target, destroy } = await mountGrid(true)
69
+ await tick()
70
+ expect(target.querySelector(UPSELL)).toBeNull()
71
+ destroy()
72
+ })
73
+
74
+ it('shows the upsell in the bar position once rows are selected', async () => {
75
+ const { api, target, destroy } = await mountGrid(true)
76
+ await tick()
77
+ api.selectRows(['1'])
78
+ await vi.waitFor(() => expect(target.querySelector(UPSELL)).not.toBeNull())
79
+
80
+ const note = target.querySelector(UPSELL)!
81
+ expect(note.getAttribute('role')).toBe('note')
82
+ expect(note.textContent).toContain('@svgrid/enterprise')
83
+ expect(note.textContent).toContain('enableSelectionBar()')
84
+ destroy()
85
+ })
86
+
87
+ it('says a license is needed, and links to it', async () => {
88
+ // Naming the package is what unblocks a developer trying it; saying a key
89
+ // is needed is what they have to know before shipping. Enterprise is
90
+ // SOFT-gated, so the wording must not imply the feature is inert without
91
+ // one - it says "watermark", which is what actually happens.
92
+ const { api, target, destroy } = await mountGrid(true)
93
+ await tick()
94
+ api.selectRows(['1'])
95
+ await vi.waitFor(() => expect(target.querySelector(UPSELL)).not.toBeNull())
96
+
97
+ const license = target.querySelector('.sv-grid-upsell-license')!
98
+ expect(license).not.toBeNull()
99
+ expect(license.textContent).toContain('license key')
100
+ expect(license.textContent).toContain('watermark')
101
+
102
+ const link = license.querySelector('a')!
103
+ expect(link.getAttribute('href')).toBe('https://svgrid.com/pricing/')
104
+ // Opening a pricing page must not navigate the host app away.
105
+ expect(link.getAttribute('target')).toBe('_blank')
106
+ expect(link.getAttribute('rel')).toContain('noopener')
107
+
108
+ destroy()
109
+ })
110
+
111
+ it('is localizable, like the rest of the grid chrome', async () => {
112
+ const target = document.createElement('div')
113
+ document.body.appendChild(target)
114
+ let api: SvGridApi<typeof features, Row>
115
+ const app = mount(SvGrid, {
116
+ target,
117
+ props: {
118
+ data: rows,
119
+ columns: cols,
120
+ features,
121
+ _rowModels: { coreRowModel: createCoreRowModel() },
122
+ getRowId: (r: Row) => String(r.id),
123
+ containerHeight: 200,
124
+ virtualization: false,
125
+ showRowSelection: true,
126
+ selectionBar: true,
127
+ localization: { text: { upsellLicense: 'Lizenz erforderlich.' } },
128
+ onApiReady: (a: SvGridApi<typeof features, Row>) => (api = a),
129
+ } as any,
130
+ })
131
+ await tick()
132
+ api!.selectRows(['1'])
133
+ await vi.waitFor(() => {
134
+ expect(target.querySelector('.sv-grid-upsell-license')?.textContent).toContain(
135
+ 'Lizenz erforderlich.',
136
+ )
137
+ })
138
+ unmount(app)
139
+ target.remove()
140
+ })
141
+
142
+ it('places the upsell on the edge the real bar would use', async () => {
143
+ // So installing enterprise does not move the thing the user was looking at.
144
+ const { api, target, destroy } = await mountGrid({ position: 'top', actions: [] })
145
+ await tick()
146
+ api.selectRows(['1'])
147
+ await vi.waitFor(() => expect(target.querySelector(UPSELL)).not.toBeNull())
148
+ expect(target.querySelector(UPSELL)!.getAttribute('data-position')).toBe('top')
149
+ destroy()
150
+ })
151
+
152
+ it('stays out of the way entirely when the prop is unset or false', async () => {
153
+ for (const prop of [undefined, false]) {
154
+ const { api, target, destroy } = await mountGrid(prop)
155
+ await tick()
156
+ api.selectRows(['1'])
157
+ await tick()
158
+ await tick()
159
+ expect(target.querySelector(UPSELL), `selectionBar={${String(prop)}}`).toBeNull()
160
+ destroy()
161
+ }
162
+ })
163
+ })
164
+
165
+ describe('selectionBar - the three prop shapes all reach the renderer', () => {
166
+ // Each shape has to produce a bar. What lands ON it is asserted against the
167
+ // real renderer in the enterprise suite; here the question is only whether
168
+ // the prop was understood at all.
169
+ const shapes: Array<[string, unknown]> = [
170
+ ['true', true],
171
+ ['an action array', [{ key: 'k', label: 'K', action: () => {} }]],
172
+ ['a config object', { position: 'bottom', actions: [], maxVisible: 2 }],
173
+ ['a config object with no actions', { position: 'top' }],
174
+ ]
175
+
176
+ for (const [name, prop] of shapes) {
177
+ it(`understands ${name}`, async () => {
178
+ const { api, target, destroy } = await mountGrid(prop)
179
+ await tick()
180
+ api.selectRows(['1'])
181
+ await vi.waitFor(() => expect(target.querySelector(UPSELL)).not.toBeNull())
182
+ destroy()
183
+ })
184
+ }
185
+ })
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Every Enterprise gate carries the same licensing line.
3
+ *
4
+ * There are four of them - Kanban board, scheduler, pivot mode and the
5
+ * selection bar - and they used to be four hand-written paragraphs. Four gates
6
+ * worded four ways is how a product ends up explaining its own licensing
7
+ * inconsistently, so they now share one snippet. This is the test that keeps
8
+ * them sharing it: a fifth Enterprise feature that hand-rolls its own note
9
+ * will show up here as a missing licensing line.
10
+ *
11
+ * None of the renderers are registered in this file, which is the point - it
12
+ * asserts what a FREE grid shows.
13
+ */
14
+ import { describe, expect, it, vi } from 'vitest'
15
+ import { mount, unmount } from 'svelte'
16
+ import SvGrid from './SvGrid.svelte'
17
+ import {
18
+ createCoreRowModel,
19
+ hasBoardView,
20
+ hasSchedulerView,
21
+ hasSelectionBarView,
22
+ tableFeatures,
23
+ } from './index'
24
+ import type { ColumnDef, SvGridApi } from './index'
25
+
26
+ type Row = { id: number; name: string; team: string; score: number }
27
+ const features = tableFeatures({})
28
+ const rows: Row[] = [
29
+ { id: 1, name: 'Ada', team: 'A', score: 3 },
30
+ { id: 2, name: 'Grace', team: 'B', score: 5 },
31
+ ]
32
+ const cols: ColumnDef<typeof features, Row>[] = [
33
+ { field: 'name', header: 'Name', width: 140 },
34
+ { field: 'team', header: 'Team', width: 120 },
35
+ { field: 'score', header: 'Score', width: 100 },
36
+ ]
37
+
38
+ const tick = () => new Promise<void>((r) => setTimeout(r))
39
+ const LICENSE = '.sv-grid-upsell-license'
40
+
41
+ function mountGrid(extra: Record<string, unknown>) {
42
+ return new Promise<{ api: SvGridApi<typeof features, Row>; target: HTMLElement; destroy: () => void }>(
43
+ (res, rej) => {
44
+ const target = document.createElement('div')
45
+ document.body.appendChild(target)
46
+ const app = mount(SvGrid, {
47
+ target,
48
+ props: {
49
+ data: rows,
50
+ columns: cols,
51
+ features,
52
+ _rowModels: { coreRowModel: createCoreRowModel() },
53
+ getRowId: (r: Row) => String(r.id),
54
+ containerHeight: 260,
55
+ virtualization: false,
56
+ onApiReady(api: SvGridApi<typeof features, Row>) {
57
+ res({ api, target, destroy: () => { unmount(app); target.remove() } })
58
+ },
59
+ ...extra,
60
+ } as any,
61
+ })
62
+ queueMicrotask(() => { if (!target.querySelector('[role="grid"]')) rej(new Error('no grid')) })
63
+ },
64
+ )
65
+ }
66
+
67
+ describe('Enterprise upsell notes', () => {
68
+ it('starts from a grid with no Pro renderer registered', () => {
69
+ // Otherwise every assertion below is checking the wrong branch.
70
+ expect(hasBoardView()).toBe(false)
71
+ expect(hasSchedulerView()).toBe(false)
72
+ expect(hasSelectionBarView()).toBe(false)
73
+ })
74
+
75
+ const gates: Array<[string, Record<string, unknown>, ((api: SvGridApi<typeof features, Row>) => void)?]> = [
76
+ ['Kanban board', { board: { groupBy: 'team' } }],
77
+ ['scheduler', { scheduler: { startField: 'name' } }],
78
+ ['selection bar', { showRowSelection: true, selectionBar: true }, (api) => api.selectRows(['1'])],
79
+ // pivotViewOn = a pivot config AND pivot mode on; without the Pro engine
80
+ // `pivotResult` stays null and the upsell renders in its place.
81
+ ['pivot mode', { pivot: { rows: ['team'], values: [{ field: 'score', agg: 'sum' }] }, pivotMode: true }],
82
+ ]
83
+
84
+ for (const [name, props, activate] of gates) {
85
+ it(`the ${name} gate explains the licensing`, async () => {
86
+ const { api, target, destroy } = await mountGrid(props)
87
+ try {
88
+ await tick()
89
+ activate?.(api)
90
+ await vi.waitFor(() => {
91
+ expect(target.querySelector(LICENSE), `${name} upsell`).not.toBeNull()
92
+ })
93
+
94
+ const note = target.querySelector(LICENSE)!
95
+ // Soft-gated: the feature runs without a key and the grid watermarks.
96
+ // The wording must not imply it is inert, because it is not.
97
+ expect(note.textContent, name).toContain('license key')
98
+ expect(note.textContent, name).toContain('watermark')
99
+ expect(note.querySelector('a')?.getAttribute('href'), name).toBe(
100
+ 'https://svgrid.com/pricing/',
101
+ )
102
+ } finally {
103
+ destroy()
104
+ }
105
+ })
106
+ }
107
+
108
+ it('says nothing about licensing on a grid using no Pro feature', async () => {
109
+ const { target, destroy } = await mountGrid({})
110
+ try {
111
+ await tick()
112
+ expect(target.querySelector(LICENSE)).toBeNull()
113
+ } finally {
114
+ destroy()
115
+ }
116
+ })
117
+ })