Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/__tests__/IssueCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,13 @@
describe('Expansion', () => {
it('shows expand button', () => {
render(<IssueCard issue={mockErrorIssue} />);
expect(screen.getByRole('button', { name: '▶' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /▶/ })).toBeInTheDocument();
});

it('expands when header is clicked', () => {
render(<IssueCard issue={mockErrorIssue} />);

const expandButton = screen.getByRole('button', { name: '▶' });
const expandButton = screen.getByRole('button', { name: /▶/ });
fireEvent.click(expandButton);

expect(screen.getByText('▼')).toBeInTheDocument();
Expand All @@ -138,7 +138,7 @@
const header = screen.getByText('Missing Key in List').closest('.issue-header');
if (header) fireEvent.click(header);

expect(screen.getByText('💡 Suggestion:')).toBeInTheDocument();

Check failure on line 141 in src/__tests__/IssueCard.test.tsx

View workflow job for this annotation

GitHub Actions / Build + Test

src/__tests__/IssueCard.test.tsx > IssueCard > Expansion > shows suggestion when expanded

TestingLibraryElementError: Unable to find an element with the text: 💡 Suggestion:. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <div class="issue-card severity-error" style="border-left-color: rgb(255, 68, 68);" > <div aria-expanded="true" class="issue-header" role="button" tabindex="0" > <span class="issue-icon indicator-dot indicator-dot--error" /> <div class="issue-info" > <div class="issue-title-row" > <h4 class="issue-title" > Missing Key in List </h4> <span class="severity-badge" style="background-color: rgba(255, 68, 68, 0.1); color: rgb(255, 68, 68); border: 1px solid rgb(255, 68, 68);" > Error </span> </div> <div class="issue-location" > <span class="element-type" > li </span> <span class="issue-component" > in <strong> UserList </strong> </span> <span class="component-path" > ( App → Dashboard → UserList ) </span> </div> </div> <span class="expand-button" > ▼ </span> </div> <div class="issue-details" > <p class="issue-message" > List items are missing key props which can cause incorrect rendering </p> <div class="issue-elements" > <strong> Affected Elements: </strong> <div class="elements-list" > <div class="element-item missing-key" > <span class="el-index" > [ 0 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> <div class="element-item missing-key" > <span class="el-index" > [ 1 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> <div class="element-item missing-key" > <span class="el-index" > [ 2 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> </div> </div> <div class="issue-why" > <strong> Why this matters: </strong> <p> Keys help React identify which items have changed, are added, or removed. </p> </div> <div class="issue-suggestion" > <strong> <span class="action-badge action-badge--suggestion" /> Suggestion: </strong>
expect(screen.getByText(/Add a unique key prop/)).toBeInTheDocument();
});
});
Expand Down Expand Up @@ -194,7 +194,7 @@
const header = screen.getByText('Stale Closure Detected').closest('.issue-header');
if (header) fireEvent.click(header);

expect(screen.getByText('🔍 Closure Timeline:')).toBeInTheDocument();

Check failure on line 197 in src/__tests__/IssueCard.test.tsx

View workflow job for this annotation

GitHub Actions / Build + Test

src/__tests__/IssueCard.test.tsx > IssueCard > Stale Closure Info > displays closure timeline

TestingLibraryElementError: Unable to find an element with the text: 🔍 Closure Timeline:. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <div class="issue-card severity-error" style="border-left-color: rgb(255, 68, 68);" > <div aria-expanded="true" class="issue-header" role="button" tabindex="0" > <span class="issue-icon indicator-dot indicator-dot--error" /> <div class="issue-info" > <div class="issue-title-row" > <h4 class="issue-title" > Stale Closure Detected </h4> <span class="severity-badge" style="background-color: rgba(255, 68, 68, 0.1); color: rgb(255, 68, 68); border: 1px solid rgb(255, 68, 68);" > Error </span> </div> <div class="issue-location" > <span class="issue-component" > in <strong> Timer </strong> </span> <span class="component-path" > ( App → Timer ) </span> </div> </div> <span class="expand-button" > ▼ </span> </div> <div class="issue-details" > <p class="issue-message" > Callback is using stale closure values </p> <div class="closure-info" > <strong> <span class="action-badge action-badge--search" /> Closure Timeline: </strong> <div class="closure-timeline" > <div class="timeline-item created" > <span class="timeline-badge" > Created </span> <span class="timeline-detail" > Render # 1 </span> </div> <div class="timeline-arrow" > → </div> <div class="timeline-item executed" > <span class="timeline-badge warning" > Executed </span> <span class="timeline-detail" > Render # 10 </span> </div> </div> <div class="closure-details" > <div class="closure-row" > <span class="closure-label" > Function: </span> <code class="closure-value" > handleTick () </code> </div> <div class="closure-row" > <span class="closure-label" > Type: </span> <span class="closure-type type-setInterval" > setInterval </span> </div> <div class="closure-row" > <span class="closure-label" > Renders behind: </span> <span class="closure-stale-count" > 9 render(s) </span> </div> </div> <div class="captured-values
});

it('shows created and executed render numbers', () => {
Expand Down Expand Up @@ -253,7 +253,7 @@
const header = screen.getByText('Missing Key in List').closest('.issue-header');
if (header) fireEvent.click(header);

expect(screen.getByText('📚 Learn more')).toBeInTheDocument();

Check failure on line 256 in src/__tests__/IssueCard.test.tsx

View workflow job for this annotation

GitHub Actions / Build + Test

src/__tests__/IssueCard.test.tsx > IssueCard > Learn More Link > displays learn more link when available

TestingLibraryElementError: Unable to find an element with the text: 📚 Learn more. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <div class="issue-card severity-error" style="border-left-color: rgb(255, 68, 68);" > <div aria-expanded="true" class="issue-header" role="button" tabindex="0" > <span class="issue-icon indicator-dot indicator-dot--error" /> <div class="issue-info" > <div class="issue-title-row" > <h4 class="issue-title" > Missing Key in List </h4> <span class="severity-badge" style="background-color: rgba(255, 68, 68, 0.1); color: rgb(255, 68, 68); border: 1px solid rgb(255, 68, 68);" > Error </span> </div> <div class="issue-location" > <span class="element-type" > li </span> <span class="issue-component" > in <strong> UserList </strong> </span> <span class="component-path" > ( App → Dashboard → UserList ) </span> </div> </div> <span class="expand-button" > ▼ </span> </div> <div class="issue-details" > <p class="issue-message" > List items are missing key props which can cause incorrect rendering </p> <div class="issue-elements" > <strong> Affected Elements: </strong> <div class="elements-list" > <div class="element-item missing-key" > <span class="el-index" > [ 0 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> <div class="element-item missing-key" > <span class="el-index" > [ 1 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> <div class="element-item missing-key" > <span class="el-index" > [ 2 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> </div> </div> <div class="issue-why" > <strong> Why this matters: </strong> <p> Keys help React identify which items have changed, are added, or removed. </p> </div> <div class="issue-suggestion" > <strong> <span class="action-badge action-badge--suggestion" /> Suggestion: </strong> <
});

it('link opens in new tab', () => {
Expand All @@ -262,7 +262,7 @@
const header = screen.getByText('Missing Key in List').closest('.issue-header');
if (header) fireEvent.click(header);

const link = screen.getByText('📚 Learn more');

Check failure on line 265 in src/__tests__/IssueCard.test.tsx

View workflow job for this annotation

GitHub Actions / Build + Test

src/__tests__/IssueCard.test.tsx > IssueCard > Learn More Link > link opens in new tab

TestingLibraryElementError: Unable to find an element with the text: 📚 Learn more. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <div class="issue-card severity-error" style="border-left-color: rgb(255, 68, 68);" > <div aria-expanded="true" class="issue-header" role="button" tabindex="0" > <span class="issue-icon indicator-dot indicator-dot--error" /> <div class="issue-info" > <div class="issue-title-row" > <h4 class="issue-title" > Missing Key in List </h4> <span class="severity-badge" style="background-color: rgba(255, 68, 68, 0.1); color: rgb(255, 68, 68); border: 1px solid rgb(255, 68, 68);" > Error </span> </div> <div class="issue-location" > <span class="element-type" > li </span> <span class="issue-component" > in <strong> UserList </strong> </span> <span class="component-path" > ( App → Dashboard → UserList ) </span> </div> </div> <span class="expand-button" > ▼ </span> </div> <div class="issue-details" > <p class="issue-message" > List items are missing key props which can cause incorrect rendering </p> <div class="issue-elements" > <strong> Affected Elements: </strong> <div class="elements-list" > <div class="element-item missing-key" > <span class="el-index" > [ 0 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> <div class="element-item missing-key" > <span class="el-index" > [ 1 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> <div class="element-item missing-key" > <span class="el-index" > [ 2 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> </div> </div> <div class="issue-why" > <strong> Why this matters: </strong> <p> Keys help React identify which items have changed, are added, or removed. </p> </div> <div class="issue-suggestion" > <strong> <span class="action-badge action-badge--suggestion" /> Suggestion: </strong> <
expect(link.getAttribute('target')).toBe('_blank');
expect(link.getAttribute('rel')).toBe('noopener noreferrer');
});
Expand Down
16 changes: 14 additions & 2 deletions src/panel/components/IssueCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,19 @@ export const IssueCard = React.memo(function IssueCard({ issue }: IssueCardProps
className={`issue-card severity-${issue.severity}`}
style={{ borderLeftColor: severity.color }}
>
<div className="issue-header" onClick={() => setExpanded(!expanded)}>
<div
className="issue-header"
role="button"
tabIndex={0}
aria-expanded={expanded}
onClick={() => setExpanded(!expanded)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setExpanded(!expanded);
}
}}
>
<span className={`issue-icon ${severity.iconClass}`} />
<div className="issue-info">
<div className="issue-title-row">
Expand Down Expand Up @@ -105,7 +117,7 @@ export const IssueCard = React.memo(function IssueCard({ issue }: IssueCardProps
)}
</div>
</div>
<button className="expand-button">{expanded ? '▼' : '▶'}</button>
<span className="expand-button">{expanded ? '▼' : '▶'}</span>
</div>

{expanded && (
Expand Down
6 changes: 4 additions & 2 deletions src/panel/styles/panel.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,14 @@ body {
}

/* Global focus ring (WCAG 2.4.7) */
button:focus-visible {
button:focus-visible,
[role="button"]:focus-visible {
outline: 2px solid var(--accent-blue);
outline-offset: 2px;
}

button:focus:not(:focus-visible) {
button:focus:not(:focus-visible),
[role="button"]:focus:not(:focus-visible) {
outline: none;
}

Expand Down
9 changes: 9 additions & 0 deletions src/panel/tabs/AIAnalysisTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ function AnalysisItemCard({ item }: { item: AIAnalysisItem }) {
<div
className="ai-item-card"
style={{ borderLeftColor: style.color, backgroundColor: style.bg }}
role="button"
tabIndex={0}
aria-expanded={expanded}
onClick={() => setExpanded(!expanded)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setExpanded(!expanded);
}
}}
>
<div className="ai-item-header">
Comment on lines 32 to 46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Applying role="button" to the entire .ai-item-card container makes all of its content—including the description, suggestions, and affected components inside .ai-item-body when expanded—part of the button's accessible content. This is an accessibility anti-pattern because buttons should not contain complex structured or interactive content.

Instead, only the .ai-item-header should act as the interactive toggle button, keeping the .ai-item-body semantically separate.

Suggested change
<div
className="ai-item-card"
style={{ borderLeftColor: style.color, backgroundColor: style.bg }}
role="button"
tabIndex={0}
aria-expanded={expanded}
onClick={() => setExpanded(!expanded)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setExpanded(!expanded);
}
}}
>
<div className="ai-item-header">
<div
className="ai-item-card"
style={{ borderLeftColor: style.color, backgroundColor: style.bg }}
>
<div
className="ai-item-header"
role="button"
tabIndex={0}
aria-expanded={expanded}
onClick={() => setExpanded(!expanded)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setExpanded(!expanded);
}
}}
>

<span className={`ai-item-icon ${style.iconClass}`} />
Expand Down
9 changes: 9 additions & 0 deletions src/panel/tabs/MemoryTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,16 @@ function CrashLogSection({ crashes }: { crashes: CrashEntry[] }) {
<div key={crash.id} className="crash-entry">
<div
className="crash-header"
role="button"
tabIndex={0}
aria-expanded={expandedId === crash.id}
onClick={() => setExpandedId(expandedId === crash.id ? null : crash.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setExpandedId(expandedId === crash.id ? null : crash.id);
}
}}
>
<span className={`crash-icon ${getCrashIconClass(crash.type)}`} />
<span className="crash-time">
Expand Down
25 changes: 24 additions & 1 deletion src/panel/tabs/TimelineTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,20 @@ export function TimelineTab({ events, tabId, onClear }: TimelineTabProps) {
</div>

<div className="snapshot-panel">
<div className="snapshot-header" onClick={() => setSnapshotPanelOpen(!snapshotPanelOpen)}>
<div
className="snapshot-header"
role="button"
tabIndex={0}
aria-expanded={snapshotPanelOpen}
onClick={() => setSnapshotPanelOpen(!snapshotPanelOpen)}
onKeyDown={(e) => {
if (e.target !== e.currentTarget) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setSnapshotPanelOpen(!snapshotPanelOpen);
}
}}
>
<span className="snapshot-toggle">{snapshotPanelOpen ? '▼' : '▶'}</span>
<span className="snapshot-title">Snapshots ({snapshots.length})</span>
Comment on lines +557 to 572

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Applying role="button" to the entire .snapshot-header container creates a nested interactive control violation because it contains a native <button className="snapshot-create-btn"> element. Nested interactive controls are invalid HTML and violate WCAG/ARIA standards, causing screen readers and keyboard navigation to behave unpredictably.

Instead of making the entire header a button and using target checks to prevent event bubbling, wrap only the toggle icon and title in a separate role="button" element, leaving the create button as a sibling.

        <div className="snapshot-header">
          <div
            className="snapshot-toggle-button"
            role="button"
            tabIndex={0}
            aria-expanded={snapshotPanelOpen}
            onClick={() => setSnapshotPanelOpen(!snapshotPanelOpen)}
            onKeyDown={(e) => {
              if (e.key === 'Enter' || e.key === ' ') {
                e.preventDefault();
                setSnapshotPanelOpen(!snapshotPanelOpen);
              }
            }}
            style={{ display: 'flex', alignItems: 'center', gap: '8px', flex: 1 }}
          >
            <span className="snapshot-toggle">{snapshotPanelOpen ? '▼' : '▶'}</span>
            <span className="snapshot-title">Snapshots ({snapshots.length})</span>
          </div>

<button
Expand All @@ -579,7 +592,17 @@ export function TimelineTab({ events, tabId, onClear }: TimelineTabProps) {
<div key={snapshot.id} className={`snapshot-item ${expandedSnapshotId === snapshot.id ? 'expanded' : ''}`}>
<div
className="snapshot-item-header"
role="button"
tabIndex={0}
aria-expanded={expandedSnapshotId === snapshot.id}
onClick={() => setExpandedSnapshotId(expandedSnapshotId === snapshot.id ? null : snapshot.id)}
onKeyDown={(e) => {
if (e.target !== e.currentTarget) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setExpandedSnapshotId(expandedSnapshotId === snapshot.id ? null : snapshot.id);
}
}}
>
<span className="snapshot-time">
{new Date(snapshot.createdAt).toLocaleTimeString()}
Expand Down
Loading