Skip to content

Commit 5a7e274

Browse files
feat: update OBS action types in tests and enhance unit test synchronization rules
1 parent 7ab5c84 commit 5a7e274

2 files changed

Lines changed: 31 additions & 36 deletions

File tree

.github/instructions/lessons.instructions.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,11 @@ applyTo: "**"
2424
6. Capture any user corrections in this file immediately.
2525

2626
**The predictability contract:** The user relies on this workflow being executed identically every time so they can trust and expand on it. Deviating silently — even when the implementation seems straightforward — breaks that contract.
27+
28+
## 2026-05-21 — Always keep unit tests in sync with component API changes
29+
30+
**Mistake:** Refactored OBS action types from a generic `{ type: 'obs', operation: '...' }` to discrete types (`obs-record`, `obs-scene`, etc.) without updating the unit tests in `src/__tests__/components.test.jsx`. This caused 8 test failures in CI.
31+
32+
**Rule:** Whenever a component's rendered output, props, or action API changes, immediately update the corresponding tests **in the same change**. Never ship a feature without running `npx vitest run` and confirming all tests pass. If tests were written against an old API, rewrite them to match the current API — do not delete them without replacement.
33+
34+
**Checklist addition:** After every non-trivial code change, explicitly run `npx vitest run` as a step in the workflow — not just `npx vite build`.

src/__tests__/components.test.jsx

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -167,73 +167,60 @@ describe('ButtonGrid', () => {
167167
// ─── ActionSection — OBS ─────────────────────────────────────────────────────
168168

169169
describe('ActionSection — OBS action', () => {
170-
const obsAction = { type: 'obs', operation: 'toggle-record' }
171170
const noop = () => {}
172171

173-
it('renders the OBS Studio action chip', () => {
174-
render(<ActionSection action={obsAction} onChange={noop} />)
175-
expect(screen.getByText('OBS Studio')).toBeInTheDocument()
172+
it('renders the OBS record chip with label "Record"', () => {
173+
render(<ActionSection action={{ type: 'obs-record' }} onChange={noop} />)
174+
expect(screen.getByText('Record')).toBeInTheDocument()
176175
})
177176

178-
it('renders the operation select dropdown', () => {
179-
render(<ActionSection action={obsAction} onChange={noop} />)
180-
expect(screen.getByDisplayValue('Toggle record')).toBeInTheDocument()
177+
it('renders an action hint for obs-record', () => {
178+
render(<ActionSection action={{ type: 'obs-record' }} onChange={noop} />)
179+
expect(screen.getByText(/toggles OBS recording/i)).toBeInTheDocument()
181180
})
182181

183-
it('does not show the scene picker when operation is not switch-scene', () => {
184-
render(<ActionSection action={obsAction} onChange={noop} />)
185-
expect(screen.queryByText('Scene name')).not.toBeInTheDocument()
182+
it('does not show a scene picker for obs-record', () => {
183+
render(<ActionSection action={{ type: 'obs-record' }} onChange={noop} />)
184+
expect(screen.queryByPlaceholderText(/connect OBS to pick/i)).not.toBeInTheDocument()
186185
})
187186

188-
it('shows a text input for scene name when operation=switch-scene and obsScenes=[]', () => {
189-
const action = { type: 'obs', operation: 'switch-scene', sceneName: '' }
187+
it('shows a text input for scene name when action=obs-scene and obsScenes=[]', () => {
188+
const action = { type: 'obs-scene', sceneName: '' }
190189
render(<ActionSection action={action} onChange={noop} obsScenes={[]} />)
191-
expect(screen.getByText('Scene name')).toBeInTheDocument()
192-
// should render an <input type="text">, not a <select>
193190
expect(screen.getByPlaceholderText(/connect OBS to pick/i)).toBeInTheDocument()
194191
})
195192

196-
it('shows a <select> dropdown when obsScenes has entries', () => {
197-
const action = { type: 'obs', operation: 'switch-scene', sceneName: 'Gaming' }
193+
it('shows a <select> dropdown when action=obs-scene and obsScenes has entries', () => {
194+
const action = { type: 'obs-scene', sceneName: 'Gaming' }
198195
const scenes = ['Gaming', 'Just Chatting', 'BRB']
199196
render(<ActionSection action={action} onChange={noop} obsScenes={scenes} />)
200197

201-
const selects = screen.getAllByRole('combobox')
202-
// The last select should be the scene picker
203-
const scenePicker = selects[selects.length - 1]
198+
const scenePicker = screen.getByRole('combobox')
204199
expect(scenePicker).toBeInTheDocument()
205200
expect(screen.getByText('Gaming')).toBeInTheDocument()
206201
expect(screen.getByText('Just Chatting')).toBeInTheDocument()
207202
expect(screen.getByText('BRB')).toBeInTheDocument()
208203
})
209204

210-
it('includes all four OBS operations in the select', () => {
211-
render(<ActionSection action={obsAction} onChange={noop} />)
212-
const select = screen.getByRole('combobox')
213-
const options = [...select.querySelectorAll('option')].map(o => o.value)
214-
expect(options).toContain('toggle-record')
215-
expect(options).toContain('toggle-stream')
216-
expect(options).toContain('toggle-pause-record')
217-
expect(options).toContain('switch-scene')
218-
})
219-
220-
it('calls onChange with updated operation when user selects one', () => {
205+
it('calls onChange with updated sceneName when user picks a scene', () => {
221206
const onChange = vi.fn()
222-
render(<ActionSection action={obsAction} onChange={onChange} />)
223-
fireEvent.change(screen.getByRole('combobox'), { target: { value: 'toggle-stream' } })
207+
const action = { type: 'obs-scene', sceneName: 'Gaming' }
208+
const scenes = ['Gaming', 'Just Chatting', 'BRB']
209+
render(<ActionSection action={action} onChange={onChange} obsScenes={scenes} />)
210+
fireEvent.change(screen.getByRole('combobox'), { target: { value: 'BRB' } })
224211
expect(onChange).toHaveBeenCalledWith(
225-
expect.objectContaining({ action: expect.objectContaining({ operation: 'toggle-stream' }) })
212+
expect.objectContaining({ action: expect.objectContaining({ sceneName: 'BRB' }) })
226213
)
227214
})
228215

229-
it('renders the remove button', () => {
230-
render(<ActionSection action={obsAction} onChange={noop} />)
216+
it('renders the remove button for obs-record', () => {
217+
render(<ActionSection action={{ type: 'obs-record' }} onChange={noop} />)
231218
expect(screen.getByTitle('Remove action')).toBeInTheDocument()
232219
})
233220

234221
it('calls onChange with action:null when remove button is clicked', () => {
235222
const onChange = vi.fn()
236-
render(<ActionSection action={obsAction} onChange={onChange} />)
223+
render(<ActionSection action={{ type: 'obs-record' }} onChange={onChange} />)
237224
fireEvent.click(screen.getByTitle('Remove action'))
238225
expect(onChange).toHaveBeenCalledWith({ action: null })
239226
})

0 commit comments

Comments
 (0)