stackchain-dashboard/tests/e2e/test_mobile_release_failure_recovery.py
timmy b753cdd922
Some checks failed
CI / lint (pull_request) Successful in 3m11s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Failing after 6m7s
CI / release-candidate (pull_request) Has been skipped
feat: prepare conflict-safe release rollback pulls (Closes #1370)
2026-08-24 23:55:33 +00:00

136 lines
6.9 KiB
Python

import os
from pathlib import Path
import pytest
if os.getenv("STACKCHAIN_RUN_RELEASE_E2E") != "1":
pytest.skip("packaged release-failure recovery runs only in the browser gate", allow_module_level=True)
pytest.importorskip("playwright.sync_api")
from playwright.sync_api import expect, sync_playwright
ROOT = Path(__file__).parents[2]
FRONTEND = ROOT / "frontend"
@pytest.mark.parametrize("viewport", [
{"width": 320, "height": 568},
{"width": 390, "height": 844},
])
def test_failed_release_check_is_diagnosable_and_retryable_on_phone(viewport):
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
page = browser.new_page(viewport=viewport)
page.set_content((FRONTEND / "index.html").read_text())
page.add_style_tag(path=FRONTEND / "dashboard.css")
page.add_script_tag(path=FRONTEND / "release-receipt.js")
result = page.evaluate("""async () => {
const calls=[];
const values=new Map();
const storage={
getItem:key=>values.get(key)||null,
setItem:(key,value)=>values.set(key,value),
removeItem:key=>values.delete(key),
};
const receipt=createReleaseReceipt({
storage,
getLogin:()=> 'timmy',
listNode:document.querySelector('#release-watchlist'),
confirmAction:()=>true,
fetchJson:async(path, options={})=>{
calls.push({path, method:options.method || 'GET'});
if (path.endsWith('/failure')) return {
commit_sha:'abc1234', run_id:91, job_index:3,
name:'CI / browser', excerpt:'AssertionError: mobile viewport overflow',
};
if (path.endsWith('/retry')) return {commit_sha:'abc1234', status:'queued'};
return {
commit_sha:'abc1234', ci_state:'failure', release:null,
checks:[{
name:'CI / browser with a deliberately long workflow name',
state:'failure', description:'Playwright failed at the narrow viewport',
url:'https://forge.example/git/stackchain/api/actions/runs/91/jobs/3',
recovery:{run_id:91, job_index:3},
}],
};
},
});
receipt.capture(
{repository:'stackchain/api-with-a-long-mobile-name', number:7, key:'stackchain/api-with-a-long-mobile-name#7'},
{merge_commit_sha:'abc1234'},
);
document.querySelector('#release-receipt-sheet').showModal();
await receipt.refresh();
const review=document.querySelector('.release-failure-summary button');
const reviewHeight=review.getBoundingClientRect().height;
review.click();
await new Promise(resolve=>setTimeout(resolve, 0));
const recovery=document.querySelector('.release-failure-recovery');
const retry=recovery.querySelector('button');
const retryHeight=retry.getBoundingClientRect().height;
const excerpt=recovery.querySelector('pre').textContent;
const sha=recovery.querySelector('strong').textContent;
const job=recovery.querySelector('a').href;
const overflow=document.documentElement.scrollWidth > document.documentElement.clientWidth;
retry.click();
await new Promise(resolve=>setTimeout(resolve, 0));
return {
calls, reviewHeight, retryHeight, excerpt, sha, job, overflow,
state:receipt.restore()[0].status.label,
};
}""")
expect(page.locator("#release-receipt-sheet")).to_be_visible()
assert result["reviewHeight"] >= 44
assert result["retryHeight"] >= 44
assert result["excerpt"] == "AssertionError: mobile viewport overflow"
assert result["sha"] == "Merge abc1234"
assert result["job"].endswith("/actions/runs/91/jobs/3")
assert result["overflow"] is False
assert result["state"] == "Checks running"
assert [call["method"] for call in result["calls"]] == ["GET", "GET", "POST"]
browser.close()
@pytest.mark.parametrize("viewport", [
{"width": 320, "height": 568},
{"width": 390, "height": 844},
])
def test_failed_release_prepares_a_visible_draft_rollback_receipt_on_phone(viewport):
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
page = browser.new_page(viewport=viewport)
page.set_content((FRONTEND / "index.html").read_text())
page.add_style_tag(path=FRONTEND / "dashboard.css")
page.add_script_tag(path=FRONTEND / "release-receipt.js")
result = page.evaluate("""async () => {
const values=new Map(), calls=[], opened=[];
const receipt=createReleaseReceipt({
storage:{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)},
getLogin:()=> 'timmy', listNode:document.querySelector('#release-watchlist'),
confirmAction:()=>true, openPull:pull=>opened.push(pull),
fetchJson:async(path, options={})=>{
calls.push({path,method:options.method||'GET'});
if(path.endsWith('/failure')) return {commit_sha:'abc1234',run_id:91,job_index:3,name:'CI / browser',excerpt:'failed'};
if(path.endsWith('/rollback')) return {repository:'stackchain/api',number:9,draft:true,rollback_of:'abc1234',url:'https://forge.example/git/stackchain/api/pulls/9'};
return {commit_sha:'abc1234',ci_state:'failure',release:null,checks:[{name:'CI / browser',state:'failure',recovery:{run_id:91,job_index:3}}]};
},
});
receipt.capture({repository:'stackchain/api',number:7,key:'stackchain/api#7'},{merge_commit_sha:'abc1234'});
document.querySelector('#release-receipt-sheet').showModal();
await receipt.refresh();
document.querySelector('.release-failure-summary button').click();
await new Promise(resolve=>setTimeout(resolve,0));
const rollback=[...document.querySelectorAll('.release-failure-actions button')].find(button=>button.textContent==='Prepare rollback PR');
const actionHeight=rollback.getBoundingClientRect().height;
rollback.click();
await new Promise(resolve=>setTimeout(resolve,0));
const status=document.querySelector('.release-rollback-status');
return {calls,opened,actionHeight,status:status?.textContent||'',overflow:document.documentElement.scrollWidth>document.documentElement.clientWidth};
}""")
assert result["actionHeight"] >= 44
assert result["status"] == "Draft rollback #9 is ready for review."
assert result["opened"][0]["number"] == 9
assert result["calls"][-1]["path"].endswith("/release-receipt/abc1234/rollback")
assert result["overflow"] is False
browser.close()