@titan-design/react-ui 0.2.5 → 0.2.7
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-3VLOBS6O.mjs +846 -0
- package/dist/chunk-3VLOBS6O.mjs.map +1 -0
- package/dist/{chunk-UXVRPOME.mjs → chunk-5SSKGS6E.mjs} +3 -783
- package/dist/chunk-5SSKGS6E.mjs.map +1 -0
- package/dist/index.d.mts +1005 -4
- package/dist/index.d.ts +1005 -4
- package/dist/index.js +6043 -213
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5901 -177
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +57 -1
- package/dist/theme/index.d.ts +57 -1
- package/dist/theme/index.js +63 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +2 -1
- package/dist/theme/tokens-css.d.mts +18 -0
- package/dist/theme/tokens-css.d.ts +18 -0
- package/dist/theme/tokens-css.js +289 -0
- package/dist/theme/tokens-css.js.map +1 -0
- package/dist/theme/tokens-css.mjs +24 -0
- package/dist/theme/tokens-css.mjs.map +1 -0
- package/docs/WEB_SETUP.md +217 -0
- package/package.json +17 -5
- package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.stories.tsx +45 -0
- package/src/components/custom/Gauge/Gauge.test.tsx +71 -0
- package/src/components/custom/Gauge/Gauge.tsx +167 -0
- package/src/components/custom/Gauge/index.ts +2 -0
- package/src/components/custom/Scatter/Scatter.stories.tsx +85 -0
- package/src/components/custom/Scatter/Scatter.test.tsx +96 -0
- package/src/components/custom/Scatter/Scatter.tsx +260 -0
- package/src/components/custom/Scatter/index.ts +2 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +1 -1
- package/src/components/custom/Treemap/Treemap.stories.tsx +60 -0
- package/src/components/custom/Treemap/Treemap.test.tsx +67 -0
- package/src/components/custom/Treemap/Treemap.tsx +192 -0
- package/src/components/custom/Treemap/index.ts +2 -0
- package/src/components/custom/Workout/BaseBadge.stories.tsx +75 -0
- package/src/components/custom/Workout/BaseBadge.test.tsx +102 -0
- package/src/components/custom/Workout/BaseBadge.tsx +113 -0
- package/src/components/custom/Workout/BodyMap.stories.tsx +121 -0
- package/src/components/custom/Workout/BodyMap.test.tsx +124 -0
- package/src/components/custom/Workout/BodyMap.tsx +348 -0
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +171 -0
- package/src/components/custom/Workout/BodyMapDetailPanel.test.tsx +225 -0
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +478 -0
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +125 -0
- package/src/components/custom/Workout/CapacityBandChart.test.tsx +193 -0
- package/src/components/custom/Workout/CapacityBandChart.tsx +493 -0
- package/src/components/custom/Workout/DeviationBar.stories.tsx +58 -0
- package/src/components/custom/Workout/DeviationBar.test.tsx +86 -0
- package/src/components/custom/Workout/DeviationBar.tsx +78 -0
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +186 -0
- package/src/components/custom/Workout/ExerciseCard.test.tsx +319 -0
- package/src/components/custom/Workout/ExerciseCard.tsx +429 -0
- package/src/components/custom/Workout/InputBar.stories.tsx +75 -0
- package/src/components/custom/Workout/InputBar.test.tsx +116 -0
- package/src/components/custom/Workout/InputBar.tsx +163 -0
- package/src/components/custom/Workout/IntensityBar.stories.tsx +60 -0
- package/src/components/custom/Workout/IntensityBar.test.tsx +113 -0
- package/src/components/custom/Workout/IntensityBar.tsx +166 -0
- package/src/components/custom/Workout/MesoCard.stories.tsx +152 -0
- package/src/components/custom/Workout/MesoCard.test.tsx +170 -0
- package/src/components/custom/Workout/MesoCard.tsx +235 -0
- package/src/components/custom/Workout/MesoProgressBar.stories.tsx +85 -0
- package/src/components/custom/Workout/MesoProgressBar.test.tsx +141 -0
- package/src/components/custom/Workout/MesoProgressBar.tsx +161 -0
- package/src/components/custom/Workout/MesoStatusCard.stories.tsx +126 -0
- package/src/components/custom/Workout/MesoStatusCard.test.tsx +209 -0
- package/src/components/custom/Workout/MesoStatusCard.tsx +456 -0
- package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +118 -0
- package/src/components/custom/Workout/MuscleGroupChip.test.tsx +91 -0
- package/src/components/custom/Workout/MuscleGroupChip.tsx +92 -0
- package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +94 -0
- package/src/components/custom/Workout/PlaceholderStrip.test.tsx +83 -0
- package/src/components/custom/Workout/PlaceholderStrip.tsx +91 -0
- package/src/components/custom/Workout/PrBadge.stories.tsx +105 -0
- package/src/components/custom/Workout/PrBadge.test.tsx +111 -0
- package/src/components/custom/Workout/PrBadge.tsx +103 -0
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +116 -0
- package/src/components/custom/Workout/PrHistoryModal.test.tsx +161 -0
- package/src/components/custom/Workout/PrHistoryModal.tsx +244 -0
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +126 -0
- package/src/components/custom/Workout/ReadinessCheck.test.tsx +181 -0
- package/src/components/custom/Workout/ReadinessCheck.tsx +266 -0
- package/src/components/custom/Workout/RestTimer.tsx +8 -6
- package/src/components/custom/Workout/SetRow.stories.tsx +165 -0
- package/src/components/custom/Workout/SetRow.test.tsx +222 -0
- package/src/components/custom/Workout/SetRow.tsx +253 -0
- package/src/components/custom/Workout/Sparkline.stories.tsx +125 -0
- package/src/components/custom/Workout/Sparkline.test.tsx +113 -0
- package/src/components/custom/Workout/Sparkline.tsx +188 -0
- package/src/components/custom/Workout/StatusDot.stories.tsx +150 -0
- package/src/components/custom/Workout/StatusDot.test.tsx +165 -0
- package/src/components/custom/Workout/StatusDot.tsx +159 -0
- package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +130 -0
- package/src/components/custom/Workout/StrengthTrendChart.test.tsx +217 -0
- package/src/components/custom/Workout/StrengthTrendChart.tsx +628 -0
- package/src/components/custom/Workout/SupersetWrapper.tsx +1 -2
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +66 -0
- package/src/components/custom/Workout/TempoDisplay.test.tsx +90 -0
- package/src/components/custom/Workout/TempoDisplay.tsx +207 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +125 -0
- package/src/components/custom/Workout/VelocityStrip.test.tsx +234 -0
- package/src/components/custom/Workout/VelocityStrip.tsx +287 -0
- package/src/components/custom/Workout/WeekRow.stories.tsx +121 -0
- package/src/components/custom/Workout/WeekRow.test.tsx +117 -0
- package/src/components/custom/Workout/WeekRow.tsx +140 -0
- package/src/components/custom/Workout/WeightBadge.stories.tsx +87 -0
- package/src/components/custom/Workout/WeightBadge.test.tsx +164 -0
- package/src/components/custom/Workout/WeightBadge.tsx +113 -0
- package/src/components/custom/Workout/WorkoutCard.stories.tsx +135 -0
- package/src/components/custom/Workout/WorkoutCard.test.tsx +172 -0
- package/src/components/custom/Workout/WorkoutCard.tsx +226 -0
- package/src/components/custom/Workout/WorkoutPill.stories.tsx +63 -0
- package/src/components/custom/Workout/WorkoutPill.test.tsx +153 -0
- package/src/components/custom/Workout/WorkoutPill.tsx +177 -0
- package/src/components/custom/Workout/index.ts +113 -0
- package/src/components/custom/Workout/muscleTaxonomy.ts +252 -0
- package/src/components/custom/index.ts +3 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
- package/src/components/custom/stepper/Stepper.tsx +2 -2
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
- package/src/components/ui/card/Card.tsx +1 -1
- package/src/components/ui/checkbox/Checkbox.tsx +1 -1
- package/src/components/ui/collapse/Collapse.tsx +1 -1
- package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
- package/src/components/ui/drawer/Drawer.tsx +3 -3
- package/src/components/ui/form-field/FormField.tsx +1 -1
- package/src/components/ui/help-tip/HelpTip.tsx +1 -1
- package/src/components/ui/menu/Menu.tsx +2 -2
- package/src/components/ui/pill/Pill.tsx +1 -1
- package/src/components/ui/popover/Popover.stories.tsx +2 -2
- package/src/components/ui/popover/Popover.tsx +1 -1
- package/src/components/ui/radio/Radio.stories.tsx +1 -1
- package/src/components/ui/section/Section.stories.tsx +4 -4
- package/src/components/ui/select/Select.tsx +1 -1
- package/src/components/ui/stack/Stack.stories.tsx +4 -4
- package/src/components/ui/tabs/Tabs.tsx +2 -2
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +1 -1
- package/src/examples/react-hook-form/ControlledFields.tsx +171 -0
- package/src/examples/react-hook-form/ReactHookForm.stories.tsx +51 -0
- package/src/examples/react-hook-form/ReactHookForm.test.tsx +61 -0
- package/src/examples/react-hook-form/RhfContactForm.tsx +99 -0
- package/src/stories/PresetShowcase.stories.tsx +15 -15
- package/src/theme/index.ts +2 -1
- package/src/theme/manifest/css-property-manifest.example.json +79 -0
- package/src/theme/manifest/css-property-manifest.schema.json +93 -0
- package/src/theme/manifest/css-property-manifest.test.ts +81 -0
- package/src/theme/manifest/css-property-manifest.types.ts +49 -0
- package/src/theme/manifest/css-property-manifest.validate.ts +84 -0
- package/src/theme/manifest/index.ts +11 -0
- package/src/theme/tokens-css.test.ts +28 -0
- package/src/theme/tokens-css.ts +34 -0
- package/src/theme/workout-tokens.ts +61 -0
- package/dist/chunk-UXVRPOME.mjs.map +0 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { View, Text } from 'react-native'
|
|
4
|
+
import { StrengthTrendChart } from './StrengthTrendChart'
|
|
5
|
+
import type { StrengthTrendDataPoint } from './StrengthTrendChart'
|
|
6
|
+
|
|
7
|
+
const actual: StrengthTrendDataPoint[] = [
|
|
8
|
+
{ date: '2026-01-06', e1rm: 215, sessionLabel: 'Jan 6' },
|
|
9
|
+
{ date: '2026-01-20', e1rm: 222, sessionLabel: 'Jan 20' },
|
|
10
|
+
{ date: '2026-02-03', e1rm: 228, isPR: true, sessionLabel: 'Feb 3' },
|
|
11
|
+
{ date: '2026-02-17', e1rm: 226, sessionLabel: 'Feb 17' },
|
|
12
|
+
{ date: '2026-03-03', e1rm: 235, sessionLabel: 'Mar 3' },
|
|
13
|
+
{ date: '2026-03-17', e1rm: 244, isPR: true, sessionLabel: 'Mar 17' },
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
const projection: StrengthTrendDataPoint[] = [
|
|
17
|
+
{ date: '2026-03-17', e1rm: 244 },
|
|
18
|
+
{ date: '2026-03-31', e1rm: 252 },
|
|
19
|
+
{ date: '2026-04-14', e1rm: 261 },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const mesoBoundaries = [
|
|
23
|
+
{ date: '2026-02-03', label: 'Intensify' },
|
|
24
|
+
{ date: '2026-03-17', label: 'Peak' },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
const decline: StrengthTrendDataPoint[] = [
|
|
28
|
+
{ date: '2026-03-03', e1rm: 248 },
|
|
29
|
+
{ date: '2026-03-10', e1rm: 244 },
|
|
30
|
+
{ date: '2026-03-17', e1rm: 239 },
|
|
31
|
+
{ date: '2026-03-24', e1rm: 235 },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const meta: Meta<typeof StrengthTrendChart> = {
|
|
35
|
+
title: 'Custom/Workout/StrengthTrendChart',
|
|
36
|
+
component: StrengthTrendChart,
|
|
37
|
+
tags: ['autodocs'],
|
|
38
|
+
argTypes: {
|
|
39
|
+
width: { control: { type: 'range', min: 160, max: 480, step: 10 }, description: 'Plot width in px' },
|
|
40
|
+
height: { control: { type: 'range', min: 100, max: 220, step: 10 }, description: 'Plot height in px (120-160 for compact)' },
|
|
41
|
+
unit: { control: 'inline-radio', options: ['lbs', 'kg'], description: 'Display unit' },
|
|
42
|
+
animateOnMount: { control: 'boolean', description: 'Left-to-right draw animation on mount' },
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default meta
|
|
47
|
+
type Story = StoryObj<typeof StrengthTrendChart>
|
|
48
|
+
|
|
49
|
+
export const Default: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
data: actual,
|
|
52
|
+
projection,
|
|
53
|
+
mesoBoundaries,
|
|
54
|
+
width: 340,
|
|
55
|
+
height: 150,
|
|
56
|
+
unit: 'lbs',
|
|
57
|
+
animateOnMount: false,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const Compact: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
...Default.args,
|
|
64
|
+
width: 240,
|
|
65
|
+
height: 120,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const WithoutProjection: Story = {
|
|
70
|
+
args: {
|
|
71
|
+
...Default.args,
|
|
72
|
+
projection: undefined,
|
|
73
|
+
mesoBoundaries: undefined,
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const Declining: Story = {
|
|
78
|
+
args: {
|
|
79
|
+
data: decline,
|
|
80
|
+
width: 340,
|
|
81
|
+
height: 150,
|
|
82
|
+
unit: 'lbs',
|
|
83
|
+
animateOnMount: false,
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const SinglePoint: Story = {
|
|
88
|
+
args: {
|
|
89
|
+
data: [{ date: '2026-03-17', e1rm: 244, isPR: true, sessionLabel: 'Mar 17' }],
|
|
90
|
+
width: 340,
|
|
91
|
+
height: 150,
|
|
92
|
+
unit: 'kg',
|
|
93
|
+
animateOnMount: false,
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const Empty: Story = {
|
|
98
|
+
args: {
|
|
99
|
+
data: [],
|
|
100
|
+
width: 340,
|
|
101
|
+
height: 150,
|
|
102
|
+
unit: 'lbs',
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function InteractiveChart() {
|
|
107
|
+
const [selected, setSelected] = useState<StrengthTrendDataPoint | null>(null)
|
|
108
|
+
return (
|
|
109
|
+
<View style={{ maxWidth: 380, padding: 16 }}>
|
|
110
|
+
<StrengthTrendChart
|
|
111
|
+
data={actual}
|
|
112
|
+
projection={projection}
|
|
113
|
+
mesoBoundaries={mesoBoundaries}
|
|
114
|
+
width={340}
|
|
115
|
+
height={150}
|
|
116
|
+
unit="lbs"
|
|
117
|
+
onPointPress={setSelected}
|
|
118
|
+
/>
|
|
119
|
+
<Text style={{ marginTop: 12, fontSize: 12, color: '#9CA3AF' }}>
|
|
120
|
+
{selected
|
|
121
|
+
? `Tapped ${selected.sessionLabel ?? selected.date}: ${selected.e1rm} lbs`
|
|
122
|
+
: 'Tap a point on the chart.'}
|
|
123
|
+
</Text>
|
|
124
|
+
</View>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const Interactive: Story = {
|
|
129
|
+
render: () => <InteractiveChart />,
|
|
130
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { StrengthTrendChart } from './StrengthTrendChart'
|
|
5
|
+
import type { StrengthTrendDataPoint } from './StrengthTrendChart'
|
|
6
|
+
|
|
7
|
+
const data: StrengthTrendDataPoint[] = [
|
|
8
|
+
{ date: '2026-01-06', e1rm: 215, sessionLabel: 'Jan 6' },
|
|
9
|
+
{ date: '2026-02-03', e1rm: 228, isPR: true, sessionLabel: 'Feb 3' },
|
|
10
|
+
{ date: '2026-03-17', e1rm: 244, isPR: true, sessionLabel: 'Mar 17' },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
const projection: StrengthTrendDataPoint[] = [
|
|
14
|
+
{ date: '2026-03-17', e1rm: 244 },
|
|
15
|
+
{ date: '2026-04-14', e1rm: 261 },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const mesoBoundaries = [
|
|
19
|
+
{ date: '2026-02-03', label: 'Intensify' },
|
|
20
|
+
{ date: '2026-03-17', label: 'Peak' },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const baseProps = {
|
|
24
|
+
data,
|
|
25
|
+
width: 340,
|
|
26
|
+
height: 150,
|
|
27
|
+
unit: 'lbs' as const,
|
|
28
|
+
animateOnMount: false,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('StrengthTrendChart', () => {
|
|
32
|
+
describe('rendering', () => {
|
|
33
|
+
it('renders the chart container and labelled canvas', () => {
|
|
34
|
+
render(<StrengthTrendChart {...baseProps} />)
|
|
35
|
+
expect(screen.getByTestId('strength-trend-chart')).toBeInTheDocument()
|
|
36
|
+
expect(screen.getByTestId('strength-trend-chart-canvas')).toBeInTheDocument()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('renders a pressable point per actual data point', () => {
|
|
40
|
+
render(<StrengthTrendChart {...baseProps} />)
|
|
41
|
+
expect(screen.getAllByTestId('strength-trend-chart-point')).toHaveLength(3)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('renders the actual line as connecting segments', () => {
|
|
45
|
+
render(<StrengthTrendChart {...baseProps} />)
|
|
46
|
+
// Two segments connect three points.
|
|
47
|
+
expect(
|
|
48
|
+
screen.getAllByTestId('strength-trend-chart-actual-segment'),
|
|
49
|
+
).toHaveLength(2)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('renders subtle horizontal gridlines', () => {
|
|
53
|
+
render(<StrengthTrendChart {...baseProps} />)
|
|
54
|
+
const grid = screen.getAllByTestId('strength-trend-chart-gridline')
|
|
55
|
+
expect(grid.length).toBeGreaterThanOrEqual(2)
|
|
56
|
+
expect(grid.length).toBeLessThanOrEqual(3)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('renders a star marker for each PR point', () => {
|
|
60
|
+
render(<StrengthTrendChart {...baseProps} />)
|
|
61
|
+
expect(screen.getAllByTestId('strength-trend-chart-pr-star')).toHaveLength(2)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('renders an Actual / Projected / PR legend', () => {
|
|
65
|
+
render(<StrengthTrendChart {...baseProps} />)
|
|
66
|
+
const legend = screen.getByTestId('strength-trend-chart-legend')
|
|
67
|
+
expect(legend).toHaveTextContent('Actual')
|
|
68
|
+
expect(legend).toHaveTextContent('Projected')
|
|
69
|
+
expect(legend).toHaveTextContent('PR')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('projection and meso boundaries', () => {
|
|
74
|
+
it('renders dashed projection segments when projection is provided', () => {
|
|
75
|
+
render(<StrengthTrendChart {...baseProps} projection={projection} />)
|
|
76
|
+
expect(
|
|
77
|
+
screen.getAllByTestId('strength-trend-chart-projection-segment'),
|
|
78
|
+
).toHaveLength(1)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('omits projection segments when none provided', () => {
|
|
82
|
+
render(<StrengthTrendChart {...baseProps} />)
|
|
83
|
+
expect(
|
|
84
|
+
screen.queryByTestId('strength-trend-chart-projection-segment'),
|
|
85
|
+
).not.toBeInTheDocument()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('renders a boundary marker and axis label per meso boundary', () => {
|
|
89
|
+
render(<StrengthTrendChart {...baseProps} mesoBoundaries={mesoBoundaries} />)
|
|
90
|
+
expect(
|
|
91
|
+
screen.getAllByTestId('strength-trend-chart-meso-boundary'),
|
|
92
|
+
).toHaveLength(2)
|
|
93
|
+
expect(
|
|
94
|
+
screen.getAllByTestId('strength-trend-chart-axis-label'),
|
|
95
|
+
).toHaveLength(2)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('trend pill', () => {
|
|
100
|
+
it('shows a positive percentage for an improving series', () => {
|
|
101
|
+
render(<StrengthTrendChart {...baseProps} />)
|
|
102
|
+
const pill = screen.getByTestId('strength-trend-chart-trend-pill')
|
|
103
|
+
expect(pill).toHaveTextContent('this meso')
|
|
104
|
+
expect(pill.textContent?.startsWith('+')).toBe(true)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('shows a negative percentage for a declining series', () => {
|
|
108
|
+
render(
|
|
109
|
+
<StrengthTrendChart
|
|
110
|
+
{...baseProps}
|
|
111
|
+
data={[
|
|
112
|
+
{ date: '2026-03-03', e1rm: 248 },
|
|
113
|
+
{ date: '2026-03-17', e1rm: 235 },
|
|
114
|
+
]}
|
|
115
|
+
/>,
|
|
116
|
+
)
|
|
117
|
+
const pill = screen.getByTestId('strength-trend-chart-trend-pill')
|
|
118
|
+
expect(pill.textContent?.startsWith('-')).toBe(true)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('stays neutral instead of using the whole series when the current meso has <2 points', () => {
|
|
122
|
+
render(
|
|
123
|
+
<StrengthTrendChart
|
|
124
|
+
{...baseProps}
|
|
125
|
+
data={[
|
|
126
|
+
{ date: '2026-01-06', e1rm: 200 },
|
|
127
|
+
{ date: '2026-02-01', e1rm: 210 },
|
|
128
|
+
{ date: '2026-03-20', e1rm: 250 },
|
|
129
|
+
]}
|
|
130
|
+
mesoBoundaries={[{ date: '2026-03-01', label: 'Peak' }]}
|
|
131
|
+
/>,
|
|
132
|
+
)
|
|
133
|
+
// Only one point falls in the current meso, so the pill must not report
|
|
134
|
+
// the whole-series jump (+25%) under the "this meso" label.
|
|
135
|
+
const pill = screen.getByTestId('strength-trend-chart-trend-pill')
|
|
136
|
+
expect(pill).toHaveTextContent('+0.0% this meso')
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('interaction', () => {
|
|
141
|
+
it('fires onPointPress with the tapped point', () => {
|
|
142
|
+
const onPointPress = vi.fn()
|
|
143
|
+
render(<StrengthTrendChart {...baseProps} onPointPress={onPointPress} />)
|
|
144
|
+
fireEvent.click(screen.getAllByTestId('strength-trend-chart-point')[1])
|
|
145
|
+
expect(onPointPress).toHaveBeenCalledWith(
|
|
146
|
+
expect.objectContaining({ e1rm: 228 }),
|
|
147
|
+
)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('shows a tooltip with value and unit after a point is tapped', () => {
|
|
151
|
+
render(<StrengthTrendChart {...baseProps} />)
|
|
152
|
+
expect(
|
|
153
|
+
screen.queryByTestId('strength-trend-chart-tooltip'),
|
|
154
|
+
).not.toBeInTheDocument()
|
|
155
|
+
fireEvent.click(screen.getAllByTestId('strength-trend-chart-point')[2])
|
|
156
|
+
const tooltip = screen.getByTestId('strength-trend-chart-tooltip')
|
|
157
|
+
expect(tooltip).toHaveTextContent('244 lbs')
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('empty state', () => {
|
|
162
|
+
it('renders an empty placeholder when there is no data', () => {
|
|
163
|
+
render(<StrengthTrendChart {...baseProps} data={[]} />)
|
|
164
|
+
expect(
|
|
165
|
+
screen.getByTestId('strength-trend-chart-empty'),
|
|
166
|
+
).toBeInTheDocument()
|
|
167
|
+
expect(
|
|
168
|
+
screen.queryByTestId('strength-trend-chart-canvas'),
|
|
169
|
+
).not.toBeInTheDocument()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('shows the placeholder when data is empty even if a projection exists', () => {
|
|
173
|
+
render(<StrengthTrendChart {...baseProps} data={[]} projection={projection} />)
|
|
174
|
+
expect(
|
|
175
|
+
screen.getByTestId('strength-trend-chart-empty'),
|
|
176
|
+
).toBeInTheDocument()
|
|
177
|
+
expect(
|
|
178
|
+
screen.queryByTestId('strength-trend-chart-canvas'),
|
|
179
|
+
).not.toBeInTheDocument()
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('accessibility', () => {
|
|
184
|
+
it('labels the chart as an image with current value and trend', () => {
|
|
185
|
+
render(<StrengthTrendChart {...baseProps} />)
|
|
186
|
+
const canvas = screen.getByTestId('strength-trend-chart-canvas')
|
|
187
|
+
expect(canvas).toHaveAttribute('role', 'img')
|
|
188
|
+
expect(canvas.getAttribute('aria-label')).toContain('Current: 244 lbs')
|
|
189
|
+
expect(canvas.getAttribute('aria-label')).toContain('Trend:')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('labels the empty state accessibly', () => {
|
|
193
|
+
render(<StrengthTrendChart {...baseProps} data={[]} />)
|
|
194
|
+
expect(
|
|
195
|
+
screen.getByLabelText('Strength trend chart, no data'),
|
|
196
|
+
).toBeInTheDocument()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('has no accessibility violations', async () => {
|
|
200
|
+
const { container } = render(<StrengthTrendChart {...baseProps} />)
|
|
201
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('has no accessibility violations with all features enabled', async () => {
|
|
205
|
+
const { container } = render(
|
|
206
|
+
<StrengthTrendChart
|
|
207
|
+
{...baseProps}
|
|
208
|
+
projection={projection}
|
|
209
|
+
mesoBoundaries={mesoBoundaries}
|
|
210
|
+
onPointPress={vi.fn()}
|
|
211
|
+
unit="kg"
|
|
212
|
+
/>,
|
|
213
|
+
)
|
|
214
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
})
|