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.
Files changed (48) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/CONTRIBUTING.md +53 -0
  3. package/README.md +247 -17
  4. package/bin/git.ts +24 -5
  5. package/bin/main.ts +11 -1
  6. package/bin/types.ts +1 -0
  7. package/bin/utils.ts +15 -8
  8. package/dist/bin/git.d.ts +2 -0
  9. package/dist/bin/git.d.ts.map +1 -1
  10. package/dist/bin/git.js +22 -5
  11. package/dist/bin/git.js.map +1 -1
  12. package/dist/bin/main.js +9 -1
  13. package/dist/bin/main.js.map +1 -1
  14. package/dist/bin/types.d.ts +1 -0
  15. package/dist/bin/types.d.ts.map +1 -1
  16. package/dist/bin/utils.d.ts +3 -0
  17. package/dist/bin/utils.d.ts.map +1 -1
  18. package/dist/bin/utils.js +13 -8
  19. package/dist/bin/utils.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/lib/lib.d.ts +23 -1
  25. package/dist/lib/lib.d.ts.map +1 -1
  26. package/dist/lib/lib.js +110 -4
  27. package/dist/lib/lib.js.map +1 -1
  28. package/dist/lib/run-test-result.d.ts.map +1 -1
  29. package/dist/lib/run-test-result.js +2 -1
  30. package/dist/lib/run-test-result.js.map +1 -1
  31. package/dist/lib/utils.d.ts +1 -0
  32. package/dist/lib/utils.d.ts.map +1 -1
  33. package/dist/lib/utils.js +3 -0
  34. package/dist/lib/utils.js.map +1 -1
  35. package/dist/test/unit/custom-matchers.test.js +23 -1
  36. package/dist/test/unit/custom-matchers.test.js.map +1 -1
  37. package/dist/test/unit/parse-commit.test.d.ts +2 -0
  38. package/dist/test/unit/parse-commit.test.d.ts.map +1 -0
  39. package/dist/test/unit/parse-commit.test.js +57 -0
  40. package/dist/test/unit/parse-commit.test.js.map +1 -0
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/index.ts +1 -1
  43. package/lib/lib.ts +137 -5
  44. package/lib/run-test-result.ts +2 -1
  45. package/lib/utils.ts +6 -1
  46. package/package.json +1 -1
  47. package/test/unit/custom-matchers.test.ts +25 -1
  48. 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
- const createStartRunFn = <T>(actorNameOrId: string, { annotate, task }: TestContext) => {
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 = `https://console.apify.com/view/runs/${run.id}`;
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 new Promise((resolve) => setTimeout(resolve, 10_000));
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
+ }
@@ -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
- return input as T;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apify-test-tools",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "TBD",
6
6
  "engines": {
@@ -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
+ });