@open-xchange/appsuite-codeceptjs 0.1.1 → 0.2.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.
- package/index.js +4 -4
- package/package.json +16 -8
- package/src/appsuiteHttpClient.js +7 -0
- package/src/contexts/contexts.js +13 -14
- package/src/contexts/reseller.js +4 -15
- package/src/helper.js +12 -0
- package/src/pageobjects/calendar.js +1 -1
- package/src/pageobjects/fragments/viewer.js +9 -9
- package/src/pageobjects/mobile/mobileCalendar.js +4 -4
- package/src/soap/services/context.js +34 -11
- package/src/soap/soap.js +4 -2
- package/src/users/users.js +0 -1
- package/src/util.js +8 -2
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'
|
|
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.
|
|
128
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.1",
|
|
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": "
|
|
18
|
-
"@types/node": "^20.11.22",
|
|
17
|
+
"@playwright/test": "1.44.0",
|
|
19
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": "
|
|
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": "
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
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)
|
package/src/contexts/contexts.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
131
|
-
|
|
132
|
-
|
|
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
|
}
|
package/src/contexts/reseller.js
CHANGED
|
@@ -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
|
|
72
|
-
|
|
73
|
-
|
|
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 = {
|
|
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 .
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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.
|
|
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('
|
|
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
|
|
|
@@ -43,18 +42,27 @@ async function remove (id) {
|
|
|
43
42
|
/**
|
|
44
43
|
* This function creates a new context.
|
|
45
44
|
* @param {Object} ctx The context to create.
|
|
45
|
+
* @param {Object} adminUser The admin user of the context.
|
|
46
|
+
* Defaults to:
|
|
47
|
+
* ```JSON
|
|
48
|
+
* {
|
|
49
|
+
name: 'oxadmin',
|
|
50
|
+
password: 'secret',
|
|
51
|
+
display_name: 'context admin',
|
|
52
|
+
sur_name: 'admin',
|
|
53
|
+
given_name: 'context',
|
|
54
|
+
email1: `oxadmin@${process.env.MX_DOMAIN}`,
|
|
55
|
+
primaryEmail: `oxadmin@${process.env.MX_DOMAIN}`
|
|
56
|
+
}
|
|
57
|
+
All properties are needed and will be inserted if not provided.
|
|
58
|
+
```
|
|
46
59
|
* @returns {Promise<Object>} The created context.
|
|
47
60
|
*/
|
|
48
|
-
async function create (ctx = {}) {
|
|
61
|
+
async function create (ctx = {}, adminUser = {}) {
|
|
49
62
|
return await (await OXContextService)
|
|
50
63
|
.createAsync({
|
|
51
|
-
ctx
|
|
52
|
-
|
|
53
|
-
maxQuota: 1000,
|
|
54
|
-
filestoreId: String(await getFilestorageId()),
|
|
55
|
-
...ctx
|
|
56
|
-
},
|
|
57
|
-
admin_user: {
|
|
64
|
+
ctx,
|
|
65
|
+
admin_user: Object.assign({
|
|
58
66
|
name: 'oxadmin',
|
|
59
67
|
password: 'secret',
|
|
60
68
|
display_name: 'context admin',
|
|
@@ -62,13 +70,16 @@ async function create (ctx = {}) {
|
|
|
62
70
|
given_name: 'context',
|
|
63
71
|
email1: `oxadmin@${process.env.MX_DOMAIN}`,
|
|
64
72
|
primaryEmail: `oxadmin@${process.env.MX_DOMAIN}`
|
|
65
|
-
}
|
|
73
|
+
}, adminUser)
|
|
66
74
|
})
|
|
67
75
|
.then(async context => {
|
|
68
76
|
await changeModuleAccessByName(context.id, 'all')
|
|
69
77
|
return context
|
|
70
78
|
})
|
|
71
|
-
|
|
79
|
+
.catch(e => {
|
|
80
|
+
logSoapError(e)
|
|
81
|
+
throw e
|
|
82
|
+
})
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
/**
|
|
@@ -116,6 +127,17 @@ async function changeModuleAccess (id, moduleAccess) {
|
|
|
116
127
|
return await (await OXContextService).changeModuleAccessAsync({ ctx: { id }, access: { ...currentAccess, ...moduleAccess } })
|
|
117
128
|
}
|
|
118
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Search for contexts.
|
|
132
|
+
* @param {String} searchPattern The pattern to search for
|
|
133
|
+
* @param {Object} options Additional options
|
|
134
|
+
* @param {Boolean} options.excludeDisabled Exclude disabled contexts from the search results (default: true)
|
|
135
|
+
* @returns {Promise<Array<Object>>} The list of contexts that match the search pattern.
|
|
136
|
+
*/
|
|
137
|
+
async function list (searchPattern, { excludeDisabled = true }) {
|
|
138
|
+
return await (await OXContextService).listAsync({ search_pattern: searchPattern, exclude_disabled: excludeDisabled })
|
|
139
|
+
}
|
|
140
|
+
|
|
119
141
|
/**
|
|
120
142
|
* This function retrieves the context with the specified ID.
|
|
121
143
|
* @param {number} id The ID of the context to retrieve.
|
|
@@ -143,5 +165,6 @@ module.exports = {
|
|
|
143
165
|
getModuleAccess,
|
|
144
166
|
changeModuleAccess,
|
|
145
167
|
get,
|
|
168
|
+
list,
|
|
146
169
|
change
|
|
147
170
|
}
|
package/src/soap/soap.js
CHANGED
|
@@ -85,7 +85,9 @@ 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/,
|
|
90
|
+
/A mapping with login info .* already exists/
|
|
89
91
|
]
|
|
90
92
|
const blockedExceptions = [
|
|
91
93
|
'ContextExistsException',
|
|
@@ -154,7 +156,7 @@ async function createClientAsync (type) {
|
|
|
154
156
|
throw new Error(soapError?.faultstring || e.message)
|
|
155
157
|
})
|
|
156
158
|
performance.mark(endMark)
|
|
157
|
-
performance.measure(` ⏱ SOAP: ${type} -> ${prop}`, startMark, endMark)
|
|
159
|
+
performance.measure(` ⏱ SOAP: ${type} -> ${String(prop)}`, startMark, endMark)
|
|
158
160
|
// Return only the first result from the SOAP method call, as we don't need the SOAP envelope and other stuff.
|
|
159
161
|
if (!result || !result[0]) return
|
|
160
162
|
return result[0]?.return
|
package/src/users/users.js
CHANGED
|
@@ -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' ?
|
|
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,
|
|
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
|
|