apify-test-tools 0.1.0

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 (103) hide show
  1. package/README.md +23 -0
  2. package/bin/build.ts +287 -0
  3. package/bin/consts.ts +6 -0
  4. package/bin/git.ts +56 -0
  5. package/bin/github.ts +75 -0
  6. package/bin/main.ts +145 -0
  7. package/bin/slack.ts +72 -0
  8. package/bin/test-report.ts +146 -0
  9. package/bin/types.ts +102 -0
  10. package/bin/utils.ts +204 -0
  11. package/dist/bin/build.d.ts +18 -0
  12. package/dist/bin/build.d.ts.map +1 -0
  13. package/dist/bin/build.js +222 -0
  14. package/dist/bin/build.js.map +1 -0
  15. package/dist/bin/consts.d.ts +4 -0
  16. package/dist/bin/consts.d.ts.map +1 -0
  17. package/dist/bin/consts.js +6 -0
  18. package/dist/bin/consts.js.map +1 -0
  19. package/dist/bin/git.d.ts +7 -0
  20. package/dist/bin/git.d.ts.map +1 -0
  21. package/dist/bin/git.js +47 -0
  22. package/dist/bin/git.js.map +1 -0
  23. package/dist/bin/github.d.ts +12 -0
  24. package/dist/bin/github.d.ts.map +1 -0
  25. package/dist/bin/github.js +64 -0
  26. package/dist/bin/github.js.map +1 -0
  27. package/dist/bin/main.d.ts +3 -0
  28. package/dist/bin/main.d.ts.map +1 -0
  29. package/dist/bin/main.js +117 -0
  30. package/dist/bin/main.js.map +1 -0
  31. package/dist/bin/slack.d.ts +12 -0
  32. package/dist/bin/slack.d.ts.map +1 -0
  33. package/dist/bin/slack.js +46 -0
  34. package/dist/bin/slack.js.map +1 -0
  35. package/dist/bin/test-report.d.ts +10 -0
  36. package/dist/bin/test-report.d.ts.map +1 -0
  37. package/dist/bin/test-report.js +74 -0
  38. package/dist/bin/test-report.js.map +1 -0
  39. package/dist/bin/types.d.ts +88 -0
  40. package/dist/bin/types.d.ts.map +1 -0
  41. package/dist/bin/types.js +2 -0
  42. package/dist/bin/types.js.map +1 -0
  43. package/dist/bin/utils.d.ts +26 -0
  44. package/dist/bin/utils.d.ts.map +1 -0
  45. package/dist/bin/utils.js +164 -0
  46. package/dist/bin/utils.js.map +1 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +3 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/lib/consts.d.ts +3 -0
  52. package/dist/lib/consts.d.ts.map +1 -0
  53. package/dist/lib/consts.js +14 -0
  54. package/dist/lib/consts.js.map +1 -0
  55. package/dist/lib/extend-expect.d.ts +3 -0
  56. package/dist/lib/extend-expect.d.ts.map +1 -0
  57. package/dist/lib/extend-expect.js +228 -0
  58. package/dist/lib/extend-expect.js.map +1 -0
  59. package/dist/lib/lib.d.ts +16 -0
  60. package/dist/lib/lib.d.ts.map +1 -0
  61. package/dist/lib/lib.js +91 -0
  62. package/dist/lib/lib.js.map +1 -0
  63. package/dist/lib/run-test-result.d.ts +20 -0
  64. package/dist/lib/run-test-result.d.ts.map +1 -0
  65. package/dist/lib/run-test-result.js +62 -0
  66. package/dist/lib/run-test-result.js.map +1 -0
  67. package/dist/lib/types.d.ts +107 -0
  68. package/dist/lib/types.d.ts.map +1 -0
  69. package/dist/lib/types.js +2 -0
  70. package/dist/lib/types.js.map +1 -0
  71. package/dist/lib/utils.d.ts +7 -0
  72. package/dist/lib/utils.d.ts.map +1 -0
  73. package/dist/lib/utils.js +30 -0
  74. package/dist/lib/utils.js.map +1 -0
  75. package/dist/test/unit/custom-matchers.test.d.ts +2 -0
  76. package/dist/test/unit/custom-matchers.test.d.ts.map +1 -0
  77. package/dist/test/unit/custom-matchers.test.js +53 -0
  78. package/dist/test/unit/custom-matchers.test.js.map +1 -0
  79. package/dist/test/unit/should-built-and-test.test.d.ts +2 -0
  80. package/dist/test/unit/should-built-and-test.test.d.ts.map +1 -0
  81. package/dist/test/unit/should-built-and-test.test.js +136 -0
  82. package/dist/test/unit/should-built-and-test.test.js.map +1 -0
  83. package/dist/test/unit/test-actor.test.d.ts +2 -0
  84. package/dist/test/unit/test-actor.test.d.ts.map +1 -0
  85. package/dist/test/unit/test-actor.test.js +17 -0
  86. package/dist/test/unit/test-actor.test.js.map +1 -0
  87. package/dist/tsconfig.tsbuildinfo +1 -0
  88. package/example-github-events/pull-request.json +523 -0
  89. package/example-github-events/push.json +216 -0
  90. package/index.ts +2 -0
  91. package/lib/consts.ts +15 -0
  92. package/lib/extend-expect.ts +268 -0
  93. package/lib/lib.ts +134 -0
  94. package/lib/run-test-result.ts +70 -0
  95. package/lib/types.ts +131 -0
  96. package/lib/utils.ts +49 -0
  97. package/package.json +41 -0
  98. package/scripts/hook-job-started.sh +11 -0
  99. package/test/tsconfig.json +15 -0
  100. package/test/unit/custom-matchers.test.ts +61 -0
  101. package/test/unit/should-built-and-test.test.ts +166 -0
  102. package/test/unit/test-actor.test.ts +17 -0
  103. package/tsconfig.json +20 -0
