@clary-so/measure 0.4.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.
Files changed (99) hide show
  1. package/.postcssrc +5 -0
  2. package/CHANGELOG.md +81 -0
  3. package/LICENSE +21 -0
  4. package/dist/__tests__/measure.test.d.ts +1 -0
  5. package/dist/__tests__/measure.test.js +152 -0
  6. package/dist/__tests__/measure.test.js.map +1 -0
  7. package/dist/__tests__/server/ServerApp.test.d.ts +1 -0
  8. package/dist/__tests__/server/ServerApp.test.js +49 -0
  9. package/dist/__tests__/server/ServerApp.test.js.map +1 -0
  10. package/dist/__tests__/utils/removeCLIColors.d.ts +1 -0
  11. package/dist/__tests__/utils/removeCLIColors.js +10 -0
  12. package/dist/__tests__/utils/removeCLIColors.js.map +1 -0
  13. package/dist/__tests__/webapp/socket.test.d.ts +1 -0
  14. package/dist/__tests__/webapp/socket.test.js +73 -0
  15. package/dist/__tests__/webapp/socket.test.js.map +1 -0
  16. package/dist/common/useLogSocketEvents.d.ts +3 -0
  17. package/dist/common/useLogSocketEvents.js +18 -0
  18. package/dist/common/useLogSocketEvents.js.map +1 -0
  19. package/dist/index.87c99d25.js +88 -0
  20. package/dist/index.87c99d25.js.map +1 -0
  21. package/dist/index.html +1 -0
  22. package/dist/server/ServerApp.d.ts +10 -0
  23. package/dist/server/ServerApp.js +131 -0
  24. package/dist/server/ServerApp.js.map +1 -0
  25. package/dist/server/ServerSocketConnectionApp.d.ts +6 -0
  26. package/dist/server/ServerSocketConnectionApp.js +105 -0
  27. package/dist/server/ServerSocketConnectionApp.js.map +1 -0
  28. package/dist/server/bin.d.ts +2 -0
  29. package/dist/server/bin.js +63 -0
  30. package/dist/server/bin.js.map +1 -0
  31. package/dist/server/components/HostAndPortInfo.d.ts +4 -0
  32. package/dist/server/components/HostAndPortInfo.js +14 -0
  33. package/dist/server/components/HostAndPortInfo.js.map +1 -0
  34. package/dist/server/constants.d.ts +2 -0
  35. package/dist/server/constants.js +12 -0
  36. package/dist/server/constants.js.map +1 -0
  37. package/dist/server/socket/socketInterface.d.ts +37 -0
  38. package/dist/server/socket/socketInterface.js +17 -0
  39. package/dist/server/socket/socketInterface.js.map +1 -0
  40. package/dist/server/socket/socketState.d.ts +5 -0
  41. package/dist/server/socket/socketState.js +47 -0
  42. package/dist/server/socket/socketState.js.map +1 -0
  43. package/dist/server/useBundleIdControls.d.ts +2 -0
  44. package/dist/server/useBundleIdControls.js +33 -0
  45. package/dist/server/useBundleIdControls.js.map +1 -0
  46. package/dist/webapp/MeasureWebApp.d.ts +2 -0
  47. package/dist/webapp/MeasureWebApp.js +29 -0
  48. package/dist/webapp/MeasureWebApp.js.map +1 -0
  49. package/dist/webapp/components/AppBar.d.ts +4 -0
  50. package/dist/webapp/components/AppBar.js +19 -0
  51. package/dist/webapp/components/AppBar.js.map +1 -0
  52. package/dist/webapp/components/BundleIdSelector.d.ts +6 -0
  53. package/dist/webapp/components/BundleIdSelector.js +20 -0
  54. package/dist/webapp/components/BundleIdSelector.js.map +1 -0
  55. package/dist/webapp/components/SocketState.d.ts +2 -0
  56. package/dist/webapp/components/SocketState.js +95 -0
  57. package/dist/webapp/components/SocketState.js.map +1 -0
  58. package/dist/webapp/components/StartButton.d.ts +6 -0
  59. package/dist/webapp/components/StartButton.js +12 -0
  60. package/dist/webapp/components/StartButton.js.map +1 -0
  61. package/dist/webapp/components/TextField.d.ts +5 -0
  62. package/dist/webapp/components/TextField.js +81 -0
  63. package/dist/webapp/components/TextField.js.map +1 -0
  64. package/dist/webapp/socket.d.ts +3 -0
  65. package/dist/webapp/socket.js +8 -0
  66. package/dist/webapp/socket.js.map +1 -0
  67. package/dist/webapp/useMeasures.d.ts +10 -0
  68. package/dist/webapp/useMeasures.js +38 -0
  69. package/dist/webapp/useMeasures.js.map +1 -0
  70. package/package.json +48 -0
  71. package/src/__tests__/__snapshots__/measure.test.tsx.snap +4389 -0
  72. package/src/__tests__/measure.test.tsx +141 -0
  73. package/src/__tests__/server/ServerApp.test.ts +49 -0
  74. package/src/__tests__/utils/removeCLIColors.ts +5 -0
  75. package/src/__tests__/webapp/socket.test.ts +37 -0
  76. package/src/common/types/index.d.ts +3 -0
  77. package/src/common/useLogSocketEvents.ts +17 -0
  78. package/src/server/ServerApp.tsx +103 -0
  79. package/src/server/ServerSocketConnectionApp.tsx +82 -0
  80. package/src/server/bin.tsx +23 -0
  81. package/src/server/components/HostAndPortInfo.tsx +11 -0
  82. package/src/server/constants.ts +8 -0
  83. package/src/server/socket/socketInterface.ts +53 -0
  84. package/src/server/socket/socketState.ts +66 -0
  85. package/src/server/useBundleIdControls.ts +38 -0
  86. package/src/webapp/MeasureWebApp.tsx +43 -0
  87. package/src/webapp/components/AppBar.tsx +19 -0
  88. package/src/webapp/components/BundleIdSelector.tsx +26 -0
  89. package/src/webapp/components/SocketState.tsx +79 -0
  90. package/src/webapp/components/StartButton.tsx +22 -0
  91. package/src/webapp/components/TextField.tsx +54 -0
  92. package/src/webapp/globals.d.ts +9 -0
  93. package/src/webapp/index.html +30 -0
  94. package/src/webapp/index.js +9 -0
  95. package/src/webapp/socket.ts +12 -0
  96. package/src/webapp/useMeasures.ts +36 -0
  97. package/tailwind.config.js +7 -0
  98. package/tsconfig.json +8 -0
  99. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,141 @@
