Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions e2e/tests/pages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ test('history query renders a seeded task and opening it marks it read', async (

test('public reading page displays transcript and opens a working text export', async ({ page }) => {
await page.goto('/view/browser-fixture-view');
const ossBar = page.locator('.oss-bar');
await expect(ossBar).toBeVisible();
await expect(ossBar.locator('a[href="https://github.com/zlxlabs/VideoTranscriptAPI"]')).toBeVisible();
await expect(ossBar.locator('a[href="https://x.com/bylixing"]')).toBeVisible();

await expect(page.locator('#calibrated-content-block')).toContainText(
'隔离浏览器夹具中的正文,验证公开阅读与导出。');
const exportMenu = page.locator('.export-links-details');
Expand Down
4 changes: 2 additions & 2 deletions src/video_transcript_api/api/routes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ async def sitemap_xml():
<a href="https://mp.weixin.qq.com/s/w8VnWJcUp5VkD5J-fYCUrg" target="_blank" rel="noopener" style="color: #667eea; text-decoration: none;">📖 开发契机和玩法分享</a>
</p>
<p class="footer">
Powered by <a href="https://github.com/zj1123581321/VideoTranscriptAPI" target="_blank" rel="noopener">VideoTranscriptAPI</a>
Powered by <a href="https://github.com/zlxlabs/VideoTranscriptAPI" target="_blank" rel="noopener">VideoTranscriptAPI</a>
· Open Source ·
<a href="https://github.com/zj1123581321/VideoTranscriptAPI" target="_blank" rel="noopener">☆ Star on GitHub</a>
<a href="https://github.com/zlxlabs/VideoTranscriptAPI" target="_blank" rel="noopener">☆ Star on GitHub</a>
</p>
</div>
</body>
Expand Down
2 changes: 1 addition & 1 deletion src/web/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ <h3>📋 最近任务</h3>
</main>

<footer class="footer">
<p>Powered by <a href="https://github.com/zj1123581321/VideoTranscriptAPI" target="_blank" rel="noopener">VideoTranscriptAPI</a> · Open Source · <a href="https://github.com/zj1123581321/VideoTranscriptAPI" target="_blank" rel="noopener">☆ Star on GitHub</a></p>
<p>Powered by <a href="https://github.com/zlxlabs/VideoTranscriptAPI" target="_blank" rel="noopener">VideoTranscriptAPI</a> · Open Source · <a href="https://github.com/zlxlabs/VideoTranscriptAPI" target="_blank" rel="noopener">☆ Star on GitHub</a></p>
</footer>
</div>

Expand Down
118 changes: 117 additions & 1 deletion src/web/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,101 @@
text-decoration: underline;
}

/* ========== 页面顶部开源信息细条 ========== */
.oss-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 6px 16px;
font-size: 0.78rem;
line-height: 1.4;
color: var(--text-secondary);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-primary);
box-sizing: border-box;
transition: background 0.3s ease, border-color 0.3s ease, color 0.3s ease;
}

.oss-bar-desc {
display: inline-flex;
align-items: center;
}

.oss-bar-desc-desktop {
display: inline;
}

.oss-bar-desc-mobile {
display: none;
}

.oss-bar-author-label {
display: inline;
}

.oss-bar-sep {
opacity: 0.6;
user-select: none;
}

.oss-bar-link {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--text-secondary);
text-decoration: none;
transition: color 0.2s ease;
}

.oss-bar-link:hover {
color: var(--text-primary);
text-decoration: underline;
}

.oss-bar-icon {
display: inline-block;
vertical-align: middle;
width: 14px;
height: 14px;
flex-shrink: 0;
}

@media (max-width: 640px) {
.oss-bar {
padding: 0 10px;
gap: 6px;
font-size: 0.75rem;
white-space: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}

.oss-bar::-webkit-scrollbar {
display: none;
}

.oss-bar-desc-desktop {
display: none;
}

.oss-bar-desc-mobile {
display: inline;
}

.oss-bar-author-label {
display: none;
}

.oss-bar-link {
min-height: 32px;
display: inline-flex;
align-items: center;
padding: 0 4px;
}
}

