@open-xchange/appsuite-codeceptjs 0.1.0

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 (48) hide show
  1. package/.env.defaults +47 -0
  2. package/README.md +40 -0
  3. package/chai.d.ts +5 -0
  4. package/customRerun.js +135 -0
  5. package/global.d.ts +5 -0
  6. package/index.js +187 -0
  7. package/package.json +39 -0
  8. package/src/actor.js +174 -0
  9. package/src/appsuiteHttpClient.js +155 -0
  10. package/src/chai.d.ts +6 -0
  11. package/src/chai.js +58 -0
  12. package/src/contexts/contexts.js +172 -0
  13. package/src/contexts/reseller.js +248 -0
  14. package/src/contexts.js +29 -0
  15. package/src/event.js +54 -0
  16. package/src/helper.js +817 -0
  17. package/src/pageobjects/calendar.js +226 -0
  18. package/src/pageobjects/contacts.js +148 -0
  19. package/src/pageobjects/drive.js +96 -0
  20. package/src/pageobjects/fragments/contact-autocomplete.js +45 -0
  21. package/src/pageobjects/fragments/contact-picker.js +50 -0
  22. package/src/pageobjects/fragments/dialogs.js +41 -0
  23. package/src/pageobjects/fragments/search.js +54 -0
  24. package/src/pageobjects/fragments/settings-mailfilter.js +90 -0
  25. package/src/pageobjects/fragments/settings.js +71 -0
  26. package/src/pageobjects/fragments/tinymce.js +41 -0
  27. package/src/pageobjects/fragments/topbar.js +43 -0
  28. package/src/pageobjects/fragments/viewer.js +67 -0
  29. package/src/pageobjects/mail.js +67 -0
  30. package/src/pageobjects/mobile/mobileCalendar.js +41 -0
  31. package/src/pageobjects/mobile/mobileContacts.js +40 -0
  32. package/src/pageobjects/mobile/mobileMail.js +51 -0
  33. package/src/pageobjects/tasks.js +58 -0
  34. package/src/plugins/emptyModule/index.js +21 -0
  35. package/src/plugins/settingsInit/index.js +35 -0
  36. package/src/plugins/testmetrics/index.js +135 -0
  37. package/src/soap/services/context.js +147 -0
  38. package/src/soap/services/oxaas.js +36 -0
  39. package/src/soap/services/resellerContext.js +65 -0
  40. package/src/soap/services/resellerUser.js +100 -0
  41. package/src/soap/services/user.js +114 -0
  42. package/src/soap/services/util.js +39 -0
  43. package/src/soap/soap.js +172 -0
  44. package/src/users/reseller.js +233 -0
  45. package/src/users/users.js +183 -0
  46. package/src/users.js +29 -0
  47. package/src/util.js +104 -0
  48. package/steps.d.ts +16 -0
