Skip to content

Commit b625079

Browse files
fix(sentry): handle undefined repo.owner in organization filter
Fixes GITBOX-1: TypeError when filtering repositories by organization. The error occurred when repo.owner was undefined while organizationFilter was set to a non-'all' value in localStorage. - Add optional chaining to safely access repo.owner?.login - Add E2E test for organization filter with missing owner data
1 parent a6c431d commit b625079

File tree

3 files changed

+202
-2
lines changed

3 files changed

+202
-2
lines changed

components/Board/AddRepositoryCombobox.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,11 @@ export const AddRepositoryCombobox = memo(function AddRepositoryCombobox({
200200
}
201201

202202
// Organization filter (exact match on owner login)
203+
// Defensive null check for repos with missing owner data (GITBOX-1 fix)
203204
if (organizationFilter !== 'all') {
204205
filtered = filtered.filter(
205206
(repo) =>
206-
repo.owner.login.toLowerCase() === organizationFilter.toLowerCase(),
207+
repo.owner?.login?.toLowerCase() === organizationFilter.toLowerCase(),
207208
)
208209
}
209210

playwright.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ export default defineConfig({
6767
...devices['Desktop Chrome'],
6868
storageState: AUTH_FILE,
6969
},
70-
testMatch: /boards\.spec\.ts|kanban\.spec\.ts|settings\.spec\.ts/,
70+
testMatch:
71+
/boards\.spec\.ts|kanban\.spec\.ts|settings\.spec\.ts|add-repository-combobox\.spec\.ts/,
7172
},
7273
],
7374

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/**
2+
* AddRepositoryCombobox E2E Tests
3+
*
4+
* Tests for the AddRepositoryCombobox component with organization filter.
5+
* Specifically tests GITBOX-1 fix: handling repos with missing owner data
6+
* when organization filter is persisted in localStorage.
7+
*
8+
* Test Scenario:
9+
* 1. Organization filter is saved in localStorage (not 'all')
10+
* 2. GitHub API returns a repo without owner field
11+
* 3. Opening AddRepositoryCombobox should NOT crash
12+
*/
13+
14+
import { test, expect } from '@playwright/test'
15+
16+
test.describe('AddRepositoryCombobox - GITBOX-1 Fix', () => {
17+
test.use({ storageState: 'tests/e2e/.auth/user.json' })
18+
19+
const BOARD_URL = '/board/board-1'
20+
21+
/**
22+
* GITBOX-1: TypeError when repo.owner is undefined with organization filter
23+
*
24+
* Reproduces the scenario where:
25+
* - User has organization filter set to a specific org (not 'all')
26+
* - GitHub API returns repos, some of which have missing owner data
27+
* - Opening the combobox should gracefully handle this without crashing
28+
*/
29+
test('should not crash when repo.owner is undefined with organization filter set', async ({
30+
page,
31+
}) => {
32+
// Step 1: Intercept GitHub user/repos API and inject a repo without owner
33+
await page.route('**/api.github.com/user/repos**', async (route) => {
34+
const response = await route.fetch()
35+
const repos = await response.json()
36+
37+
// Add a malformed repo without owner field (simulates edge case)
38+
const malformedRepo = {
39+
id: 9999,
40+
node_id: 'R_malformed',
41+
name: 'malformed-repo',
42+
full_name: 'unknown/malformed-repo',
43+
// Intentionally omit owner field to trigger the bug
44+
description: 'A repo with missing owner data',
45+
html_url: 'https://github.com/unknown/malformed-repo',
46+
homepage: null,
47+
stargazers_count: 0,
48+
watchers_count: 0,
49+
language: 'TypeScript',
50+
topics: [],
51+
visibility: 'public',
52+
updated_at: '2024-01-01T00:00:00.000Z',
53+
created_at: '2024-01-01T00:00:00.000Z',
54+
}
55+
56+
const modifiedRepos = [...repos, malformedRepo]
57+
58+
await route.fulfill({
59+
status: 200,
60+
contentType: 'application/json',
61+
body: JSON.stringify(modifiedRepos),
62+
})
63+
})
64+
65+
// Step 2: Navigate to board page
66+
await page.goto(BOARD_URL)
67+
await page.waitForLoadState('domcontentloaded')
68+
69+
// Step 3: Set organization filter in localStorage (simulating persisted state)
70+
// The Redux store uses 'gitbox-state' key with settings.organizationFilter
71+
await page.evaluate(() => {
72+
const existingState = localStorage.getItem('gitbox-state')
73+
const state = existingState ? JSON.parse(existingState) : {}
74+
75+
// Set organization filter to 'testuser' (not 'all')
76+
state.settings = state.settings || {}
77+
state.settings.organizationFilter = 'testuser'
78+
79+
localStorage.setItem('gitbox-state', JSON.stringify(state))
80+
})
81+
82+
// Step 4: Reload page to apply localStorage state
83+
await page.reload()
84+
await page.waitForLoadState('domcontentloaded')
85+
86+
// Step 5: Open AddRepositoryCombobox
87+
const addRepoButton = page.getByRole('button', {
88+
name: /add repositories/i,
89+
})
90+
await expect(addRepoButton).toBeVisible({ timeout: 10000 })
91+
await addRepoButton.click()
92+
93+
// Step 6: Verify the combobox panel is visible (no crash)
94+
// The combobox should open without throwing TypeError
95+
const searchInput = page.getByPlaceholder(/search repositories/i)
96+
await expect(searchInput).toBeVisible({ timeout: 10000 })
97+
98+
// Step 7: Verify organization filter is applied
99+
// The filter dropdown should show 'testuser' is selected
100+
const orgFilterTrigger = page.getByRole('combobox').first()
101+
await expect(orgFilterTrigger).toBeVisible()
102+
103+
// Step 8: Check console for no TypeErrors
104+
const consoleErrors: string[] = []
105+
page.on('console', (msg) => {
106+
if (msg.type() === 'error') {
107+
consoleErrors.push(msg.text())
108+
}
109+
})
110+
111+
// Wait a moment for any async errors to surface
112+
await page.waitForTimeout(1000)
113+
114+
// Verify no TypeError related to toLowerCase
115+
const hasLowerCaseError = consoleErrors.some(
116+
(error) =>
117+
error.includes('toLowerCase') ||
118+
error.includes('Cannot read properties of undefined'),
119+
)
120+
expect(hasLowerCaseError).toBe(false)
121+
})
122+
123+
/**
124+
* Test that organization filter works correctly with valid repos
125+
*/
126+
test('should filter repositories by organization when filter is set', async ({
127+
page,
128+
}) => {
129+
// Navigate to board page
130+
await page.goto(BOARD_URL)
131+
await page.waitForLoadState('domcontentloaded')
132+
133+
// Open AddRepositoryCombobox
134+
const addRepoButton = page.getByRole('button', {
135+
name: /add repositories/i,
136+
})
137+
await expect(addRepoButton).toBeVisible({ timeout: 10000 })
138+
await addRepoButton.click()
139+
140+
// Wait for the combobox panel to be visible
141+
const searchInput = page.getByPlaceholder(/search repositories/i)
142+
await expect(searchInput).toBeVisible({ timeout: 10000 })
143+
144+
// Verify organization filter selector is present
145+
const orgFilterTrigger = page.getByRole('combobox').first()
146+
await expect(orgFilterTrigger).toBeVisible()
147+
148+
// Click the organization filter
149+
await orgFilterTrigger.click()
150+
151+
// Wait for dropdown options to appear
152+
const allOption = page.getByRole('option', { name: /all organizations/i })
153+
await expect(allOption).toBeVisible({ timeout: 5000 })
154+
})
155+
156+
/**
157+
* Test that 'all' organization filter shows all repos
158+
*/
159+
test('should show all repositories when organization filter is all', async ({
160+
page,
161+
}) => {
162+
// Set organization filter to 'all' in localStorage
163+
await page.goto(BOARD_URL)
164+
await page.waitForLoadState('domcontentloaded')
165+
166+
await page.evaluate(() => {
167+
const existingState = localStorage.getItem('gitbox-state')
168+
const state = existingState ? JSON.parse(existingState) : {}
169+
170+
state.settings = state.settings || {}
171+
state.settings.organizationFilter = 'all'
172+
173+
localStorage.setItem('gitbox-state', JSON.stringify(state))
174+
})
175+
176+
await page.reload()
177+
await page.waitForLoadState('domcontentloaded')
178+
179+
// Open AddRepositoryCombobox
180+
const addRepoButton = page.getByRole('button', {
181+
name: /add repositories/i,
182+
})
183+
await expect(addRepoButton).toBeVisible({ timeout: 10000 })
184+
await addRepoButton.click()
185+
186+
// Wait for repositories to load
187+
const searchInput = page.getByPlaceholder(/search repositories/i)
188+
await expect(searchInput).toBeVisible({ timeout: 10000 })
189+
190+
// Wait for repository list to be populated
191+
await page.waitForTimeout(1000)
192+
193+
// Verify at least one repository is visible
194+
const repoItems = page.locator('[role="option"]')
195+
const count = await repoItems.count()
196+
expect(count).toBeGreaterThan(0)
197+
})
198+
})

0 commit comments

Comments
 (0)