spyne 0.23.0 → 0.25.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.
@@ -1,6 +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'
5
+ import { sanitizeAttribute, applyIframeHardening } from '../utils/sanitize-data.js'
4
6
  import { is, defaultTo, pick, mapObjIndexed, forEachObjIndexed, pipe } from 'ramda'
5
7
 
6
8
  class DomElement {
@@ -53,15 +55,33 @@ class DomElement {
53
55
  }
54
56
 
55
57
  setElAttrs(el, params) {
58
+ const testMode = this.testMode === true
56
59
  const addAttributes = (val, key) => {
57
60
  const addToDataset = (val, key) => { el.dataset[key] = val }
58
61
  if (key === 'dataset') {
62
+ // Dataset values are inert text and are the framework's payload
63
+ // surface; they are applied as-is.
59
64
  forEachObjIndexed(addToDataset, val)
60
- } else {
61
- el.setAttribute(key, val)
65
+ return
62
66
  }
67
+
68
+ if (testMode !== true) {
69
+ const { allowed, value } = sanitizeAttribute(key, val, el.tagName)
70
+ if (allowed !== true) {
71
+ spyneWarn(`SPYNE WARNING: The attribute "${key}" was removed during sanitization.`, el)
72
+ return
73
+ }
74
+ val = value
75
+ }
76
+
77
+ el.setAttribute(key, val)
63
78
  }
64
79
  this.getProp('attrs').forEach(addAttributes)
80
+
81
+ if (testMode !== true) {
82
+ applyIframeHardening(el)
83
+ }
84
+
65
85
  return el
66
86
  }
67
87
 
@@ -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
- console.warn(`Spyne Warning: the el object is not a valid single element, ${el}`)
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
- console.warn(`Spyne Warning: the selector, ${sel} does not exist in this el, ${cxt}`)
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 this
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 this
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 this
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 this
207
+ return selector
209
208
  }
210
209
 
