197 lines
7.4 KiB
Python
197 lines
7.4 KiB
Python
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,
|
||
)
|