node-turbo 1.2.1 → 1.2.3

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 CHANGED
@@ -55,10 +55,10 @@ node-turbo has been tested with 100% code coverage with the following engines/li
55
55
 
56
56
  | Name | Version(s) |
57
57
  | :--- | :--- |
58
- | [Node.js](https://nodejs.org/) | 16.6 - 21.5.0 |
59
- | [Hotwire Turbo](https://turbo.hotwired.dev/) | 7.3.0 - 8.0.6 |
58
+ | [Node.js](https://nodejs.org/) | 16.6 - 22.12.0 |
59
+ | [Hotwire Turbo](https://turbo.hotwired.dev/) | 7.3.0 - 8.0.12 |
60
60
  | [Koa](https://koajs.com/) | 2.14.2 - 2.15.3 |
61
- | [Express](https://expressjs.com/) | 4.18.2 - 4.21.0 |
61
+ | [Express](https://expressjs.com/) | 4.18.2 - 4.21.2 |
62
62
  | [ws](https://github.com/websockets/ws) | 8.15.1 - 8.18.0 |
63
63
 
64
64
  ## API docs
@@ -529,7 +529,7 @@ const httpServer = http.createServer((req, res) => {
529
529
  padding: 10px;
530
530
  }
531
531
  </style>
532
- <script type="module" src="https://unpkg.com/@hotwired/turbo@8.0.4/dist/turbo.es2017-esm.js"></script>
532
+ <script type="module" src="https://unpkg.com/@hotwired/turbo@8.0.12/dist/turbo.es2017-esm.js"></script>
533
533
  <script>
534
534
  var eventSource = new EventSource('/sse');
535
535
  eventSource.onmessage = function(event) {
@@ -616,7 +616,7 @@ app.use(async (ctx, next) => {
616
616
  padding: 10px;
617
617
  }
618
618
  </style>
619
- <script type="module" src="https://unpkg.com/@hotwired/turbo@8.0.4/dist/turbo.es2017-esm.js"></script>
619
+ <script type="module" src="https://unpkg.com/@hotwired/turbo@8.0.12/dist/turbo.es2017-esm.js"></script>
620
620
  <script>
621
621
  var eventSource = new EventSource('/sse');
622
622
  eventSource.onmessage = function(event) {
@@ -693,7 +693,7 @@ app.get('/', async (req, res) => {
693
693
  padding: 10px;
694
694
  }
695
695
  </style>
696
- <script type="module" src="https://unpkg.com/@hotwired/turbo@8.0.4/dist/turbo.es2017-esm.js"></script>
696
+ <script type="module" src="https://unpkg.com/@hotwired/turbo@8.0.12/dist/turbo.es2017-esm.js"></script>
697
697
  <script>
698
698
  var eventSource = new EventSource('/sse');
699
699
  eventSource.onmessage = function(event) {
package/docs/API.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # node-turbo API documentation
2
2
 
3
- Version 1.2.0
3
+ Version 1.2.3
4
4
 
5
5
  ## Table of Contents
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-turbo",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
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,39 +48,37 @@
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
- "docs": "esdoc -c ./.esdoc.json",
58
- "postdocs": "cp -f ./docs/internal/API.md ./docs"
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
- "debug": "^4.3.7",
62
+ "debug": "^4.4.0",
62
63
  "is-plain-object": "^5.0.0",
63
- "negotiator": "^0.6.3"
64
+ "negotiator": "^1.0.0"
64
65
  },
65
66
  "devDependencies": {
66
- "@enterthenamehere/esdoc": "^2.6.0-dev.1",
67
- "@enterthenamehere/esdoc-external-nodejs-plugin": "^2.6.0-dev.1",
68
- "@enterthenamehere/esdoc-importpath-plugin": "^2.6.0-dev.1",
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",
72
- "c8": "^10.1.2",
73
- "chai": "^5.1.1",
67
+ "@hotwired/turbo": "^8.0.12",
68
+ "@koa/bodyparser": "^5.1.1",
69
+ "@playwright/test": "^1.49.1",
70
+ "c8": "^10.1.3",
71
+ "chai": "^5.1.2",
74
72
  "chai-spies": "^1.1.0",
75
73
  "colorette": "^2.0.20",
76
- "eventsource": "^2.0.2",
77
- "express": "^4.21.0",
74
+ "eventsource": "^3.0.2",
75
+ "express": "^4.21.2",
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
- "mocha": "^10.7.3",
83
- "node-mocks-http": "^1.16.0",
80
+ "mocha": "^11.0.1",
81
+ "node-mocks-http": "^1.16.2",
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
+ });
@@ -2,7 +2,7 @@
2
2
  import { expect } from '../chai.js';
3
3
  import request from 'supertest';
4
4
  import Koa from 'koa';
5
- import EventSource from 'eventsource';
5
+ import { EventSource } from 'eventsource';
6
6
  import { TurboStream } from '#core';
7
7
  import { SseTurboStream } from '#sse';
8
8
  import { PassThrough } from 'node:stream';