testbeats 2.2.4 → 2.2.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testbeats",
3
- "version": "2.2.4",
3
+ "version": "2.2.6",
4
4
  "description": "Publish test results to Microsoft Teams, Google Chat, Slack and InfluxDB",
5
5
  "main": "src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -173,21 +173,40 @@ class PublishCommand {
173
173
  }
174
174
  }
175
175
  if (target.inputs) {
176
- const inputs = target.inputs;
177
- if (target.name === 'slack' || target.name === 'teams' || target.name === 'chat') {
178
- if (!inputs.url) {
179
- throw new Error(`missing url in ${target.name} target inputs`);
180
- }
181
- if (typeof inputs.url !== 'string') {
182
- throw new Error(`url in ${target.name} target inputs must be a string`);
183
- }
184
- if (!inputs.url.startsWith('http')) {
185
- throw new Error(`url in ${target.name} target inputs must start with 'http' or 'https'`);
176
+ this.#validateURL(target);
177
+ }
178
+ }
179
+ logger.debug("Validating targets - Successful!")
180
+ }
181
+
182
+ #validateURL(target) {
183
+ const inputs = target.inputs;
184
+ if (target.name === 'slack' || target.name === 'teams' || target.name === 'chat') {
185
+ if (inputs.token) {
186
+ if (!Array.isArray(inputs.channels)) {
187
+ throw new Error(`channels in ${target.name} target inputs must be an array`);
188
+ }
189
+ if (!inputs.channels.length) {
190
+ throw new Error(`at least one channel must be defined in ${target.name} target inputs`);
191
+ }
192
+ for (const channel of inputs.channels) {
193
+ if (typeof channel !== 'string') {
194
+ throw new Error(`channel in ${target.name} target inputs must be a string`);
186
195
  }
187
196
  }
197
+ return;
198
+ }
199
+
200
+ if (!inputs.url) {
201
+ throw new Error(`missing url in ${target.name} target inputs`);
202
+ }
203
+ if (typeof inputs.url !== 'string') {
204
+ throw new Error(`url in ${target.name} target inputs must be a string`);
205
+ }
206
+ if (!inputs.url.startsWith('http')) {
207
+ throw new Error(`url in ${target.name} target inputs must start with 'http' or 'https'`);
188
208
  }
189
209
  }
190
- logger.debug("Validating targets - Successful!")
191
210
  }
192
211
 
193
212
  #processResults() {
@@ -36,7 +36,7 @@ class AIFailureSummaryExtension extends BaseExtension {
36
36
  * @type {import('../beats/beats.types').IBeatExecutionMetric}
37
37
  */
38
38
  const execution_metrics = data.execution_metrics[0];
39
- this.text = execution_metrics.failure_summary;
39
+ this.text = this.platform.code(execution_metrics.failure_summary);
40
40
  }
41
41
  }
42
42
 
@@ -1,6 +1,7 @@
1
1
  const TestResult = require('test-results-parser/src/models/TestResult');
2
2
  const logger = require('../utils/logger');
