diff --git a/agent_test/create_7_test_projects.py b/agent_test/create_7_test_projects.py new file mode 100644 index 0000000..7491978 --- /dev/null +++ b/agent_test/create_7_test_projects.py @@ -0,0 +1,70 @@ +"""Create 7 test projects on the production server to demonstrate carousel pagination.""" +import requests +import json + +API_BASE = "http://www.1415243231.top:30081/api" + +# Existing projects on server (don't touch) +existing = [ + {"id": 1, "name": "MiniMax 套餐用量"}, + {"id": 2, "name": "GitHub每日热点-1"}, + {"id": 3, "name": "AI每日热点-1"}, +] + +# 7 new test projects with varied metadata +new_projects = [ + { + "name": "产品设计周报", + "description": "每周产品迭代总结,含 UI 改版、用户反馈分析", + }, + { + "name": "技术分享月刊", + "description": "团队技术分享、代码评审要点、最佳实践", + }, + { + "name": "运营数据日报", + "description": "DAU/MAU、转化漏斗、付费用户行为", + }, + { + "name": "AI 模型评测", + "description": "GPT-4 / Claude / Gemini / 文心一言横向对比", + }, + { + "name": "客户调研记录", + "description": "用户访谈纪要、需求池、痛点分析", + }, + { + "name": "团队 OKR 复盘", + "description": "Q2 目标完成情况、Q3 规划、风险清单", + }, + { + "name": "行业研究报告", + "description": "SaaS / AI Agent / 垂直行业趋势分析", + }, +] + +print(f"现有项目 {len(existing)} 个: {[p['name'] for p in existing]}") +print(f"\n准备创建 {len(new_projects)} 个测试项目...") + +created = [] +for p in new_projects: + try: + r = requests.post(f"{API_BASE}/projects", json=p, timeout=15) + if r.status_code == 201: + data = r.json() + created.append(data) + print(f" [OK] id={data['id']} {data['name']}") + else: + print(f" [FAIL {r.status_code}] {p['name']}: {r.text[:200]}") + except Exception as e: + print(f" [ERROR] {p['name']}: {e}") + +print(f"\n成功创建 {len(created)} 个") + +# Verify final state +r = requests.get(f"{API_BASE}/projects", timeout=10) +all_projects = r.json() +print(f"\n服务器现有项目总数: {len(all_projects)}") +print("项目列表:") +for p in all_projects: + print(f" - id={p['id']:2d} {p['name']:20s} 报告数={p.get('reportCount', 0):3d}") diff --git a/agent_test/manual_package.py b/agent_test/manual_package.py new file mode 100644 index 0000000..9b1cee3 --- /dev/null +++ b/agent_test/manual_package.py @@ -0,0 +1,48 @@ +"""Manually package the deploy zip (workaround for PowerShell encoding issues).""" +import shutil +import os +import zipfile +from datetime import datetime + +PROJECT_ROOT = r"D:\Idea Project\publish" +TMP = os.path.join(os.environ["TEMP"], f"publish_deploy_{datetime.now().strftime('%Y%m%d_%H%M%S')}") +FRONTEND_DIR = os.path.join(TMP, "frontend") +JAR_SRC = os.path.join(PROJECT_ROOT, "target", "daily-report-distribution-1.0.0.jar") +ZIP_OUT = os.path.join(PROJECT_ROOT, "deploy", "publish_deploy.zip") + +# Verify inputs +assert os.path.exists(JAR_SRC), f"Missing jar: {JAR_SRC}" +assert os.path.exists(os.path.join(PROJECT_ROOT, "dist", "index.html")), "Missing dist/index.html" +assert os.path.exists(os.path.join(PROJECT_ROOT, "server.js")), "Missing server.js" + +# Clean & create +shutil.rmtree(TMP, ignore_errors=True) +os.makedirs(FRONTEND_DIR) + +# Copy frontend build +shutil.copy2(os.path.join(PROJECT_ROOT, "dist", "index.html"), FRONTEND_DIR) +shutil.copytree(os.path.join(PROJECT_ROOT, "dist", "assets"), + os.path.join(FRONTEND_DIR, "assets")) +shutil.copy2(os.path.join(PROJECT_ROOT, "server.js"), FRONTEND_DIR) +print(" [OK] Frontend files staged") + +# Copy backend jar +shutil.copy2(JAR_SRC, os.path.join(TMP, "app.jar")) +print(" [OK] Backend jar staged") + +# Zip +if os.path.exists(ZIP_OUT): + os.remove(ZIP_OUT) +with zipfile.ZipFile(ZIP_OUT, "w", zipfile.ZIP_DEFLATED) as zf: + for root, dirs, files in os.walk(TMP): + for f in files: + full = os.path.join(root, f) + arc = os.path.relpath(full, TMP) + zf.write(full, arc) + +size_mb = os.path.getsize(ZIP_OUT) / 1024 / 1024 +print(f" [OK] Packaged: {size_mb:.1f} MB") + +# Cleanup +shutil.rmtree(TMP, ignore_errors=True) +print(f"\nDone. Output: {ZIP_OUT}") diff --git a/agent_test/test_link_click.html b/agent_test/test_link_click.html new file mode 100644 index 0000000..8437aed --- /dev/null +++ b/agent_test/test_link_click.html @@ -0,0 +1,63 @@ + + + + + 链接跳转测试报告 + + + +

