@open-xchange/appsuite-codeceptjs 0.1.0 → 0.2.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.
package/index.js CHANGED
@@ -23,7 +23,7 @@ const dotenv = require('dotenv')
23
23
  dotenv.config({ path: '.env' })
24
24
  dotenv.config({ path: '.env.defaults' })
25
25
 
26
- const requiredEnvVars = ['LAUNCH_URL', 'PROVISIONING_URL', 'CONTEXT_ID']
26
+ const requiredEnvVars = ['LAUNCH_URL', 'PROVISIONING_URL']
27
27
 
28
28
  requiredEnvVars.forEach(function notdefined (key) {
29
29
  if (process.env[key]) return
@@ -58,7 +58,7 @@ module.exports = {
58
58
  '--no-first-run',
59
59
  '--no-sandbox',
60
60
  '--no-zygote'
61
- ]
61
+ ].concat((process.env.CHROME_ARGS || '').split(' '))
62
62
  },
63
63
  url: process.env.LAUNCH_URL,
64
64
  show: process.env.HEADLESS === 'false',
@@ -124,8 +124,8 @@ module.exports = {
124
124
  async teardown () {
125
125
  const { contexts } = global.inject()
126
126
  // we need to run this sequentially, less stress on the MW
127
- for (const ctx of contexts.filter(ctx => ctx.id > 100)) {
128
- if (ctx.id !== 10) await ctx.remove().catch(e => console.error(e.message))
127
+ for (const ctx of contexts.filter(ctx => /e2e-context-/.test(String(ctx.loginMappings)))) {
128
+ await ctx.remove().catch(e => console.error(e.message))
129
129
  }
130
130
  },
131
131
  reporter: 'mocha-multi',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-xchange/appsuite-codeceptjs",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "OX App Suite CodeceptJS Configuration and Helpers",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -14,14 +14,13 @@
14
14
  "@codeceptjs/helper": "^2.0.3",
15
15
  "@influxdata/influxdb-client": "^1.33.2",
16
16
  "@open-xchange/codecept-horizontal-scaler": "^0.1.7",
17
- "@playwright/test": "^1.40.1",
18
- "@types/node": "^20.11.19",
19
- "allure-codeceptjs": "^2.12.2",
17
+ "@playwright/test": "^1.42.0",
18
+ "allure-codeceptjs": "^2.13.0",
20
19
  "chai": "^5.1.0",
21
20
  "chai-subset": "^1.6.0",
22
21
  "chalk": "^4.1.0",
23
22
  "chalk-table": "^1.0.2",
24
- "codeceptjs": "^3.5.14",
23
+ "codeceptjs": "3.6.2",
25
24
  "dotenv": "^16.4.4",
26
25
  "mocha": "^10.3.0",
27
26
  "mocha-junit-reporter": "^2.2.1",
@@ -29,11 +28,20 @@
29
28
  "moment": "^2.30.1",
30
29
  "moment-timezone": "^0.5.43",
31
30
  "p-retry": "^6.2.0",
32
- "playwright-core": "^1.40.1",
33
- "playwright": "^1.40.1",
34
- "short-uuid": "^4.2.2",
35
- "soap": "^1.0.0",
31
+ "playwright-core": "1.43.1",
32
+ "short-uuid": "^5.0.0",
33
+ "soap": "^1.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^20.11.22",
36
37
  "ts-node": "^10.9.2",
37
38
  "typescript": "^5.3.3"
39
+ },
40
+ "pnpm": {
41
+ "updateConfig": {
42
+ "ignoreDependencies": [
43
+ "chalk"
44
+ ]
45
+ }
38
46
  }
39
47
  }
@@ -26,6 +26,13 @@ const pRetry = import('p-retry').then(module => module.default)
26
26
  const cache = {}
27
27
  const baseURL = codecept.config.get().helpers.Playwright.url.replace(/\/$/, '')
28
28
 
