mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
- 菜单设置/扩展页面/页面插件三个设置 tab 合并为「页面管理」(?tab=pages, 旧链接与 /analysis 重定向兼容) - 源码扩展页(frontend/src/custom/<ns>/)支持界面内物理删除: 先自动备份 zip 到 data/backups/pages/(可下载)再删目录, 命名校验+防穿越, 产物部署无源码树 409 - 扩展页面新增 URL 嵌入模板: 仅填菜单名+网址生成 iframe 内嵌页, 复用看板外链安全约定(仅 http(s)/拒同源/sandbox 白名单/新窗口兜底) - 页面插件新增源码导入: 上传 .tsx 或从本项目 custom/ 选择, 后端用自带 esbuild 编译+契约适配(component→mount, 独立 react 实例自挂 Provider/Router, 图标映射白名单), 产物即标准页面插件; 无工具链部署 409
110 lines
4.0 KiB
Python
110 lines
4.0 KiB
Python
"""源码导入(.tsx → 页面插件): esbuild 编译、契约 shim、端点校验与 409 降级。
|
|
|
|
CI 无 frontend/node_modules → find_esbuild 为 None → 整组跳过 (源码部署本机全跑)。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.api import ext_pages
|
|
from app.services import ext_source_import
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
ext_source_import.find_esbuild() is None,
|
|
reason="当前环境无 esbuild (需要 frontend/node_modules), 源码导入仅源码/桌面部署可测",
|
|
)
|
|
|
|
# 故意经由 @/ 别名引用宿主模块, 验证 alias 注入; 独立组件避免依赖 JSX 运行时差异
|
|
_FIXTURE_TSX = """\
|
|
import { createElement } from 'react'
|
|
import { cn } from '@/lib/cn'
|
|
|
|
export function Demo() {
|
|
return createElement('div', { className: cn('p-2', 'text-sm') }, 'demo')
|
|
}
|
|
|
|
export default {
|
|
id: 'zz_fixture_demo',
|
|
apiVersion: 1 as const,
|
|
routes: [{ id: 'demo', path: '/zz_fixture_demo', component: Demo }],
|
|
navigation: [{ id: 'demo_nav', routeId: 'demo', label: '演示导入' }],
|
|
}
|
|
"""
|
|
|
|
|
|
def _client(tmp_path: Path) -> TestClient:
|
|
app = FastAPI()
|
|
app.include_router(ext_pages.router)
|
|
app.state.repo = SimpleNamespace(store=SimpleNamespace(data_dir=tmp_path / "data"))
|
|
return TestClient(app)
|
|
|
|
|
|
def test_compile_fixture_bundles_host_alias_and_shim(tmp_path):
|
|
src = tmp_path / "fixture.tsx"
|
|
src.write_text(_FIXTURE_TSX, encoding="utf-8")
|
|
data, summary = ext_source_import.compile_source(src, "fixture")
|
|
assert len(data) > 1000
|
|
# shim 的命名空间补点逻辑与源扩展 id 都应在产物里
|
|
assert b"imported." in data
|
|
assert b"zz_fixture_demo" in data
|
|
# @/lib/cn 已被 alias 解析打进包 (而不是残留裸模块说明符导致浏览器无法加载)
|
|
assert b"from '@/lib/cn'" not in data
|
|
assert "fixture.tsx" in summary
|
|
|
|
|
|
def test_upload_endpoint_compiles_and_stores(tmp_path):
|
|
c = _client(tmp_path)
|
|
r = c.post(
|
|
"/api/ext-pages/import-source",
|
|
files={"file": ("demo_page.tsx", _FIXTURE_TSX.encode(), "text/plain")},
|
|
data={"id": "zz_imported_demo"},
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
meta = r.json()
|
|
assert meta["id"] == "zz_imported_demo"
|
|
assert (tmp_path / "data" / "ext_pages" / "zz_imported_demo" / "bundle.js").is_file()
|
|
|
|
|
|
def test_upload_rejects_non_tsx(tmp_path):
|
|
c = _client(tmp_path)
|
|
r = c.post(
|
|
"/api/ext-pages/import-source",
|
|
files={"file": ("evil.js", b"export default {}", "text/plain")},
|
|
)
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_upload_without_esbuild_returns_409(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(ext_source_import, "find_esbuild", lambda: None)
|
|
c = _client(tmp_path)
|
|
r = c.post(
|
|
"/api/ext-pages/import-source",
|
|
files={"file": ("demo.tsx", _FIXTURE_TSX.encode(), "text/plain")},
|
|
)
|
|
assert r.status_code == 409
|
|
assert "esbuild" in r.json()["detail"]
|
|
|
|
|
|
def test_from_project_validates_namespace(tmp_path):
|
|
c = _client(tmp_path)
|
|
assert c.post("/api/ext-pages/import-source/from-project", json={"namespace": "_template"}).status_code == 400
|
|
assert c.post("/api/ext-pages/import-source/from-project", json={"namespace": "../x"}).status_code == 400
|
|
assert c.post("/api/ext-pages/import-source/from-project", json={"namespace": "not_exist_ns"}).status_code == 400
|
|
|
|
|
|
def test_from_project_compiles_real_namespace(tmp_path):
|
|
"""真实编译一个小体量的本项目扩展, 验证完整链路 (真实前端源码树)。"""
|
|
c = _client(tmp_path)
|
|
r = c.post("/api/ext-pages/import-source/from-project", json={"namespace": "concept-analysis"})
|
|
assert r.status_code == 200, r.text
|
|
meta = r.json()
|
|
assert meta["id"] == "concept_analysis"
|
|
bundle = (tmp_path / "data" / "ext_pages" / "concept_analysis" / "bundle.js").read_bytes()
|
|
assert b"concept-analysis" in bundle
|
|
assert len(bundle) < 5 * 1024 * 1024
|