Skip to content

Commit c4acbf5

Browse files
mchwangclaude
andcommitted
Test the control label and banner of each ended merge job, and the merging step's banner
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 09a3192 commit c4acbf5

1 file changed

Lines changed: 46 additions & 0 deletions

File tree

‎test/browser/review.spec.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,52 @@ test('keeps every blocker when the click is answered with a job that ended witho
720720
await expectEndedMergeJob(page,'Merge stopped',ended,[head.message,conflict.message,...jobResolvedBlockers.map(blocker=>blocker.message)]);
721721
expect(polls).toBe(0);
722722
});
723+
/** Ended jobs with the course production runs before each outcome and the reason it saves (runner/merge-job.ts). */
724+
const endedMergeJobs:{state:MergeJobView['state'];course:MergeJobView[];reason:string;label:string;banner:string}[]=[
725+
// GitHub refused the job's merge: the job blocks with the refusal and FIX_THEN_RETRY (#endAsAttempt).
726+
{state:'blocked',course:[mergeJobRecord({step:'merging'})],reason:'GitHub refused the merge: Required status check "ci" is expected. Fix it, then Retry merge.',label:'Merge blocked',
727+
banner:'Approved, merge blocked. GitHub refused the merge: Required status check "ci" is expected. Fix it, then Retry merge.'},
728+
// The preparation's rebase made an approval stale (runner/pre-merge.ts): the job ends from its preparing step.
729+
{state:'review-required',course:[],reason:'P1 requires refreshed attribution or approval.',label:'Review needed',
730+
banner:'Review needed before merging. P1 requires refreshed attribution or approval.'},
731+
// GitHub accepted the merge, but saving its outcome failed.
732+
{state:'submitted',course:[mergeJobRecord({step:'merging'})],reason:'GitHub accepted the merge, but its outcome was not saved; it reconciles from GitHub.',label:'Merge submitted',
733+
banner:'Merge submitted; GitHub\'s outcome is not confirmed yet. GitHub accepted the merge, but its outcome was not saved; it reconciles from GitHub.'},
734+
// The task was cancelled while the job waited for checks.
735+
{state:'cancelled',course:[mergeJobRecord({step:'checks',checks:[{context:'ci',state:'pending'}]})],reason:'Task cancelled.',label:'Merge cancelled',banner:'Merge cancelled.'},
736+
];
737+
for(const variant of endedMergeJobs)test(`names a merge job that ended ${variant.state} on its control and banner (F6b)`,async({page})=>{
738+
const config={...app.service.config,demo:false};await app.close();removeDemoOutOfScope(config);
739+
const gateway:MergeGateway={inspect:async()=>{const snapshot=(await app.service.load()).snapshot;return {base:snapshot.base,head:snapshot.head,pullRequestState:'OPEN',mergeable:'MERGEABLE',rulesKnown:true,atomicBaseGuard:true,mergeQueue:false,requiredChecks:[],alreadyFixed:'clear',pullRequest:7,draft:false};},merge:async()=>{throw new Error('The browser test answers every merge request itself.');}};
740+
app=await startServer(config,0,undefined,gateway);
741+
let view=await app.service.load();for(const segment of view.segments.filter(value=>value.row==='Unplanned'||value.row==='Ambiguous'))view=await app.service.act({action:'accept',key:segment.key,token:view.token});for(const item of view.items)view=await app.service.act({action:'approve',item:item.id,confirmNoChange:item.count===0,token:view.token});
742+
// Each saved change adds one to the revision: preparing 1, then the course, then the end. A job past preparing has
743+
// checked (revision 2) before it waits (3) or merges (4); the course's records carry those defaults.
744+
const lastRevision=variant.course.at(-1)?.revision??1;
745+
const ended=mergeJobRecord({state:variant.state,step:null,reason:variant.reason,revision:lastRevision+1,
746+
mergeActionId:variant.course.some(job=>job.step==='merging')?'0f6b0000-0000-4000-8000-00000000ac01':null});
747+
const running=mergeJobRecord({step:'preparing'});
748+
let polls=0,released=false;
749+
await page.route('**/api/action',async route=>{const body=route.request().postDataJSON();if(body.action!=='merge'){await route.continue();return;}
750+
const response=await route.fetch({url:route.request().url().replace('/api/action','/api/review'),method:'GET'}),review=await response.json();
751+
await route.fulfill({status:200,contentType:'application/json',body:JSON.stringify({...review,merge:{...review.merge,ready:false,action:null,job:running,blockers:[...review.merge.blockers,...jobResolvedBlockers],settledBeforeJobMerge:SETTLED_BEFORE_JOB_MERGE},job:running})});});
752+
// The merging step is held until the test has read its banner.
753+
await page.route('**/api/merge',async route=>{const next=polls<variant.course.length?variant.course[polls]!:ended;if(next.step!=='merging'||released)polls++;
754+
await route.fulfill({status:200,contentType:'application/json',body:JSON.stringify({queue:null,job:next})});});
755+
await page.route('**/api/review',async route=>{const response=await route.fetch(),body=await response.json();
756+
body.merge={...body.merge,ready:true,action:'merge',job:null,blockers:[...body.merge.blockers,...jobResolvedBlockers],settledBeforeJobMerge:SETTLED_BEFORE_JOB_MERGE};await route.fulfill({response,json:body});});
757+
await page.goto(app.url);page.on('dialog',dialog=>dialog.accept());
758+
await page.getByRole('button',{name:'Merge PR',exact:true}).click();
759+
if(variant.course.some(job=>job.step==='merging')){
760+
await expect(page.getByRole('button',{name:'Merging…',exact:true})).toHaveAttribute('aria-disabled','true',{timeout:10_000});
761+
await expect(page.locator('#banner')).toHaveText('Merging the reviewed head.');
762+
released=true;
763+
}
764+
const banner=`${variant.banner} Refresh to see the result.`;
765+
await expect(page.locator('#banner')).toHaveText(banner,{timeout:15_000});
766+
// Only a job that asked GitHub to merge and was accepted (submitted) settled the blockers it resolves.
767+
await expectEndedMergeJob(page,variant.label,banner,variant.state==='submitted'?[]:jobResolvedBlockers.map(blocker=>blocker.message));
768+
});
723769
test('names a failed merge-job poll as a merge-status failure (F6b)',async({page})=>{
724770
const config={...app.service.config,demo:false};await app.close();removeDemoOutOfScope(config);
725771
const gateway:MergeGateway={inspect:async()=>{const snapshot=(await app.service.load()).snapshot;return {base:snapshot.base,head:snapshot.head,pullRequestState:'OPEN',mergeable:'MERGEABLE',rulesKnown:true,atomicBaseGuard:true,mergeQueue:false,requiredChecks:[],alreadyFixed:'clear',pullRequest:7,draft:false};},merge:async()=>{throw new Error('The browser test answers every merge request itself.');}};

0 commit comments

Comments
 (0)