29
+ /**
30
+ * Fetches data from a given URL with retry functionality.
31
+ * @param {string} url - The URL to fetch data from.
32
+ * @param {object} options - The options for the fetch request.
33
+ * @returns {Promise<object>} - A promise that resolves to an object containing the fetched data and the response.
34
+ * @throws {Error} - Throws an error if there is an HTTP request error.
35
+ */
29
36
  async function fetchWithRetry (url, options) {
30
37
  return (await pRetry)(async () => {
31
38
  const res = await fetch(url, options)
@@ -23,7 +23,7 @@ const users = require('../users/users')()
23
23
  const util = require('../util')
24
24
  const event = require('../event')
25
25
  const contextService = require('../soap/services/context')
26
- const utilService = require('../soap/services/util')
26
+ const crypto = require('node:crypto')
27
27
 
28
28
  class Context {
29
29
  constructor ({ ctxdata, admin, auth }) {
@@ -105,7 +105,7 @@ class Context {
105
105
  return contextService.change({ id: this.id, maxQuota })
106
106
  }
107
107
 
108
- static async create (ctx = { }, adminUser = { }, auth = util.admin()) {
108
+ static async create (ctx = { filestoreId: undefined, id: undefined }, adminUser = { }, auth = util.admin()) {
109
109
  adminUser = Object.assign({
110
110
  name: 'oxadmin',
111
111
  password: 'secret',
@@ -116,26 +116,25 @@ class Context {
116
116
  primaryEmail: `${adminUser.name || 'oxadmin'}@${util.mxDomain()}`
117
117
  }, adminUser)
118
118
 
119
- let filestoreId = ctx.filestoreId
120
-
121
- if (typeof filestoreId === 'undefined') {
122
- filestoreId = await utilService.getFilestorageId()
123
- }
124
- const newCtx = Object.assign({ id: Number(util.userContextId()), maxQuota: -1, filestoreId }, ctx)
119
+ const loginMappings = [`e2e-context-${crypto.randomUUID()}`]
120
+ const newCtx = Object.assign({ maxQuota: -1, loginMappings }, ctx)
125
121
  event.emit(event.provisioning.context.create, newCtx, adminUser, auth)
126
122
  let data
127
-
128
123
  try {
129
124
  data = await contextService.create(newCtx)
130
- } catch (e) {
131
- newCtx.id = util.addJitter(newCtx.id)
132
- return this.create(newCtx)
125
+ } catch (error) {
126
+ // For mws that don't have autocontextid
127
+ if (error.message.includes('Mandatory fields in context not set: [id]')) {
128
+ newCtx.filestoreId = await util.getContextFilestorageId()
129
+ newCtx.id = util.addJitter(util.userContextId())
130
+ data = await contextService.create(newCtx)
131
+ } else throw error
133
132
  }
134
-
133
+ // Add loginMapping since we don't get the correct one from the mw
134
+ data.loginMappings.push(loginMappings[0])
135
135
  const context = new Context({ ctxdata: data, admin: adminUser, auth })
136
136
  created.push(context)
137
137
  // only provide defaults for fresh contexts
138
- await context.hasAccessCombination('all')
139
138
  event.emit(event.provisioning.context.created, context)
140
139
  return context
141
140
  }
@@ -68,20 +68,9 @@ class ResellerContext {
68
68
  const configMap = userAttributes.entries.find(entry => entry.key === 'config') || { key: 'config' }
69
69
  configMap.value = configMap.value || { entries: [] }
70
70
  configMap.value.entries.push({ key: `com.openexchange.capability.${cap}`, value })
71
- return resellerContextService.change({
72
- ctx: {
73
- id: this.id,
74
- userAttributes
75
- }
76
- }).then(() => {
77
- return resellerContextService.get({
78
- ctx: { id: this.id },
79
- auth: { login: this.admin.name, password: this.admin.password }
80
- })
81
- }).then(r => {
82
- this.ctxdata = r[0].return
83
- return this
84
- }).catch((err) => { throw new util.PropagatedError(err) })
71
+ return resellerContextService
72
+ .change({ id: this.ctxdata.id, userAttributes })
73
+ .then(() => resellerContextService.get(this.ctxdata.id))
85
74
  }
86
75
 
87
76
  doesntHaveCapability (capability) {
@@ -169,7 +158,7 @@ class ResellerContext {
169
158
  }
170
159
  }
171
160
 
172
- static async create (ctx = { }, adminUser = { }, auth = util.admin(), numberOfUsers = Number(process.env.PROVISIONING_USERS || 10)) {
161
+ static async create (ctx = {}, adminUser = {}, auth = util.admin(), numberOfUsers = Number(process.env.PROVISIONING_USERS || 10)) {
173
162
  ctx = Object.assign({ id: util.userContextId(), maxQuota: -1 }, ctx)
174
163
  adminUser = Object.assign({
175
164
  name: `${ctx.name || ctx.id}_admin`
package/src/helper.js CHANGED
@@ -812,6 +812,18 @@ class AppSuiteHelper extends Helper {
812
812
  await helper.browserContext.grantPermissions(['clipboard-read'], { origin: config.url })
813
813
  return await page.evaluate(async () => { return navigator.clipboard.readText() })
814
814
  }
815
+
816
+ async createGenericFile (filename, size) {
817
+ const filePath = path.join(codecept_dir, 'media/files/generic', filename)
818
+ try {
819
+ await fs.access(filePath, fs.constants.F_OK)
820
+ } catch (error) {
821
+ if (error.code === 'ENOENT') {
822
+ const content = crypto.randomBytes(size)
823
+ await fs.writeFile(filePath, content)
824
+ }
825
+ }
826
+ }
815
827
  }
816
828
 
817
829
  module.exports = AppSuiteHelper
@@ -213,7 +213,7 @@ module.exports = {
213
213
  }, title)
214
214
  if (skipRefresh === true) return
215
215
  I.click('#io-ox-refresh-icon')
216
- I.waitForDetached('#io-ox-refresh-icon .fa-spin')
216
+ I.waitForDetached('#io-ox-refresh-icon .animate-spin')
217
217
  },
218
218
 
219
219
  async setDateTo (dateString) {
@@ -45,15 +45,15 @@ module.exports = {
45
45
  },
46
46
 
47
47
  /**
48
- * Set a browser selection.
49
- * @param {string} startText - The 'innerText' of the node that starts the selection.
50
- * @param {string} endText - The 'innerText' of the node that ends the selection.
51
- * @param {string} wrapperClass - The selection is searched inside the wrapper. Provide a class name ('.className') for it.
52
- * *
53
- * Using the recommended workaround by pupeteer(v20.30) to select text.
54
- * "dragging and selecting text is not possible using page.mouse"
55
- * https://pptr.dev/api/puppeteer.mouse#example-1
56
- */
48
+ * Set a browser selection.
49
+ * @param {string} startText - The 'innerText' of the node that starts the selection.
50
+ * @param {string} endText - The 'innerText' of the node that ends the selection.
51
+ * @param {string} wrapperClass - The selection is searched inside the wrapper. Provide a class name ('.className') for it.
52
+ * *
53
+ * Using the recommended workaround by pupeteer(v20.30) to select text.
54
+ * "dragging and selecting text is not possible using page.mouse"
55
+ * https://pptr.dev/api/puppeteer.mouse#example-1
56
+ */
57
57
  async setBrowserSelection (startText, endText, wrapperClass) {
58
58
  await I.executeScript(({ startText, endText, wrapperClass }) => {
59
59
  const selection = window.getSelection()
@@ -23,13 +23,13 @@ const { I } = inject()
23
23
  module.exports = {
24
24
 
25
25
  // attr: [startDate, endDate]
26
- setDate (attr, value) {
27
- const date = value.format('YYYY-MM-DDTHH:mm')
28
- I.executeScript(({ attr, date }) => {
26
+ async setDate (attr, value) {
27
+ const date = value.toFormat("yyyy-MM-dd'T'HH:mm")
28
+ await I.executeScript(async ({ attr, date }) => {
29
29
  const fieldset = document.querySelector(`fieldset[data-attribute="${attr}"]`)
30
30
  const input = fieldset.querySelector('input')
31
31
  input.value = date
32
- input.dispatchEvent(new Event('input'))
32
+ input.dispatchEvent(new Event('mouseout'))
33
33
  }, { attr, date })
34
34
  },
35
35
  async newAppointment () {
@@ -19,7 +19,6 @@
19
19
  */
20
20
 
21
21
  const { createClientAsync, logSoapError } = require('../soap.js')
22
- const { getFilestorageId } = require('./util.js')
23
22
 
24
23
  const OXContextService = createClientAsync('OXContextService').then(client => client)
25
24
 
@@ -48,12 +47,7 @@ async function remove (id) {
48
47
  async function create (ctx = {}) {
49
48
  return await (await OXContextService)
50
49
  .createAsync({
51
- ctx: {
52
- id: process.env.CONTEXT_ID || Math.floor(Date.now() / 1000),
53
- maxQuota: 1000,
54
- filestoreId: String(await getFilestorageId()),
55
- ...ctx
56
- },
50
+ ctx,
57
51
  admin_user: {
58
52
  name: 'oxadmin',
59
53
  password: 'secret',
@@ -68,7 +62,10 @@ async function create (ctx = {}) {
68
62
  await changeModuleAccessByName(context.id, 'all')
69
63
  return context
70
64
  })
71
- // .catch(logSoapError)
65
+ .catch(e => {
66
+ logSoapError(e)
67
+ throw e
68
+ })
72
69
  }
73
70
 
74
71
  /**
package/src/soap/soap.js CHANGED
@@ -85,7 +85,8 @@ function shouldAbortRetry (error) {
85
85
  /already exists in this context/,
86
86
  /Shared Domain already exists/,
87
87
  /Shared Domain already in use/,
88
- /No such user/
88
+ /No such user/,
89
+ /Mandatory fields in context not set/
89
90
  ]
90
91
  const blockedExceptions = [
91
92
  'ContextExistsException',
@@ -154,7 +155,7 @@ async function createClientAsync (type) {
154
155
  throw new Error(soapError?.faultstring || e.message)
155
156
  })
156
157
  performance.mark(endMark)
157
- performance.measure(` ⏱ SOAP: ${type} -> ${prop}`, startMark, endMark)
158
+ performance.measure(` ⏱ SOAP: ${type} -> ${String(prop)}`, startMark, endMark)
158
159
  // Return only the first result from the SOAP method call, as we don't need the SOAP envelope and other stuff.
159
160
  if (!result || !result[0]) return
160
161
  return result[0]?.return
@@ -121,7 +121,6 @@ class User {
121
121
 
122
122
  static getRandom ({ name = 'test.user', password = util.getDefaultUserPassword(), sur_name = '', given_name = 'User' } = {}) {
123
123
  const id = short.generate().slice(0, 9).toLowerCase()
124
- // eslint-disable-next-line camelcase
125
124
  if (!sur_name) sur_name = id
126
125
  if (name === 'test.user') name = `${name}-${id}`
127
126
  const domain = util.mxDomain()
package/src/util.js CHANGED
@@ -20,6 +20,7 @@
20
20
 
21
21
  const codecept = require('codeceptjs')
22
22
  const url = require('node:url')
23
+ const { getFilestorageId } = require('./soap/services/util')
23
24
 
24
25
  module.exports = {
25
26
 
@@ -57,7 +58,7 @@ module.exports = {
57
58
 
58
59
  userContextId () {
59
60
  const ox = codecept.config.get().helpers.AppSuite
60
- return typeof ox.contextId === 'undefined' ? 10 : ox.contextId
61
+ return typeof ox.contextId === 'undefined' ? Math.floor(Date.now() / 1000) : ox.contextId
61
62
  },
62
63
 
63
64
  admin () {
@@ -84,6 +85,11 @@ module.exports = {
84
85
  return ox.defaultUserPassword || 'secret'
85
86
  },
86
87
 
88
+ async getContextFilestorageId () {
89
+ const ox = codecept.config.get().helpers.AppSuite
90
+ return ox.filestoreId || String(await getFilestorageId())
91
+ },
92
+
87
93
  PropagatedError: class PropagatedError extends Error {
88
94
  constructor (error) {
89
95
  super(error.message)
@@ -97,7 +103,7 @@ module.exports = {
97
103
  },
98
104
 
99
105
  addJitter (id) {
100
- const [JITTER_MIN, JITTER_MAX] = [1000, 2000]
106
+ const [JITTER_MIN, JITTER_MAX] = [1000, 5000]
101
107
  return Math.trunc(id) + Math.floor(Math.random() * (JITTER_MAX - JITTER_MIN + 1) + JITTER_MIN)
102
108
  }
103
109