node-turbo 1.2.1 → 1.2.2
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/README.md +2 -2
- package/package.json +11 -13
- package/test/end2end/global-setup.js +23 -0
- package/test/end2end/playwright.config.js +26 -0
- package/test/end2end/server-koa.js +155 -0
- package/test/end2end/tests/e2e.test.js +96 -0
package/README.md
CHANGED
|
@@ -56,9 +56,9 @@ node-turbo has been tested with 100% code coverage with the following engines/li
|
|
|
56
56
|
| Name | Version(s) |
|
|
57
57
|
| :--- | :--- |
|
|
58
58
|
| [Node.js](https://nodejs.org/) | 16.6 - 21.5.0 |
|
|
59
|
-
| [Hotwire Turbo](https://turbo.hotwired.dev/) | 7.3.0 - 8.0.
|
|
59
|
+
| [Hotwire Turbo](https://turbo.hotwired.dev/) | 7.3.0 - 8.0.10 |
|
|
60
60
|
| [Koa](https://koajs.com/) | 2.14.2 - 2.15.3 |
|
|
61
|
-
| [Express](https://expressjs.com/) | 4.18.2 - 4.21.
|
|
61
|
+
| [Express](https://expressjs.com/) | 4.18.2 - 4.21.1 |
|
|
62
62
|
| [ws](https://github.com/websockets/ws) | 8.15.1 - 8.18.0 |
|
|
63
63
|
|
|
64
64
|
## API docs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-turbo",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
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
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -48,14 +48,15 @@
|
|
|
48
48
|
"#errors": "./lib/errors/index.js"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
|
-
"test": "NODE_ENV=testing mocha --require \"./test/hooks.js\" \"./test/**/*.test.js\"",
|
|
51
|
+
"test": "NODE_ENV=testing mocha --require \"./test/hooks.js\" \"./test/unit/**/*.test.js\" \"./test/integration/**/*.test.js\"",
|
|
52
52
|
"test:unit": "NODE_ENV=testing mocha \"./test/unit/**/*.test.js\"",
|
|
53
53
|
"test:integration": "NODE_ENV=testing mocha \"./test/integration/**/*.test.js\"",
|
|
54
54
|
"test:package": "NODE_ENV=testing mocha \"./test/package.test.js\"",
|
|
55
|
-
"test:all": "NODE_ENV=testing mocha \"./test/**/*.test.js\"",
|
|
55
|
+
"test:all": "NODE_ENV=testing mocha \"./test/unit/**/*.test.js\" \"./test/integration/**/*.test.js\" \"./test/package.test.js\"",
|
|
56
56
|
"test:coverage": "c8 -c './.c8rc.json' npm test",
|
|
57
|
-
"
|
|
58
|
-
"
|
|
57
|
+
"pretest:e2e": "cp -f ./node_modules/@hotwired/turbo/dist/turbo.es2017-esm.js ./test/end2end/static/",
|
|
58
|
+
"test:e2e": "npx playwright test -c ./test/end2end/playwright.config.js",
|
|
59
|
+
"test:e2eserver": "node ./test/end2end/server-koa.js"
|
|
59
60
|
},
|
|
60
61
|
"dependencies": {
|
|
61
62
|
"debug": "^4.3.7",
|
|
@@ -63,24 +64,21 @@
|
|
|
63
64
|
"negotiator": "^0.6.3"
|
|
64
65
|
},
|
|
65
66
|
"devDependencies": {
|
|
66
|
-
"@
|
|
67
|
-
"@
|
|
68
|
-
"@
|
|
69
|
-
"@enterthenamehere/esdoc-standard-plugin": "^2.6.0-dev.2",
|
|
70
|
-
"@hotwired/turbo": "^8.0.6",
|
|
71
|
-
"@vividvisions/esdoc-api-doc-markdown-plugin": "file:../esdoc-api-doc-markdown-plugin",
|
|
67
|
+
"@hotwired/turbo": "^8.0.10",
|
|
68
|
+
"@koa/bodyparser": "^5.1.1",
|
|
69
|
+
"@playwright/test": "^1.48.0",
|
|
72
70
|
"c8": "^10.1.2",
|
|
73
71
|
"chai": "^5.1.1",
|
|
74
72
|
"chai-spies": "^1.1.0",
|
|
75
73
|
"colorette": "^2.0.20",
|
|
76
74
|
"eventsource": "^2.0.2",
|
|
77
|
-
"express": "^4.21.
|
|
75
|
+
"express": "^4.21.1",
|
|
78
76
|
"git-repo-info": "^2.1.1",
|
|
79
77
|
"koa": "^2.15.3",
|
|
80
78
|
"koa-route": "^4.0.1",
|
|
81
79
|
"koa-static": "^5.0.0",
|
|
82
80
|
"mocha": "^10.7.3",
|
|
83
|
-
"node-mocks-http": "^1.16.
|
|
81
|
+
"node-mocks-http": "^1.16.1",
|
|
84
82
|
"nunjucks": "^3.2.4",
|
|
85
83
|
"supertest": "^7.0.0",
|
|
86
84
|
"ws": "^8.18.0"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { green } from 'colorette';
|
|
5
|
+
|
|
6
|
+
// We're misusing Playwright's global setup functionality to log the
|
|
7
|
+
// tested version of Hotwire Turbo, since test.beforeAll() can be called
|
|
8
|
+
// multiple times.
|
|
9
|
+
try {
|
|
10
|
+
const
|
|
11
|
+
filePath = resolve('./node_modules/@hotwired/turbo/package.json'),
|
|
12
|
+
contents = await readFile(filePath, { encoding: 'utf8' }),
|
|
13
|
+
pkg = JSON.parse(contents);
|
|
14
|
+
|
|
15
|
+
console.log('–––––––––––––––––––––––––––––––––');
|
|
16
|
+
console.log(`Testing node-turbo ${green(process.env.npm_package_version)} against Hotwire Turbo ${green(pkg.version)}.`);
|
|
17
|
+
console.log('–––––––––––––––––––––––––––––––––');
|
|
18
|
+
}
|
|
19
|
+
catch(err) {
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default () => ({});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
|
|
2
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
testDir: './tests',
|
|
6
|
+
outputDir: './test-results',
|
|
7
|
+
fullyParallel: true,
|
|
8
|
+
globalSetup: './global-setup.js',
|
|
9
|
+
use: {
|
|
10
|
+
baseURL: 'http://127.0.0.1:3000',
|
|
11
|
+
},
|
|
12
|
+
webServer: {
|
|
13
|
+
command: 'npm run test:e2eserver',
|
|
14
|
+
url: 'http://127.0.0.1:3000/ready',
|
|
15
|
+
reuseExistingServer: false,
|
|
16
|
+
stdout: 'ignore',
|
|
17
|
+
stderr: 'pipe',
|
|
18
|
+
timeout: 5000
|
|
19
|
+
},
|
|
20
|
+
projects: [
|
|
21
|
+
{
|
|
22
|
+
name: 'node-turbo e2e tests',
|
|
23
|
+
use: { ...devices['Desktop Safari'] }
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
|
|
2
|
+
import Koa from 'koa';
|
|
3
|
+
import route from 'koa-route';
|
|
4
|
+
import serve from 'koa-static';
|
|
5
|
+
import bodyParser from '@koa/bodyparser';
|
|
6
|
+
import { turbochargeKoa } from '#koa';
|
|
7
|
+
import { SseTurboStream } from '#sse';
|
|
8
|
+
|
|
9
|
+
const app = new Koa();
|
|
10
|
+
let ssets;
|
|
11
|
+
|
|
12
|
+
app.on('error', (err, ctx) => {
|
|
13
|
+
console.error('Error in Koa:', err);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Add node-turbo functions to Koa context.
|
|
17
|
+
turbochargeKoa(app);
|
|
18
|
+
|
|
19
|
+
// Serve static Hotwire Turbo, which gets copied from node_modules before the test.
|
|
20
|
+
app.use(serve('./test/end2end/static'));
|
|
21
|
+
|
|
22
|
+
// Tell Playwright that this server is ready.
|
|
23
|
+
app.use(route.get('/ready', ctx => {
|
|
24
|
+
ctx.body = `OK`;
|
|
25
|
+
ctx.status = 200;
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Main testing page.
|
|
29
|
+
app.use(route.get('/test', ctx => {
|
|
30
|
+
ctx.body = `<html lang="en">
|
|
31
|
+
<head>
|
|
32
|
+
<meta charset="UTF-8">
|
|
33
|
+
<meta name="turbo-prefetch" content="false">
|
|
34
|
+
<title>node-turbo E2E Tests</title>
|
|
35
|
+
<script type="module">
|
|
36
|
+
import * as Turbo from '/turbo.es2017-esm.js';
|
|
37
|
+
const sse = new EventSource('/sse');
|
|
38
|
+
Turbo.session.connectStreamSource(sse);
|
|
39
|
+
|
|
40
|
+
document.getElementById('sse').addEventListener('click', async e => {
|
|
41
|
+
// Ignore repsonse.
|
|
42
|
+
await fetch('/sse-send');
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
45
|
+
</head>
|
|
46
|
+
<body>
|
|
47
|
+
<h1>node-turbo E2E Tests</h1>
|
|
48
|
+
<turbo-frame id="test-1">
|
|
49
|
+
<p>preclick</p>
|
|
50
|
+
<a href="/frame" data-testid="test-1-link">Link</a>
|
|
51
|
+
</turbo-frame>
|
|
52
|
+
<form action="/stream" method="POST" enctype="text/vnd.turbo-stream.html" id="test-form">
|
|
53
|
+
<select name="action" id="select">
|
|
54
|
+
<option value="append">append</option>
|
|
55
|
+
<option value="prepend">prepend</option>
|
|
56
|
+
<option value="replace">replace</option>
|
|
57
|
+
<option value="update">update</option>
|
|
58
|
+
<option value="remove">remove</option>
|
|
59
|
+
<option value="before">before</option>
|
|
60
|
+
<option value="after">after</option>
|
|
61
|
+
<option value="refresh">refresh</option>
|
|
62
|
+
</select>
|
|
63
|
+
<input type="submit" id="submit">
|
|
64
|
+
</form>
|
|
65
|
+
<ul id="stream-response">
|
|
66
|
+
<li>initial</li>
|
|
67
|
+
</ul>
|
|
68
|
+
<div id="sse-response">
|
|
69
|
+
No SSE response yet.
|
|
70
|
+
</div>
|
|
71
|
+
<input id="sse" type="button" value="SSE">
|
|
72
|
+
</body>
|
|
73
|
+
</html>`;
|
|
74
|
+
ctx.status = 200;
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
// For local use.
|
|
78
|
+
// app.use(async (ctx, next) => {
|
|
79
|
+
// console.log(`Request to ${ctx.path} (${ctx.method}).`);
|
|
80
|
+
// await next();
|
|
81
|
+
// });
|
|
82
|
+
|
|
83
|
+
// Turbo Frame response.
|
|
84
|
+
app.use(route.get('/frame', ctx => {
|
|
85
|
+
if (ctx.isTurboFrameRequest()) {
|
|
86
|
+
ctx.turboFrame('<p>afterclick</p>');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
ctx.status = 501;
|
|
90
|
+
}
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
// Initiate Turbo Stream over SSE.
|
|
94
|
+
app.use(route.get('/sse', ctx => {
|
|
95
|
+
ssets = ctx.sseTurboStream();
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
// Send SSE on request (to prevent having to deal with timings).
|
|
99
|
+
app.use(route.get('/sse-send', ctx => {
|
|
100
|
+
ssets.update('sse-response', 'sse-sent');
|
|
101
|
+
|
|
102
|
+
ctx.status = 200;
|
|
103
|
+
ctx.body = 'OK';
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
// Parse POST body.
|
|
107
|
+
app.use(bodyParser());
|
|
108
|
+
|
|
109
|
+
// Turbo Stream responses.
|
|
110
|
+
app.use(route.post('/stream', ctx => {
|
|
111
|
+
if (ctx.isTurboStreamRequest()) {
|
|
112
|
+
switch (ctx.request.body?.action) {
|
|
113
|
+
case 'append':
|
|
114
|
+
ctx.turboStream().append('stream-response', '<li>append</li>');
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case 'prepend':
|
|
118
|
+
ctx.turboStream().prepend('stream-response', '<li>prepend</li>');
|
|
119
|
+
break;
|
|
120
|
+
|
|
121
|
+
case 'replace':
|
|
122
|
+
ctx.turboStream().replace('stream-response', '<div id="stream-response">replace</div>');
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
case 'update':
|
|
126
|
+
ctx.turboStream().update('stream-response', '<li>update</li>');
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case 'remove':
|
|
130
|
+
ctx.turboStream().remove('stream-response');
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case 'before':
|
|
134
|
+
ctx.turboStream().before('stream-response', '<div id="before">before</div>');
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'after':
|
|
138
|
+
ctx.turboStream().before('stream-response', '<div id="after">after</div>');
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case 'refresh':
|
|
142
|
+
ctx.turboStream().refresh();
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
default:
|
|
146
|
+
throw new Error(`Unknown action: ${ctx.request.body?.action}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
ctx.status = 501;
|
|
151
|
+
}
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
// Start server.
|
|
155
|
+
app.listen(3000);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
|
|
2
|
+
import { test, expect } from '@playwright/test';
|
|
3
|
+
|
|
4
|
+
test.describe('Turbo Frame', () => {
|
|
5
|
+
|
|
6
|
+
test('updates turbo frame', async ({ page }) => {
|
|
7
|
+
await page.goto('/test');
|
|
8
|
+
|
|
9
|
+
const responsePromise = page.waitForResponse('/frame', { timeout: 3000 });
|
|
10
|
+
await page.getByTestId('test-1-link').click();
|
|
11
|
+
const response = await responsePromise;
|
|
12
|
+
|
|
13
|
+
await expect(page.locator('#test-1')).toContainText('afterclick');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test.describe('Turbo Stream', () => {
|
|
19
|
+
|
|
20
|
+
test.beforeEach(async ({ page }) => {
|
|
21
|
+
await page.addInitScript(() => {
|
|
22
|
+
window.addEventListener('turbo:render', e => {
|
|
23
|
+
window.TURBO_RENDER_EVENT = e.detail.renderMethod;
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
await page.goto('/test');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('action: append', async ({ page }) => {
|
|
31
|
+
await page.locator('#select').selectOption('append');
|
|
32
|
+
await page.locator('#submit').click();
|
|
33
|
+
await expect(page.locator('ul#stream-response > li')).toContainText([
|
|
34
|
+
'initial',
|
|
35
|
+
'append'
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('action: prepend', async ({ page }) => {
|
|
40
|
+
await page.locator('#select').selectOption('prepend');
|
|
41
|
+
await page.locator('#submit').click();
|
|
42
|
+
await expect(page.locator('ul#stream-response > li')).toContainText([
|
|
43
|
+
'prepend',
|
|
44
|
+
'initial'
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('action: replace', async ({ page }) => {
|
|
49
|
+
await page.locator('#select').selectOption('replace');
|
|
50
|
+
await page.locator('#submit').click();
|
|
51
|
+
await expect(page.locator('div#stream-response')).toContainText('replace');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('action: update', async ({ page }) => {
|
|
55
|
+
await page.locator('#select').selectOption('update');
|
|
56
|
+
await page.locator('#submit').click();
|
|
57
|
+
await expect(page.locator('ul#stream-response > li')).toHaveCount(1);
|
|
58
|
+
await expect(page.locator('ul#stream-response > li')).toContainText('update');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('action: remove', async ({ page }) => {
|
|
62
|
+
await page.locator('#select').selectOption('remove');
|
|
63
|
+
await page.locator('#submit').click();
|
|
64
|
+
await expect(page.locator('#stream-response')).toHaveCount(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('action: before', async ({ page }) => {
|
|
68
|
+
await page.locator('#select').selectOption('before');
|
|
69
|
+
await page.locator('#submit').click();
|
|
70
|
+
await expect(page.locator('#before')).toHaveCount(1);
|
|
71
|
+
await expect(page.locator('#before')).toContainText('before');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('action: after', async ({ page }) => {
|
|
75
|
+
await page.locator('#select').selectOption('after');
|
|
76
|
+
await page.locator('#submit').click();
|
|
77
|
+
await expect(page.locator('#after')).toHaveCount(1);
|
|
78
|
+
await expect(page.locator('#after')).toContainText('after');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('action: refresh', async ({ page }) => {
|
|
82
|
+
const func = page.waitForFunction(() => (window.TURBO_RENDER_EVENT === 'replace'), { timeout: 3000 });
|
|
83
|
+
|
|
84
|
+
await page.locator('#select').selectOption('refresh');
|
|
85
|
+
await page.locator('#submit').click();
|
|
86
|
+
await func;
|
|
87
|
+
|
|
88
|
+
await expect(page.locator('#select')).toHaveValue('append');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('async (SSE)', async ({ page }) => {
|
|
92
|
+
await page.locator('#sse').click();
|
|
93
|
+
await expect(page.locator('#sse-response')).toContainText('sse-sent');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
});
|