@steedos-labs/plugin-workflow 3.0.101 → 3.0.103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  waitForThing(window, 'antd').then(function(){
2
- var v = '3.0.101';
2
+ var v = '3.0.103';
3
3
  loadJs('/amis-renderer/amis-renderer.js?v=' + v);
4
4
  loadCss('/amis-renderer/amis-renderer.css?v=' + v)
5
5
  })
@@ -44,12 +44,23 @@ router.post('/api/workflow/v2/instance/return', requireAuthentication, async fun
44
44
  throw new Error("不符合退回条件:当前为第一步,无法退回");
45
45
  }
46
46
  flow = await UUFlowManager.getFlow(ins.flow);
47
- pre_trace = ins.traces[ins.traces.length - 2];
47
+ last_trace = _.last(ins.traces);
48
+ // 滑步处理:如果上一步是被跳过的(skipped),沿 previous_trace_ids 向前回溯,
49
+ // 退回到用户实际处理过的那个 trace(与取回 api_workflow_retrieve.router.js 的回溯逻辑一致),
50
+ // 否则 skipped approves 会被下方 new_inbox_users 过滤条件排除,导致"未找到下一步处理人"
51
+ var pre_trace_id = last_trace.previous_trace_ids && last_trace.previous_trace_ids[0];
52
+ pre_trace = (pre_trace_id && _.find(ins.traces, function (t) { return t._id === pre_trace_id; })) || ins.traces[ins.traces.length - 2];
53
+ while (pre_trace && pre_trace.judge === 'skipped' && pre_trace.previous_trace_ids && pre_trace.previous_trace_ids.length > 0) {
54
+ const skipped_prev_id = pre_trace.previous_trace_ids[0];
55
+ pre_trace = _.find(ins.traces, function (t) { return t._id === skipped_prev_id; });
56
+ }
57
+ if (!pre_trace) {
58
+ throw new Error("不符合退回条件:未找到可退回的上一步骤");
59
+ }
48
60
  pre_step = await UUFlowManager.getStep(ins, flow, pre_trace.step);
49
61
  if (pre_step.step_type === "counterSign") {
50
62
  throw new Error("不符合退回条件:上一步为会签步骤,不允许退回");
51
63
  }
52
- last_trace = _.last(ins.traces);
53
64
  current_step = await UUFlowManager.getStep(ins, flow, last_trace.step);
