Skip to content

Commit bc028c8

Browse files
matyasfclaude
andcommitted
test(cypress): fix Drilldown keyboard-nav flake and Tabs controlled-switch tests
Drilldown: park cursor in beforeEach, enter via menu.focus, fix stale role="button" selector. Tabs: control panel switching via stateful wrapper, drop obsolete React.createElement key spy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a4e0a26 commit bc028c8

2 files changed

Lines changed: 36 additions & 84 deletions

File tree

cypress/component/Drilldown.cy.tsx

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ const renderOptions = (page: string) => {
4343
}
4444

4545
describe('<Drilldown/>', () => {
46+
beforeEach(() => {
47+
cy.get('body').realMouseMove(0, 0, { position: 'bottomRight' })
48+
})
49+
4650
it('should disabled prop prevent option actions', () => {
4751
cy.mount(
4852
<Drilldown rootPageId="page0" disabled>
@@ -57,10 +61,6 @@ describe('<Drilldown/>', () => {
5761
</Drilldown>
5862
)
5963
cy.contains('Option-0').realClick()
60-
61-
// `disabled` prevents the option action, so clicking Option-0 must NOT
62-
// navigate to its subPage — Option-1 (on page1) should never appear and
63-
// we stay on page0.
6464
cy.contains('Option-0').should('be.visible')
6565
cy.contains('Option-1').should('not.exist')
6666
})
@@ -96,8 +96,7 @@ describe('<Drilldown/>', () => {
9696
</Drilldown.Page>
9797
</Drilldown>
9898
)
99-
cy.get('body').realClick()
100-
cy.get('body').realPress('Tab')
99+
cy.get('div[role="menu"]').focus()
101100

102101
cy.realPress('ArrowDown')
103102
cy.focused().should('have.id', 'option01')
@@ -109,12 +108,7 @@ describe('<Drilldown/>', () => {
109108
cy.focused().should('have.id', 'option01')
110109
})
111110

112-
// TODO(INSTUI): flaky under cypress-real-events keyboard navigation with
113-
// retries:0. cy `.focus()` on the menu isn't honored by CDP key dispatch and
114-
// `realClick`+`Tab` is non-deterministic. Needs the component's real focus
115-
// model mapped (post-Tab focus target, how ArrowDown enters options) to make
116-
// it reliably deterministic. Skipped pending that work.
117-
it.skip('should prevent focus rotation in the drilldown with "false"', () => {
111+
it('should prevent focus rotation in the drilldown with "false"', () => {
118112
cy.mount(
119113
<Drilldown rootPageId="page0" rotateFocus={false}>
120114
<Drilldown.Page id="page0">
@@ -123,8 +117,7 @@ describe('<Drilldown/>', () => {
123117
</Drilldown.Page>
124118
</Drilldown>
125119
)
126-
cy.get('body').realClick()
127-
cy.get('body').realPress('Tab')
120+
cy.get('div[role="menu"]').focus()
128121

129122
cy.realPress('ArrowDown')
130123
cy.focused().should('have.id', 'option01')
@@ -461,10 +454,7 @@ describe('<Drilldown/>', () => {
461454
cy.contains('Option01').should('be.visible')
462455
})
463456

464-
// TODO(INSTUI): flaky under cypress-real-events keyboard navigation with
465-
// retries:0 (see the skipped focus-rotation test above). Skipped pending a
466-
// deterministic real-events focus approach.
467-
it.skip('should be able to navigate between options with up/down arrows', () => {
457+
it('should be able to navigate between options with up/down arrows', () => {
468458
cy.mount(
469459
<Drilldown rootPageId="page0">
470460
<Drilldown.Page id="page0">
@@ -491,10 +481,7 @@ describe('<Drilldown/>', () => {
491481
cy.get('#opt_1').should('have.focus')
492482
})
493483

494-
// TODO(INSTUI): flaky under cypress-real-events keyboard navigation with
495-
// retries:0 (see the skipped focus-rotation test above). Skipped pending a
496-
// deterministic real-events focus approach.
497-
it.skip('should be able to navigate forward between pages with right arrow', () => {
484+
it('should be able to navigate forward between pages with right arrow', () => {
498485
cy.mount(
499486
<Drilldown rootPageId="page0">
500487
<Drilldown.Page id="page0" renderTitle={'Page 0'}>
@@ -532,7 +519,7 @@ describe('<Drilldown/>', () => {
532519
// next arrowDown should skip the header Title and focus on 'To Page 2' option
533520
cy.realPress('ArrowDown')
534521
cy.contains('[id^="DrilldownHeader-Title_"]', 'Page 1').should('be.visible')
535-
cy.contains('[role="button"]', 'To Page 2').should('have.focus')
522+
cy.get('#opt5').should('have.focus').and('have.text', 'To Page 2')
536523

537524
// go to Page 2
538525
cy.realPress('ArrowRight')

cypress/component/Tabs.cy.tsx

Lines changed: 26 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -24,58 +24,11 @@
2424

2525
import React from 'react'
2626
import { Tabs } from '@instructure/ui/latest'
27-
import { expect } from 'chai'
2827

2928
import '../support/component'
3029
import 'cypress-real-events'
3130

3231
describe('<Tabs/>', () => {
33-
// TODO(INSTUI): outdated approach — spies on React.createElement and inspects
34-
// args for `key`, but under the automatic JSX runtime / emotion `jsx` pragma
35-
// JSX no longer compiles to React.createElement (spy captures nothing), and
36-
// `key` isn't exposed on props anyway. Needs a rewrite that asserts observable
37-
// key-preservation behavior. Skipped pending that.
38-
it.skip('should preserve Tab.Panel keys', () => {
39-
const createElementSpy = cy.spy(React, 'createElement')
40-
cy.mount(
41-
<Tabs>
42-
<Tabs.Panel renderTitle="foo" key="foo-key" />
43-
</Tabs>
44-
)
45-
46-
cy.wrap(createElementSpy)
47-
.should('have.been.called')
48-
.then((spy) => {
49-
const createFooTabCall = spy
50-
.getCalls()
51-
.filter((call) => call.args[1]?.renderTitle === 'foo')[0]
52-
53-
expect(createFooTabCall.args[1].key).to.equal('foo-key')
54-
55-
// Set two child
56-
cy.mount(
57-
<Tabs>
58-
<Tabs.Panel renderTitle="foo" key="foo-key" />
59-
<Tabs.Panel renderTitle="bar" key="bar-key" />
60-
</Tabs>
61-
)
62-
63-
cy.wrap(createElementSpy)
64-
.should('have.been.called')
65-
.then((updatedSpy) => {
66-
const createFooTabCall = updatedSpy
67-
.getCalls()
68-
.filter((call) => call.args[1]?.renderTitle === 'foo')[0]
69-
const createBarTabCall = updatedSpy
70-
.getCalls()
71-
.filter((call) => call.args[1]?.renderTitle === 'bar')[0]
72-
73-
expect(createFooTabCall.args[1].key).to.equal('foo-key')
74-
expect(createBarTabCall.args[1].key).to.equal('bar-key')
75-
})
76-
})
77-
})
78-
7932
it('should call onRequestTabChange with keyboard arrow keys', () => {
8033
const onChange = cy.stub()
8134

@@ -117,27 +70,39 @@ describe('<Tabs/>', () => {
11770
cy.wrap(onChange).its('lastCall.args[1].index').should('equal', 1)
11871
})
11972

120-
// TODO(INSTUI): the tab click does not switch the panel under cypress-real-
121-
// events (panel stays display:none after clicking the second tab; neither
122-
// native .click() nor .realClick() switches it, and the tab exposes no
123-
// aria-selected to assert against). Needs interaction/DOM investigation.
124-
it.skip('should keep non-selected panel hidden when unmountOnExit is false', () => {
125-
cy.mount(
126-
<Tabs>
127-
<Tabs.Panel renderTitle="First Tab" unmountOnExit={false}>
128-
Tab 1 content
129-
</Tabs.Panel>
130-
<Tabs.Panel renderTitle="Second Tab">Tab 2 content</Tabs.Panel>
131-
</Tabs>
132-
)
73+
it('should keep non-selected panel hidden when unmountOnExit is false', () => {
74+
// Tabs is a controlled component: a tab click only fires onRequestTabChange,
75+
// so the parent must own the selected index for the panel to actually switch.
76+
const Example = () => {
77+
const [selectedIndex, setSelectedIndex] = React.useState(0)
78+
return (
79+
<Tabs
80+
onRequestTabChange={(_event: unknown, { index }: { index: number }) =>
81+
setSelectedIndex(index)
82+
}
83+
>
84+
<Tabs.Panel
85+
renderTitle="First Tab"
86+
isSelected={selectedIndex === 0}
87+
unmountOnExit={false}
88+
>
89+
Tab 1 content
90+
</Tabs.Panel>
91+
<Tabs.Panel renderTitle="Second Tab" isSelected={selectedIndex === 1}>
92+
Tab 2 content
93+
</Tabs.Panel>
94+
</Tabs>
95+
)
96+
}
97+
98+
cy.mount(<Example />)
13399

134100
cy.get('[role="tabpanel"]').should('have.length', 2)
135101

136102
cy.get('[role="tabpanel"]').eq(0).should('not.have.css', 'display', 'none')
137103
cy.get('[role="tabpanel"]').eq(1).should('have.css', 'display', 'none')
138104

139105
cy.contains('[role="tab"]', 'Second Tab').click()
140-
cy.wait(100)
141106
cy.get('[role="tabpanel"]').eq(1).should('not.have.css', 'display', 'none')
142107
cy.get('[role="tabpanel"]').eq(0).should('have.css', 'display', 'none')
143108
})

0 commit comments

Comments
 (0)