|
|
@@ -1164,3 +1164,136 @@ console.log('E3 最近作业动态断言通过:ACTIVITY_LIMIT=5 与待办条
|
|
|
assert.equal(state.receiving, false);
|
|
|
console.log('仓储看板业务断言通过:月份、空值、大整数身份、授权入口、待办范围、分组下钻、收货去重与失败重试、周转/呆滞后端分级映射、呆滞分子分母不可颠倒(3÷70=4.29%)、pp 环比缺失回归、口径条阈值占位符与 180 回落、stagnantDays 命名守卫。');
|
|
|
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
|
+
|
|
|
+
|
|
|
+// —— G1 深色主题作用域:只允许作用于看板自身,禁止 body 级全局 class ——
|
|
|
+// 背景:body.wms-dashboard-theme 曾把 .ele-admin-content 下的 Element 组件整片刷成深色,
|
|
|
+// 首页下钻到出库单详情、盘点工单详情等业务页时 URL 带着 dashboard=1,新页签因此整页变深蓝背景。
|
|
|
+const dashboardElementCode = stripCodeComments(fs.readFileSync(path.join(folder, 'dashboard-element.scss'), 'utf8'));
|
|
|
+const dashboardVueCode = stripCodeComments(fs.readFileSync(path.join(folder, 'dashboard.vue'), 'utf8'));
|
|
|
+const routerCode = stripCodeComments(fs.readFileSync(path.join(root, 'src/router/index.js'), 'utf8'));
|
|
|
+assert.doesNotMatch(dashboardElementCode, /body\.[A-Za-z-]+/, 'dashboard-element.scss 不得以 body 级 class 为作用域:它会把下钻后的业务页整页刷成深色');
|
|
|
+assert.match(dashboardElementCode, /\.wms-home\s*(,|\{)/, 'dashboard-element.scss 必须保留 .wms-home 作用域,否则看板自身的深色样式全部失效');
|
|
|
+assert.match(dashboardElementCode, /\.wms-dashboard-dialog/, '看板专用弹层 .wms-dashboard-dialog 必须保留:append-to-body 的弹层只能靠它着色');
|
|
|
+assert.match(dashboardElementCode, /\.wms-dashboard-popper/, '看板专用下拉面板 .wms-dashboard-popper 必须保留:popper 被挂到 body,靠 .wms-home 覆盖不到');
|
|
|
+assert.doesNotMatch(dashboardVueCode, /document\.body\.classList/, 'dashboard.vue 不得再操作 body 全局主题 class:销毁/失活时机与目标页渲染交错,残留即污染下一个页签');
|
|
|
+assert.doesNotMatch(routerCode, /document\.body\.classList\.(toggle|add|remove)/, 'router 不得按 to.query.dashboard 切换 body 主题 class:dashboard=1 会随首页下钻带到业务页');
|
|
|
+const navigateSource = (dashboardVueCode.match(/navigate\(path, query = \{\}\)[^\n]*/) || [''])[0];
|
|
|
+assert.ok(navigateSource, 'dashboard.vue 必须保留下钻方法 navigate');
|
|
|
+assert.doesNotMatch(navigateSource, /dashboard: *'1'/, 'navigate 不得把 dashboard=1 带进业务页 URL:系统原有入口不携带该参数');
|
|
|
+assert.match(navigateSource, /NavigationDuplicated/, 'navigate 必须吞掉 NavigationDuplicated:重复点同一行不该弹错误提示');
|
|
|
+console.log('G1 深色主题作用域断言通过:dashboard-element.scss 不再挂 body 级 class(下钻到出库单详情/盘点工单详情等业务页保持系统白底)、.wms-home/.wms-dashboard-dialog/.wms-dashboard-popper 三个作用域保留、dashboard.vue 与 router 均不再操作 body 主题 class、navigate 不再带 dashboard=1 且吞掉 NavigationDuplicated。');
|
|
|
+
|
|
|
+
|
|
|
+// —— H1 首页默认基本单位:不选单位时 KPI 整片为空,必须默认第一个 ——
|
|
|
+const unitWith = (state) => Object.assign({ defaultUnit: page.methods.defaultUnit }, state);
|
|
|
+const defaultedState = unitWith({ unit: '', filters: {}, options: { defaultUnit: 'PCS', units: [{ unit: '个' }, { unit: 'PCS' }] } });
|
|
|
+page.methods.applyDefaultUnit.call(defaultedState, false);
|
|
|
+assert.equal(defaultedState.unit, 'PCS', 'URL 未带 unit 时必须默认选中后端 home-options 的 defaultUnit');
|
|
|
+assert.equal(defaultedState.filters.unit, 'PCS', '默认单位必须同时写进 filters,否则请求参数里根本没有 unit');
|
|
|
+const explicitEmptyState = unitWith({ unit: '', filters: {}, options: { defaultUnit: 'PCS' } });
|
|
|
+page.methods.applyDefaultUnit.call(explicitEmptyState, true);
|
|
|
+assert.equal(explicitEmptyState.unit, '', 'URL 显式 unit=(用户主动选「全部单位」)时必须保持空,不能被默认值覆盖');
|
|
|
+assert.deepEqual(explicitEmptyState.filters, {}, '显式空单位不得写进 filters');
|
|
|
+const keepState = unitWith({ unit: 'KG', filters: { unit: 'KG' }, options: { defaultUnit: 'PCS' } });
|
|
|
+page.methods.applyDefaultUnit.call(keepState, false);
|
|
|
+assert.equal(keepState.unit, 'KG', '用户已选单位不得被默认值覆盖');
|
|
|
+assert.equal(page.methods.defaultUnit.call({ options: { units: [{ unit: ' ' }, { unit: 'KG' }] } }), 'KG', 'defaultUnit 缺失时必须回退 units 第一个非空单位(空白不算)');
|
|
|
+assert.equal(page.methods.defaultUnit.call({ options: { units: [] } }), '', '没有任何单位时返回空串,不得编造默认值');
|
|
|
+assert.equal(page.methods.defaultUnit.call({ options: { units: [{ unit: '个' }] } }), '个', '未下发 defaultUnit 的老响应按 units[0] 回退');
|
|
|
+const homeUnitSource = stripCodeComments(fs.readFileSync(path.join(folder, 'dashboard.vue'), 'utf8'));
|
|
|
+assert.match(homeUnitSource, /if \(this\.view === 'home'\) this\.applyDefaultUnit\(/, 'readRoute 必须在首页兜底默认单位(首次进入、切仓、返回首页都走这里)');
|
|
|
+assert.match(homeUnitSource, /this\.applyDefaultUnit\(Object\.prototype\.hasOwnProperty\.call\(this\.\$route\.query, 'unit'\)\)/, 'created 拉到筛选项后必须再兜一次默认单位:第一次 readRoute 时 options 还没到');
|
|
|
+assert.doesNotMatch(homeUnitSource, /warehouseId, unit: ''/, '切换仓库不得清空基本单位:清空后立刻落到「全部单位」,首页又没有数据');
|
|
|
+assert.match(homeUnitSource, /if \(view === 'home' && this\.view !== 'home' && !filters\.unit\) filters\.unit = this\.defaultUnit\(\)/, '从出入库申请/入总库申请返回首页必须补回单位,否则业务视图清掉的空单位会一直留着');
|
|
|
+console.log('H1 首页默认基本单位断言通过:URL 未带 unit 时默认选中 home-options.defaultUnit(缺失回退 units 首个非空单位)并写进请求参数、显式「全部单位」不被覆盖、已选单位不被覆盖、切换仓库与从业务视图返回首页都不再落回「全部单位」。');
|
|
|
+
|
|
|
+
|
|
|
+// —— I1 盘点工单详情下钻:必须带 planId,且不得把首页筛选整包塞进业务页 URL ——
|
|
|
+let pushed = null;
|
|
|
+const navState = {
|
|
|
+ filters: { startTime: '2026-10-01 00:00:00', endTime: '2026-10-31 23:59:59', page: '1', warehouseId: '2103077489545842690', unit: 'KG', categoryId: '2082647086249619457' },
|
|
|
+ $router: { push(target) { pushed = target; return Promise.resolve(); } },
|
|
|
+ $message: { error(message) { throw new Error(message); } }
|
|
|
+};
|
|
|
+// cleanQuery 在 vm 上下文里执行,返回对象与本上下文原型不同,deepEqual 会误报,统一用 JSON 串比对。
|
|
|
+const sameQuery = (actual, expected, message) => assert.equal(JSON.stringify(actual), JSON.stringify(expected), message);
|
|
|
+page.methods.navigate.call(navState, '/warehouseManagement/stocktaking/work/details', { id: '2115696383397093378', planId: '2115696383174795265' });
|
|
|
+sameQuery(pushed.query, { id: '2115696383397093378', planId: '2115696383174795265' },
|
|
|
+ '盘点工单详情 URL 只允许 id 与 planId:首页的 startTime/endTime/page/unit/categoryId/warehouseId 一个都不该带进去');
|
|
|
+assert.equal(pushed.path, '/warehouseManagement/stocktaking/work/details');
|
|
|
+pushed = null;
|
|
|
+page.methods.navigate.call(navState, '/warehouseManagement/outgoingManagement/details', { id: '2115696383397093378', planId: undefined });
|
|
|
+sameQuery(pushed.query, { id: '2115696383397093378' }, '没有 planId 的入口(旧数据/旧链接)不得拼出 planId=undefined');
|
|
|
+
|
|
|
+let businessPush = null;
|
|
|
+const businessState = {
|
|
|
+ navigate(path, query) { businessPush = { path, query }; },
|
|
|
+ open() {}, portal() {}, filters: {},
|
|
|
+ $refs: { actions: { open() {} } },
|
|
|
+ $message: { warning() {}, error() {} }
|
|
|
+};
|
|
|
+businessPush = null;
|
|
|
+page.methods.business.call(businessState, 'todo', { type: 'inventory', id: '2115696383397093378', planId: '2115696383174795265' });
|
|
|
+assert.equal(businessPush.path, '/warehouseManagement/stocktaking/work/details');
|
|
|
+sameQuery(businessPush.query, { id: '2115696383397093378', planId: '2115696383174795265' },
|
|
|
+ '待办「盘点任务进行中」下钻必须同时带 id(planOrderId) 与 planId,否则详情页拿不到计划信息');
|
|
|
+businessPush = null;
|
|
|
+page.methods.business.call(businessState, 'io', { activityType: 'inventory', orderId: '2115696383397093378', planId: '2115696383174795265' });
|
|
|
+assert.equal(businessPush.path, '/warehouseManagement/stocktaking/work/details');
|
|
|
+sameQuery(businessPush.query, { id: '2115696383397093378', planId: '2115696383174795265' },
|
|
|
+ '出入库动态里的盘点流水下钻同样要带 planId');
|
|
|
+const navigateNoFiltersSource = stripCodeComments(fs.readFileSync(path.join(folder, 'dashboard.vue'), 'utf8')).match(/navigate\(path, query = \{\}\)[^\n]*/)[0];
|
|
|
+assert.doesNotMatch(navigateNoFiltersSource, /\.\.\.this\.filters/, 'navigate 不得再整包透传首页筛选(this.filters)到业务页 URL');
|
|
|
+console.log('I1 盘点工单详情下钻断言通过:待办/动态两个入口都带 id+planId、planId 缺失时不拼 undefined、下钻 URL 只保留显式参数(首页 startTime/endTime/page/unit/categoryId/warehouseId 不再进业务页)。');
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+// —— J1 「出入库动态 › 查看动态」必须跳系统菜单真实路由 warehouseManagement/inboundRequests ——
|
|
|
+// 不是首页内嵌的 ioapp 视图,也不是自建 V4 动态列表:用户要找的是「出入库申请」这张菜单页本身。
|
|
|
+const homeTplSource = fs.readFileSync(path.join(folder, 'components/HomeOverview.vue'), 'utf8');
|
|
|
+const trendLink = (homeTplSource.match(/<a\b[^>]*>查看动态 ›<\/a>/) || [])[0] || '';
|
|
|
+assert.ok(trendLink, '「查看动态 ›」入口必须存在');
|
|
|
+assert.match(trendLink, /\$emit\('navigate', '\/warehouseManagement\/inboundRequests', \{ warehouseId: warehouseId \}\)/,
|
|
|
+ '「查看动态 ›」必须发 navigate(\'/warehouseManagement/inboundRequests\', { warehouseId }):跳菜单真实路由,而不是内嵌 ioapp 视图');
|
|
|
+assert.doesNotMatch(trendLink, /\$emit\('open', 'ioapp'/, '不得再用内嵌 ioapp 视图顶替菜单页');
|
|
|
+assert.doesNotMatch(trendLink, /\$emit\('open', 'io'\)/, '也不得落到自建 V4 动态列表');
|
|
|
+const dashboardTplSource = fs.readFileSync(path.join(folder, 'dashboard.vue'), 'utf8');
|
|
|
+const homeOverviewTag = (dashboardTplSource.match(/<home-overview\b[^>]*>/) || [])[0] || '';
|
|
|
+assert.match(homeOverviewTag, /@navigate="navigate"/, 'dashboard.vue 必须把 home-overview 的 navigate 事件接到 navigate(),否则点「查看动态」无反应');
|
|
|
+let routePushed = null;
|
|
|
+const routeState = {
|
|
|
+ $route: { path: '/warehouseManagement/index', query: {} },
|
|
|
+ $router: { push(target) { routePushed = target; return Promise.resolve(); } },
|
|
|
+ $message: { error(message) { throw new Error(message); } }
|
|
|
+};
|
|
|
+page.methods.navigate.call(routeState, '/warehouseManagement/inboundRequests', { warehouseId: '2103077489545842690' });
|
|
|
+assert.equal(routePushed.path, '/warehouseManagement/inboundRequests', '必须跳真实菜单路由,路径与系统菜单一致');
|
|
|
+sameQuery(routePushed.query, { warehouseId: '2103077489545842690' }, '只带当前仓库,状态/时间/页签交给目标页默认值');
|
|
|
+assert.ok(!('dashboard' in routePushed.query), '跳菜单页不得再带 dashboard=1:会误开看板批量收货勾选列');
|
|
|
+routePushed = null;
|
|
|
+page.methods.navigate.call(routeState, '/warehouseManagement/inboundRequests', { warehouseId: '' });
|
|
|
+sameQuery(routePushed.query, {}, '未选仓库(全部授权仓库)时 query 必须为空,不能塞空串或首页筛选');
|
|
|
+const allLink = (homeTplSource.match(/<a\b[^>]*>全部动态 ›<\/a>/) || [])[0] || '';
|
|
|
+assert.match(allLink, /\$emit\('open', 'io'\)/, '「全部动态 ›」仍落 V4 动态列表,不被本次改动带走');
|
|
|
+console.log('J1 出入库动态入口断言通过:「查看动态 ›」发 navigate(\'/warehouseManagement/inboundRequests\', { warehouseId }),dashboard.vue 已接 @navigate,跳真实菜单路由(独立页签)、只带仓库、不带 dashboard=1;「全部动态 ›」与趋势柱体仍落 V4 动态列表。');
|
|
|
+
|
|
|
+
|
|
|
+// —— K1 首页首屏遮罩:数据回来之前不显示内容,且只在首屏出现一次 ——
|
|
|
+const dashSource = stripCodeComments(fs.readFileSync(path.join(folder, 'dashboard.vue'), 'utf8'));
|
|
|
+const maskTag = (dashSource.match(/<div\b[^>]*class="home-loading"[^>]*>/) || [])[0] || '';
|
|
|
+assert.ok(maskTag, 'dashboard.vue 必须有首屏遮罩节点(.home-loading)');
|
|
|
+assert.match(maskTag, /v-if="homeLoading && view === 'home'"/, '遮罩只能在首页视图且 homeLoading 为真时出现:明细视图有自己的 v-loading,不能被这块盖住');
|
|
|
+assert.ok(dashSource.includes('正在加载首页数据'), '遮罩必须给出中文加载文案,不能只放一个转圈');
|
|
|
+assert.equal(page.data().homeLoading, true, 'homeLoading 必须默认 true:首屏还没请求就不能先渲染空卡片');
|
|
|
+const createdBlock = dashSource.slice(dashSource.indexOf('async created()'), dashSource.indexOf('activated()'));
|
|
|
+assert.match(createdBlock, /this\.homeLoading = false;/, 'created 里首屏(筛选项 + 汇总)结束后必须撤掉遮罩');
|
|
|
+assert.ok(createdBlock.indexOf('this.homeLoading = false') > createdBlock.indexOf('await this.refresh()'), '必须先 await refresh() 再撤遮罩,否则数据还没到就露出来了');
|
|
|
+assert.ok((dashSource.match(/this\.homeLoading = false/g) || []).length === 1, 'homeLoading 只能在首屏置一次 false:定时刷新与筛选项切换不该闪遮罩');
|
|
|
+const layoutScss = stripCodeComments(fs.readFileSync(path.join(folder, 'dashboard-layout.scss'), 'utf8'));
|
|
|
+assert.match(layoutScss, /\.wms-home\s*\{[^}]*position:\s*relative/, '.wms-home 必须是定位上下文,否则遮罩会跑去页面别处');
|
|
|
+assert.match(layoutScss, /\.wms-home \.home-loading\s*\{[^}]*position:\s*absolute/, '遮罩必须绝对定位在内容区内');
|
|
|
+assert.doesNotMatch(layoutScss, /\.wms-home \.home-loading\s*\{[^}]*position:\s*fixed/, '遮罩不得用 fixed:会把系统页头、菜单、标签栏一起盖掉');
|
|
|
+assert.match(layoutScss, /\.wms-home \.home-loading\s*\{[^}]*z-index:\s*\d+/, '遮罩必须显式抬层级,否则盖不住卡片');
|
|
|
+assert.match(layoutScss, /@keyframes\s+home-loading-spin\s*\{[\s\S]{0,120}?rotate\(360deg\)/, '转圈动画必须存在,否则 spinner 静态不转');
|
|
|
+console.log('K1 首页首屏遮罩断言通过:homeLoading 默认 true、created 中 await refresh() 之后置 false(且只置一次),遮罩仅在首页视图出现并带中文文案;样式为 .wms-home 内的 absolute 遮罩(非 fixed,不盖系统页头/菜单)+ z-index + 转圈关键帧。');
|