test(workout): prove double-action focus recovery

Extend the browser gate to cover double primary action recovery, post-update phone hierarchy, visible focus rings, and no-motion behavior; keep a unit proof for undoing accidental double advance.

req: examples/001

req: local/001

req: local/004
This commit is contained in:
slhx agent
2026-06-11 23:56:20 +02:00
parent 7a44c3ee9e
commit 176259afd6
3 changed files with 62 additions and 5 deletions
+32 -5
View File
@@ -58,20 +58,21 @@ async fn browser_proves_phone_first_workout_flow() -> WebDriverResult<()> {
wait_for_body_text(&driver, "Rest 90s").await?;
wait_for_body_text(&driver, "Start Goblet squat set 2").await?;
wait_for_body_text(&driver, "Undo is available").await?;
assert_viewport(&driver, "phone after rest", true).await?;
driver
.find(By::XPath("//button[contains(., 'Skip exercise')]"))
.find(By::Css(".primary-action"))
.await?
.click()
.await?;
wait_for_body_text(&driver, "skipped Goblet squat").await?;
wait_for_body_text(&driver, "started Goblet squat set 2").await?;
driver
.find(By::XPath("//button[contains(., 'Undo last action')]"))
.await?
.click()
.await?;
wait_for_body_text(&driver, "Undid: skipped Goblet squat").await?;
wait_for_body_text(&driver, "Undid: started Goblet squat set 2").await?;
let host_panel = driver.find(By::Css(".host-proof")).await?;
assert!(!host_panel.attr("open").await?.is_some());
@@ -147,14 +148,40 @@ async fn assert_viewport(
const host = document.querySelector('.host-proof');
const appRect = app.getBoundingClientRect();
const h1Rect = h1.getBoundingClientRect();
const primaryRect = primary.getBoundingClientRect();
const primaryStyle = getComputedStyle(primary);
const gridColumns = getComputedStyle(app).gridTemplateColumns.split(' ').length;
const visible = (el) => !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
const labels = [...document.querySelectorAll('button,input,summary')]
.filter(visible)
.map((el) => (el.textContent || el.getAttribute('aria-label') || '').trim());
const primaryIndex = labels.findIndex((label) => label.length > 0 && label === primary.textContent.trim());
const undoIndex = labels.findIndex((label) => label.includes('Undo last action'));
const hostIndex = labels.findIndex((label) => label.includes('Host proof panel'));
primary.focus();
const focusedStyle = getComputedStyle(primary);
const focusRingOk = document.activeElement === primary &&
focusedStyle.outlineStyle !== 'none' &&
parseFloat(focusedStyle.outlineWidth) >= 2;
const zeroDurations = (value) => value.split(',').every((part) => {
const trimmed = part.trim();
return trimmed === '0s' || trimmed === '0ms';
});
const noMotion = [...document.querySelectorAll('*')].every((el) => {
const style = getComputedStyle(el);
return zeroDurations(style.transitionDuration) && zeroDurations(style.animationDuration);
});
return document.documentElement.scrollWidth <= window.innerWidth &&
h1.textContent.trim().length > 0 &&
primary.textContent.trim().length > 0 &&
h1Rect.top < primary.getBoundingClientRect().top &&
primary.getBoundingClientRect().height >= 48 &&
h1Rect.top < primaryRect.top &&
primaryRect.height >= 48 &&
primaryStyle.borderRadius !== '0px' &&
focusRingOk &&
noMotion &&
primaryIndex === 0 &&
undoIndex > primaryIndex &&
hostIndex > undoIndex &&
appRect.width <= window.innerWidth &&
host.open === false &&
(arguments[0] ? gridColumns === 1 : gridColumns >= 2);