3
- const { addChatExtension, addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper');
3
+ const { addChatExtension, addSlackExtension, addTeamsExtension, addGithubExtension } = require('../helpers/extension.helper');
4
+ const { getPlatform } = require('../platforms');
4
5
 
5
6
  class BaseExtension {
6
7
 
@@ -32,6 +33,11 @@ class BaseExtension {
32
33
  * @type {import('..').IExtensionDefaultOptions}
33
34
  */
34
35
  this.default_options = {};
36
+
37
+ /**
38
+ * @type {import('../platforms/base.platform').BasePlatform}
39
+ */
40
+ this.platform = getPlatform(this.target.name);
35
41
  }
36
42
 
37
43
  updateExtensionInputs() {
@@ -46,6 +52,9 @@ class BaseExtension {
46
52
  case 'chat':
47
53
  this.extension.inputs = Object.assign({}, { separator: true }, this.extension.inputs);
48
54
  break;
55
+ case 'github':
56
+ this.extension.inputs = Object.assign({}, { separator: true }, this.extension.inputs);
57
+ break;
49
58
  default:
50
59
  break;
51
60
  }
@@ -67,6 +76,9 @@ class BaseExtension {
67
76
  case 'chat':
68
77
  addChatExtension({ payload: this.payload, extension: this.extension, text: this.text });
69
78
  break;
79
+ case 'github':
80
+ addGithubExtension({ payload: this.payload, extension: this.extension, text: this.text });
81
+ break;
70
82
  default:
71
83
  break;
72
84
  }
@@ -84,6 +96,8 @@ class BaseExtension {
84
96
  return _texts.join('\n');
85
97
  case 'chat':
86
98
  return _texts.join('<br>');
99
+ case 'github':
100
+ return _texts.join('\n');
87
101
  default:
88
102
  break;
89
103
  }
@@ -100,6 +114,8 @@ class BaseExtension {
100
114
  return `*${text}*`;
101
115
  case 'chat':
102
116
  return `<b>${text}</b>`;
117
+ case 'github':
118
+ return `**${text}**`;
103
119
  default:
104
120
  break;
105
121
  }
@@ -38,7 +38,7 @@ class ErrorClustersExtension extends BaseExtension {
38
38
  for (const cluster of clusters) {
39
39
  texts.push(`${truncate(cluster.failure, 150)} - ${this.bold(`(x${cluster.count})`)}`);
40
40
  }
41
- this.text = this.mergeTexts(texts);
41
+ this.text = this.platform.bullets(texts);
42
42
  }
43
43
  }
44
44
 
@@ -18,6 +18,7 @@ const TARGET = Object.freeze({
18
18
  CUSTOM: 'custom',
19
19
  DELAY: 'delay',
20
20
  INFLUX: 'influx',
21
+ HTTP: 'http',
21
22
  });
22
23
 
23
24
  const EXTENSION = Object.freeze({
@@ -100,6 +100,15 @@ function addChatExtension({ payload, extension, text }) {
100
100
  });
101
101
  }
102
102
 
103
+ function addGithubExtension({ payload, extension, text }) {
104
+ if (extension.inputs.title) {
105
+ const title = extension.inputs.title_link ? `[${extension.inputs.title}](${extension.inputs.title_link})` : extension.inputs.title;
106
+ payload.content.push(`**${title}**\n${text}\n\n`);
107
+ } else {
108
+ payload.content.push(`${text}\n\n`);
109
+ }
110
+ }
111
+
103
112
  /**
104
113
  * Sort extensions by their order property.
105
114
  * Extensions without order will appear last, maintaining their original relative order.
@@ -123,5 +132,6 @@ module.exports = {
123
132
  addSlackExtension,
124
133
  addTeamsExtension,
125
134
  addChatExtension,
135
+ addGithubExtension,
126
136
  sortExtensionsByOrder
127
137
  }
package/src/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export interface ITarget {
10
10
  name: TargetName;
11
11
  enable?: string | boolean;
12
12
  condition?: Condition;
13
- inputs?: SlackInputs | TeamsInputs | ChatInputs | GitHubInputs | CustomTargetInputs | InfluxDBTargetInputs;
13
+ inputs?: SlackInputs | TeamsInputs | ChatInputs | GitHubInputs | ICustomTargetInputs | InfluxDBTargetInputs;
14
14
  extensions?: IExtension[];
15
15
  }
16
16
 
@@ -234,6 +234,8 @@ export interface TargetInputs {
234
234
 
235
235
  export interface SlackInputs extends TargetInputs {
236
236
  message_format?: 'blocks' | 'attachments';
237
+ token?: string;
238
+ channels?: string[];
237
239
  }
238
240
 
239
241
  export interface TeamsInputs extends TargetInputs {
@@ -275,11 +277,19 @@ export interface CustomTargetFunctionContext {
275
277
 
276
278
  export type CustomTargetFunction = (ctx: CustomTargetFunctionContext) => void | Promise<void>;
277
279
 
278
- export interface CustomTargetInputs {
280
+ export interface ICustomTargetInputs {
279
281
  load: string | CustomTargetFunction;
280
282
  }
281
283
 
284
+ export interface IDelayTargetInputs {
285
+ seconds: number;
286
+ }
282
287
 
288
+ export interface IHttpTargetInputs {
289
+ url: string;
290
+ method: string;
291
+ headers: object;
292
+ }
283
293
 
284
294
  export interface CustomResultOptions {
285
295
  type: string;
@@ -337,5 +347,9 @@ export type IExtensionDefaultOptions = {
337
347
  condition: Condition
338
348
  }
339
349
 
350
+ export type ITargetDefaultOptions = {
351
+ condition: Condition
352
+ }
353
+
340
354
  export function publish(options: PublishOptions): Promise<any>
341
355
  export function defineConfig(config: PublishConfig): PublishConfig
@@ -6,13 +6,41 @@ class BasePlatform {
6
6
  * @param {string|number} text
7
7
  */
8
8
  bold(text) {
9
- throw new Error('Not Implemented');
9
+ return `**${text}**`;
10
10
  }
11
11
 
12
12
  break() {
13
- throw new Error('Not Implemented');
13
+ return '\n';
14
14
  }
15
15
 
16
+ /**
17
+ * @param {string[]} values
18
+ */
19
+ merge(values) {
20
+ return this.getValidTexts(values).join(this.break());
21
+ }
22
+
23
+ /**
24
+ * @param {string[]} items - Array of strings to convert to bullet points
25
+ * @returns {string} - Formatted bullet points as a string
26
+ */
27
+ bullets(items) {
28
+ if (!items || !Array.isArray(items) || items.length === 0) {
29
+ return '';
30
+ }
31
+ return this.merge(items.map(item => `- ${item}`));
32
+ }
33
+
34
+ /**
35
+ * @param {string} text
36
+ * @returns {string}
37
+ */
38
+ code(text) {
39
+ return text;
40
+ }
41
+
42
+
43
+
16
44
  /**
17
45
  *
18
46
  * @param {import('..').ITarget} target
@@ -123,6 +151,14 @@ class BasePlatform {
123
151
 
124
152
  return texts.join(' • ');
125
153
  }
154
+
155
+ /**
156
+ * @param {string[]} texts
157
+ * @returns {string[]}
158
+ */
159
+ getValidTexts(texts) {
160
+ return texts.filter(text => !!text);
161
+ }
126
162
  }
127
163
 
128
164
  module.exports = { BasePlatform }
@@ -13,6 +13,17 @@ class ChatPlatform extends BasePlatform {
13
13
  return '<br>';
14
14
  }
15
15
 
16
+ /**
17
+ * @param {string[]} items - Array of strings to convert to bullet points
18
+ * @returns {string} - Formatted bullet points as a string
19
+ */
20
+ bullets(items) {
21
+ if (!items || !Array.isArray(items) || items.length === 0) {
22
+ return '';
23
+ }
24
+ return this.merge(items.map(item => `• ${item}`));
25
+ }
26
+
16
27
  }
