Add UI automation test cases and webapp-testing skill

This commit is contained in:
2026-05-18 17:56:24 +08:00
parent eb053a347f
commit e0e22b895e
25 changed files with 3533 additions and 0 deletions

View File

@@ -0,0 +1,196 @@
import random
from typing import Dict, List
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, expect
from dulizhan.test_case.Resource.UI.base_page import BasePage
class BlogPage(BasePage):
"""
Joyhub Blog/内容页页面对象。
说明:
- 页面侦察结果未发现明确的 Blog 导航入口;
- 已发现真实导航候选包含 Discover、Following
- 因此进入 blog/内容列表时优先尝试 Blog失败后使用 Discover 作为内容入口兜底。
"""
HOME_URL = "https://joyhub-website-frontend-test.best-envision.com/"
EXPECTED_HOME_TITLE = "Joyhub | Explore Sexual Health, Wellness, and Connection"
NAV_DISCOVER_LINK = "Discover"
def open_home_page(self) -> None:
self.goto(self.HOME_URL)
self.accept_cookie_if_present()
expect(self.page).to_have_title(self.EXPECTED_HOME_TITLE)
def enter_blog_page(self) -> str:
"""
进入 blog/内容页。
优先级:
1. Blog 链接:如果页面后续版本新增 Blog 导航,可直接命中;
2. Discover 链接:侦察结果中存在,作为当前可用内容入口;
3. /blog 直达:仅作为最后兜底,并在无法点击入口时使用。
"""
before_url = self.page.url
blog_locators = [
self.page.get_by_role("link", name="Blog"),
self.page.locator("a", has_text="Blog"),
self.page.get_by_role("link", name=self.NAV_DISCOVER_LINK),
self.page.locator("a", has_text=self.NAV_DISCOVER_LINK),
]
for locator in blog_locators:
try:
if locator.count() > 0 and locator.first.is_visible():
locator.first.click()
self.wait_for_page_ready()
return self.page.url
except Exception:
continue
fallback_blog_url = self.HOME_URL.rstrip("/") + "/blog"
self.goto(fallback_blog_url)
assert self.page.url != before_url or "/blog" in self.page.url.lower(), (
"未能进入 blog/内容页:页面未发现 Blog 入口Discover 入口也不可点击。"
)
return self.page.url
def _get_candidate_blog_links(self) -> List[Dict[str, str]]:
"""
从渲染后的页面中提取可点击的 blog/content 候选链接。
不硬编码未知 selector优先根据 href 语义筛选:
blog/article/post/discover/detail/content。
若无命中,则退化为 main 区域内可见链接,排除导航类链接。
"""
return self.page.evaluate(
"""
() => {
const navTexts = new Set([
'Home',
'Download the App',
'Rewards',
'Support',
'About Us',
'Discover',
'Following',
'Partnerships',
'FAQs',
'Login'
]);
const normalize = value => (value || '').replace(/\\s+/g, ' ').trim();
const links = Array.from(document.querySelectorAll('a[href]'))
.filter(a => {
const rect = a.getBoundingClientRect();
const style = window.getComputedStyle(a);
return rect.width > 0
&& rect.height > 0
&& style.visibility !== 'hidden'
&& style.display !== 'none';
})
.map((a, index) => {
const href = a.href || '';
const text = normalize(a.innerText || a.textContent || a.getAttribute('aria-label') || '');
const imgAlt = normalize(
Array.from(a.querySelectorAll('img'))
.map(img => img.alt)
.filter(Boolean)
.join(' ')
);
return {
index,
href,
text,
imgAlt,
pathname: (() => {
try { return new URL(href).pathname.toLowerCase(); }
catch(e) { return ''; }
})()
};
})
.filter(item => item.href && !item.href.startsWith('javascript:'));
const semanticLinks = links.filter(item =>
/blog|article|post|discover|detail|content|story/i.test(item.href)
&& !navTexts.has(item.text)
);
if (semanticLinks.length > 0) {
return semanticLinks;
}
return links.filter(item =>
!navTexts.has(item.text)
&& item.href !== window.location.href
&& !/#$/.test(item.href)
);
}
"""
)
def click_random_blog(self) -> Dict[str, str]:
"""
随机点击一个 blog/content 候选项。
点击后等待 URL 或页面内容变化。
"""
self.wait_for_page_ready()
candidates = self._get_candidate_blog_links()
assert candidates, (
"未找到可点击的 blog/content 候选链接。"
"页面侦察结果缺少明确 blog 卡片 selector请补充稳定定位如 data-testid='blog-card'"
)
candidate = random.choice(candidates)
before_url = self.page.url
locator = self.page.locator("a[href]").nth(candidate["index"])
locator.scroll_into_view_if_needed(timeout=5000)
try:
with self.page.expect_navigation(wait_until="domcontentloaded", timeout=8000):
locator.click()
except PlaywrightTimeoutError:
locator.click()
self.wait_for_page_ready()
after_url = self.page.url
assert after_url != before_url or self.page.locator("main, article, body").first.is_visible(), (
f"点击 blog/content 候选项后页面未出现有效跳转或内容区域。候选链接:{candidate['href']}"
)
return {
"clicked_href": candidate.get("href", ""),
"clicked_text": candidate.get("text") or candidate.get("imgAlt") or "",
"before_url": before_url,
"after_url": after_url,
}
def assert_blog_content_page_loaded(self) -> None:
"""
断言已进入 blog/content 内容页。
因页面缺少稳定详情页 selector这里采用内容区域可见的稳健断言。
"""
self.wait_for_page_ready()
content_locator = self.page.locator("article, main, [role='main'], body").first
expect(content_locator).to_be_visible(timeout=10000)
body_text = self.page.locator("body").inner_text(timeout=10000).strip()
assert len(body_text) > 0, "blog/content 页面 body 内容为空,疑似跳转失败或页面未渲染完成。"
def capture_blog_content_screenshot(self, screenshot_dir: str, case_key: str) -> str:
return self.screenshot(
screenshot_dir=screenshot_dir,
file_name=f"{case_key}_blog_content.png",
full_page=True,
)