apify-test-tools 0.4.0 → 0.5.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/.prettierrc +5 -0
- package/.vscode/settings.json +3 -0
- package/CHANGELOG.md +14 -0
- package/bin/git.ts +11 -16
- package/bin/github.ts +5 -14
- package/bin/main.ts +38 -38
- package/bin/slack.ts +13 -16
- package/dist/bin/git.d.ts +2 -2
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +3 -6
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/github.d.ts +1 -1
- package/dist/bin/github.d.ts.map +1 -1
- package/dist/bin/github.js +3 -9
- package/dist/bin/github.js.map +1 -1
- package/dist/bin/main.js +5 -7
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/slack.d.ts +2 -2
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +4 -2
- package/dist/bin/slack.js.map +1 -1
- package/dist/lib/consts.d.ts +1 -1
- package/dist/lib/consts.d.ts.map +1 -1
- package/dist/lib/consts.js +2 -4
- package/dist/lib/consts.js.map +1 -1
- package/dist/lib/extend-expect.d.ts.map +1 -1
- package/dist/lib/extend-expect.js +11 -0
- package/dist/lib/extend-expect.js.map +1 -1
- package/dist/lib/lib.d.ts +1 -1
- package/dist/lib/lib.d.ts.map +1 -1
- package/dist/lib/lib.js +10 -7
- package/dist/lib/lib.js.map +1 -1
- package/dist/lib/run-test-result.d.ts +1 -1
- package/dist/lib/run-test-result.d.ts.map +1 -1
- package/dist/lib/run-test-result.js +1 -0
- package/dist/lib/run-test-result.js.map +1 -1
- package/dist/lib/types.d.ts +4 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/test/unit/custom-matchers.test.js +2 -2
- package/dist/test/unit/custom-matchers.test.js.map +1 -1
- package/dist/test/unit/parse-commit.test.js +5 -5
- package/dist/test/unit/parse-commit.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/lib/consts.ts +3 -5
- package/lib/extend-expect.ts +37 -22
- package/lib/lib.ts +29 -40
- package/lib/run-test-result.ts +9 -10
- package/lib/types.ts +70 -67
- package/package.json +4 -1
- package/test/unit/custom-matchers.test.ts +6 -9
- package/test/unit/parse-commit.test.ts +6 -7
- package/tsconfig.json +4 -8
- package/test/unit/test-actor.test.ts +0 -17
package/lib/consts.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ToFinishWithOptionsWithDefaults } from './types';
|
|
1
|
+
import type { ToFinishWithOptionsWithDefaults } from './types.js';
|
|
2
2
|
|
|
3
3
|
export const TO_FINISH_WITH_OPTIONS: ToFinishWithOptionsWithDefaults = {
|
|
4
4
|
status: 'SUCCEEDED',
|
|
@@ -8,8 +8,6 @@ export const TO_FINISH_WITH_OPTIONS: ToFinishWithOptionsWithDefaults = {
|
|
|
8
8
|
},
|
|
9
9
|
failedRequests: 0,
|
|
10
10
|
requestsRetries: { max: 3 },
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
'TypeError',
|
|
14
|
-
],
|
|
11
|
+
maxRetriesPerRequest: null,
|
|
12
|
+
forbiddenLogs: ['ReferenceError', 'TypeError'],
|
|
15
13
|
};
|
package/lib/extend-expect.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Assertion, ExpectStatic } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import { TO_FINISH_WITH_OPTIONS } from './consts.js';
|
|
4
|
-
import type { Interval, ToFinishWithOptions } from './types';
|
|
4
|
+
import type { Interval, ToFinishWithOptions } from './types.js';
|
|
5
5
|
import { RunTestResult } from './run-test-result.js';
|
|
6
6
|
|
|
7
7
|
export const extendExpect = (expect: ExpectStatic): ExpectStatic => {
|
|
@@ -110,7 +110,7 @@ export const extendExpect = (expect: ExpectStatic): ExpectStatic => {
|
|
|
110
110
|
},
|
|
111
111
|
toFinishWith: async <PpeEvent extends string>(
|
|
112
112
|
received: RunTestResult,
|
|
113
|
-
userOptions: ToFinishWithOptions<PpeEvent
|
|
113
|
+
userOptions: ToFinishWithOptions<PpeEvent>
|
|
114
114
|
) => {
|
|
115
115
|
const options = {
|
|
116
116
|
...TO_FINISH_WITH_OPTIONS,
|
|
@@ -146,11 +146,13 @@ export const extendExpect = (expect: ExpectStatic): ExpectStatic => {
|
|
|
146
146
|
const checkInterval = (
|
|
147
147
|
value: number | undefined,
|
|
148
148
|
key: ['datasetItemCount', 'duration', 'failedRequests', 'requestsRetries'][number],
|
|
149
|
-
label: string
|
|
149
|
+
label: string
|
|
150
150
|
) => {
|
|
151
151
|
const result = isWithinInterval(diffs, value, options, key);
|
|
152
152
|
if (result === false) {
|
|
153
|
-
failedAssertions.push(
|
|
153
|
+
failedAssertions.push(
|
|
154
|
+
`Failed ${label} check, expected ${JSON.stringify(options[key])}, got ${value}.`
|
|
155
|
+
);
|
|
154
156
|
}
|
|
155
157
|
};
|
|
156
158
|
|
|
@@ -159,6 +161,19 @@ export const extendExpect = (expect: ExpectStatic): ExpectStatic => {
|
|
|
159
161
|
checkInterval(stats?.requestsFailed, 'failedRequests', 'failed requests');
|
|
160
162
|
checkInterval(stats?.requestsRetries, 'requestsRetries', 'requests retries');
|
|
161
163
|
|
|
164
|
+
{
|
|
165
|
+
if (options.maxRetriesPerRequest !== null) {
|
|
166
|
+
const maxRetriesPerRequestObserved = (stats?.requestRetryHistogram ?? [0]).length - 1;
|
|
167
|
+
if (maxRetriesPerRequestObserved > options.maxRetriesPerRequest) {
|
|
168
|
+
diffs.pass = false;
|
|
169
|
+
diffs.actual.push(`maxRetriesPerRequest=${maxRetriesPerRequestObserved}`);
|
|
170
|
+
diffs.expected.push(`maxRetriesPerRequest<=${options.maxRetriesPerRequest}`);
|
|
171
|
+
failedAssertions.push(
|
|
172
|
+
`Failed max retries observed check, expected <=${options.maxRetriesPerRequest}, got ${maxRetriesPerRequestObserved}.`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
162
177
|
const ppeDiffs: Diffs = {
|
|
163
178
|
pass: true,
|
|
164
179
|
actual: ['PPE Events:'],
|
|
@@ -175,12 +190,7 @@ export const extendExpect = (expect: ExpectStatic): ExpectStatic => {
|
|
|
175
190
|
|
|
176
191
|
for (const ppeEvent of uniquePpeEvents) {
|
|
177
192
|
if (!(ppeEvent in expected)) {
|
|
178
|
-
isWithinInterval(
|
|
179
|
-
ppeDiffs,
|
|
180
|
-
actual[ppeEvent],
|
|
181
|
-
{ [ppeEvent]: 0 },
|
|
182
|
-
ppeEvent,
|
|
183
|
-
);
|
|
193
|
+
isWithinInterval(ppeDiffs, actual[ppeEvent], { [ppeEvent]: 0 }, ppeEvent);
|
|
184
194
|
continue;
|
|
185
195
|
}
|
|
186
196
|
if (!(ppeEvent in actual)) {
|
|
@@ -196,7 +206,11 @@ export const extendExpect = (expect: ExpectStatic): ExpectStatic => {
|
|
|
196
206
|
diffs.pass = ppeDiffs.pass;
|
|
197
207
|
diffs.actual.push(ppeDiffs.actual.join('\n '));
|
|
198
208
|
diffs.expected.push(ppeDiffs.expected.join('\n '));
|
|
199
|
-
failedAssertions.push(
|
|
209
|
+
failedAssertions.push(
|
|
210
|
+
`Failed PPE event counts check, expected ${JSON.stringify(
|
|
211
|
+
options.chargedEventCounts
|
|
212
|
+
)}, got ${JSON.stringify(chargedEventCounts)}.`
|
|
213
|
+
);
|
|
200
214
|
}
|
|
201
215
|
}
|
|
202
216
|
|
|
@@ -213,13 +227,15 @@ export const extendExpect = (expect: ExpectStatic): ExpectStatic => {
|
|
|
213
227
|
diffs.pass = false;
|
|
214
228
|
diffs.actual.push(` logs=[${occuredLogs.join(', ')}]`);
|
|
215
229
|
diffs.expected.push(` logs=[]`);
|
|
216
|
-
failedAssertions.push(
|
|
230
|
+
failedAssertions.push(
|
|
231
|
+
`Failed forbidden logs check, expected [] but got [${occuredLogs.join(', ')}].`
|
|
232
|
+
);
|
|
217
233
|
}
|
|
218
234
|
}
|
|
219
235
|
|
|
220
236
|
return {
|
|
221
237
|
pass: diffs.pass,
|
|
222
|
-
message: () => `Run did not finish as expected. Failed assertions: ${failedAssertions.join(' '
|
|
238
|
+
message: () => `Run did not finish as expected. Failed assertions: ${failedAssertions.join(' ')}`,
|
|
223
239
|
actual: diffs.actual.join('\n '),
|
|
224
240
|
expected: diffs.expected.join('\n '),
|
|
225
241
|
};
|
|
@@ -255,17 +271,16 @@ export const extendExpect = (expect: ExpectStatic): ExpectStatic => {
|
|
|
255
271
|
};
|
|
256
272
|
|
|
257
273
|
type Diffs = {
|
|
258
|
-
pass: boolean
|
|
259
|
-
actual: string[]
|
|
260
|
-
expected: string[]
|
|
261
|
-
|
|
262
|
-
}
|
|
274
|
+
pass: boolean;
|
|
275
|
+
actual: string[];
|
|
276
|
+
expected: string[];
|
|
277
|
+
};
|
|
263
278
|
|
|
264
279
|
const isWithinInterval = <T extends string>(
|
|
265
280
|
diffs: Diffs,
|
|
266
281
|
actual: number | undefined,
|
|
267
282
|
options: Record<T, Interval | null>,
|
|
268
|
-
intervalOption: T
|
|
283
|
+
intervalOption: T
|
|
269
284
|
) => {
|
|
270
285
|
const expected = options[intervalOption];
|
|
271
286
|
if (expected === null) {
|
|
@@ -277,7 +292,7 @@ const isWithinInterval = <T extends string>(
|
|
|
277
292
|
diffs.pass = false;
|
|
278
293
|
diffs.actual.push(`${intervalOption}=${actual}`);
|
|
279
294
|
diffs.expected.push(`${intervalOption}=${expected}`);
|
|
280
|
-
return false
|
|
295
|
+
return false;
|
|
281
296
|
}
|
|
282
297
|
} else if (typeof expected === 'object') {
|
|
283
298
|
const { min, max } = expected;
|
|
@@ -285,8 +300,8 @@ const isWithinInterval = <T extends string>(
|
|
|
285
300
|
diffs.pass = false;
|
|
286
301
|
diffs.actual.push(`${intervalOption}=${actual}`);
|
|
287
302
|
diffs.expected.push(`${intervalOption}=<${min ?? ''},${max ?? ''}>`);
|
|
288
|
-
return false
|
|
303
|
+
return false;
|
|
289
304
|
}
|
|
290
305
|
}
|
|
291
|
-
return
|
|
306
|
+
return;
|
|
292
307
|
};
|
package/lib/lib.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
SuiteFactory,
|
|
8
8
|
TestContext,
|
|
9
9
|
} from 'vitest';
|
|
10
|
-
import type { ActorBuild, ActorTestOptions, RunOptions } from './types';
|
|
10
|
+
import type { ActorBuild, ActorTestOptions, RunOptions } from './types.js';
|
|
11
11
|
import { RunTestResult } from './run-test-result.js';
|
|
12
12
|
import { extendExpect } from './extend-expect.js';
|
|
13
13
|
import { getActorPrefilledInput, sleep } from './utils.js';
|
|
@@ -26,7 +26,7 @@ try {
|
|
|
26
26
|
throw new Error(`Failed to parse actor builds: ${err}`);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const config = actorBuilds.reduce((map, cfg) => {
|
|
29
|
+
const config = actorBuilds.reduce<Map<string, ActorBuild>>((map, cfg) => {
|
|
30
30
|
map.set(cfg.actorName, cfg);
|
|
31
31
|
map.set(cfg.actorId, cfg);
|
|
32
32
|
return map;
|
|
@@ -44,11 +44,7 @@ const DEFAULT_TEST_OPTIONS: ActorTestOptions = {
|
|
|
44
44
|
timeout: 60_000 * 60,
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
export const describe = (
|
|
48
|
-
name: string,
|
|
49
|
-
fn?: SuiteFactory<object>,
|
|
50
|
-
options: ActorTestOptions = DEFAULT_TEST_OPTIONS,
|
|
51
|
-
) => {
|
|
47
|
+
export const describe = (name: string, fn?: SuiteFactory<object>, options: ActorTestOptions = DEFAULT_TEST_OPTIONS) => {
|
|
52
48
|
vitestDescribe.runIf(!!RUN_PLATFORM_TESTS || !!RUN_ALL_PLATFORM_TESTS)(name, options, fn);
|
|
53
49
|
};
|
|
54
50
|
|
|
@@ -60,7 +56,7 @@ export const testActor = <T>(
|
|
|
60
56
|
actorName: string,
|
|
61
57
|
testName: string,
|
|
62
58
|
fn: TestFunction<{ run: ReturnType<typeof createStartRunFn<T>> }>,
|
|
63
|
-
testOptions?: ActorTestOptions
|
|
59
|
+
testOptions?: ActorTestOptions
|
|
64
60
|
) => {
|
|
65
61
|
const options = {
|
|
66
62
|
...DEFAULT_TEST_ACTOR_OPTIONS,
|
|
@@ -68,8 +64,7 @@ export const testActor = <T>(
|
|
|
68
64
|
};
|
|
69
65
|
const name = `${actorName}: ${testName}`;
|
|
70
66
|
const shouldRun = !!RUN_ALL_PLATFORM_TESTS || config.has(actorName);
|
|
71
|
-
|
|
72
|
-
vitestTest.runIf(shouldRun)(name, options, async (context) => {
|
|
67
|
+
vitestTest.runIf(shouldRun)(name, options, async <T extends TestContext>(context: T) => {
|
|
73
68
|
const { expect, ...rest } = context;
|
|
74
69
|
await fn({
|
|
75
70
|
expect: extendExpect(expect),
|
|
@@ -89,7 +84,7 @@ export const testStandbyActor = <I = any, O = any>(
|
|
|
89
84
|
actorName: string,
|
|
90
85
|
testName: string,
|
|
91
86
|
fn: TestFunction<{ callStandby: ReturnType<typeof createStartStandbyFn<I, O>> }>,
|
|
92
|
-
testOptions?: ActorTestOptions
|
|
87
|
+
testOptions?: ActorTestOptions
|
|
93
88
|
) => {
|
|
94
89
|
const options = {
|
|
95
90
|
...DEFAULT_TEST_ACTOR_OPTIONS,
|
|
@@ -98,7 +93,7 @@ export const testStandbyActor = <I = any, O = any>(
|
|
|
98
93
|
const name = `${actorName}: ${testName}`;
|
|
99
94
|
const shouldRun = !!RUN_ALL_PLATFORM_TESTS || config.has(actorName);
|
|
100
95
|
|
|
101
|
-
vitestTest.runIf(shouldRun)(name, options, async (context) => {
|
|
96
|
+
vitestTest.runIf(shouldRun)(name, options, async <T extends TestContext>(context: T) => {
|
|
102
97
|
const standbyTask = await createStandbyTask(actorName, config.get(actorName)?.buildNumber);
|
|
103
98
|
const { annotate } = context;
|
|
104
99
|
const { expect, ...rest } = context;
|
|
@@ -113,29 +108,28 @@ export const testStandbyActor = <I = any, O = any>(
|
|
|
113
108
|
} catch {}
|
|
114
109
|
|
|
115
110
|
const { taskId } = standbyTask;
|
|
116
|
-
const runs = (await apifyClient.task(taskId).runs().list()).items
|
|
111
|
+
const runs = (await apifyClient.task(taskId).runs().list()).items;
|
|
117
112
|
for (const run of runs) {
|
|
118
113
|
const runLink = generateRunLink(run);
|
|
119
114
|
await annotate(runLink, 'run_link');
|
|
120
115
|
}
|
|
121
116
|
|
|
122
117
|
if (taskId) {
|
|
123
|
-
await apifyClient.task(taskId).delete()
|
|
118
|
+
await apifyClient.task(taskId).delete();
|
|
124
119
|
}
|
|
125
|
-
|
|
126
120
|
});
|
|
127
121
|
};
|
|
128
122
|
|
|
129
123
|
export const testTestActor = <T>(
|
|
130
124
|
testName: string,
|
|
131
|
-
fn: TestFunction<{ run: ReturnType<typeof createStartRunFn<T>> }
|
|
125
|
+
fn: TestFunction<{ run: ReturnType<typeof createStartRunFn<T>> }>
|
|
132
126
|
) => {
|
|
133
127
|
vitestTest(testName, async (context) => {
|
|
134
128
|
const { expect, ...rest } = context;
|
|
135
129
|
await fn({
|
|
136
130
|
expect: extendExpect(expect),
|
|
137
131
|
// @ts-expect-error: this just to test custom matchers
|
|
138
|
-
run: () => {
|
|
132
|
+
run: () => {},
|
|
139
133
|
...rest,
|
|
140
134
|
});
|
|
141
135
|
});
|
|
@@ -158,14 +152,14 @@ const createStartStandbyFn = <I, O>(standbyTask: StandbyTask) => {
|
|
|
158
152
|
body: JSON.stringify(input),
|
|
159
153
|
});
|
|
160
154
|
|
|
161
|
-
const data = await response.json() as O;
|
|
155
|
+
const data = (await response.json()) as O;
|
|
162
156
|
return {
|
|
163
157
|
data,
|
|
164
158
|
status: response.status,
|
|
165
159
|
headers: response.headers,
|
|
166
160
|
};
|
|
167
161
|
};
|
|
168
|
-
}
|
|
162
|
+
};
|
|
169
163
|
|
|
170
164
|
interface StandbyTask {
|
|
171
165
|
standbyUrl: string;
|
|
@@ -174,7 +168,7 @@ interface StandbyTask {
|
|
|
174
168
|
|
|
175
169
|
const randomInt = (min: number, max: number) => {
|
|
176
170
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
177
|
-
}
|
|
171
|
+
};
|
|
178
172
|
|
|
179
173
|
/**
|
|
180
174
|
* Creates a task with specific `build` - either `buildNumber` or default.
|
|
@@ -202,19 +196,22 @@ const createStandbyTask = async (actorNameOrId: string, buildNumber?: string): P
|
|
|
202
196
|
const actorStandbyOptions: ActorStandby = {
|
|
203
197
|
...defaultActorStandby,
|
|
204
198
|
build,
|
|
205
|
-
}
|
|
199
|
+
};
|
|
206
200
|
|
|
207
201
|
try {
|
|
208
202
|
const title = `Test task - ${build}:${actorNameOrId}`.slice(0, 62);
|
|
209
203
|
// we try to create unique task name containing only `a-z0-9-` characters and at most 63 characters long
|
|
210
|
-
const name = `${randomInt(1, 1_000_000)}${title
|
|
211
|
-
|
|
204
|
+
const name = `${randomInt(1, 1_000_000)}${title
|
|
205
|
+
.toLowerCase()
|
|
206
|
+
.replaceAll(/\s+/g, '')
|
|
207
|
+
.replaceAll(/[^a-z0-9-]+/g, '-')}`.slice(0, 62);
|
|
208
|
+
const newTask = (await apifyClient.tasks().create({
|
|
212
209
|
actId: actorNameOrId,
|
|
213
210
|
actorStandby: actorStandbyOptions,
|
|
214
211
|
description: `Task for testing standby version ${build}`,
|
|
215
212
|
title,
|
|
216
213
|
name,
|
|
217
|
-
}) as Task & { standbyUrl?: string };
|
|
214
|
+
})) as Task & { standbyUrl?: string };
|
|
218
215
|
|
|
219
216
|
const { id, standbyUrl } = newTask;
|
|
220
217
|
|
|
@@ -225,11 +222,11 @@ const createStandbyTask = async (actorNameOrId: string, buildNumber?: string): P
|
|
|
225
222
|
return {
|
|
226
223
|
standbyUrl,
|
|
227
224
|
taskId: id,
|
|
228
|
-
}
|
|
225
|
+
};
|
|
229
226
|
} catch (error) {
|
|
230
227
|
throw new Error(`Failed to create task: ${error}`);
|
|
231
228
|
}
|
|
232
|
-
}
|
|
229
|
+
};
|
|
233
230
|
|
|
234
231
|
const createStartRunFn = <T>(actorNameOrId: string, testContext: TestContext) => {
|
|
235
232
|
const { annotate, task } = testContext;
|
|
@@ -237,31 +234,23 @@ const createStartRunFn = <T>(actorNameOrId: string, testContext: TestContext) =>
|
|
|
237
234
|
const build = actorConfig?.buildNumber;
|
|
238
235
|
const buildId = actorConfig?.buildId;
|
|
239
236
|
return async (runOptions: RunOptions<T>) => {
|
|
240
|
-
const {
|
|
241
|
-
input,
|
|
242
|
-
options,
|
|
243
|
-
prefilledInput,
|
|
244
|
-
runId,
|
|
245
|
-
} = runOptions;
|
|
237
|
+
const { input, options, prefilledInput, runId } = runOptions;
|
|
246
238
|
|
|
247
239
|
if (runId) {
|
|
248
|
-
const run = await apifyClient.run(runId).get()
|
|
240
|
+
const run = await apifyClient.run(runId).get();
|
|
249
241
|
if (!run) {
|
|
250
242
|
throw new Error(`Run with id "${runId}" doesn't exist`);
|
|
251
243
|
}
|
|
252
|
-
return new RunTestResult(apifyClient, run)
|
|
244
|
+
return new RunTestResult(apifyClient, run);
|
|
253
245
|
}
|
|
254
246
|
|
|
255
247
|
const actor = apifyClient.actor(actorNameOrId);
|
|
256
248
|
|
|
257
249
|
const actorInput = {
|
|
258
|
-
...(prefilledInput && await getActorPrefilledInput(apifyClient, actorNameOrId, buildId)),
|
|
250
|
+
...(prefilledInput && (await getActorPrefilledInput(apifyClient, actorNameOrId, buildId))),
|
|
259
251
|
...input,
|
|
260
252
|
};
|
|
261
|
-
const run = await actor.call(
|
|
262
|
-
actorInput,
|
|
263
|
-
{ build, ...options, },
|
|
264
|
-
);
|
|
253
|
+
const run = await actor.call(actorInput, { build, ...options });
|
|
265
254
|
|
|
266
255
|
const runLink = generateRunLink(run);
|
|
267
256
|
await annotate(runLink, 'run_link');
|
|
@@ -281,4 +270,4 @@ const createStartRunFn = <T>(actorNameOrId: string, testContext: TestContext) =>
|
|
|
281
270
|
|
|
282
271
|
const generateRunLink = (run: ActorRun | ActorRunListItem): string => {
|
|
283
272
|
return `https://console.apify.com/view/runs/${run.id}`;
|
|
284
|
-
}
|
|
273
|
+
};
|
package/lib/run-test-result.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ActorRun, ApifyClient, KeyValueStoreClient } from 'apify-client';
|
|
2
|
-
import type { Dataset, SdkCrawlerStatistics } from './types';
|
|
2
|
+
import type { Dataset, SdkCrawlerStatistics } from './types.js';
|
|
3
3
|
|
|
4
4
|
export class RunTestResult {
|
|
5
5
|
private log: string | undefined;
|
|
@@ -9,10 +9,9 @@ export class RunTestResult {
|
|
|
9
9
|
private runInfo: ActorRun | undefined;
|
|
10
10
|
private input: unknown | undefined;
|
|
11
11
|
|
|
12
|
-
constructor(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
) { /**/ }
|
|
12
|
+
constructor(private readonly apifyClient: ApifyClient, private readonly run: ActorRun) {
|
|
13
|
+
/**/
|
|
14
|
+
}
|
|
16
15
|
|
|
17
16
|
getStatistics = async (): Promise<SdkCrawlerStatistics | undefined> => {
|
|
18
17
|
if (this.statistics) {
|
|
@@ -33,7 +32,7 @@ export class RunTestResult {
|
|
|
33
32
|
return runLog as string;
|
|
34
33
|
};
|
|
35
34
|
|
|
36
|
-
getDataset = async<T>(): Promise<Dataset<T>> => {
|
|
35
|
+
getDataset = async <T>(): Promise<Dataset<T>> => {
|
|
37
36
|
if (this.dataset) {
|
|
38
37
|
return this.dataset as Dataset<T>;
|
|
39
38
|
}
|
|
@@ -49,18 +48,18 @@ export class RunTestResult {
|
|
|
49
48
|
}
|
|
50
49
|
const keyValueStoreClient = this.apifyClient.keyValueStore(this.run.defaultKeyValueStoreId);
|
|
51
50
|
this.keyValueStoreClient = keyValueStoreClient;
|
|
52
|
-
return keyValueStoreClient
|
|
51
|
+
return keyValueStoreClient;
|
|
53
52
|
};
|
|
54
53
|
|
|
55
|
-
getInput = async<T>(): Promise<T> => {
|
|
54
|
+
getInput = async <T>(): Promise<T> => {
|
|
56
55
|
if (this.input) {
|
|
57
|
-
return this.input as T
|
|
56
|
+
return this.input as T;
|
|
58
57
|
}
|
|
59
58
|
const kvs = this.apifyClient.keyValueStore(this.run.defaultKeyValueStoreId);
|
|
60
59
|
const input = await kvs.getRecord('INPUT');
|
|
61
60
|
this.input = input?.value;
|
|
62
61
|
return this.input as T;
|
|
63
|
-
}
|
|
62
|
+
};
|
|
64
63
|
|
|
65
64
|
getRunInfo = async (): Promise<ActorRun> => {
|
|
66
65
|
if (this.runInfo) {
|
package/lib/types.ts
CHANGED
|
@@ -6,57 +6,57 @@ export type ActorBuild = {
|
|
|
6
6
|
actorId: string;
|
|
7
7
|
buildNumber: string;
|
|
8
8
|
actorName: string;
|
|
9
|
-
}
|
|
9
|
+
};
|
|
10
10
|
|
|
11
11
|
export type RunOptions<T> = {
|
|
12
|
-
input: Omit<T, 'actorName'
|
|
13
|
-
options?: ActorCallOptions
|
|
14
|
-
prefilledInput?: boolean
|
|
12
|
+
input: Omit<T, 'actorName'>;
|
|
13
|
+
options?: ActorCallOptions;
|
|
14
|
+
prefilledInput?: boolean;
|
|
15
15
|
/**
|
|
16
16
|
* If you specify `runId`, all the other options will be ignored and this run's data will
|
|
17
17
|
* be downloaded instead.
|
|
18
18
|
*
|
|
19
19
|
* This is usefull for testing your tests on existing runs
|
|
20
20
|
*/
|
|
21
|
-
runId?: string
|
|
22
|
-
}
|
|
21
|
+
runId?: string;
|
|
22
|
+
};
|
|
23
23
|
|
|
24
24
|
export type Dataset<T> = {
|
|
25
|
-
items: T[]
|
|
26
|
-
}
|
|
25
|
+
items: T[];
|
|
26
|
+
};
|
|
27
27
|
|
|
28
|
-
export type OpenInterval =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
export type OpenInterval =
|
|
29
|
+
| {
|
|
30
|
+
min: number;
|
|
31
|
+
max?: undefined;
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
min?: undefined;
|
|
35
|
+
max: number;
|
|
36
|
+
};
|
|
35
37
|
|
|
36
38
|
export type ClosedInterval = {
|
|
37
|
-
min: number
|
|
38
|
-
max: number
|
|
39
|
-
}
|
|
39
|
+
min: number;
|
|
40
|
+
max: number;
|
|
41
|
+
};
|
|
40
42
|
|
|
41
|
-
export type Interval = number | OpenInterval | ClosedInterval
|
|
43
|
+
export type Interval = number | OpenInterval | ClosedInterval;
|
|
42
44
|
|
|
43
45
|
export type ToFinishWithOptionsWithDefaults = {
|
|
44
|
-
status: RunStatus
|
|
45
|
-
duration: Interval | null
|
|
46
|
-
failedRequests: Interval | null
|
|
47
|
-
requestsRetries: Interval | null
|
|
48
|
-
forbiddenLogs: string[]
|
|
49
|
-
|
|
46
|
+
status: RunStatus;
|
|
47
|
+
duration: Interval | null;
|
|
48
|
+
failedRequests: Interval | null;
|
|
49
|
+
requestsRetries: Interval | null;
|
|
50
|
+
forbiddenLogs: string[];
|
|
51
|
+
maxRetriesPerRequest: number | null;
|
|
52
|
+
};
|
|
50
53
|
export type IntervalOption<PpeEvent extends string> = keyof Pick<
|
|
51
54
|
ToFinishWithOptions<PpeEvent>,
|
|
52
|
-
| '
|
|
53
|
-
|
|
54
|
-
| 'failedRequests'
|
|
55
|
-
| 'duration'
|
|
56
|
-
>
|
|
55
|
+
'datasetItemCount' | 'requestsRetries' | 'failedRequests' | 'duration'
|
|
56
|
+
>;
|
|
57
57
|
|
|
58
58
|
export type ToFinishWithOptions<PpeEvent extends string> = Partial<ToFinishWithOptionsWithDefaults> & {
|
|
59
|
-
datasetItemCount: Interval
|
|
59
|
+
datasetItemCount: Interval;
|
|
60
60
|
/**
|
|
61
61
|
* Define expected charged (PPE) event counts. You can also define count as `Interval`.
|
|
62
62
|
*
|
|
@@ -71,26 +71,26 @@ export type ToFinishWithOptions<PpeEvent extends string> = Partial<ToFinishWithO
|
|
|
71
71
|
* If you omit any PPE event, it's expected count will be 0.
|
|
72
72
|
* Assertion will fail if `chargedEventCounts` doesn't contain some of the expected events.
|
|
73
73
|
*/
|
|
74
|
-
chargedEventCounts?: Record<PpeEvent, Interval
|
|
74
|
+
chargedEventCounts?: Record<PpeEvent, Interval>;
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
export type SdkCrawlerStatistics = {
|
|
78
|
-
requestsFinished: number
|
|
79
|
-
requestsFailed: number
|
|
80
|
-
requestsRetries: number
|
|
81
|
-
requestsFailedPerMinute: number
|
|
82
|
-
requestsFinishedPerMinute: number
|
|
83
|
-
requestMinDurationMillis: number
|
|
84
|
-
requestMaxDurationMillis: number
|
|
85
|
-
requestRetryHistogram: number[]
|
|
86
|
-
requestTotalFailedDurationMillis: number
|
|
87
|
-
requestTotalFinishedDurationMillis: number
|
|
88
|
-
crawlerStartedAt: string
|
|
89
|
-
crawlerFinishedAt: string
|
|
90
|
-
statsPersistedAt: string
|
|
91
|
-
crawlerRuntimeMillis: number
|
|
92
|
-
crawlerLastStartTimestamp: number
|
|
93
|
-
}
|
|
78
|
+
requestsFinished: number;
|
|
79
|
+
requestsFailed: number;
|
|
80
|
+
requestsRetries: number;
|
|
81
|
+
requestsFailedPerMinute: number;
|
|
82
|
+
requestsFinishedPerMinute: number;
|
|
83
|
+
requestMinDurationMillis: number;
|
|
84
|
+
requestMaxDurationMillis: number;
|
|
85
|
+
requestRetryHistogram: number[];
|
|
86
|
+
requestTotalFailedDurationMillis: number;
|
|
87
|
+
requestTotalFinishedDurationMillis: number;
|
|
88
|
+
crawlerStartedAt: string;
|
|
89
|
+
crawlerFinishedAt: string;
|
|
90
|
+
statsPersistedAt: string;
|
|
91
|
+
crawlerRuntimeMillis: number;
|
|
92
|
+
crawlerLastStartTimestamp: number;
|
|
93
|
+
};
|
|
94
94
|
|
|
95
95
|
export type RunStatus =
|
|
96
96
|
| 'SUCCEEDED'
|
|
@@ -100,22 +100,22 @@ export type RunStatus =
|
|
|
100
100
|
| 'ABORTING'
|
|
101
101
|
| 'ABORTED'
|
|
102
102
|
| 'TIMING-OUT'
|
|
103
|
-
| 'TIMED-OUT'
|
|
103
|
+
| 'TIMED-OUT';
|
|
104
104
|
|
|
105
105
|
export interface ActorMatchers<R = unknown> {
|
|
106
|
-
toBeArray: () => R
|
|
107
|
-
toBeBoolean: () => R
|
|
108
|
-
toBeEmptyArray: () => R
|
|
109
|
-
toBeNonEmptyArray: () => R
|
|
110
|
-
toBeNonEmptyString: () => R
|
|
111
|
-
toBeNumber: () => R
|
|
112
|
-
toBeFalse: () => R
|
|
113
|
-
toBeTrue: () => R
|
|
114
|
-
toBeNonEmptyObject: () => R
|
|
115
|
-
toBeObject: () => R
|
|
116
|
-
toBeString: () => R
|
|
117
|
-
toBeWholeNumber: () => R
|
|
118
|
-
toBeWithinRange: (lower: number, upper: number) => R
|
|
106
|
+
toBeArray: () => R;
|
|
107
|
+
toBeBoolean: () => R;
|
|
108
|
+
toBeEmptyArray: () => R;
|
|
109
|
+
toBeNonEmptyArray: () => R;
|
|
110
|
+
toBeNonEmptyString: () => R;
|
|
111
|
+
toBeNumber: () => R;
|
|
112
|
+
toBeFalse: () => R;
|
|
113
|
+
toBeTrue: () => R;
|
|
114
|
+
toBeNonEmptyObject: () => R;
|
|
115
|
+
toBeObject: () => R;
|
|
116
|
+
toBeString: () => R;
|
|
117
|
+
toBeWholeNumber: () => R;
|
|
118
|
+
toBeWithinRange: (lower: number, upper: number) => R;
|
|
119
119
|
/**
|
|
120
120
|
* Validates the following properties of a run:
|
|
121
121
|
* - `status` (default: `SUCCEEDED`)
|
|
@@ -123,12 +123,13 @@ export interface ActorMatchers<R = unknown> {
|
|
|
123
123
|
* - `failedRequests` (default: `0`)
|
|
124
124
|
* - `requestsRetries` (default: `{ max: 3 }`)
|
|
125
125
|
* - `forbiddenLogs` (default: `['ReferenceError', 'TypeError']`)
|
|
126
|
+
* - `maxRetriesPerRequest` (not checked by default)
|
|
126
127
|
* - `datasetItemCount` (required)
|
|
127
128
|
* - `chargedEventCounts`
|
|
128
129
|
*/
|
|
129
|
-
toFinishWith: <PpeEvent extends string>(options: ToFinishWithOptions<PpeEvent>) => Promise<R
|
|
130
|
-
toStartWith: (prefix: string) => R
|
|
131
|
-
hard: <T>(actual: T, message?: string) => Assertion
|
|
130
|
+
toFinishWith: <PpeEvent extends string>(options: ToFinishWithOptions<PpeEvent>) => Promise<R>;
|
|
131
|
+
toStartWith: (prefix: string) => R;
|
|
132
|
+
hard: <T>(actual: T, message?: string) => Assertion;
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
export type ActorTestOptions = Omit<TestOptions, 'retry'> & {
|
|
@@ -144,6 +145,8 @@ export type ActorTestOptions = Omit<TestOptions, 'retry'> & {
|
|
|
144
145
|
|
|
145
146
|
declare module 'vitest' {
|
|
146
147
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
147
|
-
interface Assertion<T = any> extends ActorMatchers<T> {
|
|
148
|
-
|
|
148
|
+
interface Assertion<T = any> extends ActorMatchers<T> {}
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
150
|
+
interface Matchers<T = any> extends ActorMatchers<T> {}
|
|
151
|
+
interface AsymmetricMatchersContaining extends ActorMatchers {}
|
|
149
152
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apify-test-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "TBD",
|
|
6
6
|
"engines": {
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
"typescript": "^5.7",
|
|
29
29
|
"vitest": "^3.2.4"
|
|
30
30
|
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"vitest": ">=3.2.4"
|
|
33
|
+
},
|
|
31
34
|
"scripts": {
|
|
32
35
|
"start": "npm run dist/bin/main.js",
|
|
33
36
|
"start:dev": "GITHUB_WORKSPACE=local-clone tsx bin/main.ts",
|
|
@@ -2,13 +2,10 @@ import process from 'process';
|
|
|
2
2
|
import { describe } from 'vitest';
|
|
3
3
|
|
|
4
4
|
import { ApifyClient } from 'apify-client';
|
|
5
|
-
import { testStandbyActor, testTestActor } from '../../lib/lib';
|
|
6
|
-
import { RunTestResult } from '../../lib/run-test-result';
|
|
5
|
+
import { testStandbyActor, testTestActor } from '../../lib/lib.js';
|
|
6
|
+
import { RunTestResult } from '../../lib/run-test-result.js';
|
|
7
7
|
|
|
8
|
-
type PpeEventType =
|
|
9
|
-
| 'actor-start'
|
|
10
|
-
| 'search-page-scraped'
|
|
11
|
-
| 'ads-scraped'
|
|
8
|
+
type PpeEventType = 'actor-start' | 'search-page-scraped' | 'ads-scraped';
|
|
12
9
|
enum PpeEventEnum {
|
|
13
10
|
ACTOR_START = 'actor-start',
|
|
14
11
|
SEARCH_PAGE_SCRAPED = 'search-page-scraped',
|
|
@@ -45,7 +42,7 @@ describe('custom-matchers', { timeout: 100_000 }, () => {
|
|
|
45
42
|
},
|
|
46
43
|
});
|
|
47
44
|
|
|
48
|
-
await expect(runResult).toFinishWith<typeof PPE_EVENT_CONST[keyof typeof PPE_EVENT_CONST]>({
|
|
45
|
+
await expect(runResult).toFinishWith<(typeof PPE_EVENT_CONST)[keyof typeof PPE_EVENT_CONST]>({
|
|
49
46
|
datasetItemCount: { min: 1, max: 20 },
|
|
50
47
|
chargedEventCounts: {
|
|
51
48
|
'actor-start': 1,
|
|
@@ -67,7 +64,7 @@ describe('custom-matchers', { timeout: 100_000 }, () => {
|
|
|
67
64
|
maxRequests: 3,
|
|
68
65
|
aggregateContacts: true,
|
|
69
66
|
},
|
|
70
|
-
})
|
|
67
|
+
});
|
|
71
68
|
expect(status).toBe(200);
|
|
72
69
|
expect(data[0].domain).toEqual('apify.com');
|
|
73
70
|
}
|
|
@@ -78,7 +75,7 @@ describe('custom-matchers', { timeout: 100_000 }, () => {
|
|
|
78
75
|
maxRequests: 3,
|
|
79
76
|
aggregateContacts: true,
|
|
80
77
|
},
|
|
81
|
-
})
|
|
78
|
+
});
|
|
82
79
|
expect(status).toBe(200);
|
|
83
80
|
expect(data[0].domain).toEqual('apify.com');
|
|
84
81
|
});
|