codeceptjs 3.6.4-beta.2 → 3.6.5-beta.1

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.
Files changed (92) hide show
  1. package/bin/codecept.js +84 -63
  2. package/lib/ai.js +47 -1
  3. package/lib/assert/empty.js +19 -19
  4. package/lib/assert/equal.js +32 -30
  5. package/lib/assert/error.js +14 -14
  6. package/lib/assert/include.js +42 -42
  7. package/lib/assert/throws.js +13 -11
  8. package/lib/assert/truth.js +17 -18
  9. package/lib/command/configMigrate.js +57 -52
  10. package/lib/command/definitions.js +88 -88
  11. package/lib/command/dryRun.js +65 -63
  12. package/lib/command/generate.js +191 -181
  13. package/lib/command/info.js +39 -37
  14. package/lib/command/init.js +289 -286
  15. package/lib/command/interactive.js +32 -32
  16. package/lib/command/list.js +26 -26
  17. package/lib/command/run-multiple.js +113 -93
  18. package/lib/command/run-rerun.js +22 -22
  19. package/lib/command/run-workers.js +63 -63
  20. package/lib/command/run.js +24 -26
  21. package/lib/command/utils.js +64 -63
  22. package/lib/data/context.js +60 -60
  23. package/lib/data/dataScenarioConfig.js +47 -47
  24. package/lib/data/dataTableArgument.js +29 -29
  25. package/lib/data/table.js +26 -20
  26. package/lib/helper/AI.js +114 -35
  27. package/lib/helper/ApiDataFactory.js +72 -69
  28. package/lib/helper/Appium.js +409 -379
  29. package/lib/helper/ExpectHelper.js +214 -248
  30. package/lib/helper/FileSystem.js +77 -78
  31. package/lib/helper/GraphQL.js +44 -43
  32. package/lib/helper/GraphQLDataFactory.js +49 -50
  33. package/lib/helper/JSONResponse.js +64 -62
  34. package/lib/helper/Mochawesome.js +28 -28
  35. package/lib/helper/MockServer.js +12 -12
  36. package/lib/helper/Nightmare.js +664 -572
  37. package/lib/helper/Playwright.js +1320 -1211
  38. package/lib/helper/Protractor.js +663 -629
  39. package/lib/helper/Puppeteer.js +1232 -1124
  40. package/lib/helper/REST.js +115 -69
  41. package/lib/helper/TestCafe.js +490 -491
  42. package/lib/helper/WebDriver.js +1294 -1156
  43. package/lib/history.js +16 -3
  44. package/lib/interfaces/bdd.js +38 -51
  45. package/lib/interfaces/featureConfig.js +19 -19
  46. package/lib/interfaces/gherkin.js +122 -111
  47. package/lib/interfaces/scenarioConfig.js +29 -29
  48. package/lib/listener/artifacts.js +9 -9
  49. package/lib/listener/config.js +24 -23
  50. package/lib/listener/exit.js +12 -12
  51. package/lib/listener/helpers.js +42 -42
  52. package/lib/listener/mocha.js +11 -11
  53. package/lib/listener/retry.js +32 -30
  54. package/lib/listener/steps.js +50 -51
  55. package/lib/listener/timeout.js +53 -53
  56. package/lib/pause.js +17 -3
  57. package/lib/plugin/allure.js +14 -14
  58. package/lib/plugin/autoDelay.js +29 -36
  59. package/lib/plugin/autoLogin.js +70 -66
  60. package/lib/plugin/commentStep.js +18 -18
  61. package/lib/plugin/coverage.js +92 -77
  62. package/lib/plugin/customLocator.js +20 -19
  63. package/lib/plugin/debugErrors.js +24 -24
  64. package/lib/plugin/eachElement.js +37 -37
  65. package/lib/plugin/fakerTransform.js +6 -6
  66. package/lib/plugin/heal.js +66 -63
  67. package/lib/plugin/pauseOnFail.js +10 -10
  68. package/lib/plugin/retryFailedStep.js +31 -38
  69. package/lib/plugin/retryTo.js +28 -28
  70. package/lib/plugin/screenshotOnFail.js +107 -86
  71. package/lib/plugin/selenoid.js +131 -117
  72. package/lib/plugin/standardActingHelpers.js +2 -8
  73. package/lib/plugin/stepByStepReport.js +102 -92
  74. package/lib/plugin/stepTimeout.js +23 -22
  75. package/lib/plugin/subtitles.js +34 -34
  76. package/lib/plugin/tryTo.js +39 -29
  77. package/lib/plugin/wdio.js +77 -72
  78. package/lib/template/heal.js +11 -14
  79. package/package.json +5 -3
  80. package/translations/de-DE.js +1 -1
  81. package/translations/fr-FR.js +1 -1
  82. package/translations/index.js +9 -9
  83. package/translations/it-IT.js +1 -1
  84. package/translations/ja-JP.js +1 -1
  85. package/translations/pl-PL.js +1 -1
  86. package/translations/pt-BR.js +1 -1
  87. package/translations/ru-RU.js +1 -1
  88. package/translations/zh-CN.js +1 -1
  89. package/translations/zh-TW.js +1 -1
  90. package/typings/index.d.ts +42 -19
  91. package/typings/promiseBasedTypes.d.ts +280 -1
  92. package/typings/types.d.ts +76 -1