211
210
  selector.toggle = (c, bool) => {
212
211
  selector.toggleClass(c, bool)
213
- return this
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
- if (selector.el.length !== 0) {
222
- selector.el.appendChild(htmlElement)
221
+ const el = selector.el
222
+ if (el !== null) {
223
+ el.appendChild(htmlElement)
223
224
  } else {
224
- console.warn(`Spyne Warning: The selector, ${sel} does not appear to be valid!`)
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
- // window.setTimeout(delayAddClass, 1);
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
- console.warn(
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
- console.warn(
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 this
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 for the selector
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
- * The a single element or a NodeList from the selector
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 length of the selector as a NodeList
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
- Object.defineProperty(selector, 'el', { get: () => getElOrList(cxt, sel, true) })
330
- Object.defineProperty(selector, 'els', { get: () => getNodeListArray(cxt, sel) })
331
- Object.defineProperty(selector, 'len', { get: () => getNodeListArray(cxt, sel, false).length })
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 el = getElOrList(cxt, sel, true)
338
- if (el === undefined) {
339
- return []
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 })
@@ -1,7 +1,7 @@
1
1
  //const { expect, assert } = require('chai')
2
2
  //const { ChannelFetch } = require('../../spyne/channels/channel-fetch-class')
3
3
  const { expect, assert } = require('chai')
4
- import { ChannelFetch } from '../../spyne/spyne';
4
+ import { ChannelFetch, SpyneApp } from '../../spyne/spyne';
5
5
 
6
6
  describe('should test ChannelFetch', () => {
7
7
  const mapFn = (p) => {
@@ -95,4 +95,91 @@ describe('should test ChannelFetch', () => {
95
95
  const mapTypeUndefined = ChannelFetch.validateMapMethod(props, 'MY_TEST_NAME_2', true)
96
96
  // console.log('map methods is not of correct type added and is valid4 ',{mapTypeUndefined}) ; expect(mapTypeUndefined).to.be.false;
97
97
  })
98
+
99
+ describe('conformed error action', () => {
100
+ const createPayloadContext = () => {
101
+ const context = {
102
+ props: { name: channelName },
103
+ sentActions: [],
104
+ createChannelPayloadItem(payload, action) {
105
+ this.sentActions.push({ payload, action })
106
+ }
107
+ }
108
+ return context
109
+ }
110
+
111
+ it('should register the generic error action', () => {
112
+ const context = { props: {} }
113
+ const actionsArr = ChannelFetch.prototype.addRegisteredActions.call(context)
114
+
115
+ expect(actionsArr).to.include('CHANNEL_ERROR_EVENT')
116
+ expect(actionsArr).to.include('CHANNEL_RESPONSE_EVENT')
117
+ })
118
+
119
+ it('should register the instance-derived error action alongside the response action', () => {
120
+ const context = {
121
+ props: {
122
+ extendedActionsArr: [
123
+ `${channelName}_RESPONSE_EVENT`,
124
+ `${channelName}_ERROR_EVENT`,
125
+ [`${channelName}_REQUEST_EVENT`, 'onFetchUpdate']
126
+ ]
127
+ }
128
+ }
129
+ const actionsArr = ChannelFetch.prototype.addRegisteredActions.call(context)
130
+
131
+ expect(actionsArr).to.include(`${channelName}_ERROR_EVENT`)
132
+ expect(actionsArr).to.include(`${channelName}_RESPONSE_EVENT`)
133
+ })
134
+
135
+ it('should emit the response action for a successful fetch payload', () => {
136
+ const context = createPayloadContext()
137
+ const streamItem = { id: 1, title: 'foo' }
138
+
139
+ ChannelFetch.prototype.onFetchReturned.call(context, streamItem)
140
+
141
+ expect(context.sentActions[0].action).to.equal(`${channelName}_RESPONSE_EVENT`)
142
+ expect(context.sentActions[0].payload).to.deep.equal(streamItem)
143
+ })
144
+
145
+ it('should emit the error action for a conformed fetch error payload', () => {
146
+ const context = createPayloadContext()
147
+ const streamItem = {
148
+ isChannelFetchError: true,
149
+ isError: true,
150
+ error: true,
151
+ errorType: 'FETCH_HTTP_ERROR',
152
+ message: 'Fetch request failed with status 404 Not Found',
153
+ status: 404,
154
+ statusText: 'Not Found',
155
+ url: 'https://jsonplaceholder.typicode.com/posts/does-not-exist'
156
+ }
157
+
158
+ ChannelFetch.prototype.onFetchReturned.call(context, streamItem)
159
+
160
+ expect(context.sentActions[0].action).to.equal(`${channelName}_ERROR_EVENT`)
161
+ expect(context.sentActions[0].payload).to.deep.equal(streamItem)
162
+ })
163
+
164
+ it('should not route a payload to the error action without the discriminator flag', () => {
165
+ const context = createPayloadContext()
166
+ const streamItem = { isError: true, message: 'data that merely resembles an error' }
167
+
168
+ ChannelFetch.prototype.onFetchReturned.call(context, streamItem)
169
+
170
+ expect(context.sentActions[0].action).to.equal(`${channelName}_RESPONSE_EVENT`)
171
+ })
172
+
173
+ it('should add the derived error action to channelActions on a constructed instance', () => {
174
+ SpyneApp.init({})
175
+
176
+ const instanceChannelName = 'CHANNEL_FETCH_ERROR_ACTION_TEST'
177
+ const fetchChannel = new ChannelFetch(instanceChannelName, { url, pause: true })
178
+
179
+ expect(fetchChannel.channelActions).to.have.property(`${instanceChannelName}_ERROR_EVENT`)
180
+ expect(fetchChannel.channelActions).to.have.property(`${instanceChannelName}_RESPONSE_EVENT`)
181
+ expect(fetchChannel.channelActions).to.have.property(`${instanceChannelName}_REQUEST_EVENT`)
182
+ expect(fetchChannel.channelActions).to.have.property('CHANNEL_ERROR_EVENT')
183
+ })
184
+ })
98
185
  })
@@ -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";
@@ -49,9 +49,11 @@ describe('ChannelFetchUtil Tests', () => {
49
49
 
50
50
  describe('stringify body method', () => {
51
51
  it('should convert body object to string', () => {
52
- let bodyStr = ChannelFetchUtil.stringifyBodyIfItExists(propsPost)
53
- bodyStr = R.omit(['mapFn'], bodyStr)
52
+ const result = ChannelFetchUtil.stringifyBodyIfItExists(propsPost)
53
+ const bodyStr = R.omit(['mapFn', 'headers'], result)
54
+
54
55
  expect(bodyStr).to.deep.equal(propsStringified)
56
+ expect(result.headers.get('Content-Type')).to.equal('application/json')
55
57
  })
56
58
 
57
59
  it('should not parse body when its a string', () => {
@@ -127,4 +129,44 @@ describe('ChannelFetchUtil Tests', () => {
127
129
  expect(channelFetchUtil.mapFn).to.be.a('function')
128
130
  })
129
131
  })
132
+
133
+ describe('conformed fetch error payload', () => {
134
+ const metadata = {
135
+ channelName: 'CHANNEL_MY_FETCHED_DATA',
136
+ url,
137
+ responseType: 'json'
138
+ }
139
+
140
+ it('should conform an http error into a flat payload with the discriminator flag', () => {
141
+ const httpError = ChannelFetchUtil.createFetchError({
142
+ errorType: 'FETCH_HTTP_ERROR',
143
+ message: 'Fetch request failed with status 404 Not Found',
144
+ metadata: { ...metadata, status: 404, statusText: 'Not Found', ok: false },
145
+ rawBody: '{"error":"not found"}'
146
+ })
147
+
148
+ const errorPayload = ChannelFetchUtil.createFetchErrorPayload(httpError, metadata)
149
+
150
+ expect(errorPayload.isChannelFetchError).to.be.true
151
+ expect(errorPayload.isError).to.be.true
152
+ expect(errorPayload.errorType).to.equal('FETCH_HTTP_ERROR')
153
+ expect(errorPayload.status).to.equal(404)
154
+ expect(errorPayload.statusText).to.equal('Not Found')
155
+ expect(errorPayload.url).to.equal(url)
156
+ expect(errorPayload.channelName).to.equal('CHANNEL_MY_FETCHED_DATA')
157
+ expect(errorPayload.rawBodyPreview).to.equal('{"error":"not found"}')
158
+ })
159
+
160
+ it('should conform a network error using fallback metadata and error type', () => {
161
+ const networkError = new TypeError('Failed to fetch')
162
+
163
+ const errorPayload = ChannelFetchUtil.createFetchErrorPayload(networkError, metadata)
164
+
165
+ expect(errorPayload.isChannelFetchError).to.be.true
166
+ expect(errorPayload.errorType).to.equal('FETCH_UNKNOWN_ERROR')
167
+ expect(errorPayload.message).to.equal('Failed to fetch')
168
+ expect(errorPayload.url).to.equal(url)
169
+ expect(errorPayload.status).to.be.undefined
170
+ })
171
+ })
130
172
  })