Skip to content

Commit 597b571

Browse files
authored
fix(e2e): prevent host-only tenant menu detection (#21)
## Summary - tighten multi-tenant menu detection so host-only `platform` menu groups do not enable tenant-aware frontend behavior - clear user drawer and batch-edit modal loading states in `finally` during initialization - record the FB-22 verification and review notes under `release-test-and-build` ## Verification - `pnpm -C apps/lina-vben/apps/web-antd run typecheck` - `pnpm -C hack/tests test:validate` - `openspec validate release-test-and-build --strict` - clean host-only Docker PostgreSQL run: `TC0226-user-account-labels.ts` and `TC0228-user-batch-edit.ts` both passed ## Notes - media-related tests were not run per request - local `apps/lina-plugins` submodule pointer change is intentionally not included in this PR
1 parent 4827ee4 commit 597b571

4 files changed

Lines changed: 139 additions & 119 deletions

File tree

apps/lina-vben/apps/web-antd/src/store/auth.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,30 @@ type UserMenuNode = {
2424
path?: string;
2525
};
2626

27+
function normalizeMenuPath(path: string) {
28+
return path.replace(/^\/+/u, '').replace(/\/+$/u, '');
29+
}
30+
31+
function isMultiTenantMenuNode(item: UserMenuNode): boolean {
32+
const path = normalizeMenuPath(item.path || '');
33+
const name = item.name || '';
34+
35+
// The host platform group is always present; only concrete tenant pages
36+
// should enable tenant-aware frontend behavior.
37+
return (
38+
path === 'platform/tenants' ||
39+
path.startsWith('platform/tenants/') ||
40+
path === 'tenant' ||
41+
path.startsWith('tenant/') ||
42+
name.startsWith('PlatformTenant') ||
43+
name.startsWith('Tenant')
44+
);
45+
}
46+
2747
function hasMultiTenantMenu(items: UserMenuNode[] = []): boolean {
2848
return items.some((item) => {
29-
const path = item.path || '';
30-
const name = item.name || '';
3149
return (
32-
path.startsWith('/platform') ||
33-
path.startsWith('/tenant') ||
34-
name.startsWith('Platform') ||
35-
name.startsWith('Tenant') ||
50+
isMultiTenantMenuNode(item) ||
3651
hasMultiTenantMenu(item.children)
3752
);
3853
});
@@ -202,13 +217,11 @@ export const useAuthStore = defineStore('auth', () => {
202217
accessStore.setAccessCodes(userInfo.permissions);
203218
}
204219
tenantStore.setTenantContext({
205-
enabled:
206-
tenantStore.enabled ||
207-
resolveTenantEnabled(
208-
tenantStore.tenants,
209-
userInfo,
210-
tenantStore.currentTenant,
211-
),
220+
enabled: resolveTenantEnabled(
221+
tenantStore.tenants,
222+
userInfo,
223+
tenantStore.currentTenant,
224+
),
212225
});
213226

214227
return userInfo;

apps/lina-vben/apps/web-antd/src/views/system/user/user-batch-edit-modal.vue

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -180,34 +180,37 @@ const [Modal, modalApi] = useVbenModal({
180180
}
181181
182182
modalApi.setState({ loading: true });
183-
const data = modalApi.getData<{
184-
rows?: any[];
185-
tenantEnabled?: boolean;
186-
}>();
187-
selectedRows.value = data?.rows ?? [];
188-
tenantEnabled.value = data?.tenantEnabled ?? false;
183+
try {
184+
const data = modalApi.getData<{
185+
rows?: any[];
186+
tenantEnabled?: boolean;
187+
}>();
188+
selectedRows.value = data?.rows ?? [];
189+
tenantEnabled.value = data?.tenantEnabled ?? false;
189190
190-
formApi.setState({ schema: buildSchema() });
191-
await formApi.resetForm();
192-
await formApi.setValues({
193-
roleIds: [],
194-
status: 1,
195-
tenantIds: tenantStore.isPlatform
196-
? []
197-
: tenantStore.currentTenant
198-
? [tenantStore.currentTenant.id]
199-
: [],
200-
updateRoles: false,
201-
updateStatus: false,
202-
updateTenant: false,
203-
});
191+
formApi.setState({ schema: buildSchema() });
192+
await formApi.resetForm();
193+
await formApi.setValues({
194+
roleIds: [],
195+
status: 1,
196+
tenantIds: tenantStore.isPlatform
197+
? []
198+
: tenantStore.currentTenant
199+
? [tenantStore.currentTenant.id]
200+
: [],
201+
updateRoles: false,
202+
updateStatus: false,
203+
updateTenant: false,
204+
});
204205
205-
await Promise.all([
206-
setupStatusOptions(),
207-
setupRoleOptions(),
208-
setupTenantOptions(),
209-
]);
210-
modalApi.setState({ loading: false });
206+
await Promise.all([
207+
setupStatusOptions(),
208+
setupRoleOptions(),
209+
setupTenantOptions(),
210+
]);
211+
} finally {
212+
modalApi.setState({ loading: false });
213+
}
211214
},
212215
async onConfirm() {
213216
const values = await formApi.getValues();

apps/lina-vben/apps/web-antd/src/views/system/user/user-drawer.vue

Lines changed: 81 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -182,94 +182,96 @@ const [Drawer, drawerApi] = useVbenDrawer({
182182
183183
drawerApi.setState({ loading: true });
184184
185-
const data = drawerApi.getData<{
186-
isEdit: boolean;
187-
orgEnabled?: boolean;
188-
tenantEnabled?: boolean;
189-
row?: any;
190-
}>();
191-
isEdit.value = data?.isEdit ?? false;
192-
orgEnabled.value = data?.orgEnabled ?? false;
193-
tenantEnabled.value = data?.tenantEnabled ?? false;
185+
try {
186+
const data = drawerApi.getData<{
187+
isEdit: boolean;
188+
orgEnabled?: boolean;
189+
tenantEnabled?: boolean;
190+
row?: any;
191+
}>();
192+
isEdit.value = data?.isEdit ?? false;
193+
orgEnabled.value = data?.orgEnabled ?? false;
194+
tenantEnabled.value = data?.tenantEnabled ?? false;
194195
195-
formApi.setState({
196-
schema: drawerSchema(
197-
isEdit.value,
198-
orgEnabled.value,
199-
tenantEnabled.value,
200-
!tenantStore.isPlatform,
201-
),
202-
});
196+
formApi.setState({
197+
schema: drawerSchema(
198+
isEdit.value,
199+
orgEnabled.value,
200+
tenantEnabled.value,
201+
!tenantStore.isPlatform,
202+
),
203+
});
203204
204-
const setupTasks: Promise<unknown>[] = [
205-
setupRoleOptions(),
206-
setupTenantOptions(),
207-
dictStore.getDictOptionsAsync('sys_normal_disable'),
208-
];
209-
if (orgEnabled.value) {
210-
setupTasks.push(setupDeptSelect());
211-
}
205+
const setupTasks: Promise<unknown>[] = [
206+
setupRoleOptions(),
207+
setupTenantOptions(),
208+
dictStore.getDictOptionsAsync('sys_normal_disable'),
209+
];
210+
if (orgEnabled.value) {
211+
setupTasks.push(setupDeptSelect());
212+
}
212213
213-
const setupResults = await Promise.all(setupTasks);
214-
const statusOptions = setupResults[2] as Awaited<
215-
ReturnType<typeof dictStore.getDictOptionsAsync>
216-
>;
217-
formApi.updateSchema([
218-
{
219-
fieldName: 'status',
220-
componentProps: {
221-
options: statusOptions.map((d) => ({
222-
label: d.label,
223-
value: Number(d.value),
224-
})),
214+
const setupResults = await Promise.all(setupTasks);
215+
const statusOptions = setupResults[2] as Awaited<
216+
ReturnType<typeof dictStore.getDictOptionsAsync>
217+
>;
218+
formApi.updateSchema([
219+
{
220+
fieldName: 'status',
221+
componentProps: {
222+
options: statusOptions.map((d) => ({
223+
label: d.label,
224+
value: Number(d.value),
225+
})),
226+
},
225227
},
226-
},
227-
]);
228+
]);
228229
229-
if (isEdit.value && data?.row) {
230-
userId.value = data.row.id;
231-
const user = await userInfo(data.row.id);
232-
const values: Record<string, any> = {
233-
username: user.username,
234-
nickname: user.nickname,
235-
email: user.email,
236-
phone: user.phone,
237-
sex: user.sex,
238-
status: user.status,
239-
remark: user.remark,
240-
roleIds: user.roleIds,
241-
};
242-
if (tenantEnabled.value) {
243-
values.tenantIds =
244-
tenantStore.isPlatform || user.tenantIds?.length
245-
? (user.tenantIds ?? [])
246-
: tenantStore.currentTenant
247-
? [tenantStore.currentTenant.id]
248-
: [];
249-
}
230+
if (isEdit.value && data?.row) {
231+
userId.value = data.row.id;
232+
const user = await userInfo(data.row.id);
233+
const values: Record<string, any> = {
234+
username: user.username,
235+
nickname: user.nickname,
236+
email: user.email,
237+
phone: user.phone,
238+
sex: user.sex,
239+
status: user.status,
240+
remark: user.remark,
241+
roleIds: user.roleIds,
242+
};
243+
if (tenantEnabled.value) {
244+
values.tenantIds =
245+
tenantStore.isPlatform || user.tenantIds?.length
246+
? (user.tenantIds ?? [])
247+
: tenantStore.currentTenant
248+
? [tenantStore.currentTenant.id]
249+
: [];
250+
}
250251
251-
if (orgEnabled.value) {
252-
values.deptId = user.deptId;
253-
values.postIds = user.postIds;
254-
}
252+
if (orgEnabled.value) {
253+
values.deptId = user.deptId;
254+
values.postIds = user.postIds;
255+
}
255256
256-
await formApi.setValues(values);
257-
if (orgEnabled.value && user.deptId) {
258-
await setupPostOptions(user.deptId);
259-
}
260-
} else {
261-
userId.value = 0;
262-
await formApi.resetForm();
263-
if (tenantEnabled.value && !tenantStore.isPlatform) {
264-
await formApi.setValues({
265-
tenantIds: tenantStore.currentTenant
266-
? [tenantStore.currentTenant.id]
267-
: [],
268-
});
257+
await formApi.setValues(values);
258+
if (orgEnabled.value && user.deptId) {
259+
await setupPostOptions(user.deptId);
260+
}
261+
} else {
262+
userId.value = 0;
263+
await formApi.resetForm();
264+
if (tenantEnabled.value && !tenantStore.isPlatform) {
265+
await formApi.setValues({
266+
tenantIds: tenantStore.currentTenant
267+
? [tenantStore.currentTenant.id]
268+
: [],
269+
});
270+
}
269271
}
272+
} finally {
273+
drawerApi.setState({ loading: false });
270274
}
271-
272-
drawerApi.setState({ loading: false });
273275
},
274276
async onConfirm() {
275277
const values = await formApi.getValues();

openspec/changes/release-test-and-build/tasks.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,12 @@
6464
- [x] **FB-19**: GitHub Actions `E2E tests (host-only)``TC0012d` 字典类型删除和 `TC-228a` 用户批量编辑中仍存在重试状态丢失、删除响应监听不稳和全局加载遮罩拦截点击的问题,导致 nightly host-only E2E 失败
6565
- [x] **FB-20**: `.github/workflows/release-test-and-build.yml` 仍手工展开测试 job,未像 nightly 一样复用共享测试验证套件,且 release 应采用 Main CI 的简要测试范围,不运行完整 E2E
6666
- [ ] **FB-21**: host-only E2E `TC0063-auth-menu` 仍通过中文菜单名硬编码创建受限角色,未显式包含 `system:user:query` 等页面实际依赖的按钮权限,导致普通用户访问 `/system/user` 时连续收到 `403` 并卡住 loading,从而让 `TC0063b``TC0063d` 失败
67-
- [ ] **FB-22**: host-only E2E `act` 容器内执行时,用户新增抽屉和批量编辑弹窗初始化串行请求过多,导致 `TC0226-user-account-labels``TC0228-user-batch-edit` `waitForDialogReady` 的 10 秒 busy 等待内长期保持 loading 遮罩,从而让 `E2E tests (host-only)` 继续失败
67+
- [x] **FB-22**: host-only E2E 登录后把宿主普通 `platform` 顶层目录误判为多租户能力,导致 `TC0226-user-account-labels``TC0228-user-batch-edit` 的用户新增抽屉和批量编辑弹窗调用缺失的 `/auth/login-tenants` 路由并长期保持 loading 遮罩,从而让 `E2E tests (host-only)` 继续失败
6868

6969
## Feedback Verification
7070

71+
- [x] 2026-05-15: FB-22 验证通过:在 GitHub Actions run `25911233842` 对应的 `main` 分支失败日志中确认 host-only `TC0226-user-account-labels` 与 `TC0228-user-batch-edit` 均卡在用户新增抽屉/批量编辑弹窗 busy 遮罩;本地用干净 Docker PostgreSQL host-only 环境复现时,登录后的 `linapro:tenant-state` 因宿主普通 `platform` 顶层目录被 `hasMultiTenantMenu` 误判而写入 `enabled=true`,用户页随后打开弹窗会调用 host-only 后端不存在的 `/api/v1/auth/login-tenants?userId=1`,后端返回路由重定向并最终让弹窗初始化 Promise 失败,loading 未关闭。修复后多租户能力检测只识别具体租户页面路径(如 `platform/tenants`、`tenant/*`),不再把宿主 `platform` 目录作为多租户信号;用户新增抽屉与批量编辑弹窗的初始化流程均使用 `finally` 收敛 loading,避免初始化异常时长期遮罩。验证命令:`git diff --check -- apps/lina-vben/apps/web-antd/src/store/auth.ts apps/lina-vben/apps/web-antd/src/views/system/user/user-drawer.vue apps/lina-vben/apps/web-antd/src/views/system/user/user-batch-edit-modal.vue openspec/changes/release-test-and-build/tasks.md`、`openspec validate release-test-and-build --strict`、`pnpm -C apps/lina-vben/apps/web-antd run typecheck`、`pnpm -C hack/tests test:validate`、临时干净库 `GF_GCFG_PATH=/tmp/linapro-fb22-config go run main.go init --confirm=init --sql-source=local`、`E2E_BASE_URL=http://127.0.0.1:5666 E2E_API_BASE_URL=http://127.0.0.1:8080/api/v1/ E2E_PUBLIC_BASE_URL=http://127.0.0.1:8080 E2E_DB_PORT=55433 E2E_BROWSER_CHANNEL=chrome pnpm -C hack/tests exec playwright test e2e/settings/user/TC0226-user-account-labels.ts e2e/settings/user/TC0228-user-batch-edit.ts --project=chromium --workers=1`,目标 2 个 E2E 用例全部通过,登录 storage state 中 `tenant-state.enabled=false`。i18n 影响:不新增或修改前端运行时文案、manifest i18n 或 apidoc i18n 资源。缓存一致性影响:不新增或修改运行时缓存、失效或跨实例一致性策略。数据权限影响:不新增或修改 HTTP/API 数据操作接口,不涉及角色数据权限边界。开发工具与脚本影响:未新增或修改默认开发工具、CI 辅助脚本或平台脚本;临时 Docker PostgreSQL 仅用于本地验证。
72+
- [x] 2026-05-15: FB-22 `lina-review` 审查完成。审查范围来源:`git status --short`、`git ls-files --others --exclude-standard`、`git diff -- apps/lina-vben/apps/web-antd/src/store/auth.ts apps/lina-vben/apps/web-antd/src/views/system/user/user-drawer.vue apps/lina-vben/apps/web-antd/src/views/system/user/user-batch-edit-modal.vue openspec/changes/release-test-and-build/tasks.md`、`openspec status --change release-test-and-build --json`。确认本次实际改动只影响前端登录态多租户能力判定、用户新增抽屉/批量编辑弹窗 loading 收敛和 OpenSpec 反馈记录;没有未跟踪文件,没有 Go 生产代码、业务 API、数据库 schema、前端运行时文案、manifest i18n、apidoc i18n、运行时缓存或数据权限逻辑变更。现有 `TC0226-user-account-labels` 和 `TC0228-user-batch-edit` 已直接覆盖本次用户可观察 bug 的复现和修复,干净 host-only Docker PostgreSQL 环境中目标用例全部通过。当前工作区另有 `apps/lina-plugins` submodule 状态改动,本次审查未覆盖、回退或重写该独立改动。严重问题 0;警告 0。
7173
- [x] 2026-05-15: FB-21 验证通过:本地复跑 `E2E_BROWSER_CHANNEL=chrome E2E_PARALLEL_WORKERS=1 pnpm -C hack/tests test:host` 时,host-only 串行阶段新增复现 `TC0063b` 与 `TC0063d` 失败;失败表象为 `waitForBusyIndicatorsToClear` 超时,但根因是测试角色仅按中文菜单名分配“权限管理/用户管理”,未显式纳入用户管理页面真实依赖的 `system:user:query` 权限,普通用户进入 `/system/user` 后 `/api/v1/user` 与 `/api/v1/user/dept-tree` 持续返回 `403`,页面 loading 无法结束。修复后 `TC0063-auth-menu` 改为通过 `getMenuIdsByPermsWithAncestors` 以权限点装配角色菜单:基础场景显式包含 `system:user:list` 与 `system:user:query`,扩展场景额外包含 `system:role:list` 与 `system:role:query`,避免菜单显示断言依赖脆弱的中文名称推断和遗漏按钮权限。验证命令:`pnpm -C hack/tests test:validate`、`E2E_BROWSER_CHANNEL=chrome pnpm -C hack/tests exec playwright test hack/tests/e2e/iam/menu/TC0063-auth-menu.ts --project=chromium --workers=1`、`E2E_BROWSER_CHANNEL=chrome pnpm -C hack/tests exec playwright test hack/tests/e2e/iam/menu/TC0060-menu-crud.ts hack/tests/e2e/iam/menu/TC0063-auth-menu.ts --project=chromium --workers=1`。i18n 影响:仅调整 E2E 测试准备逻辑,不新增或修改前端运行时文案、接口文档、manifest i18n 或 apidoc i18n 资源。缓存一致性影响:不涉及运行时缓存、失效或跨实例一致性策略。数据权限影响:不修改生产数据权限实现;修复只让测试角色显式携带当前页面实际依赖的查询权限,以匹配现有权限模型。开发工具与脚本影响:未新增默认开发工具或平台脚本;本地使用系统 Chrome channel 运行 Playwright 验证。
7274
- [x] 2026-05-15: FB-19 验证通过:分析 GitHub Actions 日志 `/Users/john/Downloads/job-logs.txt`,确认 host-only E2E 最终失败为 `TC0012d` 字典类型删除和 `TC-228a` 用户批量编辑,`TC0054e` 首次失败但 retry 通过。修复后 `TC0012` 改为每个子用例自建自清理,避免 Playwright retry 只重跑失败子用例时丢失前序创建/编辑状态;`DictPage.deleteType` 改为先定位目标行再打开确认框,并用 `Promise.all` 将确认点击与 `/dict/type/{id}` DELETE 响应等待绑定,避免错过请求;通用 busy 等待改为等待所有可见 loading/overlay 消失,并覆盖 Vben `.bg-overlay-content` 遮罩;`UserPage.batchUpdateSelectedStatus` 在批量编辑弹窗 loading 清空后点击开关,并等待 `/user` PUT 响应完成。验证命令:`cd hack/tests && pnpm test:validate`、`pnpm -C hack/tests exec tsc --noEmit`、`openspec validate release-test-and-build --strict`、`E2E_BROWSER_CHANNEL=chrome pnpm -C hack/tests exec playwright test hack/tests/e2e/settings/dict/TC0012-dict-type-crud.ts --project=chromium --workers=1`、`E2E_BROWSER_CHANNEL=chrome pnpm -C hack/tests exec playwright test hack/tests/e2e/settings/dict/TC0012-dict-type-crud.ts hack/tests/e2e/settings/dict/TC0054-dict-type-import-upload.ts hack/tests/e2e/settings/user/TC0228-user-batch-edit.ts --project=chromium --workers=1`,相关 13 个 E2E 子用例全部通过。i18n 影响:仅调整 E2E 测试与测试 helper,不新增或修改前端运行时文案、接口文档、manifest i18n 或 apidoc i18n 资源。缓存一致性影响:不涉及运行时缓存、失效或跨实例一致性策略。数据权限影响:不新增或修改 HTTP/API 数据操作接口,不涉及角色数据权限边界。开发工具与脚本影响:未新增默认开发工具或平台脚本;本地因 Playwright 自带 Chromium 未安装且下载受阻,使用系统 Chrome channel 完成行为验证,CI 仍由 pnpm Playwright 安装链路提供浏览器。
7375
- [x] 2026-05-15: FB-19 `lina-review` 审查完成。审查范围来源:`git status --short`、`git ls-files --others --exclude-standard`、`git diff -- hack/tests/e2e/settings/dict/TC0012-dict-type-crud.ts hack/tests/pages/DictPage.ts hack/tests/pages/UserPage.ts hack/tests/support/ui.ts openspec/changes/release-test-and-build/tasks.md`、`openspec status --change release-test-and-build --json`。确认本次实际改动只影响宿主 Playwright E2E、共享 POM/helper 和 OpenSpec 任务记录;没有未跟踪文件,没有 Go 生产代码、业务 API、数据库 schema、前端运行时文案、manifest i18n、apidoc i18n、运行时缓存或数据权限逻辑变更。`TC0012` 子用例已消除跨子用例状态依赖并具备自清理;字典删除 helper 的响应等待与确认点击顺序符合 Playwright 并发等待模式;用户批量编辑等待覆盖 Vben loading overlay,避免遮罩拦截交互。E2E 清单校验、TypeScript 编译、OpenSpec 严格校验、相关 13 个 E2E 子用例和空白检查均通过。当前工作区另有 release workflow 与 OpenSpec 设计/规范改动,本次审查未覆盖、回退或重写这些独立改动。严重问题 0;警告 0。

0 commit comments

Comments
 (0)