@@ -1,17 +1,17 @@
1
- const debugModule = require('debug');
2
- const { CoverageReport } = require('monocart-coverage-reports');
3
- const Container = require('../container');
4
- const recorder = require('../recorder');
5
- const event = require('../event');
6
- const output = require('../output');
7
- const { deepMerge } = require('../utils');
1
+ const debugModule = require('debug')
2
+ const { CoverageReport } = require('monocart-coverage-reports')
3
+ const Container = require('../container')
4
+ const recorder = require('../recorder')
5
+ const event = require('../event')
6
+ const output = require('../output')
7
+ const { deepMerge } = require('../utils')
8
8
 
9
9
  const defaultConfig = {
10
10
  name: 'CodeceptJS Coverage Report',
11
11
  outputDir: 'output/coverage',
12
- };
12
+ }
13
13
 
14
- const supportedHelpers = ['Puppeteer', 'Playwright', 'WebDriver'];
14
+ const supportedHelpers = ['Puppeteer', 'Playwright', 'WebDriver']
15
15
 
16
16
  const v8CoverageHelpers = {
17
17
  Playwright: {
@@ -23,15 +23,15 @@ const v8CoverageHelpers = {
23
23
  page.coverage.startCSSCoverage({
24
24
  resetOnNavigation: false,
25
25
  }),
26
- ]);
26
+ ])
27
27
  },
28
28
  takeCoverage: async (page, coverageReport) => {
29
29
  const [jsCoverage, cssCoverage] = await Promise.all([
30
30
  page.coverage.stopJSCoverage(),
31
31
  page.coverage.stopCSSCoverage(),
32
- ]);
33
- const coverageList = [...jsCoverage, ...cssCoverage];
34
- await coverageReport.add(coverageList);
32
+ ])
33
+ const coverageList = [...jsCoverage, ...cssCoverage]
34
+ await coverageReport.add(coverageList)
35
35
  },
36
36
  },
37
37
  Puppeteer: {
@@ -44,21 +44,24 @@ const v8CoverageHelpers = {
44
44
  page.coverage.startCSSCoverage({
45
45
  resetOnNavigation: false,
46
46
  }),
47
- ]);
47
+ ])
48
48
  },
49
49
  takeCoverage: async (page, coverageReport) => {
50
50
  const [jsCoverage, cssCoverage] = await Promise.all([
51
51
  page.coverage.stopJSCoverage(),
52
52
  page.coverage.stopCSSCoverage(),
53
- ]);
53
+ ])
54
54
  // to raw V8 script coverage
55
- const coverageList = [...jsCoverage.map((it) => {
56
- return {
57
- source: it.text,
58
- ...it.rawScriptCoverage,
59
- };
60
- }), ...cssCoverage];
61
- await coverageReport.add(coverageList);
55
+ const coverageList = [
56
+ ...jsCoverage.map((it) => {
57
+ return {
58
+ source: it.text,
59
+ ...it.rawScriptCoverage,
60
+ }
61
+ }),
62
+ ...cssCoverage,
63
+ ]
64
+ await coverageReport.add(coverageList)
62
65
  },
63
66
  },
