spyne 0.24.0 → 0.26.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/lib/spyne.esm.js +3 -3
- package/lib/spyne.esm.js.LICENSE.txt +2 -2
- package/lib/spyne.esm.js.map +1 -1
- package/lib/spyne.umd.js +2 -2
- package/lib/spyne.umd.js.LICENSE.txt +2 -2
- package/package.json +1 -1
- package/src/spyne/channels/channel-payload-class.js +112 -17
- package/src/spyne/channels/channel.js +13 -0
- package/src/spyne/channels/spyne-channel-window.js +95 -3
- package/src/spyne/spyne-app.js +5 -2
- package/src/spyne/spyne-plugins.js +17 -0
- package/src/spyne/spyne.js +11 -3
- package/src/spyne/utils/channel-fetch-util.js +3 -2
- package/src/spyne/utils/channel-payload-filter.js +26 -22
- package/src/spyne/utils/safe-clone.js +128 -2
- package/src/spyne/utils/sanitize-data.js +244 -23
- package/src/spyne/utils/sanitize-html.js +17 -40
- package/src/spyne/utils/spyne-app-properties.js +30 -0
- package/src/spyne/utils/spyne-warn.js +26 -0
- package/src/spyne/views/dom-element-template.js +93 -0
- package/src/spyne/views/dom-element.js +4 -3
- package/src/spyne/views/view-stream-selector.js +52 -47
- package/src/tests/channels/channel-payload-filter.test.js +26 -0
- package/src/tests/channels/channel-window-custom-events.test.js +130 -0
- package/src/tests/index.test.js +5 -0
- package/src/tests/utils/safe-clone.test.js +193 -0
- package/src/tests/utils/security.test.js +153 -1
- package/src/tests/views/dom-el-selectors.test.js +7 -7
- package/src/tests/views/view-stream-selector.test.js +178 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central outlet for SpyneJS developer-hint warnings (invalid selectors,
|
|
3
|
+
* sanitized attributes, policy removals, deprecations).
|
|
4
|
+
*
|
|
5
|
+
* Warnings are suppressed when window.SPYNE_SUPPRESS_WARNINGS is true —
|
|
6
|
+
* primarily for test runs, where expected-failure cases would otherwise
|
|
7
|
+
* flood the reporter. The flag lives on window (not module state) so it
|
|
8
|
+
* spans separately bundled test files.
|
|
9
|
+
*
|
|
10
|
+
* Security-posture warnings (e.g. config.disableSanitize) do NOT route
|
|
11
|
+
* through here and are always printed.
|
|
12
|
+
*/
|
|
13
|
+
const spyneWarn = (...args) => {
|
|
14
|
+
const suppressed = typeof window !== 'undefined' && window.SPYNE_SUPPRESS_WARNINGS === true
|
|
15
|
+
if (suppressed !== true) {
|
|
16
|
+
console.warn(...args)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const setSpyneWarningsDisabled = (bool = true) => {
|
|
21
|
+
if (typeof window !== 'undefined') {
|
|
22
|
+
window.SPYNE_SUPPRESS_WARNINGS = bool === true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { spyneWarn, setSpyneWarningsDisabled }
|
|
@@ -36,6 +36,27 @@ export class DomElementTemplate {
|
|
|
36
36
|
this.isProxyData = data.__cms__isProxy === true
|
|
37
37
|
this.testMode = opts?.testMode
|
|
38
38
|
|
|
39
|
+
// CMS lineage pass — construction-time only, and only in authoring mode.
|
|
40
|
+
// In production (enableCMSProxies false) this costs one boolean read;
|
|
41
|
+
// nothing here ever runs inside the substitution loops.
|
|
42
|
+
if (this.isProxyData === false && SpyneAppProperties.enableCMSProxies === true) {
|
|
43
|
+
const rehydrated = SpyneAppProperties.rehydrateCmsData?.(data)
|
|
44
|
+
if (rehydrated !== undefined) {
|
|
45
|
+
// a stripped clone was re-identified — render with the revived proxy
|
|
46
|
+
data = rehydrated
|
|
47
|
+
this.isProxyData = true
|
|
48
|
+
} else if (DomElementTemplate.templateBindsProxiedChild(data, this.template) === true) {
|
|
49
|
+
// plain wrapper holding proxied values (e.g. a pageData container),
|
|
50
|
+
// AND this template actually references one of those proxied children
|
|
51
|
+
// via a dot-path or loop token — only then is CMS wrapping useful.
|
|
52
|
+
// Flipping on child presence alone put ~40% of all templates through
|
|
53
|
+
// the wrap→inflate→sanitize→strip pipeline for zero bindings.
|
|
54
|
+
this.isProxyData = true
|
|
55
|
+
} else if (SpyneAppProperties.debug === true) {
|
|
56
|
+
SpyneAppProperties.warnUnproxiedCmsData?.(data, this.template)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
39
60
|
// Normalize triple-bracket tags to double-bracket.
|
|
40
61
|
//
|
|
41
62
|
// DomElementTemplate treats {{{key}}} as an alias for {{key}} — both
|
|
@@ -105,6 +126,57 @@ export class DomElementTemplate {
|
|
|
105
126
|
return /({{\.\*?}})/.test(str)
|
|
106
127
|
}
|
|
107
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Shallow, one-level scan for CMS-proxied values inside a plain container,
|
|
131
|
+
* bailing on the first hit. Lets wrappers that hold proxied content (a
|
|
132
|
+
* pageData object, a filtered array of proxied stories) opt in to CMS
|
|
133
|
+
* template wrapping even though the wrapper itself is not a proxy.
|
|
134
|
+
* Only ever called in authoring mode (enableCMSProxies true).
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* Wrapper detection with a binding test: returns true only when data holds
|
|
138
|
+
* a proxied child under a key that the template actually references via a
|
|
139
|
+
* dot-path ({{key.prop}}) or loop ({{#key}} / {{#key.path}}) token. A
|
|
140
|
+
* template that never reaches into the proxied child gains nothing from
|
|
141
|
+
* CMS wrapping — its own top-level tokens have no dataId to bind — so
|
|
142
|
+
* flipping it into proxy mode only pays the wrap/sanitize/strip pipeline.
|
|
143
|
+
*/
|
|
144
|
+
static templateBindsProxiedChild(data, template) {
|
|
145
|
+
if (data === null || typeof data !== 'object' || typeof template !== 'string') {
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
if (template.indexOf('{{') === -1) {
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
const keys = Object.keys(data)
|
|
152
|
+
for (let i = 0; i < keys.length; i++) {
|
|
153
|
+
const key = keys[i]
|
|
154
|
+
const val = data[key]
|
|
155
|
+
if (val !== null && typeof val === 'object' && val.__cms__isProxy === true) {
|
|
156
|
+
if (template.indexOf(`{{${key}.`) !== -1 ||
|
|
157
|
+
template.indexOf(`{{#${key}}}`) !== -1 ||
|
|
158
|
+
template.indexOf(`{{#${key}.`) !== -1) {
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
static hasProxiedChild(data) {
|
|
167
|
+
if (data === null || typeof data !== 'object') {
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
const values = Object.values(data)
|
|
171
|
+
for (let i = 0; i < values.length; i++) {
|
|
172
|
+
const val = values[i]
|
|
173
|
+
if (val !== null && typeof val === 'object' && val.__cms__isProxy === true) {
|
|
174
|
+
return true
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
|
|
108
180
|
/**
|
|
109
181
|
* Normalizes triple-bracket tags to double-bracket form.
|
|
110
182
|
*
|
|
@@ -219,8 +291,26 @@ export class DomElementTemplate {
|
|
|
219
291
|
/**
|
|
220
292
|
* @desc Returns a document fragment generated from the template and any added data.
|
|
221
293
|
*/
|
|
294
|
+
/**
|
|
295
|
+
* Unwraps <spyne-cms-item> elements whose data-cms-id resolved to an empty
|
|
296
|
+
* string — tokens that live on a plain wrapper (no __cms__dataId) rather
|
|
297
|
+
* than on CMS content. Leaves their inner text in place, so the rendered
|
|
298
|
+
* DOM matches what a non-CMS render would produce. Only runs in authoring
|
|
299
|
+
* mode (isProxyData true), one pass over the final HTML string.
|
|
300
|
+
*/
|
|
301
|
+
static stripInertCmsItems(html) {
|
|
302
|
+
if (html.indexOf('data-cms-id=""') === -1) {
|
|
303
|
+
return html
|
|
304
|
+
}
|
|
305
|
+
const inertRE = /<spyne-cms-item data-cms-id=""[^>]*>[\s\S]*?<spyne-cms-item-text>([\s\S]*?)<\/spyne-cms-item-text>\s*<\/spyne-cms-item>/g
|
|
306
|
+
return html.replace(inertRE, '$1')
|
|
307
|
+
}
|
|
308
|
+
|
|
222
309
|
renderDocFrag() {
|
|
223
310
|
let html = DomElementTemplate.replaceImgPath(this.finalArr.join(''))
|
|
311
|
+
if (this.isProxyData === true) {
|
|
312
|
+
html = DomElementTemplate.stripInertCmsItems(html)
|
|
313
|
+
}
|
|
224
314
|
if (this.testMode !== true) {
|
|
225
315
|
html = sanitizeHTML(html)
|
|
226
316
|
}
|
|
@@ -234,6 +324,9 @@ export class DomElementTemplate {
|
|
|
234
324
|
renderToString() {
|
|
235
325
|
let html = this.finalArr.join('')
|
|
236
326
|
html = DomElementTemplate.replaceImgPath(html)
|
|
327
|
+
if (this.isProxyData === true) {
|
|
328
|
+
html = DomElementTemplate.stripInertCmsItems(html)
|
|
329
|
+
}
|
|
237
330
|
if (this.testMode !== true) {
|
|
238
331
|
// String(...) unwraps TrustedHTML when strict mode is active,
|
|
239
332
|
// keeping this method's string contract.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { baseCoreMixins } from '../utils/mixins/base-core-mixins.js'
|
|
2
|
+
import { spyneWarn } from '../utils/spyne-warn.js'
|
|
2
3
|
import { DomElementTemplate } from './dom-element-template.js'
|
|
3
4
|
import { deepMerge } from '../utils/deep-merge.js'
|
|
4
|
-
import { sanitizeAttribute,
|
|
5
|
+
import { sanitizeAttribute, applyElementHardening } from '../utils/sanitize-data.js'
|
|
5
6
|
import { is, defaultTo, pick, mapObjIndexed, forEachObjIndexed, pipe } from 'ramda'
|
|
6
7
|
|
|
7
8
|
class DomElement {
|
|
@@ -67,7 +68,7 @@ class DomElement {
|
|
|
67
68
|
if (testMode !== true) {
|
|
68
69
|
const { allowed, value } = sanitizeAttribute(key, val, el.tagName)
|
|
69
70
|
if (allowed !== true) {
|
|
70
|
-
|
|
71
|
+
spyneWarn(`SPYNE WARNING: The attribute "${key}" was removed during sanitization.`, el)
|
|
71
72
|
return
|
|
72
73
|
}
|
|
73
74
|
val = value
|
|
@@ -78,7 +79,7 @@ class DomElement {
|
|
|
78
79
|
this.getProp('attrs').forEach(addAttributes)
|
|
79
80
|
|
|
80
81
|
if (testMode !== true) {
|
|
81
|
-
|
|
82
|
+
applyElementHardening(el)
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
return el
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { head, compose, reject, split, isEmpty, lte, defaultTo, prop } from 'ramda'
|
|
2
|
+
import { spyneWarn } from '../utils/spyne-warn.js'
|
|
2
3
|
import { SpyneAppProperties } from '../utils/spyne-app-properties.js'
|
|
3
4
|
|
|
4
5
|
function generateSpyneSelectorId(el) {
|
|
@@ -32,7 +33,7 @@ function testSelectors(cxt, sel, verboseBool) {
|
|
|
32
33
|
const elIsDomElement = compose(lte(0), defaultTo(-1), prop('nodeType'))
|
|
33
34
|
|
|
34
35
|
if (el !== null && elIsDomElement(el) === false) {
|
|
35
|
-
|
|
36
|
+
spyneWarn(`Spyne Warning: the el object is not a valid single element, ${el}`)
|
|
36
37
|
return
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -48,7 +49,7 @@ function testSelectors(cxt, sel, verboseBool) {
|
|
|
48
49
|
|
|
49
50
|
const isValidSelectorBool = isValidSelector(sel)
|
|
50
51
|
if (verboseBool === true && isDevMode() === true && isValidSelectorBool === false) {
|
|
51
|
-
|
|
52
|
+
spyneWarn(`Spyne Warning: the selector, ${sel} does not exist in this el, ${cxt}`)
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
}
|
|
@@ -127,8 +128,6 @@ function ViewStreamSelector(cxt, sel) {
|
|
|
127
128
|
*/
|
|
128
129
|
selector.forEach = (fn) => Array.from(getNodeListArray(cxt, sel)).map(fn)
|
|
129
130
|
|
|
130
|
-
selector.getNodeListArray = () => getNodeListArray(cxt, sel)
|
|
131
|
-
|
|
132
131
|
/**
|
|
133
132
|
*
|
|
134
133
|
* Adds the class to the Element or to the NodeList.
|
|
@@ -142,7 +141,7 @@ function ViewStreamSelector(cxt, sel) {
|
|
|
142
141
|
const arr = getNodeListArray(cxt, sel)
|
|
143
142
|
const addClass = item => item.classList.add(c)
|
|
144
143
|
arr.forEach(addClass)
|
|
145
|
-
return
|
|
144
|
+
return selector
|
|
146
145
|
}
|
|
147
146
|
|
|
148
147
|
/**
|
|
@@ -156,7 +155,7 @@ function ViewStreamSelector(cxt, sel) {
|
|
|
156
155
|
item.classList.remove(c)
|
|
157
156
|
}
|
|
158
157
|
arr.forEach(removeClass)
|
|
159
|
-
return
|
|
158
|
+
return selector
|
|
160
159
|
}
|
|
161
160
|
|
|
162
161
|
/**
|
|
@@ -181,7 +180,7 @@ function ViewStreamSelector(cxt, sel) {
|
|
|
181
180
|
classStrArr.forEach(adder)
|
|
182
181
|
}
|
|
183
182
|
arr.forEach(setTheClass)
|
|
184
|
-
return
|
|
183
|
+
return selector
|
|
185
184
|
}
|
|
186
185
|
|
|
187
186
|
selector.unmount = () => {
|
|
@@ -205,23 +204,25 @@ function ViewStreamSelector(cxt, sel) {
|
|
|
205
204
|
bool ? item.classList.add(c) : item.classList.remove(c)
|
|
206
205
|
}
|
|
207
206
|
arr.forEach(toggleClass)
|
|
208
|
-
return
|
|
207
|
+
return selector
|
|
209
208
|
}
|
|
210
209
|
|
|
211
210
|
selector.toggle = (c, bool) => {
|
|
212
211
|
selector.toggleClass(c, bool)
|
|
213
|
-
return
|
|
212
|
+
return selector
|
|
214
213
|
}
|
|
215
214
|
|
|
216
215
|
/**
|
|
217
|
-
* Attaches html to the Selector's element
|
|
216
|
+
* Attaches html to the Selector's element (the first match when the
|
|
217
|
+
* selector matches multiple elements).
|
|
218
218
|
* @param htmlElement
|
|
219
219
|
*/
|
|
220
220
|
selector.appendChild = (htmlElement) => {
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
const el = selector.el
|
|
222
|
+
if (el !== null) {
|
|
223
|
+
el.appendChild(htmlElement)
|
|
223
224
|
} else {
|
|
224
|
-
|
|
225
|
+
spyneWarn(`Spyne Warning: The selector, ${sel} does not appear to be valid!`)
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
return selector.el
|
|
@@ -244,7 +245,7 @@ function ViewStreamSelector(cxt, sel) {
|
|
|
244
245
|
arr.forEach(addClass)
|
|
245
246
|
}
|
|
246
247
|
requestAnimationFrame(delayAddClass)
|
|
247
|
-
|
|
248
|
+
return selector
|
|
248
249
|
}
|
|
249
250
|
|
|
250
251
|
/**
|
|
@@ -253,19 +254,6 @@ function ViewStreamSelector(cxt, sel) {
|
|
|
253
254
|
* @param {String|HTMLElement} elSel The selector for the element.
|
|
254
255
|
* @desc Sets the class active HTMLElement from a NodeList.
|
|
255
256
|
*/
|
|
256
|
-
selector.setActiveItem2 = (c, elSel) => {
|
|
257
|
-
const arr = getNodeListArray(cxt, sel)
|
|
258
|
-
const currentEl = typeof (elSel) === 'string' ? getElOrList(cxt, elSel) : elSel
|
|
259
|
-
const toggleBool = item => item.isEqualNode(currentEl) ? item.classList.add(c) : item.classList.remove(c)
|
|
260
|
-
if (isNodeElement(currentEl) === true) {
|
|
261
|
-
arr.forEach(toggleBool)
|
|
262
|
-
} else if (isDevMode() === true) {
|
|
263
|
-
// console.log("SEL IS ",elSel,c);
|
|
264
|
-
console.warn(`Spyne Warning: The selector, ${elSel}, does not appear to be a valid item in setActiveItem: ${c}`)
|
|
265
|
-
}
|
|
266
|
-
return this
|
|
267
|
-
}
|
|
268
|
-
|
|
269
257
|
selector.setActiveItem = (c, elSel) => {
|
|
270
258
|
const arr = getNodeListArray(cxt, sel)
|
|
271
259
|
const currentEl = typeof elSel === 'string'
|
|
@@ -280,17 +268,17 @@ function ViewStreamSelector(cxt, sel) {
|
|
|
280
268
|
if (matchingEl) {
|
|
281
269
|
matchingEl.classList.add(c)
|
|
282
270
|
} else if (isDevMode() === true) {
|
|
283
|
-
|
|
271
|
+
spyneWarn(
|
|
284
272
|
`Spyne Warning: The selector, ${elSel}, is valid but does not match any item in setActiveItem: ${c}`
|
|
285
273
|
)
|
|
286
274
|
}
|
|
287
275
|
} else if (isDevMode() === true) {
|
|
288
|
-
|
|
276
|
+
spyneWarn(
|
|
289
277
|
`Spyne Warning: The selector, ${elSel}, does not appear to be a valid item in setActiveItem: ${c}`
|
|
290
278
|
)
|
|
291
279
|
}
|
|
292
280
|
|
|
293
|
-
return
|
|
281
|
+
return selector
|
|
294
282
|
}
|
|
295
283
|
|
|
296
284
|
/**
|
|
@@ -298,10 +286,24 @@ function ViewStreamSelector(cxt, sel) {
|
|
|
298
286
|
* @function el
|
|
299
287
|
*
|
|
300
288
|
* @desc
|
|
301
|
-
* getter
|
|
289
|
+
* getter that always resolves to a single element: the matched element,
|
|
290
|
+
* or the first item when the selector matches multiple elements (a
|
|
291
|
+
* warning is logged in debug mode).
|
|
302
292
|
*
|
|
303
293
|
* @returns
|
|
304
|
-
*
|
|
294
|
+
* An HTMLElement, or null when the selector matches nothing
|
|
295
|
+
* (mirroring querySelector)
|
|
296
|
+
*/
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
*
|
|
300
|
+
* @function els
|
|
301
|
+
*
|
|
302
|
+
* @desc
|
|
303
|
+
* getter that always resolves to an Array of the matched elements.
|
|
304
|
+
*
|
|
305
|
+
* @returns
|
|
306
|
+
* An Array of HTMLElements; empty when the selector matches nothing
|
|
305
307
|
*/
|
|
306
308
|
|
|
307
309
|
/**
|
|
@@ -312,7 +314,7 @@ function ViewStreamSelector(cxt, sel) {
|
|
|
312
314
|
* Determines the length of the NodeList
|
|
313
315
|
*
|
|
314
316
|
* @returns
|
|
315
|
-
* The
|
|
317
|
+
* The number of elements matched by the selector
|
|
316
318
|
*/
|
|
317
319
|
|
|
318
320
|
/**
|
|
@@ -326,24 +328,27 @@ function ViewStreamSelector(cxt, sel) {
|
|
|
326
328
|
* Boolean
|
|
327
329
|
*/
|
|
328
330
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
Object.defineProperty(selector, '
|
|
332
|
-
Object.defineProperty(selector, 'exists', { get: () => getNodeListArray(cxt, sel, false).length >= 1 })
|
|
333
|
-
Object.defineProperty(selector, 'exist', { get: () => getNodeListArray(cxt, sel, false).length >= 1 })
|
|
334
|
-
Object.defineProperty(selector, 'nodeList', { get: () => getNodeListArray(cxt, sel) })
|
|
335
|
-
Object.defineProperty(selector, 'arr', {
|
|
331
|
+
const getElsArray = (verboseBool = true) => Array.from(getNodeListArray(cxt, sel, verboseBool))
|
|
332
|
+
|
|
333
|
+
Object.defineProperty(selector, 'el', {
|
|
336
334
|
get: () => {
|
|
337
|
-
const
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
} else if (el.length === undefined) {
|
|
341
|
-
return [el]
|
|
342
|
-
} else {
|
|
343
|
-
return Array.from(el)
|
|
335
|
+
const list = getElsArray()
|
|
336
|
+
if (list.length > 1 && isDevMode() === true) {
|
|
337
|
+
spyneWarn(`Spyne Warning: el$("${sel !== undefined ? sel : cxt}").el matched ${list.length} elements; returning the first. Use .els for the full list.`)
|
|
344
338
|
}
|
|
339
|
+
return list.length >= 1 ? list[0] : null
|
|
345
340
|
}
|
|
346
341
|
})
|
|
342
|
+
Object.defineProperty(selector, 'els', { get: () => getElsArray() })
|
|
343
|
+
Object.defineProperty(selector, 'arr', { get: () => getElsArray() })
|
|
344
|
+
|
|
345
|
+
// Undocumented. Retains the pre-0.24 polymorphic el contract — a single
|
|
346
|
+
// element, a NodeList, or an empty NodeList depending on match count —
|
|
347
|
+
// to assist in debugging and migrating older SpyneJS apps.
|
|
348
|
+
Object.defineProperty(selector, 'elLegacy', { get: () => getElOrList(cxt, sel, true) })
|
|
349
|
+
Object.defineProperty(selector, 'len', { get: () => getNodeListArray(cxt, sel, false).length })
|
|
350
|
+
Object.defineProperty(selector, 'length', { get: () => getNodeListArray(cxt, sel, false).length })
|
|
351
|
+
Object.defineProperty(selector, 'exists', { get: () => getNodeListArray(cxt, sel, false).length >= 1 })
|
|
347
352
|
|
|
348
353
|
Object.defineProperty(selector, 'inline', { set: (val) => setInlineCss(val, cxt, sel), get: () => selector })
|
|
349
354
|
Object.defineProperty(selector, 'inlineCss', { set: (val) => setInlineCss(val, cxt, sel), get: () => selector })
|
|
@@ -191,6 +191,32 @@ describe('should test channel payload filters boolean correctness', () => {
|
|
|
191
191
|
|
|
192
192
|
return true
|
|
193
193
|
})
|
|
194
|
+
|
|
195
|
+
it('should return false for an invalid css selector instead of throwing', () => {
|
|
196
|
+
const cpFilter = new ChannelPayloadFilter('!!!not-a-selector')
|
|
197
|
+
const payloadBool = cpFilter(ChannelPayloadToTestFilters)
|
|
198
|
+
expect(payloadBool).to.be.false
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('should not match a structural lookalike of a selector match', () => {
|
|
202
|
+
// two identical siblings: the selector matches only the first, the
|
|
203
|
+
// payload el is the second — Element.matches tests the payload el
|
|
204
|
+
// itself, so the structurally equal non-matching twin must NOT pass
|
|
205
|
+
const ul = document.createElement('ul')
|
|
206
|
+
ul.innerHTML = '<li class="twin"><b>same</b></li><li class="twin"><b>same</b></li>'
|
|
207
|
+
document.body.appendChild(ul)
|
|
208
|
+
const cpFilter = new ChannelPayloadFilter('li.twin:first-child')
|
|
209
|
+
const payloadBool = cpFilter({ srcElement: { el: ul.querySelectorAll('li.twin')[1] } })
|
|
210
|
+
expect(payloadBool).to.be.false
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should match a detached element against its selector', () => {
|
|
214
|
+
const li = document.createElement('li')
|
|
215
|
+
li.className = 'detached-item'
|
|
216
|
+
const cpFilter = new ChannelPayloadFilter('li.detached-item')
|
|
217
|
+
const payloadBool = cpFilter({ srcElement: { el: li } })
|
|
218
|
+
expect(payloadBool).to.be.true
|
|
219
|
+
})
|
|
194
220
|
})
|
|
195
221
|
|
|
196
222
|
describe('it should test channel payload filter with data packer ', () => {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { SpyneChannelWindow } from '../../spyne/channels/spyne-channel-window'
|
|
2
|
+
import { Subject } from 'rxjs'
|
|
3
|
+
const { expect } = require('chai')
|
|
4
|
+
|
|
5
|
+
const conformedEvent = (detail) => {
|
|
6
|
+
const event = new CustomEvent('my_test_event', { detail })
|
|
7
|
+
return {
|
|
8
|
+
action: 'CHANNEL_WINDOW_MY_TEST_EVENT_EVENT',
|
|
9
|
+
payload: event,
|
|
10
|
+
srcElement: undefined,
|
|
11
|
+
event
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('window channel customEvents config conforming', () => {
|
|
16
|
+
it('should conform a string entry', () => {
|
|
17
|
+
const entry = SpyneChannelWindow.conformCustomEventConfig('my_event')
|
|
18
|
+
expect(entry).to.deep.equal({ name: 'my_event', operator: undefined, value: undefined })
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should conform an object entry with a buffer operator', () => {
|
|
22
|
+
const entry = SpyneChannelWindow.conformCustomEventConfig({ name: 'my_event', buffer: 400 })
|
|
23
|
+
expect(entry).to.deep.equal({ name: 'my_event', operator: 'buffer', value: 400 })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should conform debounce, throttle and count operators', () => {
|
|
27
|
+
expect(SpyneChannelWindow.conformCustomEventConfig({ name: 'e1', debounce: 250 }).operator).to.equal('debounce')
|
|
28
|
+
expect(SpyneChannelWindow.conformCustomEventConfig({ name: 'e2', throttle: 100 }).operator).to.equal('throttle')
|
|
29
|
+
expect(SpyneChannelWindow.conformCustomEventConfig({ name: 'e3', count: 50 }).operator).to.equal('count')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should pick the first operator when several are set', () => {
|
|
33
|
+
const entry = SpyneChannelWindow.conformCustomEventConfig({ name: 'my_event', debounce: 250, buffer: 400 })
|
|
34
|
+
expect(entry.operator).to.equal('buffer')
|
|
35
|
+
expect(entry.value).to.equal(400)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should skip invalid entries', () => {
|
|
39
|
+
expect(SpyneChannelWindow.conformCustomEventConfig(42)).to.equal(null)
|
|
40
|
+
expect(SpyneChannelWindow.conformCustomEventConfig({ buffer: 400 })).to.equal(null)
|
|
41
|
+
expect(SpyneChannelWindow.conformCustomEventConfig(null)).to.equal(null)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('window channel customEvents batching operators', () => {
|
|
46
|
+
it('should map a batch to one payload with detail as the details array', () => {
|
|
47
|
+
const batch = [conformedEvent({ id: 1 }), conformedEvent({ id: 2 }), conformedEvent({ id: 3 })]
|
|
48
|
+
const { action, payload, event } = SpyneChannelWindow.mapBatchedEvents(batch)
|
|
49
|
+
expect(action).to.equal('CHANNEL_WINDOW_MY_TEST_EVENT_EVENT')
|
|
50
|
+
expect(payload.detail).to.deep.equal([{ id: 1 }, { id: 2 }, { id: 3 }])
|
|
51
|
+
expect(payload.isBatch).to.be.true
|
|
52
|
+
expect(payload.batchCount).to.equal(3)
|
|
53
|
+
expect(event).to.equal(batch[2].event)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should close a buffer batch after the quiet gap and emit once', (done) => {
|
|
57
|
+
const source$ = new Subject()
|
|
58
|
+
const obs$ = SpyneChannelWindow.customizeCustomEventObservable(source$, 'buffer', 30)
|
|
59
|
+
const emissions = []
|
|
60
|
+
obs$.subscribe(p => emissions.push(p))
|
|
61
|
+
|
|
62
|
+
source$.next(conformedEvent({ id: 1 }))
|
|
63
|
+
source$.next(conformedEvent({ id: 2 }))
|
|
64
|
+
setTimeout(() => source$.next(conformedEvent({ id: 3 })), 10)
|
|
65
|
+
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
expect(emissions.length).to.equal(1)
|
|
68
|
+
expect(emissions[0].payload.detail).to.deep.equal([{ id: 1 }, { id: 2 }, { id: 3 }])
|
|
69
|
+
done()
|
|
70
|
+
}, 100)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should emit separate buffer batches per burst', (done) => {
|
|
74
|
+
const source$ = new Subject()
|
|
75
|
+
const obs$ = SpyneChannelWindow.customizeCustomEventObservable(source$, 'buffer', 20)
|
|
76
|
+
const emissions = []
|
|
77
|
+
obs$.subscribe(p => emissions.push(p))
|
|
78
|
+
|
|
79
|
+
source$.next(conformedEvent({ id: 1 }))
|
|
80
|
+
setTimeout(() => source$.next(conformedEvent({ id: 2 })), 60)
|
|
81
|
+
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
expect(emissions.length).to.equal(2)
|
|
84
|
+
expect(emissions[0].payload.batchCount).to.equal(1)
|
|
85
|
+
expect(emissions[1].payload.batchCount).to.equal(1)
|
|
86
|
+
done()
|
|
87
|
+
}, 140)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should batch every n events with the count operator', () => {
|
|
91
|
+
const source$ = new Subject()
|
|
92
|
+
const obs$ = SpyneChannelWindow.customizeCustomEventObservable(source$, 'count', 2)
|
|
93
|
+
const emissions = []
|
|
94
|
+
obs$.subscribe(p => emissions.push(p))
|
|
95
|
+
|
|
96
|
+
source$.next(conformedEvent({ id: 1 }))
|
|
97
|
+
source$.next(conformedEvent({ id: 2 }))
|
|
98
|
+
source$.next(conformedEvent({ id: 3 }))
|
|
99
|
+
source$.next(conformedEvent({ id: 4 }))
|
|
100
|
+
source$.next(conformedEvent({ id: 5 }))
|
|
101
|
+
|
|
102
|
+
expect(emissions.length).to.equal(2)
|
|
103
|
+
expect(emissions[0].payload.detail).to.deep.equal([{ id: 1 }, { id: 2 }])
|
|
104
|
+
expect(emissions[1].payload.detail).to.deep.equal([{ id: 3 }, { id: 4 }])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should pass only the latest event through debounce', (done) => {
|
|
108
|
+
const source$ = new Subject()
|
|
109
|
+
const obs$ = SpyneChannelWindow.customizeCustomEventObservable(source$, 'debounce', 20)
|
|
110
|
+
const emissions = []
|
|
111
|
+
obs$.subscribe(p => emissions.push(p))
|
|
112
|
+
|
|
113
|
+
source$.next(conformedEvent({ id: 1 }))
|
|
114
|
+
source$.next(conformedEvent({ id: 2 }))
|
|
115
|
+
source$.next(conformedEvent({ id: 3 }))
|
|
116
|
+
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
expect(emissions.length).to.equal(1)
|
|
119
|
+
// debounced events keep the single-event payload shape (payload = CustomEvent)
|
|
120
|
+
expect(emissions[0].payload.detail).to.deep.equal({ id: 3 })
|
|
121
|
+
done()
|
|
122
|
+
}, 80)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should return the observable untouched when there is no operator', () => {
|
|
126
|
+
const source$ = new Subject()
|
|
127
|
+
const obs$ = SpyneChannelWindow.customizeCustomEventObservable(source$, undefined, undefined)
|
|
128
|
+
expect(obs$).to.equal(source$)
|
|
129
|
+
})
|
|
130
|
+
})
|
package/src/tests/index.test.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Silence SpyneJS developer-hint warnings across all test bundles.
|
|
2
|
+
// Expected-failure cases (invalid selectors, blocked URIs, policy removals)
|
|
3
|
+
// would otherwise flood the karma reporter. Security warnings still print.
|
|
4
|
+
window.SPYNE_SUPPRESS_WARNINGS = true
|
|
5
|
+
|
|
1
6
|
import { SpyneApp } from '../spyne/spyne'
|
|
2
7
|
|
|
3
8
|
// import {AppView} from "../app/app-view";
|