apify-test-tools 0.6.0 → 0.6.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 (108) hide show
  1. package/.github/workflows/_check_code.yaml +17 -19
  2. package/.github/workflows/_update_release_metadata.yaml +1 -1
  3. package/.github/workflows/manual_release_stable.yaml +1 -0
  4. package/.github/workflows/on_master.yaml +2 -0
  5. package/.husky/pre-commit +13 -0
  6. package/CHANGELOG.md +11 -3
  7. package/CONTRIBUTING.md +5 -0
  8. package/README.md +39 -43
  9. package/bin/build.ts +138 -87
  10. package/bin/diff-changes.ts +156 -0
  11. package/bin/diff-json-schema.ts +50 -0
  12. package/bin/git.ts +42 -11
  13. package/bin/github.ts +7 -6
  14. package/bin/main.ts +21 -11
  15. package/bin/slack.ts +6 -2
  16. package/bin/test-report.ts +55 -44
  17. package/bin/types.ts +56 -59
  18. package/bin/utils.ts +19 -131
  19. package/dist/bin/build.d.ts +1 -1
  20. package/dist/bin/build.d.ts.map +1 -1
  21. package/dist/bin/build.js +49 -31
  22. package/dist/bin/build.js.map +1 -1
  23. package/dist/bin/diff-changes.d.ts +16 -0
  24. package/dist/bin/diff-changes.d.ts.map +1 -0
  25. package/dist/bin/diff-changes.js +103 -0
  26. package/dist/bin/diff-changes.js.map +1 -0
  27. package/dist/bin/diff-json-schema.d.ts +7 -0
  28. package/dist/bin/diff-json-schema.d.ts.map +1 -0
  29. package/dist/bin/diff-json-schema.js +45 -0
  30. package/dist/bin/diff-json-schema.js.map +1 -0
  31. package/dist/bin/git.d.ts +8 -3
  32. package/dist/bin/git.d.ts.map +1 -1
  33. package/dist/bin/git.js +35 -10
  34. package/dist/bin/git.js.map +1 -1
  35. package/dist/bin/github.d.ts +1 -3
  36. package/dist/bin/github.d.ts.map +1 -1
  37. package/dist/bin/github.js +5 -5
  38. package/dist/bin/github.js.map +1 -1
  39. package/dist/bin/main.js +14 -5
  40. package/dist/bin/main.js.map +1 -1
  41. package/dist/bin/slack.d.ts +1 -1
  42. package/dist/bin/slack.d.ts.map +1 -1
  43. package/dist/bin/slack.js +1 -1
  44. package/dist/bin/slack.js.map +1 -1
  45. package/dist/bin/test-report.d.ts.map +1 -1
  46. package/dist/bin/test-report.js +9 -3
  47. package/dist/bin/test-report.js.map +1 -1
  48. package/dist/bin/types.d.ts +0 -1
  49. package/dist/bin/types.d.ts.map +1 -1
  50. package/dist/bin/utils.d.ts +0 -14
  51. package/dist/bin/utils.d.ts.map +1 -1
  52. package/dist/bin/utils.js +12 -95
  53. package/dist/bin/utils.js.map +1 -1
  54. package/dist/lib/extend-expect.d.ts.map +1 -1
  55. package/dist/lib/extend-expect.js +11 -11
  56. package/dist/lib/extend-expect.js.map +1 -1
  57. package/dist/lib/lib.d.ts +3 -2
  58. package/dist/lib/lib.d.ts.map +1 -1
  59. package/dist/lib/lib.js +7 -3
  60. package/dist/lib/lib.js.map +1 -1
  61. package/dist/lib/run-test-result.d.ts +1 -1
  62. package/dist/lib/run-test-result.d.ts.map +1 -1
  63. package/dist/lib/run-test-result.js.map +1 -1
  64. package/dist/lib/types.d.ts +2 -3
  65. package/dist/lib/types.d.ts.map +1 -1
  66. package/dist/lib/utils.d.ts +1 -1
  67. package/dist/lib/utils.d.ts.map +1 -1
  68. package/dist/lib/utils.js +4 -1
  69. package/dist/lib/utils.js.map +1 -1
  70. package/dist/test/unit/bin/diff-changes.test.d.ts +2 -0
  71. package/dist/test/unit/bin/diff-changes.test.d.ts.map +1 -0
  72. package/dist/test/unit/bin/diff-changes.test.js +159 -0
  73. package/dist/test/unit/bin/diff-changes.test.js.map +1 -0
  74. package/dist/test/unit/bin/diff.test.d.ts +2 -0
  75. package/dist/test/unit/bin/diff.test.d.ts.map +1 -0
  76. package/dist/test/unit/bin/diff.test.js +143 -0
  77. package/dist/test/unit/bin/diff.test.js.map +1 -0
  78. package/dist/test/unit/bin/git.test.js +48 -19
  79. package/dist/test/unit/bin/git.test.js.map +1 -1
  80. package/dist/test/unit/custom-matchers.test.js +3 -8
  81. package/dist/test/unit/custom-matchers.test.js.map +1 -1
  82. package/dist/test/unit/parse-commit.test.js +1 -1
  83. package/dist/test/unit/parse-commit.test.js.map +1 -1
  84. package/dist/test/unit/should-built-and-test.test.js +159 -39
  85. package/dist/test/unit/should-built-and-test.test.js.map +1 -1
  86. package/dist/tsconfig.tsbuildinfo +1 -1
  87. package/eslint.config.mjs +4 -2
  88. package/knip.json +1 -1
  89. package/lib/extend-expect.ts +21 -21
  90. package/lib/lib.ts +17 -17
  91. package/lib/run-test-result.ts +6 -2
  92. package/lib/types.ts +5 -8
  93. package/lib/utils.ts +13 -9
  94. package/package.json +10 -3
  95. package/test/tsconfig.json +2 -4
  96. package/test/unit/bin/diff-changes.test.ts +182 -0
  97. package/test/unit/bin/diff.test.ts +212 -0
  98. package/test/unit/bin/git.test.ts +59 -20
  99. package/test/unit/custom-matchers.test.ts +10 -9
  100. package/test/unit/parse-commit.test.ts +2 -1
  101. package/test/unit/should-built-and-test.test.ts +181 -40
  102. package/bin/consts.ts +0 -6
  103. package/dist/bin/consts.d.ts +0 -4
  104. package/dist/bin/consts.d.ts.map +0 -1
  105. package/dist/bin/consts.js +0 -6
  106. package/dist/bin/consts.js.map +0 -1
  107. package/example-github-events/pull-request.json +0 -523
  108. package/example-github-events/push.json +0 -216