17
28
 
18
29
  module.exports = { ChatPlatform }
@@ -2,17 +2,6 @@ const { BasePlatform } = require('./base.platform');
2
2
 
3
3
  class GitHubPlatform extends BasePlatform {
4
4
 
5
- /**
6
- * @param {string|number} text
7
- */
8
- bold(text) {
9
- return `**${text}**`;
10
- }
11
-
12
- break() {
13
- return '\n';
14
- }
15
-
16
5
  }
17
6
 
18
7
  module.exports = { GitHubPlatform };
@@ -3,6 +3,7 @@ const { SlackPlatform } = require('./slack.platform');
3
3
  const { TeamsPlatform } = require('./teams.platform');
4
4
  const { ChatPlatform } = require('./chat.platform');
5
5
  const { GitHubPlatform } = require('./github.platform');
6
+ const { BasePlatform } = require('./base.platform');
6
7
 
7
8
  /**
8
9
  *
@@ -19,7 +20,7 @@ function getPlatform(name) {
19
20
  case TARGET.GITHUB:
20
21
  return new GitHubPlatform();
21
22
  default:
22
- throw new Error('Invalid Platform');
23
+ return new BasePlatform();
23
24
  }
24
25
  }
25
26
 
@@ -9,8 +9,19 @@ class SlackPlatform extends BasePlatform {
9
9
  return `*${text}*`;
10
10
  }
11
11
 
12
- break() {
13
- return '\n';
12
+ /**
13
+ * @param {string[]} items - Array of strings to convert to bullet points
14
+ * @returns {string} - Formatted bullet points as a string
15
+ */
16
+ bullets(items) {
17
+ if (!items || !Array.isArray(items) || items.length === 0) {
18
+ return '';
19
+ }
20
+ return this.merge(items.map(item => `• ${item}`));
21
+ }
22
+
23
+ code(text) {
24
+ return `\`\`\`${text}\`\`\``;
14
25
  }
