mockaton 12.7.0 → 12.7.1
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/package.json +1 -1
- package/src/client/ApiCommander.js +8 -31
- package/src/client/ApiConstants.js +0 -2
- package/src/client/IndexHtml.js +4 -6
- package/src/client/app-header.js +161 -0
- package/src/client/app-icons.js +29 -0
- package/src/client/{payload-viewer.js → app-payload-viewer.js} +12 -15
- package/src/client/app-store.js +0 -2
- package/src/client/app.css +11 -3
- package/src/client/app.js +77 -246
- package/src/client/dom-utils.js +1 -3
- package/src/client/{css-modules.test.js → dom-utils.test.js} +7 -2
- package/src/client/watcherDev.js +26 -17
- package/src/server/Api.js +6 -6
- package/src/server/Mockaton.test.js +45 -34
- package/src/server/Watcher.js +25 -21
- package/src/server/WatcherDevClient.js +23 -14
- package/src/server/cli.js +1 -1
- package/src/server/cli.test.js +13 -4
- package/src/server/utils/validate.test.js +16 -11
|
@@ -53,6 +53,7 @@ const renameInStaticMocksDir = (src, target) => rename(join(staticDir, src), joi
|
|
|
53
53
|
|
|
54
54
|
const api = new Commander(serverAddr)
|
|
55
55
|
|
|
56
|
+
|
|
56
57
|
/** @returns {Promise<State>} */
|
|
57
58
|
async function fetchState() {
|
|
58
59
|
return (await api.getState()).json()
|
|
@@ -78,14 +79,14 @@ class BaseFixture {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
async register() {
|
|
81
|
-
const nextVerPromise =
|
|
82
|
+
const nextVerPromise = resolveOnNextSyncVersion()
|
|
82
83
|
await this.#nextMacroTask()
|
|
83
84
|
await this.write()
|
|
84
85
|
await nextVerPromise
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
async unregister() {
|
|
88
|
-
const nextVerPromise =
|
|
89
|
+
const nextVerPromise = resolveOnNextSyncVersion()
|
|
89
90
|
await this.#nextMacroTask()
|
|
90
91
|
await this.unlink()
|
|
91
92
|
await nextVerPromise
|
|
@@ -1109,38 +1110,30 @@ describe('Registering Mocks', () => {
|
|
|
1109
1110
|
|
|
1110
1111
|
describe('getSyncVersion', () => {
|
|
1111
1112
|
const fx0 = new Fixture('reg0/runtime0.GET.200.txt')
|
|
1113
|
+
let version
|
|
1112
1114
|
before(async () => {
|
|
1113
1115
|
await makeDirInMocks('reg0')
|
|
1114
1116
|
await fx0.sync()
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
let version
|
|
1118
|
-
|
|
1119
|
-
test('getSyncVersion responds immediately when version mismatches', async () => {
|
|
1120
|
-
const r = await api.getSyncVersion(-1)
|
|
1121
|
-
version = await r.json()
|
|
1117
|
+
version = await resolveOnNextSyncVersion(-1)
|
|
1122
1118
|
})
|
|
1123
1119
|
|
|
1124
1120
|
const fx = new Fixture('runtime1.GET.200.txt')
|
|
1125
1121
|
test('responds when a file is added', async () => {
|
|
1126
|
-
const prom =
|
|
1122
|
+
const prom = resolveOnNextSyncVersion(version)
|
|
1127
1123
|
await fx.write()
|
|
1128
|
-
|
|
1129
|
-
equal(await r.json(), version + 1)
|
|
1124
|
+
equal(await prom, version + 1)
|
|
1130
1125
|
})
|
|
1131
1126
|
|
|
1132
1127
|
test('responds when a file is deleted', async () => {
|
|
1133
|
-
const prom =
|
|
1128
|
+
const prom = resolveOnNextSyncVersion(version + 1)
|
|
1134
1129
|
await fx.unlink()
|
|
1135
|
-
|
|
1136
|
-
equal(await r.json(), version + 2)
|
|
1130
|
+
equal(await prom, version + 2)
|
|
1137
1131
|
})
|
|
1138
1132
|
|
|
1139
1133
|
test('responds when dir is renamed', async () => {
|
|
1140
|
-
const p0 =
|
|
1134
|
+
const p0 = resolveOnNextSyncVersion(version + 2)
|
|
1141
1135
|
await renameInMocksDir('reg0', 'reg1')
|
|
1142
|
-
|
|
1143
|
-
equal(await r0.json(), version + 3)
|
|
1136
|
+
equal(await p0, version + 3)
|
|
1144
1137
|
|
|
1145
1138
|
const s = await fetchState()
|
|
1146
1139
|
equal(s.brokersByMethod.GET['/reg1/runtime0'].file, 'reg1/runtime0.GET.200.txt')
|
|
@@ -1185,38 +1178,30 @@ describe('Registering Static Mocks', () => {
|
|
|
1185
1178
|
|
|
1186
1179
|
describe('getSyncVersion', () => {
|
|
1187
1180
|
const fx0 = new FixtureStatic('reg0/static0.txt')
|
|
1181
|
+
let version
|
|
1188
1182
|
before(async () => {
|
|
1189
1183
|
await makeDirInStaticMocks('reg0')
|
|
1190
1184
|
await fx0.sync()
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
let version
|
|
1194
|
-
|
|
1195
|
-
test('getSyncVersion responds immediately when version mismatches', async () => {
|
|
1196
|
-
const r = await api.getSyncVersion(-1)
|
|
1197
|
-
version = await r.json()
|
|
1185
|
+
version = await resolveOnNextSyncVersion(-1)
|
|
1198
1186
|
})
|
|
1199
1187
|
|
|
1200
1188
|
const fx = new FixtureStatic('static1.txt')
|
|
1201
1189
|
test('responds when a file is added', async () => {
|
|
1202
|
-
const prom =
|
|
1190
|
+
const prom = resolveOnNextSyncVersion(version)
|
|
1203
1191
|
await fx.write()
|
|
1204
|
-
|
|
1205
|
-
equal(await r.json(), version + 1)
|
|
1192
|
+
equal(await prom, version + 1)
|
|
1206
1193
|
})
|
|
1207
1194
|
|
|
1208
1195
|
test('responds when a file is deleted', async () => {
|
|
1209
|
-
const prom =
|
|
1196
|
+
const prom = resolveOnNextSyncVersion(version + 1)
|
|
1210
1197
|
await fx.unlink()
|
|
1211
|
-
|
|
1212
|
-
equal(await r.json(), version + 2)
|
|
1198
|
+
equal(await prom, version + 2)
|
|
1213
1199
|
})
|
|
1214
1200
|
|
|
1215
1201
|
test('responds when dir is renamed', async () => {
|
|
1216
|
-
const p0 =
|
|
1202
|
+
const p0 = resolveOnNextSyncVersion(version + 2)
|
|
1217
1203
|
await renameInStaticMocksDir('reg0', 'reg1')
|
|
1218
|
-
|
|
1219
|
-
equal(await r0.json(), version + 3)
|
|
1204
|
+
equal(await p0, version + 3)
|
|
1220
1205
|
|
|
1221
1206
|
const s = await fetchState()
|
|
1222
1207
|
equal(s.staticBrokers['/reg1/static0.txt'].route, '/reg1/static0.txt')
|
|
@@ -1228,3 +1213,29 @@ describe('Registering Static Mocks', () => {
|
|
|
1228
1213
|
async function sleep(ms = 100) {
|
|
1229
1214
|
await new Promise(resolve => setTimeout(resolve, ms))
|
|
1230
1215
|
}
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
/** In Node, there's no EventSource, so we work around it like this.
|
|
1219
|
+
* This is for listening to real-time updates. It responds when a new mock is added, deleted, or renamed. */
|
|
1220
|
+
async function resolveOnNextSyncVersion(currSyncVer = undefined) {
|
|
1221
|
+
let skipFirst = currSyncVer === undefined
|
|
1222
|
+
const response = await api.getSyncVersion()
|
|
1223
|
+
const stream = response.body.pipeThrough(new TextDecoderStream())
|
|
1224
|
+
let buffer = ''
|
|
1225
|
+
|
|
1226
|
+
for await (const chunk of stream) {
|
|
1227
|
+
buffer += chunk
|
|
1228
|
+
const parts = buffer.split('\n\n')
|
|
1229
|
+
buffer = parts.pop() || ''
|
|
1230
|
+
|
|
1231
|
+
for (const event of parts)
|
|
1232
|
+
for (const line of event.split(/\r?\n/))
|
|
1233
|
+
if (line.startsWith('data:')) {
|
|
1234
|
+
const v = Number(line.slice(5).trim())
|
|
1235
|
+
if (skipFirst || v === currSyncVer)
|
|
1236
|
+
skipFirst = false
|
|
1237
|
+
else
|
|
1238
|
+
return v
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
package/src/server/Watcher.js
CHANGED
|
@@ -2,11 +2,6 @@ import { join } from 'node:path'
|
|
|
2
2
|
import { watch } from 'node:fs'
|
|
3
3
|
import { EventEmitter } from 'node:events'
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
HEADER_SYNC_VERSION,
|
|
7
|
-
LONG_POLL_SERVER_TIMEOUT
|
|
8
|
-
} from '../client/ApiConstants.js'
|
|
9
|
-
|
|
10
5
|
import { config } from './config.js'
|
|
11
6
|
import { isFile, isDirectory } from './utils/fs.js'
|
|
12
7
|
|
|
@@ -33,7 +28,7 @@ const uiSyncVersion = new class extends EventEmitter {
|
|
|
33
28
|
})
|
|
34
29
|
|
|
35
30
|
subscribe(listener) {
|
|
36
|
-
this.
|
|
31
|
+
this.on('ARR', listener)
|
|
37
32
|
}
|
|
38
33
|
unsubscribe(listener) {
|
|
39
34
|
this.removeListener('ARR', listener)
|
|
@@ -98,24 +93,33 @@ export function watchStaticDir() {
|
|
|
98
93
|
}
|
|
99
94
|
|
|
100
95
|
|
|
96
|
+
|
|
101
97
|
/** Realtime notify ARR Events */
|
|
102
|
-
export function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
98
|
+
export function sseClientSyncVersion(req, response) {
|
|
99
|
+
response.writeHead(200, {
|
|
100
|
+
'Content-Type': 'text/event-stream',
|
|
101
|
+
'Cache-Control': 'no-cache',
|
|
102
|
+
'Connection': 'keep-alive',
|
|
103
|
+
})
|
|
104
|
+
response.flushHeaders()
|
|
105
|
+
|
|
106
|
+
function sendVersion() {
|
|
107
|
+
response.write(`data: ${uiSyncVersion.version}\n\n`)
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
|
|
110
|
+
sendVersion()
|
|
111
|
+
uiSyncVersion.subscribe(sendVersion)
|
|
112
|
+
|
|
113
|
+
const keepAlive = setInterval(() => {
|
|
114
|
+
response.write(': ping\n\n')
|
|
115
|
+
}, 10_000)
|
|
116
|
+
|
|
117
|
+
req.on('close', cleanup)
|
|
118
|
+
req.on('error', cleanup)
|
|
119
|
+
function cleanup() {
|
|
120
|
+
clearInterval(keepAlive)
|
|
121
|
+
uiSyncVersion.unsubscribe(sendVersion)
|
|
112
122
|
}
|
|
113
|
-
response.setTimeout(LONG_POLL_SERVER_TIMEOUT, onARR)
|
|
114
|
-
req.on('error', () => {
|
|
115
|
-
uiSyncVersion.unsubscribe(onARR)
|
|
116
|
-
response.destroy()
|
|
117
|
-
})
|
|
118
|
-
uiSyncVersion.subscribe(onARR)
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
|
|
@@ -3,7 +3,6 @@ import { EventEmitter } from 'node:events'
|
|
|
3
3
|
import { watch, readdirSync } from 'node:fs'
|
|
4
4
|
|
|
5
5
|
import { config } from './config.js'
|
|
6
|
-
import { LONG_POLL_SERVER_TIMEOUT } from '../client/ApiConstants.js'
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
export const CLIENT_DIR = join(import.meta.dirname, '../client')
|
|
@@ -12,7 +11,7 @@ export const DASHBOARD_ASSETS = readdirSync(CLIENT_DIR)
|
|
|
12
11
|
|
|
13
12
|
const devClientWatcher = new class extends EventEmitter {
|
|
14
13
|
emit(file) { super.emit('RELOAD', file) }
|
|
15
|
-
subscribe(listener) { this.
|
|
14
|
+
subscribe(listener) { this.on('RELOAD', listener) }
|
|
16
15
|
unsubscribe(listener) { this.removeListener('RELOAD', listener) }
|
|
17
16
|
}
|
|
18
17
|
|
|
@@ -27,23 +26,33 @@ export function watchDevSPA() {
|
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
/** Realtime notify Dev UI changes */
|
|
30
|
-
export function
|
|
29
|
+
export function sseClientHotReload(req, response) {
|
|
31
30
|
if (!config.hotReload) {
|
|
32
31
|
response.notFound()
|
|
33
32
|
return
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
response.setTimeout(LONG_POLL_SERVER_TIMEOUT, () => {
|
|
41
|
-
devClientWatcher.unsubscribe(onDevChange)
|
|
42
|
-
response.json('')
|
|
43
|
-
})
|
|
44
|
-
req.on('error', () => {
|
|
45
|
-
devClientWatcher.unsubscribe(onDevChange)
|
|
46
|
-
response.destroy()
|
|
35
|
+
response.writeHead(200, {
|
|
36
|
+
'Content-Type': 'text/event-stream',
|
|
37
|
+
'Cache-Control': 'no-cache',
|
|
38
|
+
'Connection': 'keep-alive',
|
|
47
39
|
})
|
|
40
|
+
response.flushHeaders()
|
|
41
|
+
|
|
42
|
+
function onDevChange(file = '') {
|
|
43
|
+
response.write(`data: ${file}\n\n`)
|
|
44
|
+
}
|
|
45
|
+
|
|
48
46
|
devClientWatcher.subscribe(onDevChange)
|
|
47
|
+
|
|
48
|
+
const keepAlive = setInterval(() => {
|
|
49
|
+
response.write(': ping\n\n')
|
|
50
|
+
}, 10_000)
|
|
51
|
+
|
|
52
|
+
req.on('close', cleanup)
|
|
53
|
+
req.on('error', cleanup)
|
|
54
|
+
function cleanup() {
|
|
55
|
+
clearInterval(keepAlive)
|
|
56
|
+
devClientWatcher.unsubscribe(onDevChange)
|
|
57
|
+
}
|
|
49
58
|
}
|
package/src/server/cli.js
CHANGED
|
@@ -78,7 +78,7 @@ else {
|
|
|
78
78
|
: {}
|
|
79
79
|
|
|
80
80
|
if (args.host) opts.host = args.host
|
|
81
|
-
if (args.port) opts.port = Number(args.port)
|
|
81
|
+
if (args.port) opts.port = Number.isNaN(Number(args.port)) ? args.port : Number(args.port)
|
|
82
82
|
|
|
83
83
|
if (args['mocks-dir']) opts.mocksDir = args['mocks-dir']
|
|
84
84
|
if (args['static-dir']) opts.staticDir = args['static-dir']
|
package/src/server/cli.test.js
CHANGED
|
@@ -5,12 +5,12 @@ import { describe, test } from 'node:test'
|
|
|
5
5
|
|
|
6
6
|
import pkgJSON from '../../package.json' with { type: 'json' }
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
})
|
|
8
|
+
|
|
9
|
+
const rel = f => join(import.meta.dirname, f)
|
|
10
|
+
const cli = (...args) => spawnSync(rel('cli.js'), args, { encoding: 'utf8' })
|
|
11
11
|
|
|
12
12
|
describe('CLI', () => {
|
|
13
|
-
test('
|
|
13
|
+
test('invalid flag', () => {
|
|
14
14
|
const { stderr, status } = cli('--invalid-flag')
|
|
15
15
|
equal(stderr.trim(), `Unknown option '--invalid-flag'`)
|
|
16
16
|
equal(status, 1)
|
|
@@ -22,6 +22,15 @@ describe('CLI', () => {
|
|
|
22
22
|
equal(status, 1)
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
+
test('invalid port', () => {
|
|
26
|
+
const { stderr, status } = cli(
|
|
27
|
+
'--mocks-dir', rel('../../mockaton-mocks'),
|
|
28
|
+
'--port', 'not-a-number',
|
|
29
|
+
)
|
|
30
|
+
equal(stderr.trim(), `port="not-a-number" is invalid`)
|
|
31
|
+
equal(status, 1)
|
|
32
|
+
})
|
|
33
|
+
|
|
25
34
|
test('-v outputs version from package.json', () => {
|
|
26
35
|
const { stdout, status } = cli('-v')
|
|
27
36
|
equal(stdout.trim(), pkgJSON.version)
|
|
@@ -7,41 +7,46 @@ describe('validate', () => {
|
|
|
7
7
|
describe('optional', () => {
|
|
8
8
|
test('accepts undefined', () =>
|
|
9
9
|
doesNotThrow(() =>
|
|
10
|
-
validate({}, {
|
|
10
|
+
validate({}, { foo: optional(Number.isInteger) })))
|
|
11
11
|
|
|
12
12
|
test('accepts falsy value regardless of type', () =>
|
|
13
13
|
doesNotThrow(() =>
|
|
14
|
-
validate({
|
|
14
|
+
validate({ foo: 0 }, { foo: optional(Array.isArray) })))
|
|
15
15
|
|
|
16
16
|
test('accepts when tester func returns truthy', () =>
|
|
17
17
|
doesNotThrow(() =>
|
|
18
|
-
validate({
|
|
18
|
+
validate({ foo: [] }, { foo: optional(Array.isArray) })))
|
|
19
19
|
|
|
20
20
|
test('rejects when tester func returns falsy', () =>
|
|
21
21
|
throws(() =>
|
|
22
|
-
validate({
|
|
23
|
-
/
|
|
22
|
+
validate({ foo: 1 }, { foo: optional(Array.isArray) }),
|
|
23
|
+
/foo=1 is invalid/))
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
describe('is', () => {
|
|
27
27
|
test('rejects mismatched type', () =>
|
|
28
28
|
throws(() =>
|
|
29
|
-
validate({
|
|
30
|
-
/
|
|
29
|
+
validate({ foo: 1 }, { foo: is(String) }),
|
|
30
|
+
/foo=1 is invalid/))
|
|
31
31
|
|
|
32
32
|
test('accepts matched type', () =>
|
|
33
33
|
doesNotThrow(() =>
|
|
34
|
-
validate({
|
|
34
|
+
validate({ foo: '' }, { foo: is(String) })))
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
describe('custom tester func', () => {
|
|
38
38
|
test('rejects mismatched type', () =>
|
|
39
39
|
throws(() =>
|
|
40
|
-
validate({
|
|
41
|
-
/
|
|
40
|
+
validate({ foo: 'not-a-number' }, { foo: n => n > 1 }),
|
|
41
|
+
/foo="not-a-number" is invalid/))
|
|
42
|
+
|
|
43
|
+
test('rejects mismatched type', () =>
|
|
44
|
+
throws(() =>
|
|
45
|
+
validate({ foo: 0 }, { foo: n => n > 1 }),
|
|
46
|
+
/foo=0 is invalid/))
|
|
42
47
|
|
|
43
48
|
test('accepts matched type', () =>
|
|
44
49
|
doesNotThrow(() =>
|
|
45
|
-
validate({
|
|
50
|
+
validate({ foo: 1 }, { foo: Number.isInteger })))
|
|
46
51
|
})
|
|
47
52
|
})
|