64
67
  WebDriver: {
@@ -71,24 +74,27 @@ const v8CoverageHelpers = {
71
74
  page.coverage.startCSSCoverage({
72
75
  resetOnNavigation: false,
73
76
  }),
74
- ]);
77
+ ])
75
78
  },
76
79
  takeCoverage: async (page, coverageReport) => {
77
80
  const [jsCoverage, cssCoverage] = await Promise.all([
78
81
  page.coverage.stopJSCoverage(),
79
82
  page.coverage.stopCSSCoverage(),
80
- ]);
83
+ ])
81
84
  // to raw V8 script coverage
82
- const coverageList = [...jsCoverage.map((it) => {
83
- return {
84
- source: it.text,
85
- ...it.rawScriptCoverage,
86
- };
87
- }), ...cssCoverage];
88
- await coverageReport.add(coverageList);
85
+ const coverageList = [
86
+ ...jsCoverage.map((it) => {
87
+ return {
88
+ source: it.text,
89
+ ...it.rawScriptCoverage,
90
+ }
91
+ }),
92
+ ...cssCoverage,
93
+ ]
94
+ await coverageReport.add(coverageList)
89
95
  },
90
96
  },
91
- };
97
+ }
92
98
 
93
99
  /**
94
100
  * Dumps code coverage from Playwright/Puppeteer after every test.
@@ -117,72 +123,81 @@ const v8CoverageHelpers = {
117
123
  *
118
124
  */
119
125
  module.exports = function (config) {
120
- config = deepMerge(defaultConfig, config);
126
+ config = deepMerge(defaultConfig, config)
121
127
 
122
- if (config.debug) config.logging = 'debug';
128
+ if (config.debug) config.logging = 'debug'
123
129
 
124
- const helpers = Container.helpers();
125
- let coverageRunning = false;
130
+ const helpers = Container.helpers()
131
+ let coverageRunning = false
126
132
 
127
- const v8Names = Object.keys(v8CoverageHelpers);
128
- const helperName = Object.keys(helpers).find((it) => v8Names.includes(it));
133
+ const v8Names = Object.keys(v8CoverageHelpers)
134
+ const helperName = Object.keys(helpers).find((it) => v8Names.includes(it))
129
135
  if (!helperName) {
130
- console.error(`Coverage is only supported in ${supportedHelpers.join(' or ')}`);
136
+ console.error(`Coverage is only supported in ${supportedHelpers.join(' or ')}`)
131
137
  // no helpers for screenshot
132
- return;
138
+ return
133
139
  }
134
140
 
135
- config.name = `${config.name} - in ${helperName}`;
136
- const debug = debugModule(`codeceptjs:plugin:${helperName.toLowerCase()}Coverage`);
141
+ config.name = `${config.name} - in ${helperName}`
142
+ const debug = debugModule(`codeceptjs:plugin:${helperName.toLowerCase()}Coverage`)
137
143
 
138
- const helper = helpers[helperName];
144
+ const helper = helpers[helperName]
139
145
 
140
- if (helperName === 'WebDriver' && !helper.config.devtoolsProtocol) throw Error('Coverage is currently supporting the WebDriver running with Devtools protocol.');
146
+ if (helperName === 'WebDriver' && !helper.config.devtoolsProtocol)
147
+ throw Error('Coverage is currently supporting the WebDriver running with Devtools protocol.')
141
148
 
142
- const v8Helper = v8CoverageHelpers[helperName];
149
+ const v8Helper = v8CoverageHelpers[helperName]
143
150
 
144
151
  const coverageOptions = {
145
152
  ...config,
146
- };
153
+ }
147
154
 
148
- if (helperName === 'WebDriver') coverageOptions.coverageProvider = 'v8';
155
+ if (helperName === 'WebDriver') coverageOptions.coverageProvider = 'v8'
149
156
 
150
- const coverageReport = new CoverageReport(coverageOptions);
151
- coverageReport.cleanCache();
157
+ const coverageReport = new CoverageReport(coverageOptions)
158
+ coverageReport.cleanCache()
152
159
 
153
160
  event.dispatcher.on(event.all.after, async () => {
154
- output.print(`writing ${coverageOptions.outputDir}`);
155
- await coverageReport.generate();
156
- });
161
+ output.print(`writing ${coverageOptions.outputDir}`)
162
+ await coverageReport.generate()
163
+ })
157
164
 