@@ -0,0 +1,216 @@
1
+ {
2
+ "ref": "refs/heads/master",
3
+ "before": "e15bf2a167bec5681afef2a5c057b8a10b78850d",
4
+ "after": "7f9e105e4ce7881f9def76db60bd8e4e58ead9f2",
5
+ "repository": {
6
+ "id": 530628540,
7
+ "node_id": "R_kgDOH6C_vA",
8
+ "name": "store-booking",
9
+ "full_name": "apify-projects/store-booking",
10
+ "private": true,
11
+ "owner": {
12
+ "name": "apify-projects",
13
+ "email": "hello@apify.com",
14
+ "login": "apify-projects",
15
+ "id": 69005150,
16
+ "node_id": "MDEyOk9yZ2FuaXphdGlvbjY5MDA1MTUw",
17
+ "avatar_url": "https://avatars.githubusercontent.com/u/69005150?v=4",
18
+ "gravatar_id": "",
19
+ "url": "https://api.github.com/users/apify-projects",
20
+ "html_url": "https://github.com/apify-projects",
21
+ "followers_url": "https://api.github.com/users/apify-projects/followers",
22
+ "following_url": "https://api.github.com/users/apify-projects/following{/other_user}",
23
+ "gists_url": "https://api.github.com/users/apify-projects/gists{/gist_id}",
24
+ "starred_url": "https://api.github.com/users/apify-projects/starred{/owner}{/repo}",
25
+ "subscriptions_url": "https://api.github.com/users/apify-projects/subscriptions",
26
+ "organizations_url": "https://api.github.com/users/apify-projects/orgs",
27
+ "repos_url": "https://api.github.com/users/apify-projects/repos",
28
+ "events_url": "https://api.github.com/users/apify-projects/events{/privacy}",
29
+ "received_events_url": "https://api.github.com/users/apify-projects/received_events",
30
+ "type": "Organization",
31
+ "site_admin": false
32
+ },
33
+ "html_url": "https://github.com/apify-projects/store-booking",
34
+ "description": null,
35
+ "fork": false,
36
+ "url": "https://github.com/apify-projects/store-booking",
37
+ "forks_url": "https://api.github.com/repos/apify-projects/store-booking/forks",
38
+ "keys_url": "https://api.github.com/repos/apify-projects/store-booking/keys{/key_id}",
39
+ "collaborators_url": "https://api.github.com/repos/apify-projects/store-booking/collaborators{/collaborator}",
40
+ "teams_url": "https://api.github.com/repos/apify-projects/store-booking/teams",
41
+ "hooks_url": "https://api.github.com/repos/apify-projects/store-booking/hooks",
42
+ "issue_events_url": "https://api.github.com/repos/apify-projects/store-booking/issues/events{/number}",
43
+ "events_url": "https://api.github.com/repos/apify-projects/store-booking/events",
44
+ "assignees_url": "https://api.github.com/repos/apify-projects/store-booking/assignees{/user}",
45
+ "branches_url": "https://api.github.com/repos/apify-projects/store-booking/branches{/branch}",
46
+ "tags_url": "https://api.github.com/repos/apify-projects/store-booking/tags",
47
+ "blobs_url": "https://api.github.com/repos/apify-projects/store-booking/git/blobs{/sha}",
48
+ "git_tags_url": "https://api.github.com/repos/apify-projects/store-booking/git/tags{/sha}",
49
+ "git_refs_url": "https://api.github.com/repos/apify-projects/store-booking/git/refs{/sha}",
50
+ "trees_url": "https://api.github.com/repos/apify-projects/store-booking/git/trees{/sha}",
51
+ "statuses_url": "https://api.github.com/repos/apify-projects/store-booking/statuses/{sha}",
52
+ "languages_url": "https://api.github.com/repos/apify-projects/store-booking/languages",
53
+ "stargazers_url": "https://api.github.com/repos/apify-projects/store-booking/stargazers",
54
+ "contributors_url": "https://api.github.com/repos/apify-projects/store-booking/contributors",
55
+ "subscribers_url": "https://api.github.com/repos/apify-projects/store-booking/subscribers",
56
+ "subscription_url": "https://api.github.com/repos/apify-projects/store-booking/subscription",
57
+ "commits_url": "https://api.github.com/repos/apify-projects/store-booking/commits{/sha}",
58
+ "git_commits_url": "https://api.github.com/repos/apify-projects/store-booking/git/commits{/sha}",
59
+ "comments_url": "https://api.github.com/repos/apify-projects/store-booking/comments{/number}",
60
+ "issue_comment_url": "https://api.github.com/repos/apify-projects/store-booking/issues/comments{/number}",
61
+ "contents_url": "https://api.github.com/repos/apify-projects/store-booking/contents/{+path}",
62
+ "compare_url": "https://api.github.com/repos/apify-projects/store-booking/compare/{base}...{head}",
63
+ "merges_url": "https://api.github.com/repos/apify-projects/store-booking/merges",
64
+ "archive_url": "https://api.github.com/repos/apify-projects/store-booking/{archive_format}{/ref}",
65
+ "downloads_url": "https://api.github.com/repos/apify-projects/store-booking/downloads",
66
+ "issues_url": "https://api.github.com/repos/apify-projects/store-booking/issues{/number}",
67
+ "pulls_url": "https://api.github.com/repos/apify-projects/store-booking/pulls{/number}",
68
+ "milestones_url": "https://api.github.com/repos/apify-projects/store-booking/milestones{/number}",
69
+ "notifications_url": "https://api.github.com/repos/apify-projects/store-booking/notifications{?since,all,participating}",
70
+ "labels_url": "https://api.github.com/repos/apify-projects/store-booking/labels{/name}",
71
+ "releases_url": "https://api.github.com/repos/apify-projects/store-booking/releases{/id}",
72
+ "deployments_url": "https://api.github.com/repos/apify-projects/store-booking/deployments",
73
+ "created_at": 1661859371,
74
+ "updated_at": "2024-07-01T22:15:13Z",
75
+ "pushed_at": 1721436715,
76
+ "git_url": "git://github.com/apify-projects/store-booking.git",
77
+ "ssh_url": "git@github.com:apify-projects/store-booking.git",
78
+ "clone_url": "https://github.com/apify-projects/store-booking.git",
79
+ "svn_url": "https://github.com/apify-projects/store-booking",
80
+ "homepage": "",
81
+ "size": 3080,
82
+ "stargazers_count": 0,
83
+ "watchers_count": 0,
84
+ "language": "TypeScript",
85
+ "has_issues": true,
86
+ "has_projects": true,
87
+ "has_downloads": true,
88
+ "has_wiki": true,
89
+ "has_pages": false,
90
+ "has_discussions": false,
91
+ "forks_count": 1,
92
+ "mirror_url": null,
93
+ "archived": false,
94
+ "disabled": false,
95
+ "open_issues_count": 19,
96
+ "license": {
97
+ "key": "apache-2.0",
98
+ "name": "Apache License 2.0",
99
+ "spdx_id": "Apache-2.0",
100
+ "url": "https://api.github.com/licenses/apache-2.0",
101
+ "node_id": "MDc6TGljZW5zZTI="
102
+ },
103
+ "allow_forking": true,
104
+ "is_template": false,
105
+ "web_commit_signoff_required": false,
106
+ "topics": [],
107
+ "visibility": "private",
108
+ "forks": 1,
109
+ "open_issues": 19,
110
+ "watchers": 0,
111
+ "default_branch": "master",
112
+ "stargazers": 0,
113
+ "master_branch": "master",
114
+ "organization": "apify-projects",
115
+ "custom_properties": {}
116
+ },
117
+ "pusher": {
118
+ "name": "lhotanok",
119
+ "email": "84460709+lhotanok@users.noreply.github.com"
120
+ },
121
+ "organization": {
122
+ "login": "apify-projects",
123
+ "id": 69005150,
124
+ "node_id": "MDEyOk9yZ2FuaXphdGlvbjY5MDA1MTUw",
125
+ "url": "https://api.github.com/orgs/apify-projects",
126
+ "repos_url": "https://api.github.com/orgs/apify-projects/repos",
127
+ "events_url": "https://api.github.com/orgs/apify-projects/events",
128
+ "hooks_url": "https://api.github.com/orgs/apify-projects/hooks",
129
+ "issues_url": "https://api.github.com/orgs/apify-projects/issues",
130
+ "members_url": "https://api.github.com/orgs/apify-projects/members{/member}",
131
+ "public_members_url": "https://api.github.com/orgs/apify-projects/public_members{/member}",
132
+ "avatar_url": "https://avatars.githubusercontent.com/u/69005150?v=4",
133
+ "description": "This organization houses projects that are not part of Apify itself, such as enterprise or marketplace developed actors and scrapers."
134
+ },
135
+ "sender": {
136
+ "login": "lhotanok",
137
+ "id": 84460709,
138
+ "node_id": "MDQ6VXNlcjg0NDYwNzA5",
139
+ "avatar_url": "https://avatars.githubusercontent.com/u/84460709?v=4",
140
+ "gravatar_id": "",
141
+ "url": "https://api.github.com/users/lhotanok",
142
+ "html_url": "https://github.com/lhotanok",
143
+ "followers_url": "https://api.github.com/users/lhotanok/followers",
144
+ "following_url": "https://api.github.com/users/lhotanok/following{/other_user}",
145
+ "gists_url": "https://api.github.com/users/lhotanok/gists{/gist_id}",
146
+ "starred_url": "https://api.github.com/users/lhotanok/starred{/owner}{/repo}",
147
+ "subscriptions_url": "https://api.github.com/users/lhotanok/subscriptions",
148
+ "organizations_url": "https://api.github.com/users/lhotanok/orgs",
149
+ "repos_url": "https://api.github.com/users/lhotanok/repos",
150
+ "events_url": "https://api.github.com/users/lhotanok/events{/privacy}",
151
+ "received_events_url": "https://api.github.com/users/lhotanok/received_events",
152
+ "type": "User",
153
+ "site_admin": false
154
+ },
155
+ "created": false,
156
+ "deleted": false,
157
+ "forced": false,
158
+ "base_ref": null,
159
+ "compare": "https://github.com/apify-projects/store-booking/compare/6b186d1fa8b6...591b7f55c7b8",
160
+ "commits": [
161
+ {
162
+ "id": "7f9e105e4ce7881f9def76db60bd8e4e58ead9f2",
163
+ "tree_id": "3f16d04f6d1cb44f8b90015a69168fdcc29621e1",
164
+ "distinct": true,
165
+ "message": "fix: A",
166
+ "timestamp": "2024-07-20T02:51:54+02:00",
167
+ "url": "https://github.com/apify-projects/store-booking/commit/591b7f55c7b8feb5012937f870d43266b0650118",
168
+ "author": {
169
+ "name": "Kristýna Lhoťanová",
170
+ "email": "84460709+lhotanok@users.noreply.github.com",
171
+ "username": "oklinov"
172
+ },
173
+ "committer": {
174
+ "name": "GitHub",
175
+ "email": "noreply@github.com",
176
+ "username": "web-flow"
177
+ }
178
+ },
179
+ {
180
+ "id": "3d1556141893eb3b7f281702e5cbec13d8814156",
181
+ "tree_id": "3f16d04f6d1cb44f8b90015a69168fdcc29621e1",
182
+ "distinct": true,
183
+ "message": "feat: A",
184
+ "timestamp": "2024-07-20T02:51:54+02:00",
185
+ "url": "https://github.com/apify-projects/store-booking/commit/591b7f55c7b8feb5012937f870d43266b0650118",
186
+ "author": {
187
+ "name": "Kristýna Lhoťanová",
188
+ "email": "84460709+lhotanok@users.noreply.github.com",
189
+ "username": "oklinov"
190
+ },
191
+ "committer": {
192
+ "name": "GitHub",
193
+ "email": "noreply@github.com",
194
+ "username": "web-flow"
195
+ }
196
+ }
197
+ ],
198
+ "head_commit": {
199
+ "id": "591b7f55c7b8feb5012937f870d43266b0650118",
200
+ "tree_id": "3f16d04f6d1cb44f8b90015a69168fdcc29621e1",
201
+ "distinct": true,
202
+ "message": "fix: validate rendered detail page locale (both language & currency) (#133)",
203
+ "timestamp": "2024-07-20T02:51:54+02:00",
204
+ "url": "https://github.com/apify-projects/store-booking/commit/591b7f55c7b8feb5012937f870d43266b0650118",
205
+ "author": {
206
+ "name": "Kristýna Lhoťanová",
207
+ "email": "84460709+lhotanok@users.noreply.github.com",
208
+ "username": "lhotanok"
209
+ },
210
+ "committer": {
211
+ "name": "GitHub",
212
+ "email": "noreply@github.com",
213
+ "username": "web-flow"
214
+ }
215
+ }
216
+ }
package/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { describe, testActor, ExpectStatic } from './lib/lib.js';
2
+ export { RunTestResult } from './lib/run-test-result.js';
package/lib/consts.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { ToFinishWithOptionsWithDefaults } from './types';
2
+
3
+ export const TO_FINISH_WITH_OPTIONS: ToFinishWithOptionsWithDefaults = {
4
+ status: 'SUCCEEDED',
5
+ duration: {
6
+ min: 600, // 0.6 sec
7
+ max: 600_000, // 10 min
8
+ },
9
+ failedRequests: 0,
10
+ requestsRetries: { max: 3 },
11
+ forbiddenLogs: [
12
+ 'ReferenceError',
13
+ 'TypeError',
14
+ ],
15
+ };
@@ -0,0 +1,268 @@
1
+ import type { Assertion, ExpectStatic } from 'vitest';
2
+
3
+ import { TO_FINISH_WITH_OPTIONS } from './consts.js';
4
+ import type { Interval, ToFinishWithOptions } from './types';
5
+ import { RunTestResult } from './run-test-result.js';
6
+
7
+ export const extendExpect = (expect: ExpectStatic): ExpectStatic => {
8
+ expect.extend({
9
+ toBeArray: (actual) => {
10
+ const pass = Array.isArray(actual);
11
+ return {
12
+ pass,
13
+ message: () => `Expected "${actual}" to be a array`,
14
+ };
15
+ },
16
+ toBeBoolean: (received) => {
17
+ const actual = typeof received;
18
+ const expected = 'boolean';
19
+ return {
20
+ pass: actual === expected,
21
+ message: () => `Expected "${actual}" to be a boolean`,
22
+ expected: 'boolean',
23
+ };
24
+ },
25
+ toBeEmptyArray: (actual) => {
26
+ const pass = Array.isArray(actual) && actual.length === 0;
27
+ return {
28
+ pass,
29
+ message: () => `Expected ${actual} to be an empty array`,
30
+ actual,
31
+ expected: [],
32
+ };
33
+ },
34
+ toBeNonEmptyArray: (actual) => {
35
+ const pass = Array.isArray(actual) && actual.length > 0;
36
+ return {
37
+ pass,
38
+ message: () => `Expected ${actual} to be a non-empty array`,
39
+ };
40
+ },
41
+ toBeNonEmptyString: (actual) => {
42
+ const pass = typeof actual === 'string' && actual.length > 0;
43
+ return {
44
+ pass,
45
+ message: () => `Expected "${actual}" to be a non-empty string`,
46
+ };
47
+ },
48
+ toBeNumber: (actual) => {
49
+ const pass = typeof actual === 'number';
50
+ return {
51
+ pass,
52
+ message: () => `Expected "${actual}" to be a number`,
53
+ };
54
+ },
55
+ toBeNonEmptyObject: (actual) => {
56
+ const isObject = typeof actual === 'object' && !Array.isArray(actual);
57
+ const isNonEmpty = Object.entries(actual).length > 0;
58
+ const pass = isObject && isNonEmpty;
59
+ return {
60
+ pass,
61
+ message: () => `Expected "${actual}" to be a non-emtpy object`,
62
+ };
63
+ },
64
+ toBeObject: (actual) => {
65
+ const pass = typeof actual === 'object' && !Array.isArray(actual);
66
+ return {
67
+ pass,
68
+ message: () => `Expected "${actual}" to be an object`,
69
+ };
70
+ },
71
+ toBeString: (received) => {
72
+ const actual = typeof received;
73
+ const expected = 'string';
74
+ return {
75
+ pass: actual === expected,
76
+ message: () => `Expected "${actual}" to be a string`,
77
+ actual,
78
+ expected,
79
+ };
80
+ },
81
+ toBeTrue: (actual) => {
82
+ const pass = actual === true;
83
+ return {
84
+ pass,
85
+ message: () => `Expected "${actual}" to be true`,
86
+ expected: true,
87
+ actual,
88
+ };
89
+ },
90
+ toBeFalse: (actual) => {
91
+ const pass = actual === false;
92
+ return {
93
+ pass,
94
+ message: () => `Expected "${actual}" to be false`,
95
+ };
96
+ },
97
+ toBeWholeNumber: (received) => {
98
+ const pass = Number.isInteger(received);
99
+ return {
100
+ pass,
101
+ message: () => `Expected "${received}" to be a whole number`,
102
+ };
103
+ },
104
+ toBeWithinRange: (received, lower, upper) => {
105
+ const pass = received >= lower && received <= upper;
106
+ return {
107
+ pass,
108
+ message: () => `Expected ${received} to be within range <${lower}, ${upper}>`,
109
+ };
110
+ },
111
+ toFinishWith: async <PpeEvent extends string>(
112
+ received: RunTestResult,
113
+ userOptions: ToFinishWithOptions<PpeEvent>,
114
+ ) => {
115
+ const options = {
116
+ ...TO_FINISH_WITH_OPTIONS,
117
+ ...userOptions,
118
+ };
119
+
120
+ const diffs: Diffs = {
121
+ pass: true,
122
+ actual: ['Run:'],
123
+ expected: ['Run:'],
124
+ };
125
+ {
126
+ const expected = options?.status;
127
+ const actual = received.status;
128
+ diffs.pass = expected === actual;
129
+ diffs.actual.push(`status=${actual}`);
130
+ diffs.expected.push(`status=${expected}`);
131
+ }
132
+
133
+ const {
134
+ chargedEventCounts,
135
+ stats: { durationMillis },
136
+ } = await received.getRunInfo();
137
+ const datasetItemCount = (await received.getDataset()).items.length;
138
+ const stats = (await received.getStatistics());
139
+
140
+ isWithinInterval(diffs, datasetItemCount, options, 'datasetItemCount');
141
+ isWithinInterval(diffs, durationMillis, options, 'duration');
142
+ isWithinInterval(diffs, stats?.requestsFailed, options, 'failedRequests');
143
+ isWithinInterval(diffs, stats?.requestsRetries, options, 'requestsRetries');
144
+
145
+ const ppeDiffs: Diffs = {
146
+ pass: true,
147
+ actual: ['PPE Events:'],
148
+ expected: ['PPE Events:'],
149
+ };
150
+
151
+ if (options.chargedEventCounts) {
152
+ const expected = options.chargedEventCounts;
153
+ const actual = chargedEventCounts ?? {};
154
+ const uniquePpeEvents = new Set([
155
+ ...Array.from(Object.keys(expected)),
156
+ ...Array.from(Object.keys(actual)),
157
+ ]);
158
+
159
+ for (const ppeEvent of uniquePpeEvents) {
160
+ if (!(ppeEvent in expected)) {
161
+ isWithinInterval(
162
+ ppeDiffs,
163
+ actual[ppeEvent],
164
+ { [ppeEvent]: 0 },
165
+ ppeEvent,
166
+ );
167
+ continue;
168
+ }
169
+ if (!(ppeEvent in actual)) {
170
+ ppeDiffs.pass = false;
171
+ ppeDiffs.actual.push(`${ppeEvent}=X`);
172
+ ppeDiffs.expected.push(`${ppeEvent}=${expected[ppeEvent as PpeEvent]}`);
173
+ continue;
174
+ }
175
+ isWithinInterval(ppeDiffs, actual[ppeEvent], expected, ppeEvent as PpeEvent);
176
+ }
177
+
178
+ diffs.pass = ppeDiffs.pass;
179
+ diffs.actual.push(ppeDiffs.actual.join('\n '));
180
+ diffs.expected.push(ppeDiffs.expected.join('\n '));
181
+ }
182
+
183
+ {
184
+ const { forbiddenLogs } = options;
185
+ const log = await received.getLog();
186
+ const occuredLogs = [];
187
+ for (const forbiddenLog of forbiddenLogs) {
188
+ if (log.includes(forbiddenLog)) {
189
+ occuredLogs.push(forbiddenLog);
190
+ }
191
+ }
192
+ if (occuredLogs.length > 0) {
193
+ diffs.pass = false;
194
+ diffs.actual.push(` logs=[${occuredLogs.join(', ')}]`);
195
+ diffs.expected.push(` logs=[]`);
196
+ }
197
+ }
198
+
199
+ return {
200
+ pass: diffs.pass,
201
+ message: () => `Run ${received.id} didn't finish as expected`,
202
+ actual: diffs.actual.join('\n '),
203
+ expected: diffs.expected.join('\n '),
204
+ };
205
+ },
206
+ toStartWith: (received, prefix) => {
207
+ const pass = received?.startsWith(prefix);
208
+ return {
209
+ pass,
210
+ message: () => `Expected "${received}" to start with "${prefix}"`,
211
+ };
212
+ },
213
+ });
214
+
215
+ // here we are switching logic of expect and expect.soft
216
+ // expect => expect.soft
217
+ // expect.hard => expect
218
+ const { soft } = expect;
219
+ const oldExpect = { ...expect };
220
+ const softExpect = <T>(actual: T, message?: string): Assertion => {
221
+ return soft(actual, message);
222
+ };
223
+
224
+ for (const [key, value] of Object.entries(oldExpect)) {
225
+ // @ts-expect-error: No idea how to type this properly
226
+ softExpect[key] = value;
227
+ }
228
+
229
+ softExpect.hard = <T>(actual: T, message?: string): Assertion<T> => {
230
+ return expect(actual, message);
231
+ };
232
+
233
+ return softExpect as unknown as ExpectStatic;
234
+ };
235
+
236
+ type Diffs = {
237
+ pass: boolean
238
+ actual: string[]
239
+ expected: string[]
240
+
241
+ }
242
+
243
+ const isWithinInterval = <T extends string>(
244
+ diffs: Diffs,
245
+ actual: number | undefined,
246
+ options: Record<T, Interval | null>,
247
+ intervalOption: T,
248
+ ) => {
249
+ const expected = options[intervalOption];
250
+ if (expected === null) {
251
+ // check is disabled if expected value is null
252
+ return;
253
+ }
254
+ if (typeof expected === 'number') {
255
+ if (actual !== expected) {
256
+ diffs.pass = false;
257
+ diffs.actual.push(`${intervalOption}=${actual}`);
258
+ diffs.expected.push(`${intervalOption}=${expected}`);
259
+ }
260
+ } else if (typeof expected === 'object') {
261
+ const { min, max } = expected;
262
+ if (actual === undefined || (min !== undefined && actual < min) || (max !== undefined && actual > max)) {
263
+ diffs.pass = false;
264
+ diffs.actual.push(`${intervalOption}=${actual}`);
265
+ diffs.expected.push(`${intervalOption}=<${min ?? ''},${max ?? ''}>`);
266
+ }
267
+ }
268
+ };
package/lib/lib.ts ADDED
@@ -0,0 +1,134 @@
1
+ import { ApifyClient } from 'apify-client';
2
+ import {
3
+ describe as vitestDescribe,
4
+ ExpectStatic,
5
+ TestFunction,
6
+ test as vitestTest,
7
+ SuiteFactory,
8
+ TestOptions,
9
+ TestContext,
10
+ } from 'vitest';
11
+ import type { ActorBuild, RunOptions } from './types';
12
+ import { RunTestResult } from './run-test-result.js';
13
+ import { extendExpect } from './extend-expect.js';
14
+ import { getActorPrefilledInput } from './utils.js';
15
+
16
+ const ACTOR_BUILDS = 'ACTOR_BUILDS';
17
+ let actorBuilds: ActorBuild[] = [];
18
+ try {
19
+ const actorBuildsEnv = process.env[ACTOR_BUILDS];
20
+ if (actorBuildsEnv) {
21
+ actorBuilds = JSON.parse(actorBuildsEnv);
22
+ if (!Array.isArray(actorBuilds)) {
23
+ throw new Error(`${ACTOR_BUILDS} env variable should contain a JSON array, got ${typeof actorBuilds}`);
24
+ }
25
+ }
26
+ } catch (err) {
27
+ throw new Error(`Failed to parse actor builds: ${err}`);
28
+ }
29
+
30
+ const config = actorBuilds.reduce((map, cfg) => {
31
+ map.set(cfg.actorName, cfg);
32
+ map.set(cfg.actorId, cfg);
33
+ return map;
34
+ }, new Map<string, ActorBuild>());
35
+
36
+ export { ExpectStatic };
37
+
38
+ const { TESTER_APIFY_TOKEN, RUN_PLATFORM_TESTS, RUN_ALL_PLATFORM_TESTS } = process.env;
39
+ const apifyClient = new ApifyClient({ token: TESTER_APIFY_TOKEN });
40
+
41
+ const DEFAULT_TEST_OPTIONS: TestOptions = {
42
+ // we want to run tests concurrently
43
+ concurrent: true,
44
+ // test should finish within 1 hour
45
+ timeout: 60_000 * 60,
46
+ };
47
+
48
+ export const describe = (
49
+ name: string,
50
+ fn?: SuiteFactory<object>,
51
+ options: TestOptions = DEFAULT_TEST_OPTIONS,
52
+ ) => {
53
+ vitestDescribe.runIf(!!RUN_PLATFORM_TESTS || !!RUN_ALL_PLATFORM_TESTS)(name, options, fn);
54
+ };
55
+
56
+ const DEFAULT_TEST_ACTOR_OPTIONS: TestOptions = {
57
+ retry: 1,
58
+ };
59
+
60
+ export const testActor = <T>(
61
+ actorName: string,
62
+ testName: string,
63
+ fn: TestFunction<{ run: ReturnType<typeof createStartRunFn<T>> }>,
64
+ testOptions?: TestOptions,
65
+ ) => {
66
+ const options = {
67
+ ...DEFAULT_TEST_ACTOR_OPTIONS,
68
+ ...testOptions,
69
+ };
70
+ const name = `${actorName}: ${testName}`;
71
+ const shouldRun = !!RUN_ALL_PLATFORM_TESTS || config.has(actorName);
72
+
73
+ vitestTest.runIf(shouldRun)(name, options, async (context) => {
74
+ const { expect, ...rest } = context;
75
+ await fn({
76
+ expect: extendExpect(expect),
77
+ run: createStartRunFn(actorName, context),
78
+ ...rest,
79
+ });
80
+ });
81
+ };
82
+
83
+ export const testTestActor = <T>(
84
+ testName: string,
85
+ fn: TestFunction<{ run: ReturnType<typeof createStartRunFn<T>> }>,
86
+ ) => {
87
+ vitestTest(testName, async (context) => {
88
+ const { expect, ...rest } = context;
89
+ await fn({
90
+ expect: extendExpect(expect),
91
+ // @ts-expect-error: this just to test custom matchers
92
+ run: () => { },
93
+ ...rest,
94
+ });
95
+ });
96
+ };
97
+
98
+ export const it = testActor;
99
+
100
+ const createStartRunFn = <T>(actorNameOrId: string, { annotate, task }: TestContext) => {
101
+ const actorConfig = config.get(actorNameOrId);
102
+ const build = actorConfig?.buildNumber;
103
+ const buildId = actorConfig?.buildId;
104
+ return async (runOptions: RunOptions<T>) => {
105
+ const {
106
+ input,
107
+ options,
108
+ prefilledInput,
109
+ } = runOptions;
110
+ const actor = apifyClient.actor(actorNameOrId);
111
+
112
+ const actorInput = {
113
+ ...(prefilledInput && await getActorPrefilledInput(apifyClient, actorNameOrId, buildId)),
114
+ ...input,
115
+ };
116
+ const run = await actor.call(
117
+ actorInput,
118
+ { build, ...options, },
119
+ );
120
+
121
+ const runLink = `https://console.apify.com/view/runs/${run.id}`;
122
+ await annotate(runLink, 'run_link');
123
+ // @ts-expect-error: `TaskMeta` cannot be retyped
124
+ task.meta = {
125
+ runId: run.id,
126
+ runLink,
127
+ };
128
+
129
+ // waiting for datasetItemCount and chargedEventCounts to sync
130
+ await new Promise((resolve) => setTimeout(resolve, 10_000));
131
+
132
+ return new RunTestResult(apifyClient, run);
133
+ };
134
+ };