apify-test-tools 0.1.0 → 0.2.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/CHANGELOG.md +18 -0
- package/CONTRIBUTING.md +53 -0
- package/README.md +247 -17
- package/bin/git.ts +24 -5
- package/bin/main.ts +11 -1
- package/bin/types.ts +1 -0
- package/bin/utils.ts +15 -8
- package/dist/bin/git.d.ts +2 -0
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +22 -5
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/main.js +9 -1
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/types.d.ts +1 -0
- package/dist/bin/types.d.ts.map +1 -1
- package/dist/bin/utils.d.ts +3 -0
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +13 -8
- package/dist/bin/utils.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/lib.d.ts +23 -1
- package/dist/lib/lib.d.ts.map +1 -1
- package/dist/lib/lib.js +110 -4
- package/dist/lib/lib.js.map +1 -1
- package/dist/lib/run-test-result.d.ts.map +1 -1
- package/dist/lib/run-test-result.js +2 -1
- package/dist/lib/run-test-result.js.map +1 -1
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +3 -0
- package/dist/lib/utils.js.map +1 -1
- package/dist/test/unit/custom-matchers.test.js +23 -1
- package/dist/test/unit/custom-matchers.test.js.map +1 -1
- package/dist/test/unit/parse-commit.test.d.ts +2 -0
- package/dist/test/unit/parse-commit.test.d.ts.map +1 -0
- package/dist/test/unit/parse-commit.test.js +57 -0
- package/dist/test/unit/parse-commit.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.ts +1 -1
- package/lib/lib.ts +137 -5
- package/lib/run-test-result.ts +2 -1
- package/lib/utils.ts +6 -1
- package/package.json +1 -1
- package/test/unit/custom-matchers.test.ts +25 -1
- package/test/unit/parse-commit.test.ts +73 -0
package/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { describe, testActor, ExpectStatic } from './lib/lib.js';
|
|
1
|
+
export { describe, testActor, testStandbyActor, ExpectStatic } from './lib/lib.js';
|
|
2
2
|
export { RunTestResult } from './lib/run-test-result.js';
|
package/lib/lib.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApifyClient } from 'apify-client';
|
|
1
|
+
import { Actor, ActorRun, ActorRunListItem, ActorStandby, ApifyClient, Task } from 'apify-client';
|
|
2
2
|
import {
|
|
3
3
|
describe as vitestDescribe,
|
|
4
4
|
ExpectStatic,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import type { ActorBuild, RunOptions } from './types';
|
|
12
12
|
import { RunTestResult } from './run-test-result.js';
|
|
13
13
|
import { extendExpect } from './extend-expect.js';
|
|
14
|
-
import { getActorPrefilledInput } from './utils.js';
|
|
14
|
+
import { getActorPrefilledInput, sleep } from './utils.js';
|
|
15
15
|
|
|
16
16
|
const ACTOR_BUILDS = 'ACTOR_BUILDS';
|
|
17
17
|
let actorBuilds: ActorBuild[] = [];
|
|
@@ -80,6 +80,53 @@ export const testActor = <T>(
|
|
|
80
80
|
});
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* This wrapper creates a new task with specific `build` of the standby actor and provides
|
|
85
|
+
* `callStandby` function, which calls the task's `standbyUrl`.
|
|
86
|
+
*
|
|
87
|
+
* Using task is just current shortcoming of standby feature but ideally we would use Actor directly
|
|
88
|
+
*/
|
|
89
|
+
export const testStandbyActor = <I = any, O = any>(
|
|
90
|
+
actorName: string,
|
|
91
|
+
testName: string,
|
|
92
|
+
fn: TestFunction<{ callStandby: ReturnType<typeof createStartStandbyFn<I, O>> }>,
|
|
93
|
+
testOptions?: TestOptions,
|
|
94
|
+
) => {
|
|
95
|
+
const options = {
|
|
96
|
+
...DEFAULT_TEST_ACTOR_OPTIONS,
|
|
97
|
+
...testOptions,
|
|
98
|
+
};
|
|
99
|
+
const name = `${actorName}: ${testName}`;
|
|
100
|
+
const shouldRun = !!RUN_ALL_PLATFORM_TESTS || config.has(actorName);
|
|
101
|
+
|
|
102
|
+
vitestTest.runIf(shouldRun)(name, options, async (context) => {
|
|
103
|
+
const standbyTask = await createStandbyTask(actorName, config.get(actorName)?.buildNumber);
|
|
104
|
+
const { annotate } = context;
|
|
105
|
+
const { expect, ...rest } = context;
|
|
106
|
+
|
|
107
|
+
// NOTE: we need to wrap `fn` in try-catch so that we can always clean up (delete the task) afterwards
|
|
108
|
+
try {
|
|
109
|
+
await fn({
|
|
110
|
+
expect: extendExpect(expect),
|
|
111
|
+
callStandby: createStartStandbyFn(standbyTask),
|
|
112
|
+
...rest,
|
|
113
|
+
});
|
|
114
|
+
} catch {}
|
|
115
|
+
|
|
116
|
+
const { taskId } = standbyTask;
|
|
117
|
+
const runs = (await apifyClient.task(taskId).runs().list()).items
|
|
118
|
+
for (const run of runs) {
|
|
119
|
+
const runLink = generateRunLink(run);
|
|
120
|
+
await annotate(runLink, 'run_link');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (taskId) {
|
|
124
|
+
await apifyClient.task(taskId).delete()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
83
130
|
export const testTestActor = <T>(
|
|
84
131
|
testName: string,
|
|
85
132
|
fn: TestFunction<{ run: ReturnType<typeof createStartRunFn<T>> }>,
|
|
@@ -97,7 +144,88 @@ export const testTestActor = <T>(
|
|
|
97
144
|
|
|
98
145
|
export const it = testActor;
|
|
99
146
|
|
|
100
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Creates a function the accepts input for a standby actor and sends request containing input
|
|
149
|
+
* to the task's standby url.
|
|
150
|
+
*/
|
|
151
|
+
const createStartStandbyFn = <I, O>(standbyTask: StandbyTask) => {
|
|
152
|
+
const { standbyUrl } = standbyTask;
|
|
153
|
+
return async ({ input }: Pick<RunOptions<I>, 'input'>) => {
|
|
154
|
+
const response = await fetch(standbyUrl, {
|
|
155
|
+
headers: {
|
|
156
|
+
Authorization: `Bearer ${apifyClient.token}`,
|
|
157
|
+
},
|
|
158
|
+
method: 'POST',
|
|
159
|
+
body: JSON.stringify(input),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const data = await response.json() as O;
|
|
163
|
+
return {
|
|
164
|
+
data,
|
|
165
|
+
status: response.status,
|
|
166
|
+
headers: response.headers,
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface StandbyTask {
|
|
172
|
+
standbyUrl: string;
|
|
173
|
+
taskId: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Creates a task with specific `build` - either `buildNumber` or default.
|
|
178
|
+
*
|
|
179
|
+
* @throws if actor doesn't exist or it doesn't support standby mode.
|
|
180
|
+
*/
|
|
181
|
+
const createStandbyTask = async (actorNameOrId: string, buildNumber?: string): Promise<StandbyTask> => {
|
|
182
|
+
const actor = apifyClient.actor(actorNameOrId);
|
|
183
|
+
|
|
184
|
+
const actorInfo = (await actor.get()) as Actor & { standbyUrl?: string };
|
|
185
|
+
if (!actorInfo) {
|
|
186
|
+
throw new Error(`Actor "${actorNameOrId}" not found`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!actorInfo.standbyUrl) {
|
|
190
|
+
throw new Error(`Actor "${actorNameOrId}" doesn't support standby mode`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!actorInfo.actorStandby) {
|
|
194
|
+
throw new Error(`Actor "${actorNameOrId} doesn't contain actorStandby options`);
|
|
195
|
+
}
|
|
196
|
+
const { isEnabled, ...defaultActorStandby } = actorInfo.actorStandby;
|
|
197
|
+
const build = buildNumber ?? defaultActorStandby.build;
|
|
198
|
+
|
|
199
|
+
const actorStandbyOptions: ActorStandby = {
|
|
200
|
+
...defaultActorStandby,
|
|
201
|
+
build,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const newTask = await apifyClient.tasks().create({
|
|
206
|
+
actId: actorNameOrId,
|
|
207
|
+
actorStandby: actorStandbyOptions,
|
|
208
|
+
description: `Task for testing standby version ${build}`,
|
|
209
|
+
title: `Test task - ${build}`,
|
|
210
|
+
}) as Task & { standbyUrl?: string };
|
|
211
|
+
|
|
212
|
+
const { id, standbyUrl } = newTask;
|
|
213
|
+
|
|
214
|
+
if (!standbyUrl) {
|
|
215
|
+
throw new Error(`Task "${id} doesn't contain standbyUrl property`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
standbyUrl,
|
|
220
|
+
taskId: id,
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
throw new Error(`Failed to create task: ${error}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const createStartRunFn = <T>(actorNameOrId: string, testContext: TestContext) => {
|
|
228
|
+
const { annotate, task } = testContext;
|
|
101
229
|
const actorConfig = config.get(actorNameOrId);
|
|
102
230
|
const build = actorConfig?.buildNumber;
|
|
103
231
|
const buildId = actorConfig?.buildId;
|
|
@@ -118,7 +246,7 @@ const createStartRunFn = <T>(actorNameOrId: string, { annotate, task }: TestCont
|
|
|
118
246
|
{ build, ...options, },
|
|
119
247
|
);
|
|
120
248
|
|
|
121
|
-
const runLink =
|
|
249
|
+
const runLink = generateRunLink(run);
|
|
122
250
|
await annotate(runLink, 'run_link');
|
|
123
251
|
// @ts-expect-error: `TaskMeta` cannot be retyped
|
|
124
252
|
task.meta = {
|
|
@@ -127,8 +255,12 @@ const createStartRunFn = <T>(actorNameOrId: string, { annotate, task }: TestCont
|
|
|
127
255
|
};
|
|
128
256
|
|
|
129
257
|
// waiting for datasetItemCount and chargedEventCounts to sync
|
|
130
|
-
await
|
|
258
|
+
await sleep(10_000);
|
|
131
259
|
|
|
132
260
|
return new RunTestResult(apifyClient, run);
|
|
133
261
|
};
|
|
134
262
|
};
|
|
263
|
+
|
|
264
|
+
const generateRunLink = (run: ActorRun | ActorRunListItem): string => {
|
|
265
|
+
return `https://console.apify.com/view/runs/${run.id}`;
|
|
266
|
+
}
|
package/lib/run-test-result.ts
CHANGED
|
@@ -48,7 +48,8 @@ export class RunTestResult {
|
|
|
48
48
|
}
|
|
49
49
|
const kvs = this.apifyClient.keyValueStore(this.run.defaultKeyValueStoreId);
|
|
50
50
|
const input = await kvs.getRecord('INPUT');
|
|
51
|
-
|
|
51
|
+
this.input = input?.value;
|
|
52
|
+
return this.input as T;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
getRunInfo = async (): Promise<ActorRun> => {
|
package/lib/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ApifyClient } from 'apify-client';
|
|
1
|
+
import { ActorRun, ApifyClient } from 'apify-client';
|
|
2
|
+
import { TestContext } from 'vitest';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Gets prefilled values for a provided build or, if not provided, uses actor's
|
|
@@ -47,3 +48,7 @@ export const getActorPrefilledInput = async (
|
|
|
47
48
|
|
|
48
49
|
return prefill;
|
|
49
50
|
};
|
|
51
|
+
|
|
52
|
+
export const sleep = async (ms: number) => {
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import process from 'process';
|
|
|
2
2
|
import { describe } from 'vitest';
|
|
3
3
|
|
|
4
4
|
import { ApifyClient } from 'apify-client';
|
|
5
|
-
import { testTestActor } from '../../lib/lib';
|
|
5
|
+
import { testStandbyActor, testTestActor } from '../../lib/lib';
|
|
6
6
|
import { RunTestResult } from '../../lib/run-test-result';
|
|
7
7
|
|
|
8
8
|
type PpeEventType =
|
|
@@ -58,4 +58,28 @@ describe('custom-matchers', { timeout: 100_000 }, () => {
|
|
|
58
58
|
expect.hard(5).toEqual(5);
|
|
59
59
|
expect(10).toEqual(10);
|
|
60
60
|
});
|
|
61
|
+
|
|
62
|
+
testStandbyActor('ondrejklinovsky/contact-info', 'CDS standby', async ({ expect, callStandby }) => {
|
|
63
|
+
{
|
|
64
|
+
const { data, status } = await callStandby({
|
|
65
|
+
input: {
|
|
66
|
+
startUrls: [{ url: 'https://apify.com' }],
|
|
67
|
+
maxRequests: 3,
|
|
68
|
+
aggregateContacts: true,
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
expect(status).toBe(200);
|
|
72
|
+
expect(data[0].domain).toEqual('apify.com');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { data, status } = await callStandby({
|
|
76
|
+
input: {
|
|
77
|
+
startUrls: [{ url: 'https://apify.com' }],
|
|
78
|
+
maxRequests: 3,
|
|
79
|
+
aggregateContacts: true,
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
expect(status).toBe(200);
|
|
83
|
+
expect(data[0].domain).toEqual('apify.com');
|
|
84
|
+
});
|
|
61
85
|
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { GIT_FORMAT_SEPARATOR, parseCommit } from '../../bin/git';
|
|
3
|
+
|
|
4
|
+
describe('parseCommit', () => {
|
|
5
|
+
|
|
6
|
+
describe('valid commit strings', () => {
|
|
7
|
+
it('should parse a basic commit string correctly', () => {
|
|
8
|
+
const commitString = `abc123${GIT_FORMAT_SEPARATOR}John Doe<john@example.com>${GIT_FORMAT_SEPARATOR}Mon, 25 Dec 2023 10:30:00 +0100${GIT_FORMAT_SEPARATOR}Add new feature`;
|
|
9
|
+
|
|
10
|
+
const result = parseCommit(commitString);
|
|
11
|
+
|
|
12
|
+
expect(result).toEqual({
|
|
13
|
+
sha: 'abc123',
|
|
14
|
+
author: 'John Doe<john@example.com>',
|
|
15
|
+
date: 'Mon, 25 Dec 2023 10:30:00 +0100',
|
|
16
|
+
message: 'Add new feature'
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should parse commit with double quotes in message', () => {
|
|
21
|
+
const commitString = `def456${GIT_FORMAT_SEPARATOR}Jane Smith<jane@example.com>${GIT_FORMAT_SEPARATOR}Tue, 26 Dec 2023 14:15:00 +0100${GIT_FORMAT_SEPARATOR}test: commit with double quotes: "`;
|
|
22
|
+
|
|
23
|
+
const result = parseCommit(commitString);
|
|
24
|
+
|
|
25
|
+
expect(result).toEqual({
|
|
26
|
+
sha: 'def456',
|
|
27
|
+
author: 'Jane Smith<jane@example.com>',
|
|
28
|
+
date: 'Tue, 26 Dec 2023 14:15:00 +0100',
|
|
29
|
+
message: 'test: commit with double quotes: \"'
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should parse commit with special characters and Unicode', () => {
|
|
34
|
+
const commitString = `jkl012${GIT_FORMAT_SEPARATOR}María García<maria@example.com>${GIT_FORMAT_SEPARATOR}Thu, 28 Dec 2023 16:20:00 +0100${GIT_FORMAT_SEPARATOR}Add émojis 🚀 and "special" chars`;
|
|
35
|
+
|
|
36
|
+
const result = parseCommit(commitString);
|
|
37
|
+
|
|
38
|
+
expect(result).toEqual({
|
|
39
|
+
sha: 'jkl012',
|
|
40
|
+
author: 'María García<maria@example.com>',
|
|
41
|
+
date: 'Thu, 28 Dec 2023 16:20:00 +0100',
|
|
42
|
+
message: 'Add émojis 🚀 and "special" chars'
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should parse commit with empty message', () => {
|
|
47
|
+
const commitString = `mno345${GIT_FORMAT_SEPARATOR}Test User<test@example.com>${GIT_FORMAT_SEPARATOR}Fri, 29 Dec 2023 11:00:00 +0100${GIT_FORMAT_SEPARATOR}`;
|
|
48
|
+
|
|
49
|
+
const result = parseCommit(commitString);
|
|
50
|
+
|
|
51
|
+
expect(result).toEqual({
|
|
52
|
+
sha: 'mno345',
|
|
53
|
+
author: 'Test User<test@example.com>',
|
|
54
|
+
date: 'Fri, 29 Dec 2023 11:00:00 +0100',
|
|
55
|
+
message: ''
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('invalid commit strings', () => {
|
|
61
|
+
it('should throw error for commit string with too few parts', () => {
|
|
62
|
+
const commitString = `abc123${GIT_FORMAT_SEPARATOR}John Doe<john@example.com>${GIT_FORMAT_SEPARATOR}Mon, 25 Dec 2023 10:30:00 +0100`;
|
|
63
|
+
|
|
64
|
+
expect(() => parseCommit(commitString)).toThrow(`Failed to parse commit string: ${commitString}`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should throw error for commit string with too many parts', () => {
|
|
68
|
+
const commitString = `abc123${GIT_FORMAT_SEPARATOR}John Doe<john@example.com>${GIT_FORMAT_SEPARATOR}Mon, 25 Dec 2023 10:30:00 +0100${GIT_FORMAT_SEPARATOR}Add feature${GIT_FORMAT_SEPARATOR}extra part`;
|
|
69
|
+
|
|
70
|
+
expect(() => parseCommit(commitString)).toThrow(`Failed to parse commit string: ${commitString}`);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|