From 5c192358bf90b2ca18fa6f38c79b79c8fccbff00 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Thu, 12 Aug 2021 17:18:38 +0100 Subject: [PATCH] [v0.36] Adds ability to test api functions (#3207) Co-authored-by: Peter Pistorius --- .../api/src/functions/healthz/healthz.js | 15 +++ .../api/src/functions/invalid/x.js | 15 +++ .../api/src/functions/nested/nested.test.ts | 0 .../api/src/functions/nested/nested.ts | 15 +++ .../api/src/functions/x/index.js | 1 + packages/api/src/webhooks/index.ts | 1 + .../__tests__/__snapshots__/cell.test.js.snap | 20 ++-- .../generate/cell/templates/test.js.template | 4 +- .../__snapshots__/component.test.ts.snap | 6 +- .../component/templates/test.tsx.template | 2 +- .../__snapshots__/function.test.ts.snap | 37 ++++++ .../function/__tests__/function.test.ts | 49 +++++++- .../commands/generate/function/function.js | 67 +++++++++-- .../function/templates/scenarios.ts.template | 6 + .../function/templates/test.ts.template | 26 ++++ .../__snapshots__/layout.test.ts.snap | 4 +- .../layout/templates/test.tsx.template | 2 +- .../__tests__/__snapshots__/page.test.js.snap | 12 +- .../generate/page/templates/test.tsx.template | 2 +- packages/core/config/babel-preset.js | 2 +- packages/core/config/webpack.common.js | 6 +- packages/internal/package.json | 4 +- .../internal/src/__tests__/build_api.test.ts | 97 ++++++++++++++- packages/internal/src/__tests__/files.test.ts | 52 ++++---- .../src/__tests__/typeDefinitions.test.ts | 39 +++--- packages/internal/src/build/api.ts | 91 ++++++++++++-- packages/internal/src/files.ts | 2 +- .../templates/api-scenarios.d.ts.template | 2 +- packages/testing/api/index.js | 2 + packages/testing/api/package.json | 4 + packages/testing/config/jest/api/index.js | 13 ++ .../testing/config/jest/api/jest.setup.js | 2 +- packages/testing/config/jest/web/index.js | 12 +- .../testing/config/jest/web/jest.setup.js | 2 +- packages/testing/config/storybook/main.js | 2 +- packages/testing/config/storybook/preview.js | 2 +- packages/testing/jest.setup.ts | 2 +- packages/testing/package.json | 2 + packages/testing/src/api/apiTestingMocks.ts | 111 ++++++++++++++++++ packages/testing/src/api/index.ts | 2 + packages/testing/src/{ => api}/scenario.ts | 0 .../testing/src/{ => web}/MockProviders.tsx | 0 packages/testing/src/{ => web}/MockRouter.tsx | 0 .../src/{ => web}/StorybookProvider.tsx | 0 .../{ => web}/__tests__/MockHandlers.test.tsx | 0 .../{ => web}/__tests__/MockRouter.test.tsx | 0 .../testing/src/{ => web}/customRender.tsx | 0 packages/testing/src/{ => web}/fileMock.ts | 0 packages/testing/src/{ => web}/global.ts | 0 packages/testing/src/{ => web}/index.ts | 1 - .../testing/src/{ => web}/mockRequests.ts | 0 packages/testing/web/index.js | 2 + packages/testing/web/package.json | 4 + 53 files changed, 627 insertions(+), 115 deletions(-) create mode 100644 __fixtures__/example-todo-main/api/src/functions/nested/nested.test.ts create mode 100644 __fixtures__/example-todo-main/api/src/functions/nested/nested.ts create mode 100644 packages/cli/src/commands/generate/function/templates/scenarios.ts.template create mode 100644 packages/cli/src/commands/generate/function/templates/test.ts.template create mode 100644 packages/testing/api/index.js create mode 100644 packages/testing/api/package.json create mode 100644 packages/testing/src/api/apiTestingMocks.ts create mode 100644 packages/testing/src/api/index.ts rename packages/testing/src/{ => api}/scenario.ts (100%) rename packages/testing/src/{ => web}/MockProviders.tsx (100%) rename packages/testing/src/{ => web}/MockRouter.tsx (100%) rename packages/testing/src/{ => web}/StorybookProvider.tsx (100%) rename packages/testing/src/{ => web}/__tests__/MockHandlers.test.tsx (100%) rename packages/testing/src/{ => web}/__tests__/MockRouter.test.tsx (100%) rename packages/testing/src/{ => web}/customRender.tsx (100%) rename packages/testing/src/{ => web}/fileMock.ts (100%) rename packages/testing/src/{ => web}/global.ts (100%) rename packages/testing/src/{ => web}/index.ts (91%) rename packages/testing/src/{ => web}/mockRequests.ts (100%) create mode 100644 packages/testing/web/index.js create mode 100644 packages/testing/web/package.json diff --git a/__fixtures__/example-todo-main/api/src/functions/healthz/healthz.js b/__fixtures__/example-todo-main/api/src/functions/healthz/healthz.js index e69de29bb2d1..d5922e913ced 100644 --- a/__fixtures__/example-todo-main/api/src/functions/healthz/healthz.js +++ b/__fixtures__/example-todo-main/api/src/functions/healthz/healthz.js @@ -0,0 +1,15 @@ +import { logger } from 'src/lib/logger' + +export const handler = async (event, context) => { + logger.info('Invoked healtz function') + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: 'healthz function', + }), + } +} diff --git a/__fixtures__/example-todo-main/api/src/functions/invalid/x.js b/__fixtures__/example-todo-main/api/src/functions/invalid/x.js index e69de29bb2d1..2d2d8d79701a 100644 --- a/__fixtures__/example-todo-main/api/src/functions/invalid/x.js +++ b/__fixtures__/example-todo-main/api/src/functions/invalid/x.js @@ -0,0 +1,15 @@ +import { logger } from 'src/lib/logger' + +export const handler = async (event, context) => { + logger.info('Invoked x function') + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: 'x function', + }), + } +} diff --git a/__fixtures__/example-todo-main/api/src/functions/nested/nested.test.ts b/__fixtures__/example-todo-main/api/src/functions/nested/nested.test.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/__fixtures__/example-todo-main/api/src/functions/nested/nested.ts b/__fixtures__/example-todo-main/api/src/functions/nested/nested.ts new file mode 100644 index 000000000000..5ca76eb6c9e7 --- /dev/null +++ b/__fixtures__/example-todo-main/api/src/functions/nested/nested.ts @@ -0,0 +1,15 @@ +import { logger } from 'src/lib/logger' + +export const handler = async (event, context) => { + logger.info('Invoked nested function') + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: 'nested function', + }), + } +} diff --git a/__fixtures__/example-todo-main/api/src/functions/x/index.js b/__fixtures__/example-todo-main/api/src/functions/x/index.js index e69de29bb2d1..be173f2f91d9 100644 --- a/__fixtures__/example-todo-main/api/src/functions/x/index.js +++ b/__fixtures__/example-todo-main/api/src/functions/x/index.js @@ -0,0 +1 @@ +export const value = 'HELLO from X' diff --git a/packages/api/src/webhooks/index.ts b/packages/api/src/webhooks/index.ts index 7dbecc6ba51c..b830d8747e4e 100644 --- a/packages/api/src/webhooks/index.ts +++ b/packages/api/src/webhooks/index.ts @@ -12,6 +12,7 @@ export { VerifyOptions, WebhookVerificationError, DEFAULT_WEBHOOK_SECRET, + SupportedVerifierTypes, } from '../auth/verifiers' export const DEFAULT_WEBHOOK_SIGNATURE_HEADER = 'RW-WEBHOOK-SIGNATURE' diff --git a/packages/cli/src/commands/generate/cell/__tests__/__snapshots__/cell.test.js.snap b/packages/cli/src/commands/generate/cell/__tests__/__snapshots__/cell.test.js.snap index 95ae7547995f..015fdb201956 100644 --- a/packages/cli/src/commands/generate/cell/__tests__/__snapshots__/cell.test.js.snap +++ b/packages/cli/src/commands/generate/cell/__tests__/__snapshots__/cell.test.js.snap @@ -338,7 +338,7 @@ export default { title: 'Cells/UserProfileCell' } `; exports[`creates a cell test with a camelCase word name 1`] = ` -"import { render } from '@redwoodjs/testing' +"import { render } from '@redwoodjs/testing/web' import { Loading, Empty, Failure, Success } from './UserProfileCell' import { standard } from './UserProfileCell.mock' @@ -364,7 +364,7 @@ describe('UserProfileCell', () => { // When you're ready to test the actual output of your component render // you could test that, for example, certain text is present: // - // 1. import { screen } from '@redwoodjs/testing' + // 1. import { screen } from '@redwoodjs/testing/web' // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument() it('renders Success successfully', async () => { @@ -377,7 +377,7 @@ describe('UserProfileCell', () => { `; exports[`creates a cell test with a kebabCase word name 1`] = ` -"import { render } from '@redwoodjs/testing' +"import { render } from '@redwoodjs/testing/web' import { Loading, Empty, Failure, Success } from './UserProfileCell' import { standard } from './UserProfileCell.mock' @@ -403,7 +403,7 @@ describe('UserProfileCell', () => { // When you're ready to test the actual output of your component render // you could test that, for example, certain text is present: // - // 1. import { screen } from '@redwoodjs/testing' + // 1. import { screen } from '@redwoodjs/testing/web' // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument() it('renders Success successfully', async () => { @@ -416,7 +416,7 @@ describe('UserProfileCell', () => { `; exports[`creates a cell test with a multi word name 1`] = ` -"import { render } from '@redwoodjs/testing' +"import { render } from '@redwoodjs/testing/web' import { Loading, Empty, Failure, Success } from './UserProfileCell' import { standard } from './UserProfileCell.mock' @@ -442,7 +442,7 @@ describe('UserProfileCell', () => { // When you're ready to test the actual output of your component render // you could test that, for example, certain text is present: // - // 1. import { screen } from '@redwoodjs/testing' + // 1. import { screen } from '@redwoodjs/testing/web' // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument() it('renders Success successfully', async () => { @@ -455,7 +455,7 @@ describe('UserProfileCell', () => { `; exports[`creates a cell test with a single word name 1`] = ` -"import { render } from '@redwoodjs/testing' +"import { render } from '@redwoodjs/testing/web' import { Loading, Empty, Failure, Success } from './UserCell' import { standard } from './UserCell.mock' @@ -481,7 +481,7 @@ describe('UserCell', () => { // When you're ready to test the actual output of your component render // you could test that, for example, certain text is present: // - // 1. import { screen } from '@redwoodjs/testing' + // 1. import { screen } from '@redwoodjs/testing/web' // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument() it('renders Success successfully', async () => { @@ -494,7 +494,7 @@ describe('UserCell', () => { `; exports[`creates a cell test with a snakeCase word name 1`] = ` -"import { render } from '@redwoodjs/testing' +"import { render } from '@redwoodjs/testing/web' import { Loading, Empty, Failure, Success } from './UserProfileCell' import { standard } from './UserProfileCell.mock' @@ -520,7 +520,7 @@ describe('UserProfileCell', () => { // When you're ready to test the actual output of your component render // you could test that, for example, certain text is present: // - // 1. import { screen } from '@redwoodjs/testing' + // 1. import { screen } from '@redwoodjs/testing/web' // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument() it('renders Success successfully', async () => { diff --git a/packages/cli/src/commands/generate/cell/templates/test.js.template b/packages/cli/src/commands/generate/cell/templates/test.js.template index de9cf204e257..8e3bfc9d2a05 100644 --- a/packages/cli/src/commands/generate/cell/templates/test.js.template +++ b/packages/cli/src/commands/generate/cell/templates/test.js.template @@ -1,4 +1,4 @@ -import { render, screen } from '@redwoodjs/testing' +import { render, screen } from '@redwoodjs/testing/web' import { Loading, Empty, Failure, Success } from './${pascalName}Cell' import { standard } from './${pascalName}Cell.mock' @@ -24,7 +24,7 @@ describe('${pascalName}Cell', () => { // When you're ready to test the actual output of your component render // you could test that, for example, certain text is present: // - // 1. import { screen } from '@redwoodjs/testing' + // 1. import { screen } from '@redwoodjs/testing/web' // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument() it('renders Success successfully', async () => { diff --git a/packages/cli/src/commands/generate/component/__tests__/__snapshots__/component.test.ts.snap b/packages/cli/src/commands/generate/component/__tests__/__snapshots__/component.test.ts.snap index 1e05ea4a4b8d..e10ba175a6ac 100644 --- a/packages/cli/src/commands/generate/component/__tests__/__snapshots__/component.test.ts.snap +++ b/packages/cli/src/commands/generate/component/__tests__/__snapshots__/component.test.ts.snap @@ -15,7 +15,7 @@ export default TypescriptUser `; exports[`creates a TS component and test 2`] = ` -"import { render } from '@redwoodjs/testing' +"import { render } from '@redwoodjs/testing/web' import TypescriptUser from './TypescriptUser' @@ -55,7 +55,7 @@ export default { title: 'Components/UserProfile' } `; exports[`creates a multi word component test 1`] = ` -"import { render } from '@redwoodjs/testing' +"import { render } from '@redwoodjs/testing/web' import UserProfile from './UserProfile' @@ -95,7 +95,7 @@ export default { title: 'Components/User' } `; exports[`creates a single word component test 1`] = ` -"import { render } from '@redwoodjs/testing' +"import { render } from '@redwoodjs/testing/web' import User from './User' diff --git a/packages/cli/src/commands/generate/component/templates/test.tsx.template b/packages/cli/src/commands/generate/component/templates/test.tsx.template index 0dba20cb2f0e..1f41fecb42cc 100644 --- a/packages/cli/src/commands/generate/component/templates/test.tsx.template +++ b/packages/cli/src/commands/generate/component/templates/test.tsx.template @@ -1,4 +1,4 @@ -import { render } from '@redwoodjs/testing' +import { render } from '@redwoodjs/testing/web' import ${pascalName} from './${pascalName}' diff --git a/packages/cli/src/commands/generate/function/__tests__/__snapshots__/function.test.ts.snap b/packages/cli/src/commands/generate/function/__tests__/__snapshots__/function.test.ts.snap index 71be65494a95..e1e6bcdb2eab 100644 --- a/packages/cli/src/commands/generate/function/__tests__/__snapshots__/function.test.ts.snap +++ b/packages/cli/src/commands/generate/function/__tests__/__snapshots__/function.test.ts.snap @@ -143,3 +143,40 @@ export const handler = async (event, context) => { } " `; + +exports[`creates a single word function file: Scenario snapshot 1`] = ` +"export const standard = defineScenario({ + // Define the \\"fixture\\" to write into your test database here + // See guide: https://redwoodjs.com/docs/testing#scenarios +}) +" +`; + +exports[`creates a single word function file: Test snapshot 1`] = ` +"import { mockHttpEvent } from '@redwoodjs/testing/api' + +import { handler } from './foo' + +describe('foo function', () => { + it('Should respond with 200', async () => { + const httpEvent = mockHttpEvent({ + queryStringParameters: { + id: '42', // Add parameters here + }, + }) + + const response = await handler(httpEvent, null) + const { data } = JSON.parse(response.body) + + expect(response.statusCode).toBe(200) + expect(data).toBe('foo function') + }) + + // You can also use scenarios to test your api functions + // See guide here: https://redwoodjs.com/docs/testing#scenarios + // scenario('Scenario test', async () => { + // + // }) +}) +" +`; diff --git a/packages/cli/src/commands/generate/function/__tests__/function.test.ts b/packages/cli/src/commands/generate/function/__tests__/function.test.ts index 8d82d26db1da..440b7dfac19b 100644 --- a/packages/cli/src/commands/generate/function/__tests__/function.test.ts +++ b/packages/cli/src/commands/generate/function/__tests__/function.test.ts @@ -17,6 +17,7 @@ let typescriptFiles: WordFilesType beforeAll(() => { singleWordDefaultFiles = functionGenerator.files({ name: 'foo', + tests: true, }) multiWordDefaultFiles = functionGenerator.files({ name: 'send-mail', @@ -30,22 +31,43 @@ beforeAll(() => { }) }) -test('returns exactly 1 file', () => { - expect(Object.keys(singleWordDefaultFiles).length).toEqual(1) +test('returns tests, scenario and function file', () => { + const fileNames = Object.keys(singleWordDefaultFiles) + expect(fileNames.length).toEqual(3) + + expect(fileNames).toEqual( + expect.arrayContaining([ + expect.stringContaining('foo.js'), + expect.stringContaining('foo.test.js'), + expect.stringContaining('foo.scenarios.js'), + ]) + ) }) test('creates a single word function file', () => { expect( singleWordDefaultFiles[ - path.normalize('/path/to/project/api/src/functions/foo.js') + path.normalize('/path/to/project/api/src/functions/foo/foo.js') ] ).toMatchSnapshot() + + expect( + singleWordDefaultFiles[ + path.normalize('/path/to/project/api/src/functions/foo/foo.test.js') + ] + ).toMatchSnapshot('Test snapshot') + + expect( + singleWordDefaultFiles[ + path.normalize('/path/to/project/api/src/functions/foo/foo.scenarios.js') + ] + ).toMatchSnapshot('Scenario snapshot') }) test('creates a multi word function file', () => { expect( multiWordDefaultFiles[ - path.normalize('/path/to/project/api/src/functions/sendMail.js') + path.normalize('/path/to/project/api/src/functions/sendMail/sendMail.js') ] ).toMatchSnapshot() }) @@ -53,7 +75,9 @@ test('creates a multi word function file', () => { test('creates a .js file if --javascript=true', () => { expect( javascriptFiles[ - path.normalize('/path/to/project/api/src/functions/javascriptFunction.js') + path.normalize( + '/path/to/project/api/src/functions/javascriptFunction/javascriptFunction.js' + ) ] ).toMatchSnapshot() // ^ JS-function-args should be stripped of their types and consequently the unused 'aws-lamda' import removed. @@ -61,9 +85,22 @@ test('creates a .js file if --javascript=true', () => { }) test('creates a .ts file if --typescript=true', () => { + const fileNames = Object.keys(typescriptFiles) + expect(fileNames.length).toEqual(3) + + expect(fileNames).toEqual( + expect.arrayContaining([ + expect.stringContaining('typescriptFunction.ts'), + expect.stringContaining('typescriptFunction.test.ts'), + expect.stringContaining('typescriptFunction.scenarios.ts'), + ]) + ) + expect( typescriptFiles[ - path.normalize('/path/to/project/api/src/functions/typescriptFunction.ts') + path.normalize( + '/path/to/project/api/src/functions/typescriptFunction/typescriptFunction.ts' + ) ] ).toMatchSnapshot() // ^ TS-functions, on the other hand, retain the 'aws-lamda' import and type-declartions. diff --git a/packages/cli/src/commands/generate/function/function.js b/packages/cli/src/commands/generate/function/function.js index 10ef513aab61..2cb53ce61605 100644 --- a/packages/cli/src/commands/generate/function/function.js +++ b/packages/cli/src/commands/generate/function/function.js @@ -12,13 +12,16 @@ import { templateForComponentFile } from '../helpers' export const files = ({ name, typescript: generateTypescript = false, + tests: generateTests = true, ...rest }) => { - // Taken from ../component; should be updated to take from the project's configuration const extension = generateTypescript ? '.ts' : '.js' const functionName = camelcase(name) - const file = templateForComponentFile({ + + const outputFiles = [] + + const functionFiles = templateForComponentFile({ name: functionName, componentName: functionName, extension, @@ -28,15 +31,58 @@ export const files = ({ templateVars: { ...rest }, outputPath: path.join( getPaths().api.functions, + functionName, `${functionName}${extension}` ), }) - const template = generateTypescript - ? file[1] - : transformTSToJS(file[0], file[1]) + outputFiles.push(functionFiles) + + if (generateTests) { + const testFile = templateForComponentFile({ + name: functionName, + componentName: functionName, + extension, + apiPathSection: 'functions', + generator: 'function', + templatePath: 'test.ts.template', + templateVars: { ...rest }, + outputPath: path.join( + getPaths().api.functions, + functionName, + `${functionName}.test${extension}` + ), + }) + + const scenarioFile = templateForComponentFile({ + name: functionName, + componentName: functionName, + extension, + apiPathSection: 'functions', + generator: 'function', + templatePath: 'scenarios.ts.template', + templateVars: { ...rest }, + outputPath: path.join( + getPaths().api.functions, + functionName, + `${functionName}.scenarios${extension}` + ), + }) + + outputFiles.push(testFile) + outputFiles.push(scenarioFile) + } + + return outputFiles.reduce((acc, [outputPath, content]) => { + const template = generateTypescript + ? content + : transformTSToJS(outputPath, content) - return { [file[0]]: template } + return { + [outputPath]: template, + ...acc, + } + }, {}) } export const command = 'function ' @@ -67,14 +113,15 @@ export const builder = (yargs) => { // This could be built using createYargsForComponentGeneration; // however, we need to add a message after generating the function files -export const handler = async ({ name, ...rest }) => { +export const handler = async ({ name, force, ...rest }) => { const tasks = new Listr( [ { - title: `Generating function files...`, + title: 'Generating function files...', task: async () => { - const f = await files({ name, ...rest }) - return writeFilesTask(f, { overwriteExisting: rest.force }) + return writeFilesTask(files({ name, ...rest }), { + overwriteExisting: force, + }) }, }, ], diff --git a/packages/cli/src/commands/generate/function/templates/scenarios.ts.template b/packages/cli/src/commands/generate/function/templates/scenarios.ts.template new file mode 100644 index 000000000000..6822bd69c64a --- /dev/null +++ b/packages/cli/src/commands/generate/function/templates/scenarios.ts.template @@ -0,0 +1,6 @@ +export const standard = defineScenario({ + // Define the "fixture" to write into your test database here + // See guide: https://redwoodjs.com/docs/testing#scenarios +}) + +export type StandardScenario = typeof standard diff --git a/packages/cli/src/commands/generate/function/templates/test.ts.template b/packages/cli/src/commands/generate/function/templates/test.ts.template new file mode 100644 index 000000000000..34258a36e06f --- /dev/null +++ b/packages/cli/src/commands/generate/function/templates/test.ts.template @@ -0,0 +1,26 @@ +import { mockHttpEvent } from '@redwoodjs/testing/api' + +import { handler } from './${name}' + +describe('${name} function', () => { + + it('Should respond with 200', async () => { + const httpEvent = mockHttpEvent({ + queryStringParameters: { + id: '42', // Add parameters here + }, + }) + + const response = await handler(httpEvent, null) + const { data } = JSON.parse(response.body) + + expect(response.statusCode).toBe(200) + expect(data).toBe('${name} function') + }) + +// You can also use scenarios to test your api functions +// See guide here: https://redwoodjs.com/docs/testing#scenarios +// scenario('Scenario test', async () => { +// +// }) +}) diff --git a/packages/cli/src/commands/generate/layout/__tests__/__snapshots__/layout.test.ts.snap b/packages/cli/src/commands/generate/layout/__tests__/__snapshots__/layout.test.ts.snap index bf4c2e0dda92..39e668c219ce 100644 --- a/packages/cli/src/commands/generate/layout/__tests__/__snapshots__/layout.test.ts.snap +++ b/packages/cli/src/commands/generate/layout/__tests__/__snapshots__/layout.test.ts.snap @@ -68,7 +68,7 @@ export default SinglePageLayout `; exports[`creates a multi word layout test 1`] = ` -"import { render } from '@redwoodjs/testing' +"import { render } from '@redwoodjs/testing/web' import SinglePageLayout from './SinglePageLayout' @@ -114,7 +114,7 @@ export default { title: 'Layouts/AppLayout' } `; exports[`creates a single word layout test 1`] = ` -"import { render } from '@redwoodjs/testing' +"import { render } from '@redwoodjs/testing/web' import AppLayout from './AppLayout' diff --git a/packages/cli/src/commands/generate/layout/templates/test.tsx.template b/packages/cli/src/commands/generate/layout/templates/test.tsx.template index 5a101d796490..c3d443a398cb 100644 --- a/packages/cli/src/commands/generate/layout/templates/test.tsx.template +++ b/packages/cli/src/commands/generate/layout/templates/test.tsx.template @@ -1,4 +1,4 @@ -import { render } from '@redwoodjs/testing' +import { render } from '@redwoodjs/testing/web' import ${pascalName}Layout from './${pascalName}Layout' diff --git a/packages/cli/src/commands/generate/page/__tests__/__snapshots__/page.test.js.snap b/packages/cli/src/commands/generate/page/__tests__/__snapshots__/page.test.js.snap index c0096fb168f2..5c3f02bc9353 100644 --- a/packages/cli/src/commands/generate/page/__tests__/__snapshots__/page.test.js.snap +++ b/packages/cli/src/commands/generate/page/__tests__/__snapshots__/page.test.js.snap @@ -144,7 +144,7 @@ export default { title: 'Pages/ContactUsPage' } `; exports[`creates a page test 1`] = ` -"import { render } from '@redwoodjs/testing' +"import { render } from '@redwoodjs/testing/web' import HomePage from './HomePage' @@ -159,7 +159,7 @@ describe('HomePage', () => { `; exports[`creates a test for a component with multiple words for a name 1`] = ` -"import { render } from '@redwoodjs/testing' +"import { render } from '@redwoodjs/testing/web' import ContactUsPage from './ContactUsPage' @@ -174,7 +174,7 @@ describe('ContactUsPage', () => { `; exports[`creates a test for page component with params 1`] = ` -"import { render } from '@redwoodjs/testing' +"import { render } from '@redwoodjs/testing/web' import PostPage from './PostPage' @@ -204,7 +204,7 @@ export default { title: 'Pages/HomePage' } exports[`file generation 2`] = ` Object { - "fileContent": "import { render } from '@redwoodjs/testing' + "fileContent": "import { render } from '@redwoodjs/testing/web' import HomePage from './HomePage' @@ -288,7 +288,7 @@ export default { title: 'Pages/PostPage' } exports[`file generation with route params 2`] = ` Object { - "fileContent": "import { render } from '@redwoodjs/testing' + "fileContent": "import { render } from '@redwoodjs/testing/web' import PostPage from './PostPage' @@ -398,7 +398,7 @@ export default { title: 'Pages/TSFilesPage' } `; exports[`generates typescript pages 3`] = ` -"import { render } from '@redwoodjs/testing' +"import { render } from '@redwoodjs/testing/web' import TSFilesPage from './TSFilesPage' diff --git a/packages/cli/src/commands/generate/page/templates/test.tsx.template b/packages/cli/src/commands/generate/page/templates/test.tsx.template index f4eeb06f4628..7a3b8dc5d7b0 100644 --- a/packages/cli/src/commands/generate/page/templates/test.tsx.template +++ b/packages/cli/src/commands/generate/page/templates/test.tsx.template @@ -1,4 +1,4 @@ -import { render } from '@redwoodjs/testing' +import { render } from '@redwoodjs/testing/web' import ${pascalName}Page from './${pascalName}Page' diff --git a/packages/core/config/babel-preset.js b/packages/core/config/babel-preset.js index ea3c3cb24647..e1c8c09b4bbb 100644 --- a/packages/core/config/babel-preset.js +++ b/packages/core/config/babel-preset.js @@ -161,7 +161,7 @@ module.exports = () => { 'mockGraphQLMutation', 'mockCurrentUser', ], - path: '@redwoodjs/testing', + path: '@redwoodjs/testing/web', }, ], }, diff --git a/packages/core/config/webpack.common.js b/packages/core/config/webpack.common.js index 0fe75daacb39..d31d7dad9802 100644 --- a/packages/core/config/webpack.common.js +++ b/packages/core/config/webpack.common.js @@ -140,9 +140,9 @@ const getSharedPlugins = (isEnvProduction) => { React: 'react', PropTypes: 'prop-types', gql: 'graphql-tag', - mockGraphQLQuery: ['@redwoodjs/testing', 'mockGraphQLQuery'], - mockGraphQLMutation: ['@redwoodjs/testing', 'mockGraphQLMutation'], - mockCurrentUser: ['@redwoodjs/testing', 'mockCurrentUser'], + mockGraphQLQuery: ['@redwoodjs/testing/web', 'mockGraphQLQuery'], + mockGraphQLMutation: ['@redwoodjs/testing/web', 'mockGraphQLMutation'], + mockCurrentUser: ['@redwoodjs/testing/web', 'mockCurrentUser'], }), // The define plugin will replace these keys with their values during build // time. diff --git a/packages/internal/package.json b/packages/internal/package.json index 561094bd18e1..1941826b17e3 100644 --- a/packages/internal/package.json +++ b/packages/internal/package.json @@ -26,6 +26,7 @@ "deepmerge": "4.2.2", "fast-glob": "3.2.7", "findup-sync": "4.0.0", + "fs-extra": "10.0.0", "glob": "7.1.7", "graphql": "15.5.1", "kill-port": "1.6.1", @@ -35,8 +36,9 @@ "rimraf": "3.0.2" }, "devDependencies": { - "@types/rimraf": "3.0.1", "@types/findup-sync": "4.0.1", + "@types/fs-extra": "9.0.12", + "@types/rimraf": "3.0.1", "graphql-tag": "2.12.5" }, "jest": { diff --git a/packages/internal/src/__tests__/build_api.test.ts b/packages/internal/src/__tests__/build_api.test.ts index f6815cb7d4c1..1d53283fd43e 100644 --- a/packages/internal/src/__tests__/build_api.test.ts +++ b/packages/internal/src/__tests__/build_api.test.ts @@ -1,7 +1,12 @@ import fs from 'fs' import path from 'path' -import { getApiSideBabelConfigPath, prebuildApiFiles } from '../build/api' +import { + getApiSideBabelConfigPath, + prebuildApiFiles, + cleanApiBuild, + generateProxyFilesForNestedFunction, +} from '../build/api' import { findApiFiles } from '../files' import { ensurePosixPath } from '../paths' @@ -14,19 +19,101 @@ const cleanPaths = (p) => { return ensurePosixPath(path.relative(FIXTURE_PATH, p)) } +const fullPath = (p) => { + return path.join(FIXTURE_PATH, p) +} + +// Fixtures, filled in beforeAll +let builtFiles +let relativePaths + beforeAll(() => { process.env.RWJS_CWD = FIXTURE_PATH + cleanApiBuild() + findApiFiles() + builtFiles = prebuildApiFiles(findApiFiles()) + relativePaths = builtFiles + .filter((x) => typeof x !== 'undefined') + .map(cleanPaths) }) afterAll(() => { delete process.env.RWJS_CWD }) test('api files are prebuilt', () => { - const builtFiles = prebuildApiFiles(findApiFiles()) - const p = builtFiles.filter((x) => typeof x !== 'undefined').map(cleanPaths) + // Builds non-nested functions + expect(relativePaths).toContain( + '.redwood/prebuild/api/src/functions/graphql.js' + ) + + // Builds graphql folder + expect(relativePaths).toContain( + '.redwood/prebuild/api/src/graphql/todos.sdl.js' + ) + + // Builds nested function + expect(relativePaths).toContain( + '.redwood/prebuild/api/src/functions/nested/nested.js' + ) +}) + +describe("Should create a 'proxy' function for nested functions", () => { + it('Handles functions nested with the same name', () => { + const [buildPath, reExportPath] = generateProxyFilesForNestedFunction( + fullPath('.redwood/prebuild/api/src/functions/nested/nested.js') + ) + + // Hidden path in the _nestedFunctions folder + expect(cleanPaths(buildPath)).toBe( + '.redwood/prebuild/api/src/_nestedFunctions/nested/nested.js' + ) + + // Proxy/reExport function placed in the function directory + expect(cleanPaths(reExportPath)).toBe( + '.redwood/prebuild/api/src/functions/nested.js' + ) + + const reExportContent = fs.readFileSync(reExportPath, 'utf-8') + expect(reExportContent).toMatchInlineSnapshot( + `"export * from '../_nestedFunctions/nested/nested';"` + ) + }) + + it('Handles folders with an index file', () => { + const [buildPath, reExportPath] = generateProxyFilesForNestedFunction( + fullPath('.redwood/prebuild/api/src/functions/x/index.js') + ) + + // Hidden path in the _build folder + expect(cleanPaths(buildPath)).toBe( + '.redwood/prebuild/api/src/_nestedFunctions/x/index.js' + ) + + // Proxy/reExport function placed in the function directory + expect(cleanPaths(reExportPath)).toBe( + '.redwood/prebuild/api/src/functions/x.js' + ) + + const reExportContent = fs.readFileSync(reExportPath, 'utf-8') + + expect(reExportContent).toMatchInlineSnapshot( + `"export * from '../_nestedFunctions/x';"` + ) + }) + + it('Should not put files that dont match the folder name in dist/functions', () => { + const [buildPath, reExportPath] = generateProxyFilesForNestedFunction( + fullPath('.redwood/prebuild/api/src/functions/invalid/x.js') + ) + + // File is transpiled to the _nestedFunctions folder + expect(cleanPaths(buildPath)).toEqual( + '.redwood/prebuild/api/src/_nestedFunctions/invalid/x.js' + ) - expect(p[0].endsWith('api/src/functions/graphql.js')).toBeTruthy() - expect(p[2].endsWith('api/src/graphql/todos.sdl.js')).toBeTruthy() + // But not exposed as a serverless function + expect(reExportPath).toBe(undefined) + }) }) test('api prebuild finds babel.config.js', () => { diff --git a/packages/internal/src/__tests__/files.test.ts b/packages/internal/src/__tests__/files.test.ts index 3fb09ccb7404..37b311e7f133 100644 --- a/packages/internal/src/__tests__/files.test.ts +++ b/packages/internal/src/__tests__/files.test.ts @@ -42,25 +42,26 @@ test('finds directory named modules', () => { const p = paths.map(cleanPaths) expect(p).toMatchInlineSnapshot(` - Array [ - "api/src/functions/healthz/healthz.js", - "api/src/services/todos/todos.js", - "web/src/components/AddTodo/AddTodo.js", - "web/src/components/AddTodoControl/AddTodoControl.js", - "web/src/components/Check/Check.js", - "web/src/components/TableCell/TableCell.js", - "web/src/components/TodoItem/TodoItem.js", - "web/src/layouts/SetLayout/SetLayout.js", - "web/src/pages/BarPage/BarPage.tsx", - "web/src/pages/FatalErrorPage/FatalErrorPage.js", - "web/src/pages/FooPage/FooPage.tsx", - "web/src/pages/HomePage/HomePage.tsx", - "web/src/pages/NotFoundPage/NotFoundPage.js", - "web/src/pages/PrivatePage/PrivatePage.tsx", - "web/src/pages/TypeScriptPage/TypeScriptPage.tsx", - "web/src/pages/admin/EditUserPage/EditUserPage.jsx", - ] - `) +Array [ + "api/src/functions/healthz/healthz.js", + "api/src/functions/nested/nested.ts", + "api/src/services/todos/todos.js", + "web/src/components/AddTodo/AddTodo.js", + "web/src/components/AddTodoControl/AddTodoControl.js", + "web/src/components/Check/Check.js", + "web/src/components/TableCell/TableCell.js", + "web/src/components/TodoItem/TodoItem.js", + "web/src/layouts/SetLayout/SetLayout.js", + "web/src/pages/BarPage/BarPage.tsx", + "web/src/pages/FatalErrorPage/FatalErrorPage.js", + "web/src/pages/FooPage/FooPage.tsx", + "web/src/pages/HomePage/HomePage.tsx", + "web/src/pages/NotFoundPage/NotFoundPage.js", + "web/src/pages/PrivatePage/PrivatePage.tsx", + "web/src/pages/TypeScriptPage/TypeScriptPage.tsx", + "web/src/pages/admin/EditUserPage/EditUserPage.jsx", +] +`) }) test('finds all the page files', () => { @@ -94,10 +95,11 @@ test('find api functions', () => { const p = paths.map(cleanPaths) expect(p).toMatchInlineSnapshot(` - Array [ - "api/src/functions/graphql.js", - "api/src/functions/healthz/healthz.js", - "api/src/functions/x/index.js", - ] - `) +Array [ + "api/src/functions/graphql.js", + "api/src/functions/healthz/healthz.js", + "api/src/functions/nested/nested.ts", + "api/src/functions/x/index.js", +] +`) }) diff --git a/packages/internal/src/__tests__/typeDefinitions.test.ts b/packages/internal/src/__tests__/typeDefinitions.test.ts index 4ee5594a0215..00a9858d1723 100644 --- a/packages/internal/src/__tests__/typeDefinitions.test.ts +++ b/packages/internal/src/__tests__/typeDefinitions.test.ts @@ -72,25 +72,26 @@ test('generate the correct mirror types for directory named modules', () => { const p = paths.map(cleanPaths) expect(p).toMatchInlineSnapshot(` - Array [ - ".redwood/types/mirror/api/src/functions/healthz/index.d.ts", - ".redwood/types/mirror/api/src/services/todos/index.d.ts", - ".redwood/types/mirror/web/src/components/AddTodo/index.d.ts", - ".redwood/types/mirror/web/src/components/AddTodoControl/index.d.ts", - ".redwood/types/mirror/web/src/components/Check/index.d.ts", - ".redwood/types/mirror/web/src/components/TableCell/index.d.ts", - ".redwood/types/mirror/web/src/components/TodoItem/index.d.ts", - ".redwood/types/mirror/web/src/layouts/SetLayout/index.d.ts", - ".redwood/types/mirror/web/src/pages/BarPage/index.d.ts", - ".redwood/types/mirror/web/src/pages/FatalErrorPage/index.d.ts", - ".redwood/types/mirror/web/src/pages/FooPage/index.d.ts", - ".redwood/types/mirror/web/src/pages/HomePage/index.d.ts", - ".redwood/types/mirror/web/src/pages/NotFoundPage/index.d.ts", - ".redwood/types/mirror/web/src/pages/PrivatePage/index.d.ts", - ".redwood/types/mirror/web/src/pages/TypeScriptPage/index.d.ts", - ".redwood/types/mirror/web/src/pages/admin/EditUserPage/index.d.ts", - ] - `) +Array [ + ".redwood/types/mirror/api/src/functions/healthz/index.d.ts", + ".redwood/types/mirror/api/src/functions/nested/index.d.ts", + ".redwood/types/mirror/api/src/services/todos/index.d.ts", + ".redwood/types/mirror/web/src/components/AddTodo/index.d.ts", + ".redwood/types/mirror/web/src/components/AddTodoControl/index.d.ts", + ".redwood/types/mirror/web/src/components/Check/index.d.ts", + ".redwood/types/mirror/web/src/components/TableCell/index.d.ts", + ".redwood/types/mirror/web/src/components/TodoItem/index.d.ts", + ".redwood/types/mirror/web/src/layouts/SetLayout/index.d.ts", + ".redwood/types/mirror/web/src/pages/BarPage/index.d.ts", + ".redwood/types/mirror/web/src/pages/FatalErrorPage/index.d.ts", + ".redwood/types/mirror/web/src/pages/FooPage/index.d.ts", + ".redwood/types/mirror/web/src/pages/HomePage/index.d.ts", + ".redwood/types/mirror/web/src/pages/NotFoundPage/index.d.ts", + ".redwood/types/mirror/web/src/pages/PrivatePage/index.d.ts", + ".redwood/types/mirror/web/src/pages/TypeScriptPage/index.d.ts", + ".redwood/types/mirror/web/src/pages/admin/EditUserPage/index.d.ts", +] +`) expect(fs.readFileSync(paths[0], 'utf-8')).toMatchInlineSnapshot(` "// This file was generated by RedwoodJS diff --git a/packages/internal/src/build/api.ts b/packages/internal/src/build/api.ts index c74e5a0edc52..c26e917559ad 100644 --- a/packages/internal/src/build/api.ts +++ b/packages/internal/src/build/api.ts @@ -3,10 +3,11 @@ import path from 'path' import { transform, TransformOptions } from '@babel/core' import { buildSync } from 'esbuild' +import { moveSync } from 'fs-extra' import rimraf from 'rimraf' import { findApiFiles } from '../files' -import { getPaths } from '../paths' +import { ensurePosixPath, getPaths } from '../paths' export const buildApi = () => { // TODO: Be smarter about caching and invalidating files, @@ -14,9 +15,10 @@ export const buildApi = () => { cleanApiBuild() const srcFiles = findApiFiles() - const prebuiltFiles = prebuildApiFiles(srcFiles).filter( - (x) => typeof x !== 'undefined' - ) as string[] + + const prebuiltFiles = prebuildApiFiles(srcFiles) + .filter((path): path is string => path !== undefined) + .flatMap(generateProxyFilesForNestedFunction) return transpileApi(prebuiltFiles) } @@ -27,17 +29,89 @@ export const cleanApiBuild = () => { rimraf.sync(path.join(rwjsPaths.generated.prebuild, 'api')) } +/** + * Takes prebuilt api files, and will generate proxy functions where required + * If the function is nested in a folder, put it into the special _build directory + * at the same level as functions, then re-export it. + * + * This allows for support for nested functions across all our supported providers, + * (Netlify, Vercel, Render, Self-hosted) - and they behave consistently + * + * Note that this function takes prebuilt files in the .redwood/prebuild directory + * + */ +export const generateProxyFilesForNestedFunction = (prebuiltFile: string) => { + const rwjsPaths = getPaths() + + const relativePathFromFunctions = path.relative( + path.join(rwjsPaths.generated.prebuild, 'api/src/functions'), + prebuiltFile + ) + const folderName = path.dirname(relativePathFromFunctions) + + const isNestedFunction = + ensurePosixPath(prebuiltFile).includes('api/src/functions') && + folderName !== '.' + + if (isNestedFunction) { + const { name: fileName } = path.parse(relativePathFromFunctions) + const isIndexFile = fileName === 'index' + + // .redwood/prebuilds/api/src/_build/{folder}/{fileName} + const nestedFunctionOutputPath = path + .join( + rwjsPaths.generated.prebuild, + 'api/src/_nestedFunctions', + relativePathFromFunctions + ) + .replace(/\.(ts)$/, '.js') + + // move existing file into the new nestedOutputPath + // @Note: use fs-extra.moveSync for compatibility under docker and linux + moveSync(prebuiltFile, nestedFunctionOutputPath) + + // Only generate proxy files for the function + if (fileName === folderName || isIndexFile) { + // .redwood/prebuild/api/src/functions/{folderName}.js + const reExportPath = + path.join( + rwjsPaths.generated.prebuild, + 'api/src/functions', + folderName + ) + '.js' + + const importString = isIndexFile + ? `../_nestedFunctions/${folderName}` + : `../_nestedFunctions/${folderName}/${folderName}` + + const reExportContent = `export * from '${importString}';` + + fs.writeFileSync(reExportPath, reExportContent) + + return [nestedFunctionOutputPath, reExportPath] + } else { + // other files in the folder e.g. functions/helloWorld/otherFile.js + + return [nestedFunctionOutputPath] + } + } + + // If no post-processing required + return [prebuiltFile] +} + /** * Remove RedwoodJS "magic" from a user's code leaving JavaScript behind. */ export const prebuildApiFiles = (srcFiles: string[]) => { - const rwjsPaths = getPaths() const plugins = getBabelPlugins() + const rwjsPaths = getPaths() return srcFiles.map((srcPath) => { - let dstPath = path.relative(rwjsPaths.base, srcPath) - dstPath = path.join(rwjsPaths.generated.prebuild, dstPath) - dstPath = dstPath.replace(/\.(ts)$/, '.js') // TODO: Figure out a better way to handle extensions + const relativePathFromSrc = path.relative(rwjsPaths.base, srcPath) + const dstPath = path + .join(rwjsPaths.generated.prebuild, relativePathFromSrc) + .replace(/\.(ts)$/, '.js') const result = prebuildFile(srcPath, dstPath, plugins) if (!result?.code) { @@ -49,6 +123,7 @@ export const prebuildApiFiles = (srcFiles: string[]) => { fs.mkdirSync(path.dirname(dstPath), { recursive: true }) fs.writeFileSync(dstPath, result.code) + return dstPath }) } diff --git a/packages/internal/src/files.ts b/packages/internal/src/files.ts index b41356dde2f9..fbbc0a80aa4b 100644 --- a/packages/internal/src/files.ts +++ b/packages/internal/src/files.ts @@ -48,7 +48,7 @@ export const findGraphQLSchemas = (cwd: string = getPaths().api.graphql) => { const ignoreApiFiles = [ '**/*.test.{js,ts}', '**/*.scenarios.{js,ts}', - '**/*.scenarios.{js,ts}', + '**/*.fixtures.{js,ts}', '**/*.d.ts', ] diff --git a/packages/internal/src/generate/templates/api-scenarios.d.ts.template b/packages/internal/src/generate/templates/api-scenarios.d.ts.template index 75969135aaee..ca5328b0585e 100644 --- a/packages/internal/src/generate/templates/api-scenarios.d.ts.template +++ b/packages/internal/src/generate/templates/api-scenarios.d.ts.template @@ -1,4 +1,4 @@ -import type { Scenario, DefineScenario } from '@redwoodjs/testing' +import type { Scenario, DefineScenario } from '@redwoodjs/testing/api' declare global { /** diff --git a/packages/testing/api/index.js b/packages/testing/api/index.js new file mode 100644 index 000000000000..bd05e8c81a39 --- /dev/null +++ b/packages/testing/api/index.js @@ -0,0 +1,2 @@ +/* eslint-env es6, commonjs */ +module.exports = require('../dist/api') diff --git a/packages/testing/api/package.json b/packages/testing/api/package.json new file mode 100644 index 000000000000..d3fea6defdf5 --- /dev/null +++ b/packages/testing/api/package.json @@ -0,0 +1,4 @@ +{ + "main": "./index.js", + "types": "../dist/api/index.d.ts" +} diff --git a/packages/testing/config/jest/api/index.js b/packages/testing/config/jest/api/index.js index 6d99282d53fc..2b77eb610b22 100644 --- a/packages/testing/config/jest/api/index.js +++ b/packages/testing/config/jest/api/index.js @@ -1,5 +1,10 @@ const path = require('path') +const { getPaths } = require('@redwoodjs/internal') + +const rwjsPaths = getPaths() +const NODE_MODULES_PATH = path.join(rwjsPaths.base, 'node_modules') + module.exports = { testEnvironment: path.join(__dirname, './RedwoodApiJestEnv.js'), displayName: { @@ -7,4 +12,12 @@ module.exports = { name: 'api', }, setupFilesAfterEnv: [path.join(__dirname, './jest.setup.js')], + moduleNameMapper: { + // @NOTE: Import @redwoodjs/testing in api tests, and it automatically remaps to the api side only + // This is to prevent web stuff leaking into api, and vice versa + '^@redwoodjs/testing$': path.join( + NODE_MODULES_PATH, + '@redwoodjs/testing/api' + ), + }, } diff --git a/packages/testing/config/jest/api/jest.setup.js b/packages/testing/config/jest/api/jest.setup.js index b73fbe267978..561bf8305cce 100644 --- a/packages/testing/config/jest/api/jest.setup.js +++ b/packages/testing/config/jest/api/jest.setup.js @@ -4,7 +4,7 @@ const path = require('path') const { setContext } = require('@redwoodjs/api') const { getSchemaDefinitions } = require('@redwoodjs/cli/dist/lib') const { getPaths } = require('@redwoodjs/internal') -const { defineScenario } = require('@redwoodjs/testing/dist/scenario') +const { defineScenario } = require('@redwoodjs/testing/dist/api') const { db } = require(path.join(getPaths().api.src, 'lib', 'db')) const DEFAULT_SCENARIO = 'standard' diff --git a/packages/testing/config/jest/web/index.js b/packages/testing/config/jest/web/index.js index 0da91d4830c8..2830a93e63e6 100644 --- a/packages/testing/config/jest/web/index.js +++ b/packages/testing/config/jest/web/index.js @@ -29,16 +29,22 @@ module.exports = { // We replace imports to "@redwoodjs/router" with our own "mock" implementation. '^@redwoodjs/router$': path.join( NODE_MODULES_PATH, - '@redwoodjs/testing/dist/MockRouter.js' + '@redwoodjs/testing/dist/web/MockRouter.js' ), '^@redwoodjs/web$': path.join(NODE_MODULES_PATH, '@redwoodjs/web'), - '^@redwoodjs/testing$': path.join(NODE_MODULES_PATH, '@redwoodjs/testing'), + + // @NOTE: Import @redwoodjs/testing in web tests, and it automatically remaps to the web side only + // This is to prevent web stuff leaking into api, and vice versa + '^@redwoodjs/testing$': path.join( + NODE_MODULES_PATH, + '@redwoodjs/testing/web' + ), '~__REDWOOD__USER_ROUTES_FOR_MOCK': rwjsPaths.web.routes, /** * Mock out files that aren't particularly useful in tests. See fileMock.js for more info. */ '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|css)$': - '@redwoodjs/testing/dist/fileMock.js', + '@redwoodjs/testing/dist/web/fileMock.js', }, testEnvironment: 'jest-environment-jsdom', } diff --git a/packages/testing/config/jest/web/jest.setup.js b/packages/testing/config/jest/web/jest.setup.js index 9b28bb2ea82d..1acd6a420090 100644 --- a/packages/testing/config/jest/web/jest.setup.js +++ b/packages/testing/config/jest/web/jest.setup.js @@ -10,7 +10,7 @@ const { mockGraphQLMutation, mockGraphQLQuery, mockCurrentUser, -} = require('@redwoodjs/testing') +} = require('@redwoodjs/testing/web') global.mockGraphQLQuery = mockGraphQLQuery global.mockGraphQLMutation = mockGraphQLMutation diff --git a/packages/testing/config/storybook/main.js b/packages/testing/config/storybook/main.js index 3a6b9be65c20..4f98f24281e3 100644 --- a/packages/testing/config/storybook/main.js +++ b/packages/testing/config/storybook/main.js @@ -33,7 +33,7 @@ const baseConfig = { // We replace imports to "@redwoodjs/router" with our own implementation in "@redwoodjs/testing" sbConfig.resolve.alias['@redwoodjs/router$'] = require.resolve( - '@redwoodjs/testing/dist/MockRouter.js' + '@redwoodjs/testing/dist/web/MockRouter.js' ) sbConfig.resolve.alias['~__REDWOOD__USER_ROUTES_FOR_MOCK'] = rwjsPaths.web.routes diff --git a/packages/testing/config/storybook/preview.js b/packages/testing/config/storybook/preview.js index 3cd6a7c2d5ca..2d7fd01341eb 100644 --- a/packages/testing/config/storybook/preview.js +++ b/packages/testing/config/storybook/preview.js @@ -6,7 +6,7 @@ const { merge } = require('webpack-merge') // booting up the mock server workers, and mocking the router. const { StorybookProvider, -} = require('@redwoodjs/testing/dist/StorybookProvider') +} = require('@redwoodjs/testing/dist/web/StorybookProvider') // Import the user's default CSS file require('~__REDWOOD__USER_WEB_DEFAULT_CSS') diff --git a/packages/testing/jest.setup.ts b/packages/testing/jest.setup.ts index 30a138ecd542..6bbc20490012 100644 --- a/packages/testing/jest.setup.ts +++ b/packages/testing/jest.setup.ts @@ -1,7 +1,7 @@ import '@testing-library/jest-dom/extend-expect' import '@testing-library/jest-dom' -import { startMSW } from './src/mockRequests' +import { startMSW } from './src/web/mockRequests' beforeAll(async () => { await startMSW('node', { diff --git a/packages/testing/package.json b/packages/testing/package.json index 627b5602475b..435a7585c94c 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -4,6 +4,8 @@ "version": "0.35.2", "files": [ "config", + "web", + "api", "dist" ], "main": "dist/index.js", diff --git a/packages/testing/src/api/apiTestingMocks.ts b/packages/testing/src/api/apiTestingMocks.ts new file mode 100644 index 000000000000..ae77d97ba6c6 --- /dev/null +++ b/packages/testing/src/api/apiTestingMocks.ts @@ -0,0 +1,111 @@ +import type { + APIGatewayProxyEvent, + APIGatewayProxyEventHeaders, + Context, +} from 'aws-lambda' + +import type { SupportedVerifierTypes } from '@redwoodjs/api/webhooks' +import { signPayload } from '@redwoodjs/api/webhooks' + +interface BuildEventParams extends Partial { + payload?: string | null | Record + signature?: string + signatureHeader?: string + headers?: APIGatewayProxyEventHeaders +} + +/** + * @description Use this to mock out the http request event that is received by your function in unit tests + * + * @example Mocking sending headers + * mockHttpEvent({header: {'X-Custom-Header': 'bazinga'}}) + * + * @example Adding a JSON payload + * mockHttpEvent({payload: JSON.stringify(mockedRequestBody)}) + * + * @returns APIGatewayProxyEvent + */ +export const mockHttpEvent = ({ + payload = null, + signature, + signatureHeader, + queryStringParameters = null, + httpMethod = 'GET', + headers = {}, + path = '', + isBase64Encoded = false, + ...others +}: BuildEventParams): APIGatewayProxyEvent => { + if (signature && signatureHeader) { + headers[signatureHeader.toLocaleLowerCase()] = signature + } + + const payloadAsString = + typeof payload === 'string' ? payload : JSON.stringify(payload) + + const body = isBase64Encoded + ? Buffer.from(payloadAsString || '').toString('base64') + : payloadAsString + + return { + body, + headers, + multiValueHeaders: {}, + isBase64Encoded, + path, + pathParameters: null, + stageVariables: null, + httpMethod, + queryStringParameters, + // @ts-expect-error not required for mocks + requestContext: null, + // @ts-expect-error not required for mocks + resource: null, + multiValueQueryStringParameters: null, + ...others, + } +} + +/** + * @description Use this function to mock the http event's context + * @see: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html + **/ +export const mockContext = (): Context => { + const context = {} as Context + + return context +} + +interface MockedSignedWebhookParams + extends Omit { + signatureType: Exclude + signatureHeader: string // make this required + secret: string +} + +/** + * @description Use this function to mock a signed webhook + * @see https://redwoodjs.com/docs/webhooks#webhooks + **/ +export const mockSignedWebhook = ({ + payload = null, + signatureType, + signatureHeader, + secret, + ...others +}: MockedSignedWebhookParams) => { + const payloadAsString = + typeof payload === 'string' ? payload : JSON.stringify(payload) + + const signature = signPayload(signatureType, { + payload: payloadAsString, + secret, + }) + + return mockHttpEvent({ + payload, + signature, + signatureHeader, + ...others, + }) +} diff --git a/packages/testing/src/api/index.ts b/packages/testing/src/api/index.ts new file mode 100644 index 000000000000..5185ae3ca8f2 --- /dev/null +++ b/packages/testing/src/api/index.ts @@ -0,0 +1,2 @@ +export * from './apiTestingMocks' +export * from './scenario' diff --git a/packages/testing/src/scenario.ts b/packages/testing/src/api/scenario.ts similarity index 100% rename from packages/testing/src/scenario.ts rename to packages/testing/src/api/scenario.ts diff --git a/packages/testing/src/MockProviders.tsx b/packages/testing/src/web/MockProviders.tsx similarity index 100% rename from packages/testing/src/MockProviders.tsx rename to packages/testing/src/web/MockProviders.tsx diff --git a/packages/testing/src/MockRouter.tsx b/packages/testing/src/web/MockRouter.tsx similarity index 100% rename from packages/testing/src/MockRouter.tsx rename to packages/testing/src/web/MockRouter.tsx diff --git a/packages/testing/src/StorybookProvider.tsx b/packages/testing/src/web/StorybookProvider.tsx similarity index 100% rename from packages/testing/src/StorybookProvider.tsx rename to packages/testing/src/web/StorybookProvider.tsx diff --git a/packages/testing/src/__tests__/MockHandlers.test.tsx b/packages/testing/src/web/__tests__/MockHandlers.test.tsx similarity index 100% rename from packages/testing/src/__tests__/MockHandlers.test.tsx rename to packages/testing/src/web/__tests__/MockHandlers.test.tsx diff --git a/packages/testing/src/__tests__/MockRouter.test.tsx b/packages/testing/src/web/__tests__/MockRouter.test.tsx similarity index 100% rename from packages/testing/src/__tests__/MockRouter.test.tsx rename to packages/testing/src/web/__tests__/MockRouter.test.tsx diff --git a/packages/testing/src/customRender.tsx b/packages/testing/src/web/customRender.tsx similarity index 100% rename from packages/testing/src/customRender.tsx rename to packages/testing/src/web/customRender.tsx diff --git a/packages/testing/src/fileMock.ts b/packages/testing/src/web/fileMock.ts similarity index 100% rename from packages/testing/src/fileMock.ts rename to packages/testing/src/web/fileMock.ts diff --git a/packages/testing/src/global.ts b/packages/testing/src/web/global.ts similarity index 100% rename from packages/testing/src/global.ts rename to packages/testing/src/web/global.ts diff --git a/packages/testing/src/index.ts b/packages/testing/src/web/index.ts similarity index 91% rename from packages/testing/src/index.ts rename to packages/testing/src/web/index.ts index 5231b5871ffc..6ffe9a032700 100644 --- a/packages/testing/src/index.ts +++ b/packages/testing/src/web/index.ts @@ -7,4 +7,3 @@ export { customRender as render } from './customRender' export { MockProviders } from './MockProviders' export * from './mockRequests' -export * from './scenario' diff --git a/packages/testing/src/mockRequests.ts b/packages/testing/src/web/mockRequests.ts similarity index 100% rename from packages/testing/src/mockRequests.ts rename to packages/testing/src/web/mockRequests.ts diff --git a/packages/testing/web/index.js b/packages/testing/web/index.js new file mode 100644 index 000000000000..fb6512f8a7cc --- /dev/null +++ b/packages/testing/web/index.js @@ -0,0 +1,2 @@ +/* eslint-env es6, commonjs */ +module.exports = require('../dist/web') diff --git a/packages/testing/web/package.json b/packages/testing/web/package.json new file mode 100644 index 000000000000..3f2f649e25a6 --- /dev/null +++ b/packages/testing/web/package.json @@ -0,0 +1,4 @@ +{ + "main": "./index.js", + "types": "../dist/web/index.d.ts" +}