Skip to content

Commit c161177

Browse files
fix: delete removed tabs when pushing. (#122)
1 parent b9c7f86 commit c161177

6 files changed

Lines changed: 384 additions & 1 deletion

File tree

.github/workflows/playwright.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ on:
44
pull_request:
55
branches:
66
- main
7-
- next
87
types:
98
- opened
109
- synchronize

playwright/github-pr-drawer/active-context-sync.spec.ts

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,290 @@ test('Renaming a synced module tab keeps plain tab label and includes renamed pa
433433
})
434434
})
435435

436+
test('Removing a synced module tab includes a delete entry when pushing to an active PR', async ({
437+
page,
438+
}) => {
439+
const treeRequests: Array<Record<string, unknown>> = []
440+
441+
await page.route('https://api.github.com/user/repos**', async route => {
442+
await route.fulfill({
443+
status: 200,
444+
contentType: 'application/json',
445+
body: JSON.stringify([
446+
{
447+
id: 11,
448+
owner: { login: 'knightedcodemonkey' },
449+
name: 'develop',
450+
full_name: 'knightedcodemonkey/develop',
451+
default_branch: 'main',
452+
permissions: { push: true },
453+
},
454+
]),
455+
})
456+
})
457+
458+
await mockRepositoryBranches(page, {
459+
'knightedcodemonkey/develop': ['main', 'release', 'develop/open-pr-test'],
460+
})
461+
462+
await page.route(
463+
'https://api.github.com/repos/knightedcodemonkey/develop/pulls/2',
464+
async route => {
465+
await route.fulfill({
466+
status: 200,
467+
contentType: 'application/json',
468+
body: JSON.stringify({
469+
number: 2,
470+
state: 'open',
471+
title: 'Existing PR context from storage',
472+
html_url: 'https://github.com/knightedcodemonkey/develop/pull/2',
473+
head: { ref: 'develop/open-pr-test' },
474+
base: { ref: 'main' },
475+
}),
476+
})
477+
},
478+
)
479+
480+
await page.route(
481+
'https://api.github.com/repos/knightedcodemonkey/develop/git/ref/**',
482+
async route => {
483+
await route.fulfill({
484+
status: 200,
485+
contentType: 'application/json',
486+
body: JSON.stringify({
487+
ref: 'refs/heads/develop/open-pr-test',
488+
object: { type: 'commit', sha: 'existing-head-sha' },
489+
}),
490+
})
491+
},
492+
)
493+
494+
await page.route(
495+
'https://api.github.com/repos/knightedcodemonkey/develop/git/commits/existing-head-sha',
496+
async route => {
497+
await route.fulfill({
498+
status: 200,
499+
contentType: 'application/json',
500+
body: JSON.stringify({
501+
sha: 'existing-head-sha',
502+
tree: { sha: 'base-tree-sha' },
503+
}),
504+
})
505+
},
506+
)
507+
508+
await page.route(
509+
'https://api.github.com/repos/knightedcodemonkey/develop/contents/**',
510+
async route => {
511+
const url = new URL(route.request().url())
512+
const path = decodeURIComponent(url.pathname.split('/contents/')[1] ?? '').trim()
513+
const responseByPath: Record<string, { status: number; body: string }> = {
514+
'src/components/boop.tsx': {
515+
status: 200,
516+
body: JSON.stringify({ sha: 'boop-existing-sha' }),
517+
},
518+
}
519+
const response = responseByPath[path] ?? {
520+
status: 404,
521+
body: JSON.stringify({ message: 'Not Found' }),
522+
}
523+
524+
await route.fulfill({
525+
status: response.status,
526+
contentType: 'application/json',
527+
body: response.body,
528+
})
529+
},
530+
)
531+
532+
await page.route(
533+
'https://api.github.com/repos/knightedcodemonkey/develop/git/trees',
534+
async route => {
535+
treeRequests.push(route.request().postDataJSON() as Record<string, unknown>)
536+
537+
await route.fulfill({
538+
status: 201,
539+
contentType: 'application/json',
540+
body: JSON.stringify({ sha: 'remove-tree-sha' }),
541+
})
542+
},
543+
)
544+
545+
await page.route(
546+
'https://api.github.com/repos/knightedcodemonkey/develop/git/commits',
547+
async route => {
548+
await route.fulfill({
549+
status: 201,
550+
contentType: 'application/json',
551+
body: JSON.stringify({ sha: 'remove-commit-sha' }),
552+
})
553+
},
554+
)
555+
556+
await page.route(
557+
'https://api.github.com/repos/knightedcodemonkey/develop/git/refs/**',
558+
async route => {
559+
await route.fulfill({
560+
status: 200,
561+
contentType: 'application/json',
562+
body: JSON.stringify({
563+
ref: 'refs/heads/develop/open-pr-test',
564+
object: { type: 'commit', sha: 'remove-commit-sha' },
565+
}),
566+
})
567+
},
568+
)
569+
570+
await waitForAppReady(page, `${appEntryPath}`)
571+
572+
const now = Date.now()
573+
await seedLocalWorkspaceContexts(page, [
574+
{
575+
id: buildWorkspaceRecordId({
576+
repositoryFullName: 'knightedcodemonkey/develop',
577+
headBranch: 'develop/open-pr-test',
578+
}),
579+
repo: 'knightedcodemonkey/develop',
580+
base: 'main',
581+
head: 'develop/open-pr-test',
582+
prTitle: 'Existing PR context from storage',
583+
prNumber: 2,
584+
prContextState: 'active',
585+
renderMode: 'react',
586+
tabs: [
587+
{
588+
id: 'component',
589+
name: 'App.tsx',
590+
path: 'src/components/App.tsx',
591+
language: 'javascript-jsx',
592+
role: 'entry',
593+
isActive: true,
594+
content: 'export const App = () => <main>Hello from Knighted</main>',
595+
targetPrFilePath: 'src/components/App.tsx',
596+
syncedContent: 'export const App = () => <main>Hello from Knighted</main>',
597+
syncedAt: now,
598+
isDirty: false,
599+
},
600+
{
601+
id: 'styles',
602+
name: 'app.css',
603+
path: 'src/styles/app.css',
604+
language: 'css',
605+
role: 'module',
606+
isActive: false,
607+
content: 'main { color: #111; }',
608+
targetPrFilePath: 'src/styles/app.css',
609+
syncedContent: 'main { color: #111; }',
610+
syncedAt: now,
611+
isDirty: false,
612+
},
613+
{
614+
id: 'boop',
615+
name: 'boop.tsx',
616+
path: 'src/components/boop.tsx',
617+
language: 'javascript-jsx',
618+
role: 'module',
619+
isActive: false,
620+
content: 'export const Boop = () => <p>boop</p>',
621+
targetPrFilePath: 'src/components/boop.tsx',
622+
syncedContent: 'export const Boop = () => <p>boop</p>',
623+
syncedAt: now,
624+
isDirty: false,
625+
},
626+
],
627+
activeTabId: 'component',
628+
createdAt: now,
629+
lastModified: now,
630+
},
631+
])
632+
633+
await connectByotWithSingleRepo(page)
634+
await openMostRecentStoredWorkspaceContext(page)
635+
636+
await page.getByRole('button', { name: 'Remove tab boop.tsx' }).click()
637+
const removeDialog = page.locator('#clear-confirm-dialog')
638+
await expect(removeDialog).toBeVisible()
639+
await removeDialog.locator('button[value="confirm"]').evaluate(element => {
640+
if (element instanceof HTMLButtonElement) {
641+
element.click()
642+
}
643+
})
644+
await expect(page.getByRole('button', { name: 'Open tab boop.tsx' })).toHaveCount(0)
645+
646+
await setComponentEditorSource(
647+
page,
648+
'export const App = () => <main>Updated entry after removal</main>',
649+
)
650+
651+
await ensureOpenPrDrawerOpen(page)
652+
const pushCommitButton = page
653+
.locator('#github-pr-drawer')
654+
.getByRole('button', { name: 'Push commit', exact: true })
655+
await expect(pushCommitButton).toBeEnabled()
656+
await pushCommitButton.evaluate(element => {
657+
if (element instanceof HTMLButtonElement) {
658+
element.click()
659+
}
660+
})
661+
662+
const pushDialog = page.locator('#clear-confirm-dialog')
663+
await expect(pushDialog).toBeVisible()
664+
await expect(pushDialog.getByText('Files to commit:', { exact: true })).toBeVisible()
665+
await expect(
666+
pushDialog.getByText(/src\/components\/boop\.tsx.*\(delete\)/, { exact: false }),
667+
).toBeVisible()
668+
669+
await pushDialog.locator('button[value="confirm"]').evaluate(element => {
670+
if (element instanceof HTMLButtonElement) {
671+
element.click()
672+
}
673+
})
674+
675+
await expect(
676+
page.getByRole('status', { name: 'Open pull request status', includeHidden: true }),
677+
).toContainText('Commit pushed to develop/open-pr-test')
678+
679+
await expect
680+
.poll(async () => {
681+
const workspaceRecord = await getWorkspaceTabsRecord(page, {
682+
headBranch: 'develop/open-pr-test',
683+
})
684+
const tabs = Array.isArray(workspaceRecord?.tabs)
685+
? (workspaceRecord.tabs as Array<Record<string, unknown>>)
686+
: []
687+
688+
return tabs.some(tab => {
689+
const path = typeof tab?.path === 'string' ? tab.path.trim() : ''
690+
return path === 'src/components/boop.tsx'
691+
})
692+
})
693+
.toBe(false)
694+
695+
expect(treeRequests).toHaveLength(1)
696+
const treePayload = treeRequests[0]?.tree as Array<Record<string, unknown>>
697+
const updatedEntryBlob = treePayload?.find(
698+
file => file.path === 'src/components/App.tsx',
699+
)
700+
const deletedBlob = treePayload?.find(file => file.path === 'src/components/boop.tsx')
701+
702+
expect(updatedEntryBlob).toMatchObject({
703+
path: 'src/components/App.tsx',
704+
mode: '100644',
705+
type: 'blob',
706+
})
707+
expect(typeof updatedEntryBlob?.content).toBe('string')
708+
expect(
709+
(updatedEntryBlob?.content as string).includes('Updated entry after removal'),
710+
).toBe(true)
711+
712+
expect(deletedBlob).toEqual({
713+
path: 'src/components/boop.tsx',
714+
mode: '100644',
715+
type: 'blob',
716+
sha: null,
717+
})
718+
})
719+
436720
test('Push commit prunes stale delete entries before Git tree creation', async ({
437721
page,
438722
}) => {

src/app.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,7 @@ const {
870870
confirmAction: options => confirmAction(options),
871871
isStyleWorkspaceTab,
872872
clearTrackedWorkspaceTab,
873+
trackRemovedWorkspaceTab: tab => workspaceSyncController.trackRemovedWorkspaceTab(tab),
873874
getWorkspaceTabByKind,
874875
makeUniqueTabPath,
875876
createWorkspaceTabId,

src/modules/app-core/workspace-controllers-setup.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const createWorkspaceControllersSetup = ({
6767
confirmAction,
6868
isStyleWorkspaceTab,
6969
clearTrackedWorkspaceTab,
70+
trackRemovedWorkspaceTab,
7071
getWorkspaceTabByKind,
7172
makeUniqueTabPath,
7273
createWorkspaceTabId,
@@ -151,6 +152,7 @@ const createWorkspaceControllersSetup = ({
151152
isStyleWorkspaceTab,
152153
persistActiveTabEditorContent,
153154
clearTrackedWorkspaceTab,
155+
trackRemovedWorkspaceTab,
154156
getActiveWorkspaceTab,
155157
loadWorkspaceTabIntoEditor,
156158
getWorkspaceTabByKind,

0 commit comments

Comments
 (0)