package/src/helper.js ADDED
@@ -0,0 +1,817 @@
1
+ /**
2
+ * @copyright Copyright (c) Open-Xchange GmbH, Germany <info@open-xchange.com>
3
+ * @license AGPL-3.0
4
+ *
5
+ * This code is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU Affero General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License
16
+ * along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
17
+ *
18
+ * Any use of the work other than as authorized under this license or copyright law is prohibited.
19
+ */
20
+
21
+ const codecept = require('codeceptjs')
22
+ const AxeBuilder = require('@axe-core/playwright').default
23
+ const isEmpty = obj => [Object, Array].includes((obj || {}).constructor) && !Object.entries(obj || {}).length
24
+
25
+ const Helper = require('@codeceptjs/helper')
26
+ const { createHttpClient } = require('./appsuiteHttpClient')
27
+ const util = require('./util')
28
+ const fs = require('node:fs/promises')
29
+ const crypto = require('node:crypto')
30
+ const path = require('node:path')
31
+ const assert = require('node:assert')
32
+ const moment = require('moment')
33
+ const javascriptRoot = util.getJavascriptRoot()
34
+
35
+ const output = require('codeceptjs/lib/output')
36
+
37
+ function delay (ms) { return new Promise(resolve => setTimeout(resolve, ms)) }
38
+
39
+ async function retryPromise (fn, timeout = 5, retryDelay = 300) {
40
+ const start = Date.now()
41
+ const retry = async () => {
42
+ if (Date.now() - start > timeout * 1000) return Promise.reject(new Error('Timed out'))
43
+ await delay(retryDelay)
44
+ return fn().catch(retry)
45
+ }
46
+ return retry()
47
+ }
48
+
49
+ function jsonToForm (json) {
50
+ const form = new FormData()
51
+ for (const key in json) {
52
+ if (json[key] instanceof Object) form.append(key, JSON.stringify(json[key]))
53
+ else form.append(key, json[key])
54
+ }
55
+ return form
56
+ }
57
+
58
+ function setDeepValue (key, value, target) {
59
+ const keys = key.split('/')
60
+ keys.forEach(function (key, index) {
61
+ if (index === keys.length - 1) {
62
+ target[key] = value
63
+ } else {
64
+ target = target[key] = target[key] || {}
65
+ }
66
+ })
67
+ }
68
+
69
+ async function saveSetting (name, settings, options) {
70
+ const httpClient = createHttpClient(options)
71
+ const data = await httpClient.get('/api/jslob', {
72
+ params: { id: name, action: 'get' }
73
+ }).then(res => res.data.data)
74
+
75
+ for (const key in settings) {
76
+ const value = settings[key]
77
+ setDeepValue(key, value, data.tree)
78
+ }
79
+
80
+ return httpClient.put('/api/jslob', data.tree, {
81
+ params: { action: 'set', id: name }
82
+ })
83
+ }
84
+
85
+ async function createBlobFromFile (file) {
86
+ let filename, blob
87
+ if (typeof file === 'string') {
88
+ filename = path.basename(file)
89
+ // @ts-ignore
90
+ blob = new Blob([await fs.readFile(path.join(codecept_dir, file))])
91
+ } else {
92
+ filename = file.filename || file.name
93
+ blob = new Blob([file.content])
94
+ }
95
+ return { filename, blob }
96
+ }
97
+
98
+ async function compareBinaries (filePath1, filePath2) {
99
+ const [file1, file2] = await Promise.all([fs.readFile(filePath1), fs.readFile(filePath2)])
100
+
101
+ const hash1 = crypto.createHash('sha256').update(file1).digest('hex')
102
+ const hash2 = crypto.createHash('sha256').update(file2).digest('hex')
103
+
104
+ return hash1 === hash2
105
+ }
106
+
107
+ async function fileExists (filePath) {
108
+ try {
109
+ await fs.access(filePath)
110
+ return true
111
+ } catch (e) {
112
+ return false
113
+ }
114
+ }
115
+
116
+ class AppSuiteHelper extends Helper {
117
+ constructor (config) {
118
+ super(config)
119
+ codecept.locator.addFilter((locator, result) => {
120
+ if (typeof locator === 'string' && locator.indexOf('~') === 0) {
121
+ // accessibility locator
122
+ result.value = `[aria-label^="${locator.slice(1)}"]`
123
+ result.type = 'css'
124
+ result.output = `aria-label^=${locator.slice(1)}`
125
+ }
126
+ })
127
+ }
128
+
129
+ async waitForDownload (locator, filename, referenceFilePath) {
130
+ const { page } = this.helpers.Playwright
131
+ const downloadPromise = page.waitForEvent('download')
132
+ await page.locator(locator).click()
133
+ const download = await downloadPromise
134
+ const filePath = path.join(codecept_dir, '.tmp', download.suggestedFilename())
135
+
136
+ await download.saveAs(filePath)
137
+
138
+ // delay to ensure the file is written to disk
139
+ await new Promise(resolve => setTimeout(resolve, 1000))
140
+
141
+ assert.ok(await fileExists(filePath), `Downloaded file "${filename}" does not exist`)
142
+
143
+ if (!referenceFilePath) return
144
+ const reference = path.join(codecept_dir, referenceFilePath)
145
+ assert.ok(await compareBinaries(filePath, reference), `Downloaded file "${filename}" does not match the reference file`)
146
+ }
147
+
148
+ async grabAxeReport ({ disableRules = [], exclude = [], include = [] } = {}) {
149
+ const { page } = this.helpers.Playwright
150
+ const axeBuilder = new AxeBuilder({ page })
151
+ if (disableRules) axeBuilder.disableRules(disableRules)
152
+ if (exclude) {
153
+ exclude = Array.isArray(exclude) ? exclude : [exclude]
154
+ exclude.forEach(element => {
155
+ axeBuilder.exclude(element)
156
+ })
157
+ }
158
+ if (include) {
159
+ include = Array.isArray(include) ? include : [include]
160
+ include.forEach(element => {
161
+ axeBuilder.include(element)
162
+ })
163
+ }
164
+
165
+ const report = await axeBuilder.analyze()
166
+
167
+ const nodes = []
168
+ for (const violation of report.violations) {
169
+ for (const node of violation.nodes) {
170
+ nodes.push(node.target)
171
+ for (const combinedNodes of [node.all, node.any, node.none]) {
172
+ if (!isEmpty(combinedNodes)) {
173
+ for (const any of combinedNodes) {
174
+ for (const relatedNode of any.relatedNodes) {
175
+ nodes.push(relatedNode.target)
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ await page.evaluate(selectors => {
184
+ // Loop through the array of selectors and apply a red border to highlight elements
185
+ selectors.forEach(selector => {
186
+ const element = document.querySelector(selector)
187
+ if (element) element.style.border = '2px solid red'
188
+ })
189
+ }, nodes)
190
+ if (typeof report === 'string') throw report
191
+
192
+ return report
193
+ }
194
+
195
+ async selectFolder (id, context) {
196
+ const { page } = this.helpers.Playwright
197
+ const error = await page.evaluate(async ({ id, context, timeout }) => {
198
+ const { _, $, ox } = await import(`${window.location.pathname}e2e.js`)
199
+
200
+ import(`${window.location.pathname}io.ox/core/folder/api.js`).then(function ({ default: folderAPI }) {
201
+ function repeatUntil (cb, interval, timeout) {
202
+ const start = _.now()
203
+ const def = new $.Deferred()
204
+ const iterate = function () {
205
+ const result = cb()
206
+ if (result) return def.resolve(result)
207
+ if (_.now() - start < timeout) return _.delay(iterate, interval)
208
+ def.reject({ message: `Folder API could not resolve folder after ${timeout / 1000} seconds` })
209
+ }
210
+ iterate()
211
+ return def
212
+ }
213
+ return Promise.all([
214
+ repeatUntil(function () {
215
+ return _(folderAPI.pool.models).find(function (m) {
216
+ const res = m.get('title') === id || m.get('id') === id || m.get('display_title') === id
217
+ return context ? res && m.get('module') === context : res
218
+ })
219
+ }, 100, timeout),
220
+ repeatUntil(function () {
221
+ return ox.ui.App.getCurrentApp()
222
+ }, 100, timeout)
223
+ ]).then(function ([model, app]) {
224
+ // special handling for virtual folders
225
+ if (model.get('id').indexOf('virtual/') === 0) {
226
+ const body = app.getWindow().nodes.body
227
+ if (body) {
228
+ const view = body.find('.folder-tree').data('view')
229
+ if (view) {
230
+ view.trigger('virtual', model.get('id'))
231
+ }
232
+ }
233
+ }
234
+ return app.folder.set(model.get('id'))
235
+ })
236
+ }).then(function () { })
237
+ }, { id, context, timeout: 5000, javascriptRoot })
238
+ if (error) throw error
239
+ }
240
+
241
+ async throttleNetwork (networkConfig) {
242
+ // some network speed presets
243
+ const presets = {
244
+ OFFLINE: {
245
+ offline: true,
246
+ downloadThroughput: 0,
247
+ uploadThroughput: 0,
248
+ latency: 0
249
+ },
250
+ GPRS: {
251
+ offline: false,
252
+ downloadThroughput: 50 * 1024 / 8,
253
+ uploadThroughput: 20 * 1024 / 8,
254
+ latency: 500
255
+ },
256
+ '2G': {
257
+ offline: false,
258
+ downloadThroughput: 250 * 1024 / 8,
259
+ uploadThroughput: 50 * 1024 / 8,
260
+ latency: 300
261
+ },
262
+ '3G': {
263
+ offline: false,
264
+ downloadThroughput: 750 * 1024 / 8,
265
+ uploadThroughput: 250 * 1024 / 8,
266
+ latency: 100
267
+ },
268
+ '4G': {
269
+ offline: false,
270
+ downloadThroughput: 4 * 1024 * 1024 / 8,
271
+ uploadThroughput: 3 * 1024 * 1024 / 8,
272
+ latency: 20
273
+ },
274
+ DSL: {
275
+ offline: false,
276
+ downloadThroughput: 2 * 1024 * 1024 / 8,
277
+ uploadThroughput: 1 * 1024 * 1024 / 8,
278
+ latency: 5
279
+ },
280
+ // no throttling, use this to reset the connection
281
+ ONLINE: {
282
+ offline: false,
283
+ downloadThroughput: -1,
284
+ uploadThroughput: -1,
285
+ latency: 0
286
+ }
287
+ }
288
+
289
+ const { page } = this.helpers.Playwright
290
+
291
+ // get Chrome DevTools session
292
+ const devTools = await page.context().newCDPSession(page)
293
+
294
+ // Set network speed, use preset if its there
295
+ await devTools.send('Network.emulateNetworkConditions', presets[networkConfig.toUpperCase()] || networkConfig)
296
+ }
297
+
298
+ async haveSetting (obj, options) {
299
+ if (typeof obj === 'string') {
300
+ const input = obj.split('//')
301
+ const moduleName = input[0]
302
+ const key = input[1]
303
+ const value = options
304
+ obj = { [moduleName]: { [key]: value } }
305
+ options = arguments[2]
306
+ }
307
+ options = options || {}
308
+
309
+ // need to save settings sequentially, because concurrent requests will fail
310
+ for (const moduleName in obj) {
311
+ await saveSetting(moduleName, obj[moduleName], options)
312
+ }
313
+ }
314
+
315
+ async haveSnippet (snippet, options) {
316
+ const httpClient = createHttpClient(options)
317
+ const response = await httpClient.put('/api/snippet', snippet, { params: { action: 'new' } })
318
+ assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
319
+ return response.data
320
+ }
321
+
322
+ async haveMail (data, options) {
323
+ const httpClient = createHttpClient(options)
324
+
325
+ let form, response
326
+ if (!data.folder) data.folder = 'default0/INBOX'
327
+ if (data.folder && (data.path || data.source)) {
328
+ // import the mail
329
+ form = new FormData()
330
+ if (data.path) {
331
+ const { blob } = await createBlobFromFile(data.path)
332
+ form.append('file', blob)
333
+ } else if (data.source) {
334
+ form.append('file', new Blob([data.source], { type: 'text/plain' }), data.filename || 'test.eml')
335
+ }
336
+ response = await httpClient.post('/api/mail', form, {
337
+ params: {
338
+ action: 'import',
339
+ folder: data.folder,
340
+ force: true
341
+ }
342
+ })
343
+ } else {
344
+ // send the mail
345
+ // transform some data
346
+ if (data.content) {
347
+ data.attachments = data.attachments || []
348
+ data.attachments.unshift({
349
+ content: data.content,
350
+ content_type: 'text/html',
351
+ disp: 'inline'
352
+ })
353
+ delete data.content
354
+ }
355
+ const files = []
356
+ for (let i = 1; data.attachments && i < data.attachments.length; i++) {
357
+ const file = data.attachments[i]
358
+ const { filename, blob } = await createBlobFromFile(file)
359
+
360
+ if (filename && blob) {
361
+ data.attachments[i] = { filename, disp: 'attachment', name: filename }
362
+ files.push({ filename, blob })
363
+ }
364
+ }
365
+ if (!(data.from instanceof Array) && data.from.get) {
366
+ data.from = [[data.from.get('displayname'), data.from.get('primaryEmail')]]
367
+ }
368
+ if (!(data.to instanceof Array) && data.to.get) {
369
+ data.to = [[data.to.get('displayname'), data.to.get('primaryEmail')]]
370
+ }
371
+ if (data.sendtype === undefined) {
372
+ data.sendtype = 0
373
+ }
374
+ form = jsonToForm({ json_0: data })
375
+ for (let i = 0; i < files.length; i++) {
376
+ form.append(`file_${i}`, files[i].blob, files[i].filename)
377
+ }
378
+ response = await httpClient.post('/api/mail', form, {
379
+ params: {
380
+ action: 'new',
381
+ force_json_response: true,
382
+ lineWrapAfter: 0
383
+ }
384
+ })
385
+ }
386
+ const matches = /\((\{.*?\})\)/.exec(response.data)
387
+ const resData = matches && matches[1] ? JSON.parse(matches[1]) : response.data
388
+ assert.strictEqual(resData.error, undefined, JSON.stringify(resData))
389
+ return resData
390
+ }
391
+
392
+ async haveMails (iterable, options) {
393
+ for (const mail of iterable) await this.haveMail(mail, options)
394
+ }
395
+
396
+ async haveAnAlias (alias, options = {}) {
397
+ console.warn('This method is deprecated, use `user.hasAlias` instead')
398
+ const user = options.user || codecept.container.support('users')[0]
399
+ return user.hasAlias(alias)
400
+ }
401
+
402
+ async haveMailFilterRule (rule, options) {
403
+ const httpClient = createHttpClient(options)
404
+
405
+ const response = await httpClient.put('/api/mailfilter/v2', rule, {
406
+ params: { action: 'new' }
407
+ })
408
+ assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
409
+ return response.data
410
+ }
411
+
412
+ async haveFolder ({ title, module, permissions = [], subscribed = 1, parent }, options) {
413
+ const httpClient = createHttpClient(options)
414
+
415
+ const roles = { viewer: 257, reviewer: 33025, author: 4227332, administrator: 272662788, owner: 272662788 }
416
+ permissions = permissions.map((perm) => {
417
+ if (!perm.access || !perm.user) return perm
418
+ assert.notEqual(undefined, roles[perm.access])
419
+ return { entity: perm.user.userdata.id, bits: roles[perm.access], group: false }
420
+ })
421
+ const payload = { title, module, subscribed, permissions }
422
+ if (permissions.length === 0) delete payload.permissions
423
+ const response = await httpClient.put('/api/folders', payload, {
424
+ params: { action: 'new', folder: parent }
425
+ })
426
+ assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
427
+ return response.data.data
428
+ }
429
+
430
+ async haveContact (contact, options) {
431
+ const httpClient = createHttpClient(options)
432
+ const response = await httpClient.put('/api/contacts', contact, {
433
+ params: { action: 'new' }
434
+ })
435
+ assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
436
+ return response.data.data
437
+ }
438
+
439
+ async grabDefaultFolder (module, options = {}) {
440
+ const httpClient = createHttpClient(options)
441
+ const jslob = module === 'mail' ? 'io.ox/mail' : 'io.ox/core'
442
+ const response = await httpClient.put('/api/jslob', [jslob], {
443
+ params: { action: 'list' }
444
+ })
445
+ assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
446
+ const tree = response.data.data[0].tree
447
+ if (module === 'mail') return tree.defaultFolder[options.type || 'inbox']
448
+ return tree.folder[module]
449
+ }
450
+
451
+ /**
452
+ * Uploads a file to a specified folder.
453
+ *
454
+ * @param {string} folder - The ID of the folder where the file will be uploaded.
455
+ * @param {string|object} file - The path to the file or an object containing the file information.
456
+ * @param {object} [options] - Additional options for the file upload.
457
+ * @returns {Promise<Object>} - The uploaded file data.
458
+ */
459
+ async haveFile (folder, file, options) {
460
+ options = options || {}
461
+ const form = new FormData()
462
+ // prepare file for usage
463
+ const { filename, blob } = await createBlobFromFile(file)
464
+ form.append('file', blob, filename)
465
+ form.append('json', JSON.stringify({
466
+ folder_id: folder,
467
+ description: ''
468
+ }))
469
+ const httpClient = createHttpClient(options)
470
+ const response = await httpClient.post('/api/files', form, {
471
+ params: {
472
+ action: 'new',
473
+ extendedResponse: true,
474
+ try_add_version: true,
475
+ cryptoAction: options.cryptoAction || ''
476
+ },
477
+ headers: null
478
+ })
479
+ const matches = /\((\{.*?\})\)/.exec(response.data)
480
+ const resData = matches && matches[1] ? JSON.parse(matches[1]) : response.data.updated[0]
481
+ assert.strictEqual(resData.error, undefined, JSON.stringify(resData))
482
+ return resData.data.file
483
+ }
484
+
485
+ async haveTask (task, options) {
486
+ const httpClient = createHttpClient(options)
487
+ const response = await httpClient.put('/api/tasks', task, {
488
+ params: { action: 'new' }
489
+ })
490
+ assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
491
+ return response.data.data
492
+ }
493
+
494
+ /**
495
+ * Uploads an attachment to the specified module and object.
496
+ *
497
+ * @param {string} module - The module to attach the file to.
498
+ * @param {object} obj - The object to attach the file to.
499
+ * @param {string} file - The file to be attached.
500
+ * @param {object} options - The options for creating the HTTP client.
501
+ * @returns {Promise<object>} - The response data after attaching the file.
502
+ */
503
+ async haveAttachment (module, obj, file, options) {
504
+ const mapping = { tasks: 4, contacts: 7, drive: 137, infostore: 137, files: 137 }
505
+ const httpClient = createHttpClient(options)
506
+ const { blob, filename } = await createBlobFromFile(file)
507
+ const form = new FormData()
508
+ let response
509
+ let resData
510
+ if (module === 'calendar' || module === 'chronos') {
511
+ const index = obj.attachments ? obj.attachments.length : 0
512
+ obj = Object.assign({ attachments: [] }, obj)
513
+ obj.attachments.push({ filename, uri: `cid:file_${index}` })
514
+ form.append('file_' + index, blob, filename)
515
+ form.append('json_0', JSON.stringify(obj))
516
+ response = await httpClient.post('/api/chronos', form, {
517
+ params: {
518
+ action: 'update',
519
+ folder: obj.folder,
520
+ id: obj.id,
521
+ timestamp: moment().add(100000).valueOf()
522
+ }
523
+ })
524
+ const matches = /\((\{.*?\})\)/.exec(response.data)
525
+ resData = matches && matches[1] ? JSON.parse(matches[1]) : response.data.updated[0]
526
+ if (resData.data && resData.data.updated && resData.data.updated.length > 0) resData = resData.data.updated[0]
527
+ } else {
528
+ form.append('file_0', blob, filename)
529
+ form.append('json_0', JSON.stringify({ module: mapping[module], attached: obj.id, folder: obj.folder_id || obj.folder }))
530
+ response = await httpClient.post('/api/attachment', form, {
531
+ params: {
532
+ action: 'attach',
533
+ force_json_response: true
534
+ }
535
+ })
536
+ resData = response.data
537
+ }
538
+ assert.strictEqual(resData.error, undefined, JSON.stringify(resData))
539
+ return resData
540
+ }
541
+
542
+ async haveAppointment (appointment, options) {
543
+ const httpClient = createHttpClient(options)
544
+
545
+ if (appointment.startDate?.value?._isAMomentObject) appointment.startDate.value = appointment.startDate.value.format('YYYYMMDD[T]HHmmss')
546
+ if (appointment.endDate?.value?._isAMomentObject) appointment.endDate.value = appointment.endDate.value.format('YYYYMMDD[T]HHmmss')
547
+
548
+ if (appointment.startDate && !appointment.startDate.tzid) appointment.startDate.tzid = 'Europe/Berlin'
549
+ if (appointment.endDate && !appointment.endDate.tzid) appointment.endDate.tzid = 'Europe/Berlin'
550
+
551
+ if (!appointment.folder) {
552
+ const jslob = await httpClient.put('/api/jslob', ['io.ox/core'], {
553
+ params: { action: 'list' }
554
+ })
555
+ assert.strictEqual(jslob.data.error, undefined, JSON.stringify(jslob.data))
556
+ appointment.folder = jslob.data.data[0].tree.folder.calendar
557
+ }
558
+
559
+ if (appointment.folder && !appointment.folder.startsWith('cal://0/')) appointment.folder = `cal://0/${appointment.folder}`
560
+
561
+ const response = await httpClient.put('/api/chronos', appointment, {
562
+ params: {
563
+ action: 'new',
564
+ folder: appointment.folder
565
+ }
566
+ })
567
+ assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
568
+ return response.data.data.created[0]
569
+ }
570
+
571
+ async importAppointment ({ sourcePath, folder = undefined }, options) {
572
+ const httpClient = createHttpClient(options)
573
+
574
+ // prepare folder
575
+ if (!folder) {
576
+ const jslob = await httpClient.put('/api/jslob', ['io.ox/core'], {
577
+ params: { action: 'list' }
578
+ })
579
+ assert.strictEqual(jslob.data.error, undefined, JSON.stringify(jslob.data))
580
+ folder = jslob.data.data[0].tree.folder.calendar
581
+ }
582
+
583
+ if (folder && !folder.startsWith('cal://0/')) folder = `cal://0/${folder}`
584
+
585
+ // prepare form upload
586
+ const form = new FormData()
587
+ const { blob } = await createBlobFromFile(sourcePath)
588
+ form.append('file', blob)
589
+
590
+ const response = await httpClient.post('/api/import', form, {
591
+ params: {
592
+ action: 'ICAL',
593
+ folder,
594
+ ignoreUIDs: true
595
+ }
596
+ })
597
+ assert.strictEqual(response.data.error, undefined, JSON.stringify(response.data))
598
+ return response
599
+ }
600
+
601
+ async haveResource (data, options) {
602
+ const httpClient = createHttpClient(options)
603
+ let response = await httpClient.put('/api/resource', data, {
604
+ params: { action: 'new' }
605
+ })
606
+ if (response.data.error && response.data.code === 'RES-0006') {
607
+ const { id } = (await httpClient.put('/api/resource', { pattern: data.name }, {
608
+ params: { action: 'search' }
609
+ })).data.data[0]
610
+ response = await httpClient.put('/api/resource', data, {
611
+ params: { action: 'update', id }
612
+ })
613
+ // middleware returns no data here
614
+ Object.assign(response.data.data, data, { id })
615
+ }
616
+ if (response.data.error) throw response.data
617
+ return response.data.data.id
618
+ }
619
+
620
+ async dontHaveResource (pattern, options) {
621
+ const httpClient = createHttpClient(options)
622
+ let data
623
+ if (typeof pattern.id !== 'undefined') {
624
+ data = [(await httpClient.get('/api/resource', {
625
+ params: { action: 'get', id: pattern.id }
626
+ })).data.data]
627
+ } else {
628
+ data = (await httpClient.put('/api/resource', { pattern }, {
629
+ params: { action: 'search' }
630
+ })).data.data
631
+ }
632
+
633
+ const timestamp = require('moment')().add(30, 'years').format('x')
634
+ return Promise.all(data.map(async ({ id }) => {
635
+ await httpClient.put('/api/resource', { id }, {
636
+ params: { action: 'delete', timestamp }
637
+ })
638
+ return { id, pattern }
639
+ }))
640
+ }
641
+
642
+ async haveGroup (group, options) {
643
+ const httpClient = createHttpClient(options)
644
+ const response = await httpClient.put('/api/group', group, {
645
+ params: { action: 'new' }
646
+ })
647
+ return response.data.data
648
+ }
649
+
650
+ async dontHaveGroup (name, options) {
651
+ const httpClient = createHttpClient(options)
652
+ const { data: { data } } = await httpClient.put('/api/group', '', {
653
+ params: {
654
+ action: 'all',
655
+ columns: '1,701'
656
+ }
657
+ })
658
+ const timestamp = require('moment')().add(30, 'years').format('x')
659
+ const test = typeof name.test === 'function' ? g => name.test(g[1]) : g => name === g[1]
660
+
661
+ const ids = data.filter(test).map(g => g[0])
662
+ return Promise.all(ids.map(async (id) => {
663
+ await httpClient.put('/api/group', { id }, {
664
+ params: {
665
+ action: 'delete',
666
+ timestamp
667
+ }
668
+ })
669
+ return { id, name }
670
+ }))
671
+ }
672
+
673
+ async haveLockedFile (data, options) {
674
+ const httpClient = createHttpClient(options)
675
+ const response = await httpClient.put('/api/files', data, {
676
+ params: {
677
+ action: 'lock',
678
+ id: data.id,
679
+ folder: data.folder_id
680
+ }
681
+ })
682
+ return response.data
683
+ }
684
+
685
+ async setMailCategories (data, options) {
686
+ const httpClient = createHttpClient(options)
687
+ const { mailId, folder, categories } = data
688
+ const response = await httpClient.put('/api/mail', { set_user_flags: categories }, {
689
+ params: {
690
+ action: 'update',
691
+ id: mailId,
692
+ folder
693
+ }
694
+ })
695
+ return response.data
696
+ }
697
+
698
+ async pressKeys (key) {
699
+ return [...key].forEach(k => this.helpers.Playwright.pressKey(k))
700
+ }
701
+
702
+ /**
703
+ * @param {object} options
704
+ * @param {object} [options.user] a user object as returned by provisioning helper, default is the "first" user
705
+ * @param {object} [options.additionalAccount] an additional user that will be provisioned as the external account
706
+ * @param {string} [options.extension] optional extension added to the mail address ("ext" will be translated to: $user.primary+ext@mailDomain)
707
+ * @param {string} [options.name] name of the account
708
+ * @param {string} [options.transport_auth] transport authentication, default: 'none'
709
+ */
710
+ async haveMailAccount ({ user, additionalAccount, extension, name, transport_auth: transportAuth }) {
711
+ if (!user) user = inject().users[0]
712
+ if (!additionalAccount) additionalAccount = user
713
+ if (!transportAuth) transportAuth = 'none'
714
+
715
+ const httpClient = createHttpClient({ user })
716
+ const mailDomain = additionalAccount.get('primaryEmail').replace(/.*@/, '')
717
+ const imapServer = additionalAccount.get('imapServer') === 'localhost' ? mailDomain : additionalAccount.get('imapServer')
718
+ const smtpServer = additionalAccount.get('smtpServer') === 'localhost' ? mailDomain : additionalAccount.get('smtpServer')
719
+
720
+ const account = {
721
+ name,
722
+ primary_address: `${additionalAccount.get('primaryEmail').replace(/@.*/, '')}${extension ? '-' + extension : ''}@${mailDomain}`,
723
+ login: additionalAccount.get('imapLogin'),
724
+ password: additionalAccount.get('password'),
725
+ mail_url: `${additionalAccount.get('imapSchema')}${imapServer}:${additionalAccount.get('imapPort')}`,
726
+ transport_url: `${additionalAccount.get('smtpSchema')}${smtpServer}:${additionalAccount.get('smtpPort')}`,
727
+ transport_auth: transportAuth
728
+ }
729
+ const response = await httpClient.put('/api/account', account, {
730
+ params: {
731
+ action: 'new',
732
+ session
733
+ }
734
+ })
735
+ return response.data
736
+ }
737
+
738
+ /**
739
+ * @param {object} obj
740
+ * @param {number} timeout
741
+ * @param {object} options
742
+ * @returns
743
+ */
744
+ async waitForSetting (obj, timeout = 5, options = {}) {
745
+ const httpClient = createHttpClient(options)
746
+ const [moduleName] = Object.keys(obj)
747
+
748
+ async function checkSetting () {
749
+ const data = await httpClient.get('/api/jslob', {
750
+ params: {
751
+ id: moduleName,
752
+ action: 'get'
753
+ }
754
+ }).then(function (res) {
755
+ if (res.data.error) throw new Error(res.data.error)
756
+ return res.data.data
757
+ })
758
+ expect(data.tree).to.containSubset(obj[moduleName])
759
+ }
760
+
761
+ const startTime = Date.now()
762
+
763
+ return retryPromise(checkSetting, timeout).finally(() =>
764
+ output.say(`Waiting for settings to be saved took ${(Date.now() - startTime) / 1000} seconds`)
765
+ )
766
+ }
767
+
768
+ async waitForCapability (capability, timeout = 10, options = { shouldBe: true }) {
769
+ const httpClient = createHttpClient(options)
770
+
771
+ async function checkCapability () {
772
+ const data = await httpClient.get('/api/capabilities', {
773
+ params: {
774
+ action: 'get',
775
+ id: capability,
776
+ session
777
+ }
778
+ }).then(function (res) {
779
+ if (options.shouldBe && Object.keys(res.data).length === 0) throw new Error(res.data.error)
780
+ else if (!options.shouldBe && Object.keys(res.data).length !== 0) throw new Error(res.data.error)
781
+ return res.data
782
+ })
783
+ return data
784
+ }
785
+
786
+ const startTime = Date.now()
787
+ // We wait 2500ms to make sure that the cache was invalidated in the meantime
788
+ return retryPromise(checkCapability, timeout, 1000).finally(() =>
789
+ output.say(`Waiting for capability to be saved took ${(Date.now() - startTime) / 1000} seconds`)
790
+ )
791
+ }
792
+
793
+ async waitForApp () {
794
+ const { Playwright } = this.helpers
795
+
796
+ await Playwright.waitForInvisible('#background-loader.busy', 30)
797
+ await Playwright.waitForVisible({ css: 'html.complete' }, 10)
798
+ }
799
+
800
+ async copyToClipboard () {
801
+ const helper = this.helpers.Playwright
802
+ const { page } = helper
803
+ await page.evaluate(async () => { document.execCommand('copy') })
804
+ }
805
+
806
+ async getClipboardContent () {
807
+ const helper = this.helpers.Playwright
808
+ const { page, config } = helper
809
+ // make sure the browser is focused, might have lost the focus due to executing steps manually with 'pause()'
810
+ await page.bringToFront()
811
+ // reading the clipboard requires correct permissions
812
+ await helper.browserContext.grantPermissions(['clipboard-read'], { origin: config.url })
813
+ return await page.evaluate(async () => { return navigator.clipboard.readText() })
814
+ }
815
+ }
816
+
817
+ module.exports = AppSuiteHelper