stackchain-dashboard/tests/e2e/test_mobile_search_preview_navigation.py
timmy 4855bc7c8d
All checks were successful
CI / lint (pull_request) Successful in 3m42s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 6m32s
CI / release-candidate (pull_request) Has been skipped
feat: contain mobile Search focus through Preview (Closes #1390)
2026-08-25 10:08:55 +00:00

150 lines
7.7 KiB
Python

from __future__ import annotations
import os
import re
from pathlib import Path
import pytest
if os.getenv("STACKCHAIN_RUN_RELEASE_E2E") != "1":
pytest.skip("rendered mobile Search Preview checks run 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__).resolve().parents[2]
FRONTEND = ROOT / "frontend"
@pytest.mark.parametrize(("width", "height"), [(320, 568), (390, 844)])
def test_rendered_mobile_search_preview_navigation_preserves_reading_space_and_draft(width, height):
html = re.sub(r'<script src="static/[^"]+"></script>', "", (FRONTEND / "index.html").read_text())
with sync_playwright() as playwright:
browser = playwright.chromium.launch()
page = browser.new_page(viewport={"width": width, "height": height})
page.set_content(html)
page.add_style_tag(path=FRONTEND / "dashboard.css")
page.locator("#search-preview").evaluate("node => node.classList.add('open')")
page.locator("#search-preview-reply-workspace").evaluate("node => node.hidden = false")
page.locator(".search-preview-navigation").evaluate("node => node.hidden = false")
page.locator("#search-preview-overview").evaluate("node => node.style.minHeight = '500px'")
page.locator("#search-preview-conversation").evaluate("node => node.style.minHeight = '500px'")
page.locator("#search-preview-reply-workspace").evaluate("node => node.style.minHeight = '500px'")
page.locator("#search-preview-actions").evaluate("node => node.style.minHeight = '300px'")
page.add_script_tag(path=FRONTEND / "search-preview.js")
page.evaluate("""() => {
const assignedIssue = {
kind:'issue', state:'open', claimable:false, assigned_to_me:false,
assignees:['alexander'], watching:false,
};
globalThis.renderSearchPreviewWatch(
assignedIssue, {status:'ready'}, document.querySelector('#watch-search-result')
);
}""")
page.add_script_tag(path=FRONTEND / "mobile-search-preview-nav.js")
watch = page.locator("#watch-search-result")
expect(watch).to_be_visible()
expect(watch).to_have_text("Watch issue")
watch_bounds = watch.bounding_box()
assert watch_bounds and watch_bounds["height"] >= 44
navigation = page.locator(".mobile-search-preview-nav")
expect(navigation).to_be_visible()
buttons = navigation.locator("button:visible")
assert navigation.locator("button").count() == 5
assert buttons.count() == 4
expect(navigation.locator('[data-search-preview-section="changes"]')).to_be_hidden()
for button in buttons.all():
bounds = button.bounding_box()
assert bounds and bounds["height"] >= 44
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
assert page.locator(".search-preview-panel").evaluate("node => node.scrollWidth <= node.clientWidth")
page.locator("#search-preview-reply").fill("Keep this reply while navigating")
navigation.locator('[data-search-preview-section="conversation"]').click()
expect(navigation.locator('[data-search-preview-section="conversation"]')).to_have_attribute(
"aria-current", "location"
)
navigation.locator('[data-search-preview-section="reply"]').click()
expect(page.locator("#search-preview-reply")).to_be_focused()
assert page.locator("#search-preview-reply").input_value() == "Keep this reply while navigating"
navigation.locator('[data-search-preview-section="actions"]').click()
expect(navigation.locator('[data-search-preview-section="actions"]')).to_have_attribute(
"aria-current", "location"
)
continuation = page.locator(".search-preview-navigation")
assert continuation.evaluate("node => getComputedStyle(node).position") == "sticky"
assert continuation.evaluate("node => getComputedStyle(node).bottom") == "0px"
assert page.locator("#search-preview-actions").evaluate("node => getComputedStyle(node).position") == "static"
page.locator("#next-search-result").click()
expect(navigation.locator('[data-search-preview-section="overview"]')).to_have_attribute(
"aria-current", "location"
)
assert page.locator("#search-preview-reply").input_value() == "Keep this reply while navigating"
page.set_viewport_size({"width": 800, "height": 700})
expect(navigation).to_be_hidden()
browser.close()
@pytest.mark.parametrize(("width", "height"), [(320, 568), (390, 844)])
def test_rendered_search_to_preview_journey_contains_focus_until_final_close(width, height):
html = re.sub(r'<script src="static/[^"]+"></script>', "", (FRONTEND / "index.html").read_text())
with sync_playwright() as playwright:
browser = playwright.chromium.launch()
page = browser.new_page(viewport={"width": width, "height": height})
page.set_content(html)
page.add_style_tag(path=FRONTEND / "dashboard.css")
page.add_script_tag(path=FRONTEND / "mobile-search-modal.js")
page.evaluate("""() => {
const palette = document.querySelector('#cmd-palette');
const preview = document.querySelector('#search-preview');
palette.classList.add('open');
window.searchModalTest = createMobileSearchModal({document});
window.searchModalTest.activate(palette, {
opener:document.querySelector('[data-mobile-task="search"]'),
initialFocus:document.querySelector('#cmd-input'),
});
preview.append(Object.assign(document.createElement('button'), {id:'dynamic-preview-action', textContent:'Dynamic action'}));
}""")
expect(page.locator("#cmd-input")).to_be_focused()
expect(page.locator("#cmd-palette")).to_have_attribute("aria-modal", "true")
assert page.locator("body > header").evaluate("node => node.inert") is True
assert page.locator("main").evaluate("node => node.inert") is True
assert page.locator("#mobile-task-dock").evaluate("node => node.inert") is True
page.evaluate("""() => {
document.querySelector('#cmd-palette').classList.remove('open');
document.querySelector('#search-preview').classList.add('open');
searchModalTest.transition(document.querySelector('#search-preview'), {
initialFocus:document.querySelector('#close-search-preview'),
});
document.querySelector('#dynamic-preview-action').focus();
}""")
page.keyboard.press("Tab")
expect(page.locator("#close-search-preview")).to_be_focused()
assert page.locator("main").evaluate("node => node.inert") is True
page.evaluate("""() => {
document.querySelector('#search-preview').classList.remove('open');
document.querySelector('#cmd-palette').classList.add('open');
searchModalTest.transition(document.querySelector('#cmd-palette'), {
initialFocus:document.querySelector('#cmd-input'),
});
}""")
expect(page.locator("#cmd-input")).to_be_focused()
assert page.locator("main").evaluate("node => node.inert") is True
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
page.evaluate("searchModalTest.deactivate()")
expect(page.locator('[data-mobile-task="search"]')).to_be_focused()
assert page.locator("body > header").evaluate("node => node.inert") is False
assert page.locator("main").evaluate("node => node.inert") is False
assert page.locator("#mobile-task-dock").evaluate("node => node.inert") is False
browser.close()