node-turbo 1.0.1 → 1.1.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.
@@ -9,7 +9,7 @@ const debug = db('node-turbo:readable');
9
9
  * This class represents a readable stream which reads
10
10
  * messages/elements from a Turbo Stream instance.
11
11
  *
12
- * @extends {stream~Readable}
12
+ * @extends {node:stream~Readable}
13
13
  * @since 1.0.0
14
14
  */
15
15
  export class TurboReadable extends Readable {
@@ -18,6 +18,7 @@ export class TurboReadable extends Readable {
18
18
  * The Turbo Stream instance to create the readable stream for.
19
19
  *
20
20
  * @type {TurboStream}
21
+ * @since 1.0.0
21
22
  */
22
23
  _turboStream;
23
24
 
@@ -34,6 +35,7 @@ export class TurboReadable extends Readable {
34
35
  * @param {TurboStream} turboStream - The Turbo Stream instance to create the readable stream for.
35
36
  * @param {Object} [opts] - The options for the readable stream.
36
37
  * @throws {Error} if `turboStream` is not a TurboStream instance
38
+ * @since 1.0.0
37
39
  */
38
40
  constructor(turboStream, opts) {
39
41
  debug('new TurboReadable()', opts);
@@ -61,6 +63,7 @@ export class TurboReadable extends Readable {
61
63
  * Pushes a Turbo Stream element into the read queue.
62
64
  *
63
65
  * @param {TurboStreamElement} el - The Turbo Stream element.
66
+ * @since 1.0.0
64
67
  */
65
68
  _pushElement(el) {
66
69
  debug('_pushElement()');
@@ -73,6 +76,7 @@ export class TurboReadable extends Readable {
73
76
  * as handler for the `element` event.
74
77
  *
75
78
  * @type {Function}
79
+ * @since 1.0.0
76
80
  */
77
81
  _boundPush = this._pushElement.bind(this);
78
82
 
@@ -82,6 +86,7 @@ export class TurboReadable extends Readable {
82
86
  * This implementation does nothing.
83
87
  * (Normally, push data would be pushed into the read queue here.)
84
88
  *
89
+ * @since 1.0.0
85
90
  * @todo Do we need backpressure handling?
86
91
  */
87
92
  _read() {
@@ -94,6 +99,7 @@ export class TurboReadable extends Readable {
94
99
  * for the event `element` is removed and the configuration restored.
95
100
  *
96
101
  * @param {Error} err - The error object, if thrown.
102
+ * @since 1.0.0
97
103
  */
98
104
  _destroy(err) {
99
105
  debug('_destroy()', err);
@@ -115,7 +121,8 @@ export class TurboReadable extends Readable {
115
121
  * Pushes `null` to the readable buffer to signal the end of the input.
116
122
  *
117
123
  * @todo Should we call this end() or is this confusing because normally
118
- * only writable streams have this function.
124
+ * only writable streams have this function.
125
+ * @since 1.0.0
119
126
  */
120
127
  done() {
121
128
  debug('done()');
@@ -13,31 +13,20 @@ import { AttributeMissingError, AttributeMalformedError, AttributeInvalidError }
13
13
  export class TurboStreamElement extends TurboElement {
14
14
 
15
15
  /**
16
- * Validates the attributes. `attributes.target` (or `attributes.targets`) and `attributes.action`
17
- * are mandatory. Gets called by the constructor.
16
+ * Validates the attributes. `attributes.action` is mandatory.
17
+ * `attributes.target` (or `attributes.targets`) is mandatory for any action but 'refresh'.
18
+ * Gets called by the constructor.
18
19
  *
19
20
  * @throws {AttributeMissingError} when mandatory attributes are missing.
20
21
  * @throws {AttributeMalformedError} when mandatory attributes are malformed.
21
22
  * @throws {AttributeInvalidError} when attributes are invalid.
23
+ * @since 1.0.0
22
24
  */
23
25
  validate() {
24
26
  if (typeof this.attributes === 'undefined' || !isPlainObject(this.attributes)) {
25
27
  throw new AttributeMissingError('TurboStream: Attributes are missing.');
26
28
  }
27
29
 
28
- if (!('target' in this.attributes) && !('targets' in this.attributes)) {
29
- throw new AttributeMissingError('TurboStream: Attribute "target" or "targets" is missing.');
30
- }
31
-
32
- if ('target' in this.attributes && 'targets' in this.attributes) {
33
- throw new AttributeInvalidError('TurboStream: Attributes "target" and "targets" exclude each other.');
34
- }
35
-
36
- if ((typeof this.attributes.target !== 'string' || this.attributes.target.length === 0) &&
37
- (typeof this.attributes.targets !== 'string' || this.attributes.targets.length === 0)) {
38
- throw new AttributeMalformedError('TurboStream: Attribute "target"/"targets" must be a string with non-zero length.');
39
- }
40
-
41
30
  if (!('action' in this.attributes)) {
42
31
  throw new AttributeMissingError('TurboStream: Attribute "action" is missing.');
43
32
  }
@@ -45,22 +34,39 @@ import { AttributeMissingError, AttributeMalformedError, AttributeInvalidError }
45
34
  if (typeof this.attributes.action !== 'string' || this.attributes.action.length === 0) {
46
35
  throw new AttributeMalformedError('TurboStream: Attribute "action" must be a string with non-zero length.');
47
36
  }
37
+
38
+ if (this.attributes.action !== 'refresh') {
39
+ if (!('target' in this.attributes) && !('targets' in this.attributes)) {
40
+ throw new AttributeMissingError('TurboStream: Attribute "target" or "targets" is missing.');
41
+ }
42
+
43
+ if ('target' in this.attributes && 'targets' in this.attributes) {
44
+ throw new AttributeInvalidError('TurboStream: Attributes "target" and "targets" exclude each other.');
45
+ }
46
+
47
+ if ((typeof this.attributes.target !== 'string' || this.attributes.target.length === 0) &&
48
+ (typeof this.attributes.targets !== 'string' || this.attributes.targets.length === 0)) {
49
+ throw new AttributeMalformedError('TurboStream: Attribute "target"/"targets" must be a string with non-zero length.');
50
+ }
51
+ }
48
52
  }
49
53
 
50
54
 
51
55
  /**
52
56
  * Renders this Turbo Stream element as HTML string. Omits `<template>[content]<template>` when the attribute
53
- * `action` is 'remove'.
57
+ * `action` is 'remove' or 'refresh'.
54
58
  *
55
59
  * @returns {String} The rendered HTML fragment.
56
60
  * @see https://turbo.hotwired.dev/handbook/streams#stream-messages-and-actions
61
+ * @since 1.0.0
57
62
  */
58
63
  render() {
59
- if (this.attributes.action === 'remove') {
60
- return `<turbo-stream ${this.renderAttributesAsHtml()}></turbo-stream>`;
61
- }
62
- else {
63
- return `<turbo-stream ${this.renderAttributesAsHtml()}><template>${this.content}</template></turbo-stream>`;
64
+ switch (this.attributes.action) {
65
+ case 'remove':
66
+ case 'refresh':
67
+ return `<turbo-stream ${this.renderAttributesAsHtml()}></turbo-stream>`;
68
+ default:
69
+ return `<turbo-stream ${this.renderAttributesAsHtml()}><template>${this.content}</template></turbo-stream>`;
64
70
  }
65
71
  }
66
72
 
@@ -9,7 +9,7 @@ const debug = db('node-turbo:turbostream');
9
9
  /**
10
10
  * A Turbo Stream message.
11
11
  *
12
- * @extends {events~EventEmitter}
12
+ * @extends {node:events~EventEmitter}
13
13
  * @since 1.0.0
14
14
  */
15
15
  export class TurboStream extends EventEmitter {
@@ -19,6 +19,7 @@ export class TurboStream extends EventEmitter {
19
19
  *
20
20
  * @type {Object<String, String>}
21
21
  * @property {Boolean} buffer - Should elements be added to the buffer (default: true)?
22
+ * @since 1.0.0
22
23
  */
23
24
  config = {
24
25
  buffer: true
@@ -29,6 +30,7 @@ export class TurboStream extends EventEmitter {
29
30
  * Array of buffered elements. Gets filled if `config.buffer` is `true`.
30
31
  *
31
32
  * @type {Array}
33
+ * @since 1.0.0
32
34
  */
33
35
  elements = [];
34
36
 
@@ -39,17 +41,20 @@ export class TurboStream extends EventEmitter {
39
41
  * @see https://turbo.hotwired.dev/handbook/streams#streaming-from-http-responses
40
42
  * @type {String}
41
43
  * @static
44
+ * @since 1.0.0
42
45
  */
43
46
  static MIME_TYPE = 'text/vnd.turbo-stream.html';
44
47
 
45
48
 
46
49
  /**
47
- * List of all supported actions:
48
- * 'append', 'prepend', 'replace', 'update', 'remove', 'before' and 'after'.
50
+ * List of all supported official actions:
51
+ * `append`, `prepend`, `replace`, `update`, `remove`, `before`, `after`
52
+ * and `refresh`.
49
53
  *
50
54
  * @see https://turbo.hotwired.dev/handbook/streams#stream-messages-and-actions
51
55
  * @type {Array}
52
56
  * @static
57
+ * @since 1.0.0
53
58
  */
54
59
  static ACTIONS = [
55
60
  'append',
@@ -58,7 +63,9 @@ export class TurboStream extends EventEmitter {
58
63
  'update',
59
64
  'remove',
60
65
  'before',
61
- 'after'
66
+ 'after',
67
+ 'morph',
68
+ 'refresh'
62
69
  ];
63
70
 
64
71
 
@@ -66,6 +73,7 @@ export class TurboStream extends EventEmitter {
66
73
  * The number of buffered Turbo Stream elements.
67
74
  *
68
75
  * @type {Number}
76
+ * @since 1.0.0
69
77
  */
70
78
  get length() {
71
79
  return this.elements.length;
@@ -78,6 +86,7 @@ export class TurboStream extends EventEmitter {
78
86
  *
79
87
  * @param {Object<String, String>} [attributes] - The attributes of this element.
80
88
  * @param {String} [content] - The HTML content of this element.
89
+ * @since 1.0.0
81
90
  */
82
91
  constructor(attributes, content) {
83
92
  super();
@@ -94,6 +103,7 @@ export class TurboStream extends EventEmitter {
94
103
  * @param {Object} config - New configuration.
95
104
  * @emits config
96
105
  * @returns {TurboStream} The instance for chaining.
106
+ * @since 1.0.0
97
107
  */
98
108
  updateConfig(config) {
99
109
  if (config) {
@@ -114,6 +124,7 @@ export class TurboStream extends EventEmitter {
114
124
  * @param {String} content - The HTML content of the element.
115
125
  * @emits element
116
126
  * @returns {TurboStream} The instance for chaining.
127
+ * @since 1.0.0
117
128
  */
118
129
  addElement(attributesOrElement, content) {
119
130
  let el;
@@ -140,6 +151,7 @@ export class TurboStream extends EventEmitter {
140
151
  *
141
152
  * @emits clear
142
153
  * @returns {TurboStream} The instance for chaining.
154
+ * @since 1.0.0
143
155
  */
144
156
  clear() {
145
157
  this.elements = [];
@@ -154,6 +166,7 @@ export class TurboStream extends EventEmitter {
154
166
  *
155
167
  * @returns {String|null} The rendered Turbo Stream HTML fragment or null if there were no buffered elements.
156
168
  * @emits render
169
+ * @since 1.0.0
157
170
  */
158
171
  render() {
159
172
  const arr = this.renderElements();
@@ -172,6 +185,7 @@ export class TurboStream extends EventEmitter {
172
185
  * If there are buffered elements, renders them and returns an array with the HTML fragments.
173
186
  *
174
187
  * @returns {Array|null} The rendered Turbo Stream HTML fragments as array or null if there were no buffered elements.
188
+ * @since 1.0.0
175
189
  */
176
190
  renderElements() {
177
191
  if (this.elements.length > 0) {
@@ -188,6 +202,7 @@ export class TurboStream extends EventEmitter {
188
202
  * @returns {String|null} The rendered Turbo Stream HTML fragment or null if there were no buffered elements.
189
203
  * @emits {render}
190
204
  * @emits {clear}
205
+ * @since 1.0.0
191
206
  */
192
207
  flush() {
193
208
  const html = this.render();
@@ -204,6 +219,7 @@ export class TurboStream extends EventEmitter {
204
219
  * @param {String} target - The target ID.
205
220
  * @param {String} content - The HTML content of the element.
206
221
  * @returns {TurboStream} The instance for chaining.
222
+ * @since 1.0.0
207
223
  */
208
224
  custom(action, target, content) {
209
225
  return this.addElement({ action, target }, content);
@@ -217,6 +233,7 @@ export class TurboStream extends EventEmitter {
217
233
  * @param {String} targets - The query string targeting multiple DOM elements.
218
234
  * @param {String} content - The HTML content of the element.
219
235
  * @returns {TurboStream} The instance for chaining.
236
+ * @since 1.0.0
220
237
  */
221
238
  customAll(action, targets, content) {
222
239
  return this.addElement({ action, targets }, content);
@@ -224,7 +241,14 @@ export class TurboStream extends EventEmitter {
224
241
 
225
242
 
226
243
  /**
244
+ * Creates a readable stream.
227
245
  *
246
+ * @param {Object<String, String>} opts - The options for stream creation.
247
+ * @param {Boolean} opts.continuous - If true, a TurboReadable instance is returned.
248
+ * If false, a readable stream created from the buffered items is returned.
249
+ * @param {Object<String, String>} streamOptions - The options for the readable stream itself.
250
+ * @returns {stream.Readable|TurboReadable} Either a readable stream or a TurboReadable instance.
251
+ * @since 1.0.0
228
252
  */
229
253
  createReadableStream(opts, streamOptions = {}) {
230
254
  opts = Object.assign({}, { continuous: true }, opts);
@@ -239,11 +263,34 @@ export class TurboStream extends EventEmitter {
239
263
  return Readable.from(this.length > 0 ? this.render().split('\n') : [], streamOptions);
240
264
  }
241
265
  }
266
+
267
+
268
+ /**
269
+ * Adds a Turbo Stream Element with the action 'refresh' to the message.
270
+ *
271
+ * @param {String|Object<String, String>} requestIdOrAttributes - Either the request ID as string or all attributes as an object.
272
+ * @returns {TurboStream} The instance for chaining.
273
+ * @since 1.1.0
274
+ */
275
+ refresh(requestIdOrAttributes) {
276
+ if (typeof requestIdOrAttributes === 'string') {
277
+ return this.addElement({ action: 'refresh', 'request-id': requestIdOrAttributes });
278
+ }
279
+ else {
280
+ return this.addElement(Object.assign({ action: 'refresh' }, requestIdOrAttributes));
281
+ }
282
+ }
283
+
242
284
  }
243
285
 
244
286
 
245
287
  // Add convenience functions for all supported actions.
246
288
  TurboStream.ACTIONS.forEach(action => {
289
+ // This action needs special treatment.
290
+ if (action === 'refresh') {
291
+ return;
292
+ }
293
+
247
294
  TurboStream.prototype[action] = function(targetOrAttributes, content) {
248
295
  if (typeof targetOrAttributes === 'string') {
249
296
  return this.addElement({ action: action, target: targetOrAttributes }, content);
@@ -255,96 +302,5 @@ TurboStream.ACTIONS.forEach(action => {
255
302
 
256
303
  TurboStream.prototype[`${action}All`] = function(targets, content) {
257
304
  return this.addElement({ action: action, targets }, content);
258
- };
305
+ };
259
306
  });
260
-
261
- // Dynamic functions:
262
-
263
- /**
264
- * @name #append
265
- * @function
266
- * @description Adds a Turbo Stream element with the action 'append' to the message.
267
- * @param {String|Object<String, String>} targetOrAttributes - Either the target ID as string or all attributes as object.
268
- * @param {String} content - The HTML content of the element.
269
- */
270
-
271
- /**
272
- * @name #prepend
273
- * @function
274
- * @description Adds a Turbo Stream element with the action 'prepend' to the message.
275
- * @param {(String|Object<String, String>)} targetOrAttributes - Either the target ID as string or all attributes as object.
276
- * @param {String} content - The HTML content of the element.
277
- */
278
-
279
- /**
280
- * @name #replace
281
- * @function
282
- * @description Adds a Turbo Stream element with the action 'replace' to the message.
283
- * @param {(String|Object<String, String>)} targetOrAttributes - Either the target ID as string or all attributes as object.
284
- * @param {String} content - The HTML content of the element.
285
- */
286
-
287
- /**
288
- * @name #update
289
- * @function
290
- * @description Adds a Turbo Stream element with the action 'update' to the message.
291
- * @param {String|Object<String, String>} targetOrAttributes - Either the target ID as string or all attributes as object.
292
- * @param {String} content - The HTML content of the element.
293
- */
294
-
295
- /**
296
- * @name #remove
297
- * @function
298
- * @description Adds a Turbo Stream element with the action 'remove' to the message.
299
- * @param {String|Object<String, String>} targetOrAttributes - Either the target ID as string or all attributes as object.
300
- * @param {String} content - The HTML content of the element.
301
- */
302
-
303
- /**
304
- * @name #before
305
- * @function
306
- * @description Adds a Turbo Stream element with the action 'before' to the message.
307
- * @param {String|Object<String, String>} targetOrAttributes - Either the target ID as string or all attributes as object.
308
- * @param {String} content - The HTML content of the element.
309
- */
310
-
311
- /**
312
- * @name after
313
- * @function
314
- * @description Adds a Turbo Stream element with the action 'after' to the message.
315
- * @param {String|Object<String, String>} targetOrAttributes - Either the target ID as string or all attributes as object.
316
- * @param {String} content - The HTML content of the element.
317
- */
318
-
319
- // Events:
320
-
321
- /**
322
- * Event element.
323
- * Gets fired when a Turbo Stream element has been added to a message.
324
- *
325
- * @event element
326
- * @param {TurboStreamElement} - The added element.
327
- */
328
-
329
- /**
330
- * Event render.
331
- * Gets fired when a Turbo Stream message has been rendered.
332
- *
333
- * @event render
334
- * @param {String} - The rendered HTML fragment.
335
- */
336
-
337
- /**
338
- * Event config.
339
- * Gets fired when the config has been updated.
340
- *
341
- * @event config
342
- * @param {Object} - The updated config object.
343
- */
344
-
345
- /**
346
- * Event clear.
347
- * Gets fired when the buffer is cleared.
348
- *
349
- * @event clear
350
- */
@@ -6,6 +6,7 @@ import { TurboStream } from '#core';
6
6
  * This class represents a Turbo Stream message with added functionality for WebSockets.
7
7
  *
8
8
  * @extends {TurboStream}
9
+ * @since 1.0.0
9
10
  */
10
11
  export class WsTurboStream extends TurboStream {
11
12
 
@@ -13,6 +14,7 @@ export class WsTurboStream extends TurboStream {
13
14
  * The WebSocket instance to send to.
14
15
  *
15
16
  * @type {WebSocket}
17
+ * @since 1.0.0
16
18
  */
17
19
  ws;
18
20
 
@@ -23,6 +25,7 @@ export class WsTurboStream extends TurboStream {
23
25
  * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
24
26
  * @type {Number}
25
27
  * @static
28
+ * @since 1.0.0
26
29
  */
27
30
  static OPEN = 1;
28
31
 
@@ -34,6 +37,7 @@ export class WsTurboStream extends TurboStream {
34
37
  * @static
35
38
  * @param {WebSocket} ws - The WebSocket instance to send to.
36
39
  * @returns {WsTurboStream} - A new WsTurboStream instance.
40
+ * @since 1.0.0
37
41
  */
38
42
  static use(ws) {
39
43
  return new this(ws, { buffer: false });
@@ -46,6 +50,7 @@ export class WsTurboStream extends TurboStream {
46
50
  * @param {WebSocket} ws - The WebSocket to send to.
47
51
  * @param {Object} [config] - The config to override.
48
52
  * @listens config
53
+ * @since 1.0.0
49
54
  */
50
55
  constructor(ws, config) {
51
56
  super();
@@ -70,6 +75,7 @@ export class WsTurboStream extends TurboStream {
70
75
  * @param {Object} config - The changed config object.
71
76
  * @listens element
72
77
  * @listens render
78
+ * @since 1.0.0
73
79
  */
74
80
  handleConfig(config) {
75
81
  if (config.buffer === true) {
@@ -94,6 +100,7 @@ export class WsTurboStream extends TurboStream {
94
100
  * Sends the rendered HTML fragment to the WebSocket.
95
101
  *
96
102
  * @param {String} html - The rendered HTML fragment.
103
+ * @since 1.0.0
97
104
  */
98
105
  handleRender(html) {
99
106
  this.ws.send(html);
@@ -104,6 +111,7 @@ export class WsTurboStream extends TurboStream {
104
111
  * Sends the Turbo Stream element to the WebSocket.
105
112
  *
106
113
  * @param {TurboStreamElement} element - The Turbo Stream element to send.
114
+ * @since 1.0.0
107
115
  */
108
116
  handleElement(element) {
109
117
  this.ws.send(element.render());
package/package.json CHANGED
@@ -1,8 +1,23 @@
1
1
  {
2
2
  "name": "node-turbo",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "A library for Node.js to assist with the server side of 37signals' Hotwire Turbo framework. It provides classes and functions for Web servers and also convenience functions for the frameworks Koa and Express as well as for WebSocket and SSE.",
5
- "keywords": ["node", "turbo", "node-turbo", "nodejs", "hotwire", "hotwired", "server", "http", "koa", "express", "sse", "websocket", "ws"],
5
+ "keywords": [
6
+ "node",
7
+ "turbo",
8
+ "node-turbo",
9
+ "nodejs",
10
+ "hotwire",
11
+ "hotwired",
12
+ "server",
13
+ "http",
14
+ "http2",
15
+ "koa",
16
+ "express",
17
+ "sse",
18
+ "websocket",
19
+ "ws"
20
+ ],
6
21
  "homepage": "https://github.com/VividVisions/node-turbo",
7
22
  "bugs": "https://github.com/VividVisions/node-turbo/issues",
8
23
  "repository": {
@@ -39,37 +54,37 @@
39
54
  "test:package": "NODE_ENV=testing mocha --ignore \"./test/unit/**\" --ignore \"./test/integration/**\"",
40
55
  "test:all": "NODE_ENV=testing mocha",
41
56
  "test:coverage": "c8 -c './.c8rc.json' npm test",
42
- "pretest:coverage": "rm -rd ./coverage",
43
57
  "docs": "esdoc -c ./.esdoc.json",
44
58
  "postdocs": "cp -f ./docs/internal/API.md ./docs"
45
59
  },
46
60
  "dependencies": {
61
+ "debug": "^4.3.4",
47
62
  "is-plain-object": "^5.0.0",
48
- "negotiator": "^0.6.3",
49
- "debug": "^4.3.4"
63
+ "negotiator": "^0.6.3"
50
64
  },
51
65
  "devDependencies": {
52
66
  "@enterthenamehere/esdoc": "^2.6.0-dev.1",
53
67
  "@enterthenamehere/esdoc-external-nodejs-plugin": "^2.6.0-dev.1",
54
68
  "@enterthenamehere/esdoc-importpath-plugin": "^2.6.0-dev.1",
55
69
  "@enterthenamehere/esdoc-standard-plugin": "^2.6.0-dev.2",
70
+ "@hotwired/turbo": "^8.0.4",
71
+ "@playwright/test": "^1.42.1",
56
72
  "@vividvisions/esdoc-api-doc-markdown-plugin": "file:../esdoc-api-doc-markdown-plugin",
57
- "@hotwired/turbo": "^8.0.0-beta.1",
58
- "c8": "^8.0.1",
59
- "chai": "^4.3.10",
73
+ "c8": "^9.1.0",
74
+ "chai": "^4.1.1",
60
75
  "chai-eventemitter2": "^0.2.1",
61
76
  "chai-spies": "^1.1.0",
62
77
  "colorette": "^2.0.20",
63
78
  "eventsource": "^2.0.2",
64
- "express": "^4.18.2",
79
+ "express": "^4.18.3",
65
80
  "git-repo-info": "^2.1.1",
66
- "koa": "^2.14.2",
67
- "koa-route": "^3.2.0",
81
+ "koa": "^2.15.0",
82
+ "koa-route": "^4.0.1",
68
83
  "koa-static": "^5.0.0",
69
- "mocha": "^10.2.0",
70
- "node-mocks-http": "^1.14.0",
84
+ "mocha": "^10.3.0",
85
+ "node-mocks-http": "^1.14.1",
71
86
  "nunjucks": "^3.2.4",
72
- "supertest": "^6.3.3",
73
- "ws": "^8.15.1"
87
+ "supertest": "^6.3.4",
88
+ "ws": "^8.16.0"
74
89
  }
75
90
  }
@@ -2,6 +2,11 @@
2
2
  import { expect } from 'chai';
3
3
  import { TurboElement } from '#core';
4
4
 
5
+
6
+ class NonThrowingTurboElement extends TurboElement {
7
+ validate() {}
8
+ }
9
+
5
10
  describe('TurboElement', function() {
6
11
 
7
12
  it('Constructor correctly assigns properties (and throws Error)', function() {
@@ -12,4 +17,28 @@ describe('TurboElement', function() {
12
17
  }).to.throw();
13
18
  });
14
19
 
20
+
21
+ describe('renderAttributesAsHtml()', function() {
22
+
23
+ it('correctly renders attributes', function() {
24
+ const te = new NonThrowingTurboElement({
25
+ foo: 'bar',
26
+ two: 2
27
+ });
28
+
29
+ expect(te.renderAttributesAsHtml()).to.equal('foo="bar" two="2"');
30
+ });
31
+
32
+
33
+ it('correctly renders booelan attributes', function() {
34
+ const te = new NonThrowingTurboElement({
35
+ foo: 'bar',
36
+ bool: null
37
+ });
38
+
39
+ expect(te.renderAttributesAsHtml()).to.equal('foo="bar" bool');
40
+ });
41
+
42
+ });
43
+
15
44
  });
@@ -73,4 +73,11 @@ describe('TurboStreamElement', function() {
73
73
 
74
74
  });
75
75
 
76
+
77
+ it(`No error is thrown when action is "refresh" with missing attribute "target" or "targets"`, function() {
78
+ expect(function() {
79
+ const tse = new TurboStreamElement({ action: 'refresh' });
80
+ }).to.not.throw();
81
+ });
82
+
76
83
  });
@@ -252,6 +252,15 @@ describe('TurboStream', function() {
252
252
 
253
253
 
254
254
  TurboStream.ACTIONS.forEach(action => {
255
+ if (action === 'refresh') {
256
+ it ('refresh() correctly omits <template> and content', function() {
257
+ const ts = new TurboStream({ action: 'refresh' });
258
+
259
+ expect(ts.render()).to.equal('<turbo-stream action="refresh"></turbo-stream>');
260
+ });
261
+ return;
262
+ }
263
+
255
264
  it (`${action}() adds TurboElement with action \'${action}\'`, function() {
256
265
  const ts = new TurboStream();
257
266
  ts[action].call(ts, { target: 't' }, 'c');
@@ -278,6 +287,28 @@ describe('TurboStream', function() {
278
287
  });
279
288
  });
280
289
 
290
+
291
+ it ('refresh() adds TurboElement with action \'refresh\'', function() {
292
+ const ts = new TurboStream();
293
+ ts.refresh();
294
+
295
+ expect(ts.elements[0] instanceof TurboElement).to.be.true;
296
+ expect(ts.elements[0].attributes.action).to.equal('refresh');
297
+ });
298
+
299
+ it ('refresh(<string>) adds attribute \'request-id\'', function() {
300
+ const ts = new TurboStream();
301
+ ts.refresh('id');
302
+ expect(ts.elements[0].attributes).to.have.property('request-id');
303
+ expect(ts.elements[0].attributes['request-id']).to.equal('id');
304
+ });
305
+
306
+ it ('refresh(<object>) adds attributes', function() {
307
+ const ts = new TurboStream();
308
+ ts.refresh({ foo: 'bar' });
309
+ expect(ts.elements[0].attributes).to.have.property('foo');
310
+ expect(ts.elements[0].attributes.foo).to.equal('bar');
311
+ });
281
312
  });
282
313
 
283
314