15
26
  }
16
27
 
@@ -1,16 +1,21 @@
1
1
  const { BasePlatform } = require("./base.platform");
2
2
 
3
3
  class TeamsPlatform extends BasePlatform {
4
- /**
5
- * @param {string|number} text
6
- */
7
- bold(text) {
8
- return `**${text}**`;
9
- }
10
4
 
11
5
  break() {
12
6
  return '\n\n';
13
7
  }
8
+
9
+ /**
10
+ * @param {string[]} items - Array of strings to convert to bullet points
11
+ * @returns {string} - Formatted bullet points as a string
12
+ */
13
+ bullets(items) {
14
+ if (!items || !Array.isArray(items) || items.length === 0) {
15
+ return '';
16
+ }
17
+ return this.merge(items.map(item => `- ${item}`));
18
+ }
14
19
  }
15
20
 
16
21
  module.exports = { TeamsPlatform }
@@ -0,0 +1,45 @@
1
+ const { getPlatform } = require('../platforms');
2
+ const { STATUS } = require('../helpers/constants');
3
+
4
+ class BaseTarget {
5
+
6
+ constructor({ target }) {
7
+
8
+ /**
9
+ * @type {import('../index').ITarget}
10
+ */
11
+ this.target = target;
12
+
13
+ /**
14
+ * @type {string}
15
+ */
16
+ this.name = target.name;
17
+
18
+ /**
19
+ * @type {string | boolean}
20
+ */
21
+ this.enable = target.enable;
22
+
23
+ /**
24
+ * @type {import('../index').Condition}
25
+ */
26
+ this.condition = target.condition || STATUS.PASS_OR_FAIL;
27
+
28
+ /**
29
+ * @type {import('../index').IExtension[]}
30
+ */
31
+ this.extensions = target.extensions || [];
32
+
33
+ /**
34
+ * @type {import('../platforms/base.platform').BasePlatform}
35
+ */
36
+ this.platform = getPlatform(this.name);
37
+ }
38
+
39
+ async run({ result }) {
40
+ // throw new Error('Not implemented');
41
+ }
42
+
43
+ }
44
+
45
+ module.exports = { BaseTarget};
@@ -0,0 +1,31 @@
1
+ const { BaseTarget } = require('./base.target');
2
+ const path = require('path');
3
+
4
+ const DEFAULT_INPUTS = {};
5
+
6
+ class CustomTarget extends BaseTarget {
7
+
8
+ constructor({ target }) {
9
+ super({ target });
10
+
11
+ /**
12
+ * @type {import('../index').ICustomTargetInputs}
13
+ */
14
+ this.inputs = Object.assign({}, DEFAULT_INPUTS, target.inputs);
15
+ }
16
+
17
+ async run({ result }) {
18
+ if (typeof this.inputs.load === 'string') {
19
+ const cwd = process.cwd();
20
+ const target_runner = require(path.join(cwd, this.inputs.load));
21
+ await target_runner.run({ target: this.target, result });
22
+ } else if (typeof this.inputs.load === 'function') {
23
+ await this.inputs.load({ target: this.target, result });
24
+ } else {
25
+ throw `Invalid 'load' input in custom target - ${this.inputs.load}`;
26
+ }
27
+ }
28
+
29
+ }
30
+
31
+ module.exports = { CustomTarget };
@@ -0,0 +1,24 @@
1
+ const { BaseTarget } = require('./base.target');
2
+
3
+ const DEFAULT_INPUTS = {
4
+ seconds: 5
5
+ };
6
+
7
+ class DelayTarget extends BaseTarget {
8
+
9
+ constructor({ target }) {
10
+ super({ target });
11
+
12
+ /**
13
+ * @type {import('../index').IDelayTargetInputs}
14
+ */
15
+ this.inputs = Object.assign({}, DEFAULT_INPUTS, target.inputs);
16
+ }
17
+
18
+ async run() {
19
+ await new Promise(resolve => setTimeout(resolve, this.inputs.seconds * 1000));
20
+ }
21
+
22
+ }
23
+
24
+ module.exports = { DelayTarget };
@@ -136,7 +136,7 @@ async function publishToGitHub({ target, message }) {
136
136
  const token = target.inputs.token || process.env.GITHUB_TOKEN;
137
137
 
138
138
  if (!token) {
139
- throw new Error('GitHub token is required. Set GITHUB_TOKEN environment variable or provide github_token in target inputs.');
139
+ throw new Error('GitHub token is required. Set GITHUB_TOKEN environment variable or provide token in target inputs.');
140
140
  }
141
141
 
142
142
  if (!pull_number) {
@@ -155,7 +155,7 @@ async function publishToGitHub({ target, message }) {
155
155
 
156
156
  if (target.inputs.update_comment) {
157
157
  // Try to find existing comment and update it
158
- const existingComment = await findExistingComment({ owner, repo, pull_number, github_token: token, comment_title: target.inputs.comment_title });
158
+ const existingComment = await findExistingComment({ owner, repo, pull_number, token, comment_title: target.inputs.comment_title });
159
159
  if (existingComment) {
160
160
  return request.patch({
161
161
  url: `${url}/repos/${owner}/${repo}/issues/comments/${existingComment.id}`,
@@ -173,13 +173,13 @@ async function publishToGitHub({ target, message }) {
173
173
  });
174
174
  }
175
175
 
176
- async function findExistingComment({ owner, repo, pull_number, github_token, comment_title }) {
176
+ async function findExistingComment({ owner, repo, pull_number, token, comment_title }) {
177
177
  if (!comment_title) return null;
178
178
 
179
179
  try {
180
180
  const url = `https://api.github.com/repos/${owner}/${repo}/issues/${pull_number}/comments`;
181
181
  const headers = {
182
- 'Authorization': `token ${github_token}`,
182
+ 'Authorization': `token ${token}`,
183
183
  'Accept': 'application/vnd.github.v3+json',
184
184
  'User-Agent': 'testbeats'
185
185
  };
@@ -280,7 +280,7 @@ async function handleErrors({ target, errors }) {
280
280
  }
281
281
 
282
282
  const default_inputs = {
283
- github_token: undefined,
283
+ token: undefined,
284
284
  comment_title: undefined,
285
285
  update_comment: false,
286
286
  owner: undefined,
@@ -0,0 +1,36 @@
1
+ const { BaseTarget } = require('./base.target');
2
+ const request = require('phin-retry');
3
+
4
+ const DEFAULT_INPUTS = {
5
+ url: '',
6
+ method: 'POST',
7
+ headers: {}
8
+ };
9
+
10
+ class HttpTarget extends BaseTarget {
11
+
12
+ constructor({ target }) {
13
+ super({ target });
14
+
15
+ /**
16
+ * @type {import('../index').IHttpTargetInputs}
17
+ */
18
+ this.inputs = Object.assign({}, DEFAULT_INPUTS, target.inputs);
19
+ }
20
+
21
+ async run({ result }) {
22
+ const { url, method, headers } = this.inputs;
23
+ await request.__fetch({
24
+ url,
25
+ method,
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ ...headers
29
+ },
30
+ body: { result }
31
+ });
32
+ }
33
+
34
+ }
35
+
36
+ module.exports = { HttpTarget };
@@ -2,8 +2,9 @@ const teams = require('./teams');
2
2
  const slack = require('./slack');
3
3
  const chat = require('./chat');
4
4
  const github = require('./github');
5
- const custom = require('./custom');
6
- const delay = require('./delay');
5
+ const { CustomTarget } = require('./custom.target');
6
+ const { DelayTarget } = require('./delay.target');
7
+ const { HttpTarget } = require('./http.target');
7
8
  const influx = require('./influx');
8
9
  const { TARGET } = require('../helpers/constants');
9
10
  const { checkCondition } = require('../helpers/helper');
@@ -19,11 +20,13 @@ function getTargetRunner(target) {
19
20
  case TARGET.GITHUB:
20
21
  return github;
21
22
  case TARGET.CUSTOM:
22
- return custom;
23
+ return new CustomTarget({ target });
23
24
  case TARGET.DELAY:
24
- return delay;
25
+ return new DelayTarget({ target });
25
26
  case TARGET.INFLUX:
26
27
  return influx;
28
+ case TARGET.HTTP:
29
+ return new HttpTarget({ target });
27
30
  default:
28
31
  return require(target.name);
29
32
  }
@@ -31,8 +34,9 @@ function getTargetRunner(target) {
31
34
 
32
35
  async function run(target, result) {
33
36
  const target_runner = getTargetRunner(target);
34
- const target_options = Object.assign({}, target_runner.default_options, target);
35
- if (await checkCondition({ condition: target_options.condition, result, target })) {
37
+ // const target_options = Object.assign({}, target_runner.default_options, target);
38
+ const condition = target.condition || target_runner.default_options?.condition || target_runner.condition;
39
+ if (await checkCondition({ condition, result, target })) {
36
40
  await target_runner.run({result, target});
37
41
  }
38
42
  }
@@ -9,6 +9,8 @@ const { getValidMetrics, getMetricValuesText } = require('../helpers/performance
9
9
  const TestResult = require('test-results-parser/src/models/TestResult');
10
10
  const { getPlatform } = require('../platforms');
11
11
 
12
+ const SLACK_BASE_URL = 'https://slack.com';
13
+
12
14
  const STATUSES = {
13
15
  GOOD: ':white_check_mark:',
14
16
  WARNING: ':warning:',
@@ -31,10 +33,7 @@ async function run({ result, target }) {
31
33
  }
32
34
  const message = getRootPayload({ result, target, payload });
33
35
  logger.info(`🔔 Publishing results to Slack...`);
34
- return request.post({
35
- url: target.inputs.url,
36
- body: message
37
- });
36
+ return publish({ inputs: target.inputs, message });
38
37
  }
39
38
 
40
39
  async function setFunctionalPayload({ result, target, payload }) {
@@ -325,6 +324,28 @@ async function handleErrors({ target, errors }) {
325
324
  });
326
325
  }
327
326
 
327
+ async function publish({ inputs, message}) {
328
+ const { url, token, channels } = inputs;
329
+ if (token) {
330
+ for (let i = 0; i < channels.length; i++) {
331
+ message.channel = channels[i];
332
+ return request.post({
333
+ url: url ? url : `${SLACK_BASE_URL}/api/chat.postMessage`,
334
+ headers: {
335
+ 'Authorization': `Bearer ${token}`
336
+ },
337
+ body: message
338
+ });
339
+ }
340
+
341
+ } else {
342
+ return request.post({
343
+ url,
344
+ body: message
345
+ });
346
+ }
347
+ }
348
+
328
349
  module.exports = {
329
350
  run,
330
351
  handleErrors,
@@ -1,28 +0,0 @@
1
- const path = require('path');
2
- const { STATUS } = require('../helpers/constants');
3
-
4
- /**
5
- *
6
- * @param {object} param0
7
- * @param {import('../index').ITarget} param0.target
8
- */
9
- async function run({result, target}) {
10
- if (typeof target.inputs.load === 'string') {
11
- const cwd = process.cwd();
12
- const target_runner = require(path.join(cwd, target.inputs.load));
13
- await target_runner.run({ target, result });
14
- } else if (typeof target.inputs.load === 'function') {
15
- await target.inputs.load({ target, result });
16
- } else {
17
- throw `Invalid 'load' input in custom target - ${target.inputs.load}`;
18
- }
19
- }
20
-
21
- const default_options = {
22
- condition: STATUS.PASS_OR_FAIL
23
- }
24
-
25
- module.exports = {
26
- run,
27
- default_options
28
- }
@@ -1,19 +0,0 @@
1
- const { STATUS } = require("../helpers/constants");
2
-
3
- async function run({ target }) {
4
- target.inputs = Object.assign({}, default_inputs, target.inputs);
5
- await new Promise(resolve => setTimeout(resolve, target.inputs.seconds * 1000));
6
- }
7
-
8
- const default_options = {
9
- condition: STATUS.PASS_OR_FAIL
10
- }
11
-
12
- const default_inputs = {
13
- seconds: 5
14
- }
15
-
16
- module.exports = {
17
- run,
18
- default_options
19
- }