package/eslint.config.mjs CHANGED
@@ -6,7 +6,7 @@ import tsEslint from 'typescript-eslint';
6
6
 
7
7
  // eslint-disable-next-line import/no-default-export
8
8
  export default [
9
- { ignores: ['**/dist', 'eslint.config.mjs'] },
9
+ { ignores: ['**/dist', 'eslint.config.mjs', '.github'] },
10
10
  ...apify,
11
11
  prettier,
12
12
  {
@@ -25,6 +25,8 @@ export default [
25
25
  },
26
26
  rules: {
27
27
  'no-console': 0,
28
+ // This was used heavily, I don't have string opinion so turning it off for now, feel free to refactor later
29
+ 'no-use-before-define': 'off',
28
30
  },
29
31
  },
30
- ];
32
+ ];
package/knip.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
2
  "$schema": "https://unpkg.com/knip@5/schema.json"
3
- }
3
+ }
@@ -1,8 +1,8 @@
1
1
  import type { Assertion, ExpectStatic } from 'vitest';
2
2
 
3
3
  import { TO_FINISH_WITH_OPTIONS } from './consts.js';
4
+ import type { RunTestResult } from './run-test-result.js';
4
5
  import type { Interval, ToFinishWithOptions } from './types.js';
5
- import { RunTestResult } from './run-test-result.js';
6
6
 