/* ========== 导出链接折叠栏样式 ========== */
.export-links-details {
padding: 0;
Expand Down Expand Up @@ -1040,6 +1135,27 @@
</head>
<body>
<div class="container">
<div class="oss-bar" role="banner" aria-label="开源项目信息">
<span class="oss-bar-desc">
<span class="oss-bar-desc-desktop">本页由开源项目 VideoTranscriptAPI 生成,可自行部署</span>
<span class="oss-bar-desc-mobile">开源项目</span>
</span>
<span class="oss-bar-sep" aria-hidden="true">·</span>
<a href="https://github.com/zlxlabs/VideoTranscriptAPI" class="oss-bar-link" target="_blank" rel="noopener" aria-label="GitHub 开源仓库">
<svg class="oss-bar-icon" viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
<span>GitHub ↗</span>
</a>
<span class="oss-bar-sep" aria-hidden="true">·</span>
<a href="https://x.com/bylixing" class="oss-bar-link" target="_blank" rel="noopener" aria-label="作者 X 账号">
<span class="oss-bar-author-label">作者 </span>
<svg class="oss-bar-icon" viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 24.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
<span>@bylixing</span>
</a>
</div>
<div class="header">
<button id="theme-toggle" class="theme-toggle" title="切换到浅色模式">
☀️
Expand Down Expand Up @@ -1069,7 +1185,7 @@ <h1>{{ title or "视频转录查看器" }}</h1>
{% block content %}{% endblock %}

<div class="powered-footer">
Powered by <a href="https://github.com/zj1123581321/VideoTranscriptAPI" target="_blank" rel="noopener">VideoTranscriptAPI</a> · Open Source · <a href="https://github.com/zj1123581321/VideoTranscriptAPI" target="_blank" rel="noopener">☆ Star on GitHub</a>
Powered by <a href="https://github.com/zlxlabs/VideoTranscriptAPI" target="_blank" rel="noopener">VideoTranscriptAPI</a> · Open Source · <a href="https://github.com/zlxlabs/VideoTranscriptAPI" target="_blank" rel="noopener">☆ Star on GitHub</a>
</div>
</div>

Expand Down
173 changes: 173 additions & 0 deletions tests/unit/web/test_oss_bar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""
Structural regression tests for the top open-source / author bar:
- Presence on all pages inheriting base.html (transcript.html, error.html, etc.)
- Positioned as the first child within .container before .header
- Links pointing to zlxlabs/VideoTranscriptAPI and x.com/bylixing with target="_blank", rel="noopener", aria-label
- Inline SVGs for GitHub mark and X logo with fill="currentColor" and aria-hidden="true"
- Responsive switching via <=640px media query
- No legacy repository URL (zj1123581321) in src/web and views.py

Templates are rendered directly via a plain Jinja2 environment pointed at
src/web/templates, independent of FastAPI/config bootstrap.

All console output must be in English only per project testing conventions.
"""

from pathlib import Path
import re
import jinja2
import pytest

PROJECT_ROOT = Path(__file__).resolve().parents[3]
TEMPLATES_DIR = PROJECT_ROOT / "src" / "web" / "templates"
STATIC_DIR = PROJECT_ROOT / "src" / "web" / "static"
VIEWS_FILE = PROJECT_ROOT / "src" / "video_transcript_api" / "api" / "routes" / "views.py"

GITHUB_URL = "https://github.com/zlxlabs/VideoTranscriptAPI"
X_URL = "https://x.com/bylixing"
LEGACY_REPO = "zj1123581321/VideoTranscriptAPI"


def _jinja_env() -> jinja2.Environment:
"""Build a standalone Jinja2 environment mirroring app settings."""
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(str(TEMPLATES_DIR)),
autoescape=True,
)
env.globals["asset_v"] = "test-asset-v"
return env


def _render_transcript(**overrides) -> str:
ctx = {
"title": "Sample Transcript Title",
"author": "Sample Author",
"url": "https://example.com/video/123",
"created_at_display": "2026-07-11 10:00",
"platform": "youtube",
"summary_html": "<p>Summary text.</p>",
"calibrated_html": "<p>Calibrated transcript text.</p>",
"use_speaker_recognition": False,
"view_token": "test-view-token-123",
"stats": {
"original_length": 100,
"calibrated_length": 90,
"summary_length": 50,
},
"llm_config": None,
}
ctx.update(overrides)
return _jinja_env().get_template("transcript.html").render(**ctx)


def _render_error(**overrides) -> str:
ctx = {
"title": "出现错误",
"message": "页面无法正常显示",
"url": "https://example.com/video/123",
"created_at_display": "2026-07-11 10:00",
}
ctx.update(overrides)
return _jinja_env().get_template("error.html").render(**ctx)


class TestTopOssBarPresenceAndPlacement:
"""Assert top OSS bar markup and placement on templates inheriting base.html."""

@pytest.mark.parametrize("renderer", [_render_transcript, _render_error])
def test_renders_oss_bar_with_exact_urls(self, renderer):
html = renderer()
assert 'class="oss-bar"' in html
assert GITHUB_URL in html
assert X_URL in html

@pytest.mark.parametrize("renderer", [_render_transcript, _render_error])
def test_oss_bar_placed_before_header(self, renderer):
html = renderer()
bar_idx = html.index('class="oss-bar"')
header_idx = html.index('class="header"')
assert bar_idx < header_idx, "Expected .oss-bar to appear before .header"

def test_container_encloses_oss_bar_as_first_child(self):
html = _render_transcript()
container_tag = '<div class="container">'
container_idx = html.index(container_tag)
bar_idx = html.index('class="oss-bar"')
assert container_idx < bar_idx
snippet = html[container_idx + len(container_tag):bar_idx].strip()
assert snippet == "<div", f"Expected only div tag before class=\"oss-bar\", got: {snippet!r}"


class TestTopOssBarLinkAttributesAndIcons:
"""Verify link security attributes, accessibility labels, and inline SVGs."""

def test_links_security_and_accessibility_attributes(self):
html = _render_transcript()

github_pattern = re.compile(
r'<a[^>]+href="' + re.escape(GITHUB_URL) + r'"[^>]*>', re.IGNORECASE
)
match_gh = github_pattern.search(html)
assert match_gh is not None, "GitHub link tag not found"
gh_tag = match_gh.group(0)
assert 'target="_blank"' in gh_tag
assert 'rel="noopener"' in gh_tag
assert "aria-label=" in gh_tag

x_pattern = re.compile(
r'<a[^>]+href="' + re.escape(X_URL) + r'"[^>]*>', re.IGNORECASE
)
match_x = x_pattern.search(html)
assert match_x is not None, "X link tag not found"
x_tag = match_x.group(0)
assert 'target="_blank"' in x_tag
assert 'rel="noopener"' in x_tag
assert "aria-label=" in x_tag

def test_inline_svg_icons_used_without_emojis(self):
html = _render_transcript()
bar_html = html[html.index('class="oss-bar"'):html.index('class="header"')]

svg_matches = re.findall(r'<svg[^>]+class="oss-bar-icon"[^>]*>', bar_html)
assert len(svg_matches) == 2, "Expected exactly 2 oss-bar SVG icons"
for tag in svg_matches:
assert 'fill="currentColor"' in tag
assert 'aria-hidden="true"' in tag


class TestTopOssBarResponsiveStyling:
"""Ensure media query <=640px and desktop/mobile text classes exist."""

def test_desktop_and_mobile_text_elements_present(self):
html = _render_transcript()
assert "oss-bar-desc-desktop" in html
assert "本页由开源项目 VideoTranscriptAPI 生成,可自行部署" in html
assert "oss-bar-desc-mobile" in html
assert "开源项目" in html
assert "GitHub ↗" in html
assert "@bylixing" in html

def test_media_query_and_touch_target_in_styles(self):
base_html = (TEMPLATES_DIR / "base.html").read_text(encoding="utf-8")
assert "@media (max-width: 640px)" in base_html
assert ".oss-bar-desc-desktop" in base_html
assert ".oss-bar-desc-mobile" in base_html
assert "min-height: 32px" in base_html


class TestNoLegacyRepoUrlsInWebAssets:
"""Ensure zj1123581321/VideoTranscriptAPI does not exist in web templates, static files, or views.py."""

def test_no_legacy_repo_in_templates(self):
for template_file in TEMPLATES_DIR.glob("*.html"):
content = template_file.read_text(encoding="utf-8")
assert LEGACY_REPO not in content, f"Found legacy repo URL in {template_file.name}"

def test_no_legacy_repo_in_static_pages(self):
for static_file in STATIC_DIR.glob("*.html"):
content = static_file.read_text(encoding="utf-8")
assert LEGACY_REPO not in content, f"Found legacy repo URL in {static_file.name}"

def test_no_legacy_repo_in_views(self):
content = VIEWS_FILE.read_text(encoding="utf-8")
assert LEGACY_REPO not in content, "Found legacy repo URL in views.py"
Loading