node-turbo 1.0.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/.c8rc.json +5 -0
- package/.esdoc.json +83 -0
- package/.eslintrc.json +10 -0
- package/.mocharc.json +7 -0
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/docs/API.md +857 -0
- package/lib/errors/index.js +36 -0
- package/lib/express/express-turbo-stream.js +41 -0
- package/lib/express/index.js +4 -0
- package/lib/express/turbocharge-express.js +108 -0
- package/lib/index.js +8 -0
- package/lib/koa/index.js +4 -0
- package/lib/koa/koa-turbo-stream.js +44 -0
- package/lib/koa/turbocharge-koa.js +122 -0
- package/lib/request-helpers.js +53 -0
- package/lib/sse/index.js +3 -0
- package/lib/sse/sse-turbo-stream.js +137 -0
- package/lib/turbo-element.js +71 -0
- package/lib/turbo-frame.js +79 -0
- package/lib/turbo-readable.js +125 -0
- package/lib/turbo-stream-element.js +67 -0
- package/lib/turbo-stream.js +350 -0
- package/lib/ws/index.js +4 -0
- package/lib/ws/ws-turbo-stream.js +112 -0
- package/package.json +75 -0
- package/test/hooks.js +46 -0
- package/test/integration/express.test.js +137 -0
- package/test/integration/koa.test.js +125 -0
- package/test/integration/sse.test.js +80 -0
- package/test/integration/ws.test.js +155 -0
- package/test/package.test.js +68 -0
- package/test/unit/core/request-helpers.test.js +97 -0
- package/test/unit/core/turbo-element.test.js +15 -0
- package/test/unit/core/turbo-frame.test.js +63 -0
- package/test/unit/core/turbo-readable.test.js +93 -0
- package/test/unit/core/turbo-stream-element.test.js +76 -0
- package/test/unit/core/turbo-stream.test.js +308 -0
- package/test/unit/express/express-turbo-stream.test.js +39 -0
- package/test/unit/express/turbocharge-express.test.js +123 -0
- package/test/unit/koa/koa-turbo-stream.test.js +56 -0
- package/test/unit/koa/turbocharge-koa.test.js +141 -0
- package/test/unit/sse/sse-turbo-stream.test.js +109 -0
- package/test/unit/ws/ws-turbo-stream.test.js +46 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
|
|
2
|
+
import { TurboStream } from '#core';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This class represents a Turbo Stream message with added functionality for WebSockets.
|
|
7
|
+
*
|
|
8
|
+
* @extends {TurboStream}
|
|
9
|
+
*/
|
|
10
|
+
export class WsTurboStream extends TurboStream {
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The WebSocket instance to send to.
|
|
14
|
+
*
|
|
15
|
+
* @type {WebSocket}
|
|
16
|
+
*/
|
|
17
|
+
ws;
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ready-state `OPEN` of a WebSocket.
|
|
22
|
+
*
|
|
23
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
|
|
24
|
+
* @type {Number}
|
|
25
|
+
* @static
|
|
26
|
+
*/
|
|
27
|
+
static OPEN = 1;
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convenience function to create a non-buffering WsTurboStream
|
|
32
|
+
* instance, which uses the passed WebSocket.
|
|
33
|
+
*
|
|
34
|
+
* @static
|
|
35
|
+
* @param {WebSocket} ws - The WebSocket instance to send to.
|
|
36
|
+
* @returns {WsTurboStream} - A new WsTurboStream instance.
|
|
37
|
+
*/
|
|
38
|
+
static use(ws) {
|
|
39
|
+
return new this(ws, { buffer: false });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Listens for the event `config` and calls `handleConfig(config)` if it has been emitted.
|
|
45
|
+
*
|
|
46
|
+
* @param {WebSocket} ws - The WebSocket to send to.
|
|
47
|
+
* @param {Object} [config] - The config to override.
|
|
48
|
+
* @listens config
|
|
49
|
+
*/
|
|
50
|
+
constructor(ws, config) {
|
|
51
|
+
super();
|
|
52
|
+
this.ws = ws;
|
|
53
|
+
|
|
54
|
+
this.on('config', this.handleConfig);
|
|
55
|
+
|
|
56
|
+
if (typeof config !== 'undefined') {
|
|
57
|
+
this.updateConfig(config);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.handleConfig(this.config);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Changes event listeners depending on `config.buffer`:
|
|
67
|
+
* If `true`, it will listen for the event `render` and call `handleRender()` when the event has been emitted.
|
|
68
|
+
* If `false`, it will listen for the event `element` and call `handleElement()` when the event has been emitted.
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} config - The changed config object.
|
|
71
|
+
* @listens element
|
|
72
|
+
* @listens render
|
|
73
|
+
*/
|
|
74
|
+
handleConfig(config) {
|
|
75
|
+
if (config.buffer === true) {
|
|
76
|
+
/* c8 ignore next 3 */
|
|
77
|
+
if (this.listenerCount('element') > 0) {
|
|
78
|
+
this.removeListener('element', this.handleElement);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.on('render', this.handleRender);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
/* c8 ignore next 3 */
|
|
85
|
+
if (this.listenerCount('render') > 0) {
|
|
86
|
+
this.removeListener('render', this.handleRender);
|
|
87
|
+
}
|
|
88
|
+
this.on('element', this.handleElement);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Sends the rendered HTML fragment to the WebSocket.
|
|
95
|
+
*
|
|
96
|
+
* @param {String} html - The rendered HTML fragment.
|
|
97
|
+
*/
|
|
98
|
+
handleRender(html) {
|
|
99
|
+
this.ws.send(html);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Sends the Turbo Stream element to the WebSocket.
|
|
105
|
+
*
|
|
106
|
+
* @param {TurboStreamElement} element - The Turbo Stream element to send.
|
|
107
|
+
*/
|
|
108
|
+
handleElement(element) {
|
|
109
|
+
this.ws.send(element.render());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-turbo",
|
|
3
|
+
"version": "1.0.0",
|
|
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": ["turbo", "hotwire", "hotwired", "server", "http", "koa", "express", "sse", "websocket", "ws"],
|
|
6
|
+
"homepage": "https://github.com/VividVisions/node-turbo",
|
|
7
|
+
"bugs": "https://github.com/VividVisions/node-turbo/issues",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/VividVisions/node-turbo.git"
|
|
11
|
+
},
|
|
12
|
+
"author": "Walter Krivanek <walter@vividvisions.com>",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">= 16.6"
|
|
17
|
+
},
|
|
18
|
+
"main": "./lib/index.js",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./lib/index.js",
|
|
21
|
+
"./ws": "./lib/ws/index.js",
|
|
22
|
+
"./koa": "./lib/koa/index.js",
|
|
23
|
+
"./express": "./lib/express/index.js",
|
|
24
|
+
"./sse": "./lib/sse/index.js",
|
|
25
|
+
"./errors": "./lib/errors/index.js"
|
|
26
|
+
},
|
|
27
|
+
"imports": {
|
|
28
|
+
"#core": "./lib/index.js",
|
|
29
|
+
"#ws": "./lib/ws/index.js",
|
|
30
|
+
"#koa": "./lib/koa/index.js",
|
|
31
|
+
"#express": "./lib/express/index.js",
|
|
32
|
+
"#sse": "./lib/sse/index.js",
|
|
33
|
+
"#errors": "./lib/errors/index.js"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "NODE_ENV=testing mocha --require \"./test/hooks.js\"",
|
|
37
|
+
"test:unit": "NODE_ENV=testing mocha --ignore \"./test/integration/**\" --ignore \"./test/*\"",
|
|
38
|
+
"test:integration": "NODE_ENV=testing mocha --ignore \"./test/unit/**\" --ignore \"./test/*\"",
|
|
39
|
+
"test:package": "NODE_ENV=testing mocha --ignore \"./test/unit/**\" --ignore \"./test/integration/**\"",
|
|
40
|
+
"test:all": "NODE_ENV=testing mocha",
|
|
41
|
+
"test:coverage": "c8 -c './.c8rc.json' npm test",
|
|
42
|
+
"pretest:coverage": "rm -rd ./coverage",
|
|
43
|
+
"docs": "esdoc -c ./.esdoc.json",
|
|
44
|
+
"postdocs": "cp -f ./docs/internal/API.md ./docs"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"is-plain-object": "^5.0.0",
|
|
48
|
+
"negotiator": "^0.6.3"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@enterthenamehere/esdoc": "^2.6.0-dev.1",
|
|
52
|
+
"@enterthenamehere/esdoc-external-nodejs-plugin": "^2.6.0-dev.1",
|
|
53
|
+
"@enterthenamehere/esdoc-importpath-plugin": "^2.6.0-dev.1",
|
|
54
|
+
"@enterthenamehere/esdoc-standard-plugin": "^2.6.0-dev.2",
|
|
55
|
+
"@vividvisions/esdoc-api-doc-markdown-plugin": "file:../esdoc-api-doc-markdown-plugin",
|
|
56
|
+
"@hotwired/turbo": "^8.0.0-beta.1",
|
|
57
|
+
"c8": "^8.0.1",
|
|
58
|
+
"chai": "^4.3.10",
|
|
59
|
+
"chai-eventemitter2": "^0.2.1",
|
|
60
|
+
"chai-spies": "^1.1.0",
|
|
61
|
+
"colorette": "^2.0.20",
|
|
62
|
+
"debug": "^4.3.4",
|
|
63
|
+
"eventsource": "^2.0.2",
|
|
64
|
+
"express": "^4.18.2",
|
|
65
|
+
"git-repo-info": "^2.1.1",
|
|
66
|
+
"koa": "^2.14.2",
|
|
67
|
+
"koa-route": "^3.2.0",
|
|
68
|
+
"koa-static": "^5.0.0",
|
|
69
|
+
"mocha": "^10.2.0",
|
|
70
|
+
"node-mocks-http": "^1.14.0",
|
|
71
|
+
"nunjucks": "^3.2.4",
|
|
72
|
+
"supertest": "^6.3.3",
|
|
73
|
+
"ws": "^8.15.1"
|
|
74
|
+
}
|
|
75
|
+
}
|
package/test/hooks.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
|
|
2
|
+
// Dependencies.
|
|
3
|
+
import getRepoInfo from 'git-repo-info';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { green, yellow } from 'colorette';
|
|
6
|
+
|
|
7
|
+
const
|
|
8
|
+
__dirname = new URL('.', import.meta.url).pathname,
|
|
9
|
+
gitInfo = getRepoInfo(__dirname);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
export const mochaHooks = {
|
|
15
|
+
|
|
16
|
+
beforeAll() {
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log(`Testing time: ${green(new Date().toISOString())}`);
|
|
19
|
+
|
|
20
|
+
// Log info about application and environment.
|
|
21
|
+
console.log(`Package : ${green(process.env.npm_package_name)}`);
|
|
22
|
+
console.log(`Version : ${green(process.env.npm_package_version)}`);
|
|
23
|
+
console.log(`NODE_ENV : ${'NODE_ENV' in process.env ? green(process.env['NODE_ENV']) : yellow('-')}`);
|
|
24
|
+
|
|
25
|
+
// Log info about Git branch, etc…
|
|
26
|
+
console.log(`Git branch : ${gitInfo.branch ? green(gitInfo.branch) : yellow('-')}`);
|
|
27
|
+
if (gitInfo.sha) {
|
|
28
|
+
console.log(`Git revision: ${green(gitInfo.sha.substr(0, 8))} (${gitInfo.sha})`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log(`Git revision: ${yellow('-')}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Log info about Node.js.
|
|
35
|
+
console.log(`Node.js : ${green(process.version)}`);
|
|
36
|
+
|
|
37
|
+
// Log info about the machine.
|
|
38
|
+
console.log(`OS : ${green(os.type())} ${green(os.release())} (${green(os.platform())})`);
|
|
39
|
+
console.log(`Hostname : ${green(os.hostname())}`);
|
|
40
|
+
console.log(`CPUs : ${green(os.cpus().length)}`);
|
|
41
|
+
console.log(`CPU arch : ${green(os.arch())}`);
|
|
42
|
+
console.log(`Memory total: ${green('GB ' + Number(os.totalmem() / 1073741824).toFixed(1))}`);
|
|
43
|
+
console.log('');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import request from 'supertest';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import { TurboStream, TurboFrame } from '#core';
|
|
6
|
+
import { turbochargeExpress } from '#express';
|
|
7
|
+
|
|
8
|
+
describe('Express integration', function() {
|
|
9
|
+
|
|
10
|
+
before(function() {
|
|
11
|
+
const app = express();
|
|
12
|
+
|
|
13
|
+
turbochargeExpress(app);
|
|
14
|
+
|
|
15
|
+
app.use((req, res, next) => {
|
|
16
|
+
this.storedReq = req;
|
|
17
|
+
this.storedRes = res;
|
|
18
|
+
next();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
app.get('/frameautoid', (req, res) => {
|
|
22
|
+
if (req.isTurboFrameRequest()) {
|
|
23
|
+
res.turboFrame('<p>content</p>');
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
res.status(501).end();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
app.get('/framesetid', (req, res) => {
|
|
31
|
+
if (req.isTurboFrameRequest()) {
|
|
32
|
+
res.turboFrame('set-id', '<p>content</p>');
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
res.status(501).end();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
app.get('/stream', (req, res) => {
|
|
40
|
+
if (req.isTurboStreamRequest()) {
|
|
41
|
+
res
|
|
42
|
+
.turboStream()
|
|
43
|
+
.append('target-id', '<p>append</p>')
|
|
44
|
+
.send();
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
res.status(501).end();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.app = app;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('turbochargeExpress()', function() {
|
|
55
|
+
|
|
56
|
+
it('adds helper functions to Express\' request object', function() {
|
|
57
|
+
return request(this.app)
|
|
58
|
+
.get('/')
|
|
59
|
+
.then(response => {
|
|
60
|
+
expect(this.storedReq.getTurboFrameId).to.be.an('function');
|
|
61
|
+
expect(this.storedReq.isTurboFrameRequest).to.be.an('function');
|
|
62
|
+
expect(this.storedReq.isTurboStreamRequest).to.be.an('function');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('adds turboStream() to Express\' response object', function() {
|
|
67
|
+
return request(this.app)
|
|
68
|
+
.get('/')
|
|
69
|
+
.then(response => {
|
|
70
|
+
expect(this.storedRes.turboStream).to.be.an('function');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('adds turboFrame() to Express\' response object', function() {
|
|
75
|
+
return request(this.app)
|
|
76
|
+
.get('/')
|
|
77
|
+
.then(response => {
|
|
78
|
+
expect(this.storedRes.turboFrame).to.be.an('function');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('Turbo Frames', function() {
|
|
85
|
+
|
|
86
|
+
it('res.turboFrame() autom. retrieves Turbo-Frame ID from request when not set', function() {
|
|
87
|
+
return request(this.app)
|
|
88
|
+
.get('/frameautoid')
|
|
89
|
+
.set(TurboFrame.HEADER_KEY, 'turbo-frame-id')
|
|
90
|
+
.expect(200)
|
|
91
|
+
.then(response => {
|
|
92
|
+
expect(response.text).to.equal('<turbo-frame id="turbo-frame-id"><p>content</p></turbo-frame>');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
it('Turbo Frame request returns Turbo Frame message', function() {
|
|
98
|
+
return request(this.app)
|
|
99
|
+
.get('/framesetid')
|
|
100
|
+
.set(TurboFrame.HEADER_KEY, 'turbo-frame-id')
|
|
101
|
+
.expect(200)
|
|
102
|
+
.then(response => {
|
|
103
|
+
expect(response.text).to.equal('<turbo-frame id="set-id"><p>content</p></turbo-frame>');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
it('Non Turbo Frame request returns status 501', function() {
|
|
109
|
+
return request(this.app)
|
|
110
|
+
.get('/frameautoid')
|
|
111
|
+
.expect(501);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
describe('Turbo Streams', function() {
|
|
118
|
+
|
|
119
|
+
it('Turbo Stream request returns Turbo Stream message', function() {
|
|
120
|
+
return request(this.app)
|
|
121
|
+
.get('/stream')
|
|
122
|
+
.set('Accept', TurboStream.MIME_TYPE)
|
|
123
|
+
.expect(200)
|
|
124
|
+
.then(response => {
|
|
125
|
+
expect(response.text.trim()).to.equal('<turbo-stream action="append" target="target-id"><template><p>append</p></template></turbo-stream>');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('Non Turbo Stream request returns status 501', function() {
|
|
130
|
+
return request(this.app)
|
|
131
|
+
.get('/stream')
|
|
132
|
+
.expect(501);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import request from 'supertest';
|
|
4
|
+
import Koa from 'koa';
|
|
5
|
+
import route from 'koa-route';
|
|
6
|
+
import { TurboStream, TurboFrame } from '#core';
|
|
7
|
+
import { turbochargeKoa } from '#koa';
|
|
8
|
+
|
|
9
|
+
describe('Koa integration', function() {
|
|
10
|
+
|
|
11
|
+
before(function() {
|
|
12
|
+
const app = new Koa();
|
|
13
|
+
|
|
14
|
+
app.on('error', (err, ctx) => {
|
|
15
|
+
expect.fail(err);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
turbochargeKoa(app);
|
|
19
|
+
|
|
20
|
+
app.use(async (ctx, next) => {
|
|
21
|
+
this.storedCtx = ctx;
|
|
22
|
+
await next();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
app.use(route.get('/frame', ctx => {
|
|
26
|
+
if (ctx.isTurboFrameRequest()) {
|
|
27
|
+
ctx.turboFrame('<p>content</p>');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
ctx.status = 501;
|
|
31
|
+
}
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
app.use(route.get('/stream', ctx => {
|
|
35
|
+
if (ctx.isTurboStreamRequest()) {
|
|
36
|
+
ctx
|
|
37
|
+
.turboStream()
|
|
38
|
+
.append('target-id', '<p>append</p>');
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
ctx.status = 501;
|
|
42
|
+
}
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
this.app = app;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('turbochargeKoa()', function() {
|
|
49
|
+
|
|
50
|
+
before(function() {
|
|
51
|
+
return new Promise((resolve, reject) => {;
|
|
52
|
+
request(this.app.callback())
|
|
53
|
+
.get('/')
|
|
54
|
+
.end((err, res) => {
|
|
55
|
+
if (err) {
|
|
56
|
+
return reject(err);
|
|
57
|
+
}
|
|
58
|
+
resolve(res);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
it('adds helper functions to Koa\'s ctx', function() {
|
|
65
|
+
expect(this.storedCtx.getTurboFrameId).to.be.a('function');
|
|
66
|
+
expect(this.storedCtx.isTurboFrameRequest).to.be.a('function');
|
|
67
|
+
expect(this.storedCtx.isTurboStreamRequest).to.be.a('function');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
it('adds turboStream() to Koa\'s ctx', function() {
|
|
72
|
+
expect(this.storedCtx.turboStream).to.be.a('function');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
it('adds turboFrame() to Koa\'s ctx', function() {
|
|
77
|
+
expect(this.storedCtx.turboFrame).to.be.a('function');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('Turbo Frames', function() {
|
|
83
|
+
|
|
84
|
+
it('Turbo Frame request returns Turbo Frame message', function() {
|
|
85
|
+
return request(this.app.callback())
|
|
86
|
+
.get('/frame')
|
|
87
|
+
.set(TurboFrame.HEADER_KEY, 'turbo-frame-id')
|
|
88
|
+
.expect(200)
|
|
89
|
+
.expect('Content-Type', /text\/html/)
|
|
90
|
+
.then(response => {
|
|
91
|
+
expect(response.text).to.equal('<turbo-frame id="turbo-frame-id"><p>content</p></turbo-frame>');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('Non Turbo Frame request returns status 501', function() {
|
|
96
|
+
return request(this.app.callback())
|
|
97
|
+
.get('/frame')
|
|
98
|
+
.expect(501);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
describe('Turbo Streams', function() {
|
|
105
|
+
|
|
106
|
+
it('Turbo Stream request returns Turbo Stream message with correct MIME type', function() {
|
|
107
|
+
return request(this.app.callback())
|
|
108
|
+
.get('/stream')
|
|
109
|
+
.set('Accept', TurboStream.MIME_TYPE)
|
|
110
|
+
.expect(200)
|
|
111
|
+
.expect('Content-Type', new RegExp(TurboStream.MIME_TYPE))
|
|
112
|
+
.then(response => {
|
|
113
|
+
expect(response.text.trim()).to.equal('<turbo-stream action="append" target="target-id"><template><p>append</p></template></turbo-stream>');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('Non Turbo Stream request returns status 501', function() {
|
|
118
|
+
return request(this.app.callback())
|
|
119
|
+
.get('/stream')
|
|
120
|
+
.expect(501);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import request from 'supertest';
|
|
4
|
+
import Koa from 'koa';
|
|
5
|
+
import EventSource from 'eventsource';
|
|
6
|
+
import { TurboStream } from '#core';
|
|
7
|
+
import { SseTurboStream } from '#sse';
|
|
8
|
+
import { PassThrough } from 'node:stream';
|
|
9
|
+
|
|
10
|
+
const port = 8888;
|
|
11
|
+
|
|
12
|
+
describe('SSE integration', function() {
|
|
13
|
+
|
|
14
|
+
before(function() {
|
|
15
|
+
this.app = new Koa();
|
|
16
|
+
this.sseTurboStream = new SseTurboStream();
|
|
17
|
+
|
|
18
|
+
this.app.on('error', (err, ctx) => {
|
|
19
|
+
expect.fail(err);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
this.app.use(async (ctx, next) => {
|
|
23
|
+
if (ctx.path !== '/sse') {
|
|
24
|
+
return await next();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ctx.request.socket.setTimeout(0);
|
|
28
|
+
ctx.req.socket.setNoDelay(true);
|
|
29
|
+
ctx.req.socket.setKeepAlive(true);
|
|
30
|
+
|
|
31
|
+
ctx.set({
|
|
32
|
+
'Cache-Control': 'no-cache',
|
|
33
|
+
'Connection': 'keep-alive',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
this.readable = this.sseTurboStream.createReadableStream();
|
|
37
|
+
|
|
38
|
+
ctx.type = SseTurboStream.MIME_TYPE;
|
|
39
|
+
ctx.status = 200;
|
|
40
|
+
ctx.body = this.readable;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
this.server = this.app.listen(port);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
after(function() {
|
|
48
|
+
this.readable.destroy();
|
|
49
|
+
this.eventSource.close();
|
|
50
|
+
this.server.close();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
it('Turbo Stream messages get sent to EventSource as SSE messages', function(done) {
|
|
55
|
+
const messages = [];
|
|
56
|
+
|
|
57
|
+
this.eventSource = new EventSource(`http://localhost:${port}/sse`);
|
|
58
|
+
|
|
59
|
+
this.eventSource.addEventListener('error', e => {
|
|
60
|
+
expect.fail('An error occurred while attempting to connect');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.eventSource.addEventListener('message', e => {
|
|
64
|
+
|
|
65
|
+
messages.push(e.data);
|
|
66
|
+
|
|
67
|
+
if (messages.length === 2) {
|
|
68
|
+
expect(messages[0]).to.equal('<turbo-stream action="append" target="t1"><template>c1</template></turbo-stream>');
|
|
69
|
+
expect(messages[1]).to.equal('<turbo-stream action="replace" target="t2"><template>c2</template></turbo-stream>');
|
|
70
|
+
done();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Send Turbo Stream messages.
|
|
75
|
+
this.sseTurboStream.append('t1', 'c1');
|
|
76
|
+
this.sseTurboStream.replace('t2', 'c2');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
});
|