HTML 链接跳转测试

+ +

+ 测试目标:验证在 iframe 中点击链接,不需要 Ctrl+点击,链接应该正常打开。 +

+ +

1. 外部链接(应该打开新标签)

+
+

Google —— 普通外部链接

+

GitHub —— 另一个外部链接

+

Bing —— 强制 target=_self(应该走 base 默认行为)

+
+ +

2. 不同 target 行为对比

+
+

→ target="_blank"(新标签)

+

→ target="_self"(iframe 内)

+

→ 无 target(继承 base,默认新标签)

+
+ +

3. 锚点链接(页面内跳转)

+
+

跳到页面底部

+

跳到中间部分

+
+ +

中间部分(锚点目标 1)

+

这里是页面的中间部分。Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

+

Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

+ +

4. 相对路径链接

+
+

→ 根路径

+

→ 当前目录

+

→ 父目录

+
+ +

5. 邮件链接

+
+

发送邮件给 test@example.com

+
+ +

页面底部(锚点目标 2)

+

恭喜你滚动到了底部。所有链接测试完成!

+

↑ 回到顶部

+ + + + + + 链接跳转测试报告 + + + +

HTML 链接跳转测试

+ +

+ 测试目标:验证在 iframe 中点击链接,不需要 Ctrl+点击,链接应该正常打开。 +

+ +

1. 外部链接(应该打开新标签)

+
+

Google —— 普通外部链接

+

GitHub —— 另一个外部链接

+

Bing —— 强制 target=_self(应该走 base 默认行为)

+
+ +

2. 不同 target 行为对比

+
+

→ target="_blank"(新标签)

+

→ target="_self"(iframe 内)

+

→ 无 target(继承 base,默认新标签)

+
+ +

3. 锚点链接(页面内跳转)

+
+

跳到页面底部

+

跳到中间部分

+
+ +

中间部分(锚点目标 1)

+

这里是页面的中间部分。Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

+

Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

+ +

4. 相对路径链接

+
+

→ 根路径

+

→ 当前目录

+

→ 父目录

+
+ +

5. 邮件链接

+
+

发送邮件给 test@example.com

+
+ +

页面底部(锚点目标 2)

+

恭喜你滚动到了底部。所有链接测试完成!

+

↑ 回到顶部

+ + @@ -135,19 +135,19 @@ const renderedMarkdown = computed(() => { return marked(props.content) }) -// Inject so links open in the parent window, not the iframe +// Inject so links open in a new tab instead of being trapped in the iframe. +// Combined with sandbox="allow-popups", this lets users click links normally (no Ctrl+Click needed). const htmlContent = computed(() => { if (!props.content) return '' - const base = '' - // If content already has , inject base right after tag + const base = '' if (/]*>/i.test(props.content)) { return props.content.replace(/(]*>)/i, `$1\n${base}`) } - // Otherwise prepend before the body or first element if (/]*>/i.test(props.content)) { return props.content.replace(/(]*>)/i, `${base}\n$1`) } - return base + props.content + // Fragment (no /): wrap in a full document so the base tag is valid + return `${base}${props.content}` }) const formatUploadTime = (isoString) => { diff --git a/src/test/vue/components/FilePreview.test.js b/src/test/vue/components/FilePreview.test.js index 5462c02..5d51290 100644 --- a/src/test/vue/components/FilePreview.test.js +++ b/src/test/vue/components/FilePreview.test.js @@ -29,8 +29,9 @@ describe('FilePreview.vue', () => { const iframe = wrapper.find('iframe') expect(iframe.exists()).toBe(true) - // srcdoc now has injected for click-out-of-iframe - expect(iframe.attributes('srcdoc')).toContain('target="_top"') + // srcdoc now has injected so links open in new tab + // (combined with sandbox allow-popups, this enables normal click without Ctrl+Click) + expect(iframe.attributes('srcdoc')).toContain('target="_blank"') expect(iframe.attributes('srcdoc')).toContain('Test') }) @@ -48,7 +49,9 @@ describe('FilePreview.vue', () => { }) const iframe = wrapper.find('iframe') - expect(iframe.attributes('sandbox')).toBe('allow-same-origin') + // sandbox must allow popups so target="_blank" links can actually open a new tab + expect(iframe.attributes('sandbox')).toContain('allow-same-origin') + expect(iframe.attributes('sandbox')).toContain('allow-popups') }) })