creevey 0.10.0-beta.42 → 0.10.0-beta.44
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/CHANGELOG.md +282 -0
- package/dist/client/addon/controller.js +1 -1
- package/dist/client/addon/controller.js.map +1 -1
- package/dist/client/addon/decorator.d.ts +1 -1
- package/dist/client/addon/makeDecorator.d.ts +9 -0
- package/dist/client/addon/makeDecorator.js +48 -0
- package/dist/client/addon/makeDecorator.js.map +1 -0
- package/dist/client/addon/preview.d.ts +1 -1
- package/dist/client/addon/withCreevey.d.ts +2 -1
- package/dist/client/addon/withCreevey.js +3 -20
- package/dist/client/addon/withCreevey.js.map +1 -1
- package/dist/client/shared/components/PageHeader/PageHeader.js +13 -4
- package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
- package/dist/client/shared/creeveyClientApi.js +10 -0
- package/dist/client/shared/creeveyClientApi.js.map +1 -1
- package/dist/client/web/CreeveyApp.d.ts +1 -0
- package/dist/client/web/CreeveyApp.js +1 -0
- package/dist/client/web/CreeveyApp.js.map +1 -1
- package/dist/client/web/CreeveyContext.d.ts +1 -0
- package/dist/client/web/CreeveyContext.js +1 -0
- package/dist/client/web/CreeveyContext.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +9 -8
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +13 -3
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +2 -3
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestLink.js +2 -3
- package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestsStatus.js +1 -0
- package/dist/client/web/CreeveyView/SideBar/TestsStatus.js.map +1 -1
- package/dist/client/web/assets/{index-C47njyZV.js → index-BU4jjKVC.js} +68 -68
- package/dist/client/web/index.html +1 -1
- package/dist/client/web/index.js +8 -3
- package/dist/client/web/index.js.map +1 -1
- package/dist/creevey.d.ts +1 -1
- package/dist/creevey.js +1 -22
- package/dist/creevey.js.map +1 -1
- package/dist/playwright-reporter.d.ts +2 -0
- package/dist/playwright-reporter.js +5 -0
- package/dist/playwright-reporter.js.map +1 -0
- package/dist/playwright.d.ts +1 -1
- package/dist/server/config.js +8 -1
- package/dist/server/config.js.map +1 -1
- package/dist/server/index.js +10 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/master/api.d.ts +15 -5
- package/dist/server/master/api.js +89 -27
- package/dist/server/master/api.js.map +1 -1
- package/dist/server/master/handlers/capture-handler.d.ts +2 -0
- package/dist/server/master/handlers/capture-handler.js +35 -0
- package/dist/server/master/handlers/capture-handler.js.map +1 -0
- package/dist/server/master/handlers/index.d.ts +4 -0
- package/dist/server/master/handlers/index.js +21 -0
- package/dist/server/master/handlers/index.js.map +1 -0
- package/dist/server/master/handlers/ping-handler.d.ts +2 -0
- package/dist/server/master/handlers/ping-handler.js +7 -0
- package/dist/server/master/handlers/ping-handler.js.map +1 -0
- package/dist/server/master/handlers/static-handler.d.ts +2 -0
- package/dist/server/master/handlers/static-handler.js +32 -0
- package/dist/server/master/handlers/static-handler.js.map +1 -0
- package/dist/server/master/handlers/stories-handler.d.ts +2 -0
- package/dist/server/master/handlers/stories-handler.js +38 -0
- package/dist/server/master/handlers/stories-handler.js.map +1 -0
- package/dist/server/master/master.js +7 -24
- package/dist/server/master/master.js.map +1 -1
- package/dist/server/master/runner.d.ts +4 -6
- package/dist/server/master/runner.js +30 -127
- package/dist/server/master/runner.js.map +1 -1
- package/dist/server/master/server.js +77 -87
- package/dist/server/master/server.js.map +1 -1
- package/dist/server/master/start.d.ts +1 -2
- package/dist/server/master/start.js +11 -29
- package/dist/server/master/start.js.map +1 -1
- package/dist/server/master/testsManager.d.ts +81 -0
- package/dist/server/master/testsManager.js +281 -0
- package/dist/server/master/testsManager.js.map +1 -0
- package/dist/server/playwright/reporter.d.ts +87 -0
- package/dist/server/playwright/reporter.js +351 -0
- package/dist/server/playwright/reporter.js.map +1 -0
- package/dist/server/selenium/internal.js +20 -2
- package/dist/server/selenium/internal.js.map +1 -1
- package/dist/server/selenium/selenoid.js +4 -0
- package/dist/server/selenium/selenoid.js.map +1 -1
- package/dist/server/shutdown.d.ts +1 -0
- package/dist/server/shutdown.js +23 -0
- package/dist/server/shutdown.js.map +1 -0
- package/dist/server/stories.d.ts +0 -1
- package/dist/server/stories.js +0 -12
- package/dist/server/stories.js.map +1 -1
- package/dist/server/ui-update.d.ts +10 -0
- package/dist/server/ui-update.js +39 -0
- package/dist/server/ui-update.js.map +1 -0
- package/dist/server/utils.d.ts +6 -0
- package/dist/server/utils.js +39 -8
- package/dist/server/utils.js.map +1 -1
- package/dist/server/worker/start.js +1 -1
- package/dist/server/worker/start.js.map +1 -1
- package/dist/types.d.ts +14 -8
- package/dist/types.js.map +1 -1
- package/docs/examples/playwright-reporter-example.ts +202 -0
- package/docs/migration-0.9-to-0.10.md +144 -0
- package/docs/playwright-reporter.md +357 -0
- package/package.json +9 -13
- package/src/client/addon/controller.ts +1 -1
- package/src/client/addon/makeDecorator.ts +69 -0
- package/src/client/addon/withCreevey.ts +4 -17
- package/src/client/shared/components/PageHeader/PageHeader.tsx +18 -4
- package/src/client/shared/creeveyClientApi.ts +10 -0
- package/src/client/web/CreeveyApp.tsx +2 -0
- package/src/client/web/CreeveyContext.tsx +2 -0
- package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +19 -17
- package/src/client/web/CreeveyView/SideBar/SideBarHeader.tsx +18 -3
- package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +9 -7
- package/src/client/web/CreeveyView/SideBar/TestLink.tsx +8 -6
- package/src/client/web/CreeveyView/SideBar/TestsStatus.tsx +1 -0
- package/src/client/web/index.tsx +8 -3
- package/src/creevey.ts +1 -24
- package/src/playwright-reporter.ts +3 -0
- package/src/server/config.ts +9 -1
- package/src/server/index.ts +11 -4
- package/src/server/master/api.ts +95 -26
- package/src/server/master/handlers/capture-handler.ts +39 -0
- package/src/server/master/handlers/index.ts +4 -0
- package/src/server/master/handlers/ping-handler.ts +5 -0
- package/src/server/master/handlers/static-handler.ts +29 -0
- package/src/server/master/handlers/stories-handler.ts +48 -0
- package/src/server/master/master.ts +10 -27
- package/src/server/master/runner.ts +38 -132
- package/src/server/master/server.ts +93 -97
- package/src/server/master/start.ts +17 -41
- package/src/server/master/testsManager.ts +315 -0
- package/src/server/playwright/reporter.ts +386 -0
- package/src/server/selenium/internal.ts +23 -3
- package/src/server/selenium/selenoid.ts +5 -0
- package/src/server/shutdown.ts +19 -0
- package/src/server/stories.ts +1 -12
- package/src/server/ui-update.ts +46 -0
- package/src/server/utils.ts +40 -9
- package/src/server/worker/start.ts +1 -1
- package/src/types.ts +14 -8
@@ -1,125 +1,121 @@
|
|
1
1
|
import path from 'path';
|
2
|
-
import
|
3
|
-
import cluster from 'cluster';
|
4
|
-
import Koa from 'koa';
|
5
|
-
import cors from '@koa/cors';
|
6
|
-
import serve from 'koa-static';
|
7
|
-
import mount from 'koa-mount';
|
8
|
-
import body from 'koa-bodyparser';
|
9
|
-
import WebSocket from 'ws';
|
2
|
+
import HyperExpress from 'hyper-express';
|
10
3
|
import { fileURLToPath, pathToFileURL } from 'url';
|
11
4
|
import { CreeveyApi } from './api.js';
|
12
|
-
import {
|
13
|
-
import {
|
5
|
+
import { subscribeOn } from '../messages.js';
|
6
|
+
import { noop } from '../../types.js';
|
14
7
|
import { logger } from '../logger.js';
|
15
|
-
import {
|
8
|
+
import { pingHandler, createStoriesHandler, captureHandler, createStaticFileHandler } from './handlers/index.js';
|
16
9
|
|
17
10
|
const importMetaUrl = pathToFileURL(__filename).href;
|
18
11
|
|
19
12
|
export function start(reportDir: string, port: number, ui: boolean): (api: CreeveyApi) => void {
|
20
13
|
let resolveApi: (api: CreeveyApi) => void = noop;
|
21
|
-
let setStoriesCounter = 0;
|
22
14
|
const creeveyApi = new Promise<CreeveyApi>((resolve) => (resolveApi = resolve));
|
23
|
-
const app = new Koa();
|
24
|
-
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
25
|
-
const server = http.createServer(app.callback());
|
26
|
-
const wss = new WebSocket.Server({ server });
|
27
|
-
|
28
|
-
app.use(cors());
|
29
|
-
app.use(body());
|
30
|
-
|
31
|
-
app.use(async (ctx, next) => {
|
32
|
-
if (ctx.method == 'GET' && ctx.path == '/ping') {
|
33
|
-
ctx.body = 'pong';
|
34
|
-
return;
|
35
|
-
}
|
36
|
-
await next();
|
37
|
-
});
|
38
15
|
|
39
|
-
|
40
|
-
|
41
|
-
await creeveyApi;
|
42
|
-
await next();
|
43
|
-
});
|
44
|
-
}
|
16
|
+
// Create HyperExpress server instance
|
17
|
+
const server = new HyperExpress.Server();
|
45
18
|
|
46
|
-
|
47
|
-
|
48
|
-
const { setStoriesCounter: counter, stories } = ctx.request.body as {
|
49
|
-
setStoriesCounter: number;
|
50
|
-
stories: [string, StoryInput[]][];
|
51
|
-
};
|
52
|
-
if (setStoriesCounter >= counter) return;
|
53
|
-
|
54
|
-
const deserializedStories = stories.map<[string, StoryInput[]]>(([file, stories]) => [
|
55
|
-
file,
|
56
|
-
stories.map(deserializeStory),
|
57
|
-
]);
|
58
|
-
|
59
|
-
setStoriesCounter = counter;
|
60
|
-
emitStoriesMessage({ type: 'update', payload: deserializedStories });
|
61
|
-
Object.values(cluster.workers ?? {})
|
62
|
-
.filter(isDefined)
|
63
|
-
.filter((worker) => worker.isConnected())
|
64
|
-
.forEach((worker) => {
|
65
|
-
sendStoriesMessage(worker, { type: 'update', payload: deserializedStories });
|
66
|
-
});
|
67
|
-
return;
|
68
|
-
}
|
69
|
-
await next();
|
70
|
-
});
|
19
|
+
// Store active WebSocket connections
|
20
|
+
const activeConnections = new Set<HyperExpress.Websocket>();
|
71
21
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
await new Promise<void>((resolve) => {
|
81
|
-
const unsubscribe = subscribeOnWorker(worker, 'stories', (message) => {
|
82
|
-
if (message.type != 'capture') return;
|
83
|
-
unsubscribe();
|
84
|
-
resolve();
|
85
|
-
});
|
86
|
-
sendStoriesMessage(worker, { type: 'capture', payload: options });
|
87
|
-
});
|
88
|
-
// TODO Pass screenshot result to show it in inspector
|
89
|
-
ctx.body = 'Ok';
|
90
|
-
return;
|
22
|
+
// Enable CORS for all routes
|
23
|
+
server.use((request, response, next) => {
|
24
|
+
response.header('Access-Control-Allow-Origin', '*');
|
25
|
+
response.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
26
|
+
response.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
27
|
+
|
28
|
+
if (request.method === 'OPTIONS') {
|
29
|
+
return response.status(200).send();
|
91
30
|
}
|
92
|
-
|
31
|
+
|
32
|
+
next();
|
93
33
|
});
|
94
34
|
|
95
|
-
|
96
|
-
|
35
|
+
// Health check endpoint
|
36
|
+
server.get('/ping', pingHandler);
|
97
37
|
|
98
|
-
|
99
|
-
|
100
|
-
});
|
38
|
+
// Stories endpoint
|
39
|
+
server.post('/stories', createStoriesHandler());
|
101
40
|
|
102
|
-
|
41
|
+
// Capture endpoint
|
42
|
+
server.post('/capture', captureHandler);
|
103
43
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
44
|
+
// Serve report files
|
45
|
+
server.get('/report/*', createStaticFileHandler(reportDir, '/report/'));
|
46
|
+
|
47
|
+
// Serve static files
|
48
|
+
const webDir = path.join(path.dirname(fileURLToPath(importMetaUrl)), '../../client/web');
|
49
|
+
server.get('/*', createStaticFileHandler(webDir));
|
50
|
+
|
51
|
+
// If UI mode, wait for CreeveyApi to be resolved
|
52
|
+
if (ui) {
|
53
|
+
// Create a custom broadcast function that works with our connections
|
54
|
+
const broadcast = (message: string) => {
|
55
|
+
for (const connection of activeConnections) {
|
56
|
+
connection.send(message);
|
57
|
+
}
|
58
|
+
};
|
59
|
+
|
60
|
+
// Create a custom WebSocket server that simulates the standard behavior
|
61
|
+
const customWsServer = {
|
62
|
+
clients: activeConnections,
|
63
|
+
publish: broadcast,
|
64
|
+
};
|
65
|
+
|
66
|
+
let api: CreeveyApi | null = null;
|
67
|
+
|
68
|
+
server.use(async (request, _response, next) => {
|
69
|
+
if (!api && request.path === '/') {
|
70
|
+
api = await creeveyApi;
|
71
|
+
api.subscribe(customWsServer);
|
72
|
+
}
|
73
|
+
next();
|
109
74
|
});
|
110
|
-
});
|
111
75
|
|
112
|
-
|
113
|
-
|
76
|
+
// Create WebSocket listener
|
77
|
+
server.ws('/', (ws) => {
|
78
|
+
// Add connection to the set of active connections
|
79
|
+
activeConnections.add(ws);
|
114
80
|
|
115
|
-
|
116
|
-
ws.on('message', (message:
|
117
|
-
|
118
|
-
|
119
|
-
|
81
|
+
// Handle message events
|
82
|
+
ws.on('message', (message: string | Buffer) => {
|
83
|
+
api?.handleMessage(ws, message);
|
84
|
+
});
|
85
|
+
|
86
|
+
// Handle close events to clean up connections
|
87
|
+
ws.on('close', () => {
|
88
|
+
activeConnections.delete(ws);
|
120
89
|
});
|
121
90
|
});
|
91
|
+
}
|
92
|
+
|
93
|
+
// Shutdown handling
|
94
|
+
subscribeOn('shutdown', () => {
|
95
|
+
// Close all WebSocket connections
|
96
|
+
for (const connection of activeConnections) {
|
97
|
+
try {
|
98
|
+
connection.close();
|
99
|
+
} catch (error) {
|
100
|
+
logger().error('Error closing WebSocket connection', error);
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
// Close the server
|
105
|
+
server.close();
|
122
106
|
});
|
123
107
|
|
108
|
+
// Start server
|
109
|
+
server
|
110
|
+
.listen(port)
|
111
|
+
.then(() => {
|
112
|
+
logger().info(`Server starting on port ${port}`);
|
113
|
+
})
|
114
|
+
.catch((error: unknown) => {
|
115
|
+
logger().error('Failed to start server', error);
|
116
|
+
process.exit(1);
|
117
|
+
});
|
118
|
+
|
119
|
+
// Return the function to resolve the API
|
124
120
|
return resolveApi;
|
125
121
|
}
|
@@ -1,41 +1,14 @@
|
|
1
1
|
import path from 'path';
|
2
2
|
import { existsSync } from 'fs';
|
3
|
-
import { fileURLToPath, pathToFileURL } from 'url';
|
4
|
-
import { copyFile, readdir, mkdir, writeFile } from 'fs/promises';
|
5
3
|
import master from './master.js';
|
6
|
-
import
|
7
|
-
import { Config, Options,
|
8
|
-
import { shutdownWorkers, testsToImages, readDirRecursive } from '../utils.js';
|
4
|
+
import { CreeveyApi } from './api.js';
|
5
|
+
import { Config, Options, isDefined } from '../../types.js';
|
6
|
+
import { shutdownWorkers, testsToImages, readDirRecursive, copyStatics } from '../utils.js';
|
9
7
|
import { subscribeOn } from '../messages.js';
|
10
8
|
import Runner from './runner.js';
|
11
9
|
import { logger } from '../logger.js';
|
12
10
|
import { sendScreenshotsCount } from '../telemetry.js';
|
13
|
-
|
14
|
-
const importMetaUrl = pathToFileURL(__filename).href;
|
15
|
-
|
16
|
-
async function copyStatics(reportDir: string): Promise<void> {
|
17
|
-
const clientDir = path.join(path.dirname(fileURLToPath(importMetaUrl)), '../../../dist/client/web');
|
18
|
-
const assets = (await readdir(path.join(clientDir, 'assets'), { withFileTypes: true }))
|
19
|
-
.filter((dirent) => dirent.isFile())
|
20
|
-
.map((dirent) => dirent.name);
|
21
|
-
await mkdir(path.join(reportDir, 'assets'), { recursive: true });
|
22
|
-
await copyFile(path.join(clientDir, 'index.html'), path.join(reportDir, 'index.html'));
|
23
|
-
for (const asset of assets) {
|
24
|
-
await copyFile(path.join(clientDir, 'assets', asset), path.join(reportDir, 'assets', asset));
|
25
|
-
}
|
26
|
-
}
|
27
|
-
|
28
|
-
function reportDataModule(data: Partial<Record<string, TestData>>): string {
|
29
|
-
return `
|
30
|
-
(function (root, factory) {
|
31
|
-
if (typeof module === 'object' && module.exports) {
|
32
|
-
module.exports = factory();
|
33
|
-
} else {
|
34
|
-
root.__CREEVEY_DATA__ = factory();
|
35
|
-
}
|
36
|
-
}(this, function () { return ${JSON.stringify(data)} }));
|
37
|
-
`;
|
38
|
-
}
|
11
|
+
import { start as startServer } from './server.js';
|
39
12
|
|
40
13
|
function outputUnnecessaryImages(imagesDir: string, images: Set<string>): void {
|
41
14
|
if (!existsSync(imagesDir)) return;
|
@@ -50,12 +23,9 @@ function outputUnnecessaryImages(imagesDir: string, images: Set<string>): void {
|
|
50
23
|
}
|
51
24
|
}
|
52
25
|
|
53
|
-
export async function start(
|
54
|
-
|
55
|
-
|
56
|
-
options: Options,
|
57
|
-
resolveApi: (api: CreeveyApi) => void,
|
58
|
-
): Promise<void> {
|
26
|
+
export async function start(gridUrl: string | undefined, config: Config, options: Options): Promise<void> {
|
27
|
+
const resolveApi = startServer(config.reportDir, options.port, options.ui);
|
28
|
+
|
59
29
|
let runner: Runner | null = null;
|
60
30
|
if (config.hooks.before) {
|
61
31
|
await config.hooks.before();
|
@@ -78,13 +48,19 @@ export async function start(
|
|
78
48
|
runner = await master(config, gridUrl);
|
79
49
|
|
80
50
|
runner.on('stop', () => {
|
81
|
-
void copyStatics(config.reportDir).then(() =>
|
82
|
-
writeFile(path.join(config.reportDir, 'data.js'), reportDataModule(runner.status.tests)),
|
83
|
-
);
|
51
|
+
void copyStatics(config.reportDir).then(() => runner.testsManager.saveTestData());
|
84
52
|
});
|
85
53
|
|
86
54
|
if (options.ui) {
|
87
|
-
|
55
|
+
// Initialize TestsManager
|
56
|
+
const testsManager = runner.testsManager;
|
57
|
+
|
58
|
+
// Create the CreeveyApi instance using the existing runner
|
59
|
+
const api = new CreeveyApi(testsManager, runner);
|
60
|
+
|
61
|
+
// Resolve the API for the server
|
62
|
+
resolveApi(api);
|
63
|
+
|
88
64
|
logger().info(`Started on http://localhost:${options.port}`);
|
89
65
|
} else {
|
90
66
|
if (Object.values(runner.status.tests).filter((test) => test && !test.skip).length == 0) {
|
@@ -0,0 +1,315 @@
|
|
1
|
+
import path from 'path';
|
2
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
3
|
+
import EventEmitter from 'events';
|
4
|
+
import {
|
5
|
+
ServerTest,
|
6
|
+
TestMeta,
|
7
|
+
TestResult,
|
8
|
+
TestStatus,
|
9
|
+
CreeveyUpdate,
|
10
|
+
ApprovePayload,
|
11
|
+
isDefined,
|
12
|
+
isFunction,
|
13
|
+
CreeveyStatus,
|
14
|
+
} from '../../types.js';
|
15
|
+
import { tryToLoadTestsData } from '../utils.js';
|
16
|
+
import { copyFile, mkdir, writeFile } from 'fs/promises';
|
17
|
+
|
18
|
+
/**
|
19
|
+
* TestsManager is responsible for all operations related to test data management
|
20
|
+
* including loading, saving, merging, and updating test data.
|
21
|
+
* It extends EventEmitter to emit update events that can be subscribed to.
|
22
|
+
*/
|
23
|
+
export class TestsManager extends EventEmitter {
|
24
|
+
private tests: Partial<Record<string, ServerTest>> = {};
|
25
|
+
private screenDir: string;
|
26
|
+
private reportDir: string;
|
27
|
+
|
28
|
+
/**
|
29
|
+
* Creates a new TestsManager instance
|
30
|
+
* @param screenDir Directory for storing reference images
|
31
|
+
* @param reportDir Directory for storing reports and screenshots
|
32
|
+
*/
|
33
|
+
constructor(screenDir: string, reportDir: string) {
|
34
|
+
super();
|
35
|
+
this.screenDir = screenDir;
|
36
|
+
this.reportDir = reportDir;
|
37
|
+
}
|
38
|
+
|
39
|
+
/**
|
40
|
+
* Get a copy of all tests
|
41
|
+
* @returns all tests
|
42
|
+
*/
|
43
|
+
public getTests(): Partial<Record<string, ServerTest>> {
|
44
|
+
return this.tests;
|
45
|
+
}
|
46
|
+
|
47
|
+
/**
|
48
|
+
* Get a test by ID
|
49
|
+
* @param id Test ID
|
50
|
+
* @returns Test data
|
51
|
+
*/
|
52
|
+
public getTest(id: string): ServerTest | undefined {
|
53
|
+
return this.tests[id];
|
54
|
+
}
|
55
|
+
|
56
|
+
/**
|
57
|
+
* Get test data in a format suitable for status reporting
|
58
|
+
* @returns Test data in the format needed for status
|
59
|
+
*/
|
60
|
+
public getTestsData(): CreeveyStatus['tests'] {
|
61
|
+
const testsData: CreeveyStatus['tests'] = {};
|
62
|
+
|
63
|
+
Object.entries(this.tests).forEach(([id, test]) => {
|
64
|
+
if (!test) return;
|
65
|
+
|
66
|
+
const { story: _, fn: __, ...testData } = test;
|
67
|
+
testsData[id] = testData;
|
68
|
+
});
|
69
|
+
|
70
|
+
return testsData;
|
71
|
+
}
|
72
|
+
|
73
|
+
/**
|
74
|
+
* Load tests from a report file
|
75
|
+
*/
|
76
|
+
public loadTestsFromReport(): Partial<Record<string, ServerTest>> {
|
77
|
+
const reportDataPath = path.join(this.reportDir, 'data.js');
|
78
|
+
const testsFromReport = tryToLoadTestsData(reportDataPath) ?? {};
|
79
|
+
return testsFromReport;
|
80
|
+
}
|
81
|
+
|
82
|
+
/**
|
83
|
+
* Merge tests from report with tests from stories
|
84
|
+
*/
|
85
|
+
private mergeTests(
|
86
|
+
testsWithReports: CreeveyStatus['tests'],
|
87
|
+
testsFromStories: Partial<Record<string, ServerTest>>,
|
88
|
+
): Partial<Record<string, ServerTest>> {
|
89
|
+
Object.values(testsFromStories)
|
90
|
+
.filter(isDefined)
|
91
|
+
.forEach((test) => {
|
92
|
+
const testWithReport = testsWithReports[test.id];
|
93
|
+
if (!testWithReport) return;
|
94
|
+
test.retries = testWithReport.retries;
|
95
|
+
if (testWithReport.status === 'success' || testWithReport.status === 'failed') {
|
96
|
+
test.status = testWithReport.status;
|
97
|
+
}
|
98
|
+
test.results = testWithReport.results;
|
99
|
+
test.approved = testWithReport.approved;
|
100
|
+
});
|
101
|
+
|
102
|
+
return testsFromStories;
|
103
|
+
}
|
104
|
+
|
105
|
+
public loadAndMergeTests(testsFromStories: Partial<Record<string, ServerTest>>): Partial<Record<string, ServerTest>> {
|
106
|
+
const testsFromReport = this.loadTestsFromReport();
|
107
|
+
|
108
|
+
return this.mergeTests(testsFromReport, testsFromStories);
|
109
|
+
}
|
110
|
+
|
111
|
+
/**
|
112
|
+
* Update tests with incremental changes
|
113
|
+
* @param testsDiff Tests to update or remove
|
114
|
+
*/
|
115
|
+
public updateTests(testsDiff: Partial<Record<string, ServerTest>>): CreeveyUpdate | null {
|
116
|
+
const tests: CreeveyUpdate['tests'] = {};
|
117
|
+
const removedTests: TestMeta[] = [];
|
118
|
+
|
119
|
+
Object.entries(testsDiff).forEach(([id, newTest]) => {
|
120
|
+
if (newTest) {
|
121
|
+
if (this.tests[id]) {
|
122
|
+
this.tests[id] = {
|
123
|
+
...newTest,
|
124
|
+
retries: this.tests[id].retries,
|
125
|
+
results: this.tests[id].results,
|
126
|
+
approved: this.tests[id].approved,
|
127
|
+
};
|
128
|
+
} else {
|
129
|
+
this.tests[id] = newTest;
|
130
|
+
}
|
131
|
+
|
132
|
+
const { story: _, fn: __, ...restTest } = newTest;
|
133
|
+
tests[id] = { ...restTest, status: 'unknown' };
|
134
|
+
} else if (this.tests[id]) {
|
135
|
+
const { id: testId, browser, testName, storyPath, storyId } = this.tests[id];
|
136
|
+
removedTests.push({ id: testId, browser, testName, storyPath, storyId });
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
138
|
+
delete this.tests[id];
|
139
|
+
}
|
140
|
+
});
|
141
|
+
|
142
|
+
this.saveTestsToJson();
|
143
|
+
|
144
|
+
const update = { tests, removedTests };
|
145
|
+
this.emit('update', update);
|
146
|
+
return update;
|
147
|
+
}
|
148
|
+
|
149
|
+
/**
|
150
|
+
* Update test result
|
151
|
+
* @param id Test ID
|
152
|
+
* @param status New test status
|
153
|
+
* @param result Optional test result
|
154
|
+
*/
|
155
|
+
public updateTestStatus(id: string, status: TestStatus, result?: TestResult): CreeveyUpdate | null {
|
156
|
+
// TODO Handle 'retrying' status
|
157
|
+
const test = this.tests[id];
|
158
|
+
if (!test) return null;
|
159
|
+
|
160
|
+
const { browser, testName, storyPath, storyId } = test;
|
161
|
+
test.status = status === 'retrying' ? 'failed' : status;
|
162
|
+
|
163
|
+
if (!result) {
|
164
|
+
// NOTE: Running status
|
165
|
+
const update = { tests: { [id]: { id, browser, testName, storyPath, status, storyId } } };
|
166
|
+
this.emit('update', update);
|
167
|
+
return update;
|
168
|
+
}
|
169
|
+
|
170
|
+
test.results ??= [];
|
171
|
+
test.results.push(result);
|
172
|
+
|
173
|
+
if (status === 'failed') {
|
174
|
+
test.approved = null;
|
175
|
+
}
|
176
|
+
|
177
|
+
const update = {
|
178
|
+
tests: {
|
179
|
+
[id]: {
|
180
|
+
id,
|
181
|
+
browser,
|
182
|
+
testName,
|
183
|
+
storyPath,
|
184
|
+
status,
|
185
|
+
approved: test.approved,
|
186
|
+
results: [result],
|
187
|
+
storyId,
|
188
|
+
},
|
189
|
+
},
|
190
|
+
};
|
191
|
+
|
192
|
+
this.emit('update', update);
|
193
|
+
return update;
|
194
|
+
}
|
195
|
+
|
196
|
+
/**
|
197
|
+
* Save tests to JSON file
|
198
|
+
* @param reportDir Directory to save the JSON file
|
199
|
+
*/
|
200
|
+
public saveTestsToJson(): void {
|
201
|
+
mkdirSync(this.reportDir, { recursive: true });
|
202
|
+
writeFileSync(
|
203
|
+
path.join(this.reportDir, 'tests.json'),
|
204
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
205
|
+
JSON.stringify(this.tests, (_, value) => (isFunction(value) ? value.toString() : value), 2),
|
206
|
+
);
|
207
|
+
}
|
208
|
+
|
209
|
+
/**
|
210
|
+
* Save test data to a module
|
211
|
+
* @param data Test data to include in the module
|
212
|
+
*/
|
213
|
+
public async saveTestData(data: CreeveyStatus['tests'] = this.getTestsData()): Promise<void> {
|
214
|
+
const dataModule = `
|
215
|
+
(function (root, factory) {
|
216
|
+
if (typeof module === 'object' && module.exports) {
|
217
|
+
module.exports = factory();
|
218
|
+
} else {
|
219
|
+
root.__CREEVEY_DATA__ = factory();
|
220
|
+
}
|
221
|
+
}(this, function () { return ${JSON.stringify(data)} }));
|
222
|
+
`;
|
223
|
+
await writeFile(path.join(this.reportDir, 'data.js'), dataModule);
|
224
|
+
}
|
225
|
+
|
226
|
+
/**
|
227
|
+
* Copy image for approval
|
228
|
+
* @param test Test data
|
229
|
+
* @param image Image name
|
230
|
+
* @param actual Actual image path
|
231
|
+
*/
|
232
|
+
private async copyImage(test: ServerTest, image: string, actual: string): Promise<void> {
|
233
|
+
const { browser, testName, storyPath } = test;
|
234
|
+
const restPath = [...storyPath, testName].filter(isDefined);
|
235
|
+
const testPath = path.join(...restPath, image == browser ? '' : browser);
|
236
|
+
const srcImagePath = path.join(this.reportDir, testPath, actual);
|
237
|
+
const dstImagePath = path.join(this.screenDir, testPath, `${image}.png`);
|
238
|
+
await mkdir(path.join(this.screenDir, testPath), { recursive: true });
|
239
|
+
await copyFile(srcImagePath, dstImagePath);
|
240
|
+
}
|
241
|
+
|
242
|
+
/**
|
243
|
+
* Approve a specific test
|
244
|
+
* @param payload Approval payload with test ID, retry index, and image name
|
245
|
+
*/
|
246
|
+
public async approve({ id, retry, image }: ApprovePayload): Promise<CreeveyUpdate | null> {
|
247
|
+
const test = this.tests[id];
|
248
|
+
if (!test?.results) return null;
|
249
|
+
const result = test.results[retry];
|
250
|
+
if (!result.images) return null;
|
251
|
+
const images = result.images[image];
|
252
|
+
if (!images) return null;
|
253
|
+
test.approved ??= {};
|
254
|
+
const { browser, testName, storyPath, storyId } = test;
|
255
|
+
|
256
|
+
await this.copyImage(test, image, images.actual);
|
257
|
+
|
258
|
+
test.approved[image] = retry;
|
259
|
+
|
260
|
+
if (Object.keys(result.images).every((name) => typeof test.approved?.[name] == 'number')) {
|
261
|
+
test.status = 'approved';
|
262
|
+
}
|
263
|
+
|
264
|
+
const update = {
|
265
|
+
tests: {
|
266
|
+
[id]: {
|
267
|
+
id,
|
268
|
+
browser,
|
269
|
+
testName,
|
270
|
+
storyPath,
|
271
|
+
status: test.status,
|
272
|
+
approved: test.approved,
|
273
|
+
storyId,
|
274
|
+
},
|
275
|
+
},
|
276
|
+
};
|
277
|
+
|
278
|
+
this.emit('update', update);
|
279
|
+
return update;
|
280
|
+
}
|
281
|
+
|
282
|
+
/**
|
283
|
+
* Approve all failed tests
|
284
|
+
*/
|
285
|
+
public async approveAll(): Promise<CreeveyUpdate> {
|
286
|
+
const updatedTests: NonNullable<CreeveyUpdate['tests']> = {};
|
287
|
+
for (const test of Object.values(this.tests)) {
|
288
|
+
if (!test?.results) continue;
|
289
|
+
const retry = test.results.length - 1;
|
290
|
+
const { images, status } = test.results.at(retry) ?? {};
|
291
|
+
if (!images || status != 'failed') continue;
|
292
|
+
for (const [name, image] of Object.entries(images)) {
|
293
|
+
if (!image) continue;
|
294
|
+
await this.copyImage(test, name, image.actual);
|
295
|
+
|
296
|
+
test.approved ??= {};
|
297
|
+
test.approved[name] = retry;
|
298
|
+
test.status = 'approved';
|
299
|
+
|
300
|
+
updatedTests[test.id] = {
|
301
|
+
id: test.id,
|
302
|
+
browser: test.browser,
|
303
|
+
storyPath: test.storyPath,
|
304
|
+
storyId: test.storyId,
|
305
|
+
status: test.status,
|
306
|
+
approved: { [name]: retry },
|
307
|
+
};
|
308
|
+
}
|
309
|
+
}
|
310
|
+
|
311
|
+
const result = { tests: updatedTests };
|
312
|
+
this.emit('update', result);
|
313
|
+
return result;
|
314
|
+
}
|
315
|
+
}
|