Skip to content

Commit 1acaf11

Browse files
authored
Merge pull request #9411 from gitbutlerapp/pr-checks-branch-card
pr-checks-branch-card
2 parents eb17bc6 + f9dcbc3 commit 1acaf11

File tree

9 files changed

+385
-27
lines changed

9 files changed

+385
-27
lines changed

apps/desktop/cypress/e2e/review.cy.ts

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { clearCommandMocks, mockCommand } from './support';
22
import { PROJECT_ID } from './support/mock/projects';
33
import BranchesWithChanges from './support/scenarios/branchesWithChanges';
44
import StackBranchesWithCommits from './support/scenarios/stackBrancheshWithCommits';
5+
import type { ChecksResult } from '$lib/forge/github/types';
56

67
describe('Review', () => {
78
let mockBackend: BranchesWithChanges;
@@ -686,4 +687,291 @@ describe('Review - stacked branches', () => {
686687
draft: false
687688
});
688689
});
690+
691+
type CheckRun = Partial<ChecksResult['check_runs'][number]>;
692+
type CustomChecksData = {
693+
total_count: number;
694+
check_runs: CheckRun[];
695+
};
696+
697+
it('Should be able to create a pull request and listen for CI checks', () => {
698+
const data: CustomChecksData = {
699+
total_count: 1,
700+
check_runs: [
701+
{
702+
id: 1,
703+
started_at: new Date(Date.now() - 10000).toISOString(),
704+
conclusion: null,
705+
completed_at: null,
706+
head_sha: 'abc123',
707+
name: 'CI Check 1',
708+
status: 'in_progress'
709+
}
710+
]
711+
};
712+
713+
const finishedData: CustomChecksData = {
714+
total_count: 1,
715+
check_runs: [
716+
{
717+
...data.check_runs[0],
718+
status: 'completed',
719+
conclusion: 'success',
720+
completed_at: new Date().toISOString()
721+
}
722+
]
723+
};
724+
725+
let requestCount = 0;
726+
727+
cy.intercept(
728+
{
729+
method: 'GET',
730+
url: 'https://api.github.com/repos/example/repo/commits/check-runs'
731+
},
732+
(req) => {
733+
requestCount++;
734+
if (requestCount > 2) {
735+
req.reply({
736+
statusCode: 200,
737+
body: finishedData
738+
});
739+
return;
740+
}
741+
742+
req.reply({
743+
statusCode: 200,
744+
body: data
745+
});
746+
}
747+
).as('getChecksWithActualChecks');
748+
749+
const prTitle = 'Test PR Title';
750+
const prDescription = 'Test PR Description';
751+
752+
// Open the top branch.
753+
cy.getByTestId('branch-header', mockBackend.topBranchName).should('be.visible').click();
754+
755+
// The PR card should not be visible for the top branch.
756+
cy.getByTestId('stacked-pull-request-card').should('not.exist');
757+
758+
// Now, open a review for the top branch.
759+
cy.getByDataValue('series-name', mockBackend.topBranchName).within(() => {
760+
cy.getByTestId('create-review-button')
761+
.should('have.length', 1)
762+
.should('be.visible')
763+
.should('be.enabled')
764+
.click();
765+
});
766+
767+
// The Review Drawer should be visible.
768+
cy.getByTestId('create-review-box').should('be.visible').should('have.length', 1);
769+
770+
// Since this branch has a single commit, the commit message should be pre-filled.
771+
// Update both.
772+
cy.getByTestId('create-review-box-title-input')
773+
.should('be.visible')
774+
.should('be.enabled')
775+
.should('have.value', mockBackend.getCommitTitle(mockBackend.topBranchName))
776+
.clear()
777+
.type(prTitle);
778+
779+
cy.getByTestId('create-review-box-description-input')
780+
.should('be.visible')
781+
.should('contain', mockBackend.getCommitMessage(mockBackend.topBranchName))
782+
.click()
783+
.clear()
784+
.type(prDescription);
785+
786+
// The Create Review button should be visible.
787+
// Click it.
788+
cy.getByTestId('create-review-box-create-button')
789+
.should('be.visible')
790+
.should('be.enabled')
791+
.click();
792+
793+
// The PR card should be visible.
794+
cy.getByTestId('stacked-pull-request-card').should('be.visible');
795+
796+
cy.getByTestId('stacked-pull-request-card').within(() => {
797+
cy.getByTestId('pr-status-badge').should('be.visible');
798+
cy.getByDataValue('pr-status', 'open').should('be.visible');
799+
cy.getByTestId('pr-checks-badge').should('be.visible').contains('Checks running');
800+
});
801+
802+
cy.getByTestId('branch-card', mockBackend.topBranchName)
803+
.should('be.visible')
804+
.within(() => {
805+
cy.getByTestId('pr-checks-badge-reduced').should('be.visible');
806+
});
807+
808+
cy.wait(
809+
['@getChecksWithActualChecks', '@getChecksWithActualChecks', '@getChecksWithActualChecks'],
810+
{ timeout: 11000 }
811+
).spread((first, second, third) => {
812+
expect(first.response.body).to.deep.equal(data);
813+
expect(second.response.body).to.deep.equal(data);
814+
expect(third.response.body).to.deep.equal(finishedData);
815+
});
816+
817+
cy.getByTestId('stacked-pull-request-card').within(() => {
818+
cy.getByTestId('pr-status-badge').should('be.visible');
819+
cy.getByDataValue('pr-status', 'open').should('be.visible');
820+
cy.getByTestId('pr-checks-badge').should('be.visible').contains('Checks passed');
821+
});
822+
823+
cy.getByTestId('branch-card', mockBackend.topBranchName)
824+
.should('be.visible')
825+
.within(() => {
826+
cy.getByTestId('pr-checks-badge-reduced').should('be.visible');
827+
});
828+
});
829+
830+
it('Should fail fast when checking for multiple checks', () => {
831+
const data: CustomChecksData = {
832+
total_count: 2,
833+
check_runs: [
834+
{
835+
id: 1,
836+
started_at: new Date(Date.now() - 10000).toISOString(),
837+
conclusion: null,
838+
completed_at: null,
839+
head_sha: 'abc123',
840+
name: 'CI Check 1',
841+
status: 'in_progress'
842+
},
843+
{
844+
id: 2,
845+
started_at: new Date(Date.now() - 10000).toISOString(),
846+
conclusion: null,
847+
completed_at: null,
848+
head_sha: 'abc123',
849+
name: 'CI Check 2',
850+
status: 'in_progress'
851+
}
852+
]
853+
};
854+
855+
const oneCheckFailed: CustomChecksData = {
856+
total_count: 1,
857+
check_runs: [
858+
{
859+
...data.check_runs[0]
860+
},
861+
{
862+
...data.check_runs[1],
863+
status: 'completed',
864+
conclusion: 'failure',
865+
completed_at: new Date().toISOString()
866+
}
867+
]
868+
};
869+
870+
let requestCount = 0;
871+
872+
cy.intercept(
873+
{
874+
method: 'GET',
875+
url: 'https://api.github.com/repos/example/repo/commits/check-runs'
876+
},
877+
(req) => {
878+
requestCount++;
879+
if (requestCount > 2) {
880+
req.reply({
881+
statusCode: 200,
882+
body: oneCheckFailed
883+
});
884+
return;
885+
}
886+
887+
req.reply({
888+
statusCode: 200,
889+
body: data
890+
});
891+
}
892+
).as('getChecksWithActualChecks');
893+
894+
const prTitle = 'Test PR Title';
895+
const prDescription = 'Test PR Description';
896+
897+
// Open the top branch.
898+
cy.getByTestId('branch-header', mockBackend.topBranchName).should('be.visible').click();
899+
900+
// The PR card should not be visible for the top branch.
901+
cy.getByTestId('stacked-pull-request-card').should('not.exist');
902+
903+
// Now, open a review for the top branch.
904+
cy.getByDataValue('series-name', mockBackend.topBranchName).within(() => {
905+
cy.getByTestId('create-review-button')
906+
.should('have.length', 1)
907+
.should('be.visible')
908+
.should('be.enabled')
909+
.click();
910+
});
911+
912+
// The Review Drawer should be visible.
913+
cy.getByTestId('create-review-box').should('be.visible').should('have.length', 1);
914+
915+
// Since this branch has a single commit, the commit message should be pre-filled.
916+
// Update both.
917+
cy.getByTestId('create-review-box-title-input')
918+
.should('be.visible')
919+
.should('be.enabled')
920+
.should('have.value', mockBackend.getCommitTitle(mockBackend.topBranchName))
921+
.clear()
922+
.type(prTitle);
923+
924+
cy.getByTestId('create-review-box-description-input')
925+
.should('be.visible')
926+
.should('contain', mockBackend.getCommitMessage(mockBackend.topBranchName))
927+
.click()
928+
.clear()
929+
.type(prDescription);
930+
931+
// The Create Review button should be visible.
932+
// Click it.
933+
cy.getByTestId('create-review-box-create-button')
934+
.should('be.visible')
935+
.should('be.enabled')
936+
.click();
937+
938+
// The PR card should be visible.
939+
cy.getByTestId('stacked-pull-request-card').should('be.visible');
940+
941+
cy.getByTestId('stacked-pull-request-card').within(() => {
942+
cy.getByTestId('pr-status-badge').should('be.visible');
943+
cy.getByDataValue('pr-status', 'open').should('be.visible');
944+
cy.getByTestId('pr-checks-badge').should('be.visible').contains('Checks running');
945+
});
946+
947+
cy.getByTestId('branch-card', mockBackend.topBranchName)
948+
.should('be.visible')
949+
.within(() => {
950+
cy.getByTestId('pr-checks-badge-reduced').should('be.visible');
951+
});
952+
953+
cy.wait(
954+
['@getChecksWithActualChecks', '@getChecksWithActualChecks', '@getChecksWithActualChecks'],
955+
{ timeout: 11000 }
956+
).spread((first, second, third) => {
957+
expect(first.response.body).to.deep.equal(data);
958+
expect(second.response.body).to.deep.equal(data);
959+
expect(third.response.body).to.deep.equal(oneCheckFailed);
960+
});
961+
962+
cy.getByTestId('branch-card', mockBackend.topBranchName)
963+
.should('be.visible')
964+
.within(() => {
965+
cy.getByTestId('pr-checks-badge-reduced').should('be.visible');
966+
});
967+
968+
cy.getByTestId('stacked-pull-request-card').within(() => {
969+
cy.getByTestId('pr-status-badge').should('be.visible');
970+
cy.getByDataValue('pr-status', 'open').should('be.visible');
971+
cy.getByTestId('pr-checks-badge')
972+
.should('be.visible')
973+
.contains('Checks failed')
974+
.trigger('mouseover');
975+
});
976+
});
689977
});

