This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
# Alpha 管理固定提交受阻 Tab
|
||||||
|
Status: ready-for-agent
|
||||||
|
Type: task
|
||||||
|
|
||||||
|
筛选未提交且最近缓存 REGULAR_SUBMISSION=FAIL 的 Alpha。条件在后端分页前执行,与其他参数、导出、保存视图一致;同步和平台检查更新缓存时同步刷新索引字段。新增迁移回填历史记录,不改变质量检查分类。
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
实现中;待 API、迁移及前端检查。
|
||||||
|
|
||||||
|
已完成:固定 Tab、组合筛选、分页与导出、保存视图及恢复,迁移 0020 回填历史缓存;同步和 /check 共用 snapshot_columns 更新受阻标记。
|
||||||
|
验证:首轮 19 项后端测试通过;最终相关回归 99 项通过(含 503 行迁移回填、降级、再升级及 schema check);浏览器验证切换、叠加搜索、保存恢复和重置通过;ruff、TypeScript、生产构建及 diff 检查通过。构建保留既有 lottie eval 与大包提示。未部署、未提交。
|
||||||
@@ -35,6 +35,7 @@ def snapshot_columns(settings, metrics, checks, *, checked=False):
|
|||||||
"""
|
"""
|
||||||
settings = settings if isinstance(settings, dict) else {}
|
settings = settings if isinstance(settings, dict) else {}
|
||||||
metrics = metrics if isinstance(metrics, dict) else {}
|
metrics = metrics if isinstance(metrics, dict) else {}
|
||||||
|
blocked = submission_limits(checks)["status"] == "blocked"
|
||||||
checks, _ = split_checks(checks)
|
checks, _ = split_checks(checks)
|
||||||
valid = [check for check in checks if isinstance(check, dict)]
|
valid = [check for check in checks if isinstance(check, dict)]
|
||||||
failures = len(failed_checks(checks))
|
failures = len(failed_checks(checks))
|
||||||
@@ -52,6 +53,7 @@ def snapshot_columns(settings, metrics, checks, *, checked=False):
|
|||||||
neutralization = settings.get("neutralization")
|
neutralization = settings.get("neutralization")
|
||||||
return {
|
return {
|
||||||
"check_type": check_type,
|
"check_type": check_type,
|
||||||
|
"submission_blocked": blocked,
|
||||||
"neutralization": neutralization if isinstance(neutralization, str) else None,
|
"neutralization": neutralization if isinstance(neutralization, str) else None,
|
||||||
"pnl": number(metrics.get("pnl")),
|
"pnl": number(metrics.get("pnl")),
|
||||||
"prod_correlation": prod_correlation,
|
"prod_correlation": prod_correlation,
|
||||||
@@ -189,6 +191,10 @@ def list_statement(filters):
|
|||||||
query = select(Alpha, Research).join(Research, Research.alpha_id == Alpha.id)
|
query = select(Alpha, Research).join(Research, Research.alpha_id == Alpha.id)
|
||||||
if filters.submission:
|
if filters.submission:
|
||||||
query = query.where(submission_condition(filters.submission))
|
query = query.where(submission_condition(filters.submission))
|
||||||
|
if filters.submission_blocked is not None:
|
||||||
|
query = query.where(Alpha.submission_blocked == filters.submission_blocked)
|
||||||
|
if filters.submission_blocked:
|
||||||
|
query = query.where(submission_condition("UNSUBMITTED"))
|
||||||
source_filters = {k: getattr(filters, k) for k in ("source", "source_reference", "research_id", "backtest_run_id")}
|
source_filters = {k: getattr(filters, k) for k in ("source", "source_reference", "research_id", "backtest_run_id")}
|
||||||
if any(source_filters.values()):
|
if any(source_filters.values()):
|
||||||
query = query.where(Alpha.id.in_(source_alpha_ids(**source_filters)))
|
query = query.where(Alpha.id.in_(source_alpha_ids(**source_filters)))
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ class Alpha(Base):
|
|||||||
pnl: Mapped[float | None] = mapped_column(Float)
|
pnl: Mapped[float | None] = mapped_column(Float)
|
||||||
neutralization: Mapped[str | None] = mapped_column(Text)
|
neutralization: Mapped[str | None] = mapped_column(Text)
|
||||||
check_type: Mapped[str] = mapped_column(String(20), default="PENDING", server_default="PENDING", index=True)
|
check_type: Mapped[str] = mapped_column(String(20), default="PENDING", server_default="PENDING", index=True)
|
||||||
|
submission_blocked: Mapped[bool] = mapped_column(Boolean, default=False, server_default=false(), index=True)
|
||||||
date_created: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True)
|
date_created: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True)
|
||||||
date_submitted: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
date_submitted: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||||
synced_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now)
|
synced_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now)
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class PreferencesInput(Contract):
|
|||||||
|
|
||||||
|
|
||||||
class AlphaFilters(Contract):
|
class AlphaFilters(Contract):
|
||||||
|
submission_blocked: bool | None = None
|
||||||
submission: Submission | None = None
|
submission: Submission | None = None
|
||||||
source: str | None = Field(default=None, max_length=100)
|
source: str | None = Field(default=None, max_length=100)
|
||||||
source_reference: str | None = Field(default=None, max_length=200)
|
source_reference: str | None = Field(default=None, max_length=200)
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""Index the cached submission limit independently of Alpha quality checks."""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "0020"
|
||||||
|
down_revision = "0019"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column("alphas", sa.Column("submission_blocked", sa.Boolean(), nullable=False, server_default=sa.false()))
|
||||||
|
table = sa.table("alphas", sa.column("id", sa.String()), sa.column("checks", sa.JSON()),
|
||||||
|
sa.column("submission_blocked", sa.Boolean()))
|
||||||
|
connection = op.get_bind()
|
||||||
|
last_id = None
|
||||||
|
while True:
|
||||||
|
query = sa.select(table.c.id, table.c.checks).order_by(table.c.id).limit(500)
|
||||||
|
if last_id is not None:
|
||||||
|
query = query.where(table.c.id > last_id)
|
||||||
|
rows = connection.execute(query).mappings().all()
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
# Frozen snapshot interpretation; never depend on future application code.
|
||||||
|
blocked = [row["id"] for row in rows if isinstance(row["checks"], list) and any(
|
||||||
|
isinstance(check, dict) and check.get("name") == "REGULAR_SUBMISSION"
|
||||||
|
and isinstance(check.get("result"), str) and check["result"].upper() == "FAIL"
|
||||||
|
for check in row["checks"])]
|
||||||
|
if blocked:
|
||||||
|
connection.execute(table.update().where(table.c.id.in_(blocked)).values(submission_blocked=True))
|
||||||
|
last_id = rows[-1]["id"]
|
||||||
|
op.create_index("ix_alphas_submission_blocked", "alphas", ["submission_blocked"])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_index("ix_alphas_submission_blocked", table_name="alphas")
|
||||||
|
op.drop_column("alphas", "submission_blocked")
|
||||||
@@ -219,6 +219,7 @@ def test_migration_backfills_multiple_batches_and_preserves_research(tmp_path, m
|
|||||||
assert len(rows) == 503
|
assert len(rows) == 503
|
||||||
for i, row in enumerate(rows):
|
for i, row in enumerate(rows):
|
||||||
expected = snapshot_columns(row["settings"], row["is_metrics"], checks(i % 4), checked=True)
|
expected = snapshot_columns(row["settings"], row["is_metrics"], checks(i % 4), checked=True)
|
||||||
|
expected.pop("submission_blocked") # Added after this historical migration.
|
||||||
assert {key: row[key] for key in expected} == expected
|
assert {key: row[key] for key in expected} == expected
|
||||||
record = db.execute(sa.select(research)).mappings().one()
|
record = db.execute(sa.select(research)).mappings().one()
|
||||||
assert record["tags"] == ["PPAC"] and record["note"] == "keep" and record["version"] == 7
|
assert record["tags"] == ["PPAC"] and record["note"] == "keep" and record["version"] == 7
|
||||||
|
|||||||
@@ -60,5 +60,15 @@ def test_stage_backfill_preserves_evidence_and_uses_checkpoints(tmp_path, monkey
|
|||||||
for i, row in enumerate(rows):
|
for i, row in enumerate(rows):
|
||||||
assert row["check_type"] == patterns[i % 7][expected_index]
|
assert row["check_type"] == patterns[i % 7][expected_index]
|
||||||
assert row["checks"] == row["raw"]["is"]["checks"] == row["is_metrics"]["checks"] == patterns[i % 7][0]
|
assert row["checks"] == row["raw"]["is"]["checks"] == row["is_metrics"]["checks"] == patterns[i % 7][0]
|
||||||
|
command.upgrade(config, "head")
|
||||||
|
current = sa.Table("alphas", sa.MetaData(), autoload_with=engine)
|
||||||
|
with engine.connect() as db:
|
||||||
|
rows = db.execute(sa.select(current).order_by(current.c.id)).mappings().all()
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
assert row["submission_blocked"] is (i % 7 < 4)
|
||||||
|
assert row["checks"] == patterns[i % 7][0]
|
||||||
|
command.downgrade(config, "0019")
|
||||||
|
assert "submission_blocked" not in {c["name"] for c in sa.inspect(engine).get_columns("alphas")}
|
||||||
|
command.upgrade(config, "head")
|
||||||
command.check(config)
|
command.check(config)
|
||||||
engine.dispose()
|
engine.dispose()
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""Submission limits filter cached unsubmitted Alpha snapshots before pagination."""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
|
||||||
|
from app.alphas import snapshot_columns, upsert_alpha
|
||||||
|
from tests.conftest import alpha
|
||||||
|
|
||||||
|
|
||||||
|
async def test_blocked_filter_intersects_filters_and_export_and_refresh(app, logged_in):
|
||||||
|
async with app.state.sessions.begin() as db:
|
||||||
|
for name, result, status, region in [
|
||||||
|
("blocked1", "FAIL", "UNSUBMITTED", "USA"),
|
||||||
|
("blocked2", "fail", "UNSUBMITTED", "USA"),
|
||||||
|
("other_region", "FAIL", "UNSUBMITTED", "CHN"),
|
||||||
|
("submitted", "FAIL", "ACTIVE", "USA"),
|
||||||
|
("passed", "PASS", "UNSUBMITTED", "USA"),
|
||||||
|
("pending", "PENDING", "UNSUBMITTED", "USA"),
|
||||||
|
("missing", None, "UNSUBMITTED", "USA"),
|
||||||
|
]:
|
||||||
|
checks = [{"name": "LOW_SHARPE", "result": "PASS"}]
|
||||||
|
if result:
|
||||||
|
checks.append({"name": "REGULAR_SUBMISSION", "result": result})
|
||||||
|
await upsert_alpha(db, alpha(name, status=status, settings={"region": region}, **{"is": {"checks": checks}}))
|
||||||
|
query = "submission_blocked=true®ion=USA&check_type=PRE_CHECK&sort=id&direction=asc&limit=1&offset=1"
|
||||||
|
response = await logged_in.get(f"/api/v1/alphas?{query}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["total"] == 2
|
||||||
|
assert [row["id"] for row in response.json()["items"]] == ["blocked2"]
|
||||||
|
export = await logged_in.get(f"/api/v1/alphas/export?{query}")
|
||||||
|
assert [row["id"] for row in csv.DictReader(io.StringIO(export.text.lstrip("\ufeff")))] == ["blocked1", "blocked2"]
|
||||||
|
assert (await logged_in.get("/api/v1/alphas?submission_blocked=true&submission=SUBMITTED")).json()["total"] == 0
|
||||||
|
assert (await logged_in.get("/api/v1/alphas?submission_blocked=true&q=blocked1")).json()["total"] == 1
|
||||||
|
async with app.state.sessions.begin() as db:
|
||||||
|
await upsert_alpha(db, alpha("blocked1", **{"is": {"checks": [{"name": "REGULAR_SUBMISSION", "result": "PASS"}]}}))
|
||||||
|
assert (await logged_in.get("/api/v1/alphas?submission_blocked=true&q=blocked1")).json()["total"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_check_refreshes_limit_independently_of_quality():
|
||||||
|
for checked in (False, True):
|
||||||
|
for result, blocked in [("FAIL", True), ("PASS", False), ("PENDING", False)]:
|
||||||
|
columns = snapshot_columns({}, {}, [{"name": "LOW_SHARPE", "result": "PASS"},
|
||||||
|
{"name": "REGULAR_SUBMISSION", "result": result}], checked=checked)
|
||||||
|
assert columns["submission_blocked"] is blocked
|
||||||
|
assert columns["check_type"] == ("PASS" if checked else "PRE_CHECK")
|
||||||
@@ -184,6 +184,7 @@ export function AlphaPage({
|
|||||||
const [filters, setFilters] = useState<Record<string, string>>({});
|
const [filters, setFilters] = useState<Record<string, string>>({});
|
||||||
const [filterOpen, setFilterOpen] = useState(false);
|
const [filterOpen, setFilterOpen] = useState(false);
|
||||||
const [selectedViewId, setSelectedViewId] = useState<string | null>(null);
|
const [selectedViewId, setSelectedViewId] = useState<string | null>(null);
|
||||||
|
const [submissionBlocked, setSubmissionBlocked] = useState(false);
|
||||||
const [submission, setSubmission] = useState<Submission>("UNSUBMITTED");
|
const [submission, setSubmission] = useState<Submission>("UNSUBMITTED");
|
||||||
const [syncingScope, setSyncingScope] = useState<Submission | null>(null);
|
const [syncingScope, setSyncingScope] = useState<Submission | null>(null);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
@@ -250,12 +251,13 @@ export function AlphaPage({
|
|||||||
() => ({
|
() => ({
|
||||||
...filters,
|
...filters,
|
||||||
submission,
|
submission,
|
||||||
|
submission_blocked: submissionBlocked || undefined,
|
||||||
sort,
|
sort,
|
||||||
direction,
|
direction,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
offset: (page - 1) * pageSize,
|
offset: (page - 1) * pageSize,
|
||||||
}),
|
}),
|
||||||
[filters, submission, sort, direction, pageSize, page],
|
[filters, submission, submissionBlocked, sort, direction, pageSize, page],
|
||||||
);
|
);
|
||||||
const query = queryString(params);
|
const query = queryString(params);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -286,6 +288,7 @@ export function AlphaPage({
|
|||||||
limit: _limit,
|
limit: _limit,
|
||||||
offset: _offset,
|
offset: _offset,
|
||||||
submission: nextSubmission,
|
submission: nextSubmission,
|
||||||
|
submission_blocked: nextBlocked,
|
||||||
...values
|
...values
|
||||||
} = action.filters;
|
} = action.filters;
|
||||||
const next = Object.fromEntries(
|
const next = Object.fromEntries(
|
||||||
@@ -299,6 +302,7 @@ export function AlphaPage({
|
|||||||
setSelected([]);
|
setSelected([]);
|
||||||
if (nextSubmission === "SUBMITTED" || nextSubmission === "UNSUBMITTED")
|
if (nextSubmission === "SUBMITTED" || nextSubmission === "UNSUBMITTED")
|
||||||
setSubmission(nextSubmission);
|
setSubmission(nextSubmission);
|
||||||
|
setSubmissionBlocked(nextBlocked === true || nextBlocked === "true");
|
||||||
if (nextSort) setSort(String(nextSort));
|
if (nextSort) setSort(String(nextSort));
|
||||||
if (nextDirection) setDirection(String(nextDirection));
|
if (nextDirection) setDirection(String(nextDirection));
|
||||||
}
|
}
|
||||||
@@ -352,7 +356,8 @@ export function AlphaPage({
|
|||||||
setSelectedViewId(null);
|
setSelectedViewId(null);
|
||||||
setFilterOpen(false);
|
setFilterOpen(false);
|
||||||
setDirection("desc");
|
setDirection("desc");
|
||||||
setSubmission(value as Submission);
|
setSubmission(value === "SUBMITTED" ? "SUBMITTED" : "UNSUBMITTED");
|
||||||
|
setSubmissionBlocked(value === "SUBMISSION_BLOCKED");
|
||||||
setDraft({});
|
setDraft({});
|
||||||
setFilters({});
|
setFilters({});
|
||||||
setPage(1);
|
setPage(1);
|
||||||
@@ -925,17 +930,26 @@ export function AlphaPage({
|
|||||||
<section className="library-panel" ref={tablePanel}>
|
<section className="library-panel" ref={tablePanel}>
|
||||||
<SavedViews
|
<SavedViews
|
||||||
submission={submission}
|
submission={submission}
|
||||||
|
submissionBlocked={submissionBlocked}
|
||||||
selectedId={selectedViewId}
|
selectedId={selectedViewId}
|
||||||
onSelect={setSelectedViewId}
|
onSelect={setSelectedViewId}
|
||||||
onSubmissionChange={changeSubmission}
|
onSubmissionChange={changeSubmission}
|
||||||
suspended={overlaySuspended || !active}
|
suspended={overlaySuspended || !active}
|
||||||
onOverlay={onOverlay}
|
onOverlay={onOverlay}
|
||||||
filters={{ ...filters, submission, sort, direction, limit: pageSize }}
|
filters={{
|
||||||
|
...filters,
|
||||||
|
submission,
|
||||||
|
submission_blocked: submissionBlocked || undefined,
|
||||||
|
sort,
|
||||||
|
direction,
|
||||||
|
limit: pageSize,
|
||||||
|
}}
|
||||||
columns={displayedColumns}
|
columns={displayedColumns}
|
||||||
onRestore={(view) => {
|
onRestore={(view) => {
|
||||||
setFilterOpen(false);
|
setFilterOpen(false);
|
||||||
const {
|
const {
|
||||||
submission: savedSubmission,
|
submission: savedSubmission,
|
||||||
|
submission_blocked: savedBlocked,
|
||||||
sort: savedSort,
|
sort: savedSort,
|
||||||
direction: savedDirection,
|
direction: savedDirection,
|
||||||
limit,
|
limit,
|
||||||
@@ -954,6 +968,9 @@ export function AlphaPage({
|
|||||||
setSubmission(
|
setSubmission(
|
||||||
savedSubmission === "SUBMITTED" ? "SUBMITTED" : "UNSUBMITTED",
|
savedSubmission === "SUBMITTED" ? "SUBMITTED" : "UNSUBMITTED",
|
||||||
);
|
);
|
||||||
|
setSubmissionBlocked(
|
||||||
|
savedBlocked === true || savedBlocked === "true",
|
||||||
|
);
|
||||||
setSort(String(savedSort || "date_created"));
|
setSort(String(savedSort || "date_created"));
|
||||||
setDirection(String(savedDirection || "desc"));
|
setDirection(String(savedDirection || "desc"));
|
||||||
if (typeof limit === "number") setPageSize(limit);
|
if (typeof limit === "number") setPageSize(limit);
|
||||||
@@ -1228,7 +1245,12 @@ export function AlphaPage({
|
|||||||
<footer className="table-pagination" aria-label="Alpha 分页">
|
<footer className="table-pagination" aria-label="Alpha 分页">
|
||||||
<div className="alpha-table-meta">
|
<div className="alpha-table-meta">
|
||||||
<span className="muted">
|
<span className="muted">
|
||||||
{submission === "UNSUBMITTED" ? "待提交" : "已提交"} Alpha{" "}
|
{submissionBlocked
|
||||||
|
? "提交受阻"
|
||||||
|
: submission === "UNSUBMITTED"
|
||||||
|
? "待提交"
|
||||||
|
: "已提交"}{" "}
|
||||||
|
Alpha{" "}
|
||||||
<span className="count-pill">{data.total.toLocaleString()}</span>{" "}
|
<span className="count-pill">{data.total.toLocaleString()}</span>{" "}
|
||||||
条
|
条
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export function SavedViews({
|
|||||||
filters,
|
filters,
|
||||||
columns,
|
columns,
|
||||||
submission,
|
submission,
|
||||||
|
submissionBlocked,
|
||||||
selectedId,
|
selectedId,
|
||||||
onSelect,
|
onSelect,
|
||||||
onSubmissionChange,
|
onSubmissionChange,
|
||||||
@@ -57,6 +58,7 @@ export function SavedViews({
|
|||||||
filters: Record<string, unknown>;
|
filters: Record<string, unknown>;
|
||||||
columns: string[];
|
columns: string[];
|
||||||
submission: Submission;
|
submission: Submission;
|
||||||
|
submissionBlocked: boolean;
|
||||||
selectedId: string | null;
|
selectedId: string | null;
|
||||||
onSelect: (id: string | null) => void;
|
onSelect: (id: string | null) => void;
|
||||||
onSubmissionChange: (submission: string) => void;
|
onSubmissionChange: (submission: string) => void;
|
||||||
@@ -152,7 +154,10 @@ export function SavedViews({
|
|||||||
setItems((previous) =>
|
setItems((previous) =>
|
||||||
previous.filter((item) => item.id !== deleting.id),
|
previous.filter((item) => item.id !== deleting.id),
|
||||||
);
|
);
|
||||||
if (selectedId === deleting.id) onSubmissionChange(submission);
|
if (selectedId === deleting.id)
|
||||||
|
onSubmissionChange(
|
||||||
|
submissionBlocked ? "SUBMISSION_BLOCKED" : submission,
|
||||||
|
);
|
||||||
setDeleting(null);
|
setDeleting(null);
|
||||||
Toast.success("视图已删除");
|
Toast.success("视图已删除");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -168,7 +173,11 @@ export function SavedViews({
|
|||||||
className="alpha-view-tabs"
|
className="alpha-view-tabs"
|
||||||
type="card"
|
type="card"
|
||||||
collapsible="auto"
|
collapsible="auto"
|
||||||
activeKey={selectedId ?? submission}
|
tabPaneMotion={false}
|
||||||
|
activeKey={
|
||||||
|
selectedId ??
|
||||||
|
(submissionBlocked ? "SUBMISSION_BLOCKED" : submission)
|
||||||
|
}
|
||||||
onChange={(key) => {
|
onChange={(key) => {
|
||||||
const view = items.find((item) => item.id === key);
|
const view = items.find((item) => item.id === key);
|
||||||
if (view) {
|
if (view) {
|
||||||
@@ -187,6 +196,11 @@ export function SavedViews({
|
|||||||
tab: "已提交",
|
tab: "已提交",
|
||||||
icon: <IconList aria-hidden="true" />,
|
icon: <IconList aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
itemKey: "SUBMISSION_BLOCKED",
|
||||||
|
tab: "提交受阻",
|
||||||
|
icon: <IconList aria-hidden="true" />,
|
||||||
|
},
|
||||||
...items.map((view) => ({
|
...items.map((view) => ({
|
||||||
itemKey: view.id,
|
itemKey: view.id,
|
||||||
icon: <IconList aria-hidden="true" />,
|
icon: <IconList aria-hidden="true" />,
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("fixed blocked tab retains its scope through filtering, reset and saved views", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
let params = new URLSearchParams();
|
||||||
|
await page.route(/\/api\/v1\/alphas\?/, async (route) => {
|
||||||
|
params = new URL(route.request().url()).searchParams;
|
||||||
|
await route.fulfill({
|
||||||
|
json: { items: [], total: 0, limit: 25, offset: 0 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await page.goto("/#alphas");
|
||||||
|
await page.getByLabel("密码", { exact: true }).fill("browser-test-password");
|
||||||
|
await page.getByRole("button", { name: "进入工作空间" }).click();
|
||||||
|
const blocked = page.getByRole("tab", { name: "提交受阻", exact: true });
|
||||||
|
await blocked.click();
|
||||||
|
await expect.poll(() => params.get("submission_blocked")).toBe("true");
|
||||||
|
expect(params.get("submission")).toBe("UNSUBMITTED");
|
||||||
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
||||||
|
await page
|
||||||
|
.getByRole("textbox", { name: "搜索 Alpha", exact: true })
|
||||||
|
.fill("candidate");
|
||||||
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
||||||
|
await expect.poll(() => params.get("q")).toBe("candidate");
|
||||||
|
expect(params.get("submission_blocked")).toBe("true");
|
||||||
|
await page.getByRole("button", { name: "保存为新视图", exact: true }).click();
|
||||||
|
await page
|
||||||
|
.getByRole("textbox", { name: "视图名称", exact: true })
|
||||||
|
.fill("受阻候选");
|
||||||
|
await page.getByRole("button", { name: "保存视图", exact: true }).click();
|
||||||
|
const saved = page.getByRole("tab", { name: "受阻候选" });
|
||||||
|
await expect(saved).toBeVisible();
|
||||||
|
await page.getByRole("tab", { name: "已提交", exact: true }).click();
|
||||||
|
await expect.poll(() => params.get("submission")).toBe("SUBMITTED");
|
||||||
|
expect(params.has("submission_blocked")).toBe(false);
|
||||||
|
await saved.click();
|
||||||
|
await expect.poll(() => params.get("submission_blocked")).toBe("true");
|
||||||
|
expect(params.get("q")).toBe("candidate");
|
||||||
|
await blocked.click();
|
||||||
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
||||||
|
await page.getByRole("button", { name: "重置", exact: true }).click();
|
||||||
|
await expect.poll(() => params.has("q")).toBe(false);
|
||||||
|
expect(params.get("submission_blocked")).toBe("true");
|
||||||
|
await expect(blocked).toHaveAttribute("aria-selected", "true");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user