158
165
  // we're going to try to "start" coverage before each step because this is
159
166
  // when the browser is already up and is ready to start coverage.
160
167
  event.dispatcher.on(event.step.before, () => {
161
- recorder.add('start coverage', async () => {
162
- if (coverageRunning) {
163
- return;
164
- }
165
- if (!helper.page || !helper.page.coverage) {
166
- return;
167
- }
168
- coverageRunning = true;
169
- debug('--> starting coverage <--');
170
- await v8Helper.startCoverage(helper.page);
171
- }, true);
172
- });
168
+ recorder.add(
169
+ 'start coverage',
170
+ async () => {
171
+ if (coverageRunning) {
172
+ return
173
+ }
174
+ if (!helper.page || !helper.page.coverage) {
175
+ return
176
+ }
177
+ coverageRunning = true
178
+ debug('--> starting coverage <--')
179
+ await v8Helper.startCoverage(helper.page)
180
+ },
181
+ true,
182
+ )
183
+ })
173
184
 
174
185
  // Save coverage data after every test run
175
186
  event.dispatcher.on(event.test.after, (test) => {
176
- recorder.add('take coverage', async () => {
177
- if (!coverageRunning) {
178
- return;
179
- }
180
- if (!helper.page || !helper.page.coverage) {
181
- return;
182
- }
183
- coverageRunning = false;
184
- debug('--> stopping coverage <--');
185
- await v8Helper.takeCoverage(helper.page, coverageReport);
186
- }, true);
187
- });
188
- };
187
+ recorder.add(
188
+ 'take coverage',
189
+ async () => {
190
+ if (!coverageRunning) {
191
+ return
192
+ }
193
+ if (!helper.page || !helper.page.coverage) {
194
+ return
195
+ }
196
+ coverageRunning = false
197
+ debug('--> stopping coverage <--')
198
+ await v8Helper.takeCoverage(helper.page, coverageReport)
199
+ },
200
+ true,
201
+ )
202
+ })
203
+ }
@@ -1,12 +1,12 @@
1
- const Locator = require('../locator');
2
- const { xpathLocator } = require('../utils');
1
+ const Locator = require('../locator')
2
+ const { xpathLocator } = require('../utils')
3
3
 
4
4
  const defaultConfig = {
5
5
  prefix: '$',
6
6
  attribute: 'data-test-id',
7
7
  strategy: 'xpath',
8
8
  showActual: false,
9
- };
9
+ }
10
10
 
11
11
  /**
12
12
  * Creates a [custom locator](https://codecept.io/locators#custom-locators) by using special attributes in HTML.
@@ -112,33 +112,34 @@ const defaultConfig = {
112
112
  * ```
113
113
  */
114
114
  module.exports = (config) => {
115
- config = { ...defaultConfig, ...config };
115
+ config = { ...defaultConfig, ...config }
116
116
 
117
117
  Locator.addFilter((value, locatorObj) => {
118
- if (typeof value !== 'string') return;
119
- if (!value.startsWith(config.prefix)) return;
118
+ if (typeof value !== 'string') return
119
+ if (!value.startsWith(config.prefix)) return
120
120
 
121
- if (!['String', 'Array'].includes(config.attribute.constructor.name)) return;
121
+ if (!['String', 'Array'].includes(config.attribute.constructor.name)) return
122
122
 
123
- const val = value.substr(config.prefix.length);
123
+ const val = value.substr(config.prefix.length)
124
124
 
125
125
  if (config.strategy.toLowerCase() === 'xpath') {
126
- locatorObj.value = `.//*[${
127
- [].concat(config.attribute)
128
- .map((attr) => `@${attr}=${xpathLocator.literal(val)}`)
129
- .join(' or ')}]`;
130
- locatorObj.type = 'xpath';
126
+ locatorObj.value = `.//*[${[]
127
+ .concat(config.attribute)
128
+ .map((attr) => `@${attr}=${xpathLocator.literal(val)}`)
129
+ .join(' or ')}]`
130
+ locatorObj.type = 'xpath'
131
131
  }
132
132
 
133
133
  if (config.strategy.toLowerCase() === 'css') {
134
- locatorObj.value = [].concat(config.attribute)
134
+ locatorObj.value = []
135
+ .concat(config.attribute)
135
136
  .map((attr) => `[${attr}=${val}]`)
136
- .join(',');
137
- locatorObj.type = 'css';
137
+ .join(',')
138
+ locatorObj.type = 'css'
138
139
  }
139
140
 
140
141
  if (config.showActual) {
141
- locatorObj.output = locatorObj.value;
142
+ locatorObj.output = locatorObj.value
142
143
  }
143
- });
144
- };
144
+ })
145
+ }
@@ -1,13 +1,13 @@
1
- const Container = require('../container');
2
- const recorder = require('../recorder');
3
- const event = require('../event');
4
- const supportedHelpers = require('./standardActingHelpers');
5
- const { scanForErrorMessages } = require('../html');
6
- const { output } = require('..');
1
+ const Container = require('../container')
2
+ const recorder = require('../recorder')
3
+ const event = require('../event')
4
+ const supportedHelpers = require('./standardActingHelpers')
5
+ const { scanForErrorMessages } = require('../html')
6
+ const { output } = require('..')
7
7
 
