diff --git a/.changeset/cyan-suits-battle.md b/.changeset/cyan-suits-battle.md new file mode 100644 index 0000000000..ac8b9eb811 --- /dev/null +++ b/.changeset/cyan-suits-battle.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The `scaffolder-module` template has been updated to use a more modern layout and new testing utilities for scaffolder actions. diff --git a/.changeset/popular-items-retire.md b/.changeset/popular-items-retire.md new file mode 100644 index 0000000000..2b737d9710 --- /dev/null +++ b/.changeset/popular-items-retire.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +The `createMockDirectory` cleanup strategy has been changed, no longer requiring it to be called outside individual tests. diff --git a/packages/backend-test-utils/src/filesystem/MockDirectory.ts b/packages/backend-test-utils/src/filesystem/MockDirectory.ts index 39518f5d9c..a1d616d8e0 100644 --- a/packages/backend-test-utils/src/filesystem/MockDirectory.ts +++ b/packages/backend-test-utils/src/filesystem/MockDirectory.ts @@ -349,8 +349,11 @@ class MockDirectoryImpl { */ export interface CreateMockDirectoryOptions { /** - * In addition to creating a temporary directory, also mock `os.tmpdir()` to return the - * mock directory path until the end of the test suite. + * In addition to creating a temporary directory, also mock `os.tmpdir()` to + * return the mock directory path until the end of the test suite. + * + * When this option is provided the `createMockDirectory` call must happen in + * a scope where calling `afterAll` from Jest is allowed * * @returns */ @@ -362,6 +365,34 @@ export interface CreateMockDirectoryOptions { content?: MockDirectoryContent; } +const cleanupCallbacks = new Array<() => void>(); + +let registered = false; +function registerTestHooks() { + if (typeof afterAll !== 'function') { + return; + } + if (registered) { + return; + } + registered = true; + + afterAll(async () => { + for (const callback of cleanupCallbacks) { + try { + callback(); + } catch (error) { + console.error( + `Failed to clean up mock directory after tests, ${error}`, + ); + } + } + cleanupCallbacks.length = 0; + }); +} + +registerTestHooks(); + /** * Creates a new temporary mock directory that will be removed after the tests have completed. * @@ -410,17 +441,14 @@ export function createMockDirectory( process.on('beforeExit', mocker.remove); } - try { + if (needsCleanup) { + cleanupCallbacks.push(() => mocker.remove()); + } + + if (origTmpdir) { afterAll(() => { - if (origTmpdir) { - os.tmpdir = origTmpdir; - } - if (needsCleanup) { - mocker.remove(); - } + os.tmpdir = origTmpdir; }); - } catch { - /* ignore */ } if (options?.content) { diff --git a/packages/cli/package.json b/packages/cli/package.json index ef75a3f3b4..15ac0e5eb3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -174,6 +174,7 @@ "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", "@backstage/test-utils": "workspace:^", "@backstage/theme": "workspace:^", "@rspack/core": "^1.0.10", diff --git a/packages/cli/src/lib/new/factories/scaffolderModule.test.ts b/packages/cli/src/lib/new/factories/scaffolderModule.test.ts index bdcd8b6bdc..160177f5c0 100644 --- a/packages/cli/src/lib/new/factories/scaffolderModule.test.ts +++ b/packages/cli/src/lib/new/factories/scaffolderModule.test.ts @@ -93,10 +93,8 @@ describe('scaffolderModule factory', () => { 'templating README.md.hbs', 'templating package.json.hbs', 'templating index.ts.hbs', - 'copying index.ts', 'copying example.test.ts', 'copying example.ts', - 'copying index.ts', 'copying module.ts', 'Installing:', `moving plugins${sep}scaffolder-backend-module-test`, diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index fb4a7105c3..51826ef681 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -44,6 +44,7 @@ import { version as devUtils } from '../../../../packages/dev-utils/package.json import { version as errors } from '../../../../packages/errors/package.json'; import { version as testUtils } from '../../../../packages/test-utils/package.json'; import { version as scaffolderNode } from '../../../../plugins/scaffolder-node/package.json'; +import { version as scaffolderNodeTestUtils } from '../../../../plugins/scaffolder-node-test-utils/package.json'; import { version as authBackend } from '../../../../plugins/auth-backend/package.json'; import { version as authBackendModuleGuestProvider } from '../../../../plugins/auth-backend-module-guest-provider/package.json'; import { version as catalogNode } from '../../../../plugins/catalog-node/package.json'; @@ -65,6 +66,7 @@ export const packageVersions: Record = { '@backstage/test-utils': testUtils, '@backstage/theme': theme, '@backstage/plugin-scaffolder-node': scaffolderNode, + '@backstage/plugin-scaffolder-node-test-utils': scaffolderNodeTestUtils, '@backstage/plugin-auth-backend': authBackend, '@backstage/plugin-auth-backend-module-guest-provider': authBackendModuleGuestProvider, diff --git a/packages/cli/templates/scaffolder-module/package.json.hbs b/packages/cli/templates/scaffolder-module/package.json.hbs index a6e6b5c5bc..25cd8616f2 100644 --- a/packages/cli/templates/scaffolder-module/package.json.hbs +++ b/packages/cli/templates/scaffolder-module/package.json.hbs @@ -29,10 +29,12 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { + "@backstage/backend-plugin-api": "{{versionQuery '@backstage/backend-plugin-api'}}", "@backstage/plugin-scaffolder-node": "{{versionQuery '@backstage/plugin-scaffolder-node'}}" }, "devDependencies": { - "@backstage/cli": "{{versionQuery '@backstage/cli'}}" + "@backstage/cli": "{{versionQuery '@backstage/cli'}}", + "@backstage/plugin-scaffolder-node-test-utils": "{{versionQuery '@backstage/plugin-scaffolder-node-test-utils'}}" }, "files": [ "dist" diff --git a/packages/cli/templates/scaffolder-module/src/actions/example.test.ts b/packages/cli/templates/scaffolder-module/src/actions/example.test.ts new file mode 100644 index 0000000000..2e6020c240 --- /dev/null +++ b/packages/cli/templates/scaffolder-module/src/actions/example.test.ts @@ -0,0 +1,24 @@ +import { createExampleAction } from './example'; +import {createMockActionContext} from '@backstage/plugin-scaffolder-node-test-utils' + +describe('createExampleAction', () => { + it('should call action', async () => { + const action = createExampleAction(); + + await expect(action.handler(createMockActionContext({ + input: { + myParameter: 'test', + }, + }))).resolves.toBeUndefined() + }); + + it('should fail when passing foo', async () => { + const action = createExampleAction(); + + await expect(action.handler(createMockActionContext({ + input: { + myParameter: 'foo', + }, + }))).rejects.toThrow("myParameter cannot be 'foo'") + }); +}); diff --git a/packages/cli/templates/scaffolder-module/src/actions/example/example.ts b/packages/cli/templates/scaffolder-module/src/actions/example.ts similarity index 77% rename from packages/cli/templates/scaffolder-module/src/actions/example/example.ts rename to packages/cli/templates/scaffolder-module/src/actions/example.ts index 2b05c7fe38..7e47a89bcc 100644 --- a/packages/cli/templates/scaffolder-module/src/actions/example/example.ts +++ b/packages/cli/templates/scaffolder-module/src/actions/example.ts @@ -9,14 +9,14 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; * * @public */ -export function createAcmeExampleAction() { +export function createExampleAction() { // For more information on how to define custom actions, see // https://backstage.io/docs/features/software-templates/writing-custom-actions return createTemplateAction<{ myParameter: string; }>({ id: 'acme:example', - description: 'Runs Yeoman on an installed Yeoman generator', + description: 'Runs an example action', schema: { input: { type: 'object', @@ -24,7 +24,7 @@ export function createAcmeExampleAction() { properties: { myParameter: { title: 'An example parameter', - description: 'This is the schema for our example parameter', + description: "This is an example parameter, don't set it to foo", type: 'string', }, }, @@ -35,6 +35,10 @@ export function createAcmeExampleAction() { `Running example template with parameters: ${ctx.input.myParameter}`, ); + if (ctx.input.myParameter === 'foo') { + throw new Error(`myParameter cannot be 'foo'`); + } + await new Promise(resolve => setTimeout(resolve, 1000)); }, }); diff --git a/packages/cli/templates/scaffolder-module/src/actions/example/example.test.ts b/packages/cli/templates/scaffolder-module/src/actions/example/example.test.ts deleted file mode 100644 index d0e5374f3f..0000000000 --- a/packages/cli/templates/scaffolder-module/src/actions/example/example.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { PassThrough } from 'stream'; -import { createAcmeExampleAction } from './example'; - -describe('acme:example', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should call action', async () => { - const action = createAcmeExampleAction(); - - const logger = { info: jest.fn() }; - - await action.handler({ - input: { - myParameter: 'test', - }, - workspacePath: '/tmp', - logger: logger as any, - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory() { - // Usage of createMockDirectory is recommended for testing of filesystem operations - throw new Error('Not implemented'); - }, - }); - - expect(logger.info).toHaveBeenCalledWith( - 'Running example template with parameters: test', - ); - }); -}); diff --git a/packages/cli/templates/scaffolder-module/src/actions/example/index.ts b/packages/cli/templates/scaffolder-module/src/actions/example/index.ts deleted file mode 100644 index 06ce5befc0..0000000000 --- a/packages/cli/templates/scaffolder-module/src/actions/example/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { scaffolderModule } from './module'; - -/* - @deprecated - this way of importing modules will soon be unsupported, and you should use `backend.add(import(...))` instead. -*/ -export { createAcmeExampleAction } from './example'; -export default scaffolderModule; diff --git a/packages/cli/templates/scaffolder-module/src/actions/index.ts b/packages/cli/templates/scaffolder-module/src/actions/index.ts deleted file mode 100644 index ab6642ebb0..0000000000 --- a/packages/cli/templates/scaffolder-module/src/actions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './example'; diff --git a/packages/cli/templates/scaffolder-module/src/index.ts.hbs b/packages/cli/templates/scaffolder-module/src/index.ts.hbs index 3690e43b8e..58d5da4a57 100644 --- a/packages/cli/templates/scaffolder-module/src/index.ts.hbs +++ b/packages/cli/templates/scaffolder-module/src/index.ts.hbs @@ -5,4 +5,4 @@ * @packageDocumentation */ -export * from './actions'; +export { scaffolderModule as default } from './module'; diff --git a/packages/cli/templates/scaffolder-module/src/actions/example/module.ts b/packages/cli/templates/scaffolder-module/src/module.ts similarity index 77% rename from packages/cli/templates/scaffolder-module/src/actions/example/module.ts rename to packages/cli/templates/scaffolder-module/src/module.ts index 238268dcec..56ec38ac33 100644 --- a/packages/cli/templates/scaffolder-module/src/actions/example/module.ts +++ b/packages/cli/templates/scaffolder-module/src/module.ts @@ -1,12 +1,12 @@ import { createBackendModule } from "@backstage/backend-plugin-api"; import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha'; -import { createAcmeExampleAction } from "./example"; +import { createExampleAction } from "./actions/example"; /** * A backend module that registers the action into the scaffolder */ export const scaffolderModule = createBackendModule({ - moduleId: 'acme:example', + moduleId: 'example-action', pluginId: 'scaffolder', register({ registerInit }) { registerInit({ @@ -14,7 +14,7 @@ export const scaffolderModule = createBackendModule({ scaffolderActions: scaffolderActionsExtensionPoint }, async init({ scaffolderActions}) { - scaffolderActions.addActions(createAcmeExampleAction()); + scaffolderActions.addActions(createExampleAction()); } }); }, diff --git a/yarn.lock b/yarn.lock index 26dc3ecd5b..c8bbb716f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3930,6 +3930,7 @@ __metadata: "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" "@backstage/release-manifests": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^"