feat(wiki): 新增 GHC06 任务页与图库发布体系,级联更新既有页面
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the GHC-05 and GHC-06 galleries from the existing KSP screenshots."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE_ROOT = ROOT / "user_input" / "广寒计划"
|
||||
WIKI_ROOT = ROOT / "data" / "wiki"
|
||||
STAGING_ROOT = ROOT / "output" / "wiki-gallery-upload"
|
||||
|
||||
|
||||
GALLERIES = {
|
||||
"GHC-05": {
|
||||
"title": "广寒基地建设任务-05 任务图集",
|
||||
"page": "home/mission/Guanghan_Program/GHC-05",
|
||||
"previous": ("GHC-04", "/home/mission/Guanghan_Program/GHC-04/Gallery"),
|
||||
"next": ("GHC-06", "/home/mission/Guanghan_Program/GHC-06/Gallery"),
|
||||
"folder": "guanghan-ghc-05",
|
||||
"output": "guanghan_ghc05_gallery_zh.html",
|
||||
"intro": "现有任务影像按任务阶段整理,共 28 张。点击图片可打开大图并使用方向键切换。",
|
||||
"items": [
|
||||
("载人发射与地月转移", "CZ-16A二级视角-夜间发射.png", "crew_launch_night.png", "CZ-16A 夜间发射", "CZ-16A 搭载嫦娥六号与吴刚二号升空,画面为夜间上升段的二级视角。"),
|
||||
("载人发射与地月转移", "CZ-16A执行TLI燃烧.png", "crew_tli_burn.png", "CZ-16A 执行 TLI", "运载火箭上面级执行地月转移注入,将嫦娥六号与吴刚二号组合体送往月球。"),
|
||||
("载人发射与地月转移", "嫦娥6号分离.png", "change6_separation.png", "嫦娥六号分离", "嫦娥六号与上面级分离,准备执行掉头和着陆器提取操作。"),
|
||||
("载人发射与地月转移", "嫦娥6号掉头,准备取出吴刚2.png", "change6_turnaround.png", "嫦娥六号掉头", "嫦娥六号完成掉头,接近仍固定在桁架上的吴刚二号。"),
|
||||
("载人发射与地月转移", "嫦娥6号取出吴刚2.png", "change6_extracts_wugang2.png", "嫦娥六号取出吴刚二号", "嫦娥六号对接吴刚二号并将其从发射桁架中拖出,组合体进入巡航构型。"),
|
||||
("载人发射与地月转移", "嫦娥6号近月制动.png", "change6_loi.png", "嫦娥六号近月制动", "嫦娥六号与吴刚二号组合体执行近月制动,进入绕月轨道。"),
|
||||
("吴刚二号着陆", "吴刚2号与嫦娥分离准备着陆.png", "wugang2_separation.png", "吴刚二号分离", "吴刚二号与嫦娥六号分离,开始独立下降与着陆准备。"),
|
||||
("吴刚二号着陆", "吴刚2号主减速段.png", "wugang2_descent_burn.png", "吴刚二号主减速", "吴刚二号主发动机点火执行下降主减速。"),
|
||||
("吴刚二号着陆", "吴刚2号着陆.png", "wugang2_landing.png", "吴刚二号着陆", "吴刚二号在东方海广寒基地附近完成月面着陆。"),
|
||||
("广寒 03 模块运输", "CZ-16发射广寒03模块.png", "gh03_launch.png", "CZ-16 发射广寒 03", "CZ-16 搭载广寒 03 扩建模块从文昌升空。"),
|
||||
("广寒 03 模块运输", "CZ-16发射广寒03模块2.png", "gh03_launch_second_view.png", "广寒 03 发射另一视角", "广寒 03 模块发射上升段的另一视角。"),
|
||||
("广寒 03 模块运输", "金蟾1号与广寒03模块对接.png", "jinchan1_docks_gh03.png", "金蟾一号对接广寒 03", "金蟾一号在近地轨道与广寒 03 模块完成对接。"),
|
||||
("广寒 03 模块运输", "金蟾1号携带广寒03TLI.png", "jinchan1_gh03_tli.png", "金蟾一号携带广寒 03 执行 TLI", "金蟾一号携带广寒 03 模块执行地月转移点火。"),
|
||||
("广寒 03 模块运输", "冰轮3号和金蟾1号交汇.png", "binglun3_rendezvous_jinchan1.png", "冰轮三号与金蟾一号交会", "冰轮三号在绕月轨道接近携带广寒 03 模块的金蟾一号。"),
|
||||
("广寒 03 模块运输", "冰轮3号着陆.png", "binglun3_landing_gh03.png", "冰轮三号运送广寒 03 着陆", "冰轮三号携带广寒 03 模块在广寒基地着陆。"),
|
||||
("冰轮运输调度", "冰轮2号起飞.png", "binglun2_takeoff.png", "冰轮二号起飞", "冰轮二号从月面起飞,执行燃料运输任务。"),
|
||||
("冰轮运输调度", "冰轮2号与金蟾1号对接.png", "binglun2_docks_jinchan1.png", "冰轮二号对接金蟾一号", "冰轮二号在绕月轨道与金蟾一号对接并转移燃料。"),
|
||||
("冰轮运输调度", "金蟾1号点火返回地球(可见后方冰轮2号).png", "jinchan1_return_burn_binglun2.png", "金蟾一号返航点火", "金蟾一号执行返回地球点火,画面后方可见冰轮二号。"),
|
||||
("冰轮运输调度", "金蟾1号近地制动.png", "jinchan1_earth_braking.png", "金蟾一号近地制动", "金蟾一号返回地球附近并执行近地制动。"),
|
||||
("冰轮运输调度", "冰轮2号着陆中.png", "binglun2_descent.png", "冰轮二号下降", "冰轮二号完成轨道补给后返回月面。"),
|
||||
("冰轮运输调度", "冰轮2号着陆.png", "binglun2_landing.png", "冰轮二号着陆", "冰轮二号重新着陆于广寒基地。"),
|
||||
("乘组撤离与返回", "吴刚2号起飞(01-19).png", "wugang2_takeoff.png", "吴刚二号起飞", "吴刚二号搭载 GHC-05 乘组从月面起飞。"),
|
||||
("乘组撤离与返回", "嫦娥6号与吴刚2号对接(01-19).png", "change6_docks_wugang2.png", "嫦娥六号对接吴刚二号", "嫦娥六号在绕月轨道与吴刚二号完成对接。"),
|
||||
("乘组撤离与返回", "嫦娥6号携带吴刚2号升轨(01-19).png", "change6_wugang2_raise_orbit.png", "嫦娥六号携带吴刚二号升轨", "组合体升轨至停泊轨道,吴刚二号随后留轨复用。"),
|
||||
("乘组撤离与返回", "冰轮3号起飞.png", "binglun3_takeoff.png", "冰轮三号起飞", "冰轮三号从月面起飞执行下一轮轨道运输。"),
|
||||
("乘组撤离与返回", "冰轮3号起飞过程中,从嫦娥6号45公里处略过.png", "binglun3_passes_change6.png", "冰轮三号掠过嫦娥六号", "冰轮三号上升过程中从嫦娥六号约 45 公里外掠过。"),
|
||||
("乘组撤离与返回", "嫦娥6号点火返回地球(01-20).png", "change6_return_burn.png", "嫦娥六号返回点火", "嫦娥六号离开绕月轨道,执行返回地球点火。"),
|
||||
("乘组撤离与返回", "嫦娥6号再入(01-22).png", "change6_reentry.png", "嫦娥六号再入", "嫦娥六号返回舱再入地球大气层。"),
|
||||
],
|
||||
},
|
||||
"GHC-06": {
|
||||
"title": "广寒基地建设任务-06 任务图集",
|
||||
"page": "home/mission/Guanghan_Program/GHC-06",
|
||||
"previous": ("GHC-05", "/home/mission/Guanghan_Program/GHC-05/Gallery"),
|
||||
"next": None,
|
||||
"folder": "guanghan-ghc-06",
|
||||
"output": "guanghan_ghc06_gallery_zh.html",
|
||||
"intro": "现有任务影像按任务阶段整理,共 25 张。点击图片可打开大图并使用方向键切换。",
|
||||
"items": [
|
||||
("载人飞行与月面着陆", "CZ-16A二级携带组合体进行TLI.png", "crew_tli.png", "CZ-16A 二级执行 TLI", "CZ-16A 二级携带嫦娥七号与吴刚三号组合体执行地月转移注入。"),
|
||||
("载人飞行与月面着陆", "嫦娥7号准备和吴刚飞船对接(1月6日).png", "change7_dock_approach.png", "嫦娥七号接近吴刚三号", "嫦娥七号完成掉头,准备与吴刚三号对接。"),
|
||||
("载人飞行与月面着陆", "嫦娥7号取出吴刚飞船.png", "change7_extracts_wugang3.png", "嫦娥七号取出吴刚三号", "嫦娥七号将吴刚三号从发射桁架中拖出。"),
|
||||
("载人飞行与月面着陆", "嫦娥7号近月制动.png", "change7_loi.png", "嫦娥七号近月制动", "嫦娥七号与吴刚三号组合体执行近月制动。"),
|
||||
("载人飞行与月面着陆", "吴刚3号准备着陆(1月11号).png", "wugang3_landing_approach.png", "吴刚三号准备着陆", "吴刚三号脱离绕月轨道,开始月面下降。"),
|
||||
("载人飞行与月面着陆", "吴刚3号着陆中.png", "wugang3_landing.png", "吴刚三号着陆", "吴刚三号执行末段下降并着陆于东方海。"),
|
||||
("广寒 04 模块运输", "CZ-10E发射广寒04模块(02-01).png", "gh04_launch.png", "CZ-10E 发射广寒 04", "CZ-10E 搭载广寒 04 扩建模块升空。"),
|
||||
("广寒 04 模块运输", "金蟾3号准备和广寒04模块对接(02-01).png", "jinchan3_dock_gh04.png", "金蟾三号接近广寒 04", "金蟾三号在近地轨道接近并准备对接广寒 04 模块。"),
|
||||
("广寒 04 模块运输", "金蟾3号携带广寒04模块执行TLI.png", "jinchan3_gh04_tli.png", "金蟾三号携带广寒 04 执行 TLI", "金蟾三号携带广寒 04 模块开始地月转移。"),
|
||||
("广寒 04 模块运输", "冰轮3号变轨(02-08).png", "binglun3_deorbit_gh04.png", "冰轮三号变轨", "冰轮三号接收广寒 04 模块后执行下降变轨。"),
|
||||
("广寒 04 模块运输", "冰轮3号着陆主减速段(02-08).png", "binglun3_descent_gh04.png", "冰轮三号主减速", "冰轮三号携带广寒 04 模块执行着陆主减速。"),
|
||||
("广寒 04 模块运输", "冰轮3号着陆(02-08).png", "binglun3_landing_gh04.png", "冰轮三号运送广寒 04 着陆", "冰轮三号将广寒 04 模块送达广寒基地。"),
|
||||
("冰轮编队调度", "冰轮2号起飞(02-08).png", "binglun2_takeoff.png", "冰轮二号起飞", "冰轮二号从月面起飞执行轨道燃料运输。"),
|
||||
("冰轮编队调度", "冰轮2号与金蟾1号交汇(02-08).png", "binglun2_rendezvous_jinchan1.png", "冰轮二号与金蟾一号交会", "冰轮二号在绕月轨道接近金蟾一号。"),
|
||||
("冰轮编队调度", "冰轮2号着陆(02-08).png", "binglun2_landing.png", "冰轮二号返回月面", "冰轮二号完成补给任务后返回广寒基地。"),
|
||||
("冰轮编队调度", "冰轮3号起飞(02-08).png", "binglun3_takeoff.png", "冰轮三号起飞", "完成广寒 04 卸载后,冰轮三号再次起飞执行轨道任务。"),
|
||||
("冰轮编队调度", "金蟾1号泊入近地轨道(02-13).png", "jinchan1_leo_arrival.png", "金蟾一号泊入近地轨道", "金蟾一号返回地球附近并泊入近地轨道。"),
|
||||
("乘组撤离与返回", "吴刚3号准备起飞(25-03-14).png", "wugang3_prepares_takeoff.png", "吴刚三号准备起飞", "燃料系统修复完成后,吴刚三号进行月面起飞准备。"),
|
||||
("乘组撤离与返回", "吴刚3号起飞(25-03-15).png", "wugang3_takeoff.png", "吴刚三号起飞", "吴刚三号搭载 GHC-06 乘组从月面起飞。"),
|
||||
("乘组撤离与返回", "吴刚3号与嫦娥7号准备对接(25-03-15).png", "wugang3_change7_rendezvous.png", "吴刚三号接近嫦娥七号", "吴刚三号在绕月轨道接近嫦娥七号并完成乘组转移;随后组合体升轨至 400×400 km,吴刚三号留轨复用。"),
|
||||
("乘组撤离与返回", "嫦娥7号点火返回地球(25-03-16).png", "change7_return_burn.png", "嫦娥七号返回点火", "嫦娥七号离开绕月轨道,开始返回地球。"),
|
||||
("乘组撤离与返回", "嫦娥7号抛服务舱(25-03-19).png", "change7_service_module_sep.png", "嫦娥七号抛离服务舱", "再入前,嫦娥七号返回舱与服务舱分离。"),
|
||||
("乘组撤离与返回", "嫦娥7号再入(25-03-19).png", "change7_reentry.png", "嫦娥七号再入", "嫦娥七号返回舱穿越地球大气层。"),
|
||||
("乘组撤离与返回", "嫦娥7号开主伞(25-03-19).png", "change7_main_chutes.png", "嫦娥七号打开主伞", "返回舱主降落伞展开,进入最终下降阶段。"),
|
||||
("乘组撤离与返回", "嫦娥7号返回文昌(25-03-19).png", "change7_wenchang_landing.png", "嫦娥七号返回文昌", "嫦娥七号返回舱在文昌回收场安全着陆,GHC-06 任务完成。"),
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
BASE_STYLE = """<style>
|
||||
.wiki-article { max-width: 1180px; margin: 0 auto; padding: 24px 32px 48px; color: #202122; background: #fff; font-family: sans-serif; font-size: 14px; line-height: 1.65; }
|
||||
.wiki-article h1 { font-family: \"Linux Libertine\", Georgia, serif; font-weight: normal; border-bottom: 1px solid #a2a9b1; margin: 0 0 8px; font-size: 2.15em; }
|
||||
.wiki-article h2 { font-family: \"Linux Libertine\", Georgia, serif; font-weight: normal; border-bottom: 1px solid #a2a9b1; clear: both; margin: 1.35em 0 0.6em; padding-bottom: 0.18em; font-size: 1.55em; }
|
||||
.wiki-article p { margin: 0.45em 0 0.85em; }
|
||||
.wiki-article a { color: #0645ad; text-decoration: none; }
|
||||
.wiki-article a:hover { text-decoration: underline; }
|
||||
.wiki-article img { max-width: 100%; height: auto; }
|
||||
.wiki-article .thumbcaption { padding: 4px 3px 2px; text-align: left; color: #202122; }
|
||||
.wiki-article .gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; margin: 1em 0; }
|
||||
.wiki-article .gallery-item { border: 1px solid #c8ccd1; background: #f8f9fa; padding: 4px; font-size: 88%; line-height: 1.4; }
|
||||
.wiki-article .gallery-item img { display: block; width: 100%; cursor: zoom-in; }
|
||||
.wiki-article .gallery-item .thumbcaption { padding: 4px 2px 2px; }
|
||||
@media (max-width: 820px) { .wiki-article { padding: 18px 16px 36px; } .gallery-grid { grid-template-columns: 1fr; } }
|
||||
</style>"""
|
||||
|
||||
|
||||
def create_staged_asset(source: Path, staged: Path) -> None:
|
||||
staged.parent.mkdir(parents=True, exist_ok=True)
|
||||
if staged.exists():
|
||||
if staged.stat().st_size != source.stat().st_size:
|
||||
staged.unlink()
|
||||
else:
|
||||
return
|
||||
try:
|
||||
os.link(source, staged)
|
||||
except OSError:
|
||||
staged.write_bytes(source.read_bytes())
|
||||
|
||||
|
||||
def build_gallery(code: str, config: dict) -> None:
|
||||
source_dir = SOURCE_ROOT / code
|
||||
staging_dir = STAGING_ROOT / code.lower()
|
||||
sections: dict[str, list[str]] = {}
|
||||
|
||||
for section, source_name, upload_name, alt, caption in config["items"]:
|
||||
source = source_dir / source_name
|
||||
if not source.exists():
|
||||
raise FileNotFoundError(source)
|
||||
create_staged_asset(source, staging_dir / upload_name)
|
||||
asset_path = f'/{config["folder"]}/{upload_name}'
|
||||
card = (
|
||||
f'<div class="gallery-item"><img src="{asset_path}" alt="{html.escape(alt)}" '
|
||||
f'data-wiki-asset="{asset_path}"><div class="thumbcaption">{html.escape(caption)}</div></div>'
|
||||
)
|
||||
sections.setdefault(section, []).append(card)
|
||||
|
||||
previous = config["previous"]
|
||||
next_item = config["next"]
|
||||
nav_parts = [f'<a href="{previous[1]}">← {previous[0]} 图集</a>']
|
||||
if next_item:
|
||||
nav_parts.append(f'<a href="{next_item[1]}">{next_item[0]} 图集 →</a>')
|
||||
nav = " | ".join(nav_parts)
|
||||
|
||||
body = [BASE_STYLE, '<article class="mw-parser-output wiki-article">']
|
||||
body.append(f'<h1 id="top">{config["title"]}</h1>')
|
||||
body.append(f'<p><a href="/{config["page"]}">← 返回任务页</a> | 共 {len(config["items"])} 张现有影像</p>')
|
||||
body.append(f'<p style="margin-bottom:1.5em;"><b>任务图集导航</b>:{nav}</p>')
|
||||
body.append(f'<p>{config["intro"]}</p>')
|
||||
for section, cards in sections.items():
|
||||
body.extend((f'<h2>{section}</h2>', '<div class="gallery-grid">', *cards, '</div>'))
|
||||
body.append(f'<p style="margin-top:1.5em;"><b>任务图集导航</b>:{nav}</p>')
|
||||
body.append(f'<p style="margin-top:1.5em;"><a href="/{config["page"]}">← 返回任务页</a></p>')
|
||||
body.append('</article>')
|
||||
|
||||
output_path = WIKI_ROOT / config["output"]
|
||||
output_path.write_text("\n".join(body) + "\n", encoding="utf-8", newline="\n")
|
||||
print(f"gallery_built mission={code} images={len(config['items'])} output={output_path}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
for code, config in GALLERIES.items():
|
||||
build_gallery(code, config)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,324 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply the shared gallery presentation and lightbox behavior to mission galleries."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
WIKI_DIR = ROOT / "data" / "wiki"
|
||||
GALLERY_FILES = sorted(WIKI_DIR.glob("guanghan_*_gallery_zh.html"))
|
||||
|
||||
STYLE_END_RE = re.compile(r"</style>", re.IGNORECASE)
|
||||
SCRIPT_RE = re.compile(r"\s*<script>.*?</script>\s*", re.IGNORECASE | re.DOTALL)
|
||||
ASSET_IMAGE_RE = re.compile(r"<img\b(?=[^>]*\bdata-wiki-asset=)[^>]*>", re.IGNORECASE)
|
||||
CSS_BLOCK_RE = re.compile(
|
||||
r"\n?/\* mission-gallery-enhancements:start \*/.*?"
|
||||
r"/\* mission-gallery-enhancements:end \*/\n?",
|
||||
re.DOTALL,
|
||||
)
|
||||
LEGACY_LIGHTBOX_CSS_RE = re.compile(
|
||||
r"\n?\.wiki-article img\[data-wiki-asset\][^\n]*\n"
|
||||
r".*?@media \(max-width: 820px\) \{ \.wiki-image-lightbox \{.*?\}\s*\}\n?",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
GALLERY_CSS = r"""
|
||||
/* mission-gallery-enhancements:start */
|
||||
.wiki-article .gallery-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
|
||||
align-items: stretch;
|
||||
}
|
||||
.wiki-article .gallery-item {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
overflow: hidden;
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: 340px 300px;
|
||||
}
|
||||
.wiki-article .gallery-item img[data-wiki-asset] {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
object-fit: contain;
|
||||
background: #202122;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
.wiki-article .gallery-item .thumbcaption {
|
||||
padding: 8px 7px 7px;
|
||||
}
|
||||
.wiki-article .gallery-notice {
|
||||
margin: 1em 0 1.25em;
|
||||
padding: 10px 12px;
|
||||
border-left: 4px solid #36c;
|
||||
background: #f1f4fd;
|
||||
color: #202122;
|
||||
}
|
||||
.wiki-article .image-placeholder {
|
||||
position: relative;
|
||||
border: 1px dashed #72777d;
|
||||
background: linear-gradient(135deg, #f8f9fa, #eaecf0);
|
||||
color: #54595d;
|
||||
}
|
||||
.wiki-article.gallery-planning .gallery-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 360px), 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.wiki-article.gallery-planning .gallery-item {
|
||||
display: block;
|
||||
content-visibility: visible;
|
||||
contain-intrinsic-size: none;
|
||||
border-color: #a2a9b1;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(32, 33, 34, 0.08);
|
||||
}
|
||||
.wiki-article.gallery-planning .image-placeholder {
|
||||
min-height: 0 !important;
|
||||
display: block;
|
||||
padding: 14px 14px 12px;
|
||||
border: 0;
|
||||
border-left: 4px solid #72777d;
|
||||
background: #f8f9fa;
|
||||
color: #202122;
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.wiki-article.gallery-planning .gallery-item .thumbcaption {
|
||||
padding: 10px 14px 12px;
|
||||
border-top: 1px solid #eaecf0;
|
||||
color: #54595d;
|
||||
}
|
||||
.wiki-article.gallery-planning > .image-placeholder {
|
||||
min-height: 0;
|
||||
padding: 22px 18px;
|
||||
border-left: 4px solid #72777d;
|
||||
}
|
||||
.wiki-image-lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: rgba(32, 33, 34, 0.88);
|
||||
z-index: 10000;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.wiki-image-lightbox[hidden] { display: none; }
|
||||
.wiki-image-lightbox-panel {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr) 44px;
|
||||
grid-template-areas: "close close close" "prev image next" "counter caption caption";
|
||||
gap: 8px;
|
||||
width: min(1320px, 96vw);
|
||||
max-height: 94vh;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #a2a9b1;
|
||||
background: #fff;
|
||||
box-shadow: 0 12px 38px rgba(0, 0, 0, 0.35);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.wiki-image-lightbox-panel > img {
|
||||
grid-area: image;
|
||||
width: 100%;
|
||||
height: min(78vh, 900px);
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
}
|
||||
.wiki-image-lightbox-close {
|
||||
grid-area: close;
|
||||
justify-self: end;
|
||||
position: static;
|
||||
float: none;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
margin: 0;
|
||||
border: 1px solid #a2a9b1;
|
||||
border-radius: 3px;
|
||||
background: #eaecf0;
|
||||
color: #202122;
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.wiki-image-lightbox-close:hover,
|
||||
.wiki-image-lightbox-close:focus { background: #fff; }
|
||||
.wiki-image-lightbox-prev { grid-area: prev; }
|
||||
.wiki-image-lightbox-next { grid-area: next; }
|
||||
.wiki-image-lightbox-nav {
|
||||
align-self: center;
|
||||
width: 40px;
|
||||
height: 56px;
|
||||
border: 1px solid #a2a9b1;
|
||||
border-radius: 3px;
|
||||
background: #eaecf0;
|
||||
color: #202122;
|
||||
font-size: 26px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.wiki-image-lightbox-nav:hover,
|
||||
.wiki-image-lightbox-nav:focus { background: #fff; }
|
||||
.wiki-image-lightbox-counter { grid-area: counter; color: #54595d; white-space: nowrap; }
|
||||
.wiki-image-lightbox-panel .thumbcaption { grid-area: caption; padding: 0 4px 3px; }
|
||||
body.wiki-image-lightbox-open { overflow: hidden; }
|
||||
@media (max-width: 820px) {
|
||||
.wiki-article .gallery-grid { grid-template-columns: 1fr; }
|
||||
.wiki-image-lightbox-panel {
|
||||
grid-template-columns: 36px minmax(0, 1fr) 36px;
|
||||
width: 98vw;
|
||||
}
|
||||
.wiki-image-lightbox-panel > img { height: 70vh; }
|
||||
.wiki-image-lightbox-nav { width: 34px; height: 48px; }
|
||||
}
|
||||
/* mission-gallery-enhancements:end */
|
||||
""".strip()
|
||||
|
||||
GALLERY_JS = r"""<script>
|
||||
(function () {
|
||||
function ready(fn) {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', fn, { once: true });
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
}
|
||||
|
||||
function captionFor(image) {
|
||||
var container = image.closest('.gallery-item, figure, td, tr, .infobox-image');
|
||||
var caption = container && container.querySelector('.thumbcaption');
|
||||
return caption ? caption.textContent.trim() : (image.alt || '');
|
||||
}
|
||||
|
||||
function install(article) {
|
||||
if (!article || article.dataset.galleryLightboxReady === '1') { return; }
|
||||
var images = Array.prototype.slice.call(article.querySelectorAll('img[data-wiki-asset]'));
|
||||
if (!images.length) { return; }
|
||||
article.dataset.galleryLightboxReady = '1';
|
||||
|
||||
var overlay = document.createElement('div');
|
||||
overlay.className = 'wiki-image-lightbox';
|
||||
overlay.hidden = true;
|
||||
overlay.innerHTML = '<figure class="wiki-image-lightbox-panel" role="dialog" aria-modal="true" aria-label="任务图集图片预览">' +
|
||||
'<button class="wiki-image-lightbox-close" type="button" aria-label="关闭图片预览">×</button>' +
|
||||
'<button class="wiki-image-lightbox-nav wiki-image-lightbox-prev" type="button" aria-label="上一张">‹</button>' +
|
||||
'<img alt="">' +
|
||||
'<button class="wiki-image-lightbox-nav wiki-image-lightbox-next" type="button" aria-label="下一张">›</button>' +
|
||||
'<span class="wiki-image-lightbox-counter" aria-live="polite"></span>' +
|
||||
'<figcaption class="thumbcaption"></figcaption></figure>';
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
var previewImage = overlay.querySelector('img');
|
||||
var previewCaption = overlay.querySelector('figcaption');
|
||||
var counter = overlay.querySelector('.wiki-image-lightbox-counter');
|
||||
var closeButton = overlay.querySelector('.wiki-image-lightbox-close');
|
||||
var previousButton = overlay.querySelector('.wiki-image-lightbox-prev');
|
||||
var nextButton = overlay.querySelector('.wiki-image-lightbox-next');
|
||||
var activeIndex = 0;
|
||||
var lastTrigger = null;
|
||||
|
||||
function render(index) {
|
||||
activeIndex = (index + images.length) % images.length;
|
||||
var image = images[activeIndex];
|
||||
previewImage.src = image.dataset.wikiAsset || image.currentSrc || image.src;
|
||||
previewImage.alt = image.alt || '';
|
||||
previewCaption.textContent = captionFor(image);
|
||||
counter.textContent = (activeIndex + 1) + ' / ' + images.length;
|
||||
}
|
||||
|
||||
function closePreview() {
|
||||
overlay.hidden = true;
|
||||
previewImage.removeAttribute('src');
|
||||
document.body.classList.remove('wiki-image-lightbox-open');
|
||||
if (lastTrigger) { lastTrigger.focus(); }
|
||||
}
|
||||
|
||||
function openPreview(image, event) {
|
||||
if (event) { event.preventDefault(); event.stopPropagation(); }
|
||||
lastTrigger = image;
|
||||
render(images.indexOf(image));
|
||||
overlay.hidden = false;
|
||||
document.body.classList.add('wiki-image-lightbox-open');
|
||||
closeButton.focus();
|
||||
}
|
||||
|
||||
images.forEach(function (image) {
|
||||
image.tabIndex = 0;
|
||||
image.setAttribute('role', 'button');
|
||||
image.setAttribute('aria-label', '打开图片预览:' + (image.alt || '任务影像'));
|
||||
image.addEventListener('click', function (event) { openPreview(image, event); });
|
||||
image.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Enter' || event.key === ' ') { openPreview(image, event); }
|
||||
});
|
||||
});
|
||||
|
||||
previousButton.hidden = images.length < 2;
|
||||
nextButton.hidden = images.length < 2;
|
||||
previousButton.addEventListener('click', function () { render(activeIndex - 1); });
|
||||
nextButton.addEventListener('click', function () { render(activeIndex + 1); });
|
||||
closeButton.addEventListener('click', closePreview);
|
||||
overlay.addEventListener('click', function (event) {
|
||||
if (event.target === overlay) { closePreview(); }
|
||||
});
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (overlay.hidden) { return; }
|
||||
if (event.key === 'Escape') { closePreview(); }
|
||||
if (event.key === 'ArrowLeft') { render(activeIndex - 1); }
|
||||
if (event.key === 'ArrowRight') { render(activeIndex + 1); }
|
||||
});
|
||||
}
|
||||
|
||||
ready(function () {
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.wiki-article'), install);
|
||||
});
|
||||
}());
|
||||
</script>"""
|
||||
|
||||
|
||||
def add_image_loading_attributes(match: re.Match[str]) -> str:
|
||||
tag = match.group(0)
|
||||
additions = []
|
||||
if not re.search(r"\bloading=", tag, re.IGNORECASE):
|
||||
additions.append('loading="lazy"')
|
||||
if not re.search(r"\bdecoding=", tag, re.IGNORECASE):
|
||||
additions.append('decoding="async"')
|
||||
if not re.search(r"\bfetchpriority=", tag, re.IGNORECASE):
|
||||
additions.append('fetchpriority="low"')
|
||||
if not additions:
|
||||
return tag
|
||||
return tag[:-1].rstrip() + " " + " ".join(additions) + ">"
|
||||
|
||||
|
||||
def normalize(path: Path) -> bool:
|
||||
original = path.read_text(encoding="utf-8")
|
||||
updated = LEGACY_LIGHTBOX_CSS_RE.sub("\n", original)
|
||||
updated = CSS_BLOCK_RE.sub("\n", updated)
|
||||
updated = STYLE_END_RE.sub(GALLERY_CSS + "\n</style>", updated, count=1)
|
||||
updated = ASSET_IMAGE_RE.sub(add_image_loading_attributes, updated)
|
||||
|
||||
updated = SCRIPT_RE.sub("\n", updated).rstrip()
|
||||
updated += "\n"
|
||||
|
||||
if updated == original:
|
||||
return False
|
||||
path.write_text(updated, encoding="utf-8", newline="\n")
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
changed = 0
|
||||
for path in GALLERY_FILES:
|
||||
was_changed = normalize(path)
|
||||
changed += int(was_changed)
|
||||
print(f"gallery_normalized file={path.name} changed={str(was_changed).lower()}")
|
||||
print(f"gallery_normalized_summary files={len(GALLERY_FILES)} changed={changed}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,108 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__kspMissionGalleryRuntimeV1) { return; }
|
||||
window.__kspMissionGalleryRuntimeV1 = true;
|
||||
|
||||
var selector = '.wiki-article .gallery-grid img[data-wiki-asset]';
|
||||
var overlay = document.createElement('div');
|
||||
overlay.className = 'wiki-image-lightbox';
|
||||
overlay.hidden = true;
|
||||
overlay.innerHTML = '<figure class="wiki-image-lightbox-panel" role="dialog" aria-modal="true" aria-label="Mission gallery image preview">' +
|
||||
'<button class="wiki-image-lightbox-close" type="button" aria-label="Close image preview">×</button>' +
|
||||
'<button class="wiki-image-lightbox-nav wiki-image-lightbox-prev" type="button" aria-label="Previous image">‹</button>' +
|
||||
'<img alt="">' +
|
||||
'<button class="wiki-image-lightbox-nav wiki-image-lightbox-next" type="button" aria-label="Next image">›</button>' +
|
||||
'<span class="wiki-image-lightbox-counter" aria-live="polite"></span>' +
|
||||
'<figcaption class="thumbcaption"></figcaption></figure>';
|
||||
|
||||
var previewImage = overlay.querySelector('img');
|
||||
var previewCaption = overlay.querySelector('figcaption');
|
||||
var counter = overlay.querySelector('.wiki-image-lightbox-counter');
|
||||
var closeButton = overlay.querySelector('.wiki-image-lightbox-close');
|
||||
var previousButton = overlay.querySelector('.wiki-image-lightbox-prev');
|
||||
var nextButton = overlay.querySelector('.wiki-image-lightbox-next');
|
||||
var images = [];
|
||||
var activeIndex = 0;
|
||||
var lastTrigger = null;
|
||||
|
||||
function captionFor(image) {
|
||||
var container = image.closest('.gallery-item, figure, td, tr, .infobox-image');
|
||||
var caption = container && container.querySelector('.thumbcaption');
|
||||
return caption ? caption.textContent.trim() : (image.alt || '');
|
||||
}
|
||||
|
||||
function enhanceImages(root) {
|
||||
var scope = root && root.querySelectorAll ? root : document;
|
||||
Array.prototype.forEach.call(scope.querySelectorAll(selector), function (image) {
|
||||
image.tabIndex = 0;
|
||||
image.setAttribute('role', 'button');
|
||||
image.setAttribute('aria-label', 'Open image preview: ' + (image.alt || 'mission image'));
|
||||
var article = image.closest('.wiki-article');
|
||||
if (article) { article.dataset.galleryLightboxReady = '1'; }
|
||||
});
|
||||
}
|
||||
|
||||
function render(index) {
|
||||
activeIndex = (index + images.length) % images.length;
|
||||
var image = images[activeIndex];
|
||||
previewImage.src = image.dataset.wikiAsset || image.currentSrc || image.src;
|
||||
previewImage.alt = image.alt || '';
|
||||
previewCaption.textContent = captionFor(image);
|
||||
counter.textContent = (activeIndex + 1) + ' / ' + images.length;
|
||||
previousButton.hidden = images.length < 2;
|
||||
nextButton.hidden = images.length < 2;
|
||||
}
|
||||
|
||||
function openPreview(image, event) {
|
||||
var article = image.closest('.wiki-article');
|
||||
images = Array.prototype.slice.call(article.querySelectorAll('.gallery-grid img[data-wiki-asset]'));
|
||||
if (!images.length) { return; }
|
||||
if (event) { event.preventDefault(); event.stopPropagation(); }
|
||||
lastTrigger = image;
|
||||
render(images.indexOf(image));
|
||||
overlay.hidden = false;
|
||||
document.body.classList.add('wiki-image-lightbox-open');
|
||||
closeButton.focus();
|
||||
}
|
||||
|
||||
function closePreview() {
|
||||
overlay.hidden = true;
|
||||
previewImage.removeAttribute('src');
|
||||
document.body.classList.remove('wiki-image-lightbox-open');
|
||||
if (lastTrigger && lastTrigger.isConnected) { lastTrigger.focus(); }
|
||||
}
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
enhanceImages(document);
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var image = event.target.closest && event.target.closest(selector);
|
||||
if (image) { openPreview(image, event); }
|
||||
});
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (!overlay.hidden) {
|
||||
if (event.key === 'Escape') { closePreview(); }
|
||||
if (event.key === 'ArrowLeft') { render(activeIndex - 1); }
|
||||
if (event.key === 'ArrowRight') { render(activeIndex + 1); }
|
||||
return;
|
||||
}
|
||||
if ((event.key === 'Enter' || event.key === ' ') && document.activeElement.matches(selector)) {
|
||||
openPreview(document.activeElement, event);
|
||||
}
|
||||
});
|
||||
previousButton.addEventListener('click', function () { render(activeIndex - 1); });
|
||||
nextButton.addEventListener('click', function () { render(activeIndex + 1); });
|
||||
closeButton.addEventListener('click', closePreview);
|
||||
overlay.addEventListener('click', function (event) {
|
||||
if (event.target === overlay) { closePreview(); }
|
||||
});
|
||||
|
||||
new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (mutation) {
|
||||
Array.prototype.forEach.call(mutation.addedNodes, function (node) {
|
||||
if (node.nodeType === 1) { enhanceImages(node); }
|
||||
});
|
||||
});
|
||||
}).observe(document.body, { childList: true, subtree: true });
|
||||
}());
|
||||
@@ -0,0 +1,114 @@
|
||||
"""Publish the mission-gallery runtime through Wiki.js global theme injection."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import psycopg
|
||||
from psycopg.rows import dict_row
|
||||
|
||||
from wiki_update_page import (
|
||||
WikiUpdateError,
|
||||
connect_info,
|
||||
find_auth_group,
|
||||
get_json_cast,
|
||||
graphql,
|
||||
grant_temporary_system_permission,
|
||||
load_env,
|
||||
login,
|
||||
normalize_json,
|
||||
remove_temporary_system_permission,
|
||||
)
|
||||
|
||||
|
||||
START = "<!-- ksp-mission-gallery-runtime:start -->"
|
||||
END = "<!-- ksp-mission-gallery-runtime:end -->"
|
||||
|
||||
|
||||
def replace_managed_block(existing: str, block: str) -> str:
|
||||
start = existing.find(START)
|
||||
end = existing.find(END)
|
||||
if start >= 0 and end >= start:
|
||||
end += len(END)
|
||||
return (existing[:start] + block + existing[end:]).strip()
|
||||
return (existing.strip() + "\n" + block).strip()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
env = load_env(root / ".env")
|
||||
conninfo = connect_info(env)
|
||||
identities = [value for value in (env.get("wiki_useremail"), env.get("wiki_username")) if value]
|
||||
if not identities or not env.get("wiki_password"):
|
||||
raise WikiUpdateError("Missing Wiki credentials")
|
||||
|
||||
runtime = (root / "scripts" / "wiki_mission_gallery_runtime.js").read_text(encoding="utf-8").strip()
|
||||
managed_block = f"{START}\n<script data-ksp-mission-gallery-runtime>\n{runtime}\n</script>\n{END}"
|
||||
group_id = None
|
||||
try:
|
||||
with psycopg.connect(**conninfo, row_factory=dict_row) as conn:
|
||||
with conn.cursor() as cur:
|
||||
group = find_auth_group(cur, "automation", [identity.lower() for identity in identities])
|
||||
group_id = group["id"]
|
||||
grant_temporary_system_permission(cur, group)
|
||||
conn.commit()
|
||||
|
||||
token = login(env.get("WIKI_URL", "http://192.168.195.241:38353").rstrip("/"), identities, env["wiki_password"])
|
||||
wiki_url = env.get("WIKI_URL", "http://192.168.195.241:38353").rstrip("/")
|
||||
query = """
|
||||
query ThemeConfig {
|
||||
theming { config { theme iconset darkMode tocPosition injectCSS injectHead injectBody } }
|
||||
}
|
||||
"""
|
||||
current_payload = graphql(wiki_url, query, token=token)
|
||||
config = current_payload.get("data", {}).get("theming", {}).get("config")
|
||||
if not config:
|
||||
raise WikiUpdateError(f"Unable to read theme config: {json.dumps(current_payload)}")
|
||||
|
||||
inject_body = replace_managed_block(config.get("injectBody") or "", managed_block)
|
||||
mutation = """
|
||||
mutation SetTheme($theme: String!, $iconset: String!, $darkMode: Boolean!, $tocPosition: String, $injectCSS: String, $injectHead: String, $injectBody: String) {
|
||||
theming {
|
||||
setConfig(theme: $theme, iconset: $iconset, darkMode: $darkMode, tocPosition: $tocPosition, injectCSS: $injectCSS, injectHead: $injectHead, injectBody: $injectBody) {
|
||||
responseResult { succeeded errorCode slug message }
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
updated = graphql(
|
||||
wiki_url,
|
||||
mutation,
|
||||
{
|
||||
"theme": config["theme"],
|
||||
"iconset": config["iconset"],
|
||||
"darkMode": config["darkMode"],
|
||||
"tocPosition": config.get("tocPosition"),
|
||||
"injectCSS": config.get("injectCSS") or "",
|
||||
"injectHead": config.get("injectHead") or "",
|
||||
"injectBody": inject_body,
|
||||
},
|
||||
token,
|
||||
)
|
||||
result = updated.get("data", {}).get("theming", {}).get("setConfig", {}).get("responseResult", {})
|
||||
print(
|
||||
"theming.setConfig succeeded={succeeded} errorCode={errorCode} slug={slug} message={message}".format(
|
||||
**{key: result.get(key) for key in ("succeeded", "errorCode", "slug", "message")}
|
||||
)
|
||||
)
|
||||
if not result.get("succeeded"):
|
||||
raise WikiUpdateError(f"Theme runtime publish failed: {json.dumps(updated)}")
|
||||
print(f"mission_gallery_runtime_published bytes={len(runtime.encode('utf-8'))}")
|
||||
return 0
|
||||
finally:
|
||||
if group_id is not None:
|
||||
remove_temporary_system_permission(conninfo, group_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except WikiUpdateError as exc:
|
||||
print(f"wiki_runtime_error={exc}")
|
||||
sys.exit(2)
|
||||
@@ -328,8 +328,8 @@ def extract_script_to_script_js(content):
|
||||
if not match:
|
||||
print("script_js_extracted=False")
|
||||
return content, ""
|
||||
# Wiki.js injects scriptJs verbatim into the rendered page body, so the
|
||||
# payload must retain its script element instead of containing bare JS.
|
||||
# Wiki.js injects scriptJs verbatim into the rendered body. Preserve the
|
||||
# script element; a bare payload is parsed as body text instead of executed.
|
||||
script_js = match.group(0).strip()
|
||||
content_without_script = (content[: match.start()] + content[match.end() :]).rstrip()
|
||||
print(f"script_js_extracted=True length={len(script_js)}")
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"""End-to-end regression for every Guanghan mission gallery on Wiki.js."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
|
||||
REAL_GALLERIES = (
|
||||
"BL-01",
|
||||
"BL-02",
|
||||
"BL-03",
|
||||
"GHC-01",
|
||||
"GHC-02",
|
||||
"GHC-03",
|
||||
"GHC-04",
|
||||
"GHC-05",
|
||||
"GHC-06",
|
||||
"JC-01",
|
||||
)
|
||||
PLANNING_GALLERIES = ("JC-02",)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--base-url", default="http://118.89.192.134:59100")
|
||||
parser.add_argument("--screenshot-dir", type=Path)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def gallery_url(base_url: str, mission: str) -> str:
|
||||
return f"{base_url.rstrip('/')}/zh/home/mission/Guanghan_Program/{mission}/Gallery"
|
||||
|
||||
|
||||
def verify_real_gallery(page, mission: str, url: str, screenshot_dir: Path | None) -> dict:
|
||||
page.goto(url, wait_until="domcontentloaded", timeout=60_000)
|
||||
article = page.locator(".wiki-article")
|
||||
article.wait_for(state="attached", timeout=30_000)
|
||||
images = page.locator(".wiki-article img[data-wiki-asset]")
|
||||
images.first.wait_for(state="attached", timeout=30_000)
|
||||
image_count = images.count()
|
||||
if image_count < 1:
|
||||
raise AssertionError("no real gallery images found")
|
||||
|
||||
page.wait_for_function(
|
||||
"document.querySelector('.wiki-article')?.dataset.galleryLightboxReady === '1'",
|
||||
timeout=15_000,
|
||||
)
|
||||
first_image = images.first
|
||||
first_image.scroll_into_view_if_needed(timeout=15_000)
|
||||
first_image.wait_for(state="visible", timeout=15_000)
|
||||
first_src = first_image.get_attribute("data-wiki-asset") or first_image.get_attribute("src")
|
||||
|
||||
first_image.click(timeout=15_000)
|
||||
overlay = page.locator(".wiki-image-lightbox:not([hidden])")
|
||||
overlay.wait_for(state="visible", timeout=10_000)
|
||||
preview = overlay.locator("img")
|
||||
page.wait_for_function(
|
||||
"document.querySelector('.wiki-image-lightbox:not([hidden]) img')?.naturalWidth > 0",
|
||||
timeout=30_000,
|
||||
)
|
||||
assert overlay.locator(".wiki-image-lightbox-counter").inner_text().strip() == f"1 / {image_count}"
|
||||
assert preview.get_attribute("src") == first_src
|
||||
|
||||
if screenshot_dir is not None:
|
||||
screenshot_dir.mkdir(parents=True, exist_ok=True)
|
||||
page.screenshot(path=str(screenshot_dir / f"{mission.lower()}-lightbox.png"), timeout=120_000)
|
||||
|
||||
if image_count > 1:
|
||||
page.get_by_role("button", name="Next image").click(timeout=10_000)
|
||||
assert overlay.locator(".wiki-image-lightbox-counter").inner_text().strip() == f"2 / {image_count}"
|
||||
assert preview.get_attribute("src") != first_src
|
||||
page.keyboard.press("ArrowLeft")
|
||||
assert overlay.locator(".wiki-image-lightbox-counter").inner_text().strip() == f"1 / {image_count}"
|
||||
|
||||
page.keyboard.press("Escape")
|
||||
overlay.wait_for(state="hidden", timeout=10_000)
|
||||
|
||||
first_image.focus()
|
||||
page.keyboard.press("Enter")
|
||||
overlay.wait_for(state="visible", timeout=10_000)
|
||||
page.get_by_role("button", name="Close image preview").click(timeout=10_000)
|
||||
overlay.wait_for(state="hidden", timeout=10_000)
|
||||
|
||||
return {"mission": mission, "images": image_count, "click": True, "next": image_count > 1, "keyboard": True}
|
||||
|
||||
|
||||
def verify_planning_gallery(page, mission: str, url: str) -> dict:
|
||||
page.goto(url, wait_until="domcontentloaded", timeout=60_000)
|
||||
page.locator(".wiki-article").wait_for(state="attached", timeout=30_000)
|
||||
page.locator(".wiki-article.gallery-planning").wait_for(state="attached", timeout=30_000)
|
||||
assert page.locator(".wiki-article.gallery-planning").count() == 1
|
||||
assert page.locator("img[data-wiki-asset]").count() == 0
|
||||
assert page.locator(".wiki-image-lightbox:not([hidden])").count() == 0
|
||||
return {"mission": mission, "images": 0, "planning": True}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
results = []
|
||||
failures = []
|
||||
with sync_playwright() as playwright:
|
||||
browser = playwright.chromium.launch(headless=True)
|
||||
context = browser.new_context(viewport={"width": 1440, "height": 1000})
|
||||
for mission in REAL_GALLERIES:
|
||||
page = context.new_page()
|
||||
page_errors = []
|
||||
page.on("pageerror", lambda error, bucket=page_errors: bucket.append(str(error)))
|
||||
try:
|
||||
result = verify_real_gallery(page, mission, gallery_url(args.base_url, mission), args.screenshot_dir)
|
||||
if page_errors:
|
||||
raise AssertionError(f"page errors: {page_errors}")
|
||||
results.append(result)
|
||||
print(f"gallery_e2e_ok mission={mission} images={result['images']}", flush=True)
|
||||
except (AssertionError, PlaywrightTimeoutError, Exception) as exc:
|
||||
failures.append({"mission": mission, "error": f"{type(exc).__name__}: {exc}"})
|
||||
print(f"gallery_e2e_failed mission={mission} error={type(exc).__name__}: {exc}", flush=True)
|
||||
finally:
|
||||
page.close()
|
||||
|
||||
for mission in PLANNING_GALLERIES:
|
||||
page = context.new_page()
|
||||
try:
|
||||
results.append(verify_planning_gallery(page, mission, gallery_url(args.base_url, mission)))
|
||||
print(f"gallery_e2e_ok mission={mission} planning=true", flush=True)
|
||||
except (AssertionError, PlaywrightTimeoutError, Exception) as exc:
|
||||
failures.append({"mission": mission, "error": f"{type(exc).__name__}: {exc}"})
|
||||
print(f"gallery_e2e_failed mission={mission} error={type(exc).__name__}: {exc}", flush=True)
|
||||
finally:
|
||||
page.close()
|
||||
browser.close()
|
||||
|
||||
print(json.dumps({"results": results, "failures": failures}, ensure_ascii=False, indent=2))
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user