@open-xchange/appsuite-codeceptjs 0.1.1 → 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 +4 -4
- package/package.json +15 -7
- 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 +5 -8
- package/src/soap/soap.js +3 -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.0",
|
|
4
4
|
"description": "OX App Suite CodeceptJS Configuration and Helpers",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,13 +15,12 @@
|
|
|
15
15
|
"@influxdata/influxdb-client": "^1.33.2",
|
|
16
16
|
"@open-xchange/codecept-horizontal-scaler": "^0.1.7",
|
|
17
17
|
"@playwright/test": "^1.42.0",
|
|
18
|
-
"@types/node": "^20.11.22",
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
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
|
|