Skip to content

Commit f3ca116

Browse files
authored
Merge pull request #15 from qoretechnologies/feature/websockets
Feature/websockets
2 parents 47dc2d2 + 2cc2c17 commit f3ca116

File tree

9 files changed

+2484
-396
lines changed

9 files changed

+2484
-396
lines changed

__tests__/utils.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, screen, waitFor } from '@storybook/test';
1+
import { expect, fireEvent, screen, waitFor } from '@storybook/test';
22

33
export const sleep = (ms: number) => {
44
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -9,3 +9,34 @@ export async function testsWaitForText(text: string, selector?: string) {
99
timeout: 10000,
1010
});
1111
}
12+
13+
export async function testsClickButton({
14+
label,
15+
selector,
16+
nth = 0,
17+
wait = 7000,
18+
parent = '.reqore-button',
19+
}: {
20+
label?: string;
21+
selector?: string;
22+
nth?: number;
23+
wait?: number;
24+
parent?: string;
25+
}) {
26+
if (!label) {
27+
await waitFor(() => expect(document.querySelectorAll(selector)[nth]).toBeInTheDocument(), {
28+
timeout: wait,
29+
});
30+
await fireEvent.click(document.querySelectorAll(selector)[nth]);
31+
} else {
32+
await waitFor(
33+
() => expect(screen.queryAllByText(label, { selector })[nth]).toBeInTheDocument(),
34+
{ timeout: wait }
35+
);
36+
await waitFor(
37+
() => expect(screen.queryAllByText(label, { selector })[nth].closest(parent)).toBeEnabled(),
38+
{ timeout: wait }
39+
);
40+
await fireEvent.click(screen.queryAllByText(label, { selector })[nth]);
41+
}
42+
}

chromatic.config.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"onlyChanged": true,
3+
"projectId": "Project:662bcb94acac0fd11755c83a",
4+
"zip": true
5+
}

package.json

+16-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@qoretechnologies/reqraft",
3-
"version": "0.3.4",
3+
"version": "0.4.0",
44
"description": "ReQraft is a collection of React components and hooks that are used across Qore Technologies' products made using the ReQore component library from Qore.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -16,7 +16,7 @@
1616
"lint": "yarn run eslint ./src/",
1717
"storybook": "storybook dev -p 6008",
1818
"update-reqore": "yarn add @qoretechnologies/reqore@beta",
19-
"test-storybook": "DEBUG_PRINT_LIMIT=0 test-storybook --url http://localhost:6008",
19+
"test-storybook": "DEBUG_PRINT_LIMIT=300 test-storybook --url http://localhost:6008",
2020
"install-playwright": "npx playwright install --with-deps",
2121
"build-storybook": "storybook build",
2222
"chromatic": "npx chromatic"
@@ -51,20 +51,20 @@
5151
"@babel/preset-env": "^7.12.11",
5252
"@babel/preset-react": "^7.12.10",
5353
"@babel/preset-typescript": "^7.12.7",
54-
"@chromatic-com/storybook": "^1",
54+
"@chromatic-com/storybook": "^1.5.0",
5555
"@qoretechnologies/reqore": "^0.43.0",
56-
"@storybook/addon-actions": "^8.0.9",
57-
"@storybook/addon-essentials": "^8.0.9",
58-
"@storybook/addon-interactions": "^8.0.9",
59-
"@storybook/addon-links": "^8.0.9",
56+
"@storybook/addon-actions": "^8.1.7",
57+
"@storybook/addon-essentials": "^8.1.7",
58+
"@storybook/addon-interactions": "^8.1.7",
59+
"@storybook/addon-links": "^8.1.7",
6060
"@storybook/addon-webpack5-compiler-babel": "^3.0.3",
61-
"@storybook/manager-api": "^8.0.9",
62-
"@storybook/react": "^8.0.9",
63-
"@storybook/react-webpack5": "^8.0.9",
64-
"@storybook/test": "^8.0.9",
65-
"@storybook/test-runner": "^0.17.0",
66-
"@storybook/theming": "^8.0.9",
67-
"@storybook/types": "^8.0.9",
61+
"@storybook/manager-api": "^8.1.7",
62+
"@storybook/react": "^8.1.7",
63+
"@storybook/react-webpack5": "^8.1.7",
64+
"@storybook/test": "^8.1.7",
65+
"@storybook/test-runner": "^0.18.2",
66+
"@storybook/theming": "^8.1.7",
67+
"@storybook/types": "^8.1.7",
6868
"@testing-library/jest-dom": "^5.16.5",
6969
"@testing-library/react": "^13.4.0",
7070
"@types/jest": "^26.0.19",
@@ -85,14 +85,15 @@
8585
"jest": "^29.4.3",
8686
"jest-environment-jsdom": "^29.4.3",
8787
"jest-github-actions-reporter": "^1.0.2",
88+
"mock-socket": "^9.3.1",
8889
"npm-cli-login": "^0.1.1",
8990
"playwright": "^1.16.3",
9091
"pre-push": "^0.1.4",
9192
"react": "^18.3.1",
9293
"react-dom": "^18.3.1",
9394
"react-intersection-observer": "^9.4.2",
9495
"react-test-renderer": "^18.2.0",
95-
"storybook": "^8.0.9",
96+
"storybook": "^8.1.7",
9697
"storybook-addon-mock": "^5.0.0",
9798
"ts-jest": "^29.1.2",
9899
"ts-node": "^9.1.1",

