Skip to content

Commit 99284c1

Browse files
refactor: pr dependent dropdowns, toast position on mobile. (#31)
1 parent d99740b commit 99284c1

6 files changed

Lines changed: 417 additions & 13 deletions

File tree

playwright/app.spec.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ type PullRequestCreateBody = {
2727
base?: string
2828
}
2929

30+
type BranchesByRepo = Record<string, string[]>
31+
3032
const waitForAppReady = async (page: Page, path = appEntryPath) => {
3133
await page.goto(path)
3234
await expect(page.getByRole('heading', { name: '@knighted/develop' })).toBeVisible()
@@ -147,6 +149,28 @@ const ensureOpenPrDrawerOpen = async (page: Page) => {
147149
await expect(page.locator('#github-pr-drawer')).toBeVisible()
148150
}
149151

152+
const mockRepositoryBranches = async (
153+
page: Page,
154+
branchesByRepo: BranchesByRepo = {},
155+
) => {
156+
await page.route('https://api.github.com/repos/**/branches**', async route => {
157+
const url = new URL(route.request().url())
158+
const match = url.pathname.match(/^\/repos\/([^/]+)\/([^/]+)\/branches$/)
159+
const repositoryKey = match ? `${match[1]}/${match[2]}` : ''
160+
161+
const branchNames =
162+
branchesByRepo[repositoryKey] && branchesByRepo[repositoryKey].length > 0
163+
? branchesByRepo[repositoryKey]
164+
: ['main']
165+
166+
await route.fulfill({
167+
status: 200,
168+
contentType: 'application/json',
169+
body: JSON.stringify(branchNames.map(name => ({ name }))),
170+
})
171+
})
172+
}
173+
150174
const connectByotWithSingleRepo = async (page: Page) => {
151175
await page.route('https://api.github.com/user/repos**', async route => {
152176
await route.fulfill({
@@ -165,6 +189,10 @@ const connectByotWithSingleRepo = async (page: Page) => {
165189
})
166190
})
167191

192+
await mockRepositoryBranches(page, {
193+
'knightedcodemonkey/develop': ['main', 'release'],
194+
})
195+
168196
await page.locator('#github-token-input').fill('github_pat_fake_chat_1234567890')
169197
await page.locator('#github-token-add').click()
170198
await expect(page.locator('#status')).toHaveText('Loaded 1 writable repositories')
@@ -528,6 +556,11 @@ test('BYOT remembers selected repository across reloads', async ({ page }) => {
528556
})
529557
})
530558

559+
await mockRepositoryBranches(page, {
560+
'knightedcodemonkey/develop': ['main', 'release'],
561+
'knightedcodemonkey/css': ['main', 'release/1.x'],
562+
})
563+
531564
await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
532565

533566
await page.locator('#github-token-input').fill('github_pat_fake_1234567890')
@@ -577,6 +610,10 @@ test('Open PR drawer confirms and submits component/styles filepaths', async ({
577610
})
578611
})
579612

613+
await mockRepositoryBranches(page, {
614+
'knightedcodemonkey/develop': ['main', 'release'],
615+
})
616+
580617
await page.route(
581618
'https://api.github.com/repos/knightedcodemonkey/develop/git/ref/**',
582619
async route => {
@@ -717,6 +754,82 @@ test('Open PR drawer confirms and submits component/styles filepaths', async ({
717754
)
718755
})
719756

757+
test('Open PR drawer base dropdown updates from mocked repo branches', async ({
758+
page,
759+
}) => {
760+
const branchRequestUrls: string[] = []
761+
762+
await page.route('https://api.github.com/user/repos**', async route => {
763+
await route.fulfill({
764+
status: 200,
765+
contentType: 'application/json',
766+
body: JSON.stringify([
767+
{
768+
id: 2,
769+
owner: { login: 'knightedcodemonkey' },
770+
name: 'develop',
771+
full_name: 'knightedcodemonkey/develop',
772+
default_branch: 'main',
773+
permissions: { push: true },
774+
},
775+
{
776+
id: 1,
777+
owner: { login: 'knightedcodemonkey' },
778+
name: 'css',
779+
full_name: 'knightedcodemonkey/css',
780+
default_branch: 'stable',
781+
permissions: { push: true },
782+
},
783+
]),
784+
})
785+
})
786+
787+
await page.route('https://api.github.com/repos/**/branches**', async route => {
788+
const url = route.request().url()
789+
branchRequestUrls.push(url)
790+
791+
const branchNames = url.includes('/repos/knightedcodemonkey/css/branches')
792+
? ['stable', 'release/1.x']
793+
: ['main', 'develop-next']
794+
795+
await route.fulfill({
796+
status: 200,
797+
contentType: 'application/json',
798+
body: JSON.stringify(branchNames.map(name => ({ name }))),
799+
})
800+
})
801+
802+
await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
803+
804+
await page.locator('#github-token-input').fill('github_pat_fake_1234567890')
805+
await page.locator('#github-token-add').click()
806+
await expect(page.locator('#status')).toHaveText('Loaded 2 writable repositories')
807+
808+
await ensureOpenPrDrawerOpen(page)
809+
810+
const repoSelect = page.locator('#github-pr-repo-select')
811+
const baseSelect = page.locator('#github-pr-base-branch')
812+
813+
await repoSelect.selectOption('knightedcodemonkey/develop')
814+
await expect(baseSelect).toHaveValue('main')
815+
await expect(baseSelect.locator('option')).toHaveText(['main', 'develop-next'])
816+
817+
await repoSelect.selectOption('knightedcodemonkey/css')
818+
await expect(baseSelect).toHaveValue('stable')
819+
await expect(baseSelect.locator('option')).toHaveText(['stable', 'release/1.x'])
820+
821+
expect(
822+
branchRequestUrls.some(url =>
823+
url.includes('https://api.github.com/repos/knightedcodemonkey/develop/branches'),
824+
),
825+
).toBe(true)
826+
expect(
827+
branchRequestUrls.some(url =>
828+
url.includes('https://api.github.com/repos/knightedcodemonkey/css/branches'),
829+
),
830+
).toBe(true)
831+
})
832+
720833
test('Open PR drawer validates unsafe filepaths', async ({ page }) => {
721834
await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
722835
await connectByotWithSingleRepo(page)

src/index.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -659,13 +659,13 @@ <h2>Open Pull Request</h2>
659659
for="github-pr-base-branch"
660660
>
661661
<span>Base</span>
662-
<input
662+
<select
663663
id="github-pr-base-branch"
664-
type="text"
665-
autocomplete="off"
666-
spellcheck="false"
667-
placeholder="main"
668-
/>
664+
aria-label="Pull request base branch"
665+
disabled
666+
>
667+
<option value="main" selected>main</option>
668+
</select>
669669
</label>
670670

671671
<label

src/modules/github-api.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ const normalizeRepo = repo => {
107107

108108
const hasWritePermission = permissions => Boolean(permissions && permissions.push)
109109

110+
const normalizeBranchName = branch => {
111+
if (!branch || typeof branch !== 'object') {
112+
return null
113+
}
114+
115+
return typeof branch.name === 'string' && branch.name.trim() ? branch.name : null
116+
}
117+
110118
const buildRequestHeaders = token => ({
111119
Accept: 'application/vnd.github+json',
112120
Authorization: `Bearer ${token}`,
@@ -363,6 +371,19 @@ const listReposPage = async ({ token, url, signal }) => {
363371
}
364372
}
365373

374+
const listBranchesPage = async ({ token, url, signal }) => {
375+
const { data, nextPageUrl } = await fetchJson({ token, url, signal })
376+
377+
if (!Array.isArray(data)) {
378+
throw new Error('Unexpected response while loading repository branches from GitHub.')
379+
}
380+
381+
return {
382+
branches: data.map(normalizeBranchName).filter(Boolean),
383+
nextPageUrl,
384+
}
385+
}
386+
366387
export const listWritableRepositories = async ({ token, signal }) => {
367388
if (typeof token !== 'string' || token.trim().length === 0) {
368389
throw new Error('A GitHub token is required to load repositories.')
@@ -394,6 +415,51 @@ export const listWritableRepositories = async ({ token, signal }) => {
394415
return writableRepos
395416
}
396417

418+
export const listRepositoryBranches = async ({ token, owner, repo, signal }) => {
419+
if (typeof token !== 'string' || token.trim().length === 0) {
420+
throw new Error('A GitHub token is required to load branches.')
421+
}
422+
423+
const normalizedOwner = typeof owner === 'string' ? owner.trim() : ''
424+
const normalizedRepo = typeof repo === 'string' ? repo.trim() : ''
425+
426+
if (!normalizedOwner || !normalizedRepo) {
427+
throw new Error('A valid repository owner/name is required to load branches.')
428+
}
429+
430+
const branches = []
431+
const dedupe = new Set()
432+
const collectBranchesByPage = async ({ url, remainingPageBudget }) => {
433+
if (!url || remainingPageBudget <= 0) {
434+
return
435+
}
436+
437+
const page = await listBranchesPage({ token, url, signal })
438+
439+
for (const name of page.branches) {
440+
if (dedupe.has(name)) {
441+
continue
442+
}
443+
444+
dedupe.add(name)
445+
branches.push(name)
446+
}
447+
448+
await collectBranchesByPage({
449+
url: page.nextPageUrl,
450+
remainingPageBudget: remainingPageBudget - 1,
451+
})
452+
}
453+
454+
await collectBranchesByPage({
455+
url: `${githubApiBaseUrl}/repos/${normalizedOwner}/${normalizedRepo}/branches?per_page=100`,
456+
remainingPageBudget: 5,
457+
})
458+
459+
branches.sort((left, right) => left.localeCompare(right))
460+
return branches
461+
}
462+
397463
export const streamGitHubChatCompletion = async ({
398464
token,
399465
messages,

0 commit comments

Comments
 (0)