diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 15d45fb1..da157a73 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -80,6 +80,7 @@ export default class Deploy extends AuthCommand { repoUrl: checklyConfig.repoUrl, checkMatch: checklyConfig.checks?.checkMatch, browserCheckMatch: checklyConfig.checks?.browserChecks?.testMatch, + multiStepCheckMatch: checklyConfig.checks?.multiStepChecks?.testMatch, ignoreDirectoriesMatch: checklyConfig.checks?.ignoreDirectoriesMatch, checkDefaults: checklyConfig.checks, browserCheckDefaults: checklyConfig.checks?.browserChecks, diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index bff0c871..69ec300e 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -156,6 +156,7 @@ export default class Test extends AuthCommand { repoUrl: checklyConfig.repoUrl, checkMatch: checklyConfig.checks?.checkMatch, browserCheckMatch: checklyConfig.checks?.browserChecks?.testMatch, + multiStepCheckMatch: checklyConfig.checks?.multiStepChecks?.testMatch, ignoreDirectoriesMatch: checklyConfig.checks?.ignoreDirectoriesMatch, checkDefaults: checklyConfig.checks, browserCheckDefaults: checklyConfig.checks?.browserChecks, diff --git a/packages/cli/src/constructs/check-group.ts b/packages/cli/src/constructs/check-group.ts index 2f79000c..70f0eca4 100644 --- a/packages/cli/src/constructs/check-group.ts +++ b/packages/cli/src/constructs/check-group.ts @@ -16,6 +16,8 @@ import type { Region } from '..' import type { Frequency } from './frequency' import type { RetryStrategy } from './retry-strategy' import { AlertEscalation } from './alert-escalation-policy' +import { MultiStepCheck } from './multi-step-check' +import CheckTypes from '../constants' const defaultApiCheckDefaults: ApiCheckDefaultConfig = { headers: [], @@ -189,14 +191,22 @@ export class CheckGroup extends Construct { this.browserChecks = props.browserChecks const fileAbsolutePath = Session.checkFileAbsolutePath! if (props.browserChecks?.testMatch) { - this.__addChecks(fileAbsolutePath, props.browserChecks.testMatch) + this.__addChecks(fileAbsolutePath, props.browserChecks.testMatch, CheckTypes.BROWSER) + } + this.multiStepChecks = props.multiStepChecks + if (props.multiStepChecks?.testMatch) { + this.__addChecks(fileAbsolutePath, props.multiStepChecks.testMatch, CheckTypes.MULTI_STEP) } Session.registerConstruct(this) this.__addSubscriptions() this.__addPrivateLocationGroupAssignments() } - private __addChecks (fileAbsolutePath: string, testMatch: string|string[]) { + private __addChecks ( + fileAbsolutePath: string, + testMatch: string|string[], + checkType: typeof CheckTypes.BROWSER | typeof CheckTypes.MULTI_STEP, + ) { const parent = path.dirname(fileAbsolutePath) const matched = glob.sync(testMatch, { nodir: true, cwd: parent }) for (const match of matched) { @@ -210,7 +220,9 @@ export class CheckGroup extends Construct { // the browserChecks props inherited from the group are applied in BrowserCheck.constructor() } const checkLogicalId = pathToPosix(path.relative(Session.basePath!, filepath)) - const check = new BrowserCheck(checkLogicalId, props) + checkType === CheckTypes.BROWSER + ? new BrowserCheck(checkLogicalId, props) + : new MultiStepCheck(checkLogicalId, props) } } @@ -261,7 +273,7 @@ export class CheckGroup extends Construct { public getMultiStepCheckDefaults (): CheckConfigDefaults { return { - frequency: this.browserChecks?.frequency, + frequency: this.multiStepChecks?.frequency, } } diff --git a/packages/cli/src/services/__tests__/project-parser-fixtures/multistep-browser-glob-patterns/__checks__/browser/check2.spec.js b/packages/cli/src/services/__tests__/project-parser-fixtures/multistep-browser-glob-patterns/__checks__/browser/check2.spec.js new file mode 100644 index 00000000..37e23206 --- /dev/null +++ b/packages/cli/src/services/__tests__/project-parser-fixtures/multistep-browser-glob-patterns/__checks__/browser/check2.spec.js @@ -0,0 +1,4 @@ +import { test } from '@playwright/test' +test('browser -> check2', async () => { + // Go to https://example.com/ +}) diff --git a/packages/cli/src/services/__tests__/project-parser-fixtures/multistep-browser-glob-patterns/__checks__/multistep/check1.spec.js b/packages/cli/src/services/__tests__/project-parser-fixtures/multistep-browser-glob-patterns/__checks__/multistep/check1.spec.js new file mode 100644 index 00000000..1c79cf7b --- /dev/null +++ b/packages/cli/src/services/__tests__/project-parser-fixtures/multistep-browser-glob-patterns/__checks__/multistep/check1.spec.js @@ -0,0 +1,4 @@ +import { test } from '@playwright/test' +test('multistep -> check1', async () => { + // GET https://api.example.com/ +}) diff --git a/packages/cli/src/services/__tests__/project-parser.spec.ts b/packages/cli/src/services/__tests__/project-parser.spec.ts index 3a93b15e..040d47a2 100644 --- a/packages/cli/src/services/__tests__/project-parser.spec.ts +++ b/packages/cli/src/services/__tests__/project-parser.spec.ts @@ -6,7 +6,7 @@ import { parseProject } from '../project-parser' const runtimes = { 2023.02: { name: '2023.02', default: false, stage: 'CURRENT', description: 'Main updates are Playwright 1.28.0, Node.js 16.x and Typescript support. We are also dropping support for Puppeteer', dependencies: { '@playwright/test': '1.28.0', '@opentelemetry/api': '1.0.4', '@opentelemetry/sdk-trace-base': '1.0.1', '@faker-js/faker': '5.5.3', aws4: '1.11.0', axios: '0.27.2', btoa: '1.2.1', chai: '4.3.7', 'chai-string': '1.5.0', 'crypto-js': '4.1.1', expect: '29.3.1', 'form-data': '4.0.0', jsonwebtoken: '8.5.1', lodash: '4.17.21', mocha: '10.1.0', moment: '2.29.2', node: '16.x', otpauth: '9.0.2', playwright: '1.28.0', typescript: '4.8.4', uuid: '9.0.0' } }, - 2023.09: { name: '2023.09', default: true, stage: 'CURRENT', description: 'Main updates are Playwright 1.38.1 and the addition of ethers 6.7.1, prisma 5.1.1, zod 3.22.2, @t3-oss/env-nextjs 0.6.1 and @xmldom/xmldom 0.8.10. Node version is 18.', dependencies: { '@faker-js/faker': '8.0.2', '@google-cloud/local-auth': '3.0.0', '@opentelemetry/api': '1.4.1', '@opentelemetry/sdk-trace-base': '1.15.2', '@playwright/test': '1.38.1', '@t3-oss/env-nextjs': '0.6.1', '@xmldom/xmldom': '0.8.10', aws4: '1.12.0', axios: '0.27.2', btoa: '1.2.1', chai: '4.3.7', 'chai-string': '1.5.0', 'crypto-js': '4.1.1', 'date-fns': '2.30.0', 'date-fns-tz': '2.0.0', dotenv: '16.3.1', ethers: '6.7.1', expect: '29.6.2', 'form-data': '4.0.0', 'gmail-api-parse-message-ts': '2.2.32', 'google-auth-library': '9.0.0', googleapis: '126.0.0', jose: '4.14.4', jsdom: '22.1.0', jsonwebtoken: '9.0.1', lodash: '4.17.21', moment: '2.29.4', otpauth: '9.1.4', playwright: '1.38.1', prisma: '5.1.1', twilio: '4.15.0', uuid: '9.0.0', ws: '8.13.0', 'xml-crypto': '4.1.0', 'xml-encryption': '3.0.2', zod: '3.22.2' } }, + 2023.09: { name: '2023.09', default: true, stage: 'CURRENT', description: 'Main updates are Playwright 1.38.1 and the addition of ethers 6.7.1, prisma 5.1.1, zod 3.22.2, @t3-oss/env-nextjs 0.6.1 and @xmldom/xmldom 0.8.10. Node version is 18.', dependencies: { '@faker-js/faker': '8.0.2', '@google-cloud/local-auth': '3.0.0', '@opentelemetry/api': '1.4.1', '@opentelemetry/sdk-trace-base': '1.15.2', '@playwright/test': '1.38.1', '@t3-oss/env-nextjs': '0.6.1', '@xmldom/xmldom': '0.8.10', aws4: '1.12.0', axios: '0.27.2', btoa: '1.2.1', chai: '4.3.7', 'chai-string': '1.5.0', 'crypto-js': '4.1.1', 'date-fns': '2.30.0', 'date-fns-tz': '2.0.0', dotenv: '16.3.1', ethers: '6.7.1', expect: '29.6.2', 'form-data': '4.0.0', 'gmail-api-parse-message-ts': '2.2.32', 'google-auth-library': '9.0.0', googleapis: '126.0.0', jose: '4.14.4', jsdom: '22.1.0', jsonwebtoken: '9.0.1', lodash: '4.17.21', moment: '2.29.4', otpauth: '9.1.4', playwright: '1.38.1', prisma: '5.1.1', twilio: '4.15.0', uuid: '9.0.0', ws: '8.13.0', 'xml-crypto': '4.1.0', 'xml-encryption': '3.0.2', zod: '3.22.2' }, multiStepSupport: true }, } const privateLocationId = uuidv4() @@ -194,4 +194,29 @@ describe('parseProject()', () => { expect(e.message).toContain('Environment variable "EMPTY_FOO" from check group "check-group-1" is not allowed to be empty') } }) + + it('should parse a project with multistep & browser glob patterns', async () => { + const globProjectPath = path.join(__dirname, 'project-parser-fixtures', 'multistep-browser-glob-patterns') + const project = await parseProject({ + directory: globProjectPath, + projectLogicalId: 'glob-project-id', + projectName: 'glob project', + availableRuntimes: runtimes, + checkMatch: [], + browserCheckMatch: ['**/__checks__/browser/*.spec.js'], + multiStepCheckMatch: ['**/__checks__/multistep/*.spec.js'], + checkDefaults: { + runtimeId: '2023.09', + }, + }) + expect(project.synthesize()).toMatchObject({ + project: { + logicalId: 'glob-project-id', + }, + resources: [ + { type: 'check', logicalId: '__checks__/browser/check2.spec.js' }, + { type: 'check', logicalId: '__checks__/multistep/check1.spec.js' }, + ], + }) + }) }) diff --git a/packages/cli/src/services/checkly-config-loader.ts b/packages/cli/src/services/checkly-config-loader.ts index 070d59e1..563d279c 100644 --- a/packages/cli/src/services/checkly-config-loader.ts +++ b/packages/cli/src/services/checkly-config-loader.ts @@ -50,6 +50,15 @@ export type ChecklyConfig = { */ testMatch?: string | string[], }, + /** + * Multistep checks default configuration properties. + */ + multiStepChecks?: CheckConfigDefaults & { + /** + * Glob pattern where the CLI looks for Playwright test files, i.e. all `.spec.ts` files + */ + testMatch?: string | string[], + }, }, /** * CLI default configuration properties. diff --git a/packages/cli/src/services/project-parser.ts b/packages/cli/src/services/project-parser.ts index 866bb085..6d562da1 100644 --- a/packages/cli/src/services/project-parser.ts +++ b/packages/cli/src/services/project-parser.ts @@ -18,6 +18,7 @@ type ProjectParseOpts = { repoUrl?: string, checkMatch?: string | string[], browserCheckMatch?: string | string[], + multiStepCheckMatch?: string | string[], ignoreDirectoriesMatch?: string[], checkDefaults?: CheckConfigDefaults, browserCheckDefaults?: CheckConfigDefaults, @@ -34,6 +35,7 @@ export async function parseProject (opts: ProjectParseOpts): Promise { directory, checkMatch = '**/*.check.{js,ts}', browserCheckMatch, + multiStepCheckMatch, projectLogicalId, projectName, repoUrl, @@ -60,6 +62,7 @@ export async function parseProject (opts: ProjectParseOpts): Promise { const ignoreDirectories = ['**/node_modules/**', '**/.git/**', ...ignoreDirectoriesMatch] await loadAllCheckFiles(directory, checkMatch, ignoreDirectories) await loadAllBrowserChecks(directory, browserCheckMatch, ignoreDirectories, project) + await loadAllMultiStepChecks(directory, multiStepCheckMatch, ignoreDirectories, project) // private-location must be processed after all checks and groups are loaded. await loadAllPrivateLocationsSlugNames(project) @@ -124,6 +127,38 @@ async function loadAllBrowserChecks ( } } +async function loadAllMultiStepChecks ( + directory: string, + multiStepCheckFilePattern: string | string[] | undefined, + ignorePattern: string[], + project: Project, +): Promise { + if (!multiStepCheckFilePattern) { + return + } + const checkFiles = await findFilesWithPattern(directory, multiStepCheckFilePattern, ignorePattern) + const preexistingCheckFiles = new Set() + Object.values(project.data.check).forEach((check) => { + if ((check instanceof MultiStepCheck || check instanceof BrowserCheck) && check.scriptPath) { + preexistingCheckFiles.add(check.scriptPath) + } + }) + + for (const checkFile of checkFiles) { + const relPath = pathToPosix(path.relative(directory, checkFile)) + // Don't create an additional check if the checkFile was already added to a check in loadAllCheckFiles. + if (preexistingCheckFiles.has(relPath)) { + continue + } + const multistepCheck = new MultiStepCheck(pathToPosix(relPath), { + name: path.basename(checkFile), + code: { + entrypoint: checkFile, + }, + }) + } +} + // TODO: create a function to process slug names for check or check-group to reduce duplicated code. async function loadAllPrivateLocationsSlugNames ( project: Project,