apps/desktop/src/components/BranchCard.svelte

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
import BranchHeader from '$components/BranchHeader.svelte';
55
import BranchHeaderContextMenu from '$components/BranchHeaderContextMenu.svelte';
66
import CardOverlay from '$components/CardOverlay.svelte';
7+
import ChecksPolling from '$components/ChecksPolling.svelte';
78
import CreateReviewBox from '$components/CreateReviewBox.svelte';
89
import Dropzone from '$components/Dropzone.svelte';
910
import PrNumberUpdater from '$components/PrNumberUpdater.svelte';
1011
import { MoveCommitDzHandler } from '$lib/commits/dropHandler';
1112
import { ReorderCommitDzHandler } from '$lib/dragging/stackingReorderDropzoneManager';
13+
import { DefaultForgeFactory } from '$lib/forge/forgeFactory.svelte';
1214
import { StackService } from '$lib/stacks/stackService.svelte';
1315
import { UiState } from '$lib/state/uiState.svelte';
1416
import { TestId } from '$lib/testing/testIds';
@@ -80,7 +82,8 @@
8082
8183
let { projectId, branchName, active, lineColor, readonly, ...args }: Props = $props();
8284
83-
const [uiState, stackService] = inject(UiState, StackService);
85+
const [uiState, stackService, forge] = inject(UiState, StackService, DefaultForgeFactory);
86+
const prService = $derived(forge.current.prService);
8487
8588
const [updateName, nameUpdate] = stackService.updateBranchName;
8689
@@ -191,7 +194,17 @@
191194
<ReviewBadge brId={args.reviewId} brStatus="unknown" />
192195
{/if}
193196
{#if args.prNumber}
197+
{@const prResult = prService?.get(args.prNumber, { forceRefetch: true })}
198+
{@const pr = prResult?.current.data}
194199
<ReviewBadge prNumber={args.prNumber} prStatus="unknown" />
200+
{#if pr && !pr.closedAt && forge.current.checks && pr.state === 'open'}
201+
<ChecksPolling
202+
branchName={pr.sourceBranch}
203+
isFork={pr.fork}
204+
isMerged={pr.merged}
205+
reduced
206+
/>
207+
{/if}
195208
{/if}
196209
</div>
197210
{/if}
@@ -316,7 +329,9 @@
316329
}
317330
318331
.branch-header__review-badges {
332+
box-sizing: border-box;
319333
display: flex;
334+
align-items: center;
320335
gap: 4px;
321336
}
322337

0 commit comments

Comments
 (0)