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:
@@ -1104,6 +1104,33 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn double_primary_action_recovers_through_undo() {
|
||||||
|
// req: examples/001 req: local/001 req: local/004
|
||||||
|
let app = AppState::demo();
|
||||||
|
run(|()| complete_set(app.clone()), ());
|
||||||
|
let double_action = run(|()| complete_set(app.clone()), ());
|
||||||
|
assert!(contains_payload_text(
|
||||||
|
&double_action,
|
||||||
|
"started Goblet squat set 2"
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
app.with_workout(|state| (state.events.len(), state.projection.phase.clone())),
|
||||||
|
(2, WorkoutPhase::Ready)
|
||||||
|
);
|
||||||
|
|
||||||
|
let undone = run(|()| undo_last_action(app.clone()), ());
|
||||||
|
assert!(contains_payload_text(
|
||||||
|
&undone,
|
||||||
|
"Undid: started Goblet squat set 2"
|
||||||
|
));
|
||||||
|
assert!(contains_payload_text(&undone, "Rest 90s"));
|
||||||
|
assert_eq!(
|
||||||
|
app.with_workout(|state| (state.events.len(), state.projection.phase.clone())),
|
||||||
|
(1, WorkoutPhase::Resting)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn correct_last_set_is_replayable_and_undoable() {
|
fn correct_last_set_is_replayable_and_undoable() {
|
||||||
// req: examples/001 req: local/001 req: local/003 req: local/004
|
// req: examples/001 req: local/001 req: local/003 req: local/004
|
||||||
|
|||||||
@@ -46,6 +46,9 @@
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
button:focus,
|
||||||
|
input:focus,
|
||||||
|
summary:focus,
|
||||||
button:focus-visible,
|
button:focus-visible,
|
||||||
input:focus-visible,
|
input:focus-visible,
|
||||||
summary:focus-visible {
|
summary:focus-visible {
|
||||||
|
|||||||
@@ -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, "Rest 90s").await?;
|
||||||
wait_for_body_text(&driver, "Start Goblet squat set 2").await?;
|
wait_for_body_text(&driver, "Start Goblet squat set 2").await?;
|
||||||
wait_for_body_text(&driver, "Undo is available").await?;
|
wait_for_body_text(&driver, "Undo is available").await?;
|
||||||
|
assert_viewport(&driver, "phone after rest", true).await?;
|
||||||
|
|
||||||
driver
|
driver
|
||||||
.find(By::XPath("//button[contains(., 'Skip exercise')]"))
|
.find(By::Css(".primary-action"))
|
||||||
.await?
|
.await?
|
||||||
.click()
|
.click()
|
||||||
.await?;
|
.await?;
|
||||||
wait_for_body_text(&driver, "skipped Goblet squat").await?;
|
wait_for_body_text(&driver, "started Goblet squat set 2").await?;
|
||||||
|
|
||||||
driver
|
driver
|
||||||
.find(By::XPath("//button[contains(., 'Undo last action')]"))
|
.find(By::XPath("//button[contains(., 'Undo last action')]"))
|
||||||
.await?
|
.await?
|
||||||
.click()
|
.click()
|
||||||
.await?;
|
.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?;
|
let host_panel = driver.find(By::Css(".host-proof")).await?;
|
||||||
assert!(!host_panel.attr("open").await?.is_some());
|
assert!(!host_panel.attr("open").await?.is_some());
|
||||||
@@ -147,14 +148,40 @@ async fn assert_viewport(
|
|||||||
const host = document.querySelector('.host-proof');
|
const host = document.querySelector('.host-proof');
|
||||||
const appRect = app.getBoundingClientRect();
|
const appRect = app.getBoundingClientRect();
|
||||||
const h1Rect = h1.getBoundingClientRect();
|
const h1Rect = h1.getBoundingClientRect();
|
||||||
|
const primaryRect = primary.getBoundingClientRect();
|
||||||
const primaryStyle = getComputedStyle(primary);
|
const primaryStyle = getComputedStyle(primary);
|
||||||
const gridColumns = getComputedStyle(app).gridTemplateColumns.split(' ').length;
|
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 &&
|
return document.documentElement.scrollWidth <= window.innerWidth &&
|
||||||
h1.textContent.trim().length > 0 &&
|
h1.textContent.trim().length > 0 &&
|
||||||
primary.textContent.trim().length > 0 &&
|
primary.textContent.trim().length > 0 &&
|
||||||
h1Rect.top < primary.getBoundingClientRect().top &&
|
h1Rect.top < primaryRect.top &&
|
||||||
primary.getBoundingClientRect().height >= 48 &&
|
primaryRect.height >= 48 &&
|
||||||
primaryStyle.borderRadius !== '0px' &&
|
primaryStyle.borderRadius !== '0px' &&
|
||||||
|
focusRingOk &&
|
||||||
|
noMotion &&
|
||||||
|
primaryIndex === 0 &&
|
||||||
|
undoIndex > primaryIndex &&
|
||||||
|
hostIndex > undoIndex &&
|
||||||
appRect.width <= window.innerWidth &&
|
appRect.width <= window.innerWidth &&
|
||||||
host.open === false &&
|
host.open === false &&
|
||||||
(arguments[0] ? gridColumns === 1 : gridColumns >= 2);
|
(arguments[0] ? gridColumns === 1 : gridColumns >= 2);
|
||||||
|
|||||||
Reference in New Issue
Block a user