src/hooks/useFetch/useFetch.stories.tsx

+8-11
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,21 @@
11
import { ReqoreSpinner, ReqoreTree } from '@qoretechnologies/reqore';
22
import { StoryObj } from '@storybook/react';
3-
import { useEffectOnce } from 'react-use';
43
import { testsWaitForText } from '../../../__tests__/utils';
54
import { StoryMeta } from '../../types';
65
import { useFetch } from './useFetch';
76

87
const meta = {
98
title: 'Hooks/useFetch',
109
render: (args) => {
11-
const { data = {}, load, loading } = useFetch<any>({ url: 'public/info', method: args.method });
10+
const {
11+
data = {},
12+
load,
13+
loading,
14+
} = useFetch<any>({ url: 'public/info', method: args.method, loadOnMount: true });
1215

13-
useEffectOnce(() => {
14-
load();
15-
});
16-
17-
return loading ? (
18-
<ReqoreSpinner />
19-
) : (
20-
<ReqoreTree data={data} bottomActions={[{ label: 'Refetch', onClick: load }]} />
21-
);
16+
return loading ?
17+
<ReqoreSpinner />
18+
: <ReqoreTree data={data} bottomActions={[{ label: 'Refetch', onClick: load }]} />;
2219
},
2320
} as StoryMeta<any>;
2421

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { useState } from 'react';
2+
import { useEffectOnce, useUnmount } from 'react-use';
3+
import { IReqraftWebSocketConfig, ReqraftWebSocket } from '../../utils/websocket';
4+
5+
export interface IUseReqraftWebSocketOptions extends IReqraftWebSocketConfig {
6+
onMessage?: (ev: MessageEvent) => void;
7+
useState?: boolean;
8+
includeSentMessagesInState?: boolean;
9+
includeLogMessagesInState?: boolean;
10+
openOnMount?: boolean;
11+
closeOnUnmount?: boolean;
12+
}
13+
14+
export interface IUseReqraftWebSocket {
15+
messages: string[];
16+
status: keyof typeof ReqraftWebSocketStatus;
17+
open: () => void;
18+
close: () => void;
19+
socket: ReqraftWebSocket;
20+
send: (data: string) => void;
21+
clear: () => void;
22+
on: (type: keyof WebSocketEventMap, handler: (ev: Event) => void) => void;
23+
addMessage: (message: string) => void;
24+
}
25+
26+
export enum ReqraftWebSocketStatus {
27+
OPEN = 'OPEN',
28+
CLOSED = 'CLOSED',
29+
CONNECTING = 'CONNECTING',
30+
}
31+
32+
export const useReqraftWebSocket = (options: IUseReqraftWebSocketOptions): IUseReqraftWebSocket => {
33+
const [messages, setMessages] = useState<string[]>([]);
34+
const [status, setStatus] = useState<keyof typeof ReqraftWebSocketStatus>('CLOSED');
35+
const [socket, setSocket] = useState<ReqraftWebSocket>(undefined);
36+
37+
const updateStates = (status: keyof typeof ReqraftWebSocketStatus, log?: string) => {
38+
setStatus(status);
39+
40+
if (log && options?.includeLogMessagesInState && options?.useState) {
41+
setMessages((prev) => [...prev, log]);
42+
}
43+
};
44+
45+
const handleOpen = (ev?: Event) => {
46+
updateStates(ReqraftWebSocketStatus.OPEN, 'Connection opened');
47+
48+
options?.onOpen?.(ev);
49+
};
50+
51+
const open = () => {
52+
const socket = new ReqraftWebSocket({
53+
...options,
54+
onOpen: handleOpen,
55+
onMessage: (ev) => {
56+
if (options?.useState) {
57+
setMessages((prev) => [...prev, ev.data]);
58+
}
59+
60+
options?.onMessage?.(ev);
61+
},
62+
onClose: (...args) => {
63+
updateStates(ReqraftWebSocketStatus.CLOSED, 'Connection closed');
64+
65+
options?.onClose?.(...args);
66+
},
67+
onError: (...args) => {
68+
updateStates(ReqraftWebSocketStatus.CLOSED, 'Connection error');
69+
70+
options?.onError?.(...args);
71+
},
72+
onReconnecting: (reconnectNumber) => {
73+
updateStates(
74+
ReqraftWebSocketStatus.CONNECTING,
75+
`Reconnecting... Attempt ${reconnectNumber}`
76+
);
77+
78+
options?.onReconnecting?.(reconnectNumber);
79+
},
80+
onReconnectFailed: () => {
81+
updateStates(ReqraftWebSocketStatus.CLOSED, 'Reconnect failed');
82+
83+
options?.onReconnectFailed?.();
84+
},
85+
});
86+
87+
setSocket(socket);
88+
};
89+
90+
const close = () => {
91+
socket?.remove();
92+
setSocket(undefined);
93+
setStatus(ReqraftWebSocketStatus.CLOSED);
94+
};
95+
96+
const send = (data: string) => {
97+
socket?.send(data);
98+
99+
if (options?.includeSentMessagesInState) {
100+
setMessages((prev) => [...prev, data]);
101+
}
102+
};
103+
104+
const on = (type: keyof WebSocketEventMap, handler: (ev: Event) => void) => {
105+
// Special case for message event
106+
// We want to handle it differently
107+
// We want to filter out the ping messages
108+
if (type === 'message') {
109+
socket?.addHandler('message', (ev) => {
110+
if ((<MessageEvent>ev).data === 'pong') {
111+
return;
112+
}
113+
114+
handler(ev);
115+
});
116+
117+
return;
118+
}
119+
120+
socket?.addHandler(type, handler);
121+
};
122+
123+
const clear = () => {
124+
setMessages([]);
125+
};
126+
127+
const addMessage = (message: string) => {
128+
if (options?.useState) {
129+
setMessages((prev) => [...prev, message]);
130+
}
131+
};
132+
133+
useEffectOnce(() => {
134+
if (options?.openOnMount) {
135+
open();
136+
}
137+
});
138+
139+
useUnmount(() => {
140+
if (options?.closeOnUnmount) {
141+
close();
142+
}
143+
});
144+
145+
return { messages, status, open, socket, close, send, clear, on, addMessage };
146+
};

0 commit comments

Comments
 (0)