7
7
  export const extendExpect = (expect: ExpectStatic): ExpectStatic => {
8
8
  expect.extend({
@@ -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,12 +146,12 @@ 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
153
  failedAssertions.push(
154
- `Failed ${label} check, expected ${JSON.stringify(options[key])}, got ${value}.`
154
+ `Failed ${label} check, expected ${JSON.stringify(options[key])}, got ${value}.`,
155
155
  );
156
156
  }
157
157
  };
@@ -161,17 +161,15 @@ export const extendExpect = (expect: ExpectStatic): ExpectStatic => {
161
161
  checkInterval(stats?.requestsFailed, 'failedRequests', 'failed requests');
162
162
  checkInterval(stats?.requestsRetries, 'requestsRetries', 'requests retries');
163
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
- }
164
+ if (options.maxRetriesPerRequest !== null) {
165
+ const maxRetriesPerRequestObserved = (stats?.requestRetryHistogram ?? [0]).length - 1;
166
+ if (maxRetriesPerRequestObserved > options.maxRetriesPerRequest) {
167
+ diffs.pass = false;
168
+ diffs.actual.push(`maxRetriesPerRequest=${maxRetriesPerRequestObserved}`);
169
+ diffs.expected.push(`maxRetriesPerRequest<=${options.maxRetriesPerRequest}`);
170
+ failedAssertions.push(
171
+ `Failed max retries observed check, expected <=${options.maxRetriesPerRequest}, got ${maxRetriesPerRequestObserved}.`,
172
+ );
175
173
  }
176
174
  }
177
175
  const ppeDiffs: Diffs = {
@@ -208,8 +206,8 @@ export const extendExpect = (expect: ExpectStatic): ExpectStatic => {
208
206
  diffs.expected.push(ppeDiffs.expected.join('\n '));
209
207
  failedAssertions.push(
210
208
  `Failed PPE event counts check, expected ${JSON.stringify(
211
- options.chargedEventCounts
212
- )}, got ${JSON.stringify(chargedEventCounts)}.`
209
+ options.chargedEventCounts,
210
+ )}, got ${JSON.stringify(chargedEventCounts)}.`,
213
211
  );
214
212
  }
215
213
  }
@@ -228,7 +226,7 @@ export const extendExpect = (expect: ExpectStatic): ExpectStatic => {
228
226
  diffs.actual.push(` logs=[${occuredLogs.join(', ')}]`);
229
227
  diffs.expected.push(` logs=[]`);
230
228
  failedAssertions.push(
231
- `Failed forbidden logs check, expected [] but got [${occuredLogs.join(', ')}].`
229
+ `Failed forbidden logs check, expected [] but got [${occuredLogs.join(', ')}].`,
232
230
  );
233
231
  }
234
232
  }
@@ -280,15 +278,16 @@ const isWithinInterval = <T extends string>(
280
278
  diffs: Diffs,
281
279
  actual: number | undefined,
282
280
  options: Record<T, Interval | null>,
283
- intervalOption: T
281
+ intervalOption: T,
284
282
  ) => {
285
283
  const expected = options[intervalOption];
286
284
  if (expected === null) {
287
285
  // check is disabled if expected value is null
288
- return;
286
+ return undefined;
289
287
  }
290
288
  if (typeof expected === 'number') {
291
289
  if (actual !== expected) {
290
+ // eslint-disable-next-line no-param-reassign --- Don't want to touch this code :)
292
291
  diffs.pass = false;
293
292
  diffs.actual.push(`${intervalOption}=${actual}`);
294
293
  diffs.expected.push(`${intervalOption}=${expected}`);
@@ -297,11 +296,12 @@ const isWithinInterval = <T extends string>(
297
296
  } else if (typeof expected === 'object') {
298
297
  const { min, max } = expected;
299
298
  if (actual === undefined || (min !== undefined && actual < min) || (max !== undefined && actual > max)) {
299
+ // eslint-disable-next-line no-param-reassign
300
300
  diffs.pass = false;
301
301
  diffs.actual.push(`${intervalOption}=${actual}`);
302
302
  diffs.expected.push(`${intervalOption}=<${min ?? ''},${max ?? ''}>`);
303
303
  return false;
304
304
  }
305
305
  }
306
- return;
306
+ return undefined;
307
307
  };
package/lib/lib.ts CHANGED
@@ -1,15 +1,11 @@
1
- import { Actor, ActorRun, ActorRunListItem, ActorStandby, ApifyClient, Task } from 'apify-client';
2
- import {
3
- describe as vitestDescribe,
4
- ExpectStatic,
5
- TestFunction,
6
- test as vitestTest,
7
- SuiteFactory,
8
- TestContext,
9
- } from 'vitest';
10
- import type { ActorBuild, ActorTestOptions, RunOptions } from './types.js';
11
- import { RunTestResult } from './run-test-result.js';
1
+ import type { Actor, ActorRun, ActorRunListItem, ActorStandby, Task } from 'apify-client';
2
+ import { ApifyClient } from 'apify-client';
3
+ import type { SuiteFactory, TestContext, TestFunction } from 'vitest';
4
+ import { describe as vitestDescribe, ExpectStatic, test as vitestTest } from 'vitest';
5
+
12
6
  import { extendExpect } from './extend-expect.js';
7
+ import { RunTestResult } from './run-test-result.js';
8
+ import type { ActorBuild, ActorTestOptions, RunOptions } from './types.js';
13
9
  import { getActorPrefilledInput, sleep } from './utils.js';
14
10
 
15
11
  const ACTOR_BUILDS = 'ACTOR_BUILDS';
@@ -56,7 +52,7 @@ export const testActor = <T>(
56
52
  actorName: string,
57
53
  testName: string,
58
54
  fn: TestFunction<{ run: ReturnType<typeof createStartRunFn<T>> }>,
59
- testOptions?: ActorTestOptions
55
+ testOptions?: ActorTestOptions,
60
56
  ) => {
61
57
  const options = {
62
58
  ...DEFAULT_TEST_ACTOR_OPTIONS,
@@ -64,7 +60,7 @@ export const testActor = <T>(
64
60
  };
65
61
  const name = `${actorName}: ${testName}`;
66
62
  const shouldRun = !!RUN_ALL_PLATFORM_TESTS || config.has(actorName);
67
- vitestTest.runIf(shouldRun)(name, options, async <T extends TestContext>(context: T) => {
63
+ vitestTest.runIf(shouldRun)(name, options, async <TYPE extends TestContext>(context: TYPE) => {
68
64
  const { expect, ...rest } = context;
69
65
  await fn({
70
66
  expect: extendExpect(expect),
@@ -80,11 +76,12 @@ export const testActor = <T>(
80
76
  *
81
77
  * Using task is just current shortcoming of standby feature but ideally we would use Actor directly
82
78
  */
79
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
80
  export const testStandbyActor = <I = any, O = any>(
84
81
  actorName: string,
85
82
  testName: string,
86
83
  fn: TestFunction<{ callStandby: ReturnType<typeof createStartStandbyFn<I, O>> }>,
87
- testOptions?: ActorTestOptions
84
+ testOptions?: ActorTestOptions,
88
85
  ) => {
89
86
  const options = {
90
87
  ...DEFAULT_TEST_ACTOR_OPTIONS,
@@ -105,7 +102,9 @@ export const testStandbyActor = <I = any, O = any>(
105
102
  callStandby: createStartStandbyFn(standbyTask),
106
103
  ...rest,
107
104
  });
108
- } catch {}
105
+ } catch {
106
+ /* */
107
+ }
109
108
 
110
109
  const { taskId } = standbyTask;
111
110
  const runs = (await apifyClient.task(taskId).runs().list()).items;
@@ -122,13 +121,14 @@ export const testStandbyActor = <I = any, O = any>(
122
121
 
123
122
  export const testTestActor = <T>(
124
123
  testName: string,
125
- fn: TestFunction<{ run: ReturnType<typeof createStartRunFn<T>> }>
124
+ fn: TestFunction<{ run: ReturnType<typeof createStartRunFn<T>> }>,
126
125
  ) => {
127
126
  vitestTest(testName, async (context) => {
128
127
  const { expect, ...rest } = context;
129
128
  await fn({
130
129
  expect: extendExpect(expect),
131
130
  // @ts-expect-error: this just to test custom matchers
131
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
132
132
  run: () => {},
133
133
  ...rest,
134
134
  });
@@ -191,7 +191,7 @@ const createStandbyTask = async (actorNameOrId: string, buildNumber?: string): P
191
191
  throw new Error(`Actor "${actorNameOrId} doesn't contain actorStandby options`);
192
192
  }
193
193
  const { isEnabled, ...defaultActorStandby } = actorInfo.actorStandby;
194
- delete defaultActorStandby.disableStandbyFieldsOverride
194
+ delete defaultActorStandby.disableStandbyFieldsOverride;
195
195
 
196
196
  const build = buildNumber ?? defaultActorStandby.build;
197
197
 
@@ -1,4 +1,5 @@
1
- import { ActorRun, ApifyClient, KeyValueStoreClient } from 'apify-client';
1
+ import type { ActorRun, ApifyClient, KeyValueStoreClient } from 'apify-client';
2
+
2
3
  import type { Dataset, SdkCrawlerStatistics } from './types.js';
3
4
 
4
5
  export class RunTestResult {
@@ -9,7 +10,10 @@ export class RunTestResult {
9
10
  private runInfo: ActorRun | undefined;
10
11
  private input: unknown | undefined;
11
12
 
12
- constructor(private readonly apifyClient: ApifyClient, private readonly run: ActorRun) {
13
+ constructor(
14
+ private readonly apifyClient: ApifyClient,
15
+ private readonly run: ActorRun,
16
+ ) {
13
17
  /**/
14
18
  }
15
19
 
package/lib/types.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { ActorCallOptions } from 'apify-client';
2
- import { Assertion, TestOptions } from 'vitest';
1
+ import type { ActorCallOptions } from 'apify-client';
2
+ import type { Assertion, TestOptions } from 'vitest';
3
3
 
4
4
  export type ActorBuild = {
5
5
  buildId: string;
@@ -50,10 +50,6 @@ export type ToFinishWithOptionsWithDefaults = {
50
50
  forbiddenLogs: string[];
51
51
  maxRetriesPerRequest: number | null;
52
52
  };
53
- export type IntervalOption<PpeEvent extends string> = keyof Pick<
54
- ToFinishWithOptions<PpeEvent>,
55
- 'datasetItemCount' | 'requestsRetries' | 'failedRequests' | 'duration'
56
- >;
57
53
 
58
54
  export type ToFinishWithOptions<PpeEvent extends string> = Partial<ToFinishWithOptionsWithDefaults> & {
59
55
  datasetItemCount: Interval;
@@ -144,9 +140,10 @@ export type ActorTestOptions = Omit<TestOptions, 'retry'> & {
144
140
  };
145
141
 
146
142
  declare module 'vitest' {
147
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
143
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type
148
144
  interface Assertion<T = any> extends ActorMatchers<T> {}
149
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type
150
146
  interface Matchers<T = any> extends ActorMatchers<T> {}
147
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
151
148
  interface AsymmetricMatchersContaining extends ActorMatchers {}
152
149
  }
package/lib/utils.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { ActorRun, ApifyClient } from 'apify-client';
2
- import { TestContext } from 'vitest';
1
+ import type { ApifyClient } from 'apify-client';
3
2
 
4
3
  /**
5
4
  * Gets prefilled values for a provided build or, if not provided, uses actor's
@@ -16,26 +15,29 @@ export const getActorPrefilledInput = async (
16
15
  const defaultBuildTag = actorInfo?.defaultRunOptions.build;
17
16
 
18
17
  const taggedBuild = actorInfo?.taggedBuilds?.[defaultBuildTag || ''];
18
+ // eslint-disable-next-line no-param-reassign --- I think here it is cleaner than creating dummy variable
19
19
  buildId = taggedBuild?.buildId;
20
20
 
21
21
  if (!buildId) {
22
22
  console.error(`Coudn't find default build for actor ${actorNameOrId}. Prefilled values will not be used.`);
23
- return {}
23
+ return {};
24
24
  }
25
25
  }
26
26
 
27
27
  const buildInfo = await apifyClient.build(buildId).get();
28
28
 
29
- const inputSchema = buildInfo?.actorDefinition?.input as {
30
- properties: Record<string, { prefill?: unknown }>
31
- } | undefined
29
+ const inputSchema = buildInfo?.actorDefinition?.input as
30
+ | {
31
+ properties: Record<string, { prefill?: unknown }>;
32
+ }
33
+ | undefined;
32
34
 
33
35
  if (!inputSchema) {
34
36
  console.error(
35
37
  `Coudn't find input schema definition for actor ${actorNameOrId}, build ${buildId}.`,
36
38
  'Prefilled values will not be used',
37
39
  );
38
- return {}
40
+ return {};
39
41
  }
40
42
 
41
43
  const prefill: Record<string, unknown> = {};
@@ -50,5 +52,7 @@ export const getActorPrefilledInput = async (
50
52
  };
51
53
 
52
54
  export const sleep = async (ms: number) => {
53
- await new Promise((resolve) => setTimeout(resolve, ms));
54
- }
55
+ await new Promise((resolve) => {
56
+ setTimeout(resolve, ms);
57
+ });
58
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apify-test-tools",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "TBD",
6
6
  "repository": {
@@ -34,7 +34,9 @@
34
34
  "typescript": "^5.9.3",
35
35
  "typescript-eslint": "^8.34.1",
36
36
  "vitest": "^3.2.4",
37
- "knip": "^5.65.0"
37
+ "knip": "^5.65.0",
38
+ "husky": "^9.0.11",
39
+ "lint-staged": "^15.2.2"
38
40
  },
39
41
  "peerDependencies": {
40
42
  "vitest": ">=3.2.4"
@@ -49,7 +51,12 @@
49
51
  "format:check": "prettier --check .",
50
52
  "type-check": "tsc --noEmit",
51
53
  "test": "vitest run",
52
- "check-unused": "npx knip"
54
+ "check-unused": "npx knip",
55
+ "prepare": "husky .husky || true"
56
+ },
57
+ "lint-staged": {
58
+ "*.js": "eslint",
59
+ "*.ts": "eslint"
53
60
  },
54
61
  "author": "oklinov",
55
62
  "license": "ISC"
@@ -7,9 +7,7 @@
7
7
  "noUnusedLocals": false,
8
8
  "noEmit": true,
9
9
  "lib": ["DOM", "es2021"],
10
- "skipLibCheck": true,
10
+ "skipLibCheck": true
11
11
  },
12
- "include": [
13
- "./**/*",
14
- ]
12
+ "include": ["./**/*"]
15
13
  }
@@ -0,0 +1,182 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { getChangedActors, maybeParseActorFolder } from '../../../bin/diff-changes.js';
4
+ import * as DiffJsonSchema from '../../../bin/diff-json-schema.js';
5
+ import type { ActorConfig } from '../../../bin/types.js';
6
+
7
+ const miniActor: ActorConfig = { actorName: 'foo/bar', folder: 'actors/foo_bar', isStandalone: false };
8
+ const standaloneActor: ActorConfig = {
9
+ actorName: 'standalone',
10
+ folder: 'standalone-actors/standalone',
11
+ isStandalone: true,
12
+ };
13
+ const actorConfigs = [miniActor, standaloneActor];
14
+
15
+ const commits = [{ sha: 'Commit1', author: '', date: '', message: '' }];
16
+
17
+ describe('maybeParseActorFolder', () => {
18
+ it('returns actorName for actors/ path', () => {
19
+ expect(maybeParseActorFolder('actors/foo_bar/actor.json')).toEqual({
20
+ isActorFolder: true,
21
+ actorName: 'foo/bar',
22
+ });
23
+ });
24
+
25
+ it('returns actorName for standalone-actors/ path', () => {
26
+ expect(maybeParseActorFolder('standalone-actors/my_actor/main.ts')).toEqual({
27
+ isActorFolder: true,
28
+ actorName: 'my/actor',
29
+ });
30
+ });
31
+
32
+ it('returns false for top-level file', () => {
33
+ expect(maybeParseActorFolder('package.json')).toEqual({ isActorFolder: false });
34
+ });
35
+
36
+ it('returns false for path with no file inside actor folder', () => {
37
+ expect(maybeParseActorFolder('actors/foo_bar')).toEqual({ isActorFolder: false });
38
+ });
39
+
40
+ it('returns false for unrelated folder', () => {
41
+ expect(maybeParseActorFolder('src/utils.ts')).toEqual({ isActorFolder: false });
42
+ });
43
+ });
44
+
45
+ describe('getChangedActors', () => {
46
+ beforeEach(() => {
47
+ vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockReturnValue(false);
48
+ });
49
+
50
+ it('returns empty array when no files changed', () => {
51
+ expect(getChangedActors({ filepathsChanged: [], actorConfigs, commits })).toEqual([]);
52
+ });
53
+
54
+ it('returns empty array when only ignored top-level files changed', () => {
55
+ const result = getChangedActors({
56
+ filepathsChanged: ['.gitignore', 'README.md', '.husky/pre-commit', '.vscode/settings.json'],
57
+ actorConfigs,
58
+ commits,
59
+ });
60
+ expect(result).toEqual([]);
61
+ });
62
+
63
+ it('returns the actor when a functional file in its folder changes', () => {
64
+ const result = getChangedActors({
65
+ filepathsChanged: ['actors/foo_bar/src/main.ts'],
66
+ actorConfigs,
67
+ commits,
68
+ });
69
+ expect(result).toEqual([miniActor]);
70
+ });
71
+
72
+ it('returns actor when isLatest and README in actor folder changed (cosmetic)', () => {
73
+ const result = getChangedActors({
74
+ filepathsChanged: ['actors/foo_bar/README.md'],
75
+ actorConfigs,
76
+ commits,
77
+ isLatest: true,
78
+ });
79
+ expect(result).toEqual([miniActor]);
80
+ });
81
+
82
+ it('does not return actor when not isLatest and only README changed (cosmetic)', () => {
83
+ const result = getChangedActors({
84
+ filepathsChanged: ['actors/foo_bar/README.md'],
85
+ actorConfigs,
86
+ commits,
87
+ isLatest: false,
88
+ });
89
+ expect(result).toEqual([]);
90
+ });
91
+
92
+ it('returns actor when isLatest and JSON file has only cosmetic changes', () => {
93
+ vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockReturnValue(true);
94
+ const result = getChangedActors({
95
+ filepathsChanged: ['actors/foo_bar/actor.json'],
96
+ actorConfigs,
97
+ commits,
98
+ isLatest: true,
99
+ });
100
+ expect(result).toEqual([miniActor]);
101
+ });
102
+
103
+ it('does not return actor when not isLatest and JSON file has only cosmetic changes', () => {
104
+ vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockReturnValue(true);
105
+ const result = getChangedActors({
106
+ filepathsChanged: ['actors/foo_bar/actor.json'],
107
+ actorConfigs,
108
+ commits,
109
+ isLatest: false,
110
+ });
111
+ expect(result).toEqual([]);
112
+ });
113
+
114
+ it('returns actor when JSON file has functional changes', () => {
115
+ vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockReturnValue(false);
116
+ const result = getChangedActors({
117
+ filepathsChanged: ['actors/foo_bar/actor.json'],
118
+ actorConfigs,
119
+ commits,
120
+ });
121
+ expect(result).toEqual([miniActor]);
122
+ });
123
+
124
+ it('returns all non-standalone actors when a non-actor-folder functional file changes', () => {
125
+ const result = getChangedActors({
126
+ filepathsChanged: ['shared/utils.ts'],
127
+ actorConfigs,
128
+ commits,
129
+ });
130
+ expect(result).toContainEqual(miniActor);
131
+ expect(result).not.toContainEqual(standaloneActor);
132
+ });
133
+
134
+ it('does not include standalone actor in all-actors expansion from changelog', () => {
135
+ const result = getChangedActors({
136
+ filepathsChanged: ['CHANGELOG.md'],
137
+ actorConfigs,
138
+ commits,
139
+ isLatest: true,
140
+ });
141
+ expect(result).toContainEqual(miniActor);
142
+ expect(result).not.toContainEqual(standaloneActor);
143
+ });
144
+
145
+ it('includes standalone actor when its own folder changes', () => {
146
+ const result = getChangedActors({
147
+ filepathsChanged: ['standalone-actors/standalone/src/main.ts'],
148
+ actorConfigs,
149
+ commits,
150
+ });
151
+ expect(result).toContainEqual(standaloneActor);
152
+ });
153
+
154
+ it('deduplicates actors when multiple files in same actor folder change', () => {
155
+ const result = getChangedActors({
156
+ filepathsChanged: ['actors/foo_bar/src/main.ts', 'actors/foo_bar/package.json'],
157
+ actorConfigs,
158
+ commits,
159
+ });
160
+ expect(result).toHaveLength(1);
161
+ expect(result).toContainEqual(miniActor);
162
+ });
163
+
164
+ it('handles mixed changes: returns both mini and standalone actors', () => {
165
+ const result = getChangedActors({
166
+ filepathsChanged: ['actors/foo_bar/src/main.ts', 'standalone-actors/standalone/Dockerfile'],
167
+ actorConfigs,
168
+ commits,
169
+ });
170
+ expect(result).toContainEqual(miniActor);
171
+ expect(result).toContainEqual(standaloneActor);
172
+ });
173
+
174
+ it('file paths are matched case-insensitively', () => {
175
+ const result = getChangedActors({
176
+ filepathsChanged: ['Actors/FOO_BAR/Main.ts'],
177
+ actorConfigs,
178
+ commits,
179
+ });
180
+ expect(result).toEqual([miniActor]);
181
+ });
182
+ });