1
+ import "@clary-so/e2e/src/utils/test/mockChildProcess";
2
+ import {
3
+ emitMeasures,
4
+ perfProfilerMock,
5
+ aTraceMock,
6
+ } from "@clary-so/e2e/src/utils/test/mockEmitMeasures";
7
+ import { fireEvent, render as webRender, screen, waitFor, act } from "@testing-library/react";
8
+ import { render as cliRender } from "ink-testing-library";
9
+ import React from "react";
10
+ import { ServerApp } from "../server/ServerApp";
11
+ import { open } from "@clary-so/shell";
12
+ import { matchSnapshot } from "@clary-so/web-reporter-ui/utils/testUtils";
13
+ import { removeCLIColors } from "./utils/removeCLIColors";
14
+ import { LogLevel, Logger } from "@clary-so/logger";
15
+ import { DEFAULT_PORT } from "../server/constants";
16
+
17
+ jest.mock("@clary-so/shell", () => ({
18
+ open: jest.fn(),
19
+ }));
20
+
21
+ Math.random = () => 0.5;
22
+
23
+ // Set me to LogLevel.DEBUG to see the debug logs
24
+ Logger.setLogLevel(LogLevel.SILENT);
25
+
26
+ let originalWindow: Window & typeof globalThis;
27
+ let MeasureWebApp: React.FC;
28
+
29
+ describe("flashlight measure interactive", () => {
30
+ beforeAll(async () => {
31
+ originalWindow = global.window;
32
+
33
+ global.window = Object.create(window);
34
+ Object.defineProperty(window, "__FLASHLIGHT_DATA__", {
35
+ value: { socketServerUrl: `http://localhost:${DEFAULT_PORT}` },
36
+ writable: true,
37
+ });
38
+
39
+ MeasureWebApp = (await import("../webapp/MeasureWebApp")).MeasureWebApp;
40
+ });
41
+
42
+ afterAll(() => {
43
+ global.window = originalWindow;
44
+ });
45
+
46
+ const expectWebAppToBeOpened = () =>
47
+ waitFor(() => expect(open).toHaveBeenCalledWith(`http://localhost:${DEFAULT_PORT}`));
48
+
49
+ const setupCli = (customPort = DEFAULT_PORT) => {
50
+ const { lastFrame, unmount } = cliRender(<ServerApp port={customPort} />);
51
+ const closeCli = async () => {
52
+ unmount();
53
+ // Seems like we need to wait for the useEffect cleanup to happen
54
+ await new Promise((resolve) => setTimeout(resolve, 0));
55
+ };
56
+
57
+ return {
58
+ closeCli,
59
+ expectCliOutput: () => expect(removeCLIColors(lastFrame())),
60
+ };
61
+ };
62
+
63
+ const setupWebApp = () => {
64
+ const view = webRender(<MeasureWebApp />);
65
+
66
+ return {
67
+ closeWebApp: view.unmount,
68
+ expectWebAppToMatchSnapshot: (snapshotName: string) => matchSnapshot(view, snapshotName),
69
+ };
70
+ };
71
+
72
+ test("it displays measures", async () => {
73
+ const { closeCli, expectCliOutput } = setupCli();
74
+ const { closeWebApp, expectWebAppToMatchSnapshot } = setupWebApp();
75
+ await expectWebAppToBeOpened();
76
+
77
+ expectCliOutput().toMatchInlineSnapshot(`
78
+ "
79
+ Flashlight web app running on: http://localhost:${DEFAULT_PORT}
80
+ "
81
+ `);
82
+
83
+ // Autodetect app id com.example
84
+ await screen.findByText("Auto-Detect");
85
+ fireEvent.click(screen.getByText("Auto-Detect"));
86
+ await screen.findByDisplayValue("com.example");
87
+
88
+ // Start measuring
89
+ fireEvent.click(screen.getByText("Start Measuring"));
90
+
91
+ // Initial report screen with no measures
92
+ await screen.findByText("Average Test Runtime");
93
+ expectWebAppToMatchSnapshot("Web app with no measures yet");
94
+
95
+ // Simulate measures being emitted on the device
96
+ act(() => emitMeasures());
97
+
98
+ // We should now see 1000ms of measures: 3 measures at 0/500/1000ms
99
+ await screen.findByText("1000 ms");
100
+ // Find the score!
101
+ screen.getByText("47");
102
+
103
+ // expand threads
104
+ await screen.findByText("Other threads");
105
+ fireEvent.click(screen.getByText("Other threads"));
106
+
107
+ expectWebAppToMatchSnapshot("Web app with measures and threads opened");
108
+
109
+ // Stop measuring
110
+ fireEvent.click(screen.getByText("Stop Measuring"));
111
+ await waitFor(() => expect(aTraceMock.kill).toHaveBeenCalled());
112
+ await waitFor(() => expect(perfProfilerMock.kill).toHaveBeenCalled());
113
+
114
+ // Close apps
115
+
116
+ await closeCli();
117
+ closeWebApp();
118
+ });
119
+
120
+ test("it handles the --port flag correctly", async () => {
121
+ const customPort = 1001;
122
+
123
+ const { closeCli, expectCliOutput } = setupCli(customPort);
124
+
125
+ const { closeWebApp } = setupWebApp();
126
+
127
+ const expectWebAppToBeOpenedOnCustomPort = () =>
128
+ waitFor(() => expect(open).toHaveBeenCalledWith(`http://localhost:${customPort}`));
129
+ await expectWebAppToBeOpenedOnCustomPort();
130
+
131
+ expectCliOutput().toMatchInlineSnapshot(`
132
+ "
133
+ Flashlight web app running on: http://localhost:${customPort}
134
+ "
135
+ `);
136
+
137
+ // Close apps
138
+ await closeCli();
139
+ closeWebApp();
140
+ });
141
+ });
@@ -0,0 +1,49 @@
1
+ import supertest from "supertest";
2
+ import express from "express";
3
+ import fs from "fs";
4
+
5
+ import { createExpressApp } from "../../server/ServerApp";
6
+
7
+ jest.mock("fs", () => ({
8
+ promises: {
9
+ readFile: jest.fn(),
10
+ },
11
+ }));
12
+
13
+ describe("ServerApp", () => {
14
+ let app: express.Express;
15
+
16
+ beforeAll(() => {
17
+ jest.spyOn(express, "static").mockImplementation(() => (req, res, next) => next());
18
+ });
19
+
20
+ const FLASHLIGHT_DATA_PLACEHOLDER =
21
+ 'window.__FLASHLIGHT_DATA__ = { socketServerUrl: "http://localhost:4000" };';
22
+
23
+ beforeEach(() => {
24
+ (fs.promises.readFile as jest.Mock).mockResolvedValue(
25
+ `<html><script>${FLASHLIGHT_DATA_PLACEHOLDER}</script></html>`
26
+ );
27
+
28
+ app = createExpressApp({
29
+ port: 9999,
30
+ });
31
+ });
32
+
33
+ describe("GET /", () => {
34
+ it("injects FlashlightData into index.html", async () => {
35
+ const response = await supertest(app).get("/");
36
+
37
+ expect(response.statusCode).toBe(200);
38
+ expect(response.text).toContain(
39
+ `window.__FLASHLIGHT_DATA__ = { socketServerUrl: "http://localhost:9999" };`
40
+ );
41
+ });
42
+ });
43
+
44
+ test("index.html contains the FlashlightData placeholder", async () => {
45
+ const fsPromises = jest.requireActual("fs").promises;
46
+ const fileContent = await fsPromises.readFile(`${__dirname}/../../webapp/index.html`, "utf8");
47
+ expect(fileContent).toContain(FLASHLIGHT_DATA_PLACEHOLDER);
48
+ });
49
+ });
@@ -0,0 +1,5 @@
1
+ // from https://stackoverflow.com/questions/17998978/removing-colors-from-output
2
+ // Remove colors so that snapshots are not polluted
3
+ export const removeCLIColors = (str?: string) =>
4
+ // eslint-disable-next-line no-control-regex
5
+ str?.replace(/\x1B\[([0-9]{1,3}(;[0-9]{1,2};?)?)?[mGK]/g, "");
@@ -0,0 +1,37 @@
1
+ import { io } from "socket.io-client";
2
+
3
+ jest.mock("socket.io-client", () => {
4
+ return {
5
+ ...jest.requireActual("socket.io-client"),
6
+ io: jest.fn().mockImplementation(() => {
7
+ return {
8
+ on: jest.fn(),
9
+ close: jest.fn(),
10
+ };
11
+ }),
12
+ };
13
+ });
14
+
15
+ let originalWindow: Window & typeof globalThis;
16
+
17
+ describe("socket", () => {
18
+ beforeAll(async () => {
19
+ originalWindow = global.window;
20
+
21
+ global.window = Object.create(window);
22
+ Object.defineProperty(window, "__FLASHLIGHT_DATA__", {
23
+ value: { socketServerUrl: "http://localhost:9999" },
24
+ writable: true,
25
+ });
26
+ });
27
+
28
+ afterAll(() => {
29
+ // Restore the original window object
30
+ global.window = originalWindow;
31
+ });
32
+
33
+ it("sets the expected socket server URL", async () => {
34
+ await import("../../webapp/socket");
35
+ expect(io).toHaveBeenCalledWith("http://localhost:9999");
36
+ });
37
+ });
@@ -0,0 +1,3 @@
1
+ export interface FlashlightData {
2
+ socketServerUrl: string;
3
+ }
@@ -0,0 +1,17 @@
1
+ import { Logger } from "@clary-so/logger";
2
+ import { useEffect } from "react";
3
+ import type { Socket } from "socket.io";
4
+ import type { Socket as ClientSocket } from "socket.io-client";
5
+
6
+ export const useLogSocketEvents = (socket: Socket | ClientSocket) => {
7
+ useEffect(() => {
8
+ function onSocketEvent(event: string, ...args: unknown[]) {
9
+ Logger.debug(`Received socket event: ${event} with ${JSON.stringify(args)}`);
10
+ }
11
+ socket.onAny(onSocketEvent);
12
+
13
+ return () => {
14
+ socket.offAny(onSocketEvent);
15
+ };
16
+ }, [socket]);
17
+ };
@@ -0,0 +1,103 @@
1
+ import express from "express";
2
+ import http from "http";
3
+ import { promises as fs } from "fs";
4
+ import path from "path";
5
+ import cors from "cors";
6
+ import { Server } from "socket.io";
7
+ import { open } from "@clary-so/shell";
8
+ import React, { useEffect, useState } from "react";
9
+ import { SocketType, SocketServer } from "./socket/socketInterface";
10
+ import { HostAndPortInfo } from "./components/HostAndPortInfo";
11
+ import { getWebAppUrl } from "./constants";
12
+ import { ServerSocketConnectionApp } from "./ServerSocketConnectionApp";
13
+ import { render, useInput } from "ink";
14
+ import { profiler } from "@clary-so/profiler";
15
+
16
+ const pathToDist = path.join(__dirname, "../../dist");
17
+
18
+ export const createExpressApp = ({ port }: { port: number }) => {
19
+ const app = express();
20
+ app.use(cors({ origin: true }));
21
+
22
+ app.get("/", async (_, res) => {
23
+ try {
24
+ const indexHtml = path.join(pathToDist, "index.html");
25
+ let data = await fs.readFile(indexHtml, "utf8");
26
+ data = data.replace("localhost:4000", `localhost:${port}`);
27
+
28
+ res.send(data);
29
+ } catch (err) {
30
+ res.status(500).send("Error loading the page");
31
+ }
32
+ });
33
+
34
+ // Serve the webapp folder built by parcel
35
+ app.use(express.static(pathToDist));
36
+ return app;
37
+ };
38
+
39
+ const allowOnlyOneSocketClient = (io: SocketServer, onConnect: (socket: SocketType) => void) => {
40
+ let currentSocketClient: SocketType | null = null;
41
+ io.on("connection", (socket) => {
42
+ currentSocketClient?.disconnect(true);
43
+ onConnect(socket);
44
+ currentSocketClient = socket;
45
+ });
46
+ };
47
+
48
+ const useCleanupOnManualExit = () => {
49
+ useInput(async (input) => {
50
+ switch (input) {
51
+ case "q":
52
+ case "c":
53
+ profiler.cleanup();
54
+ process.exit();
55
+ }
56
+ });
57
+ };
58
+
59
+ interface ServerAppProps {
60
+ port: number;
61
+ }
62
+
63
+ export const ServerApp = ({ port }: ServerAppProps) => {
64
+ const [socket, setSocket] = useState<SocketType | null>(null);
65
+ const webAppUrl = getWebAppUrl(port);
66
+ useEffect(() => {
67
+ const app = createExpressApp({ port });
68
+
69
+ const server = http.createServer(app);
70
+ const io: SocketServer = new Server(server, {
71
+ cors: {
72
+ origin: [webAppUrl],
73
+ methods: ["GET", "POST"],
74
+ },
75
+ });
76
+
77
+ allowOnlyOneSocketClient(io, setSocket);
78
+
79
+ server.listen(port, () => {
80
+ open(webAppUrl);
81
+ });
82
+
83
+ return () => {
84
+ server.close();
85
+ io.close();
86
+ };
87
+ }, [port, webAppUrl]);
88
+ useCleanupOnManualExit();
89
+
90
+ return socket ? (
91
+ <ServerSocketConnectionApp socket={socket} url={webAppUrl} />
92
+ ) : (
93
+ <HostAndPortInfo url={webAppUrl} />
94
+ );
95
+ };
96
+
97
+ export const runServerApp = (port: number) => {
98
+ render(
99
+ <ServerApp port={port} />,
100
+ // handle it ourselves in the profiler to kill child processes thanks to useCleanupOnManualExit
101
+ { exitOnCtrlC: false }
102
+ );
103
+ };
@@ -0,0 +1,82 @@
1
+ import { PerformanceMeasurer } from "@clary-so/e2e";
2
+ import { Logger } from "@clary-so/logger";
3
+ import { profiler } from "@clary-so/profiler";
4
+ import { Measure } from "@clary-so/types";
5
+ import React, { useCallback, useEffect } from "react";
6
+ import { HostAndPortInfo } from "./components/HostAndPortInfo";
7
+ import { SocketType, SocketEvents } from "./socket/socketInterface";
8
+ import { useSocketState, updateMeasuresReducer, addNewResultReducer } from "./socket/socketState";
9
+ import { useBundleIdControls } from "./useBundleIdControls";
10
+ import { useLogSocketEvents } from "../common/useLogSocketEvents";
11
+
12
+ export const ServerSocketConnectionApp = ({ socket, url }: { socket: SocketType; url: string }) => {
13
+ useLogSocketEvents(socket);
14
+ const [state, setState] = useSocketState(socket);
15
+ const performanceMeasureRef = React.useRef<PerformanceMeasurer | null>(null);
16
+
17
+ const stop = useCallback(async () => {
18
+ performanceMeasureRef.current?.forceStop();
19
+ setState({
20
+ isMeasuring: false,
21
+ });
22
+ }, [setState]);
23
+
24
+ useBundleIdControls(socket, setState, stop);
25
+
26
+ useEffect(() => {
27
+ const updateMeasures = (measures: Measure[]) =>
28
+ setState((state) => updateMeasuresReducer(state, measures));
29
+ const addNewResult = (bundleId: string) =>
30
+ setState((state) =>
31
+ addNewResultReducer(
32
+ state,
33
+ `${bundleId}${state.results.length > 0 ? ` (${state.results.length + 1})` : ""}`,
34
+ profiler.detectDeviceRefreshRate()
35
+ )
36
+ );
37
+
38
+ socket.on(SocketEvents.START, async () => {
39
+ setState({
40
+ isMeasuring: true,
41
+ });
42
+
43
+ if (!state.bundleId) {
44
+ Logger.error("No bundle id provided");
45
+ return;
46
+ }
47
+
48
+ profiler.installProfilerOnDevice();
49
+ performanceMeasureRef.current = new PerformanceMeasurer(state.bundleId, {
50
+ recordOptions: {
51
+ record: false,
52
+ },
53
+ });
54
+
55
+ addNewResult(state.bundleId);
56
+ performanceMeasureRef.current?.start(() =>
57
+ updateMeasures(performanceMeasureRef.current?.measures || [])
58
+ );
59
+ });
60
+
61
+ socket.on(SocketEvents.STOP, stop);
62
+
63
+ socket.on(SocketEvents.RESET, () => {
64
+ stop();
65
+ setState({
66
+ results: [],
67
+ });
68
+ });
69
+
70
+ return () => {
71
+ socket.removeAllListeners(SocketEvents.START);
72
+ socket.removeAllListeners(SocketEvents.STOP);
73
+ socket.removeAllListeners(SocketEvents.RESET);
74
+ };
75
+ }, [setState, socket, state.bundleId, stop]);
76
+
77
+ return (
78
+ <>
79
+ <HostAndPortInfo url={url} />
80
+ </>
81
+ );
82
+ };
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from "commander";
4
+ import { DEFAULT_PORT } from "./constants";
5
+
6
+ program
7
+ .command("measure")
8
+ .summary("Measure performance of an Android app")
9
+ .description(
10
+ `Measure performance of an Android app. Display the results live in a web app.
11
+
12
+ Main usage:
13
+ flashlight measure`
14
+ )
15
+ .option("-p, --port [port]", "Specify the port number for the server")
16
+ .action(async (options) => {
17
+ const port = Number(options.port) || DEFAULT_PORT;
18
+ // measure command can be a bit slow to load since we run ink, express and socket.io, so lazy load it
19
+ const { runServerApp } = await import("./ServerApp");
20
+ runServerApp(port);
21
+ });
22
+
23
+ program.parse();
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ export const HostAndPortInfo = ({ url }: { url: string }) => (
5
+ <Box padding={1} flexDirection="column">
6
+ <Text>
7
+ <Text bold>Flashlight web app running on: </Text>
8
+ <Text color={"blue"}>{url}</Text>
9
+ </Text>
10
+ </Box>
11
+ );
@@ -0,0 +1,8 @@
1
+ export const DEFAULT_PORT = 4000;
2
+
3
+ export const getWebAppUrl = (port: number = DEFAULT_PORT) => {
4
+ if (process.env.DEVELOPMENT_MODE === "true") {
5
+ return "http://localhost:1234";
6
+ }
7
+ return `http://localhost:${port}`;
8
+ };
@@ -0,0 +1,53 @@
1
+ import { TestCaseResult } from "@clary-so/types";
2
+ import { Server, Socket } from "socket.io";
3
+
4
+ export interface SocketData {
5
+ isMeasuring: boolean;
6
+ bundleId: string | null;
7
+ results: TestCaseResult[];
8
+ }
9
+
10
+ export interface ServerToClientEvents {
11
+ updateState: (state: SocketData) => void;
12
+ sendError(error: unknown): void;
13
+ }
14
+
15
+ export interface ClientToServerEvents {
16
+ start: () => void;
17
+ stop: () => void;
18
+ reset: () => void;
19
+ autodetectBundleId: () => void;
20
+ setBundleId: (bundleId: string) => void;
21
+ autodetectRefreshRate: () => void;
22
+ }
23
+
24
+ interface InterServerEvents {
25
+ ping: () => void;
26
+ }
27
+
28
+ export type SocketServer = Server<
29
+ ClientToServerEvents,
30
+ ServerToClientEvents,
31
+ InterServerEvents,
32
+ SocketData
33
+ >;
34
+
35
+ export type SocketType = Socket<
36
+ ClientToServerEvents,
37
+ ServerToClientEvents,
38
+ InterServerEvents,
39
+ SocketData
40
+ >;
41
+
42
+ export enum SocketEvents {
43
+ START = "start",
44
+ STOP = "stop",
45
+ RESET = "reset",
46
+ AUTODETECT_BUNDLE_ID = "autodetectBundleId",
47
+ SET_BUNDLE_ID = "setBundleId",
48
+ UPDATE_STATE = "updateState",
49
+ SEND_ERROR = "sendError",
50
+ PING = "ping",
51
+ CONNECT = "connect",
52
+ DISCONNECT = "disconnect",
53
+ }
@@ -0,0 +1,66 @@
1
+ import { Measure, POLLING_INTERVAL } from "@clary-so/types";
2
+ import { useState, useEffect } from "react";
3
+ import { SocketType, SocketData, SocketEvents } from "./socketInterface";
4
+
5
+ export const useSocketState = (socket: SocketType) => {
6
+ const [state, _setState] = useState<SocketData>({
7
+ isMeasuring: false,
8
+ bundleId: null,
9
+ results: [],
10
+ });
11
+
12
+ const setState = (
13
+ newState: Partial<SocketData> | ((previousState: SocketData) => SocketData)
14
+ ) => {
15
+ _setState(
16
+ typeof newState === "function"
17
+ ? newState
18
+ : (previousState) => ({
19
+ ...previousState,
20
+ ...newState,
21
+ })
22
+ );
23
+ };
24
+
25
+ useEffect(() => {
26
+ socket.emit(SocketEvents.UPDATE_STATE, state);
27
+ }, [state, socket]);
28
+
29
+ return [state, setState] as const;
30
+ };
31
+
32
+ export const updateMeasuresReducer = (state: SocketData, measures: Measure[]): SocketData => ({
33
+ ...state,
34
+ results: [
35
+ ...state.results.slice(0, state.results.length - 1),
36
+ {
37
+ ...state.results[state.results.length - 1],
38
+ iterations: [
39
+ {
40
+ measures,
41
+ time: (measures.length || 0) * POLLING_INTERVAL,
42
+ status: "SUCCESS",
43
+ },
44
+ ],
45
+ },
46
+ ],
47
+ });
48
+
49
+ export const addNewResultReducer = (
50
+ state: SocketData,
51
+ name: string,
52
+ refreshRate: number
53
+ ): SocketData => ({
54
+ ...state,
55
+ results: [
56
+ ...state.results,
57
+ {
58
+ name,
59
+ iterations: [],
60
+ status: "SUCCESS",
61
+ specs: {
62
+ refreshRate,
63
+ },
64
+ },
65
+ ],
66
+ });
@@ -0,0 +1,38 @@
1
+ import { profiler } from "@clary-so/profiler";
2
+ import { useEffect } from "react";
3
+ import { SocketType, SocketData, SocketEvents } from "./socket/socketInterface";
4
+
5
+ export const useBundleIdControls = (
6
+ socket: SocketType,
7
+ setState: (state: Partial<SocketData>) => void,
8
+ stop: () => void
9
+ ) => {
10
+ useEffect(() => {
11
+ socket.on(SocketEvents.SET_BUNDLE_ID, (bundleId) => {
12
+ setState({
13
+ bundleId,
14
+ });
15
+ });
16
+
17
+ socket.on(SocketEvents.AUTODETECT_BUNDLE_ID, () => {
18
+ stop();
19
+
20
+ try {
21
+ const bundleId = profiler.detectCurrentBundleId();
22
+ setState({
23
+ bundleId,
24
+ });
25
+ } catch (error) {
26
+ socket.emit(
27
+ SocketEvents.SEND_ERROR,
28
+ error instanceof Error ? error.message : "unknown error"
29
+ );
30
+ }
31
+ });
32
+
33
+ return () => {
34
+ socket.removeAllListeners(SocketEvents.SET_BUNDLE_ID);
35
+ socket.removeAllListeners(SocketEvents.AUTODETECT_BUNDLE_ID);
36
+ };
37
+ }, [setState, socket, stop]);
38
+ };
@@ -0,0 +1,43 @@
1
+ import React from "react";
2
+ import {
3
+ Button,
4
+ setThemeAtRandom,
5
+ IterationsReporterView,
6
+ getThemeColorPalette,
7
+ } from "@clary-so/web-reporter-ui";
8
+
9
+ import { BundleIdSelector } from "./components/BundleIdSelector";
10
+ import { StartButton } from "./components/StartButton";
11
+ import { Delete } from "@mui/icons-material";
12
+ import { AppBar } from "./components/AppBar";
13
+ import { useMeasures } from "./useMeasures";
14
+ import { SocketState } from "./components/SocketState";
15
+
16
+ setThemeAtRandom();
17
+
18
+ export const MeasureWebApp = () => {
19
+ const { autodetect, bundleId, start, stop, results, isMeasuring, reset, setBundleId } =
20
+ useMeasures();
21
+
22
+ return (
23
+ <div className="bg-light-charcoal h-full text-black">
24
+ <SocketState />
25
+ <AppBar>
26
+ <BundleIdSelector autodetect={autodetect} bundleId={bundleId} onChange={setBundleId} />
27
+ {bundleId ? (
28
+ <div className="flex flex-row gap-2">
29
+ <StartButton start={start} stop={stop} isMeasuring={isMeasuring} />
30
+ {/* It's assumed that the color palette is fixed randomly by setThemeAtRandom
31
+ and is an array of >= 4 colors */}
32
+ <div data-theme={getThemeColorPalette()[1]}>
33
+ <Button onClick={reset} icon={<Delete />}>
34
+ Reset
35
+ </Button>
36
+ </div>
37
+ </div>
38
+ ) : null}
39
+ </AppBar>
40
+ <IterationsReporterView results={results} />
41
+ </div>
42
+ );
43
+ };