add-command.e2e-spec.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. /*
  2. * E2E tests for the add command
  3. *
  4. * To run these tests:
  5. * npm run vitest -- --config e2e/vitest.e2e.config.mts
  6. */
  7. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
  8. import { performAddOperation } from '../src/commands/add/add-operations';
  9. import { addApiExtensionCommand } from '../src/commands/add/api-extension/add-api-extension';
  10. import { addCodegenCommand } from '../src/commands/add/codegen/add-codegen';
  11. import { addEntityCommand } from '../src/commands/add/entity/add-entity';
  12. import { addJobQueueCommand } from '../src/commands/add/job-queue/add-job-queue';
  13. import { createNewPluginCommand } from '../src/commands/add/plugin/create-new-plugin';
  14. import { addServiceCommand } from '../src/commands/add/service/add-service';
  15. import { addUiExtensionsCommand } from '../src/commands/add/ui-extensions/add-ui-extensions';
  16. type Spy = ReturnType<typeof vi.spyOn>;
  17. /**
  18. * Utility to stub the `run` function of a CliCommand so that the command
  19. * doesn't actually execute any file-system or project manipulation during the
  20. * tests. The stub resolves immediately, emulating a successful CLI command.
  21. */
  22. function stubRun(cmd: { run: (...args: any[]) => any }): Spy {
  23. // Cast to 'any' to avoid over-constraining the generic type parameters that
  24. // vitest uses on spyOn, which causes type inference issues in strict mode.
  25. // The runtime behaviour (spying on an object method) is what matters for
  26. // these tests – the precise compile-time types are not important.
  27. return vi
  28. .spyOn(cmd as any, 'run')
  29. .mockResolvedValue({ project: undefined, modifiedSourceFiles: [] } as any);
  30. }
  31. let pluginRunSpy: Spy;
  32. let entityRunSpy: Spy;
  33. let serviceRunSpy: Spy;
  34. let jobQueueRunSpy: Spy;
  35. let codegenRunSpy: Spy;
  36. let apiExtRunSpy: Spy;
  37. let uiExtRunSpy: Spy;
  38. beforeEach(() => {
  39. // Stub all sub-command `run` handlers before every test
  40. pluginRunSpy = stubRun(createNewPluginCommand);
  41. entityRunSpy = stubRun(addEntityCommand);
  42. serviceRunSpy = stubRun(addServiceCommand);
  43. jobQueueRunSpy = stubRun(addJobQueueCommand);
  44. codegenRunSpy = stubRun(addCodegenCommand);
  45. apiExtRunSpy = stubRun(addApiExtensionCommand);
  46. uiExtRunSpy = stubRun(addUiExtensionsCommand);
  47. });
  48. afterEach(() => {
  49. vi.restoreAllMocks();
  50. });
  51. describe('Add Command E2E', () => {
  52. describe('performAddOperation', () => {
  53. it('creates a plugin when the "plugin" option is provided', async () => {
  54. const result = await performAddOperation({ plugin: 'test-plugin' });
  55. expect(pluginRunSpy).toHaveBeenCalledOnce();
  56. expect(pluginRunSpy).toHaveBeenCalledWith({ name: 'test-plugin', config: undefined });
  57. expect(result.success).toBe(true);
  58. expect(result.message).toContain('test-plugin');
  59. });
  60. it('throws when the plugin name is empty', async () => {
  61. await expect(performAddOperation({ plugin: ' ' } as any)).rejects.toThrow(
  62. 'Plugin name is required',
  63. );
  64. expect(pluginRunSpy).not.toHaveBeenCalled();
  65. });
  66. it('adds an entity to the specified plugin', async () => {
  67. const result = await performAddOperation({
  68. entity: 'MyEntity',
  69. selectedPlugin: 'MyPlugin',
  70. });
  71. expect(entityRunSpy).toHaveBeenCalledOnce();
  72. expect(entityRunSpy).toHaveBeenCalledWith({
  73. className: 'MyEntity',
  74. isNonInteractive: true,
  75. config: undefined,
  76. pluginName: 'MyPlugin',
  77. customFields: undefined,
  78. translatable: undefined,
  79. });
  80. expect(result.success).toBe(true);
  81. expect(result.message).toContain('MyEntity');
  82. expect(result.message).toContain('MyPlugin');
  83. });
  84. it('fails when adding an entity without specifying a plugin in non-interactive mode', async () => {
  85. await expect(performAddOperation({ entity: 'MyEntity' })).rejects.toThrow(
  86. 'Plugin name is required when running in non-interactive mode',
  87. );
  88. expect(entityRunSpy).not.toHaveBeenCalled();
  89. });
  90. it('adds a service to the specified plugin', async () => {
  91. const result = await performAddOperation({
  92. service: 'MyService',
  93. selectedPlugin: 'MyPlugin',
  94. });
  95. expect(serviceRunSpy).toHaveBeenCalledOnce();
  96. expect(serviceRunSpy.mock.calls[0][0]).toMatchObject({
  97. serviceName: 'MyService',
  98. pluginName: 'MyPlugin',
  99. });
  100. expect(result.success).toBe(true);
  101. expect(result.message).toContain('MyService');
  102. });
  103. it('adds a job-queue when required parameters are provided', async () => {
  104. const options = {
  105. jobQueue: 'MyPlugin',
  106. name: 'ReindexJob',
  107. selectedService: 'SearchService',
  108. } as const;
  109. const result = await performAddOperation(options);
  110. expect(jobQueueRunSpy).toHaveBeenCalledOnce();
  111. expect(jobQueueRunSpy.mock.calls[0][0]).toMatchObject({
  112. pluginName: 'MyPlugin',
  113. name: 'ReindexJob',
  114. selectedService: 'SearchService',
  115. });
  116. expect(result.success).toBe(true);
  117. expect(result.message).toContain('Job-queue');
  118. });
  119. it('fails when job-queue parameters are incomplete', async () => {
  120. await expect(
  121. performAddOperation({ jobQueue: true, name: 'JobWithoutService' } as any),
  122. ).rejects.toThrow('Service name is required for job queue');
  123. expect(jobQueueRunSpy).not.toHaveBeenCalled();
  124. });
  125. it('adds codegen configuration with boolean flag (interactive plugin selection)', async () => {
  126. const result = await performAddOperation({ codegen: true });
  127. expect(codegenRunSpy).toHaveBeenCalledOnce();
  128. expect(codegenRunSpy.mock.calls[0][0]).toMatchObject({ pluginName: undefined });
  129. expect(result.success).toBe(true);
  130. expect(result.message).toContain('Codegen');
  131. });
  132. it('adds codegen configuration to a specific plugin when plugin name is supplied', async () => {
  133. const result = await performAddOperation({ codegen: 'MyPlugin' });
  134. expect(codegenRunSpy).toHaveBeenCalledOnce();
  135. expect(codegenRunSpy.mock.calls[0][0]).toMatchObject({ pluginName: 'MyPlugin' });
  136. expect(result.success).toBe(true);
  137. });
  138. it('adds an API extension scaffold when queryName is provided', async () => {
  139. const result = await performAddOperation({
  140. apiExtension: 'MyPlugin',
  141. queryName: 'myQuery',
  142. });
  143. expect(apiExtRunSpy).toHaveBeenCalledOnce();
  144. expect(apiExtRunSpy.mock.calls[0][0]).toMatchObject({
  145. pluginName: 'MyPlugin',
  146. queryName: 'myQuery',
  147. mutationName: undefined,
  148. });
  149. expect(result.success).toBe(true);
  150. });
  151. it('fails when neither queryName nor mutationName is provided for API extension', async () => {
  152. await expect(performAddOperation({ apiExtension: true } as any)).rejects.toThrow(
  153. 'At least one of query-name or mutation-name must be specified',
  154. );
  155. expect(apiExtRunSpy).not.toHaveBeenCalled();
  156. });
  157. it('adds UI extensions when the uiExtensions flag is used', async () => {
  158. const result = await performAddOperation({ uiExtensions: 'MyPlugin' });
  159. expect(uiExtRunSpy).toHaveBeenCalledOnce();
  160. expect(uiExtRunSpy.mock.calls[0][0]).toMatchObject({ pluginName: 'MyPlugin' });
  161. expect(result.success).toBe(true);
  162. expect(result.message).toContain('UI extensions');
  163. });
  164. it('returns a failure result when no valid operation is specified', async () => {
  165. const result = await performAddOperation({});
  166. expect(result.success).toBe(false);
  167. expect(result.message).toContain('No valid add operation specified');
  168. expect(pluginRunSpy).not.toHaveBeenCalled();
  169. expect(entityRunSpy).not.toHaveBeenCalled();
  170. expect(serviceRunSpy).not.toHaveBeenCalled();
  171. });
  172. });
  173. });