feat(plugins): 4 个业务插件代码 + 前台公开页面改造
plugins/navManager:导航 CRUD + 导航分类 CRUD(独立 blueprint) plugins/friendManager:友情链接 CRUD plugins/aboutManager:关于本站 CRUD plugins/siteStats:访问统计主页 + JSON 接口 + /site/* GET 埋点 + nav 卡片点击埋点 applications/view/public/nav.py:导航聚合页 / 分类详情 / JSON 接口,支持 visit_count 注入 applications/view/public/about.py:关于本站公开页 applications/view/public/friend.py:友情链接公开页 templates/public/base.html:响应式首页(搜索框 / 分类抽屉 / 5 主题切换 / footer 隐私小字) templates/public/index.html / category.html:卡片右下角访问次数 templates/public/about.html / friend.html:关于 + 友链前台页 docs/plugins-development.md:插件开发完整指南(生命周期 + 4 个示例 + FAQ + framework 迁移) scripts/:探针与端到端验证脚本(probe_*/verify_*/test_*)
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
"""截图对比分类详情页手机/桌面效果"""
|
||||
import os
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
OUT = r'D:\bwstudio\pear-admin-flask\scripts\screenshots\compare_home'
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=True, args=['--no-sandbox', '--disable-gpu'])
|
||||
for vp_name, vp in [('mobile', {'width':390,'height':844}), ('desktop', {'width':1440,'height':900})]:
|
||||
ctx = browser.new_context(viewport=vp)
|
||||
page = ctx.new_page()
|
||||
url = 'http://192.168.1.10:5000/site/category/%E4%BC%91%E9%97%B2%E5%A8%B1%E4%B9%90'
|
||||
page.goto(url, wait_until='networkidle')
|
||||
page.wait_for_timeout(1500)
|
||||
path = os.path.join(OUT, f'{vp_name}_category_local.png')
|
||||
page.screenshot(path=path, full_page=True)
|
||||
print(f'-> {path}')
|
||||
ctx.close()
|
||||
browser.close()
|
||||
@@ -0,0 +1,36 @@
|
||||
"""对比截图:本地手机端首页 vs 参考站首页(桌面 + 手机两套视口)"""
|
||||
import os
|
||||
import time
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
OUT = r'D:\bwstudio\pear-admin-flask\scripts\screenshots\compare_home'
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
URLS = {
|
||||
'local': 'http://192.168.1.10:5000/',
|
||||
'ref': 'http://192.168.1.100/',
|
||||
}
|
||||
|
||||
VIEWPORTS = {
|
||||
'desktop': {'width': 1440, 'height': 900},
|
||||
'mobile': {'width': 390, 'height': 844}, # iPhone 13
|
||||
}
|
||||
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=True, args=['--no-sandbox', '--disable-gpu'])
|
||||
for vp_name, vp in VIEWPORTS.items():
|
||||
ctx = browser.new_context(viewport=vp, device_scale_factor=2,
|
||||
user_agent='Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15' if vp_name=='mobile' else None)
|
||||
page = ctx.new_page()
|
||||
for label, url in URLS.items():
|
||||
print(f'>> [{vp_name}] {label}: {url}')
|
||||
try:
|
||||
page.goto(url, wait_until='networkidle', timeout=20000)
|
||||
time.sleep(2)
|
||||
path = os.path.join(OUT, f'{vp_name}_{label}.png')
|
||||
page.screenshot(path=path, full_page=True)
|
||||
print(f' -> {path}')
|
||||
except Exception as e:
|
||||
print(f' ERR: {e}')
|
||||
ctx.close()
|
||||
browser.close()
|
||||
@@ -0,0 +1,17 @@
|
||||
"""调试抽屉图标渲染"""
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=True, args=['--no-sandbox', '--disable-gpu'])
|
||||
ctx = browser.new_context(viewport={'width':390,'height':844}, device_scale_factor=2)
|
||||
page = ctx.new_page()
|
||||
page.goto('http://192.168.1.10:5000/', wait_until='networkidle')
|
||||
page.wait_for_timeout(1000)
|
||||
page.click('#cat-trigger-mobile')
|
||||
page.wait_for_timeout(800)
|
||||
|
||||
list_html = page.locator('#cat-drawer-list').evaluate('el => el.innerHTML')
|
||||
print('list innerHTML:', list_html[:800])
|
||||
print('cat-icon count:', page.locator('#cat-drawer-list .cat-icon').count())
|
||||
|
||||
browser.close()
|
||||
@@ -0,0 +1,56 @@
|
||||
"""调试主题按钮点击"""
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=True, args=['--no-sandbox', '--disable-gpu'])
|
||||
ctx = browser.new_context(viewport={'width':1440,'height':900})
|
||||
page = ctx.new_page()
|
||||
|
||||
msgs = []
|
||||
page.on('console', lambda m: msgs.append(f'[{m.type}] {m.text}'))
|
||||
page.on('pageerror', lambda e: msgs.append(f'[PAGE-ERR] {e}'))
|
||||
|
||||
page.goto('http://192.168.1.10:5000/', wait_until='networkidle')
|
||||
page.wait_for_timeout(800)
|
||||
|
||||
print('=== 1. 检查按钮存在 ===')
|
||||
btn_count = page.locator('#theme-toggle').count()
|
||||
print(f' #theme-toggle count: {btn_count}')
|
||||
btn_html = page.locator('#theme-toggle').evaluate('el => el.outerHTML') if btn_count else 'N/A'
|
||||
print(f' btn html: {btn_html[:200]}')
|
||||
|
||||
print('=== 2. 检查菜单存在 ===')
|
||||
menu_count = page.locator('#theme-menu').count()
|
||||
print(f' #theme-menu count: {menu_count}')
|
||||
if menu_count:
|
||||
menu_html = page.locator('#theme-menu').evaluate('el => el.outerHTML.substring(0, 300)')
|
||||
print(f' menu html: {menu_html}')
|
||||
|
||||
print('=== 3. 检查菜单项 ===')
|
||||
items = page.locator('.theme-menu li').count()
|
||||
print(f' li count: {items}')
|
||||
|
||||
print('=== 4. 检查 z-index / 遮挡 ===')
|
||||
btn_box = page.locator('#theme-toggle').bounding_box()
|
||||
print(f' btn box: {btn_box}')
|
||||
menu_box = page.locator('#theme-menu').bounding_box()
|
||||
print(f' menu box (closed): {menu_box}')
|
||||
|
||||
print('=== 5. 点击按钮 ===')
|
||||
page.click('#theme-toggle')
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
print(' menu open class:', page.locator('#theme-menu').evaluate('el => el.className'))
|
||||
print(' menu opacity:', page.locator('#theme-menu').evaluate('el => window.getComputedStyle(el).opacity'))
|
||||
print(' menu pointer-events:', page.locator('#theme-menu').evaluate('el => window.getComputedStyle(el).pointerEvents'))
|
||||
print(' menu display:', page.locator('#theme-menu').evaluate('el => window.getComputedStyle(el).display'))
|
||||
print(' menu visibility:', page.locator('#theme-menu').evaluate('el => window.getComputedStyle(el).visibility'))
|
||||
|
||||
box2 = page.locator('#theme-menu').bounding_box()
|
||||
print(f' menu box (after click): {box2}')
|
||||
|
||||
print('=== 6. 控制台 / 错误日志 ===')
|
||||
for m in msgs[-30:]:
|
||||
print(' ', m)
|
||||
|
||||
browser.close()
|
||||
@@ -0,0 +1,7 @@
|
||||
"""一次性脚本:建 site_nav_click 表(走 Flask app factory)。"""
|
||||
from app import app
|
||||
from applications.extensions import db
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
print("OK: site_nav_click ensured")
|
||||
@@ -0,0 +1,18 @@
|
||||
"""截图本地手机端抽屉展开状态"""
|
||||
import os
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
OUT = r'D:\bwstudio\pear-admin-flask\scripts\screenshots\compare_home'
|
||||
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=True, args=['--no-sandbox', '--disable-gpu'])
|
||||
ctx = browser.new_context(viewport={'width':390,'height':844}, device_scale_factor=2)
|
||||
page = ctx.new_page()
|
||||
page.goto('http://192.168.1.10:5000/', wait_until='networkidle')
|
||||
page.wait_for_timeout(1000)
|
||||
page.click('#cat-trigger-mobile')
|
||||
page.wait_for_timeout(800)
|
||||
path = os.path.join(OUT, 'mobile_local_drawer_v2.png')
|
||||
page.screenshot(path=path, full_page=False)
|
||||
print(f'-> {path}')
|
||||
browser.close()
|
||||
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
导航分类功能端到端探针
|
||||
======================
|
||||
|
||||
不依赖浏览器 + ddddocr + 调试 header,直接用 Flask test_client 走完整流程:
|
||||
|
||||
1. 登录 admin
|
||||
2. GET /system/nav-category/ (主页面 HTML)
|
||||
3. GET /system/nav-category/data (table_api JSON)
|
||||
4. POST /system/nav-category/save (新增)
|
||||
5. PUT /system/nav-category/update (更新)
|
||||
6. PUT /system/nav-category/enable (启用/禁用)
|
||||
7. DELETE /system/nav-category/remove/<id> (删除)
|
||||
8. 验证每个 API 的 {code, message, data} 协议
|
||||
|
||||
退出码:全部通过 → 0;任一失败 → 1
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
sys.path.insert(0, r'D:\bwstudio\pear-admin-flask')
|
||||
|
||||
from applications import create_app
|
||||
|
||||
app = create_app()
|
||||
client = app.test_client()
|
||||
|
||||
|
||||
def _login():
|
||||
"""admin/123456 登录拿 cookie session。"""
|
||||
# 拿一张 captcha —— 通过 test_client 共享 session_transaction
|
||||
r = client.get('/system/passport/getCaptcha')
|
||||
assert r.status_code == 200, r.status_code
|
||||
|
||||
with client.session_transaction() as sess:
|
||||
code = sess.get('code') or ''
|
||||
code = code.lower()
|
||||
assert code, 'captcha code missing'
|
||||
|
||||
r = client.post('/system/passport/login', data={
|
||||
'username': 'admin',
|
||||
'password': '123456',
|
||||
'captcha': code,
|
||||
}, follow_redirects=False)
|
||||
assert r.status_code in (200, 302), r.status_code
|
||||
|
||||
|
||||
def _show(label, resp):
|
||||
body = resp.get_data(as_text=True)
|
||||
print(f' [{resp.status_code}] {label} -> {body[:200]}{"..." if len(body) > 200 else ""}')
|
||||
|
||||
|
||||
def main():
|
||||
_login()
|
||||
print('=== 已登录,开始 E2E 探针 ===\n')
|
||||
|
||||
# 1. 分类主页面 HTML
|
||||
r = client.get('/system/nav-category/')
|
||||
print(f'[HTML main] {r.status_code} content-type={r.content_type}')
|
||||
assert r.status_code == 200
|
||||
assert b'nav-category/data' in r.data or b'\u5bfc\u822a\u5206\u7c7b' in r.data, 'main 页面未包含预期内容'
|
||||
print(' [OK] 分类主页面返回 200,含预期关键字\n')
|
||||
|
||||
# 2. data 接口(table_api 协议:{code, data, count, msg, limit})
|
||||
r = client.get('/system/nav-category/data')
|
||||
assert r.status_code == 200
|
||||
payload = r.get_json()
|
||||
assert payload['code'] == 0, payload
|
||||
before_ids = {row['id'] for row in payload['data']}
|
||||
print(f' [OK] data 接口 code=0,count={payload["count"]},已有分类 {len(before_ids)} 条:{sorted(before_ids)}\n')
|
||||
|
||||
# 3. 新增(success_api 协议:{success, msg})
|
||||
unique_name = f'E2E临时分类{int(__import__("time").time())}'
|
||||
r = client.post('/system/nav-category/save',
|
||||
json={'name': unique_name, 'icon': 'layui-icon-test',
|
||||
'description': 'e2e created', 'sort': 1, 'enable': 1})
|
||||
payload = r.get_json()
|
||||
assert payload.get('success') is True, payload
|
||||
print(f' [OK] 新增成功:{unique_name}\n')
|
||||
|
||||
# 4. 拿到 id
|
||||
r = client.get('/system/nav-category/data')
|
||||
payload = r.get_json()
|
||||
new_id = next((row['id'] for row in payload['data'] if row['name'] == unique_name), None)
|
||||
assert new_id is not None, '新增的分类查不到'
|
||||
print(f' [OK] 查回新增分类 id={new_id}\n')
|
||||
|
||||
# 5. 更新
|
||||
r = client.put('/system/nav-category/update',
|
||||
json={'catId': new_id, 'name': unique_name + '_改',
|
||||
'icon': 'layui-icon-edit', 'description': 'e2e updated',
|
||||
'sort': 99, 'enable': 1})
|
||||
payload = r.get_json()
|
||||
assert payload.get('success') is True, payload
|
||||
print(f' [OK] 更新成功\n')
|
||||
|
||||
# 6. 禁用 → 启用
|
||||
r = client.put('/system/nav-category/disable', json={'catId': new_id})
|
||||
assert r.get_json().get('success') is True
|
||||
r = client.put('/system/nav-category/enable', json={'catId': new_id})
|
||||
assert r.get_json().get('success') is True
|
||||
print(f' [OK] 禁用→启用通过\n')
|
||||
|
||||
# 7. 删除
|
||||
r = client.delete(f'/system/nav-category/remove/{new_id}')
|
||||
assert r.get_json().get('success') is True, r.get_json()
|
||||
print(f' [OK] 删除成功\n')
|
||||
|
||||
# 8. 查回 count 必须等于开始前
|
||||
r = client.get('/system/nav-category/data')
|
||||
payload = r.get_json()
|
||||
after_ids = {row['id'] for row in payload['data']}
|
||||
assert before_ids == after_ids, f'分类集合变化:before={before_ids}, after={after_ids}'
|
||||
print(f' [OK] 删除后 count 还原,新增的 id 已不在集合中\n')
|
||||
|
||||
# 9. 重名检测
|
||||
existing_name = None
|
||||
if payload['data']:
|
||||
existing_name = payload['data'][0]['name']
|
||||
if existing_name:
|
||||
r = client.post('/system/nav-category/save',
|
||||
json={'name': existing_name, 'icon': 'x', 'description': '', 'sort': 0, 'enable': 1})
|
||||
body = r.get_json()
|
||||
assert body.get('success') is False, body
|
||||
print(f' [OK] 重名拦截正常:{body["msg"]}\n')
|
||||
|
||||
print('=== ALL PASS ===')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
sys.exit(0)
|
||||
except AssertionError as e:
|
||||
print(f'\n!!! ASSERT FAIL: {e}')
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f'\n!!! UNEXPECTED: {e}')
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,64 @@
|
||||
import sys, time
|
||||
sys.path.insert(0, r'D:\bwstudio\pear-admin-flask')
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
BASE = 'http://127.0.0.1:5000'
|
||||
|
||||
def login(page):
|
||||
page.goto(f'{BASE}/system/passport/login', wait_until='networkidle')
|
||||
page.wait_for_selector('input[name="captcha"]')
|
||||
# 用一个新的 context request 取同一个 session 的 captcha —— 因为 page.request 和 page 可能共享 cookie
|
||||
resp = page.request.get(f'{BASE}/system/passport/getCaptcha')
|
||||
code = resp.headers.get('x-captcha-code', '').lower()
|
||||
if not code:
|
||||
raise RuntimeError('no captcha code')
|
||||
# 再强制刷新一遍 login 页上的 captcha 图片,确保 server session code == 这张图绑定的那个
|
||||
# 因为 server 端 captcha() 会覆写 session['code'] 为最近一张图
|
||||
page.reload(wait_until='networkidle')
|
||||
page.wait_for_selector('input[name="captcha"]')
|
||||
resp = page.request.get(f'{BASE}/system/passport/getCaptcha')
|
||||
code = resp.headers.get('x-captcha-code', '').lower()
|
||||
print('captcha code =', code)
|
||||
page.fill('input[name="username"]', 'admin')
|
||||
page.fill('input[name="password"]', '123456')
|
||||
page.fill('input[name="captcha"]', code)
|
||||
page.click('button[lay-filter="login"]')
|
||||
# 等待跳转离开 login 页
|
||||
time.sleep(2)
|
||||
print('after login url =', page.url)
|
||||
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=True, args=['--no-sandbox', '--disable-gpu'])
|
||||
ctx = browser.new_context()
|
||||
page = ctx.new_page()
|
||||
|
||||
msgs = []
|
||||
page.on('console', lambda m: msgs.append(f'[{m.type}] {m.text}'))
|
||||
page.on('pageerror', lambda e: msgs.append(f'[PAGE-ERR] {e}'))
|
||||
|
||||
login(page)
|
||||
|
||||
for menu_path in ['/system/nav/', '/system/friend/', '/system/about/', '/system/stats/']:
|
||||
msgs.clear()
|
||||
page.on('console', lambda m: msgs.append(f'[{m.type}] {m.text}'))
|
||||
page.on('pageerror', lambda e: msgs.append(f'[PAGE-ERR] {e}'))
|
||||
print(f'\n===== Visit {menu_path} =====')
|
||||
page.goto(BASE + menu_path, wait_until='networkidle')
|
||||
time.sleep(2.5)
|
||||
row_count = page.locator('.layui-table tbody tr').count()
|
||||
has_loading = page.locator('.layui-table-init').count()
|
||||
none_elem = page.locator('.layui-table-none').count()
|
||||
empty_text = ''
|
||||
if none_elem:
|
||||
empty_text = page.locator('.layui-table-none').first.inner_text() or ''
|
||||
print(f' rows={row_count}, init={has_loading}, none={none_elem}, none_text={empty_text!r}')
|
||||
out = menu_path.strip('/').replace('/', '_') + '.png'
|
||||
path = 'D:\\bwstudio\\pear-admin-flask\\scripts\\screenshots\\' + out
|
||||
import os
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
page.screenshot(path=path, full_page=True)
|
||||
print(f' screenshot -> {path}')
|
||||
for m in msgs[-15:]:
|
||||
print(' ', m)
|
||||
|
||||
browser.close()
|
||||
@@ -0,0 +1,24 @@
|
||||
"""抓参考站 mobile 抽屉展开后的截图"""
|
||||
import os
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
OUT = r'D:\bwstudio\pear-admin-flask\scripts\screenshots\compare_home'
|
||||
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=True, args=['--no-sandbox', '--disable-gpu'])
|
||||
ctx = browser.new_context(viewport={'width':390,'height':844}, device_scale_factor=2,
|
||||
user_agent='Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15')
|
||||
page = ctx.new_page()
|
||||
page.goto('http://192.168.1.100/', wait_until='networkidle')
|
||||
page.wait_for_timeout(1500)
|
||||
|
||||
# 点击左上角汉堡按钮
|
||||
try:
|
||||
page.click('span.mdui-btn-icon', timeout=5000)
|
||||
page.wait_for_timeout(1500)
|
||||
except Exception as e:
|
||||
print(f'click err: {e}')
|
||||
path = os.path.join(OUT, 'mobile_ref_drawer.png')
|
||||
page.screenshot(path=path, full_page=False)
|
||||
print(f'-> {path}')
|
||||
browser.close()
|
||||
@@ -0,0 +1,23 @@
|
||||
"""截图:auto 项 + 5 主题展开"""
|
||||
import os
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
OUT = r'D:\bwstudio\pear-admin-flask\scripts\screenshots\themes'
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=True, args=['--no-sandbox', '--disable-gpu'])
|
||||
ctx = browser.new_context(viewport={'width':1440,'height':900})
|
||||
page = ctx.new_page()
|
||||
page.goto('http://127.0.0.1:5000/', wait_until='networkidle')
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
# 展开菜单
|
||||
page.click('#theme-toggle')
|
||||
page.wait_for_timeout(300)
|
||||
page.evaluate('document.getElementById("theme-menu").classList.add("open")')
|
||||
page.wait_for_timeout(300)
|
||||
path = os.path.join(OUT, 'menu_open_v2.png')
|
||||
page.screenshot(path=path, full_page=False)
|
||||
print(f'-> {path}')
|
||||
browser.close()
|
||||
@@ -0,0 +1,19 @@
|
||||
"""测试点击抽屉分类跳转"""
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=True, args=['--no-sandbox', '--disable-gpu'])
|
||||
ctx = browser.new_context(viewport={'width':390,'height':844}, device_scale_factor=2)
|
||||
page = ctx.new_page()
|
||||
page.goto('http://192.168.1.10:5000/', wait_until='networkidle')
|
||||
page.wait_for_timeout(1000)
|
||||
page.click('#cat-trigger-mobile')
|
||||
page.wait_for_timeout(500)
|
||||
# 点击第二个分类
|
||||
page.locator('#cat-drawer-list li a').nth(1).click()
|
||||
page.wait_for_timeout(800)
|
||||
print('URL after click:', page.url)
|
||||
# 检查当前滚动位置是否在目标 section 附近
|
||||
active_id = page.evaluate('() => { var s = document.querySelector("section.section"); return s ? s.id : null; }')
|
||||
print('first section id:', active_id)
|
||||
browser.close()
|
||||
@@ -0,0 +1,32 @@
|
||||
"""简单交互测试:手机端搜索过滤 + 分类 Tab 点击"""
|
||||
import time
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=True, args=['--no-sandbox', '--disable-gpu'])
|
||||
ctx = browser.new_context(viewport={'width':390,'height':844}, device_scale_factor=2)
|
||||
page = ctx.new_page()
|
||||
page.goto('http://192.168.1.10:5000/', wait_until='networkidle')
|
||||
page.wait_for_timeout(1000)
|
||||
|
||||
# 1. 搜索过滤
|
||||
page.fill('#search-input', '百度')
|
||||
page.wait_for_timeout(800)
|
||||
visible = page.locator('.grid .card:not([style*="display: none"])').count()
|
||||
hidden = page.locator('.grid .card[style*="display: none"]').count()
|
||||
print(f'搜索"百度":可见 {visible} 条,隐藏 {hidden} 条')
|
||||
|
||||
# 2. 清空搜索
|
||||
page.fill('#search-input', '')
|
||||
page.wait_for_timeout(800)
|
||||
visible2 = page.locator('.grid .card:not([style*="display: none"])').count()
|
||||
print(f'清空后:可见 {visible2} 条')
|
||||
|
||||
# 3. 点击第二个分类 Tab
|
||||
tabs = page.locator('.cat-nav-inner a')
|
||||
if tabs.count() > 1:
|
||||
tabs.nth(1).click()
|
||||
page.wait_for_timeout(800)
|
||||
print('点击分类 Tab 后 URL:', page.url)
|
||||
|
||||
browser.close()
|
||||
@@ -0,0 +1,41 @@
|
||||
"""4 种主题切换 + 截图"""
|
||||
import os
|
||||
import time
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
OUT = r'D:\bwstudio\pear-admin-flask\scripts\screenshots\themes'
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
THEMES = ['default', 'sunset', 'forest', 'dark']
|
||||
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=True, args=['--no-sandbox', '--disable-gpu'])
|
||||
ctx = browser.new_context(viewport={'width':1440,'height':900})
|
||||
page = ctx.new_page()
|
||||
page.goto('http://192.168.1.10:5000/', wait_until='networkidle')
|
||||
page.wait_for_timeout(800)
|
||||
|
||||
for theme in THEMES:
|
||||
# 切换主题
|
||||
page.evaluate(f'document.documentElement.setAttribute("data-theme", "{theme}"); localStorage.setItem("bw-theme", "{theme}");')
|
||||
page.wait_for_timeout(500)
|
||||
attr = page.evaluate('document.documentElement.getAttribute("data-theme")')
|
||||
print(f' theme={theme} -> html data-theme={attr}')
|
||||
path = os.path.join(OUT, f'{theme}.png')
|
||||
page.screenshot(path=path, full_page=False)
|
||||
print(f' -> {path}')
|
||||
|
||||
# 验证下拉菜单
|
||||
page.evaluate('document.documentElement.setAttribute("data-theme", "default"); localStorage.removeItem("bw-theme");')
|
||||
page.reload(wait_until='networkidle')
|
||||
page.wait_for_timeout(800)
|
||||
page.click('#theme-toggle')
|
||||
page.wait_for_timeout(300)
|
||||
# JS 强开菜单,避免点击被 document 监听关闭
|
||||
page.evaluate('document.getElementById("theme-menu").classList.add("open")')
|
||||
page.wait_for_timeout(500)
|
||||
path = os.path.join(OUT, 'menu_open.png')
|
||||
page.screenshot(path=path, full_page=False)
|
||||
print(f' menu -> {path}')
|
||||
|
||||
browser.close()
|
||||
@@ -0,0 +1,75 @@
|
||||
"""A+E 主题自适应验证(每个场景前清 localStorage)"""
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=True, args=['--no-sandbox', '--disable-gpu'])
|
||||
ctx = browser.new_context(viewport={'width':1440,'height':900})
|
||||
page = ctx.new_page()
|
||||
|
||||
msgs = []
|
||||
page.on('pageerror', lambda e: msgs.append(f'[ERR] {e}'))
|
||||
|
||||
# 场景 1:saved=auto + system=light → 应 default
|
||||
page.goto('http://127.0.0.1:5000/', wait_until='networkidle')
|
||||
page.evaluate('localStorage.setItem("bw-theme", "auto")')
|
||||
page.reload(wait_until='networkidle')
|
||||
page.wait_for_timeout(500)
|
||||
theme = page.evaluate('document.documentElement.getAttribute("data-theme")')
|
||||
label = page.locator('#theme-toggle-label').inner_text()
|
||||
sys_dark = page.evaluate('window.matchMedia("(prefers-color-scheme: dark)").matches')
|
||||
print(f'[1] saved=auto, system_dark={sys_dark} -> data-theme={theme!r}, label={label!r} (expect default, "跟随系统")')
|
||||
|
||||
# 场景 2:saved=dark + system=light → 应 dark
|
||||
page.evaluate('localStorage.setItem("bw-theme", "dark")')
|
||||
page.reload(wait_until='networkidle')
|
||||
page.wait_for_timeout(500)
|
||||
theme = page.evaluate('document.documentElement.getAttribute("data-theme")')
|
||||
label = page.locator('#theme-toggle-label').inner_text()
|
||||
print(f'[2] saved=dark, system_dark={sys_dark} -> data-theme={theme!r}, label={label!r} (expect dark, "暗色")')
|
||||
|
||||
# 场景 3:saved=default → 应 default
|
||||
page.evaluate('localStorage.setItem("bw-theme", "default")')
|
||||
page.reload(wait_until='networkidle')
|
||||
page.wait_for_timeout(500)
|
||||
theme = page.evaluate('document.documentElement.getAttribute("data-theme")')
|
||||
label = page.locator('#theme-toggle-label').inner_text()
|
||||
print(f'[3] saved=default -> data-theme={theme!r}, label={label!r} (expect default, "蓝绿")')
|
||||
|
||||
# 场景 4:无 localStorage + system=light → 应 default(不持久化为 auto)
|
||||
page.evaluate('localStorage.removeItem("bw-theme")')
|
||||
page.reload(wait_until='networkidle')
|
||||
page.wait_for_timeout(500)
|
||||
theme = page.evaluate('document.documentElement.getAttribute("data-theme")')
|
||||
label = page.locator('#theme-toggle-label').inner_text()
|
||||
saved_after = page.evaluate('localStorage.getItem("bw-theme")')
|
||||
print(f'[4] no saved, system_dark={sys_dark} -> data-theme={theme!r}, label={label!r}, after-save={saved_after!r}')
|
||||
print(f' (system=light 时应 default,不写 auto;只有 system=dark 才写 auto)')
|
||||
|
||||
# 场景 5:从 sunset 点 auto → 应 default + label="跟随系统" + saved=auto
|
||||
page.evaluate('localStorage.setItem("bw-theme", "sunset")')
|
||||
page.reload(wait_until='networkidle')
|
||||
page.wait_for_timeout(500)
|
||||
page.click('#theme-toggle')
|
||||
page.wait_for_timeout(300)
|
||||
page.click('.theme-menu li[data-theme="auto"]')
|
||||
page.wait_for_timeout(500)
|
||||
theme = page.evaluate('document.documentElement.getAttribute("data-theme")')
|
||||
label = page.locator('#theme-toggle-label').inner_text()
|
||||
saved = page.evaluate('localStorage.getItem("bw-theme")')
|
||||
print(f'[5] click auto -> data-theme={theme!r}, label={label!r}, saved={saved!r} (expect default, "跟随系统", auto)')
|
||||
|
||||
# 场景 6:早期脚本无闪烁(head 检查 data-theme 在脚本运行前已设)
|
||||
page.evaluate('localStorage.setItem("bw-theme", "dark")')
|
||||
early_attr = page.evaluate('''
|
||||
() => {
|
||||
return new Promise(resolve => {
|
||||
const iframe = document.createElement('iframe');
|
||||
// 没法直接测 early-script 在主页面执行前的状态,改用:再次模拟 reload 看 meta
|
||||
resolve(null);
|
||||
});
|
||||
}
|
||||
''')
|
||||
|
||||
print()
|
||||
print('JS errors:', msgs[-5:])
|
||||
browser.close()
|
||||
@@ -0,0 +1,30 @@
|
||||
"""验证点击卡片触发 sendBeacon,数据库 +1。"""
|
||||
from playwright.sync_api import sync_playwright
|
||||
from app import app
|
||||
from applications.extensions import db
|
||||
from applications.models import NavClick
|
||||
|
||||
with app.app_context():
|
||||
before = db.session.query(db.func.count(NavClick.id)).filter(NavClick.nav_id == 1).scalar()
|
||||
print('before click:', before)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
ctx = browser.new_context(viewport={'width': 1280, 'height': 800})
|
||||
page = ctx.new_page()
|
||||
page.goto('http://127.0.0.1:5000/site/', wait_until='networkidle')
|
||||
# 拦截请求,看是不是 POST
|
||||
reqs = []
|
||||
page.on('request', lambda r: reqs.append(r) if '/site/nav/1/click' in r.url else None)
|
||||
# 点击第一张卡片
|
||||
card = page.locator('.card[data-nav-id="1"]').first
|
||||
card.click(modifiers=['Meta']) # 按住 Meta 打开新标签,保持当前页
|
||||
page.wait_for_timeout(500)
|
||||
print('captured requests:', [(r.method, r.url) for r in reqs])
|
||||
browser.close()
|
||||
|
||||
with app.app_context():
|
||||
after = db.session.query(db.func.count(NavClick.id)).filter(NavClick.nav_id == 1).scalar()
|
||||
print('after click:', after)
|
||||
assert after == before + 1, f"Expected {before+1}, got {after}"
|
||||
print('PASS: beacon recorded click')
|
||||
@@ -0,0 +1,41 @@
|
||||
"""验证点击菜单项切换主题"""
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=True, args=['--no-sandbox', '--disable-gpu'])
|
||||
ctx = browser.new_context(viewport={'width':1440,'height':900})
|
||||
page = ctx.new_page()
|
||||
page.goto('http://192.168.1.10:5000/', wait_until='networkidle')
|
||||
page.wait_for_timeout(800)
|
||||
|
||||
# 初始状态
|
||||
print('初始主题:', page.evaluate('document.documentElement.getAttribute("data-theme")'))
|
||||
|
||||
# 点 sunset
|
||||
page.click('#theme-toggle')
|
||||
page.wait_for_timeout(300)
|
||||
page.click('.theme-menu li[data-theme="sunset"]')
|
||||
page.wait_for_timeout(300)
|
||||
print('点 sunset 后:', page.evaluate('document.documentElement.getAttribute("data-theme")'))
|
||||
print(' 按钮文字:', page.locator('#theme-toggle-label').inner_text())
|
||||
|
||||
# 点 forest
|
||||
page.click('#theme-toggle')
|
||||
page.wait_for_timeout(300)
|
||||
page.click('.theme-menu li[data-theme="forest"]')
|
||||
page.wait_for_timeout(300)
|
||||
print('点 forest 后:', page.evaluate('document.documentElement.getAttribute("data-theme")'))
|
||||
print(' 按钮文字:', page.locator('#theme-toggle-label').inner_text())
|
||||
|
||||
# 点 dark
|
||||
page.click('#theme-toggle')
|
||||
page.wait_for_timeout(300)
|
||||
page.click('.theme-menu li[data-theme="dark"]')
|
||||
page.wait_for_timeout(300)
|
||||
print('点 dark 后:', page.evaluate('document.documentElement.getAttribute("data-theme")'))
|
||||
print(' 按钮文字:', page.locator('#theme-toggle-label').inner_text())
|
||||
|
||||
# localStorage
|
||||
print('localStorage:', page.evaluate('localStorage.getItem("bw-theme")'))
|
||||
|
||||
browser.close()
|
||||
@@ -0,0 +1,24 @@
|
||||
"""验收:访问首页与移动端首页,截图检查 visit-count 样式与文字。"""
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
with sync_playwright() as p:
|
||||
# 桌面
|
||||
desk = p.chromium.launch().new_context(viewport={'width': 1280, 'height': 800})
|
||||
page = desk.new_page()
|
||||
page.goto('http://127.0.0.1:5000/site/', wait_until='networkidle')
|
||||
page.screenshot(path='D:/bwstudio/pear-admin-flask/scripts/_verify_visit_desktop.png', full_page=True)
|
||||
# 检查 nav_id=1 卡片右下角文字
|
||||
cnt1 = page.evaluate("() => { var c = document.querySelector('.card[data-nav-id=\"1\"] .visit-count'); return c ? c.textContent.trim() : null; }")
|
||||
print('nav_id=1 visit-count text:', cnt1)
|
||||
cnt2 = page.evaluate("() => { var c = document.querySelector('.card[data-nav-id=\"2\"] .visit-count'); return c ? c.textContent.trim() : null; }")
|
||||
print('nav_id=2 visit-count text:', cnt2)
|
||||
desk.close()
|
||||
|
||||
# 移动
|
||||
mob = p.chromium.launch().new_context(viewport={'width': 390, 'height': 844}, is_mobile=True)
|
||||
page2 = mob.new_page()
|
||||
page2.goto('http://127.0.0.1:5000/site/', wait_until='networkidle')
|
||||
page2.screenshot(path='D:/bwstudio/pear-admin-flask/scripts/_verify_visit_mobile.png', full_page=True)
|
||||
mob.close()
|
||||
|
||||
print("OK")
|
||||
Reference in New Issue
Block a user