@lovett/ui 0.0.10 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-RBYWGBQ2.js +2752 -0
- package/dist/chunk-RBYWGBQ2.js.map +1 -0
- package/dist/index.d.ts +6064 -80
- package/dist/index.js +21561 -9696
- package/dist/index.js.map +1 -1
- package/dist/rich-composer-impl-5NO443A6.js +1859 -0
- package/dist/rich-composer-impl-5NO443A6.js.map +1 -0
- package/dist/styles.css +1586 -2
- package/dist/theme-v2.css +228 -0
- package/dist/tokens.css +121 -0
- package/package.json +8 -1
- package/src/__tests__/anchor.test.tsx +422 -0
- package/src/__tests__/avatar.test.tsx +272 -0
- package/src/__tests__/bar-chart.test.tsx +809 -0
- package/src/__tests__/board.test.tsx +420 -0
- package/src/__tests__/chart-math.test.ts +922 -0
- package/src/__tests__/chart-series.test.ts +339 -0
- package/src/__tests__/code-block.test.tsx +134 -0
- package/src/__tests__/combobox.test.tsx +677 -0
- package/src/__tests__/display-popover.test.tsx +195 -0
- package/src/__tests__/display-store.test.tsx +208 -0
- package/src/__tests__/donut-chart.test.tsx +397 -0
- package/src/__tests__/dropdown-menu.test.tsx +572 -0
- package/src/__tests__/filter-menu.test.tsx +175 -0
- package/src/__tests__/gauge-ring.test.tsx +233 -0
- package/src/__tests__/helpers/geometry.ts +58 -0
- package/src/__tests__/layer-stack.test.tsx +228 -0
- package/src/__tests__/line-chart.test.tsx +612 -0
- package/src/__tests__/popover.test.tsx +460 -0
- package/src/__tests__/ranked-bars.test.tsx +343 -0
- package/src/__tests__/remark-underline.test.ts +194 -0
- package/src/__tests__/select.test.tsx +543 -0
- package/src/__tests__/sparkline.test.tsx +368 -0
- package/src/__tests__/stat-layer.test.tsx +271 -0
- package/src/__tests__/stat-strip.test.tsx +175 -0
- package/src/__tests__/status.test.tsx +293 -0
- package/src/__tests__/tabs.test.tsx +303 -0
- package/src/__tests__/token-shape.test.ts +132 -2
- package/src/__tests__/tooltip.test.tsx +355 -0
- package/src/avatar.tsx +352 -0
- package/src/bar-chart.tsx +1214 -0
- package/src/board.tsx +658 -0
- package/src/calculator-shell-v2.tsx +19 -39
- package/src/chart-frame.tsx +960 -0
- package/src/chart-legend.tsx +304 -0
- package/src/chart-tooltip.tsx +267 -0
- package/src/code-block.tsx +62 -8
- package/src/combobox.tsx +796 -0
- package/src/delta-chip.tsx +263 -0
- package/src/detail/__tests__/activity-pane.test.tsx +186 -0
- package/src/detail/__tests__/detail-chrome.test.tsx +134 -0
- package/src/detail/__tests__/detail-surface.test.tsx +529 -0
- package/src/detail/__tests__/field-row.test.tsx +357 -0
- package/src/detail/activity-pane.tsx +507 -0
- package/src/detail/calendar.tsx +355 -0
- package/src/detail/detail-divider.tsx +261 -0
- package/src/detail/detail-header.tsx +287 -0
- package/src/detail/detail-menu.tsx +254 -0
- package/src/detail/detail-surface.tsx +1110 -0
- package/src/detail/field-list.tsx +196 -0
- package/src/detail/field-row.tsx +1131 -0
- package/src/detail/index.ts +58 -0
- package/src/detail/segmented-choice.tsx +94 -0
- package/src/detail/types.ts +129 -0
- package/src/display-popover.tsx +487 -0
- package/src/display-store.tsx +242 -0
- package/src/donut-chart.tsx +988 -0
- package/src/dropdown-menu.tsx +412 -151
- package/src/filter-core/EXPORTS.md +85 -0
- package/src/filter-core/__tests__/columns.test.ts +159 -0
- package/src/filter-core/__tests__/faceting.test.ts +193 -0
- package/src/filter-core/__tests__/filter-fns.test.ts +519 -0
- package/src/filter-core/__tests__/operators.test.ts +235 -0
- package/src/filter-core/__tests__/state.test.ts +268 -0
- package/src/filter-core/__tests__/url.test.ts +350 -0
- package/src/filter-core/columns.ts +134 -0
- package/src/filter-core/date-utils.ts +38 -0
- package/src/filter-core/examples/task-filter-columns.ts +121 -0
- package/src/filter-core/faceting.ts +120 -0
- package/src/filter-core/filter-fns.ts +335 -0
- package/src/filter-core/index.ts +105 -0
- package/src/filter-core/operators.ts +433 -0
- package/src/filter-core/state.ts +280 -0
- package/src/filter-core/types.ts +247 -0
- package/src/filter-core/url.ts +261 -0
- package/src/filter-dropdown.tsx +12 -0
- package/src/filter-menu.tsx +649 -0
- package/src/floating-drawer.tsx +19 -1
- package/src/gauge-ring.tsx +435 -0
- package/src/hue.ts +52 -0
- package/src/index.ts +402 -0
- package/src/kbd.tsx +27 -4
- package/src/lib/anchor.ts +427 -0
- package/src/lib/chart.ts +866 -0
- package/src/lib/focus.ts +74 -0
- package/src/lib/layer-stack.ts +215 -0
- package/src/lib/refs.ts +31 -0
- package/src/lib/remark-underline.ts +443 -0
- package/src/lib/series.ts +169 -0
- package/src/line-chart.tsx +1176 -0
- package/src/markdown.tsx +26 -7
- package/src/metric-card.tsx +57 -22
- package/src/modal.tsx +73 -66
- package/src/popover.tsx +407 -0
- package/src/progress-ledger.tsx +304 -0
- package/src/ranked-bars.tsx +386 -0
- package/src/segmented-pill.tsx +32 -9
- package/src/select.tsx +646 -0
- package/src/sortable.tsx +315 -1
- package/src/sparkline.tsx +416 -0
- package/src/stat-card.tsx +376 -0
- package/src/stat-row.tsx +108 -70
- package/src/stat-strip.tsx +327 -0
- package/src/status.tsx +215 -0
- package/src/styles.css +1586 -2
- package/src/tabs.tsx +206 -25
- package/src/task-card.tsx +610 -0
- package/src/thread/__tests__/comment-body-hostile.test.tsx +331 -0
- package/src/thread/__tests__/comment-tree.test.ts +151 -0
- package/src/thread/__tests__/emoji.test.ts +187 -0
- package/src/thread/__tests__/fixtures/thread-fixture.ts +235 -0
- package/src/thread/__tests__/link-preview-source.test.ts +120 -0
- package/src/thread/__tests__/link-preview.test.tsx +600 -0
- package/src/thread/__tests__/markdown-format.test.ts +82 -0
- package/src/thread/__tests__/markdown-spec.test.ts +469 -0
- package/src/thread/__tests__/relative-time.test.ts +71 -0
- package/src/thread/__tests__/rich-composer.test.tsx +799 -0
- package/src/thread/__tests__/scroll-caret.test.ts +58 -0
- package/src/thread/__tests__/suggestion-list.test.tsx +648 -0
- package/src/thread/__tests__/thread-scroll-ownership.test.tsx +88 -0
- package/src/thread/__tests__/thread.test.tsx +662 -0
- package/src/thread/__tests__/use-attachments.test.tsx +679 -0
- package/src/thread/actions.tsx +196 -0
- package/src/thread/attachments.tsx +1071 -0
- package/src/thread/comment-body.tsx +148 -0
- package/src/thread/comment-tree.ts +182 -0
- package/src/thread/comment.tsx +915 -0
- package/src/thread/composer-footer.tsx +125 -0
- package/src/thread/composer.tsx +319 -0
- package/src/thread/emoji.ts +283 -0
- package/src/thread/index.ts +153 -0
- package/src/thread/link-preview.tsx +341 -0
- package/src/thread/markdown-format.ts +155 -0
- package/src/thread/markdown-spec.ts +754 -0
- package/src/thread/rail.tsx +372 -0
- package/src/thread/reactions.tsx +415 -0
- package/src/thread/relative-time.tsx +94 -0
- package/src/thread/rich-composer-impl.tsx +1601 -0
- package/src/thread/rich-composer.tsx +195 -0
- package/src/thread/scroll-caret.ts +37 -0
- package/src/thread/suggestion-list.tsx +182 -0
- package/src/thread/thread.tsx +718 -0
- package/src/thread/types.ts +218 -0
- package/src/thread/use-attachments.ts +598 -0
- package/src/thread/use-now.ts +73 -0
- package/src/thread/use-thread.ts +316 -0
- package/src/tokens.css +121 -0
- package/src/tooltip.tsx +297 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DisplayPopover + its row primitives.
|
|
3
|
+
*
|
|
4
|
+
* The contract is mostly about what CANNOT happen, because each of these
|
|
5
|
+
* primitives exists to make one of the source's defects unreachable
|
|
6
|
+
* (`docs/design-briefs/tasks-board-header-primitives.md` §4):
|
|
7
|
+
*
|
|
8
|
+
* - `ToggleRow` owns the id/htmlFor pair, so clicking the LABEL toggles.
|
|
9
|
+
* Theirs is hand-written at three call sites.
|
|
10
|
+
* - every `ChipToggleGroup` chip is `type="button"` with `aria-pressed`.
|
|
11
|
+
* Theirs is neither — inside a form it submits, and a screen reader is
|
|
12
|
+
* told nothing about which chips are on.
|
|
13
|
+
* - `PopoverFooterActions` renders NO primary unless one is supplied.
|
|
14
|
+
* Theirs is a `<button>` with no `onClick` in three files.
|
|
15
|
+
* - the panel keeps Tab inside itself, closes on Escape and hands focus
|
|
16
|
+
* back to its trigger.
|
|
17
|
+
*/
|
|
18
|
+
import { useState } from 'react'
|
|
19
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
20
|
+
import { render, screen } from '@testing-library/react'
|
|
21
|
+
import userEvent from '@testing-library/user-event'
|
|
22
|
+
import {
|
|
23
|
+
ChipToggleGroup,
|
|
24
|
+
DisplayPopover,
|
|
25
|
+
PopoverFooterActions,
|
|
26
|
+
SelectRow,
|
|
27
|
+
ToggleRow,
|
|
28
|
+
} from '../display-popover'
|
|
29
|
+
import { mockGeometry, setViewport } from './helpers/geometry'
|
|
30
|
+
|
|
31
|
+
describe('ToggleRow', () => {
|
|
32
|
+
it('pairs the label with the control so clicking the label toggles', async () => {
|
|
33
|
+
const user = userEvent.setup()
|
|
34
|
+
const onCheckedChange = vi.fn()
|
|
35
|
+
render(
|
|
36
|
+
<ToggleRow
|
|
37
|
+
label="Show empty columns"
|
|
38
|
+
checked={false}
|
|
39
|
+
onCheckedChange={onCheckedChange}
|
|
40
|
+
/>,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// getByLabelText only resolves when the htmlFor/id pair is real.
|
|
44
|
+
const control = screen.getByLabelText('Show empty columns')
|
|
45
|
+
expect(control).toHaveAttribute('type', 'checkbox')
|
|
46
|
+
|
|
47
|
+
await user.click(screen.getByText('Show empty columns'))
|
|
48
|
+
expect(onCheckedChange).toHaveBeenCalledWith(true)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('mints a unique id per row, so two rows do not collide', () => {
|
|
52
|
+
render(
|
|
53
|
+
<>
|
|
54
|
+
<ToggleRow label="First" checked={false} onCheckedChange={() => {}} />
|
|
55
|
+
<ToggleRow label="Second" checked={false} onCheckedChange={() => {}} />
|
|
56
|
+
</>,
|
|
57
|
+
)
|
|
58
|
+
const first = screen.getByLabelText('First')
|
|
59
|
+
const second = screen.getByLabelText('Second')
|
|
60
|
+
expect(first.id).not.toBe('')
|
|
61
|
+
expect(first.id).not.toBe(second.id)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('SelectRow', () => {
|
|
66
|
+
it('names the control from the row label when none is given', () => {
|
|
67
|
+
render(
|
|
68
|
+
<SelectRow
|
|
69
|
+
label="Grouping"
|
|
70
|
+
value="column"
|
|
71
|
+
onValueChange={() => {}}
|
|
72
|
+
options={[
|
|
73
|
+
{ value: 'column', label: 'Column' },
|
|
74
|
+
{ value: 'assignee', label: 'Assignee' },
|
|
75
|
+
]}
|
|
76
|
+
/>,
|
|
77
|
+
)
|
|
78
|
+
expect(screen.getByRole('combobox', { name: 'Grouping' })).toBeInTheDocument()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('ChipToggleGroup', () => {
|
|
83
|
+
it('gives every chip type=button and aria-pressed', () => {
|
|
84
|
+
render(
|
|
85
|
+
<ChipToggleGroup
|
|
86
|
+
aria-label="Card properties"
|
|
87
|
+
items={[
|
|
88
|
+
{ key: 'priority', label: 'Priority' },
|
|
89
|
+
{ key: 'due', label: 'Due date' },
|
|
90
|
+
]}
|
|
91
|
+
value={{ priority: true }}
|
|
92
|
+
onToggle={() => {}}
|
|
93
|
+
/>,
|
|
94
|
+
)
|
|
95
|
+
const priority = screen.getByRole('button', { name: 'Priority', pressed: true })
|
|
96
|
+
const due = screen.getByRole('button', { name: 'Due date', pressed: false })
|
|
97
|
+
// A chip without type="button" submits the form it sits in.
|
|
98
|
+
expect(priority).toHaveAttribute('type', 'button')
|
|
99
|
+
expect(due).toHaveAttribute('type', 'button')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('reports the key and the NEXT state, not just the key', async () => {
|
|
103
|
+
const user = userEvent.setup()
|
|
104
|
+
const onToggle = vi.fn()
|
|
105
|
+
render(
|
|
106
|
+
<ChipToggleGroup
|
|
107
|
+
aria-label="Card properties"
|
|
108
|
+
items={[{ key: 'priority', label: 'Priority' }]}
|
|
109
|
+
value={{ priority: true }}
|
|
110
|
+
onToggle={onToggle}
|
|
111
|
+
/>,
|
|
112
|
+
)
|
|
113
|
+
await user.click(screen.getByRole('button', { name: 'Priority' }))
|
|
114
|
+
expect(onToggle).toHaveBeenCalledWith('priority', false)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('PopoverFooterActions', () => {
|
|
119
|
+
it('renders no primary action when none is supplied', () => {
|
|
120
|
+
render(<PopoverFooterActions onReset={() => {}} />)
|
|
121
|
+
expect(screen.getByRole('button', { name: 'Reset to defaults' })).toBeInTheDocument()
|
|
122
|
+
// Exactly one button: the dead "Set default for everyone" cannot happen.
|
|
123
|
+
expect(screen.getAllByRole('button')).toHaveLength(1)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('renders the primary when one is supplied', () => {
|
|
127
|
+
const onClick = vi.fn()
|
|
128
|
+
render(
|
|
129
|
+
<PopoverFooterActions
|
|
130
|
+
onReset={() => {}}
|
|
131
|
+
primaryAction={{ label: 'Apply', onClick }}
|
|
132
|
+
/>,
|
|
133
|
+
)
|
|
134
|
+
expect(screen.getAllByRole('button')).toHaveLength(2)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('disables reset when the panel is already at its defaults', () => {
|
|
138
|
+
render(<PopoverFooterActions onReset={() => {}} resetDisabled />)
|
|
139
|
+
expect(screen.getByRole('button', { name: 'Reset to defaults' })).toBeDisabled()
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe('DisplayPopover', () => {
|
|
144
|
+
let restore: () => void
|
|
145
|
+
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
restore = mockGeometry()
|
|
148
|
+
setViewport(1280, 800)
|
|
149
|
+
})
|
|
150
|
+
afterEach(() => restore())
|
|
151
|
+
|
|
152
|
+
function Panel() {
|
|
153
|
+
const [flag, setFlag] = useState(false)
|
|
154
|
+
return (
|
|
155
|
+
<DisplayPopover
|
|
156
|
+
label="Display options"
|
|
157
|
+
trigger={<button type="button">Display</button>}
|
|
158
|
+
>
|
|
159
|
+
<DisplayPopover.Section>
|
|
160
|
+
<ToggleRow label="Show empty columns" checked={flag} onCheckedChange={setFlag} />
|
|
161
|
+
</DisplayPopover.Section>
|
|
162
|
+
<DisplayPopover.Section>
|
|
163
|
+
<PopoverFooterActions onReset={() => setFlag(false)} />
|
|
164
|
+
</DisplayPopover.Section>
|
|
165
|
+
</DisplayPopover>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
it('opens, closes on Escape, and returns focus to its trigger', async () => {
|
|
170
|
+
const user = userEvent.setup()
|
|
171
|
+
render(<Panel />)
|
|
172
|
+
const trigger = screen.getByRole('button', { name: 'Display' })
|
|
173
|
+
|
|
174
|
+
await user.click(trigger)
|
|
175
|
+
expect(screen.getByRole('dialog', { name: 'Display options' })).toBeInTheDocument()
|
|
176
|
+
|
|
177
|
+
await user.keyboard('{Escape}')
|
|
178
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
|
179
|
+
expect(trigger).toHaveFocus()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('keeps Tab inside the panel', async () => {
|
|
183
|
+
const user = userEvent.setup()
|
|
184
|
+
render(<Panel />)
|
|
185
|
+
await user.click(screen.getByRole('button', { name: 'Display' }))
|
|
186
|
+
|
|
187
|
+
const panel = screen.getByRole('dialog', { name: 'Display options' })
|
|
188
|
+
const reset = screen.getByRole('button', { name: 'Reset to defaults' })
|
|
189
|
+
|
|
190
|
+
reset.focus()
|
|
191
|
+
await user.tab()
|
|
192
|
+
// Without the trap, focus walks out of the portal onto the page behind.
|
|
193
|
+
expect(panel.contains(document.activeElement)).toBe(true)
|
|
194
|
+
})
|
|
195
|
+
})
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createDisplayStore — per-scope display settings, by context.
|
|
3
|
+
*
|
|
4
|
+
* Two contracts, both of them inversions of a source defect
|
|
5
|
+
* (`docs/design-briefs/tasks-board-header-primitives.md` §4):
|
|
6
|
+
*
|
|
7
|
+
* 1. TWO PROVIDERS ARE TWO PANELS. Their cards subscribe to a module-level
|
|
8
|
+
* singleton, so two boards on one page cannot hold different settings.
|
|
9
|
+
* The test below renders two and changes one.
|
|
10
|
+
* 2. PERSISTENCE IS PER SCOPE and VALIDATED. A key from another scope never
|
|
11
|
+
* leaks; a hand-edited value that is the wrong type or outside the
|
|
12
|
+
* allow-list falls back to the default instead of reaching a component.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, it, beforeEach } from 'vitest'
|
|
15
|
+
import { render, screen } from '@testing-library/react'
|
|
16
|
+
import userEvent from '@testing-library/user-event'
|
|
17
|
+
import { createDisplayStore } from '../display-store'
|
|
18
|
+
|
|
19
|
+
type Settings = {
|
|
20
|
+
readonly grouping: 'column' | 'assignee'
|
|
21
|
+
readonly showEmpty: boolean
|
|
22
|
+
/** A MEASUREMENT — a size the operator drags rather than picks. */
|
|
23
|
+
readonly panelSize: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULTS: Settings = { grouping: 'column', showEmpty: true, panelSize: 560 }
|
|
27
|
+
|
|
28
|
+
const store = createDisplayStore<Settings>(
|
|
29
|
+
'test',
|
|
30
|
+
DEFAULTS,
|
|
31
|
+
{ grouping: ['column', 'assignee'] },
|
|
32
|
+
{
|
|
33
|
+
// An allow-list cannot express a RANGE, so a numeric setting sanitises on
|
|
34
|
+
// the migration seam and anything that is not a plausible size falls back
|
|
35
|
+
// to the default exactly as a bad enum would.
|
|
36
|
+
panelSize: (raw) =>
|
|
37
|
+
typeof raw === 'number' && Number.isFinite(raw) && raw >= 1 && raw <= 8000
|
|
38
|
+
? Math.round(raw)
|
|
39
|
+
: undefined,
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
function Panel({ name }: { name: string }) {
|
|
44
|
+
const { settings, set, reset, isDefault } = store.useDisplay()
|
|
45
|
+
return (
|
|
46
|
+
<div>
|
|
47
|
+
<span data-testid={`${name}-grouping`}>{settings.grouping}</span>
|
|
48
|
+
<span data-testid={`${name}-empty`}>{String(settings.showEmpty)}</span>
|
|
49
|
+
<span data-testid={`${name}-default`}>{String(isDefault)}</span>
|
|
50
|
+
<button type="button" onClick={() => set('grouping', 'assignee')}>
|
|
51
|
+
{`${name}: group by assignee`}
|
|
52
|
+
</button>
|
|
53
|
+
<button type="button" onClick={() => set('showEmpty', false)}>
|
|
54
|
+
{`${name}: hide empty`}
|
|
55
|
+
</button>
|
|
56
|
+
<span data-testid={`${name}-size`}>{String(settings.panelSize)}</span>
|
|
57
|
+
<button type="button" onClick={() => set('panelSize', 820)}>
|
|
58
|
+
{`${name}: widen`}
|
|
59
|
+
</button>
|
|
60
|
+
<button type="button" onClick={reset}>{`${name}: reset`}</button>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('createDisplayStore', () => {
|
|
66
|
+
beforeEach(() => window.localStorage.clear())
|
|
67
|
+
|
|
68
|
+
it('namespaces its key by name and scope', () => {
|
|
69
|
+
expect(store.storageKey('brn_1')).toBe('workspace:test:brn_1:display')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('persists a change and reads it back on the next mount', async () => {
|
|
73
|
+
const user = userEvent.setup()
|
|
74
|
+
const { unmount } = render(
|
|
75
|
+
<store.Provider scope="brn_1">
|
|
76
|
+
<Panel name="a" />
|
|
77
|
+
</store.Provider>,
|
|
78
|
+
)
|
|
79
|
+
await user.click(screen.getByRole('button', { name: 'a: group by assignee' }))
|
|
80
|
+
expect(screen.getByTestId('a-grouping')).toHaveTextContent('assignee')
|
|
81
|
+
unmount()
|
|
82
|
+
|
|
83
|
+
render(
|
|
84
|
+
<store.Provider scope="brn_1">
|
|
85
|
+
<Panel name="b" />
|
|
86
|
+
</store.Provider>,
|
|
87
|
+
)
|
|
88
|
+
expect(screen.getByTestId('b-grouping')).toHaveTextContent('assignee')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('partitions by scope — one brand never sees another brand settings', async () => {
|
|
92
|
+
const user = userEvent.setup()
|
|
93
|
+
const { unmount } = render(
|
|
94
|
+
<store.Provider scope="brn_1">
|
|
95
|
+
<Panel name="a" />
|
|
96
|
+
</store.Provider>,
|
|
97
|
+
)
|
|
98
|
+
await user.click(screen.getByRole('button', { name: 'a: group by assignee' }))
|
|
99
|
+
unmount()
|
|
100
|
+
|
|
101
|
+
render(
|
|
102
|
+
<store.Provider scope="brn_2">
|
|
103
|
+
<Panel name="b" />
|
|
104
|
+
</store.Provider>,
|
|
105
|
+
)
|
|
106
|
+
expect(screen.getByTestId('b-grouping')).toHaveTextContent('column')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('two providers hold independent settings', async () => {
|
|
110
|
+
const user = userEvent.setup()
|
|
111
|
+
render(
|
|
112
|
+
<>
|
|
113
|
+
<store.Provider>
|
|
114
|
+
<Panel name="left" />
|
|
115
|
+
</store.Provider>
|
|
116
|
+
<store.Provider>
|
|
117
|
+
<Panel name="right" />
|
|
118
|
+
</store.Provider>
|
|
119
|
+
</>,
|
|
120
|
+
)
|
|
121
|
+
await user.click(screen.getByRole('button', { name: 'left: group by assignee' }))
|
|
122
|
+
expect(screen.getByTestId('left-grouping')).toHaveTextContent('assignee')
|
|
123
|
+
// The singleton trap: with a module-level store this would read "assignee".
|
|
124
|
+
expect(screen.getByTestId('right-grouping')).toHaveTextContent('column')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('drops a persisted value that is the wrong type or off the allow-list', () => {
|
|
128
|
+
window.localStorage.setItem(
|
|
129
|
+
store.storageKey('brn_3'),
|
|
130
|
+
JSON.stringify({ grouping: 'sideways', showEmpty: 'yes' }),
|
|
131
|
+
)
|
|
132
|
+
render(
|
|
133
|
+
<store.Provider scope="brn_3">
|
|
134
|
+
<Panel name="c" />
|
|
135
|
+
</store.Provider>,
|
|
136
|
+
)
|
|
137
|
+
expect(screen.getByTestId('c-grouping')).toHaveTextContent('column')
|
|
138
|
+
expect(screen.getByTestId('c-empty')).toHaveTextContent('true')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('persists a numeric setting and reads it back', async () => {
|
|
142
|
+
const user = userEvent.setup()
|
|
143
|
+
const { unmount } = render(
|
|
144
|
+
<store.Provider scope="brn_n">
|
|
145
|
+
<Panel name="n" />
|
|
146
|
+
</store.Provider>,
|
|
147
|
+
)
|
|
148
|
+
await user.click(screen.getByRole('button', { name: 'n: widen' }))
|
|
149
|
+
expect(screen.getByTestId('n-size')).toHaveTextContent('820')
|
|
150
|
+
unmount()
|
|
151
|
+
|
|
152
|
+
render(
|
|
153
|
+
<store.Provider scope="brn_n">
|
|
154
|
+
<Panel name="n2" />
|
|
155
|
+
</store.Provider>,
|
|
156
|
+
)
|
|
157
|
+
expect(screen.getByTestId('n2-size')).toHaveTextContent('820')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('drops a stored size that is not a size', () => {
|
|
161
|
+
// `NaN` is `typeof 'number'` and survives a hand edit; a pane laid out at
|
|
162
|
+
// `NaN` px has no width at all, so it is rejected like any other value
|
|
163
|
+
// that does not hold up.
|
|
164
|
+
for (const [scope, stored] of [
|
|
165
|
+
['brn_n1', Number.NaN],
|
|
166
|
+
['brn_n2', -40],
|
|
167
|
+
['brn_n3', 99999],
|
|
168
|
+
['brn_n4', '560'],
|
|
169
|
+
] as const) {
|
|
170
|
+
window.localStorage.setItem(
|
|
171
|
+
store.storageKey(scope),
|
|
172
|
+
JSON.stringify({ panelSize: stored }),
|
|
173
|
+
)
|
|
174
|
+
const view = render(
|
|
175
|
+
<store.Provider scope={scope}>
|
|
176
|
+
<Panel name={scope} />
|
|
177
|
+
</store.Provider>,
|
|
178
|
+
)
|
|
179
|
+
expect(screen.getByTestId(`${scope}-size`)).toHaveTextContent('560')
|
|
180
|
+
view.unmount()
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('survives a payload that is not an object at all', () => {
|
|
185
|
+
window.localStorage.setItem(store.storageKey('brn_4'), 'not json {{{')
|
|
186
|
+
render(
|
|
187
|
+
<store.Provider scope="brn_4">
|
|
188
|
+
<Panel name="d" />
|
|
189
|
+
</store.Provider>,
|
|
190
|
+
)
|
|
191
|
+
expect(screen.getByTestId('d-grouping')).toHaveTextContent('column')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('reports isDefault, and reset returns to it', async () => {
|
|
195
|
+
const user = userEvent.setup()
|
|
196
|
+
render(
|
|
197
|
+
<store.Provider scope="brn_5">
|
|
198
|
+
<Panel name="e" />
|
|
199
|
+
</store.Provider>,
|
|
200
|
+
)
|
|
201
|
+
expect(screen.getByTestId('e-default')).toHaveTextContent('true')
|
|
202
|
+
await user.click(screen.getByRole('button', { name: 'e: hide empty' }))
|
|
203
|
+
expect(screen.getByTestId('e-default')).toHaveTextContent('false')
|
|
204
|
+
await user.click(screen.getByRole('button', { name: 'e: reset' }))
|
|
205
|
+
expect(screen.getByTestId('e-empty')).toHaveTextContent('true')
|
|
206
|
+
expect(screen.getByTestId('e-default')).toHaveTextContent('true')
|
|
207
|
+
})
|
|
208
|
+
})
|