codeceptjs 4.0.0-rc.15 → 4.0.0-rc.17
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/lib/command/check.js +0 -1
- package/lib/helper/extras/richTextEditor.js +86 -23
- package/lib/locator.js +12 -1
- package/package.json +1 -1
package/lib/command/check.js
CHANGED
|
@@ -155,7 +155,6 @@ export default async function (options) {
|
|
|
155
155
|
checks.teardown = true
|
|
156
156
|
for (const helper of Object.values(helpers).reverse()) {
|
|
157
157
|
try {
|
|
158
|
-
if (helper._passed) await helper._passed(test)
|
|
159
158
|
if (helper._after) await helper._after(test)
|
|
160
159
|
if (helper._finishTest) await helper._finishTest(suite)
|
|
161
160
|
if (helper._afterSuite) await helper._afterSuite(suite)
|
|
@@ -7,13 +7,13 @@ const EDITOR = {
|
|
|
7
7
|
IFRAME: 'iframe',
|
|
8
8
|
CONTENTEDITABLE: 'contenteditable',
|
|
9
9
|
HIDDEN_TEXTAREA: 'hidden-textarea',
|
|
10
|
+
UNREACHABLE: 'unreachable',
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
function detectAndMark(el, opts) {
|
|
13
14
|
const marker = opts.marker
|
|
14
15
|
const kinds = opts.kinds
|
|
15
16
|
const CE = '[contenteditable="true"], [contenteditable=""]'
|
|
16
|
-
const MAX_HIDDEN_ASCENT = 3
|
|
17
17
|
|
|
18
18
|
function mark(kind, target) {
|
|
19
19
|
document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
|
|
@@ -27,38 +27,76 @@ function detectAndMark(el, opts) {
|
|
|
27
27
|
if (tag === 'IFRAME') return mark(kinds.IFRAME, el)
|
|
28
28
|
if (el.isContentEditable) return mark(kinds.CONTENTEDITABLE, el)
|
|
29
29
|
|
|
30
|
+
const isFormHidden = tag === 'INPUT' && el.type === 'hidden'
|
|
31
|
+
if ((tag === 'INPUT' || tag === 'TEXTAREA') && !isFormHidden) {
|
|
32
|
+
const style = window.getComputedStyle(el)
|
|
33
|
+
if (style.display === 'none') return mark(kinds.UNREACHABLE, el)
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
const canSearchDescendants = tag !== 'INPUT' && tag !== 'TEXTAREA'
|
|
31
37
|
if (canSearchDescendants) {
|
|
32
38
|
const iframe = el.querySelector('iframe')
|
|
33
39
|
if (iframe) return mark(kinds.IFRAME, iframe)
|
|
34
40
|
const ce = el.querySelector(CE)
|
|
35
41
|
if (ce) return mark(kinds.CONTENTEDITABLE, ce)
|
|
36
|
-
const
|
|
42
|
+
const textareas = [...el.querySelectorAll('textarea')]
|
|
43
|
+
const focusable = textareas.find(t => window.getComputedStyle(t).display !== 'none')
|
|
44
|
+
const textarea = focusable || textareas[0]
|
|
37
45
|
if (textarea) return mark(kinds.HIDDEN_TEXTAREA, textarea)
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
return mark(kinds.STANDARD, el)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function detectInsideFrame() {
|
|
52
|
+
const MARKER = 'data-codeceptjs-rte-target'
|
|
53
|
+
const CE = '[contenteditable="true"], [contenteditable=""]'
|
|
54
|
+
const CONTENTEDITABLE = 'contenteditable'
|
|
55
|
+
const HIDDEN_TEXTAREA = 'hidden-textarea'
|
|
56
|
+
const body = document.body
|
|
57
|
+
document.querySelectorAll('[' + MARKER + ']').forEach(n => n.removeAttribute(MARKER))
|
|
47
58
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (iframeNear) return mark(kinds.IFRAME, iframeNear)
|
|
55
|
-
const ceNear = scope.querySelector(CE)
|
|
56
|
-
if (ceNear) return mark(kinds.CONTENTEDITABLE, ceNear)
|
|
57
|
-
const textareaNear = [...scope.querySelectorAll('textarea')].find(t => t !== el)
|
|
58
|
-
if (textareaNear) return mark(kinds.HIDDEN_TEXTAREA, textareaNear)
|
|
59
|
+
if (body.isContentEditable) return CONTENTEDITABLE
|
|
60
|
+
|
|
61
|
+
const ce = body.querySelector(CE)
|
|
62
|
+
if (ce) {
|
|
63
|
+
ce.setAttribute(MARKER, '1')
|
|
64
|
+
return CONTENTEDITABLE
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
|
|
67
|
+
const textareas = [...body.querySelectorAll('textarea')]
|
|
68
|
+
const focusable = textareas.find(t => window.getComputedStyle(t).display !== 'none')
|
|
69
|
+
const textarea = focusable || textareas[0]
|
|
70
|
+
if (textarea) {
|
|
71
|
+
textarea.setAttribute(MARKER, '1')
|
|
72
|
+
return HIDDEN_TEXTAREA
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return CONTENTEDITABLE
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function evaluateInFrame(helper, body, fn) {
|
|
79
|
+
if (body.helperType === 'webdriver') {
|
|
80
|
+
return helper.executeScript(fn)
|
|
81
|
+
}
|
|
82
|
+
return body.element.evaluate(fn)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function focusMarkedInFrameScript() {
|
|
86
|
+
const el = document.querySelector('[data-codeceptjs-rte-target]') || document.body
|
|
87
|
+
el.focus()
|
|
88
|
+
return document.activeElement === el
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function selectAllInFrameScript() {
|
|
92
|
+
const el = document.querySelector('[data-codeceptjs-rte-target]') || document.body
|
|
93
|
+
el.focus()
|
|
94
|
+
const range = document.createRange()
|
|
95
|
+
range.selectNodeContents(el)
|
|
96
|
+
const sel = window.getSelection()
|
|
97
|
+
sel.removeAllRanges()
|
|
98
|
+
sel.addRange(range)
|
|
99
|
+
return document.activeElement === el
|
|
62
100
|
}
|
|
63
101
|
|
|
64
102
|
function selectAllInEditable(el) {
|
|
@@ -76,6 +114,17 @@ function unmarkAll(marker) {
|
|
|
76
114
|
document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
|
|
77
115
|
}
|
|
78
116
|
|
|
117
|
+
function isActive(el) {
|
|
118
|
+
return el.ownerDocument.activeElement === el
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function assertFocused(target) {
|
|
122
|
+
const focused = await target.evaluate(isActive)
|
|
123
|
+
if (!focused) {
|
|
124
|
+
throw new Error('fillField: rich editor target did not accept focus. Locator must point at the visible editor surface (a wrapper, iframe, or contenteditable) — not a hidden backing element.')
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
79
128
|
async function findMarked(helper) {
|
|
80
129
|
const root = helper.page || helper.browser
|
|
81
130
|
const raw = await root.$('[' + MARKER + ']')
|
|
@@ -91,22 +140,36 @@ export async function fillRichEditor(helper, el, value) {
|
|
|
91
140
|
const source = el instanceof WebElement ? el : new WebElement(el, helper)
|
|
92
141
|
const kind = await source.evaluate(detectAndMark, { marker: MARKER, kinds: EDITOR })
|
|
93
142
|
if (kind === EDITOR.STANDARD) return false
|
|
143
|
+
if (kind === EDITOR.UNREACHABLE) {
|
|
144
|
+
throw new Error('fillField: cannot fill a display:none form control. Locator must point at the visible editor surface (a wrapper, iframe, or contenteditable).')
|
|
145
|
+
}
|
|
94
146
|
|
|
95
147
|
const target = await findMarked(helper)
|
|
96
148
|
const delay = helper.options.pressKeyDelay
|
|
97
149
|
|
|
98
150
|
if (kind === EDITOR.IFRAME) {
|
|
99
151
|
await target.inIframe(async body => {
|
|
100
|
-
await body
|
|
101
|
-
|
|
152
|
+
const innerKind = await evaluateInFrame(helper, body, detectInsideFrame)
|
|
153
|
+
if (innerKind === EDITOR.HIDDEN_TEXTAREA) {
|
|
154
|
+
const focused = await evaluateInFrame(helper, body, focusMarkedInFrameScript)
|
|
155
|
+
if (!focused) throw new Error('fillField: rich editor target inside iframe did not accept focus.')
|
|
156
|
+
await body.selectAllAndDelete()
|
|
157
|
+
await body.typeText(value, { delay })
|
|
158
|
+
} else {
|
|
159
|
+
const focused = await evaluateInFrame(helper, body, selectAllInFrameScript)
|
|
160
|
+
if (!focused) throw new Error('fillField: rich editor target inside iframe did not accept focus.')
|
|
161
|
+
await body.typeText(value, { delay })
|
|
162
|
+
}
|
|
102
163
|
})
|
|
103
164
|
} else if (kind === EDITOR.HIDDEN_TEXTAREA) {
|
|
104
165
|
await target.focus()
|
|
166
|
+
await assertFocused(target)
|
|
105
167
|
await target.selectAllAndDelete()
|
|
106
168
|
await target.typeText(value, { delay })
|
|
107
169
|
} else if (kind === EDITOR.CONTENTEDITABLE) {
|
|
108
170
|
await target.click()
|
|
109
171
|
await target.evaluate(selectAllInEditable)
|
|
172
|
+
await assertFocused(target)
|
|
110
173
|
await target.typeText(value, { delay })
|
|
111
174
|
}
|
|
112
175
|
|
package/lib/locator.js
CHANGED
|
@@ -591,13 +591,24 @@ Locator.clickable = {
|
|
|
591
591
|
`.//*[@title = ${literal}]`,
|
|
592
592
|
`.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id ]`,
|
|
593
593
|
`.//*[@role='button'][normalize-space(.)=${literal}]`,
|
|
594
|
+
`.//*[@role='tab' or @role='link' or @role='menuitem' or @role='menuitemcheckbox' or @role='menuitemradio' or @role='option' or @role='treeitem'][contains(normalize-space(string(.)), ${literal})]`,
|
|
594
595
|
]),
|
|
595
596
|
|
|
596
597
|
/**
|
|
597
598
|
* @param {string} literal
|
|
598
599
|
* @returns {string}
|
|
599
600
|
*/
|
|
600
|
-
self: literal =>
|
|
601
|
+
self: literal => {
|
|
602
|
+
// Narrowest-match: prefer the deepest descendant whose string-value contains the literal.
|
|
603
|
+
// Falling back to `self` without the `not(descendant...)` guard would match a container
|
|
604
|
+
// whose concatenated text happens to include the literal (e.g. a <ul role="tablist"> whose
|
|
605
|
+
// tab labels all sit in its string-value) and click the container itself.
|
|
606
|
+
const narrowest = `contains(normalize-space(string(.)), ${literal}) and not(.//*[contains(normalize-space(string(.)), ${literal})])`
|
|
607
|
+
return xpathLocator.combine([
|
|
608
|
+
`.//*[${narrowest}]`,
|
|
609
|
+
`./self::*[${narrowest} or contains(normalize-space(@value), ${literal})]`,
|
|
610
|
+
])
|
|
611
|
+
},
|
|
601
612
|
}
|
|
602
613
|
|
|
603
614
|
Locator.field = {
|