54
65
  if (current_step.step_type !== "submit" && current_step.step_type !== "sign" && current_step.step_type !== "counterSign") {
55
66
  throw new Error("不符合退回条件:当前步骤类型(" + current_step.step_type + ")不支持退回操作");
@@ -291,10 +291,12 @@ router.post('/api/workflow/v2/nextStepUsersValue', requireAuthentication, async
291
291
  for (let i = traces.length - 1; i >= 0; i--) {
292
292
  const trace = traces[i];
293
293
  if (trace.step === nextStepId && trace.approves && trace.approves.length > 0) {
294
- // 排除非实际处理人的 approve
294
+ // 排除非实际处理人的 approve;传阅/分发的 approve 不是本步骤处理人
295
295
  const excludedApproveJudges = ['returned', 'relocated', 'terminated', 'retrieved'];
296
+ const excludedApproveTypes = ['cc', 'distribute'];
296
297
  newNextUsers = trace.approves
297
- .filter(approve => !excludedApproveJudges.includes(approve.judge))
298
+ .filter(approve => !excludedApproveJudges.includes(approve.judge)
299
+ && !excludedApproveTypes.includes(approve.type))
298
300
  .map(approve => approve.handler);
299
301
  break;
300
302
  }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * 回归测试:/api/workflow/v2/nextStepUsersValue 的 traces 回退逻辑
3
+ * 不能把传阅(type='cc')/分发(type='distribute')的 approve 算作下一步处理人。
4
+ *
5
+ * 背景(线上现象):
6
+ * - 步骤A的审批人传阅给用户X → 步骤A当前 trace 里 push 一条
7
+ * {type:'cc', handler:X} 的 approve(未读时无 judge,提交意见后 judge='submitted')。
8
+ * - 审批人驳回到上一步 → 上一步待办点提交时,提交对话框通过本接口预填
9
+ * 下一步(=步骤A)处理人。
10
+ * - 回退逻辑只按 judge 排除(returned/relocated/terminated/retrieved),
11
+ * cc approve 的 judge 不在排除表 → 被传阅用户X被预填为步骤A处理人。
12
+ *
13
+ * 期望(修复后):
14
+ * - traces 回退取处理人时排除 type='cc' 与 type='distribute' 的 approve,
15
+ * 与引擎其他取实际处理人处(uuflow_manager L1139/L3086/L4862)口径一致。
16
+ * - 正常(无传阅)驳回场景行为不变。
17
+ *
18
+ * 用法:
19
+ * node main/default/test/test_next_step_users_value_cc.js
20
+ * 或 npm run test:next-step-users-cc
21
+ */
22
+
23
+ const assert = require('assert');
24
+ const Module = require('module');
25
+
26
+ // ---- 伪造无法在独立环境 resolve 的外部依赖(express / @steedos/*) ----
27
+ const capturedRoutes = {};
28
+ const fakeModules = {
29
+ 'express': {
30
+ Router: function () {
31
+ return {
32
+ post: function (path) {
33
+ capturedRoutes[path] = arguments[arguments.length - 1];
34
+ },
35
+ };
36
+ },
37
+ },
38
+ '@steedos/auth': {
39
+ requireAuthentication: function (req, res, next) { next(); },
40
+ },
41
+ '@steedos/objectql': {
42
+ getObject: function (name) {
43
+ return {
44
+ // space_users 校验:认为查询到的用户都在职,原样回显
45
+ find: async function (query) {
46
+ const inFilter = (query.filters || []).find(f => f[1] === 'in');
47
+ const userIds = inFilter ? inFilter[2] : [];
48
+ return userIds.map(u => ({ user: u, name: 'name_of_' + u }));
49
+ },
50
+ };
51
+ },
52
+ },
53
+ '@steedos/i18n': {
54
+ t: function (key) { return key; },
55
+ },
56
+ };
57
+ const origResolve = Module._resolveFilename;
58
+ Module._resolveFilename = function (request) {
59
+ if (fakeModules[request]) return 'fake:' + request;
60
+ return origResolve.apply(this, arguments);
61
+ };
62
+ Object.keys(fakeModules).forEach(function (name) {
63
+ require.cache['fake:' + name] = {
64
+ id: 'fake:' + name,
65
+ filename: 'fake:' + name,
66
+ loaded: true,
67
+ exports: fakeModules[name],
68
+ };
69
+ });
70
+
71
+ // ---- 伪造插件内部依赖 ----
72
+ function seedCache(relPath, exportsObj) {
73
+ const p = require.resolve(relPath);
74
+ require.cache[p] = { id: p, filename: p, loaded: true, exports: exportsObj };
75
+ }
76
+ seedCache('../utils/trigger.js', {
77
+ excuteTriggers: async function () { },
78
+ });
79
+ seedCache('../manager/workflow_manager.js', {});
80
+ let fakeInstance = null;
81
+ seedCache('../manager/uuflow_manager.js', {
82
+ getInstance: async function () { return fakeInstance; },
83
+ });
84
+
85
+ require('../routes/api_workflow_next_step_users.router.js');
86
+ const handler = capturedRoutes['/api/workflow/v2/nextStepUsersValue'];
87
+ assert.ok(handler, '未捕获到 nextStepUsersValue 路由 handler');
88
+
89
+ // ---- 构造实例:submit → 步骤P(已审) → 步骤A(传阅X后驳回) → 步骤P(当前待办) ----
90
+ function buildInstance(opts) {
91
+ opts = opts || {};
92
+ const stepATrace = {
93
+ _id: 't3', step: 'step_A', name: '步骤A', is_finished: true, judge: 'rejected',
94
+ approves: [
95
+ { _id: 'a3', user: 'user_a', handler: 'user_a', judge: 'rejected', is_finished: true },
96
+ ],
97
+ };
98
+ if (opts.ccUnread) {
99
+ stepATrace.approves.push({
100
+ _id: 'cc1', user: 'user_x', handler: 'user_x', type: 'cc', is_finished: false, is_read: false,
101
+ });
102
+ }
103
+ if (opts.ccSubmitted) {
104
+ stepATrace.approves.push({
105
+ _id: 'cc2', user: 'user_y', handler: 'user_y', type: 'cc', is_finished: true, judge: 'submitted',
106
+ });
107
+ }
108
+ if (opts.distribute) {
109
+ stepATrace.approves.push({
110
+ _id: 'd1', user: 'user_d', handler: 'user_d', type: 'distribute', is_finished: false,
111
+ });
112
+ }
113
+ if (opts.returnedApprove) {
114
+ stepATrace.approves.push({
115
+ _id: 'r1', user: 'user_r', handler: 'user_r', judge: 'returned', is_finished: true,
116
+ });
117
+ }
118
+ return {
119
+ _id: 'ins1', space: 'space1', state: 'pending', applicant: 'user_p', submitter: 'user_p',
120
+ flow: 'flow1', form: 'form1', form_version: 'fv1',
121
+ step_approve: {},
122
+ traces: [
123
+ {
124
+ _id: 't1', step: 'step_start', name: '填写申请单', is_finished: true, judge: 'submitted',
125
+ approves: [{ _id: 'a1', user: 'user_p', handler: 'user_p', type: 'draft', judge: 'submitted' }],
126
+ },
127
+ {
128
+ _id: 't2', step: 'step_P', name: '步骤P', is_finished: true, judge: 'approved',
129
+ approves: [{ _id: 'a2', user: 'user_p', handler: 'user_p', judge: 'approved' }],
130
+ },
131
+ stepATrace,
132
+ {
133
+ _id: 't4', step: 'step_P', name: '步骤P', is_finished: false,
134
+ approves: [{ _id: 'a4', user: 'user_p', handler: 'user_p', is_finished: false }],
135
+ },
136
+ ],
137
+ };
138
+ }
139
+
140
+ async function callValue(nextStepId) {
141
+ const req = {
142
+ user: { userId: 'user_p', spaceId: 'space1' },
143
+ body: { instanceId: 'ins1', nextStepId: nextStepId },
144
+ };
145
+ const res = {
146
+ statusCode: null, body: null,
147
+ status: function (c) { this.statusCode = c; return this; },
148
+ send: function (b) { this.body = b; return this; },
149
+ };
150
+ await handler(req, res);
151
+ return res;
152
+ }
153
+
154
+ let passed = 0;
155
+ let failed = 0;
156
+ async function run(name, fn) {
157
+ try {
158
+ await fn();
159
+ console.log(` ✓ ${name}`);
160
+ passed++;
161
+ } catch (e) {
162
+ console.log(` ✗ ${name}`);
163
+ console.log(` ${e.message}`);
164
+ failed++;
165
+ }
166
+ }
167
+
168
+ (async () => {
169
+ console.log('nextStepUsersValue 回退取人排除传阅/分发 approve');
170
+
171
+ await run('传阅(未读,无 judge)后驳回 → 预填处理人不含被传阅用户', async () => {
172
+ fakeInstance = buildInstance({ ccUnread: true });
173
+ const res = await callValue('step_A');
174
+ assert.strictEqual(res.statusCode, 200);
175
+ assert.deepStrictEqual(res.body.value, ['user_a'],
176
+ '应只含原审批人,实际:' + JSON.stringify(res.body.value));
177
+ });
178
+
179
+ await run('传阅(已提交意见,judge=submitted)后驳回 → 预填处理人不含被传阅用户', async () => {
180
+ fakeInstance = buildInstance({ ccSubmitted: true });
181
+ const res = await callValue('step_A');
182
+ assert.strictEqual(res.statusCode, 200);
183
+ assert.deepStrictEqual(res.body.value, ['user_a'],
184
+ '应只含原审批人,实际:' + JSON.stringify(res.body.value));
185
+ });
186
+
187
+ await run('分发(type=distribute)approve 同样被排除', async () => {
188
+ fakeInstance = buildInstance({ distribute: true });
189
+ const res = await callValue('step_A');
190
+ assert.strictEqual(res.statusCode, 200);
191
+ assert.deepStrictEqual(res.body.value, ['user_a'],
192
+ '应只含原审批人,实际:' + JSON.stringify(res.body.value));
193
+ });
194
+
195
+ await run('judge 排除表行为不变(returned 仍被排除)', async () => {
196
+ fakeInstance = buildInstance({ returnedApprove: true });
197
+ const res = await callValue('step_A');
198
+ assert.strictEqual(res.statusCode, 200);
199
+ assert.deepStrictEqual(res.body.value, ['user_a'],
200
+ '应只含原审批人,实际:' + JSON.stringify(res.body.value));
201
+ });
202
+
203
+ await run('正常(无传阅)驳回场景行为不变', async () => {
204
+ fakeInstance = buildInstance();
205
+ const res = await callValue('step_A');
206
+ assert.strictEqual(res.statusCode, 200);
207
+ assert.deepStrictEqual(res.body.value, ['user_a']);
208
+ });
209
+
210
+ console.log(`\n${passed} passed, ${failed} failed`);
211
+ process.exit(failed > 0 ? 1 : 0);
212
+ })();
@@ -0,0 +1,226 @@
1
+ /**
2
+ * 回归测试:滑步(skip processed)之后点退回,不能再报
3
+ * 500「未找到下一步处理人,退回失败」。
4
+ *
5
+ * 背景(线上现象):
6
+ * - 滑步把被跳过 trace 及其所有 approves 的 judge 写成 'skipped'
7
+ * (uuflow_manager.handleSkipProcessed)。
8
+ * - 退回路由取上一条 trace 的 approves 筛选退回对象,过滤条件只认
9
+ * judge ∈ (空/submitted/approved/rejected),'skipped' 被排除 →
10
+ * 候选人为空 → throw「未找到下一步处理人,退回失败」→ 500。
11
+ *
12
+ * 期望(修复后):
13
+ * - 与取回(api_workflow_retrieve.router.js)一致:沿 previous_trace_ids
14
+ * 回溯跳过所有 judge='skipped' 的 trace,退回到用户实际处理过的步骤。
15
+ * - 会签拦截检查作用于回溯后的真实目标步骤。
16
+ * - 正常(无滑步)退回行为不变。
17
+ *
18
+ * 用法:
19
+ * node main/default/test/test_return_after_skip.js
20
+ * 或 npm run test:return-after-skip
21
+ */
22
+
23
+ const assert = require('assert');
24
+ const Module = require('module');
25
+
26
+ // ---- 伪造无法在独立环境 resolve 的外部依赖(express / @steedos/*) ----
27
+ // 路由文件加载时即 require 这些包;用 _resolveFilename 钩子把它们指到
28
+ // 预先塞进 require.cache 的假模块上。
29
+ const capturedRoutes = {};
30
+ const objectqlUpdates = [];
31
+ const fakeModules = {
32
+ 'express': {
33
+ Router: function () {
34
+ return {
35
+ post: function (path) {
36
+ // (path, requireAuthentication, handler) → 取最后一个参数为业务 handler
37
+ capturedRoutes[path] = arguments[arguments.length - 1];
38
+ },
39
+ };
40
+ },
41
+ },
42
+ '@steedos/auth': {
43
+ requireAuthentication: function (req, res, next) { next(); },
44
+ },
45
+ '@steedos/objectql': {
46
+ getObject: function (name) {
47
+ return {
48
+ update: async function (id, doc) { objectqlUpdates.push({ name, id, doc }); return doc; },
49
+ };
50
+ },
51
+ },
52
+ };
53
+ const origResolve = Module._resolveFilename;
54
+ Module._resolveFilename = function (request) {
55
+ if (fakeModules[request]) return 'fake:' + request;
56
+ return origResolve.apply(this, arguments);
57
+ };
58
+ Object.keys(fakeModules).forEach(function (name) {
59
+ require.cache['fake:' + name] = {
60
+ id: 'fake:' + name,
61
+ filename: 'fake:' + name,
62
+ loaded: true,
63
+ exports: fakeModules[name],
64
+ };
65
+ });
66
+
67
+ // ---- 伪造插件内部依赖 ----
68
+ let instanceUpdateCalls = [];
69
+ const dummyInstancesCollection = {
70
+ updateOne: async function (selector, modifier) {
71
+ instanceUpdateCalls.push({ selector: selector, modifier: modifier });
72
+ return { modifiedCount: 1 };
73
+ },
74
+ };
75
+ const dummyUsersCollection = {
76
+ findOne: async function (selector) { return { _id: selector._id, name: 'name_of_' + selector._id }; },
77
+ };
78
+ function seedCache(relPath, exportsObj) {
79
+ const p = require.resolve(relPath);
80
+ require.cache[p] = { id: p, filename: p, loaded: true, exports: exportsObj };
81
+ }
82
+ seedCache('../utils/collection.js', {
83
+ getCollection: async function (name) {
84
+ return name === 'instances' ? dummyInstancesCollection : dummyUsersCollection;
85
+ },
86
+ _makeNewID: (function () { let n = 0; return function () { return 'new_id_' + (++n); }; })(),
87
+ });
88
+ seedCache('../manager/push_manager.js', {
89
+ send_message_to_specifyUser: async function () { },
90
+ send_instance_notification: async function () { },
91
+ triggerWebhook: async function () { },
92
+ });
93
+ seedCache('../manager/instance_tasks_manager.js', {
94
+ update_instance_tasks: async function () { },
95
+ insert_instance_tasks: async function () { },
96
+ });
97
+
98
+ let fakeInstance = null;
99
+ let stepTypes = {};
100
+ seedCache('../manager/uuflow_manager.js', {
101
+ getInstance: async function () { return fakeInstance; },
102
+ getFlow: async function () { return { _id: 'flow1' }; },
103
+ getStep: function (ins, flow, stepId) {
104
+ return { _id: stepId, name: 'name_' + stepId, step_type: stepTypes[stepId] || 'sign' };
105
+ },
106
+ getApproveValues: async function (values) { return values || {}; },
107
+ getDueDate: async function () { return null; },
108
+ getAgent: async function () { return null; },
109
+ getSpaceUser: async function () { return {}; },
110
+ getSpaceUserOrgInfo: async function () {
111
+ return { organization: 'org1', organization_name: 'org', organization_fullname: 'org' };
112
+ },
113
+ setRemindInfo: async function () { },
114
+ });
115
+
116
+ require('../routes/api_workflow_instance_return.router.js');
117
+ const handler = capturedRoutes['/api/workflow/v2/instance/return'];
118
+ assert.ok(handler, '未捕获到退回路由 handler');
119
+
120
+ // ---- 构造实例:submit → sign(已审批) → sign(被滑步跳过) → sign(当前待审) ----
121
+ function buildInstance(opts) {
122
+ opts = opts || {};
123
+ const traces = [
124
+ {
125
+ _id: 't1', step: 'step_start', name: '填写申请单', is_finished: true, judge: 'submitted',
126
+ approves: [{ _id: 'a1', user: 'user_a', handler: 'user_a', type: 'draft', judge: 'submitted' }],
127
+ },
128
+ {
129
+ _id: 't2', step: 'step_real', name: '实际审批步骤', is_finished: true, judge: 'approved',
130
+ previous_trace_ids: ['t1'],
131
+ approves: [{ _id: 'a2', user: 'user_a', handler: 'user_a', judge: 'approved' }],
132
+ },
133
+ {
134
+ _id: 't3', step: 'step_skipped', name: '被滑步跳过的步骤', is_finished: true, judge: 'skipped',
135
+ previous_trace_ids: ['t2'],
136
+ approves: [{ _id: 'a3', user: 'user_a', handler: 'user_a', judge: 'skipped' }],
137
+ },
138
+ {
139
+ _id: 't4', step: 'step_current', name: '当前步骤', is_finished: false,
140
+ previous_trace_ids: ['t3'],
141
+ approves: [{ _id: 'a4', user: 'user_c', handler: 'user_c', is_finished: false, start_date: new Date() }],
142
+ },
143
+ ];
144
+ if (opts.noSkip) {
145
+ traces.splice(2, 1); // 去掉滑步 trace,t4 直接跟在 t2 后
146
+ traces[2].previous_trace_ids = ['t2'];
147
+ }
148
+ return {
149
+ _id: 'ins1', space: 'space1', state: 'pending', applicant: 'user_a', submitter: 'user_a',
150
+ flow: 'flow1', form: 'form1', form_version: 'fv1',
151
+ inbox_users: ['user_c'], outbox_users: ['user_a'], cc_users: [],
152
+ values: {}, current_step_name: '当前步骤',
153
+ traces: traces,
154
+ };
155
+ }
156
+
157
+ async function callReturn() {
158
+ instanceUpdateCalls = [];
159
+ objectqlUpdates.length = 0;
160
+ const req = {
161
+ user: { userId: 'user_c' },
162
+ body: { approve: { _id: 'a4', instance: 'ins1', trace: 't4', values: {} }, reason: '退回测试' },
163
+ };
164
+ const res = {
165
+ statusCode: null, body: null,
166
+ status: function (c) { this.statusCode = c; return this; },
167
+ send: function (b) { this.body = b; return this; },
168
+ };
169
+ await handler(req, res);
170
+ return res;
171
+ }
172
+
173
+ let passed = 0;
174
+ let failed = 0;
175
+ async function run(name, fn) {
176
+ try {
177
+ await fn();
178
+ console.log(` ✓ ${name}`);
179
+ passed++;
180
+ } catch (e) {
181
+ console.log(` ✗ ${name}`);
182
+ console.log(` ${e.message}`);
183
+ failed++;
184
+ }
185
+ }
186
+
187
+ (async () => {
188
+ console.log('退回接口滑步回溯(return after skip)');
189
+
190
+ await run('上一步被滑步跳过 → 回溯退回到实际处理过的步骤,不报 500', async () => {
191
+ stepTypes = {};
192
+ fakeInstance = buildInstance();
193
+ const res = await callReturn();
194
+ assert.strictEqual(res.statusCode, 200, '应返回 200,实际 ' + res.statusCode + ':' + (res.body && res.body.msg));
195
+ assert.strictEqual(res.body.status, 0);
196
+ // 新 trace 落在实际处理过的 step_real,而不是被跳过的 step_skipped
197
+ const pushCall = instanceUpdateCalls.find(c => c.modifier.$push && c.modifier.$push.traces);
198
+ assert.ok(pushCall, '未创建退回目标新 trace');
199
+ const newTrace = pushCall.modifier.$push.traces;
200
+ assert.strictEqual(newTrace.step, 'step_real');
201
+ assert.strictEqual(newTrace.approves[0].user, 'user_a');
202
+ // 退回后的待办人是实际处理人
203
+ const setCall = instanceUpdateCalls.find(c => c.modifier.$set && c.modifier.$set.inbox_users);
204
+ assert.deepStrictEqual(setCall.modifier.$set.inbox_users, ['user_a']);
205
+ });
206
+
207
+ await run('会签拦截作用于回溯后的真实目标步骤', async () => {
208
+ stepTypes = { step_real: 'counterSign' };
209
+ fakeInstance = buildInstance();
210
+ const res = await callReturn();
211
+ assert.strictEqual(res.statusCode, 500);
212
+ assert.ok(/会签/.test(res.body.msg), '应报会签不允许退回,实际:' + res.body.msg);
213
+ });
214
+
215
+ await run('正常(无滑步)退回行为不变', async () => {
216
+ stepTypes = {};
217
+ fakeInstance = buildInstance({ noSkip: true });
218
+ const res = await callReturn();
219
+ assert.strictEqual(res.statusCode, 200, '应返回 200,实际 ' + res.statusCode + ':' + (res.body && res.body.msg));
220
+ const pushCall = instanceUpdateCalls.find(c => c.modifier.$push && c.modifier.$push.traces);
221
+ assert.strictEqual(pushCall.modifier.$push.traces.step, 'step_real');
222
+ });
223
+
224
+ console.log(`\n${passed} passed, ${failed} failed`);
225
+ process.exit(failed > 0 ? 1 : 0);
226
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steedos-labs/plugin-workflow",
3
- "version": "3.0.101",
3
+ "version": "3.0.103",
4
4
  "main": "package.service.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -26,6 +26,8 @@
26
26
  "test:formula-compat": "node main/default/test/test_formula_compat.js",
27
27
  "test:approve-values": "node main/default/test/test_getApproveValues.js",
28
28
  "test:skip-handler-error": "node main/default/test/test_handleSkipProcessed_handlerError.js",
29
+ "test:return-after-skip": "node main/default/test/test_return_after_skip.js",
30
+ "test:next-step-users-cc": "node main/default/test/test_next_step_users_value_cc.js",
29
31
  "test:flow-validator": "node main/default/test/test_flow_validator.js",
30
32
  "test:ajax-error-message": "node main/default/test/test_ajax_error_message.js",
31
33
  "test:webhook-error-message": "node main/default/test/test_webhook_error_message.js"