8
8
  const defaultConfig = {
9
9
  errorClasses: ['error', 'warning', 'alert', 'danger'],
10
- };
10
+ }
11
11
 
12
12
  /**
13
13
  * Prints errors found in HTML code after each failed test.
@@ -30,38 +30,38 @@ const defaultConfig = {
30
30
  *
31
31
  */
32
32
  module.exports = function (config = {}) {
33
- const helpers = Container.helpers();
34
- let helper;
33
+ const helpers = Container.helpers()
34
+ let helper
35
35
 
36
- config = Object.assign(defaultConfig, config);
36
+ config = Object.assign(defaultConfig, config)
37
37
 
38
38
  for (const helperName of supportedHelpers) {
39
39
  if (Object.keys(helpers).indexOf(helperName) > -1) {
40
- helper = helpers[helperName];
40
+ helper = helpers[helperName]
41
41
  }
42
42
  }
43
43
 
44
- if (!helper) return; // no helpers for screenshot
44
+ if (!helper) return // no helpers for screenshot
45
45
 
46
46
  event.dispatcher.on(event.test.failed, (test) => {
47
47
  recorder.add('HTML snapshot failed test', async () => {
48
48
  try {
49
- const currentOutputLevel = output.level();
50
- output.level(0);
51
- const html = await helper.grabHTMLFrom('body');
52
- output.level(currentOutputLevel);
49
+ const currentOutputLevel = output.level()
50
+ output.level(0)
51
+ const html = await helper.grabHTMLFrom('body')
52
+ output.level(currentOutputLevel)
53
53
 
54
- if (!html) return;
54
+ if (!html) return
55
55
 
56
- const errors = scanForErrorMessages(html, config.errorClasses);
56
+ const errors = scanForErrorMessages(html, config.errorClasses)
57
57
  if (errors.length) {
58
- output.debug('Detected errors in HTML code');
59
- errors.forEach((error) => output.debug(error));
60
- test.artifacts.errors = errors;
58
+ output.debug('Detected errors in HTML code')
59
+ errors.forEach((error) => output.debug(error))
60
+ test.artifacts.errors = errors
61
61
  }
62
62
  } catch (err) {
63
63
  // not really needed
64
64
  }
65
- });
66
- });
67
- };
65
+ })
66
+ })
67
+ }
@@ -1,14 +1,14 @@
1
- const output = require('../output');
2
- const store = require('../store');
3
- const recorder = require('../recorder');
4
- const container = require('../container');
5
- const event = require('../event');
6
- const Step = require('../step');
7
- const { isAsyncFunction } = require('../utils');
1
+ const output = require('../output')
2
+ const store = require('../store')
3
+ const recorder = require('../recorder')
4
+ const container = require('../container')
5
+ const event = require('../event')
6
+ const Step = require('../step')
7
+ const { isAsyncFunction } = require('../utils')
8
8
 
9
9
  const defaultConfig = {
10
10
  registerGlobal: true,
11
- };
11
+ }
12
12
 
13
13
  /**
14
14
  * Provides `eachElement` global function to iterate over found elements to perform actions on them.
@@ -69,59 +69,59 @@ const defaultConfig = {
69
69
  * @return {Promise<*> | undefined}
70
70
  */
