diff --git a/playwright/github-pr-drawer.spec.ts b/playwright/github-pr-drawer.spec.ts index 7d550bc..bf31180 100644 --- a/playwright/github-pr-drawer.spec.ts +++ b/playwright/github-pr-drawer.spec.ts @@ -496,6 +496,179 @@ test('Open PR drawer does not prune saved PR context on repo switch before save' expect(contexts[0]?.parsed?.componentFilePath).toBe('examples/develop/App.tsx') }) +test('Active PR context disconnect uses local-only confirmation flow', async ({ + page, +}) => { + let closePullRequestRequestCount = 0 + + await page.route('https://api.github.com/user/repos**', async route => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { + id: 11, + owner: { login: 'knightedcodemonkey' }, + name: 'develop', + full_name: 'knightedcodemonkey/develop', + default_branch: 'main', + permissions: { push: true }, + }, + ]), + }) + }) + + await mockRepositoryBranches(page, { + 'knightedcodemonkey/develop': ['main', 'release'], + }) + + await page.route( + 'https://api.github.com/repos/knightedcodemonkey/develop/pulls/2', + async route => { + if (route.request().method() === 'PATCH') { + closePullRequestRequestCount += 1 + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + number: 2, + state: 'closed', + title: 'Existing PR context from storage', + html_url: 'https://github.com/knightedcodemonkey/develop/pull/2', + head: { ref: 'develop/open-pr-test' }, + base: { ref: 'main' }, + }), + }) + return + } + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + number: 2, + state: 'open', + title: 'Existing PR context from storage', + html_url: 'https://github.com/knightedcodemonkey/develop/pull/2', + head: { ref: 'develop/open-pr-test' }, + base: { ref: 'main' }, + }), + }) + }, + ) + + await page.route( + 'https://api.github.com/repos/knightedcodemonkey/develop/git/ref/**', + async route => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + ref: 'refs/heads/develop/open-pr-test', + object: { type: 'commit', sha: 'existing-head-sha' }, + }), + }) + }, + ) + + await waitForAppReady(page, `${appEntryPath}`) + + await page.evaluate(() => { + localStorage.setItem( + 'knighted:develop:github-pr-config:knightedcodemonkey/develop', + JSON.stringify({ + componentFilePath: 'examples/component/App.tsx', + stylesFilePath: 'examples/styles/app.css', + renderMode: 'react', + baseBranch: 'main', + headBranch: 'develop/open-pr-test', + prTitle: 'Existing PR context from storage', + prBody: 'Saved body', + isActivePr: true, + pullRequestNumber: 2, + pullRequestUrl: 'https://github.com/knightedcodemonkey/develop/pull/2', + }), + ) + }) + + await connectByotWithSingleRepo(page) + + await expect( + page.getByRole('button', { name: 'Disconnect active pull request context' }), + ).toBeVisible() + + await page + .getByRole('button', { name: 'Disconnect active pull request context' }) + .click() + + const dialog = page.getByRole('dialog') + await expect(dialog).toBeVisible() + await expect(dialog).toContainText('Disconnect PR context?') + await expect(dialog).toContainText( + 'This will disconnect the active pull request context in this app only.', + ) + await expect(dialog).toContainText('Your pull request will stay open on GitHub.') + await expect(dialog).toContainText( + 'Your GitHub token and selected repository will stay connected.', + ) + + await dialog.getByRole('button', { name: 'Cancel' }).click() + + await expect( + page.getByRole('button', { name: 'Push commit to active pull request branch' }), + ).toBeVisible() + + const savedActiveStateAfterCancel = await page.evaluate(() => { + const raw = localStorage.getItem( + 'knighted:develop:github-pr-config:knightedcodemonkey/develop', + ) + + if (!raw) { + return null + } + + try { + const parsed = JSON.parse(raw) + return parsed?.isActivePr === true + } catch { + return null + } + }) + + expect(savedActiveStateAfterCancel).toBe(true) + + await page + .getByRole('button', { name: 'Disconnect active pull request context' }) + .click() + await dialog.getByRole('button', { name: 'Disconnect' }).click() + + await expect(page.getByRole('button', { name: 'Open pull request' })).toBeVisible() + await expect( + page.getByRole('button', { name: 'Disconnect active pull request context' }), + ).toBeHidden() + + const savedContextAfterDisconnect = await page.evaluate(() => { + const raw = localStorage.getItem( + 'knighted:develop:github-pr-config:knightedcodemonkey/develop', + ) + + if (!raw) { + return null + } + + try { + return JSON.parse(raw) + } catch { + return null + } + }) + + expect(savedContextAfterDisconnect).not.toBeNull() + expect(savedContextAfterDisconnect?.isActivePr).toBe(false) + expect(savedContextAfterDisconnect?.pullRequestNumber).toBe(2) + expect(closePullRequestRequestCount).toBe(0) +}) + test('Active PR context updates controls and can be closed from AI controls', async ({ page, }) => { diff --git a/src/app.js b/src/app.js index ed193d6..81d6ef8 100644 --- a/src/app.js +++ b/src/app.js @@ -47,6 +47,7 @@ const githubPrToggleLabel = document.getElementById('github-pr-toggle-label') const githubPrToggleIcon = document.getElementById('github-pr-toggle-icon') const githubPrToggleIconPath = document.getElementById('github-pr-toggle-icon-path') const githubPrContextClose = document.getElementById('github-pr-context-close') +const githubPrContextDisconnect = document.getElementById('github-pr-context-disconnect') const githubPrDrawer = document.getElementById('github-pr-drawer') const openPrTitle = document.getElementById('open-pr-title') const githubPrClose = document.getElementById('github-pr-close') @@ -634,16 +635,14 @@ const syncActivePrContextUi = activeContext => { setGitHubPrToggleVisual(hasActiveContext ? 'push-commit' : 'open-pr') syncEditorPrContextIndicators(shouldShowEditorSyncIndicators) - if (!(githubPrContextClose instanceof HTMLButtonElement)) { - return - } - if (!hasActiveContext) { - githubPrContextClose.setAttribute('hidden', '') + githubPrContextClose?.setAttribute('hidden', '') + githubPrContextDisconnect?.setAttribute('hidden', '') return } - githubPrContextClose.removeAttribute('hidden') + githubPrContextClose?.removeAttribute('hidden') + githubPrContextDisconnect?.removeAttribute('hidden') } const syncAiChatTokenVisibility = token => { @@ -656,8 +655,10 @@ const syncAiChatTokenVisibility = token => { if (githubAiContextState.activePrContext) { githubPrContextClose?.removeAttribute('hidden') + githubPrContextDisconnect?.removeAttribute('hidden') } else { githubPrContextClose?.setAttribute('hidden', '') + githubPrContextDisconnect?.setAttribute('hidden', '') } return } @@ -672,6 +673,7 @@ const syncAiChatTokenVisibility = token => { githubPrToggle?.setAttribute('hidden', '') githubPrToggle?.setAttribute('aria-expanded', 'false') githubPrContextClose?.setAttribute('hidden', '') + githubPrContextDisconnect?.setAttribute('hidden', '') chatDrawerController.setOpen(false) prDrawerController.setOpen(false) } @@ -919,6 +921,31 @@ githubPrContextClose?.addEventListener('click', () => { }) }) +githubPrContextDisconnect?.addEventListener('click', () => { + if (!githubAiContextState.activePrContext) { + return + } + + const activePrReference = formatActivePrReference(githubAiContextState.activePrContext) + const referenceLine = activePrReference ? `PR: ${activePrReference}\n` : '' + + confirmAction({ + title: 'Disconnect PR context?', + copy: `${referenceLine}This will disconnect the active pull request context in this app only.\nYour pull request will stay open on GitHub.\nYour GitHub token and selected repository will stay connected.`, + confirmButtonText: 'Disconnect', + onConfirm: () => { + const result = prDrawerController.disconnectActivePrContext() + const reference = result?.reference + setStatus( + reference + ? `Disconnected PR context (${reference}). Pull request remains open on GitHub.` + : 'Disconnected PR context. Pull request remains open on GitHub.', + 'neutral', + ) + }, + }) +}) + const getStyleEditorLanguage = mode => { if (mode === 'less') return 'less' if (mode === 'sass') return 'sass' diff --git a/src/index.html b/src/index.html index 2fec4a3..e4cd022 100644 --- a/src/index.html +++ b/src/index.html @@ -177,6 +177,30 @@