creevey 0.10.0-beta.44 → 0.10.0-beta.46
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/dist/server/index.js +2 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/master/api.d.ts +5 -10
- package/dist/server/master/api.js +19 -18
- package/dist/server/master/api.js.map +1 -1
- package/dist/server/master/handlers/capture-handler.d.ts +5 -2
- package/dist/server/master/handlers/capture-handler.js +6 -16
- package/dist/server/master/handlers/capture-handler.js.map +1 -1
- package/dist/server/master/handlers/ping-handler.d.ts +2 -2
- package/dist/server/master/handlers/ping-handler.js +2 -1
- package/dist/server/master/handlers/ping-handler.js.map +1 -1
- package/dist/server/master/handlers/static-handler.d.ts +1 -2
- package/dist/server/master/handlers/static-handler.js +10 -20
- package/dist/server/master/handlers/static-handler.js.map +1 -1
- package/dist/server/master/handlers/stories-handler.d.ts +4 -2
- package/dist/server/master/handlers/stories-handler.js +13 -27
- package/dist/server/master/handlers/stories-handler.js.map +1 -1
- package/dist/server/master/server.js +182 -70
- package/dist/server/master/server.js.map +1 -1
- package/dist/server/playwright/docker-file.js +2 -2
- package/dist/server/playwright/docker-file.js.map +1 -1
- package/dist/server/playwright/internal.js +3 -1
- package/dist/server/playwright/internal.js.map +1 -1
- package/dist/server/selenium/internal.js +3 -1
- package/dist/server/selenium/internal.js.map +1 -1
- package/package.json +3 -3
- package/src/server/index.ts +2 -2
- package/src/server/master/api.ts +24 -27
- package/src/server/master/handlers/capture-handler.ts +5 -24
- package/src/server/master/handlers/ping-handler.ts +4 -3
- package/src/server/master/handlers/static-handler.ts +10 -21
- package/src/server/master/handlers/stories-handler.ts +12 -40
- package/src/server/master/server.ts +194 -78
- package/src/server/playwright/docker-file.ts +2 -2
- package/src/server/playwright/internal.ts +3 -1
- package/src/server/selenium/internal.ts +3 -1
package/src/server/master/api.ts
CHANGED
@@ -1,25 +1,28 @@
|
|
1
|
-
import
|
2
|
-
import { Request, Response, CreeveyUpdate } from '../../types.js';
|
1
|
+
import { Data, WebSocket, WebSocketServer } from 'ws';
|
2
|
+
import type { Request, Response, CreeveyUpdate } from '../../types.js';
|
3
|
+
import type { TestsManager } from './testsManager.js';
|
4
|
+
import type Runner from './runner.js';
|
3
5
|
import { logger } from '../logger.js';
|
4
|
-
import { TestsManager } from './testsManager.js';
|
5
|
-
import HyperExpress from 'hyper-express';
|
6
6
|
|
7
|
-
|
8
|
-
clients
|
9
|
-
|
7
|
+
function broadcast(wss: WebSocketServer, message: Response): void {
|
8
|
+
wss.clients.forEach((ws) => {
|
9
|
+
if (ws.readyState === WebSocket.OPEN) {
|
10
|
+
ws.send(JSON.stringify(message));
|
11
|
+
}
|
12
|
+
});
|
10
13
|
}
|
11
14
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
15
|
+
function send(ws: WebSocket, message: Response): void {
|
16
|
+
if (ws.readyState === WebSocket.OPEN) {
|
17
|
+
ws.send(JSON.stringify(message));
|
18
|
+
}
|
16
19
|
}
|
17
20
|
|
18
|
-
// The class-based implementation of CreeveyApi
|
21
|
+
// The class-based implementation of CreeveyApi for native WebSockets
|
19
22
|
export class CreeveyApi {
|
20
23
|
private runner: Runner | null = null;
|
21
24
|
private testsManager: TestsManager;
|
22
|
-
private wss:
|
25
|
+
private wss: WebSocketServer | null = null;
|
23
26
|
|
24
27
|
constructor(testsManager: TestsManager, runner?: Runner) {
|
25
28
|
this.testsManager = testsManager;
|
@@ -30,7 +33,7 @@ export class CreeveyApi {
|
|
30
33
|
}
|
31
34
|
}
|
32
35
|
|
33
|
-
subscribe(wss:
|
36
|
+
subscribe(wss: WebSocketServer): void {
|
34
37
|
this.wss = wss;
|
35
38
|
|
36
39
|
// If we have a runner, subscribe to its updates
|
@@ -46,26 +49,20 @@ export class CreeveyApi {
|
|
46
49
|
}
|
47
50
|
}
|
48
51
|
|
49
|
-
handleMessage(ws:
|
52
|
+
handleMessage(ws: WebSocket, message: Data): void {
|
50
53
|
if (typeof message != 'string') {
|
51
|
-
|
52
|
-
|
53
|
-
} else {
|
54
|
-
logger().info('unhandled message', message);
|
55
|
-
return;
|
56
|
-
}
|
54
|
+
logger().info('unhandled message', message);
|
55
|
+
return;
|
57
56
|
}
|
58
57
|
|
59
58
|
const command = JSON.parse(message) as Request;
|
60
|
-
const sendResponse = (response: Response) => {
|
61
|
-
ws.send(JSON.stringify(response));
|
62
|
-
};
|
63
59
|
|
64
60
|
if (this.runner) {
|
65
61
|
// Normal mode handling with runner
|
66
62
|
switch (command.type) {
|
67
63
|
case 'status': {
|
68
|
-
|
64
|
+
const status = this.runner.status;
|
65
|
+
send(ws, { type: 'status', payload: status });
|
69
66
|
return;
|
70
67
|
}
|
71
68
|
case 'start': {
|
@@ -98,7 +95,7 @@ export class CreeveyApi {
|
|
98
95
|
}
|
99
96
|
case 'status': {
|
100
97
|
// In update mode, respond with static status including tests data
|
101
|
-
|
98
|
+
send(ws, {
|
102
99
|
type: 'status',
|
103
100
|
payload: {
|
104
101
|
isRunning: false,
|
@@ -123,6 +120,6 @@ export class CreeveyApi {
|
|
123
120
|
|
124
121
|
const message: Response = { type: 'update', payload };
|
125
122
|
|
126
|
-
|
123
|
+
broadcast(this.wss, message);
|
127
124
|
}
|
128
125
|
}
|
@@ -1,39 +1,20 @@
|
|
1
|
-
import { Request, Response } from 'hyper-express';
|
2
1
|
import cluster from 'cluster';
|
3
2
|
import { subscribeOnWorker, sendStoriesMessage } from '../../messages.js';
|
4
3
|
import { CaptureOptions, isDefined } from '../../../types.js';
|
5
4
|
|
6
|
-
export
|
7
|
-
const { workerId, options } = await request.json<
|
8
|
-
{ workerId: number; options?: CaptureOptions },
|
9
|
-
{
|
10
|
-
workerId: number;
|
11
|
-
options?: CaptureOptions;
|
12
|
-
}
|
13
|
-
>({
|
14
|
-
workerId: 0,
|
15
|
-
options: undefined,
|
16
|
-
});
|
17
|
-
|
5
|
+
export function captureHandler({ workerId, options }: { workerId: number; options?: CaptureOptions }): void {
|
18
6
|
const worker = Object.values(cluster.workers ?? {})
|
19
7
|
.filter(isDefined)
|
20
8
|
.find((worker) => worker.process.pid == workerId);
|
21
9
|
|
22
10
|
// NOTE: Hypothetical case when someone send to us capture req and we don't have a worker with browser session for it
|
23
11
|
if (!worker) {
|
24
|
-
response.send();
|
25
12
|
return;
|
26
13
|
}
|
27
14
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
unsubscribe();
|
32
|
-
resolve();
|
33
|
-
});
|
34
|
-
sendStoriesMessage(worker, { type: 'capture', payload: options });
|
15
|
+
const unsubscribe = subscribeOnWorker(worker, 'stories', (message) => {
|
16
|
+
if (message.type != 'capture') return;
|
17
|
+
unsubscribe();
|
35
18
|
});
|
36
|
-
|
37
|
-
// TODO Pass screenshot result to show it in inspector
|
38
|
-
response.send('Ok');
|
19
|
+
sendStoriesMessage(worker, { type: 'capture', payload: options });
|
39
20
|
}
|
@@ -1,5 +1,6 @@
|
|
1
|
-
import {
|
1
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
2
2
|
|
3
|
-
export function pingHandler(_request:
|
4
|
-
response.
|
3
|
+
export function pingHandler(_request: IncomingMessage, response: ServerResponse): void {
|
4
|
+
response.setHeader('Content-Type', 'text/plain');
|
5
|
+
response.end('pong');
|
5
6
|
}
|
@@ -1,29 +1,18 @@
|
|
1
|
-
import { Request, Response } from 'hyper-express';
|
2
1
|
import path from 'path';
|
3
2
|
import fs from 'fs';
|
4
|
-
import { logger } from '../../logger.js';
|
5
3
|
|
6
|
-
export function
|
7
|
-
return (
|
8
|
-
|
9
|
-
|
10
|
-
const relativePath = pathPrefix ? decodedPath.replace(pathPrefix, '') : decodedPath;
|
11
|
-
let filePath = path.join(baseDir, relativePath || 'index.html');
|
4
|
+
export function staticHandler(baseDir: string, pathPrefix?: string) {
|
5
|
+
return (requestedPath: string): string | undefined => {
|
6
|
+
const relativePath = pathPrefix ? requestedPath.replace(pathPrefix, '') : requestedPath;
|
7
|
+
let filePath = path.join(baseDir, relativePath || 'index.html');
|
12
8
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
if (!fs.existsSync(filePath)) {
|
19
|
-
response.status(404).send('File not found');
|
20
|
-
return;
|
21
|
-
}
|
9
|
+
// If the path points to a directory, append index.html
|
10
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
|
11
|
+
filePath = path.join(filePath, 'index.html');
|
12
|
+
}
|
22
13
|
|
23
|
-
|
24
|
-
|
25
|
-
logger().error('Error serving file', error);
|
26
|
-
response.status(500).send('Internal server error');
|
14
|
+
if (!fs.existsSync(filePath)) {
|
15
|
+
return undefined;
|
27
16
|
}
|
28
17
|
};
|
29
18
|
}
|
@@ -1,48 +1,20 @@
|
|
1
|
-
import { Request, Response } from 'hyper-express';
|
2
1
|
import cluster from 'cluster';
|
3
2
|
import { emitStoriesMessage, sendStoriesMessage } from '../../messages.js';
|
4
3
|
import { isDefined, StoryInput } from '../../../types.js';
|
5
4
|
import { deserializeStory } from '../../../shared/index.js';
|
6
5
|
|
7
|
-
export function
|
8
|
-
|
6
|
+
export function storiesHandler({ stories }: { stories: [string, StoryInput[]][] }): void {
|
7
|
+
const deserializedStories = stories.map<[string, StoryInput[]]>(([file, stories]) => [
|
8
|
+
file,
|
9
|
+
stories.map(deserializeStory),
|
10
|
+
]);
|
9
11
|
|
10
|
-
|
11
|
-
return async (request: Request, response: Response): Promise<void> => {
|
12
|
-
const { setStoriesCounter: counter, stories } = await request.json<
|
13
|
-
{
|
14
|
-
setStoriesCounter: number;
|
15
|
-
stories: [string, StoryInput[]][];
|
16
|
-
},
|
17
|
-
{
|
18
|
-
setStoriesCounter: number;
|
19
|
-
stories: [string, StoryInput[]][];
|
20
|
-
}
|
21
|
-
>({
|
22
|
-
setStoriesCounter: 0,
|
23
|
-
stories: [],
|
24
|
-
});
|
25
|
-
|
26
|
-
if (setStoriesCounter >= counter) {
|
27
|
-
response.send();
|
28
|
-
return;
|
29
|
-
}
|
30
|
-
|
31
|
-
const deserializedStories = stories.map<[string, StoryInput[]]>(([file, stories]) => [
|
32
|
-
file,
|
33
|
-
stories.map(deserializeStory),
|
34
|
-
]);
|
12
|
+
emitStoriesMessage({ type: 'update', payload: deserializedStories });
|
35
13
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
.forEach((worker) => {
|
43
|
-
sendStoriesMessage(worker, { type: 'update', payload: deserializedStories });
|
44
|
-
});
|
45
|
-
|
46
|
-
response.send();
|
47
|
-
};
|
14
|
+
Object.values(cluster.workers ?? {})
|
15
|
+
.filter(isDefined)
|
16
|
+
.filter((worker) => worker.isConnected())
|
17
|
+
.forEach((worker) => {
|
18
|
+
sendStoriesMessage(worker, { type: 'update', payload: deserializedStories });
|
19
|
+
});
|
48
20
|
}
|
@@ -1,121 +1,237 @@
|
|
1
|
+
import fs from 'fs';
|
2
|
+
import url from 'url';
|
1
3
|
import path from 'path';
|
2
|
-
import
|
4
|
+
import { IncomingMessage, ServerResponse, createServer } from 'http';
|
5
|
+
import { WebSocketServer, WebSocket, RawData } from 'ws';
|
3
6
|
import { fileURLToPath, pathToFileURL } from 'url';
|
4
|
-
import {
|
7
|
+
import { shutdownOnException } from '../utils.js';
|
5
8
|
import { subscribeOn } from '../messages.js';
|
6
9
|
import { noop } from '../../types.js';
|
7
10
|
import { logger } from '../logger.js';
|
8
|
-
import {
|
11
|
+
import { CreeveyApi } from './api.js';
|
12
|
+
import { pingHandler, captureHandler, storiesHandler, staticHandler } from './handlers/index.js';
|
9
13
|
|
10
|
-
|
14
|
+
function json<T = unknown>(
|
15
|
+
handler: (data: T) => void,
|
16
|
+
defaultValue: T,
|
17
|
+
): (request: IncomingMessage, response: ServerResponse) => void {
|
18
|
+
return (request: IncomingMessage, response: ServerResponse) => {
|
19
|
+
const chunks: Buffer[] = [];
|
11
20
|
|
12
|
-
|
13
|
-
|
14
|
-
|
21
|
+
request.on('data', (chunk: Buffer) => {
|
22
|
+
chunks.push(chunk);
|
23
|
+
});
|
15
24
|
|
16
|
-
|
17
|
-
|
25
|
+
request.on('end', () => {
|
26
|
+
try {
|
27
|
+
const body = Buffer.concat(chunks);
|
28
|
+
const value = body.length === 0 ? defaultValue : (JSON.parse(body.toString('utf-8')) as T);
|
18
29
|
|
19
|
-
|
20
|
-
|
30
|
+
handler(value);
|
31
|
+
response.end();
|
32
|
+
} catch (error) {
|
33
|
+
logger().error('Failed to parse JSON', error);
|
34
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
35
|
+
response.statusCode = 500;
|
36
|
+
response.setHeader('Content-Type', 'text/plain');
|
37
|
+
response.end(`Failed to parse JSON: ${errorMessage}`);
|
38
|
+
}
|
39
|
+
});
|
21
40
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
41
|
+
request.on('error', (error) => {
|
42
|
+
logger().error('Failed to parse JSON', error);
|
43
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
44
|
+
response.statusCode = 500;
|
45
|
+
response.setHeader('Content-Type', 'text/plain');
|
46
|
+
response.end(`Failed to parse JSON: ${errorMessage}`);
|
47
|
+
});
|
48
|
+
};
|
49
|
+
}
|
27
50
|
|
28
|
-
|
29
|
-
|
51
|
+
function file(handler: (requestedPath: string) => string | undefined) {
|
52
|
+
return (request: IncomingMessage, response: ServerResponse) => {
|
53
|
+
const parsedUrl = url.parse(request.url ?? '/', true);
|
54
|
+
const requestedPath = parsedUrl.pathname ?? '/';
|
55
|
+
|
56
|
+
try {
|
57
|
+
const filePath = handler(requestedPath);
|
58
|
+
if (filePath) {
|
59
|
+
const stat = fs.statSync(filePath);
|
60
|
+
// Set appropriate MIME type
|
61
|
+
const ext = path.extname(filePath).toLowerCase();
|
62
|
+
const mimeTypes: Record<string, string> = {
|
63
|
+
'.html': 'text/html',
|
64
|
+
'.js': 'application/javascript',
|
65
|
+
'.css': 'text/css',
|
66
|
+
'.json': 'application/json',
|
67
|
+
'.png': 'image/png',
|
68
|
+
'.jpg': 'image/jpeg',
|
69
|
+
'.jpeg': 'image/jpeg',
|
70
|
+
'.gif': 'image/gif',
|
71
|
+
'.svg': 'image/svg+xml',
|
72
|
+
'.ico': 'image/x-icon',
|
73
|
+
};
|
74
|
+
|
75
|
+
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
76
|
+
|
77
|
+
response.statusCode = 200;
|
78
|
+
response.setHeader('Content-Type', contentType);
|
79
|
+
response.setHeader('Content-Length', stat.size);
|
80
|
+
|
81
|
+
// Stream the file
|
82
|
+
const stream = fs.createReadStream(filePath);
|
83
|
+
stream.pipe(response);
|
84
|
+
|
85
|
+
stream.on('error', (error) => {
|
86
|
+
logger().error('Error streaming file', error);
|
87
|
+
if (!response.headersSent) {
|
88
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
89
|
+
response.statusCode = 500;
|
90
|
+
response.setHeader('Content-Type', 'text/plain');
|
91
|
+
response.end(`Internal server error: ${errorMessage}`);
|
92
|
+
}
|
93
|
+
});
|
94
|
+
} else {
|
95
|
+
logger().error('File not found', requestedPath);
|
96
|
+
response.statusCode = 404;
|
97
|
+
response.setHeader('Content-Type', 'text/plain');
|
98
|
+
response.end('File not found');
|
99
|
+
}
|
100
|
+
} catch (error) {
|
101
|
+
logger().error('Failed to serve file', error);
|
102
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
103
|
+
response.statusCode = 500;
|
104
|
+
response.setHeader('Content-Type', 'text/plain');
|
105
|
+
response.end(`Failed to serve file: ${errorMessage}`);
|
30
106
|
}
|
107
|
+
};
|
108
|
+
}
|
31
109
|
|
32
|
-
|
33
|
-
});
|
34
|
-
|
35
|
-
// Health check endpoint
|
36
|
-
server.get('/ping', pingHandler);
|
37
|
-
|
38
|
-
// Stories endpoint
|
39
|
-
server.post('/stories', createStoriesHandler());
|
40
|
-
|
41
|
-
// Capture endpoint
|
42
|
-
server.post('/capture', captureHandler);
|
110
|
+
const importMetaUrl = pathToFileURL(__filename).href;
|
43
111
|
|
44
|
-
|
45
|
-
|
112
|
+
export function start(reportDir: string, port: number, ui: boolean): (api: CreeveyApi) => void {
|
113
|
+
let wss: WebSocketServer | null = null;
|
114
|
+
let creeveyApi: CreeveyApi | null = null;
|
115
|
+
let resolveApi: (api: CreeveyApi) => void = noop;
|
46
116
|
|
47
|
-
// Serve static files
|
48
117
|
const webDir = path.join(path.dirname(fileURLToPath(importMetaUrl)), '../../client/web');
|
49
|
-
server
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
118
|
+
const server = createServer();
|
119
|
+
|
120
|
+
const routes = [
|
121
|
+
{
|
122
|
+
path: '/ping',
|
123
|
+
method: 'GET',
|
124
|
+
handler: pingHandler,
|
125
|
+
},
|
126
|
+
{
|
127
|
+
path: '/stories',
|
128
|
+
method: 'POST',
|
129
|
+
handler: json(storiesHandler, { stories: [] }),
|
130
|
+
},
|
131
|
+
{
|
132
|
+
path: '/capture',
|
133
|
+
method: 'POST',
|
134
|
+
handler: json(captureHandler, { workerId: 0, options: undefined }),
|
135
|
+
},
|
136
|
+
{
|
137
|
+
path: '/report/',
|
138
|
+
method: 'GET',
|
139
|
+
handler: file(staticHandler(reportDir, '/report/')),
|
140
|
+
},
|
141
|
+
{
|
142
|
+
path: '/',
|
143
|
+
method: 'GET',
|
144
|
+
handler: file(staticHandler(webDir)),
|
145
|
+
},
|
146
|
+
];
|
147
|
+
|
148
|
+
const router = (request: IncomingMessage, response: ServerResponse): void => {
|
149
|
+
const parsedUrl = url.parse(request.url ?? '/', true);
|
150
|
+
const path = parsedUrl.pathname ?? '/';
|
151
|
+
const method = request.method ?? 'GET';
|
152
|
+
|
153
|
+
try {
|
154
|
+
const route = routes.find((route) => path.startsWith(route.path) && route.method === method);
|
155
|
+
if (route) {
|
156
|
+
route.handler(request, response);
|
157
|
+
} else {
|
158
|
+
response.statusCode = 404;
|
159
|
+
response.setHeader('Content-Type', 'text/plain');
|
160
|
+
response.end('Not Found');
|
57
161
|
}
|
58
|
-
}
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
162
|
+
} catch (error) {
|
163
|
+
logger().error('Request handling error', error);
|
164
|
+
response.statusCode = 500;
|
165
|
+
response.setHeader('Content-Type', 'text/plain');
|
166
|
+
response.end('Internal Server Error');
|
167
|
+
}
|
168
|
+
};
|
65
169
|
|
66
|
-
|
170
|
+
server.on('request', (request: IncomingMessage, response: ServerResponse): void => {
|
171
|
+
response.setHeader('Access-Control-Allow-Origin', '*');
|
172
|
+
response.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
173
|
+
response.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
67
174
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
next();
|
74
|
-
});
|
175
|
+
if (request.method === 'OPTIONS') {
|
176
|
+
response.statusCode = 200;
|
177
|
+
response.end();
|
178
|
+
return;
|
179
|
+
}
|
75
180
|
|
76
|
-
|
77
|
-
|
78
|
-
// Add connection to the set of active connections
|
79
|
-
activeConnections.add(ws);
|
181
|
+
router(request, response);
|
182
|
+
});
|
80
183
|
|
81
|
-
|
82
|
-
|
83
|
-
|
184
|
+
if (ui) {
|
185
|
+
wss = new WebSocketServer({ server });
|
186
|
+
wss.on('connection', (ws: WebSocket) => {
|
187
|
+
ws.on('message', (message: RawData, isBinary: boolean) => {
|
188
|
+
if (creeveyApi) {
|
189
|
+
// NOTE Text messages are passed as Buffer https://github.com/websockets/ws/releases/tag/8.0.0
|
190
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
191
|
+
creeveyApi.handleMessage(ws, isBinary ? message : message.toString('utf-8'));
|
192
|
+
return;
|
193
|
+
}
|
84
194
|
});
|
85
195
|
|
86
|
-
|
87
|
-
|
88
|
-
activeConnections.delete(ws);
|
196
|
+
ws.on('error', (error) => {
|
197
|
+
logger().error('WebSocket error', error);
|
89
198
|
});
|
90
199
|
});
|
200
|
+
|
201
|
+
wss.on('error', (error) => {
|
202
|
+
logger().error('WebSocket error', error);
|
203
|
+
});
|
91
204
|
}
|
92
205
|
|
93
|
-
// Shutdown handling
|
94
206
|
subscribeOn('shutdown', () => {
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
logger().error('Error closing WebSocket connection', error);
|
101
|
-
}
|
207
|
+
if (wss) {
|
208
|
+
wss.clients.forEach((ws) => {
|
209
|
+
ws.close();
|
210
|
+
});
|
211
|
+
wss.close();
|
102
212
|
}
|
103
213
|
|
104
|
-
// Close the server
|
105
214
|
server.close();
|
106
215
|
});
|
107
216
|
|
108
|
-
// Start server
|
109
217
|
server
|
110
|
-
.listen(port)
|
111
|
-
.then(() => {
|
218
|
+
.listen(port, () => {
|
112
219
|
logger().info(`Server starting on port ${port}`);
|
113
220
|
})
|
114
|
-
.
|
221
|
+
.on('error', (error: unknown) => {
|
115
222
|
logger().error('Failed to start server', error);
|
116
223
|
process.exit(1);
|
117
224
|
});
|
118
225
|
|
226
|
+
void new Promise<CreeveyApi>((resolve) => (resolveApi = resolve))
|
227
|
+
.then((api) => {
|
228
|
+
creeveyApi = api;
|
229
|
+
if (wss) {
|
230
|
+
creeveyApi.subscribe(wss);
|
231
|
+
}
|
232
|
+
})
|
233
|
+
.catch(shutdownOnException);
|
234
|
+
|
119
235
|
// Return the function to resolve the API
|
120
236
|
return resolveApi;
|
121
237
|
}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { readFile } from 'fs/promises';
|
2
2
|
import { pathToFileURL } from 'url';
|
3
3
|
import semver from 'semver';
|
4
|
-
import
|
4
|
+
import sh from 'shelljs';
|
5
5
|
|
6
6
|
const importMetaUrl = pathToFileURL(__filename).href;
|
7
7
|
|
@@ -11,7 +11,7 @@ export async function playwrightDockerFile(browser: string, version: string): Pr
|
|
11
11
|
|
12
12
|
let npmRegistry;
|
13
13
|
try {
|
14
|
-
npmRegistry = exec('npm config get registry', { silent: true }).stdout.trim();
|
14
|
+
npmRegistry = sh.exec('npm config get registry', { silent: true }).stdout.trim();
|
15
15
|
} catch {
|
16
16
|
/* noop */
|
17
17
|
}
|
@@ -439,7 +439,9 @@ export class InternalBrowser {
|
|
439
439
|
}
|
440
440
|
|
441
441
|
private async resolveCreeveyHost(): Promise<void> {
|
442
|
-
const
|
442
|
+
const storybookUrl = this.#page.url();
|
443
|
+
const storybookHost = new URL(storybookUrl).hostname;
|
444
|
+
const addresses = [storybookHost, ...getAddresses()];
|
443
445
|
|
444
446
|
this.#serverHost = await this.#page.evaluate(
|
445
447
|
([hosts, port]) => {
|
@@ -591,7 +591,9 @@ export class InternalBrowser {
|
|
591
591
|
}
|
592
592
|
|
593
593
|
private async resolveCreeveyHost(): Promise<void> {
|
594
|
-
const
|
594
|
+
const storybookUrl = await this.#browser.getCurrentUrl();
|
595
|
+
const storybookHost = new URL(storybookUrl).hostname;
|
596
|
+
const addresses = [storybookHost, ...getAddresses()];
|
595
597
|
|
596
598
|
this.#serverHost = await this.#browser.executeAsyncScript(
|
597
599
|
function (hosts: string[], port: number, callback: (host?: string | null) => void) {
|