71
71
  function eachElement(purpose, locator, fn) {
72
- if (store.dryRun) return;
73
- const helpers = Object.values(container.helpers());
72
+ if (store.dryRun) return
73
+ const helpers = Object.values(container.helpers())
74
74
 
75
- const helper = helpers.filter(h => !!h._locate)[0];
75
+ const helper = helpers.filter((h) => !!h._locate)[0]
76
76
 
77
77
  if (!helper) {
78
- throw new Error('No helper enabled with _locate method with returns a list of elements.');
78
+ throw new Error('No helper enabled with _locate method with returns a list of elements.')
79
79
  }
80
80
 
81
81
  if (!isAsyncFunction(fn)) {
82
- throw new Error('Async function should be passed into each element');
82
+ throw new Error('Async function should be passed into each element')
83
83
  }
84
84
 
85
- const step = new Step(helper, `${purpose || 'each element'} within "${locator}"`);
86
- step.helperMethod = '_locate';
85
+ const step = new Step(helper, `${purpose || 'each element'} within "${locator}"`)
86
+ step.helperMethod = '_locate'
87
87
  // eachElement('select all users', 'locator', async (el) => {
88
- event.dispatcher.emit(event.step.before, step);
88
+ event.dispatcher.emit(event.step.before, step)
89
89
 
90
90
  return recorder.add('register each element wrapper', async () => {
91
- event.emit(event.step.started, step);
92
- const els = await helper._locate(locator);
93
- output.debug(`Found ${els.length} elements for each elements to iterate`);
91
+ event.emit(event.step.started, step)
92
+ const els = await helper._locate(locator)
93
+ output.debug(`Found ${els.length} elements for each elements to iterate`)
94
94
 
95
- const errs = [];
96
- let i = 0;
95
+ const errs = []
96
+ let i = 0
97
97
  for (const el of els) {
98
98
  try {
99
- await fn(el, i);
99
+ await fn(el, i)
100
100
  } catch (err) {
101
- output.error(`eachElement: failed operation on element #${i} ${el}`);
102
- errs.push(err);
101
+ output.error(`eachElement: failed operation on element #${i} ${el}`)
102
+ errs.push(err)
103
103
  }
104
- i++;
104
+ i++
105
105
  }
106
106
 
107
107
  if (errs.length) {
108
- event.dispatcher.emit(event.step.after, step);
109
- event.emit(event.step.failed, step, errs[0]);
110
- event.emit(event.step.finished, step);
111
- throw errs[0];
108
+ event.dispatcher.emit(event.step.after, step)
109
+ event.emit(event.step.failed, step, errs[0])
110
+ event.emit(event.step.finished, step)
111
+ throw errs[0]
112
112
  }
113
113
 
114
- event.dispatcher.emit(event.step.after, step);
115
- event.emit(event.step.passed, step, null);
116
- event.emit(event.step.finished, step);
117
- });
114
+ event.dispatcher.emit(event.step.after, step)
115
+ event.emit(event.step.passed, step, null)
116
+ event.emit(event.step.finished, step)
117
+ })
118
118
  }
119
119
 
120
120
  module.exports = function (config) {
121
- config = Object.assign(defaultConfig, config);
121
+ config = Object.assign(defaultConfig, config)
122
122
 
123
123
  if (config.registerGlobal) {
124
- global.eachElement = eachElement;
124
+ global.eachElement = eachElement
125
125
  }
126
- return eachElement;
127
- };
126
+ return eachElement
127
+ }
@@ -1,5 +1,5 @@
1
- const { faker } = require('@faker-js/faker');
2
- const transform = require('../transform');
1
+ const { faker } = require('@faker-js/faker')
2
+ const transform = require('../transform')
3
3
 
4
4
  /**
5
5
  * Use the `@faker-js/faker` package to generate fake data inside examples on your gherkin tests
@@ -42,8 +42,8 @@ const transform = require('../transform');
42
42
  module.exports = function (config) {
43
43
  transform.addTransformerBeforeAll('gherkin.examples', (value) => {
44
44
  if (typeof value === 'string' && value.length > 0) {
45
- return faker.helpers.fake(value);
45
+ return faker.helpers.fake(value)
46
46
  }
47
- return value;
48
- });
49
- };
47
+ return value
48
+ })
49
+ }